graphddb 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdc/index.d.ts +2 -2
- package/dist/{chunk-GFGVDF4W.js → chunk-C5Q2NMRW.js} +581 -22
- package/dist/cli.js +186 -11
- package/dist/{from-change-pnURY-cV.d.ts → from-change-CWiXBcgi.d.ts} +1 -1
- package/dist/{index-Eg94ChE1.d.ts → index-CvS9ATKB.d.ts} +289 -104
- package/dist/index.d.ts +85 -7
- package/dist/index.js +259 -1
- package/dist/linter/index.d.ts +4 -4
- package/dist/{maintenance-view-adapter-BP2CJDdz.d.ts → maintenance-view-adapter-NBTZbE8-.d.ts} +57 -4
- package/dist/{registry-Cv9nl_3i.d.ts → registry-BndKbZbg.d.ts} +1 -1
- package/dist/{relation-depth-BR0y7Q1i.d.ts → relation-depth-CCGHLTOv.d.ts} +1 -1
- package/dist/spec/index.d.ts +3 -3
- package/dist/spec/index.js +13 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/transform/index.d.ts +51 -2
- package/dist/transform/index.js +264 -1
- package/docs/cqrs-contract.md +1 -1
- package/docs/design-patterns.md +9 -7
- package/docs/prepared-statements.md +70 -3
- package/docs/python-bridge.md +24 -4
- package/package.json +1 -1
package/dist/transform/index.js
CHANGED
|
@@ -712,8 +712,271 @@ function transformPreparedSource(fileName, sourceText, options = {}) {
|
|
|
712
712
|
function lintPreparedSource(fileName, sourceText) {
|
|
713
713
|
return transformPreparedSource(fileName, sourceText, { mode: "lint" }).diagnostics;
|
|
714
714
|
}
|
|
715
|
+
var AOT_LOADER_LOCAL = "__gddbLoadPreparedPlan";
|
|
716
|
+
var AOT_PLANS_LOCAL = "__gddbPreparedPlans";
|
|
717
|
+
var AOT_SHIM_EXPORT = "__gddbAotBodies";
|
|
718
|
+
function referencedIdentifierNames(node, out) {
|
|
719
|
+
const visit = (n) => {
|
|
720
|
+
if (ts.isIdentifier(n)) {
|
|
721
|
+
const parent = n.parent;
|
|
722
|
+
if (parent !== void 0) {
|
|
723
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === n) return;
|
|
724
|
+
if (ts.isQualifiedName(parent) && parent.right === n) return;
|
|
725
|
+
if ((ts.isClassDeclaration(parent) || ts.isFunctionDeclaration(parent) || ts.isEnumDeclaration(parent) || ts.isVariableDeclaration(parent) || ts.isParameter(parent) || ts.isMethodDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent) || ts.isBindingElement(parent)) && parent.name === n) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (ts.isPropertyAssignment(parent) && parent.name === n) return;
|
|
729
|
+
if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent)) return;
|
|
730
|
+
}
|
|
731
|
+
out.add(n.text);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
ts.forEachChild(n, visit);
|
|
735
|
+
};
|
|
736
|
+
visit(node);
|
|
737
|
+
}
|
|
738
|
+
function collectShimDeps(roots, bindings) {
|
|
739
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
740
|
+
for (const root of roots) referencedIdentifierNames(root, referenced);
|
|
741
|
+
const deps = /* @__PURE__ */ new Set();
|
|
742
|
+
const queue = [...referenced];
|
|
743
|
+
while (queue.length > 0) {
|
|
744
|
+
const name = queue.pop();
|
|
745
|
+
if (deps.has(name)) continue;
|
|
746
|
+
const binding = bindings.get(name);
|
|
747
|
+
if (binding === void 0) continue;
|
|
748
|
+
deps.add(name);
|
|
749
|
+
const nested = /* @__PURE__ */ new Set();
|
|
750
|
+
if (binding.kind === "const" && binding.initializer !== void 0) {
|
|
751
|
+
referencedIdentifierNames(binding.initializer, nested);
|
|
752
|
+
} else if (binding.kind === "class" || binding.kind === "function") {
|
|
753
|
+
referencedIdentifierNames(binding.node, nested);
|
|
754
|
+
}
|
|
755
|
+
for (const n of nested) {
|
|
756
|
+
if (!deps.has(n)) queue.push(n);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return deps;
|
|
760
|
+
}
|
|
761
|
+
function statementBoundNames(stmt) {
|
|
762
|
+
const names = [];
|
|
763
|
+
if (ts.isImportDeclaration(stmt) && stmt.importClause) {
|
|
764
|
+
const clause = stmt.importClause;
|
|
765
|
+
if (clause.name) names.push(clause.name.text);
|
|
766
|
+
const b = clause.namedBindings;
|
|
767
|
+
if (b) {
|
|
768
|
+
if (ts.isNamespaceImport(b)) names.push(b.name.text);
|
|
769
|
+
else for (const spec of b.elements) names.push(spec.name.text);
|
|
770
|
+
}
|
|
771
|
+
} else if (ts.isVariableStatement(stmt)) {
|
|
772
|
+
for (const d of stmt.declarationList.declarations) names.push(...bindingNames(d.name));
|
|
773
|
+
} else if ((ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt) || ts.isEnumDeclaration(stmt)) && stmt.name !== void 0) {
|
|
774
|
+
names.push(stmt.name.text);
|
|
775
|
+
}
|
|
776
|
+
return names;
|
|
777
|
+
}
|
|
778
|
+
function transformPreparedSourceAot(fileName, sourceText, options) {
|
|
779
|
+
const scriptKind = fileName.endsWith(".tsx") ? ts.ScriptKind.TSX : fileName.endsWith(".jsx") ? ts.ScriptKind.JSX : ts.ScriptKind.TS;
|
|
780
|
+
const sf = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, scriptKind);
|
|
781
|
+
const bindings = collectModuleBindings(sf);
|
|
782
|
+
const ctx = { sf, fileName, bindings, diagnostics: [], visitedConsts: /* @__PURE__ */ new Set() };
|
|
783
|
+
const detected = [];
|
|
784
|
+
const visit = (node) => {
|
|
785
|
+
if (ts.isCallExpression(node)) {
|
|
786
|
+
if (ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "prepare" && isDDBModelRef(node.expression.expression, bindings)) {
|
|
787
|
+
const arg = node.arguments.length >= 1 ? node.arguments[0] : void 0;
|
|
788
|
+
const resolved = arg !== void 0 ? resolveBodyFn(arg, bindings) : void 0;
|
|
789
|
+
if (arg !== void 0) {
|
|
790
|
+
detected.push({ call: node, form: "prepare", bodyArg: arg, bodyFn: resolved });
|
|
791
|
+
} else {
|
|
792
|
+
report(ctx, node, "unverifiable-body", "A `prepare` call needs a body argument.");
|
|
793
|
+
}
|
|
794
|
+
} else if (ts.isIdentifier(node.expression) && node.expression.text === AOT_LOADER_LOCAL) {
|
|
795
|
+
if (node.arguments.length !== 3 || !ts.isStringLiteral(node.arguments[1])) {
|
|
796
|
+
report(
|
|
797
|
+
ctx,
|
|
798
|
+
node,
|
|
799
|
+
"unverifiable-body",
|
|
800
|
+
`A '${AOT_LOADER_LOCAL}' call must have the generated shape (document, '<plan id>', body). Regenerate with --aot --write.`
|
|
801
|
+
);
|
|
802
|
+
} else {
|
|
803
|
+
const arg = node.arguments[2];
|
|
804
|
+
detected.push({
|
|
805
|
+
call: node,
|
|
806
|
+
form: "aot",
|
|
807
|
+
bodyArg: arg,
|
|
808
|
+
bodyFn: resolveBodyFn(arg, bindings),
|
|
809
|
+
idLiteral: node.arguments[1]
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
ts.forEachChild(node, visit);
|
|
815
|
+
};
|
|
816
|
+
visit(sf);
|
|
817
|
+
const edits = [];
|
|
818
|
+
const slotNames = [];
|
|
819
|
+
const sites = [];
|
|
820
|
+
const verifiedBodies = [];
|
|
821
|
+
let slotIndex = 0;
|
|
822
|
+
for (const m of sourceText.matchAll(/__gddbPrepared(\d+)\b/g)) {
|
|
823
|
+
slotIndex = Math.max(slotIndex, Number(m[1]));
|
|
824
|
+
}
|
|
825
|
+
let needsHeaderImports = false;
|
|
826
|
+
let index = 0;
|
|
827
|
+
for (const site of detected) {
|
|
828
|
+
index += 1;
|
|
829
|
+
const planId = `${options.planIdPrefix}#${index}`;
|
|
830
|
+
if (site.bodyFn === void 0) {
|
|
831
|
+
report(
|
|
832
|
+
ctx,
|
|
833
|
+
site.call,
|
|
834
|
+
"unverifiable-body",
|
|
835
|
+
"The prepared body is not a statically visible function (an inline arrow/function or a module-scope `const` body) \u2014 it cannot be verified or compiled at build time."
|
|
836
|
+
);
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const bodyErrors = lintPreparedBody(ctx, site.bodyFn);
|
|
840
|
+
if (bodyErrors > 0) continue;
|
|
841
|
+
sites.push({ index, planId, form: site.form });
|
|
842
|
+
verifiedBodies.push({ index, bodyArg: site.bodyArg, bodyFn: site.bodyFn });
|
|
843
|
+
if (options.mode === "check") {
|
|
844
|
+
if (site.form === "prepare") {
|
|
845
|
+
report(
|
|
846
|
+
ctx,
|
|
847
|
+
site.call,
|
|
848
|
+
"aot-pending",
|
|
849
|
+
`Call site not yet rewritten to the static prepared plan '${planId}' \u2014 run \`graphddb transform prepared --aot <artifact> --write\`.`,
|
|
850
|
+
"note"
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (site.form === "prepare") {
|
|
856
|
+
needsHeaderImports = true;
|
|
857
|
+
edits.push({
|
|
858
|
+
pos: site.call.getStart(sf),
|
|
859
|
+
end: site.bodyArg.getStart(sf),
|
|
860
|
+
insert: `${AOT_LOADER_LOCAL}(${AOT_PLANS_LOCAL}, ${JSON.stringify(planId)}, `
|
|
861
|
+
});
|
|
862
|
+
if (isPerCallSite(site.call) && !isAlreadyHoisted(site.call)) {
|
|
863
|
+
slotIndex += 1;
|
|
864
|
+
const slot = `${HOIST_SLOT_PREFIX}${slotIndex}`;
|
|
865
|
+
slotNames.push(slot);
|
|
866
|
+
edits.push({ pos: site.call.getStart(sf), end: site.call.getStart(sf), insert: `(${slot} ??= ` });
|
|
867
|
+
edits.push({ pos: site.call.getEnd(), end: site.call.getEnd(), insert: `)` });
|
|
868
|
+
}
|
|
869
|
+
} else if (site.idLiteral !== void 0 && site.idLiteral.text !== planId) {
|
|
870
|
+
edits.push({
|
|
871
|
+
pos: site.idLiteral.getStart(sf),
|
|
872
|
+
end: site.idLiteral.getEnd(),
|
|
873
|
+
insert: JSON.stringify(planId)
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
const errorCount = countErrors(ctx.diagnostics);
|
|
878
|
+
if (errorCount > 0) {
|
|
879
|
+
return {
|
|
880
|
+
fileName,
|
|
881
|
+
text: sourceText,
|
|
882
|
+
changed: false,
|
|
883
|
+
sites: [],
|
|
884
|
+
diagnostics: ctx.diagnostics,
|
|
885
|
+
errorCount
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
let shimSource;
|
|
889
|
+
if (verifiedBodies.length > 0) {
|
|
890
|
+
const deps = collectShimDeps(
|
|
891
|
+
verifiedBodies.flatMap((b) => [b.bodyFn, b.bodyArg]),
|
|
892
|
+
bindings
|
|
893
|
+
);
|
|
894
|
+
const parts = [
|
|
895
|
+
"// Generated by `graphddb transform prepared --aot` \u2014 build-time evaluation",
|
|
896
|
+
"// shim (auto-deleted). Carries ONLY the module-static declarations the",
|
|
897
|
+
"// prepared bodies reference; top-level side effects are NOT copied."
|
|
898
|
+
];
|
|
899
|
+
for (const stmt of sf.statements) {
|
|
900
|
+
const bound = statementBoundNames(stmt);
|
|
901
|
+
if (bound.length === 0) continue;
|
|
902
|
+
const isDecl = ts.isImportDeclaration(stmt) || ts.isVariableStatement(stmt) || ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt) || ts.isEnumDeclaration(stmt);
|
|
903
|
+
if (!isDecl) continue;
|
|
904
|
+
if (bound.some((n) => deps.has(n))) {
|
|
905
|
+
parts.push(sourceText.slice(stmt.getStart(sf), stmt.getEnd()));
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
for (const b of verifiedBodies) {
|
|
909
|
+
parts.push(
|
|
910
|
+
`const __gddbAotBody_${b.index} = (${sourceText.slice(
|
|
911
|
+
b.bodyArg.getStart(sf),
|
|
912
|
+
b.bodyArg.getEnd()
|
|
913
|
+
)});`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
parts.push(
|
|
917
|
+
`export const ${AOT_SHIM_EXPORT} = {` + verifiedBodies.map((b) => ` ${b.index}: __gddbAotBody_${b.index},`).join("") + " };"
|
|
918
|
+
);
|
|
919
|
+
shimSource = parts.join("\n") + "\n";
|
|
920
|
+
}
|
|
921
|
+
let text = sourceText;
|
|
922
|
+
if (options.mode === "transform" && (edits.length > 0 || needsHeaderImports)) {
|
|
923
|
+
let header = "";
|
|
924
|
+
const nl = sourceText.includes("\r\n") ? "\r\n" : "\n";
|
|
925
|
+
if (needsHeaderImports && !bindings.has(AOT_LOADER_LOCAL)) {
|
|
926
|
+
header += `${nl}${nl}// graphddb transform prepared --aot (#208): static prepared plans \u2014 each${nl}// call site loads its build-time-compiled plan; the body stays in source as${nl}// the plan's compile source (drift regeneration) and typed fallback.${nl}import { loadPreparedPlan as ${AOT_LOADER_LOCAL} } from 'graphddb';${nl}import ${AOT_PLANS_LOCAL} from '${options.artifactImport}';`;
|
|
927
|
+
}
|
|
928
|
+
if (slotNames.length > 0) {
|
|
929
|
+
header += `${nl}${nl}// graphddb transform prepared (#206): module-scope lazy slots.` + slotNames.map((s) => `${nl}let ${s};`).join("");
|
|
930
|
+
}
|
|
931
|
+
if (header !== "") {
|
|
932
|
+
let declPos = 0;
|
|
933
|
+
for (const stmt of sf.statements) {
|
|
934
|
+
if (ts.isImportDeclaration(stmt)) declPos = stmt.getEnd();
|
|
935
|
+
}
|
|
936
|
+
edits.push({
|
|
937
|
+
pos: declPos,
|
|
938
|
+
end: declPos,
|
|
939
|
+
insert: declPos === 0 ? header.trimStart() + nl : header
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
edits.sort((a, b) => b.pos - a.pos || b.end - a.end);
|
|
943
|
+
for (const e of edits) {
|
|
944
|
+
text = text.slice(0, e.pos) + e.insert + text.slice(e.end);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
fileName,
|
|
949
|
+
text,
|
|
950
|
+
changed: text !== sourceText,
|
|
951
|
+
sites,
|
|
952
|
+
...shimSource !== void 0 ? { shimSource } : {},
|
|
953
|
+
diagnostics: ctx.diagnostics,
|
|
954
|
+
errorCount: 0
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
function resolveBodyFn(arg, bindings) {
|
|
958
|
+
const e = unwrapExpr(arg);
|
|
959
|
+
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) return e;
|
|
960
|
+
if (ts.isIdentifier(e)) {
|
|
961
|
+
const res = resolveIdentifier(e, void 0, bindings);
|
|
962
|
+
if (res.at === "module") {
|
|
963
|
+
const b = res.binding;
|
|
964
|
+
if (b.kind === "const" && b.initializer !== void 0) {
|
|
965
|
+
const init = unwrapExpr(b.initializer);
|
|
966
|
+
if (ts.isArrowFunction(init) || ts.isFunctionExpression(init)) return init;
|
|
967
|
+
} else if (b.kind === "function") {
|
|
968
|
+
return b.node;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return void 0;
|
|
973
|
+
}
|
|
715
974
|
export {
|
|
975
|
+
AOT_LOADER_LOCAL,
|
|
976
|
+
AOT_PLANS_LOCAL,
|
|
977
|
+
AOT_SHIM_EXPORT,
|
|
716
978
|
HOIST_SLOT_PREFIX,
|
|
717
979
|
lintPreparedSource,
|
|
718
|
-
transformPreparedSource
|
|
980
|
+
transformPreparedSource,
|
|
981
|
+
transformPreparedSourceAot
|
|
719
982
|
};
|
package/docs/cqrs-contract.md
CHANGED
package/docs/design-patterns.md
CHANGED
|
@@ -180,13 +180,15 @@ BatchGet, so a `refs` read's `select` cannot push a server-side `FilterExpressio
|
|
|
180
180
|
on the children (BatchGet has none); filter on the returned `items` if needed.
|
|
181
181
|
|
|
182
182
|
> **Runtime scope.** A `refs` read fans a **parent-row list attribute** out into a
|
|
183
|
-
> BatchGet.
|
|
184
|
-
>
|
|
185
|
-
>
|
|
186
|
-
>
|
|
187
|
-
>
|
|
188
|
-
>
|
|
189
|
-
>
|
|
183
|
+
> BatchGet. Since issue #208 (spec version 1.1) the shared operation IR carries this
|
|
184
|
+
> as a first-class **binding form** (`sourceList` on a `BatchGetItem` op), so a
|
|
185
|
+
> select that traverses a `refs` relation compiles to `operations.json` and the
|
|
186
|
+
> generated Python runtime resolves it with the same semantics as the TS in-process
|
|
187
|
+
> runtime (first-seen order, dedup, dangling refs dropped, `cursor: null`; the
|
|
188
|
+
> parent list attribute is projected implicitly and stripped from the result when
|
|
189
|
+
> unselected). One remaining spec-path difference: a client-side `filter` on a
|
|
190
|
+
> `refs` select is TS-in-process-only and is rejected loudly by the spec compiler
|
|
191
|
+
> (never silently dropped).
|
|
190
192
|
|
|
191
193
|
---
|
|
192
194
|
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
`execute(params)` binds the per-call values into the precompiled plan and runs
|
|
6
6
|
it through the **same execution cores** `DDBModel.mutate` / `Model.query` /
|
|
7
7
|
`Model.list` and the public CQRS contracts use — effects are identical by
|
|
8
|
-
construction (design #203, runtime #205, compile-time transform #206
|
|
8
|
+
construction (design #203, runtime #205, compile-time transform #206, static
|
|
9
|
+
AOT plans #208).
|
|
9
10
|
|
|
10
11
|
## The three execution layers (orthogonal, pick per use)
|
|
11
12
|
|
|
@@ -61,14 +62,80 @@ whole contract:
|
|
|
61
62
|
|
|
62
63
|
| Build | Per-op cost of an inline `prepare(fn).execute()` | Result |
|
|
63
64
|
|---|---|---|
|
|
64
|
-
| **
|
|
65
|
+
| **AOT** (`graphddb transform prepared --aot <artifact> --write`, #208) | **zero compilation at runtime, including the first call** — the plan was compiled at BUILD time into a static artifact; the call site loads it and every call binds params into the frozen plan | identical |
|
|
66
|
+
| **transform applied** (`graphddb transform prepared --write`) | zero per-op after the FIRST call (the lazy slot compiles once per module at first use) | identical |
|
|
65
67
|
| **no transform** (fallback) | phase-1 **structural memoization**: the body is re-evaluated and structure-hashed per call, then the compiled handle is reused from a bounded LRU (no recompile) | identical |
|
|
66
|
-
| hand-hoisted module-level `const stmt = DDBModel.prepare(...)` |
|
|
68
|
+
| hand-hoisted module-level `const stmt = DDBModel.prepare(...)` | compile once at first use (runtime); the transform leaves it untouched | identical |
|
|
67
69
|
|
|
68
70
|
The transform exists to remove the placement footgun: without it, an inline
|
|
69
71
|
`prepare` is *correct but warmer* than a module-level one (silent-slow). With
|
|
70
72
|
it, both spellings compile to the same optimal form.
|
|
71
73
|
|
|
74
|
+
### The three optimization tiers — when to use which
|
|
75
|
+
|
|
76
|
+
1. **AOT static plans** (`--aot`, #208) — production builds with a build step.
|
|
77
|
+
Plan construction happens at BUILD time: cold starts (Lambda SnapStart,
|
|
78
|
+
first request) pay **zero** compilation, and a broken plan fails the
|
|
79
|
+
**build**, not the first request. Requires committing the generated
|
|
80
|
+
artifact module and running the drift check in CI (below).
|
|
81
|
+
2. **Lazy slot + structural memoization** (`--write`, #206 / #205) — dev runs,
|
|
82
|
+
`tsx`/`ts-node` no-build-step deployments, and every call site the AOT pass
|
|
83
|
+
did not rewrite (e.g. `prepare` calls inside `node_modules`). The plan
|
|
84
|
+
compiles at runtime, once.
|
|
85
|
+
3. **Ad-hoc `mutate` / `query`** — one-shot operations where preparing buys
|
|
86
|
+
nothing (recompiles per call, by design).
|
|
87
|
+
|
|
88
|
+
The runtime structural-memoization cache is deliberately **kept** even when AOT
|
|
89
|
+
is applied: untransformed builds and third-party `prepare` call sites fall back
|
|
90
|
+
to it with identical behavior (perf only differs), so a build without the
|
|
91
|
+
transform can never become silently per-call-slow.
|
|
92
|
+
|
|
93
|
+
## True build-time compilation: `--aot` static plans (#208)
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# compile every verified prepare body at BUILD time, write the static plan
|
|
97
|
+
# artifact module, and rewrite call sites to load it
|
|
98
|
+
graphddb transform prepared --src src/ --aot src/graphddb.prepared.ts --write
|
|
99
|
+
|
|
100
|
+
# CI drift gate: recompile and compare — exits 1 when model declarations (or
|
|
101
|
+
# prepare bodies) changed without regenerating the artifact
|
|
102
|
+
graphddb transform prepared --src src/ --aot src/graphddb.prepared.ts
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The rewrite keeps the body **in source** — it is the plan's compile source, the
|
|
106
|
+
type source, and the untransformed-build fallback; at runtime the loader never
|
|
107
|
+
evaluates it:
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
// after `--aot src/graphddb.prepared.ts --write`
|
|
111
|
+
import { loadPreparedPlan as __gddbLoadPreparedPlan } from 'graphddb';
|
|
112
|
+
import __gddbPreparedPlans from './graphddb.prepared.js';
|
|
113
|
+
let __gddbPrepared1;
|
|
114
|
+
export async function getUser(userId: string) {
|
|
115
|
+
return (__gddbPrepared1 ??= __gddbLoadPreparedPlan(__gddbPreparedPlans, 'src/app.ts#1', ($) => ({
|
|
116
|
+
u: { query: () => User, key: { userId: $.userId }, select: USER_SELECT },
|
|
117
|
+
}))).execute({ userId });
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
What the artifact carries (the same serialized-IR vocabulary `operations.json`
|
|
122
|
+
uses — templated key conditions / transaction items, model references by
|
|
123
|
+
**entity name**):
|
|
124
|
+
|
|
125
|
+
- a **write** plan freezes the whole mutation compile — lifecycle resolution
|
|
126
|
+
and every derived effect (edges, counters, uniqueness guards, outbox,
|
|
127
|
+
idempotency, maintenance writes, referential ConditionChecks) — into one
|
|
128
|
+
declarative `TransactionSpec` per op;
|
|
129
|
+
- a **read** plan carries the analyzed route (entity, select template, param
|
|
130
|
+
binds, options) plus its physical `QuerySpec`, so a broken read plan fails
|
|
131
|
+
the build.
|
|
132
|
+
|
|
133
|
+
**Stale plans never execute.** Each plan records a fingerprint of every entity
|
|
134
|
+
declaration it touches; the loader recomputes them against the live
|
|
135
|
+
`MetadataRegistry` on first use and rejects loudly on any mismatch (model /
|
|
136
|
+
table-mapping drift since generation), version skew, or a missing plan id — in
|
|
137
|
+
addition to the CI drift gate above.
|
|
138
|
+
|
|
72
139
|
### CLI
|
|
73
140
|
|
|
74
141
|
```bash
|
package/docs/python-bridge.md
CHANGED
|
@@ -153,7 +153,7 @@ so the output is stable across runs.
|
|
|
153
153
|
|
|
154
154
|
```jsonc
|
|
155
155
|
{
|
|
156
|
-
"version": "1.
|
|
156
|
+
"version": "1.1",
|
|
157
157
|
"tables": { "UserPermissions": { "physicalName": "UserPermissions" } },
|
|
158
158
|
"entities": {
|
|
159
159
|
"UserModel": {
|
|
@@ -199,14 +199,17 @@ so the output is stable across runs.
|
|
|
199
199
|
- `key` / `gsis[*]` carry `inputFields` and `pkTemplate` / `skTemplate` where the
|
|
200
200
|
templates use `{field}` placeholders (e.g. `USER#{userId}`, `EMAIL#{email}`).
|
|
201
201
|
`skTemplate` is `null` when the access pattern has no sort key.
|
|
202
|
-
- `relations[*]` is one of `hasMany | hasOne | belongsTo` with a `target`
|
|
203
|
-
and a `keyBinding` (target field → source field).
|
|
202
|
+
- `relations[*]` is one of `hasMany | hasOne | belongsTo | refs` with a `target`
|
|
203
|
+
entity and a `keyBinding` (target field → source field). A `refs` relation
|
|
204
|
+
(issue #197 / #208) additionally carries `refs: { from, key }` — the parent
|
|
205
|
+
LIST attribute holding the inline reference elements and the field read off
|
|
206
|
+
each element.
|
|
204
207
|
|
|
205
208
|
### 4.2 `operations.json` — Execution Specs
|
|
206
209
|
|
|
207
210
|
```ts
|
|
208
211
|
interface OperationsDocument {
|
|
209
|
-
readonly version: '1.
|
|
212
|
+
readonly version: '1.1';
|
|
210
213
|
readonly queries: Record<string, QuerySpec>;
|
|
211
214
|
readonly commands: Record<string, CommandSpec>;
|
|
212
215
|
readonly transactions?: Record<string, TransactionSpec>;
|
|
@@ -468,6 +471,23 @@ GraphDDBRuntime(
|
|
|
468
471
|
`table_mapping` maps logical table names to deployed physical names. The
|
|
469
472
|
specs are loaded and cached at construction.
|
|
470
473
|
|
|
474
|
+
**Spec-version guard (#208).** The IR is a stable execution contract, so
|
|
475
|
+
construction validates both documents' `version` against
|
|
476
|
+
`SPEC_VERSION_SUPPORTED` (currently `1.1`) and raises `GraphDDBError` — loudly,
|
|
477
|
+
never a silent per-operation mis-execution — when a document cannot be
|
|
478
|
+
faithfully executed. The compatibility rule for a `<major>.<minor>` document:
|
|
479
|
+
|
|
480
|
+
- the **major** must equal the supported major (a major bump changes the shape
|
|
481
|
+
of existing vocabulary);
|
|
482
|
+
- the **minor** must be **≤** the supported minor — minors are additive (e.g.
|
|
483
|
+
`1.1` added the `sourceList` list fan-out binding form for `refs`), so every
|
|
484
|
+
*older* document is fully understood, while a *newer* one may carry binding
|
|
485
|
+
forms this runtime does not implement and is forward-rejected;
|
|
486
|
+
- a missing or malformed `version` is rejected.
|
|
487
|
+
|
|
488
|
+
Upgrade `graphddb_runtime` (or regenerate the documents with a matching
|
|
489
|
+
generator) to clear the error.
|
|
490
|
+
|
|
471
491
|
| Method | Behavior |
|
|
472
492
|
|---|---|
|
|
473
493
|
| `execute_query(query_id, params, options=None) -> dict \| None` | Run a single- or multi-operation query; returns the hydrated result, or `None` when the root matches nothing. `options` may carry a pagination `cursor`. |
|