Skip to content

F3: graphdb_query language -- Q1-Q6, snapshot sessions, bounded BFS#25

Merged
david-w-t merged 20 commits into
davidwt-com:mainfrom
david-w-t:develop
May 25, 2026
Merged

F3: graphdb_query language -- Q1-Q6, snapshot sessions, bounded BFS#25
david-w-t merged 20 commits into
davidwt-com:mainfrom
david-w-t:develop

Conversation

@david-w-t
Copy link
Copy Markdown
Contributor

Summary

Implements F3, the graphdb query language, as a new graphdb_query
gen_server peer under graphdb_sup. The graphdb_language slot is
held 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:

  • Q1 #q_get_node{} — raw node by nref
  • Q1b #q_get_arcs{} — arcs filtered by direction + kind
  • Q2 #q_describe{} for kind = attribute — taxonomy + label resolution
  • Q3 #q_describe{} for kind = class — taxonomy + QC inheritance
  • Q4 #q_describe{} for kind = instance — 4-priority inheritance + both-direction arcs
  • Q5 #q_instances_of{} — set query, optionally recursive over subclasses
  • Q6 #q_find_path{} — bounded BFS with {partial, _, Cont} + resume/2

Session shape: #{snapshot_at => Timestamp, cache => #{}}. Every
Mnesia read goes through session_read_node/2 or
session_read_arcs/4 — no direct mnesia:dirty_* calls in the
executor. refresh/1 is the only cache invalidation path. Resuming
a continuation against a refreshed session returns
{error, snapshot_expired}.

Notable implementation choices:

  • Continuations store the original max_depth as remaining_depth
    so resume/2 gets a fresh full budget rather than the exhausted 0.
  • BFS expansion filters kind = category targets as structural
    scaffold, matching the semantics already encoded in
    graphdb_class:ancestors/1's NREF_CLASSES filter.
  • graphdb_instance:resolve_value/2 extended to return
    {ok, Value, Source} where Source :: local | {class, N} | {compositional, N} | {connected, N} — exposes the four-priority
    resolution chain to Q4's resolved_attributes map.
  • New graphdb_class:bind_qc_value/3 lets tests and clients bind a
    class-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

  • New docs/f3-graphdb-query-design.md — durable architectural
    contract (moved from project root in the final commit).
  • ARCHITECTURE.md gains §11 Query Layer; status table updated to
    267 CT + 103 EUnit.
  • Root CLAUDE.md supervision tree picks up graphdb_query; NYI list
    shrinks to graphdb_rules only.
  • apps/graphdb/CLAUDE.md reflects the implemented public API.
  • TASKS.md: F3 marked RESOLVED.

Test plan

  • ./rebar3 compile — zero warnings
  • ./rebar3 ct — 267 cases pass (40 new under graphdb_query_SUITE)
  • ./rebar3 eunit — 103 cases pass
  • Per-task: failing-test step verified before each implementation

🤖 Generated with Claude Code

david-w-t and others added 20 commits May 23, 2026 20:11
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>
@david-w-t david-w-t merged commit af05055 into davidwt-com:main May 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant