Skip to content

fix: Isolate insert values and resolve options consistently across insert paths - #102

Merged
mhiro2 merged 9 commits into
mainfrom
fix/core-insert-consistency
Oct 14, 2026
Merged

mhiro2 merged 9 commits into
mainfrom
fix/core-insert-consistency

Conversation

@mhiro2

@mhiro2 mhiro2 commented Oct 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Every record now owns the values it is built from: batch inserts clone their graph, pointer/slice/map foreign keys get their own copy of the parent key, and cleanup uses each value as it was when its Insert returned
  • Options resolve in one trait expansion pass on every insert path, so WithContext and Seq / SeqRef / SeqUse work inside traits, and Set applies in the order written
  • Use / Ref on a has-many back-reference is rejected instead of silently dropped, and NodeAt / NodesForRoot include has-many children generated under shared parents
  • Docs describe option resolution, value isolation, and batch root lookups

Changes

  • b801f4a : fix: isolate batch insert values from the caller and other records
    • InsertMany executes a clone of the planned graph, like Plan.InsertE, so an Insert that mutates a Set slice, map, or pointer no longer changes the caller's value or other records
  • 8715379 : fix: give pointer foreign keys their own storage and snapshot cleanup values on insert
    • Binding a key into a pointer FK (including **T) copies the referenced storage, so a child Insert writing through its FK cannot rewrite the parent's key
    • The executor captures each node's cleanup value right after its Insert returns, so later inserts cannot change which row Delete receives
  • 6fd5e4b : fix: resolve WithContext after trait expansion on every insert path
    • A WithContext inside InlineTrait / BlueprintTrait is honored, and Plan.InsertE lets a build-time WithContext override its ctx argument
  • 6620d6a : fix: resolve sequence options while expanding traits
    • Seq, SeqRef, and SeqUse are resolved per record during trait expansion, so they work in blueprint traits and in traits returned by SeqRef
  • 983cd9c : fix(planner): apply Set options in the order they were written
    • Sets are kept as an ordered list, so setting an embedded struct and a promoted field is deterministic and the later Set wins
  • 07bf96f : fix(planner): reject Use and Ref on a has-many back-reference
    • The structural binding to the creating parent is kept, and a conflicting Use / Ref on that relation returns ErrInvalidOption
  • e6af3b3 : fix: include has-many children of shared parents in root lookups
    • Root lookups also follow dependents planned under each selected node, so a shared parent's generated subtree is included without mixing in other roots
  • 8a61aa5 : docs: describe option resolution, value isolation, and batch root lookups
    • Updates the guide, ARCHITECTURE.md, and the test setup skill
  • ea4a142 : fix: let WithContext(nil) override the caller context
    • A nil WithContext runs with context.Background() instead of falling back to the caller's ctx

InsertMany executed the planned graph directly, so every record of a
batch received the same slice, map, or pointer passed through Set, and
an Insert callback that mutated it changed the caller's value and the
records inserted before it. The batch path now executes a clone of the
graph, matching single inserts, so each node owns its values.
… values on insert

Binding a pointer primary key to a pointer foreign key copied the
pointer itself, so a child Insert that wrote through its FK also
rewrote the parent's key in the returned result and in the value
Cleanup passed to Delete. Pointer destinations now always receive
freshly allocated storage. The executor also captures each node's
cleanup value as soon as its Insert returns instead of after the whole
graph ran, so later inserts cannot change which rows cleanup removes.
The insert entry points pulled WithContext out of the top-level
options before traits were expanded, so a WithContext inside an
InlineTrait or BlueprintTrait was ignored and a canceled context still
inserted records. The context is now taken from the expanded root
options, and Plan.InsertE honors a WithContext given at build time over
its ctx argument, so single, batch, and plan inserts share one rule.
InsertMany resolved Seq, SeqRef, and SeqUse before traits were
expanded, so a sequence defined in a blueprint trait reached validation
unresolved and failed with "only supported by InsertMany", and a trait
returned by SeqRef was left for a later pass. Trait expansion now
resolves sequence options for the current record in the same pass, so
they behave the same whether written directly, in an inline trait, in
a blueprint trait, or under Ref.
Set options were collected into a map and applied in map iteration
order, so setting an embedded struct and a field promoted from it in
the same call produced either value at random. Sets are now kept as an
ordered list and applied in the order written, so the later Set wins
the same way later options override earlier ones elsewhere.
A child generated through has-many binds its belongs-to relation back
to the parent that created it, and the planner returned as soon as that
binding succeeded. A Use or Ref given for the same relation, such as
Ref("employees", Use("department", d)), passed validation and was then
silently dropped. The structural binding is kept and such an option is
now reported as ErrInvalidOption, since honoring it would mean
replacing or rebuilding the parent that already exists.
NodeAt and NodesForRoot followed only the dependencies of a root's own
nodes, so when a batch shared a belongs-to parent, the has-many
children generated under that parent belonged to no root and both
lookups returned nothing for them. The lookup now also follows the
dependents planned under each selected node, identified by their node
ID, so a shared parent brings its generated subtree while nodes of
other roots that merely reference it stay out.
…kups

Explain that options resolve in one trait expansion pass on every
insert path, that Set applies in written order, that each record owns
the values it is built from, that Use and Ref on a has-many
back-reference are rejected, and that root lookups include records
generated under shared parents.
With the context now read from the expanded options, a nil
WithContext looked the same as no WithContext and the caller's ctx was
used instead. A nil WithContext again runs with context.Background(),
so it overrides the caller's ctx like any other WithContext.
@github-actions

Copy link
Copy Markdown

Code Metrics Report

main (9a2b1f6) #102 (6d0f4d6) +/-
Coverage 86.9% 86.9% +0.0%
Code to Test Ratio 1:1.0 1:1.0 +0.0
Test Execution Time 1m11s 55s -16s
Details
  |                     | main (9a2b1f6) | #102 (6d0f4d6) |  +/-  |
  |---------------------|----------------|----------------|-------|
+ | Coverage            |          86.9% |          86.9% | +0.0% |
  |   Files             |             59 |             59 |     0 |
  |   Lines             |           4791 |           4784 |    -7 |
- |   Covered           |           4165 |           4160 |    -5 |
+ | Code to Test Ratio  |          1:1.0 |          1:1.0 |  +0.0 |
  |   Code              |          17787 |          17800 |   +13 |
+ |   Test              |          17916 |          18365 |  +449 |
+ | Test Execution Time |          1m11s |            55s |  -16s |

Code coverage of files in pull request scope (82.5% → 82.6%, patch 86.6%)

Files Coverage +/- Patch Coverage Status
insert.go 86.2% -1.7% 100.0% modified
internal/errx/errors.go 76.6% -2.7% 0.0% modified
internal/executor/executor.go 86.5% +0.1% 100.0% modified
internal/field/lookup.go 56.2% +1.6% 100.0% modified
internal/planner/apply_opts.go 77.4% +0.7% 83.3% modified
internal/planner/expand_relations.go 83.1% -0.4% 73.6% modified
internal/planner/validate.go 84.3% 0.0% 66.6% modified
option.go 89.7% +0.2% 100.0% modified
plan.go 84.9% +0.2% 86.2% modified
result.go 88.6% +1.5% 100.0% modified

Reported by octocov

@mhiro2 mhiro2 self-assigned this Oct 14, 2026
@mhiro2 mhiro2 added the bug Something isn't working label Oct 14, 2026
@mhiro2
mhiro2 merged commit 69d7a67 into main Oct 14, 2026
21 of 22 checks passed
@mhiro2
mhiro2 deleted the fix/core-insert-consistency branch October 14, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant