@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
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Conflict target (upsert natural key) resolution shared by every schema path
3
+ * and by the runtime (#2360, epic #2382 finding B1/A4).
4
+ *
5
+ * `SmrtObject.save()` upserts a NEW object on
6
+ * `ObjectRegistry.getConflictColumns()`; the schema generator emits ONE unique
7
+ * index over the same columns so `INSERT … ON CONFLICT (…)` binds on every
8
+ * engine. Before #2360 the tenancy interceptor populated `tenant_id` on save
9
+ * but neither the conflict target nor the unique index carried it, so a
10
+ * second tenant creating the same natural key (`slug`, `context`) matched the
11
+ * first tenant's row and `DO UPDATE SET` rewrote its `id` and `tenant_id`
12
+ * (probed: tenant A's row was silently adopted by tenant B).
13
+ *
14
+ * The rule now: **a tenant-scoped class's default conflict target is
15
+ * `[tenantColumn, ...naturalKey]`** on the build path (manifest
16
+ * `decoratorConfig.conflictColumns`, materialized by
17
+ * `ManifestGenerator.normalizeConflictColumns()`) and on the runtime path
18
+ * (`ObjectRegistry.getConflictColumns()`), so schema and upsert agree by
19
+ * construction. Explicit `@smrt({ conflictColumns })` always wins and is
20
+ * never rewritten — a class that declares its own natural key owns it.
21
+ *
22
+ * NULL-tenant rows (`mode: 'optional'` outside a tenant context) keep the
23
+ * SDK's null-aware upsert semantics: `@happyvertical/sql` matches conflict
24
+ * columns with `IS NOT DISTINCT FROM`, so `(NULL, 'widget', '')` updates the
25
+ * existing `(NULL, 'widget', '')` row through the model layer even though the
26
+ * database index treats NULLs as distinct. Global rows therefore dedup among
27
+ * themselves exactly as every row did before, and per-tenant rows dedup per
28
+ * tenant.
29
+ *
30
+ * Index NAMES stay stable across this change — the tenant-led default key
31
+ * keeps `<table>_slug_context_idx` (CTI) / `<table>_slug_context_meta_type_idx`
32
+ * (STI) — so `SchemaComparer`'s same-name shape-drift repair swaps the columns
33
+ * in place (`DROP INDEX` + `CREATE UNIQUE INDEX`) instead of leaving the old
34
+ * global unique index behind to keep blocking cross-tenant duplicates. The new
35
+ * key is a superset of the old one, so creating it cannot fail on existing
36
+ * data.
37
+ */
38
+ /** Default natural key of a class-per-table (CTI) object. */
39
+ export declare const DEFAULT_CTI_CONFLICT_COLUMNS: readonly string[];
40
+ /**
41
+ * Default natural key of a single-table-inheritance (STI) hierarchy: the
42
+ * discriminator participates in identity so two subtypes may share a slug.
43
+ */
44
+ export declare const DEFAULT_STI_CONFLICT_COLUMNS: readonly string[];
45
+ export type ConflictTableStrategy = 'cti' | 'sti';
46
+ /**
47
+ * The default conflict target for a table with the given strategy, led by the
48
+ * tenant column when the schema-owning class is tenant-scoped.
49
+ */
50
+ export declare function defaultConflictColumns(strategy: ConflictTableStrategy, tenantColumn?: string | null): string[];
51
+ /**
52
+ * Resolve the conflict target of a table.
53
+ *
54
+ * `explicit` (a declared `@smrt({ conflictColumns })`) always wins; otherwise
55
+ * the strategy default, tenant-led when `tenantColumn` is given.
56
+ */
57
+ export declare function resolveConflictColumns(options: {
58
+ explicit?: readonly string[] | null;
59
+ strategy: ConflictTableStrategy;
60
+ tenantColumn?: string | null;
61
+ }): string[];
62
+ /**
63
+ * Whether `conflictColumns` is exactly a default natural key (CTI or STI),
64
+ * optionally led by `tenantColumn`. Used to keep the default index names
65
+ * stable when the tenant column is prepended.
66
+ */
67
+ export declare function isDefaultNaturalKey(conflictColumns: readonly string[], tenantColumn?: string | null): boolean;
68
+ /**
69
+ * The stable name of the unique conflict index for `conflictColumns`.
70
+ *
71
+ * - default CTI key `(slug, context)` → `<table>_slug_context_idx`
72
+ * - default STI key `(slug, context, _meta_type)` →
73
+ * `<table>_slug_context_meta_type_idx`
74
+ * - either default led by the tenant column → the SAME name as without it
75
+ * (#2360: the differ swaps the columns in place by name)
76
+ * - any custom key → `<table>_<first two columns>_idx` (the historical rule;
77
+ * a longer key keeps the two-column name so existing databases are not
78
+ * renamed)
79
+ *
80
+ * @param tenantColumn - the table's tenant column, when the schema owner is
81
+ * tenant-scoped; only consulted to recognize a tenant-led default key
82
+ */
83
+ export declare function conflictIndexName(tableName: string, conflictColumns: readonly string[], tenantColumn?: string | null): string;
84
+ /**
85
+ * Whether an unqualified index over `indexColumns` serves slug lookups
86
+ * (`loadFromSlug()` / `getId()` / `getSavedId()` filter on `slug`, `context`):
87
+ * it leads with `slug`, or it is the tenant-led default key — a tenant-scoped
88
+ * lookup carries the tenant predicate (#2365) and is served by the prefix.
89
+ */
90
+ export declare function servesSlugLookup(indexColumns: readonly string[], tenantColumn?: string | null): boolean;
91
+ /**
92
+ * The tenant column of a table, from the schema-owning class's normalized
93
+ * tenant-scoped configuration. Returns `undefined` when the class is not
94
+ * tenant-scoped or the configured field is not among its fields (the field is
95
+ * always injected for tenant-scoped classes; the guard keeps the runtime and
96
+ * build paths honest about the column actually existing).
97
+ *
98
+ * @param tenantField - the configured tenant field name (`tenantScoped.field`,
99
+ * default `tenantId`), or `undefined` when the class is not tenant-scoped
100
+ * @param hasField - whether the schema owner declares that field
101
+ * @param toColumn - field-name → column-name conversion (`toSnakeCase`)
102
+ */
103
+ export declare function resolveTenantColumn(tenantField: string | undefined | null, hasField: (fieldName: string) => boolean, toColumn: (fieldName: string) => string): string | undefined;
104
+ //# sourceMappingURL=conflict-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conflict-target.d.ts","sourceRoot":"","sources":["../../src/schema/conflict-target.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,6DAA6D;AAC7D,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAGzD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,MAAM,EAIzD,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,KAAK,GAAG,KAAK,CAAC;AAElD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,qBAAqB,EAC/B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,MAAM,EAAE,CAMV;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE;IAC9C,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACpC,QAAQ,EAAE,qBAAqB,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B,GAAG,MAAM,EAAE,CAKX;AAMD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,eAAe,EAAE,SAAS,MAAM,EAAE,EAClC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,OAAO,CAST;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,SAAS,MAAM,EAAE,EAClC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,MAAM,CAYR;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,GAC3B,OAAO,CAQT;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACtC,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,EACxC,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GACtC,MAAM,GAAG,SAAS,CAGpB"}
@@ -0,0 +1,129 @@
1
+ //#region src/schema/conflict-target.ts
2
+ /**
3
+ * Conflict target (upsert natural key) resolution shared by every schema path
4
+ * and by the runtime (#2360, epic #2382 finding B1/A4).
5
+ *
6
+ * `SmrtObject.save()` upserts a NEW object on
7
+ * `ObjectRegistry.getConflictColumns()`; the schema generator emits ONE unique
8
+ * index over the same columns so `INSERT … ON CONFLICT (…)` binds on every
9
+ * engine. Before #2360 the tenancy interceptor populated `tenant_id` on save
10
+ * but neither the conflict target nor the unique index carried it, so a
11
+ * second tenant creating the same natural key (`slug`, `context`) matched the
12
+ * first tenant's row and `DO UPDATE SET` rewrote its `id` and `tenant_id`
13
+ * (probed: tenant A's row was silently adopted by tenant B).
14
+ *
15
+ * The rule now: **a tenant-scoped class's default conflict target is
16
+ * `[tenantColumn, ...naturalKey]`** on the build path (manifest
17
+ * `decoratorConfig.conflictColumns`, materialized by
18
+ * `ManifestGenerator.normalizeConflictColumns()`) and on the runtime path
19
+ * (`ObjectRegistry.getConflictColumns()`), so schema and upsert agree by
20
+ * construction. Explicit `@smrt({ conflictColumns })` always wins and is
21
+ * never rewritten — a class that declares its own natural key owns it.
22
+ *
23
+ * NULL-tenant rows (`mode: 'optional'` outside a tenant context) keep the
24
+ * SDK's null-aware upsert semantics: `@happyvertical/sql` matches conflict
25
+ * columns with `IS NOT DISTINCT FROM`, so `(NULL, 'widget', '')` updates the
26
+ * existing `(NULL, 'widget', '')` row through the model layer even though the
27
+ * database index treats NULLs as distinct. Global rows therefore dedup among
28
+ * themselves exactly as every row did before, and per-tenant rows dedup per
29
+ * tenant.
30
+ *
31
+ * Index NAMES stay stable across this change — the tenant-led default key
32
+ * keeps `<table>_slug_context_idx` (CTI) / `<table>_slug_context_meta_type_idx`
33
+ * (STI) — so `SchemaComparer`'s same-name shape-drift repair swaps the columns
34
+ * in place (`DROP INDEX` + `CREATE UNIQUE INDEX`) instead of leaving the old
35
+ * global unique index behind to keep blocking cross-tenant duplicates. The new
36
+ * key is a superset of the old one, so creating it cannot fail on existing
37
+ * data.
38
+ */
39
+ /** Default natural key of a class-per-table (CTI) object. */
40
+ var DEFAULT_CTI_CONFLICT_COLUMNS = ["slug", "context"];
41
+ /**
42
+ * Default natural key of a single-table-inheritance (STI) hierarchy: the
43
+ * discriminator participates in identity so two subtypes may share a slug.
44
+ */
45
+ var DEFAULT_STI_CONFLICT_COLUMNS = [
46
+ "slug",
47
+ "context",
48
+ "_meta_type"
49
+ ];
50
+ /**
51
+ * The default conflict target for a table with the given strategy, led by the
52
+ * tenant column when the schema-owning class is tenant-scoped.
53
+ */
54
+ function defaultConflictColumns(strategy, tenantColumn) {
55
+ const naturalKey = strategy === "sti" ? [...DEFAULT_STI_CONFLICT_COLUMNS] : [...DEFAULT_CTI_CONFLICT_COLUMNS];
56
+ return tenantColumn ? [tenantColumn, ...naturalKey] : naturalKey;
57
+ }
58
+ /**
59
+ * Resolve the conflict target of a table.
60
+ *
61
+ * `explicit` (a declared `@smrt({ conflictColumns })`) always wins; otherwise
62
+ * the strategy default, tenant-led when `tenantColumn` is given.
63
+ */
64
+ function resolveConflictColumns(options) {
65
+ if (options.explicit && options.explicit.length > 0) return [...options.explicit];
66
+ return defaultConflictColumns(options.strategy, options.tenantColumn);
67
+ }
68
+ function sameColumns(a, b) {
69
+ return a.length === b.length && a.every((column, i) => column === b[i]);
70
+ }
71
+ /**
72
+ * Whether `conflictColumns` is exactly a default natural key (CTI or STI),
73
+ * optionally led by `tenantColumn`. Used to keep the default index names
74
+ * stable when the tenant column is prepended.
75
+ */
76
+ function isDefaultNaturalKey(conflictColumns, tenantColumn) {
77
+ const key = tenantColumn && conflictColumns[0] === tenantColumn ? conflictColumns.slice(1) : conflictColumns;
78
+ return sameColumns(key, DEFAULT_CTI_CONFLICT_COLUMNS) || sameColumns(key, DEFAULT_STI_CONFLICT_COLUMNS);
79
+ }
80
+ /**
81
+ * The stable name of the unique conflict index for `conflictColumns`.
82
+ *
83
+ * - default CTI key `(slug, context)` → `<table>_slug_context_idx`
84
+ * - default STI key `(slug, context, _meta_type)` →
85
+ * `<table>_slug_context_meta_type_idx`
86
+ * - either default led by the tenant column → the SAME name as without it
87
+ * (#2360: the differ swaps the columns in place by name)
88
+ * - any custom key → `<table>_<first two columns>_idx` (the historical rule;
89
+ * a longer key keeps the two-column name so existing databases are not
90
+ * renamed)
91
+ *
92
+ * @param tenantColumn - the table's tenant column, when the schema owner is
93
+ * tenant-scoped; only consulted to recognize a tenant-led default key
94
+ */
95
+ function conflictIndexName(tableName, conflictColumns, tenantColumn) {
96
+ const key = tenantColumn && conflictColumns[0] === tenantColumn && isDefaultNaturalKey(conflictColumns, tenantColumn) ? conflictColumns.slice(1) : conflictColumns;
97
+ if (sameColumns(key, DEFAULT_STI_CONFLICT_COLUMNS)) return `${tableName}_slug_context_meta_type_idx`;
98
+ return `${tableName}_${(key.length > 2 ? key.slice(0, 2) : key).join("_")}_idx`;
99
+ }
100
+ /**
101
+ * Whether an unqualified index over `indexColumns` serves slug lookups
102
+ * (`loadFromSlug()` / `getId()` / `getSavedId()` filter on `slug`, `context`):
103
+ * it leads with `slug`, or it is the tenant-led default key — a tenant-scoped
104
+ * lookup carries the tenant predicate (#2365) and is served by the prefix.
105
+ */
106
+ function servesSlugLookup(indexColumns, tenantColumn) {
107
+ if (indexColumns[0] === "slug") return true;
108
+ return Boolean(tenantColumn && indexColumns[0] === tenantColumn && indexColumns[1] === "slug" && indexColumns[2] === "context");
109
+ }
110
+ /**
111
+ * The tenant column of a table, from the schema-owning class's normalized
112
+ * tenant-scoped configuration. Returns `undefined` when the class is not
113
+ * tenant-scoped or the configured field is not among its fields (the field is
114
+ * always injected for tenant-scoped classes; the guard keeps the runtime and
115
+ * build paths honest about the column actually existing).
116
+ *
117
+ * @param tenantField - the configured tenant field name (`tenantScoped.field`,
118
+ * default `tenantId`), or `undefined` when the class is not tenant-scoped
119
+ * @param hasField - whether the schema owner declares that field
120
+ * @param toColumn - field-name → column-name conversion (`toSnakeCase`)
121
+ */
122
+ function resolveTenantColumn(tenantField, hasField, toColumn) {
123
+ if (!tenantField || !hasField(tenantField)) return void 0;
124
+ return toColumn(tenantField);
125
+ }
126
+ //#endregion
127
+ export { DEFAULT_CTI_CONFLICT_COLUMNS, DEFAULT_STI_CONFLICT_COLUMNS, conflictIndexName, defaultConflictColumns, isDefaultNaturalKey, resolveConflictColumns, resolveTenantColumn, servesSlugLookup };
128
+
129
+ //# sourceMappingURL=conflict-target.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conflict-target.js","names":[],"sources":["../../src/schema/conflict-target.ts"],"sourcesContent":["/**\n * Conflict target (upsert natural key) resolution shared by every schema path\n * and by the runtime (#2360, epic #2382 finding B1/A4).\n *\n * `SmrtObject.save()` upserts a NEW object on\n * `ObjectRegistry.getConflictColumns()`; the schema generator emits ONE unique\n * index over the same columns so `INSERT … ON CONFLICT (…)` binds on every\n * engine. Before #2360 the tenancy interceptor populated `tenant_id` on save\n * but neither the conflict target nor the unique index carried it, so a\n * second tenant creating the same natural key (`slug`, `context`) matched the\n * first tenant's row and `DO UPDATE SET` rewrote its `id` and `tenant_id`\n * (probed: tenant A's row was silently adopted by tenant B).\n *\n * The rule now: **a tenant-scoped class's default conflict target is\n * `[tenantColumn, ...naturalKey]`** on the build path (manifest\n * `decoratorConfig.conflictColumns`, materialized by\n * `ManifestGenerator.normalizeConflictColumns()`) and on the runtime path\n * (`ObjectRegistry.getConflictColumns()`), so schema and upsert agree by\n * construction. Explicit `@smrt({ conflictColumns })` always wins and is\n * never rewritten — a class that declares its own natural key owns it.\n *\n * NULL-tenant rows (`mode: 'optional'` outside a tenant context) keep the\n * SDK's null-aware upsert semantics: `@happyvertical/sql` matches conflict\n * columns with `IS NOT DISTINCT FROM`, so `(NULL, 'widget', '')` updates the\n * existing `(NULL, 'widget', '')` row through the model layer even though the\n * database index treats NULLs as distinct. Global rows therefore dedup among\n * themselves exactly as every row did before, and per-tenant rows dedup per\n * tenant.\n *\n * Index NAMES stay stable across this change — the tenant-led default key\n * keeps `<table>_slug_context_idx` (CTI) / `<table>_slug_context_meta_type_idx`\n * (STI) — so `SchemaComparer`'s same-name shape-drift repair swaps the columns\n * in place (`DROP INDEX` + `CREATE UNIQUE INDEX`) instead of leaving the old\n * global unique index behind to keep blocking cross-tenant duplicates. The new\n * key is a superset of the old one, so creating it cannot fail on existing\n * data.\n */\n\n/** Default natural key of a class-per-table (CTI) object. */\nexport const DEFAULT_CTI_CONFLICT_COLUMNS: readonly string[] = [\n 'slug',\n 'context',\n];\n\n/**\n * Default natural key of a single-table-inheritance (STI) hierarchy: the\n * discriminator participates in identity so two subtypes may share a slug.\n */\nexport const DEFAULT_STI_CONFLICT_COLUMNS: readonly string[] = [\n 'slug',\n 'context',\n '_meta_type',\n];\n\nexport type ConflictTableStrategy = 'cti' | 'sti';\n\n/**\n * The default conflict target for a table with the given strategy, led by the\n * tenant column when the schema-owning class is tenant-scoped.\n */\nexport function defaultConflictColumns(\n strategy: ConflictTableStrategy,\n tenantColumn?: string | null,\n): string[] {\n const naturalKey =\n strategy === 'sti'\n ? [...DEFAULT_STI_CONFLICT_COLUMNS]\n : [...DEFAULT_CTI_CONFLICT_COLUMNS];\n return tenantColumn ? [tenantColumn, ...naturalKey] : naturalKey;\n}\n\n/**\n * Resolve the conflict target of a table.\n *\n * `explicit` (a declared `@smrt({ conflictColumns })`) always wins; otherwise\n * the strategy default, tenant-led when `tenantColumn` is given.\n */\nexport function resolveConflictColumns(options: {\n explicit?: readonly string[] | null;\n strategy: ConflictTableStrategy;\n tenantColumn?: string | null;\n}): string[] {\n if (options.explicit && options.explicit.length > 0) {\n return [...options.explicit];\n }\n return defaultConflictColumns(options.strategy, options.tenantColumn);\n}\n\nfunction sameColumns(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((column, i) => column === b[i]);\n}\n\n/**\n * Whether `conflictColumns` is exactly a default natural key (CTI or STI),\n * optionally led by `tenantColumn`. Used to keep the default index names\n * stable when the tenant column is prepended.\n */\nexport function isDefaultNaturalKey(\n conflictColumns: readonly string[],\n tenantColumn?: string | null,\n): boolean {\n const key =\n tenantColumn && conflictColumns[0] === tenantColumn\n ? conflictColumns.slice(1)\n : conflictColumns;\n return (\n sameColumns(key, DEFAULT_CTI_CONFLICT_COLUMNS) ||\n sameColumns(key, DEFAULT_STI_CONFLICT_COLUMNS)\n );\n}\n\n/**\n * The stable name of the unique conflict index for `conflictColumns`.\n *\n * - default CTI key `(slug, context)` → `<table>_slug_context_idx`\n * - default STI key `(slug, context, _meta_type)` →\n * `<table>_slug_context_meta_type_idx`\n * - either default led by the tenant column → the SAME name as without it\n * (#2360: the differ swaps the columns in place by name)\n * - any custom key → `<table>_<first two columns>_idx` (the historical rule;\n * a longer key keeps the two-column name so existing databases are not\n * renamed)\n *\n * @param tenantColumn - the table's tenant column, when the schema owner is\n * tenant-scoped; only consulted to recognize a tenant-led default key\n */\nexport function conflictIndexName(\n tableName: string,\n conflictColumns: readonly string[],\n tenantColumn?: string | null,\n): string {\n const key =\n tenantColumn &&\n conflictColumns[0] === tenantColumn &&\n isDefaultNaturalKey(conflictColumns, tenantColumn)\n ? conflictColumns.slice(1)\n : conflictColumns;\n if (sameColumns(key, DEFAULT_STI_CONFLICT_COLUMNS)) {\n return `${tableName}_slug_context_meta_type_idx`;\n }\n const nameColumns = key.length > 2 ? key.slice(0, 2) : key;\n return `${tableName}_${nameColumns.join('_')}_idx`;\n}\n\n/**\n * Whether an unqualified index over `indexColumns` serves slug lookups\n * (`loadFromSlug()` / `getId()` / `getSavedId()` filter on `slug`, `context`):\n * it leads with `slug`, or it is the tenant-led default key — a tenant-scoped\n * lookup carries the tenant predicate (#2365) and is served by the prefix.\n */\nexport function servesSlugLookup(\n indexColumns: readonly string[],\n tenantColumn?: string | null,\n): boolean {\n if (indexColumns[0] === 'slug') return true;\n return Boolean(\n tenantColumn &&\n indexColumns[0] === tenantColumn &&\n indexColumns[1] === 'slug' &&\n indexColumns[2] === 'context',\n );\n}\n\n/**\n * The tenant column of a table, from the schema-owning class's normalized\n * tenant-scoped configuration. Returns `undefined` when the class is not\n * tenant-scoped or the configured field is not among its fields (the field is\n * always injected for tenant-scoped classes; the guard keeps the runtime and\n * build paths honest about the column actually existing).\n *\n * @param tenantField - the configured tenant field name (`tenantScoped.field`,\n * default `tenantId`), or `undefined` when the class is not tenant-scoped\n * @param hasField - whether the schema owner declares that field\n * @param toColumn - field-name → column-name conversion (`toSnakeCase`)\n */\nexport function resolveTenantColumn(\n tenantField: string | undefined | null,\n hasField: (fieldName: string) => boolean,\n toColumn: (fieldName: string) => string,\n): string | undefined {\n if (!tenantField || !hasField(tenantField)) return undefined;\n return toColumn(tenantField);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,IAAa,+BAAkD,CAC7D,QACA,SACF;;;;;AAMA,IAAa,+BAAkD;CAC7D;CACA;CACA;AACF;;;;;AAQA,SAAgB,uBACd,UACA,cACU;CACV,MAAM,aACJ,aAAa,QACT,CAAC,GAAG,4BAA4B,IAChC,CAAC,GAAG,4BAA4B;CACtC,OAAO,eAAe,CAAC,cAAc,GAAG,UAAU,IAAI;AACxD;;;;;;;AAQA,SAAgB,uBAAuB,SAI1B;CACX,IAAI,QAAQ,YAAY,QAAQ,SAAS,SAAS,GAChD,OAAO,CAAC,GAAG,QAAQ,QAAQ;CAE7B,OAAO,uBAAuB,QAAQ,UAAU,QAAQ,YAAY;AACtE;AAEA,SAAS,YAAY,GAAsB,GAA+B;CACxE,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,QAAQ,MAAM,WAAW,EAAE,EAAE;AACxE;;;;;;AAOA,SAAgB,oBACd,iBACA,cACS;CACT,MAAM,MACJ,gBAAgB,gBAAgB,OAAO,eACnC,gBAAgB,MAAM,CAAC,IACvB;CACN,OACE,YAAY,KAAK,4BAA4B,KAC7C,YAAY,KAAK,4BAA4B;AAEjD;;;;;;;;;;;;;;;;AAiBA,SAAgB,kBACd,WACA,iBACA,cACQ;CACR,MAAM,MACJ,gBACA,gBAAgB,OAAO,gBACvB,oBAAoB,iBAAiB,YAAY,IAC7C,gBAAgB,MAAM,CAAC,IACvB;CACN,IAAI,YAAY,KAAK,4BAA4B,GAC/C,OAAO,GAAG,UAAU;CAGtB,OAAO,GAAG,UAAU,IADA,IAAI,SAAS,IAAI,IAAI,MAAM,GAAG,CAAC,IAAI,IAAA,CACpB,KAAK,GAAG,EAAE;AAC/C;;;;;;;AAQA,SAAgB,iBACd,cACA,cACS;CACT,IAAI,aAAa,OAAO,QAAQ,OAAO;CACvC,OAAO,QACL,gBACE,aAAa,OAAO,gBACpB,aAAa,OAAO,UACpB,aAAa,OAAO,SACxB;AACF;;;;;;;;;;;;;AAcA,SAAgB,oBACd,aACA,UACA,UACoB;CACpB,IAAI,CAAC,eAAe,CAAC,SAAS,WAAW,GAAG,OAAO,KAAA;CACnD,OAAO,SAAS,WAAW;AAC7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"duckdb-strategy.d.ts","sourceRoot":"","sources":["../../../src/schema/ddl/duckdb-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,qBAAa,cAAe,SAAQ,eAAe;IACjD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAY;IAE3C;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM;IAalC;;;;OAIG;IACH,eAAe,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE;IA4BnD;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,EAAE;IAIrD;;OAEG;IACH,gBAAgB,IAAI,OAAO;IAI3B;;;;;OAKG;IACH,oBAAoB,IAAI,OAAO;CAGhC"}
1
+ {"version":3,"file":"duckdb-strategy.d.ts","sourceRoot":"","sources":["../../../src/schema/ddl/duckdb-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,qBAAa,cAAe,SAAQ,eAAe;IACjD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAY;IAE3C;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM;IAalC;;;;OAIG;IACH,eAAe,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE;IAwCnD;;OAEG;IACH,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,MAAM,EAAE;IAIrD;;OAEG;IACH,gBAAgB,IAAI,OAAO;IAI3B;;;;;OAKG;IACH,oBAAoB,IAAI,OAAO;CAGhC"}
@@ -1,5 +1,5 @@
1
1
  import { BaseDDLStrategy } from "./base-strategy.js";
2
- import { renderIndexTarget } from "../index-utils.js";
2
+ import { isStiSubtypeUniqueIndex, renderIndexTarget } from "../index-utils.js";
3
3
  //#region src/schema/ddl/duckdb-strategy.ts
4
4
  /**
5
5
  * DuckDB DDL Strategy
@@ -37,6 +37,7 @@ var DuckDBStrategy = class extends BaseDDLStrategy {
37
37
  const statements = [];
38
38
  for (const index of indexes) {
39
39
  if (index.unique && !index.where && !index.jsonPath) continue;
40
+ if (isStiSubtypeUniqueIndex(index)) continue;
40
41
  const target = renderIndexTarget(index, this.engine);
41
42
  if (!target) continue;
42
43
  let sql = `CREATE ${index.unique ? "UNIQUE INDEX" : "INDEX"} IF NOT EXISTS "${index.name}" ON "${tableName}" (${target})`;
@@ -1 +1 @@
1
- {"version":3,"file":"duckdb-strategy.js","names":[],"sources":["../../../src/schema/ddl/duckdb-strategy.ts"],"sourcesContent":["/**\n * DuckDB DDL Strategy\n *\n * DuckDB-specific DDL generation with the following characteristics:\n * - UNIQUE constraints MUST be inline for UPSERT to work (DuckDB issue #12684)\n * - Supports CAST in DEFAULT values\n * - Native BOOLEAN, JSON types\n * - Does NOT support triggers\n *\n * CRITICAL: DuckDB's ON CONFLICT clause only works with inline UNIQUE constraints,\n * not with separate UNIQUE indexes. This is a known DuckDB limitation.\n */\n\nimport { renderIndexTarget } from '../index-utils.js';\nimport type { SchemaDefinition, SQLDataType } from '../types.js';\nimport { BaseDDLStrategy } from './base-strategy.js';\nimport type { DatabaseEngine } from './types.js';\n\nexport class DuckDBStrategy extends BaseDDLStrategy {\n readonly engine: DatabaseEngine = 'duckdb';\n\n /**\n * Map types for DuckDB\n * DuckDB has native support for most types\n */\n mapType(type: SQLDataType): string {\n switch (type) {\n case 'TIMESTAMP':\n return 'TIMESTAMP'; // DuckDB supports TIMESTAMP natively\n case 'JSON':\n return 'JSON'; // DuckDB has native JSON type\n case 'UUID':\n return 'UUID'; // DuckDB has a native UUID type (R11)\n default:\n return super.mapType(type);\n }\n }\n\n /**\n * Generate CREATE INDEX statements\n *\n * CRITICAL: Skip UNIQUE indexes - they must be inline constraints for UPSERT\n */\n generateIndexes(schema: SchemaDefinition): string[] {\n const { tableName, indexes } = schema;\n const statements: string[] = [];\n\n for (const index of indexes) {\n // Skip UNIQUE indexes - they're inline constraints\n if (index.unique && !index.where && !index.jsonPath) {\n continue;\n }\n\n const target = renderIndexTarget(index, this.engine);\n if (!target) {\n continue;\n }\n const indexType = index.unique ? 'UNIQUE INDEX' : 'INDEX';\n let sql = `CREATE ${indexType} IF NOT EXISTS \"${index.name}\" ON \"${tableName}\" (${target})`;\n\n // DuckDB (and the JSON adapter backed by it) rejects partial indexes.\n // Match the migration differ's documented compatibility behavior by\n // degrading the declaration to a full index while preserving UNIQUE.\n\n sql += ';';\n statements.push(sql);\n }\n\n return statements;\n }\n\n /**\n * DuckDB does NOT support triggers\n */\n generateTriggers(_schema: SchemaDefinition): string[] {\n return []; // DuckDB doesn't support triggers\n }\n\n /**\n * DuckDB does NOT support triggers\n */\n supportsTriggers(): boolean {\n return false;\n }\n\n /**\n * CRITICAL: DuckDB requires inline UNIQUE for UPSERT\n *\n * DuckDB issue #12684: ON CONFLICT only works with inline UNIQUE constraints,\n * not with separate CREATE UNIQUE INDEX statements.\n */\n requiresInlineUnique(): boolean {\n return true;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,IAAa,iBAAb,cAAoC,gBAAgB;CAClD,SAAkC;;;;;CAMlC,QAAQ,MAA2B;EACjC,QAAQ,MAAR;GACE,KAAK,aACH,OAAO;GACT,KAAK,QACH,OAAO;GACT,KAAK,QACH,OAAO;GACT,SACE,OAAO,MAAM,QAAQ,IAAI;EAC7B;CACF;;;;;;CAOA,gBAAgB,QAAoC;EAClD,MAAM,EAAE,WAAW,YAAY;EAC/B,MAAM,aAAuB,CAAC;EAE9B,KAAK,MAAM,SAAS,SAAS;GAE3B,IAAI,MAAM,UAAU,CAAC,MAAM,SAAS,CAAC,MAAM,UACzC;GAGF,MAAM,SAAS,kBAAkB,OAAO,KAAK,MAAM;GACnD,IAAI,CAAC,QACH;GAGF,IAAI,MAAM,UADQ,MAAM,SAAS,iBAAiB,QACpB,kBAAkB,MAAM,KAAK,QAAQ,UAAU,KAAK,OAAO;GAMzF,OAAO;GACP,WAAW,KAAK,GAAG;EACrB;EAEA,OAAO;CACT;;;;CAKA,iBAAiB,SAAqC;EACpD,OAAO,CAAC;CACV;;;;CAKA,mBAA4B;EAC1B,OAAO;CACT;;;;;;;CAQA,uBAAgC;EAC9B,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"duckdb-strategy.js","names":[],"sources":["../../../src/schema/ddl/duckdb-strategy.ts"],"sourcesContent":["/**\n * DuckDB DDL Strategy\n *\n * DuckDB-specific DDL generation with the following characteristics:\n * - UNIQUE constraints MUST be inline for UPSERT to work (DuckDB issue #12684)\n * - Supports CAST in DEFAULT values\n * - Native BOOLEAN, JSON types\n * - Does NOT support triggers\n *\n * CRITICAL: DuckDB's ON CONFLICT clause only works with inline UNIQUE constraints,\n * not with separate UNIQUE indexes. This is a known DuckDB limitation.\n */\n\nimport { isStiSubtypeUniqueIndex, renderIndexTarget } from '../index-utils.js';\nimport type { SchemaDefinition, SQLDataType } from '../types.js';\nimport { BaseDDLStrategy } from './base-strategy.js';\nimport type { DatabaseEngine } from './types.js';\n\nexport class DuckDBStrategy extends BaseDDLStrategy {\n readonly engine: DatabaseEngine = 'duckdb';\n\n /**\n * Map types for DuckDB\n * DuckDB has native support for most types\n */\n mapType(type: SQLDataType): string {\n switch (type) {\n case 'TIMESTAMP':\n return 'TIMESTAMP'; // DuckDB supports TIMESTAMP natively\n case 'JSON':\n return 'JSON'; // DuckDB has native JSON type\n case 'UUID':\n return 'UUID'; // DuckDB has a native UUID type (R11)\n default:\n return super.mapType(type);\n }\n }\n\n /**\n * Generate CREATE INDEX statements\n *\n * CRITICAL: Skip UNIQUE indexes - they must be inline constraints for UPSERT\n */\n generateIndexes(schema: SchemaDefinition): string[] {\n const { tableName, indexes } = schema;\n const statements: string[] = [];\n\n for (const index of indexes) {\n // Skip UNIQUE indexes - they're inline constraints\n if (index.unique && !index.where && !index.jsonPath) {\n continue;\n }\n\n // An STI subtype-scoped UNIQUE (`unique: true` declared only on a\n // descendant, #2359) cannot degrade to a full index the way the other\n // partial indexes below do: dropping the predicate while keeping UNIQUE\n // would enforce one subtype's constraint across every sibling's rows in\n // the shared table. DuckDB has no faithful shape for it, so it is not\n // emitted here (nor by the migration differ on this engine) — the\n // constraint is simply not enforced on DuckDB/JSON. Caller-declared\n // partial uniques (`WHERE active = TRUE`) keep degrading to full UNIQUE.\n if (isStiSubtypeUniqueIndex(index)) {\n continue;\n }\n\n const target = renderIndexTarget(index, this.engine);\n if (!target) {\n continue;\n }\n const indexType = index.unique ? 'UNIQUE INDEX' : 'INDEX';\n let sql = `CREATE ${indexType} IF NOT EXISTS \"${index.name}\" ON \"${tableName}\" (${target})`;\n\n // DuckDB (and the JSON adapter backed by it) rejects partial indexes.\n // Match the migration differ's documented compatibility behavior by\n // degrading the declaration to a full index while preserving UNIQUE.\n\n sql += ';';\n statements.push(sql);\n }\n\n return statements;\n }\n\n /**\n * DuckDB does NOT support triggers\n */\n generateTriggers(_schema: SchemaDefinition): string[] {\n return []; // DuckDB doesn't support triggers\n }\n\n /**\n * DuckDB does NOT support triggers\n */\n supportsTriggers(): boolean {\n return false;\n }\n\n /**\n * CRITICAL: DuckDB requires inline UNIQUE for UPSERT\n *\n * DuckDB issue #12684: ON CONFLICT only works with inline UNIQUE constraints,\n * not with separate CREATE UNIQUE INDEX statements.\n */\n requiresInlineUnique(): boolean {\n return true;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,IAAa,iBAAb,cAAoC,gBAAgB;CAClD,SAAkC;;;;;CAMlC,QAAQ,MAA2B;EACjC,QAAQ,MAAR;GACE,KAAK,aACH,OAAO;GACT,KAAK,QACH,OAAO;GACT,KAAK,QACH,OAAO;GACT,SACE,OAAO,MAAM,QAAQ,IAAI;EAC7B;CACF;;;;;;CAOA,gBAAgB,QAAoC;EAClD,MAAM,EAAE,WAAW,YAAY;EAC/B,MAAM,aAAuB,CAAC;EAE9B,KAAK,MAAM,SAAS,SAAS;GAE3B,IAAI,MAAM,UAAU,CAAC,MAAM,SAAS,CAAC,MAAM,UACzC;GAWF,IAAI,wBAAwB,KAAK,GAC/B;GAGF,MAAM,SAAS,kBAAkB,OAAO,KAAK,MAAM;GACnD,IAAI,CAAC,QACH;GAGF,IAAI,MAAM,UADQ,MAAM,SAAS,iBAAiB,QACpB,kBAAkB,MAAM,KAAK,QAAQ,UAAU,KAAK,OAAO;GAMzF,OAAO;GACP,WAAW,KAAK,GAAG;EACrB;EAEA,OAAO;CACT;;;;CAKA,iBAAiB,SAAqC;EACpD,OAAO,CAAC;CACV;;;;CAKA,mBAA4B;EAC1B,OAAO;CACT;;;;;;;CAQA,uBAAgC;EAC9B,OAAO;CACT;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-strategy.d.ts","sourceRoot":"","sources":["../../../src/schema/ddl/postgres-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,qBAAa,gBAAiB,SAAQ,eAAe;IACnD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAc;IAE7C;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM;IAkBlC;;;;;OAKG;IACH,SAAS,CAAC,wBAAwB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,iBAAiB,GACzB,MAAM;IAkDT;;OAEG;IACH,gBAAgB,IAAI,OAAO;IAI3B;;;OAGG;IACH,oBAAoB,IAAI,OAAO;CAGhC"}
1
+ {"version":3,"file":"postgres-strategy.d.ts","sourceRoot":"","sources":["../../../src/schema/ddl/postgres-strategy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,qBAAa,gBAAiB,SAAQ,eAAe;IACnD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAc;IAE7C;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM;IAkBlC;;;;;OAKG;IACH,SAAS,CAAC,wBAAwB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,iBAAiB,GACzB,MAAM;IAsDT;;OAEG;IACH,gBAAgB,IAAI,OAAO;IAI3B;;;OAGG;IACH,oBAAoB,IAAI,OAAO;CAGhC"}
@@ -1,5 +1,16 @@
1
1
  import { BaseDDLStrategy } from "./base-strategy.js";
2
+ import { shortenIdentifier } from "../index-utils.js";
2
3
  //#region src/schema/ddl/postgres-strategy.ts
4
+ /**
5
+ * PostgreSQL DDL Strategy
6
+ *
7
+ * PostgreSQL-specific DDL generation with the following characteristics:
8
+ * - JSONB type for JSON columns (more efficient than JSON)
9
+ * - Native BOOLEAN type
10
+ * - Full trigger support with PL/pgSQL functions
11
+ * - UNIQUE can be inline or separate indexes
12
+ * - Supports CAST in DEFAULT values
13
+ */
3
14
  var PostgresStrategy = class extends BaseDDLStrategy {
4
15
  engine = "postgres";
5
16
  /**
@@ -24,7 +35,7 @@ var PostgresStrategy = class extends BaseDDLStrategy {
24
35
  */
25
36
  generateTriggerStatement(tableName, trigger) {
26
37
  if (trigger.name.includes("updated_at")) {
27
- const functionName = `update_${tableName}_updated_at`;
38
+ const functionName = shortenIdentifier(`update_${tableName}_updated_at`);
28
39
  let sql = `-- Function for ${trigger.name}\n`;
29
40
  sql += `CREATE OR REPLACE FUNCTION ${functionName}()\n`;
30
41
  sql += `RETURNS TRIGGER AS $$\n`;
@@ -1 +1 @@
1
- {"version":3,"file":"postgres-strategy.js","names":[],"sources":["../../../src/schema/ddl/postgres-strategy.ts"],"sourcesContent":["/**\n * PostgreSQL DDL Strategy\n *\n * PostgreSQL-specific DDL generation with the following characteristics:\n * - JSONB type for JSON columns (more efficient than JSON)\n * - Native BOOLEAN type\n * - Full trigger support with PL/pgSQL functions\n * - UNIQUE can be inline or separate indexes\n * - Supports CAST in DEFAULT values\n */\n\nimport type { SQLDataType, TriggerDefinition } from '../types.js';\nimport { BaseDDLStrategy } from './base-strategy.js';\nimport type { DatabaseEngine } from './types.js';\n\nexport class PostgresStrategy extends BaseDDLStrategy {\n readonly engine: DatabaseEngine = 'postgres';\n\n /**\n * Map types for PostgreSQL\n * - JSON → JSONB (more efficient, supports indexing)\n * - TIMESTAMP → TIMESTAMP WITH TIME ZONE (best practice)\n */\n mapType(type: SQLDataType): string {\n switch (type) {\n case 'JSON':\n return 'JSONB'; // PostgreSQL JSONB is more efficient\n case 'TIMESTAMP':\n // JavaScript Date represents an instant. Preserve that contract in\n // PostgreSQL instead of discarding the ISO offset in a timezone-naive\n // TIMESTAMP column and reinterpreting the wall time at hydration.\n return 'TIMESTAMPTZ';\n case 'REAL':\n return 'DOUBLE PRECISION'; // PostgreSQL convention\n case 'UUID':\n return 'uuid'; // Native 16-byte uuid (R11) — vs 37-byte text\n default:\n return super.mapType(type);\n }\n }\n\n /**\n * Generate trigger for PostgreSQL\n *\n * PostgreSQL triggers require a separate function, then the trigger.\n * For updated_at timestamps, we generate both.\n */\n protected generateTriggerStatement(\n tableName: string,\n trigger: TriggerDefinition,\n ): string {\n // For simple triggers, we need to create a function first\n // This is PostgreSQL's two-step trigger process\n\n // Check if this is an updated_at trigger\n if (trigger.name.includes('updated_at')) {\n // Generate the function and trigger for updated_at\n const functionName = `update_${tableName}_updated_at`;\n\n let sql = `-- Function for ${trigger.name}\\n`;\n sql += `CREATE OR REPLACE FUNCTION ${functionName}()\\n`;\n sql += `RETURNS TRIGGER AS $$\\n`;\n sql += `BEGIN\\n`;\n sql += ` NEW.updated_at = CURRENT_TIMESTAMP;\\n`;\n sql += ` RETURN NEW;\\n`;\n sql += `END;\\n`;\n sql += `$$ LANGUAGE plpgsql;\\n\\n`;\n\n sql += `-- Trigger\\n`;\n sql += `DROP TRIGGER IF EXISTS \"${trigger.name}\" ON \"${tableName}\";\\n`;\n sql += `CREATE TRIGGER \"${trigger.name}\"\\n`;\n sql += `${trigger.when} ${trigger.event} ON \"${tableName}\"\\n`;\n sql += `FOR EACH ROW\\n`;\n sql += `EXECUTE FUNCTION ${functionName}();`;\n\n return sql;\n }\n\n // For custom triggers, use the provided body\n let sql = `DROP TRIGGER IF EXISTS \"${trigger.name}\" ON \"${tableName}\";\\n`;\n sql += `CREATE TRIGGER \"${trigger.name}\"\\n`;\n sql += `${trigger.when} ${trigger.event} ON \"${tableName}\"\\n`;\n sql += `FOR EACH ROW\\n`;\n\n if (trigger.condition) {\n sql += `WHEN (${trigger.condition})\\n`;\n }\n\n // PostgreSQL triggers need EXECUTE FUNCTION or EXECUTE PROCEDURE\n // If body contains a function call, use it directly\n if (trigger.body.includes('EXECUTE')) {\n sql += trigger.body;\n } else {\n // Wrap in anonymous DO block or assume it's a function name\n sql += `EXECUTE FUNCTION ${trigger.body}`;\n }\n\n return `${sql};`;\n }\n\n /**\n * PostgreSQL supports triggers\n */\n supportsTriggers(): boolean {\n return true;\n }\n\n /**\n * PostgreSQL doesn't require inline UNIQUE for UPSERT\n * Both inline and separate indexes work with ON CONFLICT\n */\n requiresInlineUnique(): boolean {\n return false;\n }\n}\n"],"mappings":";;AAeA,IAAa,mBAAb,cAAsC,gBAAgB;CACpD,SAAkC;;;;;;CAOlC,QAAQ,MAA2B;EACjC,QAAQ,MAAR;GACE,KAAK,QACH,OAAO;GACT,KAAK,aAIH,OAAO;GACT,KAAK,QACH,OAAO;GACT,KAAK,QACH,OAAO;GACT,SACE,OAAO,MAAM,QAAQ,IAAI;EAC7B;CACF;;;;;;;CAQA,yBACE,WACA,SACQ;EAKR,IAAI,QAAQ,KAAK,SAAS,YAAY,GAAG;GAEvC,MAAM,eAAe,UAAU,UAAU;GAEzC,IAAI,MAAM,mBAAmB,QAAQ,KAAK;GAC1C,OAAO,8BAA8B,aAAa;GAClD,OAAO;GACP,OAAO;GACP,OAAO;GACP,OAAO;GACP,OAAO;GACP,OAAO;GAEP,OAAO;GACP,OAAO,2BAA2B,QAAQ,KAAK,QAAQ,UAAU;GACjE,OAAO,mBAAmB,QAAQ,KAAK;GACvC,OAAO,GAAG,QAAQ,KAAK,GAAG,QAAQ,MAAM,OAAO,UAAU;GACzD,OAAO;GACP,OAAO,oBAAoB,aAAa;GAExC,OAAO;EACT;EAGA,IAAI,MAAM,2BAA2B,QAAQ,KAAK,QAAQ,UAAU;EACpE,OAAO,mBAAmB,QAAQ,KAAK;EACvC,OAAO,GAAG,QAAQ,KAAK,GAAG,QAAQ,MAAM,OAAO,UAAU;EACzD,OAAO;EAEP,IAAI,QAAQ,WACV,OAAO,SAAS,QAAQ,UAAU;EAKpC,IAAI,QAAQ,KAAK,SAAS,SAAS,GACjC,OAAO,QAAQ;OAGf,OAAO,oBAAoB,QAAQ;EAGrC,OAAO,GAAG,IAAI;CAChB;;;;CAKA,mBAA4B;EAC1B,OAAO;CACT;;;;;CAMA,uBAAgC;EAC9B,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"postgres-strategy.js","names":[],"sources":["../../../src/schema/ddl/postgres-strategy.ts"],"sourcesContent":["/**\n * PostgreSQL DDL Strategy\n *\n * PostgreSQL-specific DDL generation with the following characteristics:\n * - JSONB type for JSON columns (more efficient than JSON)\n * - Native BOOLEAN type\n * - Full trigger support with PL/pgSQL functions\n * - UNIQUE can be inline or separate indexes\n * - Supports CAST in DEFAULT values\n */\n\nimport { shortenIdentifier } from '../index-utils.js';\nimport type { SQLDataType, TriggerDefinition } from '../types.js';\nimport { BaseDDLStrategy } from './base-strategy.js';\nimport type { DatabaseEngine } from './types.js';\n\nexport class PostgresStrategy extends BaseDDLStrategy {\n readonly engine: DatabaseEngine = 'postgres';\n\n /**\n * Map types for PostgreSQL\n * - JSON → JSONB (more efficient, supports indexing)\n * - TIMESTAMP → TIMESTAMP WITH TIME ZONE (best practice)\n */\n mapType(type: SQLDataType): string {\n switch (type) {\n case 'JSON':\n return 'JSONB'; // PostgreSQL JSONB is more efficient\n case 'TIMESTAMP':\n // JavaScript Date represents an instant. Preserve that contract in\n // PostgreSQL instead of discarding the ISO offset in a timezone-naive\n // TIMESTAMP column and reinterpreting the wall time at hydration.\n return 'TIMESTAMPTZ';\n case 'REAL':\n return 'DOUBLE PRECISION'; // PostgreSQL convention\n case 'UUID':\n return 'uuid'; // Native 16-byte uuid (R11) — vs 37-byte text\n default:\n return super.mapType(type);\n }\n }\n\n /**\n * Generate trigger for PostgreSQL\n *\n * PostgreSQL triggers require a separate function, then the trigger.\n * For updated_at timestamps, we generate both.\n */\n protected generateTriggerStatement(\n tableName: string,\n trigger: TriggerDefinition,\n ): string {\n // For simple triggers, we need to create a function first\n // This is PostgreSQL's two-step trigger process\n\n // Check if this is an updated_at trigger\n if (trigger.name.includes('updated_at')) {\n // Generate the function and trigger for updated_at. The function name is\n // a PostgreSQL identifier like any other, so it is subject to the same\n // 63-byte limit — and unlike an index it is *referenced* by the trigger\n // below, so a truncation that CREATE FUNCTION accepts would leave\n // EXECUTE FUNCTION pointing at a name PostgreSQL never stored (#2374).\n const functionName = shortenIdentifier(`update_${tableName}_updated_at`);\n\n let sql = `-- Function for ${trigger.name}\\n`;\n sql += `CREATE OR REPLACE FUNCTION ${functionName}()\\n`;\n sql += `RETURNS TRIGGER AS $$\\n`;\n sql += `BEGIN\\n`;\n sql += ` NEW.updated_at = CURRENT_TIMESTAMP;\\n`;\n sql += ` RETURN NEW;\\n`;\n sql += `END;\\n`;\n sql += `$$ LANGUAGE plpgsql;\\n\\n`;\n\n sql += `-- Trigger\\n`;\n sql += `DROP TRIGGER IF EXISTS \"${trigger.name}\" ON \"${tableName}\";\\n`;\n sql += `CREATE TRIGGER \"${trigger.name}\"\\n`;\n sql += `${trigger.when} ${trigger.event} ON \"${tableName}\"\\n`;\n sql += `FOR EACH ROW\\n`;\n sql += `EXECUTE FUNCTION ${functionName}();`;\n\n return sql;\n }\n\n // For custom triggers, use the provided body\n let sql = `DROP TRIGGER IF EXISTS \"${trigger.name}\" ON \"${tableName}\";\\n`;\n sql += `CREATE TRIGGER \"${trigger.name}\"\\n`;\n sql += `${trigger.when} ${trigger.event} ON \"${tableName}\"\\n`;\n sql += `FOR EACH ROW\\n`;\n\n if (trigger.condition) {\n sql += `WHEN (${trigger.condition})\\n`;\n }\n\n // PostgreSQL triggers need EXECUTE FUNCTION or EXECUTE PROCEDURE\n // If body contains a function call, use it directly\n if (trigger.body.includes('EXECUTE')) {\n sql += trigger.body;\n } else {\n // Wrap in anonymous DO block or assume it's a function name\n sql += `EXECUTE FUNCTION ${trigger.body}`;\n }\n\n return `${sql};`;\n }\n\n /**\n * PostgreSQL supports triggers\n */\n supportsTriggers(): boolean {\n return true;\n }\n\n /**\n * PostgreSQL doesn't require inline UNIQUE for UPSERT\n * Both inline and separate indexes work with ON CONFLICT\n */\n requiresInlineUnique(): boolean {\n return false;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAa,mBAAb,cAAsC,gBAAgB;CACpD,SAAkC;;;;;;CAOlC,QAAQ,MAA2B;EACjC,QAAQ,MAAR;GACE,KAAK,QACH,OAAO;GACT,KAAK,aAIH,OAAO;GACT,KAAK,QACH,OAAO;GACT,KAAK,QACH,OAAO;GACT,SACE,OAAO,MAAM,QAAQ,IAAI;EAC7B;CACF;;;;;;;CAQA,yBACE,WACA,SACQ;EAKR,IAAI,QAAQ,KAAK,SAAS,YAAY,GAAG;GAMvC,MAAM,eAAe,kBAAkB,UAAU,UAAU,YAAY;GAEvE,IAAI,MAAM,mBAAmB,QAAQ,KAAK;GAC1C,OAAO,8BAA8B,aAAa;GAClD,OAAO;GACP,OAAO;GACP,OAAO;GACP,OAAO;GACP,OAAO;GACP,OAAO;GAEP,OAAO;GACP,OAAO,2BAA2B,QAAQ,KAAK,QAAQ,UAAU;GACjE,OAAO,mBAAmB,QAAQ,KAAK;GACvC,OAAO,GAAG,QAAQ,KAAK,GAAG,QAAQ,MAAM,OAAO,UAAU;GACzD,OAAO;GACP,OAAO,oBAAoB,aAAa;GAExC,OAAO;EACT;EAGA,IAAI,MAAM,2BAA2B,QAAQ,KAAK,QAAQ,UAAU;EACpE,OAAO,mBAAmB,QAAQ,KAAK;EACvC,OAAO,GAAG,QAAQ,KAAK,GAAG,QAAQ,MAAM,OAAO,UAAU;EACzD,OAAO;EAEP,IAAI,QAAQ,WACV,OAAO,SAAS,QAAQ,UAAU;EAKpC,IAAI,QAAQ,KAAK,SAAS,SAAS,GACjC,OAAO,QAAQ;OAGf,OAAO,oBAAoB,QAAQ;EAGrC,OAAO,GAAG,IAAI;CAChB;;;;CAKA,mBAA4B;EAC1B,OAAO;CACT;;;;;CAMA,uBAAgC;EAC9B,OAAO;CACT;AACF"}