@happyvertical/smrt-core 0.40.70 → 0.41.0

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.
Files changed (157) hide show
  1. package/AGENTS.md +2 -2
  2. package/agents/change-feed.md +1 -1
  3. package/agents/schema-paths.md +487 -29
  4. package/dist/browser.js +5 -4
  5. package/dist/cascade.d.ts +120 -0
  6. package/dist/cascade.d.ts.map +1 -0
  7. package/dist/cascade.js +430 -0
  8. package/dist/cascade.js.map +1 -0
  9. package/dist/change-feed.d.ts +34 -2
  10. package/dist/change-feed.d.ts.map +1 -1
  11. package/dist/change-feed.js +52 -11
  12. package/dist/change-feed.js.map +1 -1
  13. package/dist/class.d.ts +36 -3
  14. package/dist/class.d.ts.map +1 -1
  15. package/dist/class.js +59 -5
  16. package/dist/class.js.map +1 -1
  17. package/dist/collection-cache.js +0 -0
  18. package/dist/collection-cache.js.map +1 -1
  19. package/dist/collection.d.ts +29 -0
  20. package/dist/collection.d.ts.map +1 -1
  21. package/dist/collection.js +66 -14
  22. package/dist/collection.js.map +1 -1
  23. package/dist/config.d.ts +10 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js.map +1 -1
  26. package/dist/decorators/index.d.ts +63 -5
  27. package/dist/decorators/index.d.ts.map +1 -1
  28. package/dist/decorators/index.js +37 -5
  29. package/dist/decorators/index.js.map +1 -1
  30. package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
  31. package/dist/dispatch/collections/Dispatches.js +19 -4
  32. package/dist/dispatch/collections/Dispatches.js.map +1 -1
  33. package/dist/dispatch/types.d.ts +5 -0
  34. package/dist/dispatch/types.d.ts.map +1 -1
  35. package/dist/embedded-write-queue.d.ts +46 -0
  36. package/dist/embedded-write-queue.d.ts.map +1 -0
  37. package/dist/embedded-write-queue.js +66 -0
  38. package/dist/embedded-write-queue.js.map +1 -0
  39. package/dist/embeddings/storage.d.ts +7 -0
  40. package/dist/embeddings/storage.d.ts.map +1 -1
  41. package/dist/embeddings/storage.js +29 -12
  42. package/dist/embeddings/storage.js.map +1 -1
  43. package/dist/errors.d.ts +1 -1
  44. package/dist/hierarchical.js +1 -1
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8 -6
  48. package/dist/interceptors.d.ts +21 -0
  49. package/dist/interceptors.d.ts.map +1 -1
  50. package/dist/interceptors.js +27 -1
  51. package/dist/interceptors.js.map +1 -1
  52. package/dist/manifest/generator.d.ts.map +1 -1
  53. package/dist/manifest/generator.js +4 -7
  54. package/dist/manifest/generator.js.map +1 -1
  55. package/dist/manifest/static-manifest.js +10 -10
  56. package/dist/manifest/static-manifest.js.map +1 -1
  57. package/dist/manifest/store.js +1 -1
  58. package/dist/manifest/store.js.map +1 -1
  59. package/dist/manifest.json +19 -19
  60. package/dist/migrations/differ.d.ts +9 -1
  61. package/dist/migrations/differ.d.ts.map +1 -1
  62. package/dist/migrations/differ.js +10 -5
  63. package/dist/migrations/differ.js.map +1 -1
  64. package/dist/migrations/index.d.ts +1 -0
  65. package/dist/migrations/index.d.ts.map +1 -1
  66. package/dist/migrations/index.js +2 -1
  67. package/dist/migrations/minor-units.d.ts +162 -0
  68. package/dist/migrations/minor-units.d.ts.map +1 -0
  69. package/dist/migrations/minor-units.js +381 -0
  70. package/dist/migrations/minor-units.js.map +1 -0
  71. package/dist/migrations.js +2 -1
  72. package/dist/object.d.ts +98 -5
  73. package/dist/object.d.ts.map +1 -1
  74. package/dist/object.js +138 -30
  75. package/dist/object.js.map +1 -1
  76. package/dist/query-bounds.d.ts +5 -1
  77. package/dist/query-bounds.d.ts.map +1 -1
  78. package/dist/query-bounds.js +5 -1
  79. package/dist/query-bounds.js.map +1 -1
  80. package/dist/registry/class-registration.d.ts.map +1 -1
  81. package/dist/registry/class-registration.js +3 -1
  82. package/dist/registry/class-registration.js.map +1 -1
  83. package/dist/registry/manifest-field-merge.d.ts +12 -0
  84. package/dist/registry/manifest-field-merge.d.ts.map +1 -1
  85. package/dist/registry/manifest-field-merge.js +14 -2
  86. package/dist/registry/manifest-field-merge.js.map +1 -1
  87. package/dist/registry/schema-builder.d.ts +22 -1
  88. package/dist/registry/schema-builder.d.ts.map +1 -1
  89. package/dist/registry/schema-builder.js +205 -165
  90. package/dist/registry/schema-builder.js.map +1 -1
  91. package/dist/registry/types.d.ts +3 -2
  92. package/dist/registry/types.d.ts.map +1 -1
  93. package/dist/registry.d.ts +41 -46
  94. package/dist/registry.d.ts.map +1 -1
  95. package/dist/registry.js +61 -83
  96. package/dist/registry.js.map +1 -1
  97. package/dist/scanner/manifest-generator.d.ts +45 -0
  98. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  99. package/dist/scanner/manifest-generator.js +92 -28
  100. package/dist/scanner/manifest-generator.js.map +1 -1
  101. package/dist/schema/conflict-target.d.ts +104 -0
  102. package/dist/schema/conflict-target.d.ts.map +1 -0
  103. package/dist/schema/conflict-target.js +129 -0
  104. package/dist/schema/conflict-target.js.map +1 -0
  105. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  106. package/dist/schema/ddl/duckdb-strategy.js +2 -1
  107. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  108. package/dist/schema/ddl/postgres-strategy.d.ts.map +1 -1
  109. package/dist/schema/ddl/postgres-strategy.js +12 -1
  110. package/dist/schema/ddl/postgres-strategy.js.map +1 -1
  111. package/dist/schema/generator.d.ts +252 -16
  112. package/dist/schema/generator.d.ts.map +1 -1
  113. package/dist/schema/generator.js +411 -106
  114. package/dist/schema/generator.js.map +1 -1
  115. package/dist/schema/index-utils.d.ts +120 -0
  116. package/dist/schema/index-utils.d.ts.map +1 -1
  117. package/dist/schema/index-utils.js +242 -1
  118. package/dist/schema/index-utils.js.map +1 -1
  119. package/dist/schema/index.d.ts +1 -1
  120. package/dist/schema/index.d.ts.map +1 -1
  121. package/dist/schema/index.js +2 -2
  122. package/dist/schema/live-parity.d.ts.map +1 -1
  123. package/dist/schema/live-parity.js +4 -2
  124. package/dist/schema/live-parity.js.map +1 -1
  125. package/dist/schema/system-table-shapes.d.ts +13 -0
  126. package/dist/schema/system-table-shapes.d.ts.map +1 -1
  127. package/dist/schema/system-table-shapes.js +14 -1
  128. package/dist/schema/system-table-shapes.js.map +1 -1
  129. package/dist/schema/utils.d.ts +1 -1
  130. package/dist/schema/utils.d.ts.map +1 -1
  131. package/dist/schema/utils.js +3 -3
  132. package/dist/schema/utils.js.map +1 -1
  133. package/dist/schema.js +2 -2
  134. package/dist/smrt-knowledge.json +9 -9
  135. package/dist/system/compatibility.d.ts +42 -0
  136. package/dist/system/compatibility.d.ts.map +1 -1
  137. package/dist/system/compatibility.js +182 -9
  138. package/dist/system/compatibility.js.map +1 -1
  139. package/dist/system/index.d.ts +1 -0
  140. package/dist/system/index.d.ts.map +1 -1
  141. package/dist/system/index.js +3 -2
  142. package/dist/system/retention.d.ts +237 -0
  143. package/dist/system/retention.d.ts.map +1 -0
  144. package/dist/system/retention.js +497 -0
  145. package/dist/system/retention.js.map +1 -0
  146. package/dist/system/schema.d.ts +100 -15
  147. package/dist/system/schema.d.ts.map +1 -1
  148. package/dist/system/schema.js +81 -45
  149. package/dist/system/schema.js.map +1 -1
  150. package/dist/system/types.d.ts +0 -2
  151. package/dist/system/types.d.ts.map +1 -1
  152. package/dist/testing/database.d.ts.map +1 -1
  153. package/dist/testing/database.js.map +1 -1
  154. package/dist/vite-plugin/index.d.ts.map +1 -1
  155. package/dist/vite-plugin/index.js +4 -9
  156. package/dist/vite-plugin/index.js.map +1 -1
  157. package/package.json +4 -4
@@ -11,6 +11,18 @@ import { RegisteredField } from './types';
11
11
  * `Map<string, RegisteredField>` they came from.
12
12
  */
13
13
  type ManifestFieldInput = FieldDefinition;
14
+ /**
15
+ * Read a schema-shaping field attribute from either placement.
16
+ *
17
+ * `required`, `default`, and `description` are written at the top level by
18
+ * manifest field definitions but are normalized into `_meta` by
19
+ * {@link createFieldFromManifest} / {@link mergeManifestField}. Any consumer
20
+ * that shapes a column from a field must therefore check both placements, or
21
+ * it silently drops NOT NULL and DEFAULT for every registry-sourced field
22
+ * (issue #2372). Top level wins so an explicit manifest value overrides the
23
+ * normalized copy.
24
+ */
25
+ export declare function readFieldAttribute(fieldDef: ManifestFieldInput, key: 'required' | 'default' | 'description'): unknown;
14
26
  export declare function createFieldFromManifest(fieldDef: ManifestFieldInput): RegisteredField;
15
27
  export declare function mergeManifestField(existingField: RegisteredField, fieldDef: ManifestFieldInput): RegisteredField;
16
28
  export declare function manifestFieldDiffers(existingField: RegisteredField, fieldDef: ManifestFieldInput): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-field-merge.d.ts","sourceRoot":"","sources":["../../src/registry/manifest-field-merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAa,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C;;;;;;;;;GASG;AACH,KAAK,kBAAkB,GAAG,eAAe,CAAC;AAuD1C,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,kBAAkB,GAC3B,eAAe,CA0BjB;AAED,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,eAAe,EAC9B,QAAQ,EAAE,kBAAkB,GAC3B,eAAe,CAiCjB;AAED,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,eAAe,EAC9B,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAQT"}
1
+ {"version":3,"file":"manifest-field-merge.d.ts","sourceRoot":"","sources":["../../src/registry/manifest-field-merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAa,MAAM,qBAAqB,CAAC;AACtE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C;;;;;;;;;GASG;AACH,KAAK,kBAAkB,GAAG,eAAe,CAAC;AAyB1C;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,kBAAkB,EAC5B,GAAG,EAAE,UAAU,GAAG,SAAS,GAAG,aAAa,WAW5C;AAmBD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,kBAAkB,GAC3B,eAAe,CA0BjB;AAED,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,eAAe,EAC9B,QAAQ,EAAE,kBAAkB,GAC3B,eAAe,CAiCjB;AAED,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,eAAe,EAC9B,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAQT"}
@@ -6,10 +6,22 @@ function readExistingFieldMeta(existingField, key) {
6
6
  if (hasOwnDefinedValue(existingField, key)) return existingField[key];
7
7
  if (hasOwnDefinedValue(existingField?._meta, key)) return existingField._meta?.[key];
8
8
  }
9
- function readManifestFieldMeta(fieldDef, key) {
9
+ /**
10
+ * Read a schema-shaping field attribute from either placement.
11
+ *
12
+ * `required`, `default`, and `description` are written at the top level by
13
+ * manifest field definitions but are normalized into `_meta` by
14
+ * {@link createFieldFromManifest} / {@link mergeManifestField}. Any consumer
15
+ * that shapes a column from a field must therefore check both placements, or
16
+ * it silently drops NOT NULL and DEFAULT for every registry-sourced field
17
+ * (issue #2372). Top level wins so an explicit manifest value overrides the
18
+ * normalized copy.
19
+ */
20
+ function readFieldAttribute(fieldDef, key) {
10
21
  if (hasOwnDefinedValue(fieldDef, key)) return fieldDef[key];
11
22
  if (hasOwnDefinedValue(fieldDef?._meta, key)) return fieldDef._meta?.[key];
12
23
  }
24
+ var readManifestFieldMeta = readFieldAttribute;
13
25
  function assignDefinedMeta(target, source) {
14
26
  if (!source) return;
15
27
  for (const [key, value] of Object.entries(source)) if (value !== void 0) target[key] = value;
@@ -62,6 +74,6 @@ function manifestFieldDiffers(existingField, fieldDef) {
62
74
  return mergedField.type !== existingField.type || JSON.stringify(mergedField._meta || {}) !== JSON.stringify(existingField._meta || {});
63
75
  }
64
76
  //#endregion
65
- export { createFieldFromManifest, manifestFieldDiffers, mergeManifestField };
77
+ export { createFieldFromManifest, manifestFieldDiffers, mergeManifestField, readFieldAttribute };
66
78
 
67
79
  //# sourceMappingURL=manifest-field-merge.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest-field-merge.js","names":[],"sources":["../../src/registry/manifest-field-merge.ts"],"sourcesContent":["import type { FieldDefinition, FieldMeta } from '../scanner/types.js';\nimport type { RegisteredField } from './types';\n\n/**\n * Manifest-sourced field input handled by the merge helpers.\n *\n * Manifest fields are plain {@link FieldDefinition}s. Already-registered\n * runtime fields are the canonical {@link RegisteredField} superset, which\n * extends `FieldDefinition`, so a `FieldDefinition` parameter accepts both\n * shapes as the incoming `fieldDef`. The helpers return the canonical\n * {@link RegisteredField} so merged fields stay assignable back into the\n * `Map<string, RegisteredField>` they came from.\n */\ntype ManifestFieldInput = FieldDefinition;\n\nfunction hasOwnDefinedValue(source: object | undefined, key: string): boolean {\n return (\n !!source &&\n Object.hasOwn(source, key) &&\n (source as Record<string, unknown>)[key] !== undefined\n );\n}\n\nfunction readExistingFieldMeta(\n existingField: RegisteredField,\n key: 'required' | 'default' | 'description',\n) {\n if (hasOwnDefinedValue(existingField, key)) {\n return existingField[key];\n }\n\n if (hasOwnDefinedValue(existingField?._meta, key)) {\n return existingField._meta?.[key];\n }\n\n return undefined;\n}\n\nfunction readManifestFieldMeta(\n fieldDef: ManifestFieldInput,\n key: 'required' | 'default' | 'description',\n) {\n if (hasOwnDefinedValue(fieldDef, key)) {\n return fieldDef[key];\n }\n\n if (hasOwnDefinedValue(fieldDef?._meta, key)) {\n return fieldDef._meta?.[key];\n }\n\n return undefined;\n}\n\nfunction assignDefinedMeta(\n target: Record<string, unknown>,\n source: Record<string, unknown> | undefined,\n) {\n if (!source) {\n return;\n }\n\n for (const [key, value] of Object.entries(source)) {\n if (value !== undefined) {\n target[key] = value;\n }\n }\n}\n\nexport function createFieldFromManifest(\n fieldDef: ManifestFieldInput,\n): RegisteredField {\n const meta: FieldMeta = {};\n\n assignDefinedMeta(meta, fieldDef?._meta);\n\n // Write through the open `FieldMeta` index signature: assigning the same\n // loop value to a union of named keys (`default` is `unknown`, the others\n // narrower) collapses the write target to `undefined`, so route by string.\n const metaRecord = meta as Record<string, unknown>;\n for (const key of ['required', 'default', 'description'] as const) {\n const value = readManifestFieldMeta(fieldDef, key);\n if (value !== undefined) {\n metaRecord[key] = value;\n }\n }\n\n // Preserve top-level `related` for relationship-graph lookups\n // (foreignKey / crossPackageRef / oneToMany / manyToMany all rely on it).\n const out: RegisteredField = {\n type: fieldDef.type,\n _meta: meta,\n };\n if (fieldDef.related !== undefined) {\n out.related = fieldDef.related;\n }\n return out;\n}\n\nexport function mergeManifestField(\n existingField: RegisteredField,\n fieldDef: ManifestFieldInput,\n): RegisteredField {\n const hasTenancyMarker =\n existingField.__tenancy?.isTenantIdField ||\n existingField._meta?.__tenancy?.isTenantIdField;\n\n const nextMeta: FieldMeta = {\n ...(existingField._meta || {}),\n };\n\n assignDefinedMeta(nextMeta, fieldDef?._meta);\n\n // See createFieldFromManifest: route writes through the index signature so a\n // shared loop value isn't narrowed to the `undefined`-only union write target.\n const nextMetaRecord = nextMeta as Record<string, unknown>;\n for (const key of ['required', 'default', 'description'] as const) {\n const manifestValue = readManifestFieldMeta(fieldDef, key);\n if (manifestValue !== undefined) {\n nextMetaRecord[key] = manifestValue;\n continue;\n }\n\n const existingValue = readExistingFieldMeta(existingField, key);\n if (existingValue !== undefined && nextMetaRecord[key] === undefined) {\n nextMetaRecord[key] = existingValue;\n }\n }\n\n return {\n ...existingField,\n type:\n !hasTenancyMarker && fieldDef.type ? fieldDef.type : existingField.type,\n _meta: nextMeta,\n };\n}\n\nexport function manifestFieldDiffers(\n existingField: RegisteredField,\n fieldDef: ManifestFieldInput,\n): boolean {\n const mergedField = mergeManifestField(existingField, fieldDef);\n\n return (\n mergedField.type !== existingField.type ||\n JSON.stringify(mergedField._meta || {}) !==\n JSON.stringify(existingField._meta || {})\n );\n}\n"],"mappings":";AAeA,SAAS,mBAAmB,QAA4B,KAAsB;CAC5E,OACE,CAAC,CAAC,UACF,OAAO,OAAO,QAAQ,GAAG,KACxB,OAAmC,SAAS,KAAA;AAEjD;AAEA,SAAS,sBACP,eACA,KACA;CACA,IAAI,mBAAmB,eAAe,GAAG,GACvC,OAAO,cAAc;CAGvB,IAAI,mBAAmB,eAAe,OAAO,GAAG,GAC9C,OAAO,cAAc,QAAQ;AAIjC;AAEA,SAAS,sBACP,UACA,KACA;CACA,IAAI,mBAAmB,UAAU,GAAG,GAClC,OAAO,SAAS;CAGlB,IAAI,mBAAmB,UAAU,OAAO,GAAG,GACzC,OAAO,SAAS,QAAQ;AAI5B;AAEA,SAAS,kBACP,QACA,QACA;CACA,IAAI,CAAC,QACH;CAGF,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,UAAU,KAAA,GACZ,OAAO,OAAO;AAGpB;AAEA,SAAgB,wBACd,UACiB;CACjB,MAAM,OAAkB,CAAC;CAEzB,kBAAkB,MAAM,UAAU,KAAK;CAKvC,MAAM,aAAa;CACnB,KAAK,MAAM,OAAO;EAAC;EAAY;EAAW;CAAa,GAAY;EACjE,MAAM,QAAQ,sBAAsB,UAAU,GAAG;EACjD,IAAI,UAAU,KAAA,GACZ,WAAW,OAAO;CAEtB;CAIA,MAAM,MAAuB;EAC3B,MAAM,SAAS;EACf,OAAO;CACT;CACA,IAAI,SAAS,YAAY,KAAA,GACvB,IAAI,UAAU,SAAS;CAEzB,OAAO;AACT;AAEA,SAAgB,mBACd,eACA,UACiB;CACjB,MAAM,mBACJ,cAAc,WAAW,mBACzB,cAAc,OAAO,WAAW;CAElC,MAAM,WAAsB,EAC1B,GAAI,cAAc,SAAS,CAAC,EAC9B;CAEA,kBAAkB,UAAU,UAAU,KAAK;CAI3C,MAAM,iBAAiB;CACvB,KAAK,MAAM,OAAO;EAAC;EAAY;EAAW;CAAa,GAAY;EACjE,MAAM,gBAAgB,sBAAsB,UAAU,GAAG;EACzD,IAAI,kBAAkB,KAAA,GAAW;GAC/B,eAAe,OAAO;GACtB;EACF;EAEA,MAAM,gBAAgB,sBAAsB,eAAe,GAAG;EAC9D,IAAI,kBAAkB,KAAA,KAAa,eAAe,SAAS,KAAA,GACzD,eAAe,OAAO;CAE1B;CAEA,OAAO;EACL,GAAG;EACH,MACE,CAAC,oBAAoB,SAAS,OAAO,SAAS,OAAO,cAAc;EACrE,OAAO;CACT;AACF;AAEA,SAAgB,qBACd,eACA,UACS;CACT,MAAM,cAAc,mBAAmB,eAAe,QAAQ;CAE9D,OACE,YAAY,SAAS,cAAc,QACnC,KAAK,UAAU,YAAY,SAAS,CAAC,CAAC,MACpC,KAAK,UAAU,cAAc,SAAS,CAAC,CAAC;AAE9C"}
1
+ {"version":3,"file":"manifest-field-merge.js","names":[],"sources":["../../src/registry/manifest-field-merge.ts"],"sourcesContent":["import type { FieldDefinition, FieldMeta } from '../scanner/types.js';\nimport type { RegisteredField } from './types';\n\n/**\n * Manifest-sourced field input handled by the merge helpers.\n *\n * Manifest fields are plain {@link FieldDefinition}s. Already-registered\n * runtime fields are the canonical {@link RegisteredField} superset, which\n * extends `FieldDefinition`, so a `FieldDefinition` parameter accepts both\n * shapes as the incoming `fieldDef`. The helpers return the canonical\n * {@link RegisteredField} so merged fields stay assignable back into the\n * `Map<string, RegisteredField>` they came from.\n */\ntype ManifestFieldInput = FieldDefinition;\n\nfunction hasOwnDefinedValue(source: object | undefined, key: string): boolean {\n return (\n !!source &&\n Object.hasOwn(source, key) &&\n (source as Record<string, unknown>)[key] !== undefined\n );\n}\n\nfunction readExistingFieldMeta(\n existingField: RegisteredField,\n key: 'required' | 'default' | 'description',\n) {\n if (hasOwnDefinedValue(existingField, key)) {\n return existingField[key];\n }\n\n if (hasOwnDefinedValue(existingField?._meta, key)) {\n return existingField._meta?.[key];\n }\n\n return undefined;\n}\n\n/**\n * Read a schema-shaping field attribute from either placement.\n *\n * `required`, `default`, and `description` are written at the top level by\n * manifest field definitions but are normalized into `_meta` by\n * {@link createFieldFromManifest} / {@link mergeManifestField}. Any consumer\n * that shapes a column from a field must therefore check both placements, or\n * it silently drops NOT NULL and DEFAULT for every registry-sourced field\n * (issue #2372). Top level wins so an explicit manifest value overrides the\n * normalized copy.\n */\nexport function readFieldAttribute(\n fieldDef: ManifestFieldInput,\n key: 'required' | 'default' | 'description',\n) {\n if (hasOwnDefinedValue(fieldDef, key)) {\n return fieldDef[key];\n }\n\n if (hasOwnDefinedValue(fieldDef?._meta, key)) {\n return fieldDef._meta?.[key];\n }\n\n return undefined;\n}\n\nconst readManifestFieldMeta = readFieldAttribute;\n\nfunction assignDefinedMeta(\n target: Record<string, unknown>,\n source: Record<string, unknown> | undefined,\n) {\n if (!source) {\n return;\n }\n\n for (const [key, value] of Object.entries(source)) {\n if (value !== undefined) {\n target[key] = value;\n }\n }\n}\n\nexport function createFieldFromManifest(\n fieldDef: ManifestFieldInput,\n): RegisteredField {\n const meta: FieldMeta = {};\n\n assignDefinedMeta(meta, fieldDef?._meta);\n\n // Write through the open `FieldMeta` index signature: assigning the same\n // loop value to a union of named keys (`default` is `unknown`, the others\n // narrower) collapses the write target to `undefined`, so route by string.\n const metaRecord = meta as Record<string, unknown>;\n for (const key of ['required', 'default', 'description'] as const) {\n const value = readManifestFieldMeta(fieldDef, key);\n if (value !== undefined) {\n metaRecord[key] = value;\n }\n }\n\n // Preserve top-level `related` for relationship-graph lookups\n // (foreignKey / crossPackageRef / oneToMany / manyToMany all rely on it).\n const out: RegisteredField = {\n type: fieldDef.type,\n _meta: meta,\n };\n if (fieldDef.related !== undefined) {\n out.related = fieldDef.related;\n }\n return out;\n}\n\nexport function mergeManifestField(\n existingField: RegisteredField,\n fieldDef: ManifestFieldInput,\n): RegisteredField {\n const hasTenancyMarker =\n existingField.__tenancy?.isTenantIdField ||\n existingField._meta?.__tenancy?.isTenantIdField;\n\n const nextMeta: FieldMeta = {\n ...(existingField._meta || {}),\n };\n\n assignDefinedMeta(nextMeta, fieldDef?._meta);\n\n // See createFieldFromManifest: route writes through the index signature so a\n // shared loop value isn't narrowed to the `undefined`-only union write target.\n const nextMetaRecord = nextMeta as Record<string, unknown>;\n for (const key of ['required', 'default', 'description'] as const) {\n const manifestValue = readManifestFieldMeta(fieldDef, key);\n if (manifestValue !== undefined) {\n nextMetaRecord[key] = manifestValue;\n continue;\n }\n\n const existingValue = readExistingFieldMeta(existingField, key);\n if (existingValue !== undefined && nextMetaRecord[key] === undefined) {\n nextMetaRecord[key] = existingValue;\n }\n }\n\n return {\n ...existingField,\n type:\n !hasTenancyMarker && fieldDef.type ? fieldDef.type : existingField.type,\n _meta: nextMeta,\n };\n}\n\nexport function manifestFieldDiffers(\n existingField: RegisteredField,\n fieldDef: ManifestFieldInput,\n): boolean {\n const mergedField = mergeManifestField(existingField, fieldDef);\n\n return (\n mergedField.type !== existingField.type ||\n JSON.stringify(mergedField._meta || {}) !==\n JSON.stringify(existingField._meta || {})\n );\n}\n"],"mappings":";AAeA,SAAS,mBAAmB,QAA4B,KAAsB;CAC5E,OACE,CAAC,CAAC,UACF,OAAO,OAAO,QAAQ,GAAG,KACxB,OAAmC,SAAS,KAAA;AAEjD;AAEA,SAAS,sBACP,eACA,KACA;CACA,IAAI,mBAAmB,eAAe,GAAG,GACvC,OAAO,cAAc;CAGvB,IAAI,mBAAmB,eAAe,OAAO,GAAG,GAC9C,OAAO,cAAc,QAAQ;AAIjC;;;;;;;;;;;;AAaA,SAAgB,mBACd,UACA,KACA;CACA,IAAI,mBAAmB,UAAU,GAAG,GAClC,OAAO,SAAS;CAGlB,IAAI,mBAAmB,UAAU,OAAO,GAAG,GACzC,OAAO,SAAS,QAAQ;AAI5B;AAEA,IAAM,wBAAwB;AAE9B,SAAS,kBACP,QACA,QACA;CACA,IAAI,CAAC,QACH;CAGF,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,UAAU,KAAA,GACZ,OAAO,OAAO;AAGpB;AAEA,SAAgB,wBACd,UACiB;CACjB,MAAM,OAAkB,CAAC;CAEzB,kBAAkB,MAAM,UAAU,KAAK;CAKvC,MAAM,aAAa;CACnB,KAAK,MAAM,OAAO;EAAC;EAAY;EAAW;CAAa,GAAY;EACjE,MAAM,QAAQ,sBAAsB,UAAU,GAAG;EACjD,IAAI,UAAU,KAAA,GACZ,WAAW,OAAO;CAEtB;CAIA,MAAM,MAAuB;EAC3B,MAAM,SAAS;EACf,OAAO;CACT;CACA,IAAI,SAAS,YAAY,KAAA,GACvB,IAAI,UAAU,SAAS;CAEzB,OAAO;AACT;AAEA,SAAgB,mBACd,eACA,UACiB;CACjB,MAAM,mBACJ,cAAc,WAAW,mBACzB,cAAc,OAAO,WAAW;CAElC,MAAM,WAAsB,EAC1B,GAAI,cAAc,SAAS,CAAC,EAC9B;CAEA,kBAAkB,UAAU,UAAU,KAAK;CAI3C,MAAM,iBAAiB;CACvB,KAAK,MAAM,OAAO;EAAC;EAAY;EAAW;CAAa,GAAY;EACjE,MAAM,gBAAgB,sBAAsB,UAAU,GAAG;EACzD,IAAI,kBAAkB,KAAA,GAAW;GAC/B,eAAe,OAAO;GACtB;EACF;EAEA,MAAM,gBAAgB,sBAAsB,eAAe,GAAG;EAC9D,IAAI,kBAAkB,KAAA,KAAa,eAAe,SAAS,KAAA,GACzD,eAAe,OAAO;CAE1B;CAEA,OAAO;EACL,GAAG;EACH,MACE,CAAC,oBAAoB,SAAS,OAAO,SAAS,OAAO,cAAc;EACrE,OAAO;CACT;AACF;AAEA,SAAgB,qBACd,eACA,UACS;CACT,MAAM,cAAc,mBAAmB,eAAe,QAAQ;CAE9D,OACE,YAAY,SAAS,cAAc,QACnC,KAAK,UAAU,YAAY,SAAS,CAAC,CAAC,MACpC,KAAK,UAAU,cAAc,SAAS,CAAC,CAAC;AAE9C"}
@@ -101,17 +101,38 @@ export declare function generateDDLFromColumns(tableName: string, columns: Recor
101
101
  * @returns Formatted SQL default value
102
102
  */
103
103
  export declare function formatDefaultValue(value: unknown, type: string): string;
104
+ /**
105
+ * Options for {@link fieldsToColumns}.
106
+ */
107
+ export interface FieldsToColumnsOptions {
108
+ /**
109
+ * Shape the columns for a single-table-inheritance table.
110
+ *
111
+ * An STI table holds the union of every subtype's fields, so a column that
112
+ * only one subtype declares must stay nullable — rows of sibling subtypes
113
+ * never populate it. This matches `generateSTISchemaFromManifest`, which
114
+ * emits `notNull: false` for every non-system column on an STI table.
115
+ * Declared defaults are still emitted.
116
+ */
117
+ stiUnionColumns?: boolean;
118
+ }
104
119
  /**
105
120
  * Convert a Map of field definitions to column definitions
106
121
  *
107
122
  * Used by getAllSchemas() to generate columns from fields when a class
108
123
  * has no pre-generated schema (e.g., STI subclasses registered from manifest).
109
124
  *
125
+ * `required`, `default`, and `description` are read from either the top level
126
+ * or `_meta`: registry-sourced fields normalize them into `_meta`, so reading
127
+ * only the top level dropped NOT NULL and DEFAULT for every such field
128
+ * (#2372).
129
+ *
110
130
  * @param fields - Map of field name to field definition
131
+ * @param options - Table-shape options (see {@link FieldsToColumnsOptions})
111
132
  * @returns Record of column name to column definition
112
133
  * @private
113
134
  */
114
- export declare function fieldsToColumns(fields: Map<string, FieldDefinition>): Record<string, ColumnDefinition>;
135
+ export declare function fieldsToColumns(fields: Map<string, FieldDefinition>, options?: FieldsToColumnsOptions): Record<string, ColumnDefinition>;
115
136
  /**
116
137
  * Map field type to SQL data type
117
138
  * @private
@@ -1 +1 @@
1
- {"version":3,"file":"schema-builder.d.ts","sourceRoot":"","sources":["../../src/registry/schema-builder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAK7D,OAAO,KAAK,EACV,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,EACZ,MAAM,oBAAoB,CAAC;AA6I5B;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAIpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAkB7D;AAyCD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA+OxE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CA2K7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACzC,KAAK,UAAQ,EACb,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CAwDR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAgFlC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,CAAC,MAAM,CAAC,GACjC,WAAW,CAqBb"}
1
+ {"version":3,"file":"schema-builder.d.ts","sourceRoot":"","sources":["../../src/registry/schema-builder.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAM7D,OAAO,KAAK,EACV,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,EACZ,MAAM,oBAAoB,CAAC;AAmL5B;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAIpE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,GAAG,SAAS,CAWpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAkB7D;AAwTD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CAAC,MAAM,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAmFxE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAsC7E;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,EACzC,KAAK,UAAQ,EACb,MAAM,CAAC,EAAE,cAAc,GACtB,MAAM,CAwDR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;;;OAQG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EACpC,OAAO,CAAC,EAAE,sBAAsB,GAC/B,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAwFlC;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,CAAC,MAAM,CAAC,GACjC,WAAW,CAqBb"}
@@ -1,9 +1,12 @@
1
+ import { classnameToTablename, toSnakeCase } from "../utils/naming.js";
1
2
  import { formatDefaultValue as formatDefaultValue$1, quoteIdentifier } from "../schema/sql-identifiers.js";
3
+ import { shortenIdentifier } from "../schema/index-utils.js";
2
4
  import { getDDLStrategy } from "../schema/ddl/index.js";
3
5
  import { getClasses, getCollectionTableNames } from "./shared-state.js";
4
6
  import { findClass } from "./name-resolver.js";
5
- import { classnameToTablename, toSnakeCase } from "../utils/naming.js";
7
+ import { conflictIndexName } from "../schema/conflict-target.js";
6
8
  import "../utils.js";
9
+ import { readFieldAttribute } from "./manifest-field-merge.js";
7
10
  import { isCollectionRegistration } from "./collection-resolution.js";
8
11
  import { ObjectRegistry } from "../registry.js";
9
12
  //#region src/registry/schema-builder.ts
@@ -34,10 +37,10 @@ function applyDecoratorSqlTypeOverrides(className, columns) {
34
37
  }
35
38
  return columns;
36
39
  }
37
- function mergeRuntimeFieldColumns(className, schemaColumns, fields) {
40
+ function mergeRuntimeFieldColumns(className, schemaColumns, fields, options) {
38
41
  const columnsToUse = { ...schemaColumns || {} };
39
42
  if (fields.size > 0) {
40
- const fieldColumns = fieldsToColumns(fields);
43
+ const fieldColumns = fieldsToColumns(fields, options);
41
44
  for (const [columnName, columnDef] of Object.entries(fieldColumns)) if (!columnsToUse[columnName]) columnsToUse[columnName] = columnDef;
42
45
  for (const [fieldName, fieldDef] of fields) {
43
46
  const columnName = toSnakeCase(fieldName);
@@ -60,24 +63,58 @@ function getReferenceKind(fieldDef) {
60
63
  if (fieldDef.type === "foreignKey") return "foreignKey";
61
64
  if (fieldDef.type === "crossPackageRef") return "crossPackageRef";
62
65
  }
63
- function shouldEmitDefault(fieldDef, sqlType) {
64
- return !(getReferenceKind(fieldDef) === "tenantId" && sqlType === "UUID" && fieldDef.default === "");
66
+ function shouldEmitDefault(fieldDef, sqlType, defaultValue) {
67
+ return !(getReferenceKind(fieldDef) === "tenantId" && sqlType === "UUID" && defaultValue === "");
65
68
  }
66
- function createBaseColumns(registered) {
67
- return {
69
+ /**
70
+ * Fallback base columns for a table whose contributing class carries no
71
+ * manifest columns.
72
+ *
73
+ * These mirror what the manifest schema generators emit for the same table
74
+ * (`generateSchemaFromManifest` / `generateSTISchemaFromManifest`), so a table
75
+ * assembled from runtime field metadata alone has the same NOT NULL and
76
+ * DEFAULT shape as one assembled from a manifest. Divergence here is what made
77
+ * the merged table shape depend on which class registered first (#2372).
78
+ */
79
+ function createBaseColumns(registered, isSTI) {
80
+ const baseColumns = {
68
81
  id: {
69
82
  type: registered.config?.idType === "text" ? "TEXT" : "UUID",
70
83
  primaryKey: true,
84
+ notNull: true,
71
85
  referenceKind: "id"
72
86
  },
73
87
  slug: {
74
88
  type: "TEXT",
75
89
  notNull: true
76
90
  },
77
- context: { type: "TEXT" },
78
- created_at: { type: "TIMESTAMP" },
79
- updated_at: { type: "TIMESTAMP" }
91
+ context: {
92
+ type: "TEXT",
93
+ notNull: true,
94
+ defaultValue: ""
95
+ },
96
+ created_at: {
97
+ type: "TIMESTAMP",
98
+ notNull: true,
99
+ defaultValue: "current_timestamp"
100
+ },
101
+ updated_at: {
102
+ type: "TIMESTAMP",
103
+ notNull: true,
104
+ defaultValue: "current_timestamp"
105
+ }
80
106
  };
107
+ if (isSTI) {
108
+ baseColumns._meta_type = {
109
+ type: "TEXT",
110
+ notNull: true
111
+ };
112
+ baseColumns._meta_data = {
113
+ type: "JSON",
114
+ notNull: false
115
+ };
116
+ }
117
+ return baseColumns;
81
118
  }
82
119
  /**
83
120
  * Get cached schema definition for a registered class
@@ -139,14 +176,151 @@ function getTableName(name) {
139
176
  }
140
177
  function withConflictIndex(tableName, columns, indexes, conflictColumns) {
141
178
  if (conflictColumns.length === 0 || !conflictColumns.every((column) => columns[column])) return indexes;
179
+ const primaryKeyColumns = Object.entries(columns).filter(([, column]) => column.primaryKey === true).map(([name]) => name);
180
+ if (primaryKeyColumns.length > 0 && primaryKeyColumns.length === conflictColumns.length && primaryKeyColumns.every((column) => conflictColumns.includes(column))) return indexes;
142
181
  if (indexes.some((index) => index.unique === true && !index.where && !index.jsonPath && index.columns.length === conflictColumns.length && index.columns.every((column) => conflictColumns.includes(column)))) return indexes;
143
- const preferredName = `${tableName}_${(conflictColumns.length > 2 ? conflictColumns.slice(0, 2) : conflictColumns).join("_")}_idx`;
144
- const name = indexes.some((index) => index.name === preferredName) ? `${preferredName}_unique` : preferredName;
145
- return [...indexes, {
182
+ const tenantColumn = Object.entries(columns).find(([, column]) => column.referenceKind === "tenantId")?.[0];
183
+ const name = shortenIdentifier(conflictIndexName(tableName, conflictColumns, tenantColumn));
184
+ const conflictIndex = {
146
185
  name,
147
186
  columns: conflictColumns,
148
187
  unique: true
149
- }];
188
+ };
189
+ if (indexes.some((index) => index.name === name)) return indexes.map((index) => index.name === name ? conflictIndex : index);
190
+ return [...indexes, conflictIndex];
191
+ }
192
+ /**
193
+ * Resolve the physical table a registered class writes to.
194
+ *
195
+ * STI subclasses are folded onto their base's table. Returns `undefined` when
196
+ * the class has no table (for example an unresolvable manifest stub).
197
+ *
198
+ * R5-canon: STI lookups use the qualified key so a colliding simple name in
199
+ * another package cannot yield the wrong table strategy or STI base and move
200
+ * this class's columns under that other package's table.
201
+ */
202
+ function resolveContributorTable(registered, fallbackName) {
203
+ const simpleName = registered.name || fallbackName;
204
+ const qualifiedName = registered.qualifiedName ?? simpleName;
205
+ if (!registered.schema?.tableName && registered.extends) {
206
+ if (ObjectRegistry.getTableStrategy(qualifiedName) === "sti") {
207
+ const stiBaseName = ObjectRegistry.getSTIBase(qualifiedName);
208
+ if (stiBaseName && stiBaseName !== qualifiedName) {
209
+ const stiBaseClass = findClass(stiBaseName);
210
+ if (stiBaseClass?.schema?.tableName) {
211
+ if (!registered.schema) registered.schema = {
212
+ tableName: "",
213
+ ddl: "",
214
+ columns: {},
215
+ indexes: [],
216
+ triggers: [],
217
+ foreignKeys: [],
218
+ dependencies: [],
219
+ version: ""
220
+ };
221
+ registered.schema.tableName = stiBaseClass.schema.tableName;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ if (!registered.schema?.tableName) return;
227
+ let tableName = registered.schema.tableName;
228
+ const isSTI = ObjectRegistry.getTableStrategy(qualifiedName) === "sti";
229
+ let isSTIBase = isSTI;
230
+ let conflictKey = qualifiedName;
231
+ if (isSTI) {
232
+ const stiBaseName = ObjectRegistry.getSTIBase(qualifiedName);
233
+ if (stiBaseName) {
234
+ conflictKey = stiBaseName;
235
+ if (stiBaseName !== qualifiedName) {
236
+ isSTIBase = false;
237
+ const stiBaseClass = findClass(stiBaseName);
238
+ if (stiBaseClass?.schema?.tableName) tableName = stiBaseClass.schema.tableName;
239
+ }
240
+ }
241
+ }
242
+ return {
243
+ tableName,
244
+ contributor: {
245
+ registered,
246
+ simpleName,
247
+ qualifiedName,
248
+ isSTI,
249
+ isSTIBase,
250
+ depth: ObjectRegistry.getInheritanceChain(qualifiedName).length,
251
+ conflictKey
252
+ }
253
+ };
254
+ }
255
+ /**
256
+ * Order the classes that share one table deterministically.
257
+ *
258
+ * The first contributor seeds the table: it supplies the base columns, the
259
+ * `idType`, the conflict columns and the cached DDL, and its columns win every
260
+ * merge conflict. Registration order must not decide that, or the same
261
+ * hierarchy yields a different table shape depending on which class a manifest
262
+ * happens to list first — child-first dropped NOT NULL and DEFAULT from the
263
+ * base columns (#2372).
264
+ *
265
+ * Order: the STI base first, then ancestors before descendants, then by
266
+ * qualified name so the result is a total order.
267
+ */
268
+ function sortTableContributors(contributors) {
269
+ return [...contributors].sort((a, b) => {
270
+ if (a.isSTIBase !== b.isSTIBase) return a.isSTIBase ? -1 : 1;
271
+ if (a.depth !== b.depth) return a.depth - b.depth;
272
+ return a.qualifiedName.localeCompare(b.qualifiedName);
273
+ });
274
+ }
275
+ /**
276
+ * Assemble every physical table from the classes that contribute to it.
277
+ *
278
+ * Shared by {@link getAllSchemas} and {@link getAllSchemasAsDefinitions} so
279
+ * both produce the same merged shape. The result depends only on what is
280
+ * registered, never on the order it was registered in.
281
+ */
282
+ function buildMergedTableSchemas() {
283
+ const contributorsByTable = /* @__PURE__ */ new Map();
284
+ for (const [className, registered] of getClasses()) {
285
+ if (isCollectionRegistration(className, registered, collectionRegistrationLookup)) continue;
286
+ const resolved = resolveContributorTable(registered, className);
287
+ if (!resolved) continue;
288
+ const existing = contributorsByTable.get(resolved.tableName);
289
+ if (existing) existing.push(resolved.contributor);
290
+ else contributorsByTable.set(resolved.tableName, [resolved.contributor]);
291
+ }
292
+ const tableSchemas = {};
293
+ for (const [tableName, contributors] of contributorsByTable) for (const contributor of sortTableContributors(contributors)) {
294
+ const { registered, simpleName, isSTI } = contributor;
295
+ const columnsToUse = mergeRuntimeFieldColumns(simpleName, registered.schema?.columns, registered.fields, { stiUnionColumns: isSTI });
296
+ let tableSchema = tableSchemas[tableName];
297
+ if (!tableSchema) {
298
+ tableSchema = {
299
+ tableName,
300
+ columns: {
301
+ ...createBaseColumns(registered, isSTI),
302
+ ...columnsToUse
303
+ },
304
+ indexes: [],
305
+ ddl: registered.schema?.ddl || "",
306
+ isSTI,
307
+ conflictColumns: ObjectRegistry.getConflictColumns(contributor.conflictKey)
308
+ };
309
+ tableSchemas[tableName] = tableSchema;
310
+ } else for (const [colName, colDef] of Object.entries(columnsToUse)) if (!tableSchema.columns[colName]) tableSchema.columns[colName] = colDef;
311
+ const schemaIndexes = registered.schema?.indexes;
312
+ if (schemaIndexes && schemaIndexes.length > 0) {
313
+ const existingNames = new Set(tableSchema.indexes.map((index) => index.name));
314
+ for (const index of schemaIndexes) {
315
+ if (typeof index === "string") continue;
316
+ if (!existingNames.has(index.name)) {
317
+ tableSchema.indexes.push(index);
318
+ existingNames.add(index.name);
319
+ }
320
+ }
321
+ }
322
+ }
323
+ return tableSchemas;
150
324
  }
151
325
  /**
152
326
  * Get all pre-generated schemas for explicit adapter bootstrap paths.
@@ -164,81 +338,7 @@ function withConflictIndex(tableName, columns, indexes, conflictColumns) {
164
338
  * ```
165
339
  */
166
340
  function getAllSchemas(engine) {
167
- const tableSchemas = {};
168
- for (const [_className, registered] of getClasses()) {
169
- if (isCollectionRegistration(_className, registered, collectionRegistrationLookup)) continue;
170
- const simpleName = registered.name || _className;
171
- if (!registered.schema?.tableName && registered.extends) {
172
- const qualifiedName = registered.qualifiedName ?? simpleName;
173
- const lookupKey = qualifiedName;
174
- if (ObjectRegistry.getTableStrategy(lookupKey) === "sti") {
175
- const stiBaseName = ObjectRegistry.getSTIBase(lookupKey);
176
- if (stiBaseName && stiBaseName !== qualifiedName) {
177
- const stiBaseClass = findClass(stiBaseName);
178
- if (stiBaseClass?.schema?.tableName) {
179
- if (!registered.schema) registered.schema = {
180
- tableName: "",
181
- ddl: "",
182
- columns: {},
183
- indexes: [],
184
- triggers: [],
185
- foreignKeys: [],
186
- dependencies: [],
187
- version: ""
188
- };
189
- registered.schema.tableName = stiBaseClass.schema.tableName;
190
- }
191
- }
192
- }
193
- }
194
- if (registered.schema?.tableName) {
195
- let tableName = registered.schema.tableName;
196
- const qualifiedName = registered.qualifiedName ?? simpleName;
197
- const lookupKey = qualifiedName;
198
- const tableStrategy = ObjectRegistry.getTableStrategy(lookupKey);
199
- if (tableStrategy === "sti") {
200
- const stiBaseName = ObjectRegistry.getSTIBase(lookupKey);
201
- if (stiBaseName && stiBaseName !== qualifiedName) {
202
- const stiBaseClass = findClass(stiBaseName);
203
- if (stiBaseClass?.schema?.tableName) tableName = stiBaseClass.schema.tableName;
204
- }
205
- }
206
- const columnsToUse = mergeRuntimeFieldColumns(simpleName, registered.schema.columns, registered.fields);
207
- if (!tableSchemas[tableName]) {
208
- const baseColumns = createBaseColumns(registered);
209
- const isSTI = tableStrategy === "sti";
210
- if (isSTI) {
211
- baseColumns._meta_type = {
212
- type: "TEXT",
213
- notNull: true,
214
- defaultValue: ""
215
- };
216
- baseColumns._meta_data = { type: "JSON" };
217
- }
218
- tableSchemas[tableName] = {
219
- tableName,
220
- columns: {
221
- ...baseColumns,
222
- ...columnsToUse
223
- },
224
- indexes: [],
225
- ddl: registered.schema.ddl || "",
226
- isSTI,
227
- conflictColumns: ObjectRegistry.getConflictColumns(isSTI ? ObjectRegistry.getSTIBase(lookupKey) || lookupKey : lookupKey)
228
- };
229
- } else for (const [colName, colDef] of Object.entries(columnsToUse)) if (!tableSchemas[tableName].columns[colName]) tableSchemas[tableName].columns[colName] = colDef;
230
- if (registered.schema.indexes && registered.schema.indexes.length > 0) {
231
- const existingNames = new Set(tableSchemas[tableName].indexes.map((idx) => typeof idx === "string" ? idx : idx.name));
232
- for (const idx of registered.schema.indexes) {
233
- const indexName = typeof idx === "string" ? idx : idx.name;
234
- if (!existingNames.has(indexName)) if (typeof idx === "string") {} else {
235
- tableSchemas[tableName].indexes.push(idx);
236
- existingNames.add(idx.name);
237
- }
238
- }
239
- }
240
- }
241
- }
341
+ const tableSchemas = buildMergedTableSchemas();
242
342
  const schemas = {};
243
343
  for (const [tableName, tableSchema] of Object.entries(tableSchemas)) {
244
344
  const engineIndexes = engine ? withConflictIndex(tableName, tableSchema.columns, tableSchema.indexes, tableSchema.conflictColumns) : tableSchema.indexes;
@@ -285,77 +385,7 @@ function getAllSchemas(engine) {
285
385
  * @returns Map of tableName to SchemaDefinition
286
386
  */
287
387
  function getAllSchemasAsDefinitions() {
288
- const tableSchemas = {};
289
- for (const [_className, registered] of getClasses()) {
290
- if (isCollectionRegistration(_className, registered, collectionRegistrationLookup)) continue;
291
- const simpleName = registered.name || _className;
292
- if (!registered.schema?.tableName && registered.extends) {
293
- const qualifiedName = registered.qualifiedName ?? simpleName;
294
- const lookupKey = qualifiedName;
295
- if (ObjectRegistry.getTableStrategy(lookupKey) === "sti") {
296
- const stiBaseName = ObjectRegistry.getSTIBase(lookupKey);
297
- if (stiBaseName && stiBaseName !== qualifiedName) {
298
- const stiBaseClass = findClass(stiBaseName);
299
- if (stiBaseClass?.schema?.tableName) {
300
- if (!registered.schema) registered.schema = {
301
- tableName: "",
302
- ddl: "",
303
- columns: {},
304
- indexes: [],
305
- triggers: [],
306
- foreignKeys: [],
307
- dependencies: [],
308
- version: ""
309
- };
310
- registered.schema.tableName = stiBaseClass.schema.tableName;
311
- }
312
- }
313
- }
314
- }
315
- if (registered.schema?.tableName) {
316
- let tableName = registered.schema.tableName;
317
- const qualifiedName = registered.qualifiedName ?? simpleName;
318
- const lookupKey = qualifiedName;
319
- const tableStrategy = ObjectRegistry.getTableStrategy(lookupKey);
320
- if (tableStrategy === "sti") {
321
- const stiBaseName = ObjectRegistry.getSTIBase(lookupKey);
322
- if (stiBaseName && stiBaseName !== qualifiedName) {
323
- const stiBaseClass = findClass(stiBaseName);
324
- if (stiBaseClass?.schema?.tableName) tableName = stiBaseClass.schema.tableName;
325
- }
326
- }
327
- const columnsToUse = mergeRuntimeFieldColumns(simpleName, registered.schema.columns, registered.fields);
328
- if (!tableSchemas[tableName]) {
329
- const baseColumns = createBaseColumns(registered);
330
- const isSTI = tableStrategy === "sti";
331
- if (isSTI) {
332
- baseColumns._meta_type = {
333
- type: "TEXT",
334
- notNull: true,
335
- defaultValue: ""
336
- };
337
- baseColumns._meta_data = { type: "JSON" };
338
- }
339
- tableSchemas[tableName] = {
340
- tableName,
341
- columns: {
342
- ...baseColumns,
343
- ...columnsToUse
344
- },
345
- indexes: [],
346
- isSTI,
347
- conflictColumns: ObjectRegistry.getConflictColumns(isSTI ? ObjectRegistry.getSTIBase(lookupKey) || lookupKey : lookupKey)
348
- };
349
- } else for (const [colName, colDef] of Object.entries(columnsToUse)) if (!tableSchemas[tableName].columns[colName]) tableSchemas[tableName].columns[colName] = colDef;
350
- if (registered.schema.indexes && registered.schema.indexes.length > 0) {
351
- const existingNames = new Set(tableSchemas[tableName].indexes.map((idx) => idx.name));
352
- for (const idx of registered.schema.indexes) if (!existingNames.has(idx.name)) {
353
- tableSchemas[tableName].indexes.push(idx);
354
- existingNames.add(idx.name);
355
- }
356
- }
357
- }
358
- }
388
+ const tableSchemas = buildMergedTableSchemas();
359
389
  const schemas = {};
360
390
  for (const [tableName, tableSchema] of Object.entries(tableSchemas)) {
361
391
  if (Object.keys(tableSchema.columns).length === 0) continue;
@@ -427,11 +457,17 @@ function formatDefaultValue(value, type) {
427
457
  * Used by getAllSchemas() to generate columns from fields when a class
428
458
  * has no pre-generated schema (e.g., STI subclasses registered from manifest).
429
459
  *
460
+ * `required`, `default`, and `description` are read from either the top level
461
+ * or `_meta`: registry-sourced fields normalize them into `_meta`, so reading
462
+ * only the top level dropped NOT NULL and DEFAULT for every such field
463
+ * (#2372).
464
+ *
430
465
  * @param fields - Map of field name to field definition
466
+ * @param options - Table-shape options (see {@link FieldsToColumnsOptions})
431
467
  * @returns Record of column name to column definition
432
468
  * @private
433
469
  */
434
- function fieldsToColumns(fields) {
470
+ function fieldsToColumns(fields, options) {
435
471
  const columns = {};
436
472
  for (const [fieldName, fieldDef] of fields) {
437
473
  if (fieldName === "id" || fieldName === "created_at" || fieldName === "updated_at" || fieldName === "slug" || fieldName === "context") continue;
@@ -440,14 +476,18 @@ function fieldsToColumns(fields) {
440
476
  if (fieldDef.type === "meta") continue;
441
477
  const sqlType = fieldDef._meta?.sqlType || (fieldDef.type === "crossPackageRef" && (fieldDef._meta?.idType === "text" || fieldDef.idType === "text") ? "TEXT" : mapFieldTypeToSQL(fieldDef.type));
442
478
  const normalizedSqlType = String(sqlType).toUpperCase();
479
+ const referenceKind = getReferenceKind(fieldDef);
480
+ const required = readFieldAttribute(fieldDef, "required");
481
+ const defaultValue = readFieldAttribute(fieldDef, "default");
482
+ const description = readFieldAttribute(fieldDef, "description");
443
483
  const column = {
444
484
  type: normalizedSqlType,
445
- referenceKind: getReferenceKind(fieldDef),
446
- notNull: fieldDef._meta?.nullable ? false : fieldDef.required || false,
485
+ referenceKind,
486
+ notNull: options?.stiUnionColumns ? false : fieldDef._meta?.nullable ? false : Boolean(required),
447
487
  unique: fieldDef._meta?.unique || false,
448
- description: fieldDef.description
488
+ description
449
489
  };
450
- if (fieldDef.default !== void 0 && shouldEmitDefault(fieldDef, normalizedSqlType)) column.defaultValue = fieldDef.default;
490
+ if (defaultValue !== void 0 && shouldEmitDefault(fieldDef, normalizedSqlType, defaultValue)) column.defaultValue = defaultValue;
451
491
  if (fieldDef.type === "foreignKey" && fieldDef.related) {
452
492
  const [table, columnName = "id"] = fieldDef.related.split(".");
453
493
  const fieldMeta = fieldDef._meta;