F3: graphdb_query language -- Q1-Q6, snapshot sessions, bounded BFS#25
Merged
Conversation
Architectural contract for the query layer — seven independent
building blocks (Q1 get_node, Q1b get_arcs, Q2-Q4 describes,
Q5 list_instances_of, Q6 find_path) chosen to span the dimensions
of the eventual surface.
Pinned now (forward-compatible API shape):
- AST as #q_*{} records
- result maps with explicit keys; opt-in labels sub-map
- session() + continuation() opaque types
- read-through cache via session_read_node/2 + session_read_arcs/4
- {ok, _} / {partial, _, Cont} / {error, _} return-shape variants
Deferred (implementation, not contract):
- text DSL surface syntax
- planner/optimizer
- continuation realization beyond Q6
- session as gen_server process
Review still in progress — sections through 5.4 (Q4) reviewed;
5.5 onward pending.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add §8.4 Freshness Model — sessions are snapshots; refresh/1 is the
sole invalidation path; event-driven invalidation deferred. Cleanly
separates internal cache (governed by snapshot) from returned results
(caller-owned, immutable). resume/2 now returns {error, snapshot_expired}
when continuation outlives its snapshot.
Renumber 8.5–8.7. Pinned decision davidwt-com#10 added. Out-of-scope §10 refined
to scope only event-driven invalidation as deferred.
The graphdb_language slot is occupied by M6's multilingual overlay layer (label resolution, language registration, translation hooks). The query language is moved to a new module graphdb_query so the two unrelated concerns don't share a gen_server. - Rename design doc: f3-graphdb-language-design.md -> f3-graphdb-query-design.md - Update title and section §2/§9 references inside the doc - Add a module-name note at the top explaining the rename - Fix apps/graphdb/CLAUDE.md: graphdb_language now described as the M6 multilingual layer; graphdb_query added as F3 planned module
Eleven-task plan executing the F3 design doc as a walking skeleton:
Task 0 Extend graphdb_instance:resolve_value/2 to return Source;
add graphdb_class:bind_qc_value/3 (prep work Q4 depends on)
Task 1 AST header (graphdb_query.hrl)
Task 2 Module skeleton + session API + sup wiring + smoke tests
Task 3 Q1 get_node (the walking skeleton)
Task 4 Q1b get_arcs (relationships table via index reads)
Task 5 Q2 describe_attribute (label resolution + taxonomy)
Task 6 Q3 describe_class (taxonomy + flat QC list)
Task 7 Q4 describe_instance (4-priority inheritance + both-direction arcs)
Task 8 Q5 list_instances_of (recursive set query)
Task 9 Q6 find_path + resume + snapshot_expired
Task 10 Documentation closeout
Each task ends in a commit with TDD-style steps (failing test, impl,
passing test). Test setup uses ?NREF_CLASSES / ?NREF_PROJECTS for
top-level parents matching existing CT convention. graphdb_class and
graphdb_instance API returns of [#node{}] are projected to nrefs at
call sites. Q3's own/inherited QC split is deferred (flat list for v1).
Prep work for the graphdb_query language (F3 plan). Two changes:
- graphdb_instance:resolve_value/2 now returns {ok, Value, Source}
instead of {ok, Value}. Source identifies the priority level that
held the AVP: local | {class, N} | {compositional, N} |
{connected, N}. Threaded through resolve_from_class,
resolve_from_ancestors, and resolve_from_connected/search_targets.
- graphdb_class:bind_qc_value/3 binds a value on a declared
qualifying characteristic. Aborts with qc_not_declared when the
QC has not yet been added. This is the Priority-2 input for the
inheritance chain, enabling end-to-end exercising of all four
priority levels.
Existing tests adapted: ?assertEqual({ok, V}, ...) -> ?assertMatch(
{ok, V, _}, ...) for non-source-asserting cases. Four new source-
asserting cases in graphdb_instance_SUITE and three bind_qc_value
cases in graphdb_class_SUITE. Errors (ambiguous_class_value) are
unchanged -- only successful resolutions carry a Source.
Test counts: CT 217 -> 224, EUnit 103 unchanged. All green.
… query Polish from code review (commit 1de3ba6). Two minor changes: - attach_session/2 comment now describes the dual reply-shape contract (2-tuple append vs 3-tuple replace) rather than the vague "add to tail". - unimplemented_query_returns_error/1 now uses an obviously-bogus query shape {unknown_query_shape, foo} instead of #q_instances_of{} which will become a real query in Task 8. Keeps the catch-all test durable across the rest of F3.
Implements the smallest query end-to-end so every layer of the
pipeline (parse -> dispatch -> executor -> reply) exists:
- dispatch/2 gains a #q_get_node{} clause routing to a new
read-through helper.
- session_read_node/2 reads from the session cache on hit,
falls through to mnesia:dirty_read(nodes, Nref) on miss,
and stores either the #node{} or the literal atom not_found
sentinel keyed at {node, Nref} so subsequent reads short-circuit.
- node_to_map/1 projects #node{} into the public result map shape.
- Drop 'node' from nowarn_unused_record now that the record is
pattern-matched in node_to_map/1 (relationship still untouched).
Suite: +5 cases under new q1_get_node CT group covering bootstrap
node, attribute node parents cache, not_found error, /2 session
form snapshot preservation, and cache population.
Test counts: graphdb_query_SUITE 10/10, full CT 234/234, EUnit 103/103.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements Q1b (#q_get_arcs{}) by reading the relationships Mnesia
table through its secondary indexes on source_nref / target_nref.
- New dispatch clause for #q_get_arcs{}, returning {ok, [map()]}.
- session_read_arcs/4 read-through cache helper keyed by
{arcs, Nref, Direction, KindFilter}.
- read_arcs/3 multiplexes outgoing/incoming/both over the two
index reads; the two index reads are disjoint by construction.
- filter_kinds/2 narrows by relationship.kind.
- arc_to_map/1 projects #relationship{} to the public map shape.
- nowarn_unused_record directive dropped -- the record is now used.
Test invariant note: bootstrap labels the Attributes-subtree child
arcs with ?ARC_ATTR_CHILD (24, kind=taxonomy) per PR davidwt-com#15, NOT
?ARC_CAT_CHILD (22, kind=composition). Test cases assert against
?ARC_ATTR_CHILD to match the actual stored shape.
6 new CT cases (q1b_get_arcs group): outgoing-all, incoming-all,
both-equals-sum, kind-filter, unknown-nref, cache-key-shape.
Tests: 241 CT + 103 EUnit, all green.
Mirrors the Q1 cache-hit invariant test (q1_cache_hit_skips_mnesia) for
the new {arcs, N, Dir, Kinds} cache key shape introduced in Task 4.
Stops Mnesia between two reads and asserts the second returns from
cache. Locks the contract that a hit truly skips storage access.
Implements #q_describe{} dispatch for kind=attribute nodes.
Composes Q1 (session_read_node) + Q1b (session_read_arcs) with
M6 label resolution via graphdb_language:resolve_label/4.
Returns a map with nref, kind, attribute_type, parent, children,
avps, labels. Non-attribute kinds return {error, {unsupported_kind,
Kind}} -- Q3/Q4 will add class/instance clauses.
5 new CT cases under q2_describe_attribute group.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends #q_describe{} dispatch for kind=class. Returns nref, kind,
superclasses (from parents cache), ancestors (multi-parent DAG walk
via graphdb_class:ancestors/1), subclasses, qualifying_characteristics
(flat [{AttrNref, Value}] list from inherited_qcs/1 -- own/inherited
split deferred), avps, labels.
4 new CT cases under q3_describe_class group.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extends #q_describe{} dispatch for kind=instance. Returns nref, kind,
classes, class_ancestors (direct classes + transitive ancestors),
compositional_parent, compositional_ancestors, resolved_attributes
(4-priority inheritance via graphdb_instance:resolve_value/2's
{ok, Value, Source} return from Task 0), outgoing_connections,
incoming_connections (separate maps per direction since
characterization, AVPs, and row IDs differ), avps, labels.
Implementation note: class_ancestors includes the direct classes
themselves so callers can ask one list "what is this instance".
Test note: create_relationship_attribute/3 creates BOTH directions
atomically and returns {ok, {FwdNref, RevNref}} -- a single pair-call,
not two single-direction calls.
5 new CT cases under q4_describe_instance group.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds #q_instances_of{} dispatch. For recursive=false, returns the
class's direct instantiation arcs (characterization=30 from the class
node). For recursive=true, transitively walks subclasses via
all_subclasses/1 (since graphdb_class:subclasses/1 is direct-only)
and unions all members.
4 new CT cases under q5_list_instances_of group.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements bounded-BFS Q6 (#q_find_path{}), the resume/2 gen_server
clause, and snapshot mismatch detection. The walking skeleton is now
complete: Q1-Q6 all wired.
Design notes:
- Cont stores the original max_depth as remaining_depth so resume
gets a fresh full allotment, not the exhausted 0.
- expand_arcs filters target nodes of kind=category as scaffold,
matching the semantics already encoded in
graphdb_class:ancestors/1's NREF_CLASSES filter. Without it, two
classes sharing only NREF_CLASSES as a parent would be considered
taxonomically connected.
- snapshot_expired detected by record-pattern guard on the resume
call: cont.snapshot_at must match session.snapshot_at.
7 new CT cases under q6_find_path group (6 Q6 + 1 resume guard).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ARCHITECTURE.md: new §11 Query Layer (snapshot semantics, session cache, continuation BFS, scaffold filter); status table updated to 267 CT + 103 EUnit. CLAUDE.md (root): supervision tree adds graphdb_query as implemented gen_server; NYI list shrinks to graphdb_rules only; worker table re-categorises graphdb_language as M6 overlay and graphdb_query as the F3 query layer. apps/graphdb/CLAUDE.md: graphdb_query.erl marked implemented; "planned" worker subsection replaced with public API + design pointer; NYI block updated. TASKS.md: F3 marked RESOLVED with pointer to design doc + plan. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Durable architectural reference (snapshot semantics, Q1-Q6 contract, decision log) belongs alongside other architectural docs, not at repo root. Update active references (ARCHITECTURE.md, TASKS.md, apps/graphdb/CLAUDE.md, graphdb_query.hrl, graphdb_query.erl); leave the frozen implementation plan in docs/superpowers/plans/ alone. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements F3, the graphdb query language, as a new
graphdb_querygen_server peer under
graphdb_sup. Thegraphdb_languageslot isheld by the M6 multilingual overlay (PR #20), so the query language
gets its own module.
Six query primitives, snapshot-semantics sessions, and bounded BFS
with continuation/resume:
#q_get_node{}— raw node by nref#q_get_arcs{}— arcs filtered by direction + kind#q_describe{}forkind = attribute— taxonomy + label resolution#q_describe{}forkind = class— taxonomy + QC inheritance#q_describe{}forkind = instance— 4-priority inheritance + both-direction arcs#q_instances_of{}— set query, optionally recursive over subclasses#q_find_path{}— bounded BFS with{partial, _, Cont}+resume/2Session shape:
#{snapshot_at => Timestamp, cache => #{}}. EveryMnesia read goes through
session_read_node/2orsession_read_arcs/4— no directmnesia:dirty_*calls in theexecutor.
refresh/1is the only cache invalidation path. Resuminga continuation against a refreshed session returns
{error, snapshot_expired}.Notable implementation choices:
max_depthasremaining_depthso
resume/2gets a fresh full budget rather than the exhausted 0.kind = categorytargets as structuralscaffold, matching the semantics already encoded in
graphdb_class:ancestors/1's NREF_CLASSES filter.graphdb_instance:resolve_value/2extended to return{ok, Value, Source}whereSource :: local | {class, N} | {compositional, N} | {connected, N}— exposes the four-priorityresolution chain to Q4's
resolved_attributesmap.graphdb_class:bind_qc_value/3lets tests and clients bind aclass-level value to a QC for Q4 inheritance verification.
Process
Followed the subagent-driven-development workflow against the plan at
docs/superpowers/plans/2026-05-23-f3-graphdb-query.md. Eleven tasks(0-10), each landed as one or two commits; each post-Task-2 task
followed TDD (failing test → implementation → green).
Documentation
docs/f3-graphdb-query-design.md— durable architecturalcontract (moved from project root in the final commit).
ARCHITECTURE.mdgains §11 Query Layer; status table updated to267 CT + 103 EUnit.
CLAUDE.mdsupervision tree picks upgraphdb_query; NYI listshrinks to
graphdb_rulesonly.apps/graphdb/CLAUDE.mdreflects the implemented public API.TASKS.md: F3 marked RESOLVED.Test plan
./rebar3 compile— zero warnings./rebar3 ct— 267 cases pass (40 new undergraphdb_query_SUITE)./rebar3 eunit— 103 cases pass🤖 Generated with Claude Code