feat(typed): generic, type-safe client and query builder#17
Conversation
Add a generic typed layer over modusgraph.Client: typed.Client[T] with CRUD and iterators; a fluent Query[T] builder (filters, ordering, paging, edge traversal, IterNodes); MultiQuery for N homogeneous blocks in one round-trip; functional options; a filter DSL (typed/filter); and ordered result merging (typed/search). A small no-op-by-default Tracer seam (typed.SetTracer) lets a host plug in tracing without the typed package depending on any telemetry library. Self-contained: builds and tests against the current client with no other changes.
There was a problem hiding this comment.
5 issues found across 16 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="typed/filter/filter_test.go">
<violation number="1" location="typed/filter/filter_test.go:115">
P2: `TestBuilder_PositionalParamsAreSequential` uses weak substring checks that can produce false positives and fail to catch broken placeholder sequencing.</violation>
</file>
<file name="typed/multi_query.go">
<violation number="1" location="typed/multi_query.go:85">
P1: In-place mutation of shared Query[T] pointers during Execute can corrupt multi-block composition when the same query instance is added under multiple names or reused elsewhere.</violation>
</file>
<file name="typed/query.go">
<violation number="1" location="typed/query.go:104">
P1: AND-combining filter fragments without per-fragment parentheses changes logical precedence and can return incorrect query results.</violation>
<violation number="2" location="typed/query.go:486">
P1: WhereEdge pre-pass silently drops user-provided RootFunc/UID root constraints by unconditionally overwriting qb.q.RootFunc() with pre-pass UIDs, while the pre-pass itself starts from a fresh query that ignores the user's existing root narrowing.</violation>
</file>
<file name="typed/query_test.go">
<violation number="1" location="typed/query_test.go:795">
P2: Test uses absolute query count instead of delta-based assertion for laziness verification</violation>
</file>
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
| } | ||
| // Name the underlying dgman query so blocks do not collide on the | ||
| // default "data" name and so the response JSON keys are predictable. | ||
| block.q.Name(name) |
There was a problem hiding this comment.
P1: In-place mutation of shared Query[T] pointers during Execute can corrupt multi-block composition when the same query instance is added under multiple names or reused elsewhere.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typed/multi_query.go, line 85:
<comment>In-place mutation of shared Query[T] pointers during Execute can corrupt multi-block composition when the same query instance is added under multiple names or reused elsewhere.</comment>
<file context>
@@ -0,0 +1,191 @@
+ }
+ // Name the underlying dgman query so blocks do not collide on the
+ // default "data" name and so the response JSON keys are predictable.
+ block.q.Name(name)
+ rawBlocks = append(rawBlocks, block.q)
+ }
</file context>
| if len(uids) == 0 { | ||
| return false, nil | ||
| } | ||
| qb.q.RootFunc("uid(" + strings.Join(uids, ", ") + ")") |
There was a problem hiding this comment.
P1: WhereEdge pre-pass silently drops user-provided RootFunc/UID root constraints by unconditionally overwriting qb.q.RootFunc() with pre-pass UIDs, while the pre-pass itself starts from a fresh query that ignores the user's existing root narrowing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typed/query.go, line 486:
<comment>WhereEdge pre-pass silently drops user-provided RootFunc/UID root constraints by unconditionally overwriting qb.q.RootFunc() with pre-pass UIDs, while the pre-pass itself starts from a fresh query that ignores the user's existing root narrowing.</comment>
<file context>
@@ -0,0 +1,565 @@
+ if len(uids) == 0 {
+ return false, nil
+ }
+ qb.q.RootFunc("uid(" + strings.Join(uids, ", ") + ")")
+ return true, nil
+}
</file context>
| if f.expr == "" { | ||
| continue | ||
| } | ||
| parts = append(parts, shiftPlaceholders(f.expr, len(params))) |
There was a problem hiding this comment.
P1: AND-combining filter fragments without per-fragment parentheses changes logical precedence and can return incorrect query results.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typed/query.go, line 104:
<comment>AND-combining filter fragments without per-fragment parentheses changes logical precedence and can return incorrect query results.</comment>
<file context>
@@ -0,0 +1,565 @@
+ if f.expr == "" {
+ continue
+ }
+ parts = append(parts, shiftPlaceholders(f.expr, len(params)))
+ params = append(params, f.params...)
+ }
</file context>
| parts = append(parts, shiftPlaceholders(f.expr, len(params))) | |
| parts = append(parts, "("+shiftPlaceholders(f.expr, len(params))+")") |
| b.EqGroupUUID("id", []filter.UUID{{Value: "a"}, {Value: "b"}}) | ||
| b.EqGroupString("name", []filter.String{{Value: "c"}}) | ||
| expr, _ := b.Build() | ||
| if !strings.Contains(expr, "$1") || !strings.Contains(expr, "$2") || !strings.Contains(expr, "$3") { |
There was a problem hiding this comment.
P2: TestBuilder_PositionalParamsAreSequential uses weak substring checks that can produce false positives and fail to catch broken placeholder sequencing.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typed/filter/filter_test.go, line 115:
<comment>`TestBuilder_PositionalParamsAreSequential` uses weak substring checks that can produce false positives and fail to catch broken placeholder sequencing.</comment>
<file context>
@@ -0,0 +1,118 @@
+ b.EqGroupUUID("id", []filter.UUID{{Value: "a"}, {Value: "b"}})
+ b.EqGroupString("name", []filter.String{{Value: "c"}})
+ expr, _ := b.Build()
+ if !strings.Contains(expr, "$1") || !strings.Contains(expr, "$2") || !strings.Contains(expr, "$3") {
+ t.Errorf("expected $1, $2, $3 in expr; got %q", expr)
+ }
</file context>
| } | ||
| } | ||
| // Obtaining the iterator runs no query — IterNodes is lazy. | ||
| seq := c.Query(ctx).IterNodes() |
There was a problem hiding this comment.
P2: Test uses absolute query count instead of delta-based assertion for laziness verification
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At typed/query_test.go, line 795:
<comment>Test uses absolute query count instead of delta-based assertion for laziness verification</comment>
<file context>
@@ -0,0 +1,1294 @@
+ }
+ }
+ // Obtaining the iterator runs no query — IterNodes is lazy.
+ seq := c.Query(ctx).IterNodes()
+ if queriesExecuted != 0 {
+ t.Fatalf("building the IterNodes iterator executed %d queries, want 0", queriesExecuted)
</file context>
Adds a generic
typedlayer overmodusgraph.Client:typed.Client[T]— CRUD + iterators over a schema struct T.typed.Query[T]— fluent builder: filters, ordering, paging, edge traversal (WhereEdge),IterNodesauto-paging iterator.MultiQuery— N homogeneous-type blocks in one round-trip.typed/filter— a small filter DSL (incl. fulltext);typed/search— ordered merge/dedup of result sets.Tracerseam (typed.SetTracer) so hosts can plug in tracing without the typed package depending on any telemetry library.Self-contained: the diff is entirely under
typed/— it builds andgo tests green against the current client with no changes to the root package.Part of a series upstreaming work from a downstream fork; opened for review/discussion.
Summary by cubic
Adds a generic
typedlayer overmodusgraph.Clientfor type-safe CRUD and fluent queries, plus batched multi-block queries, small filter/search helpers, and an optional tracing seam. All code is new undertyped/and builds green against the current client.New Features
typed.Client[T]: type-safe CRUD andIterwith auto-paging.typed.Query[T]: fluent filters and OR groups, order/limit/offset/after,WhereEdge(edge-target filters via pre-pass),Nodes/First/IterNodes(consistent snapshot),NodesAndCount,UID,All, raw access,As/Var/GroupBy,Vars,Name.typed.MultiQuery[T]: batch N same-type blocks in one round-trip; remaps predicate→JSON tags; rejectsWhereEdge.typed/filter: small DSL (EqGroup*,RequiredEq,AnyOfText/AllOfText) for building@filterclauses.typed/search:MergeByIDto merge and dedup ordered result sets.typed.Optionandtyped.Apply.typed.SetTracerinstalls a process-wide tracer; default is no-op.Migration
typed/.typed.SetTracerto emit spans.Written for commit e9028a1. Summary will update on new commits.