@metaobjectsdev/codegen-ts 0.24.5 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +37 -12
  2. package/dist/api-surface.d.ts +29 -0
  3. package/dist/api-surface.d.ts.map +1 -1
  4. package/dist/api-surface.js +47 -0
  5. package/dist/api-surface.js.map +1 -1
  6. package/dist/column-mapper.d.ts +12 -0
  7. package/dist/column-mapper.d.ts.map +1 -1
  8. package/dist/column-mapper.js +1 -0
  9. package/dist/column-mapper.js.map +1 -1
  10. package/dist/constants.d.ts +24 -11
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +54 -19
  13. package/dist/constants.js.map +1 -1
  14. package/dist/db-emitting.d.ts +6 -0
  15. package/dist/db-emitting.d.ts.map +1 -0
  16. package/dist/db-emitting.js +26 -0
  17. package/dist/db-emitting.js.map +1 -0
  18. package/dist/generator-registry.d.ts.map +1 -1
  19. package/dist/generator-registry.js +14 -1
  20. package/dist/generator-registry.js.map +1 -1
  21. package/dist/generator.d.ts +5 -0
  22. package/dist/generator.d.ts.map +1 -1
  23. package/dist/generator.js.map +1 -1
  24. package/dist/generators/agent-docs-file.d.ts +19 -0
  25. package/dist/generators/agent-docs-file.d.ts.map +1 -0
  26. package/dist/generators/agent-docs-file.js +257 -0
  27. package/dist/generators/agent-docs-file.js.map +1 -0
  28. package/dist/generators/agent-requirements-page.d.ts +22 -0
  29. package/dist/generators/agent-requirements-page.d.ts.map +1 -0
  30. package/dist/generators/agent-requirements-page.js +175 -0
  31. package/dist/generators/agent-requirements-page.js.map +1 -0
  32. package/dist/generators/agent-schema-input.d.ts +69 -0
  33. package/dist/generators/agent-schema-input.d.ts.map +1 -0
  34. package/dist/generators/agent-schema-input.js +23 -0
  35. package/dist/generators/agent-schema-input.js.map +1 -0
  36. package/dist/generators/agent-schema-page.d.ts +21 -0
  37. package/dist/generators/agent-schema-page.d.ts.map +1 -0
  38. package/dist/generators/agent-schema-page.js +241 -0
  39. package/dist/generators/agent-schema-page.js.map +1 -0
  40. package/dist/generators/agent-ui-page.d.ts +22 -0
  41. package/dist/generators/agent-ui-page.d.ts.map +1 -0
  42. package/dist/generators/agent-ui-page.js +253 -0
  43. package/dist/generators/agent-ui-page.js.map +1 -0
  44. package/dist/generators/api-docs-file.d.ts.map +1 -1
  45. package/dist/generators/api-docs-file.js +13 -6
  46. package/dist/generators/api-docs-file.js.map +1 -1
  47. package/dist/generators/api-model.d.ts +6 -0
  48. package/dist/generators/api-model.d.ts.map +1 -1
  49. package/dist/generators/api-model.js +54 -35
  50. package/dist/generators/api-model.js.map +1 -1
  51. package/dist/generators/callable-file.js +1 -1
  52. package/dist/generators/callable-file.js.map +1 -1
  53. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  54. package/dist/generators/docs-data-builder.js +47 -2
  55. package/dist/generators/docs-data-builder.js.map +1 -1
  56. package/dist/generators/docs-data.d.ts +6 -2
  57. package/dist/generators/docs-data.d.ts.map +1 -1
  58. package/dist/generators/index.d.ts +3 -8
  59. package/dist/generators/index.d.ts.map +1 -1
  60. package/dist/generators/index.js +19 -14
  61. package/dist/generators/index.js.map +1 -1
  62. package/dist/generators/names-file.d.ts +17 -0
  63. package/dist/generators/names-file.d.ts.map +1 -0
  64. package/dist/generators/names-file.js +95 -0
  65. package/dist/generators/names-file.js.map +1 -0
  66. package/dist/generators/requirements-markdown.d.ts +18 -1
  67. package/dist/generators/requirements-markdown.d.ts.map +1 -1
  68. package/dist/generators/requirements-markdown.js +11 -9
  69. package/dist/generators/requirements-markdown.js.map +1 -1
  70. package/dist/generators/routes-file-hono.d.ts +17 -2
  71. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  72. package/dist/generators/routes-file-hono.js +9 -11
  73. package/dist/generators/routes-file-hono.js.map +1 -1
  74. package/dist/generators/routes-file.d.ts +17 -2
  75. package/dist/generators/routes-file.d.ts.map +1 -1
  76. package/dist/generators/routes-file.js +10 -8
  77. package/dist/generators/routes-file.js.map +1 -1
  78. package/dist/generators/template-generator.d.ts.map +1 -1
  79. package/dist/generators/template-generator.js +20 -6
  80. package/dist/generators/template-generator.js.map +1 -1
  81. package/dist/index.d.ts +20 -5
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +43 -4
  84. package/dist/index.js.map +1 -1
  85. package/dist/metaobjects-config.d.ts +38 -1
  86. package/dist/metaobjects-config.d.ts.map +1 -1
  87. package/dist/metaobjects-config.js +9 -1
  88. package/dist/metaobjects-config.js.map +1 -1
  89. package/dist/names.d.ts +275 -0
  90. package/dist/names.d.ts.map +1 -0
  91. package/dist/names.js +433 -0
  92. package/dist/names.js.map +1 -0
  93. package/dist/overwrite-policy.d.ts.map +1 -1
  94. package/dist/overwrite-policy.js +15 -3
  95. package/dist/overwrite-policy.js.map +1 -1
  96. package/dist/projection/extract-view-spec.d.ts +25 -1
  97. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  98. package/dist/projection/extract-view-spec.js +79 -6
  99. package/dist/projection/extract-view-spec.js.map +1 -1
  100. package/dist/reference-templates.d.ts +1 -1
  101. package/dist/reference-templates.d.ts.map +1 -1
  102. package/dist/reference-templates.js +1 -1
  103. package/dist/reference-templates.js.map +1 -1
  104. package/dist/render-context.d.ts +12 -1
  105. package/dist/render-context.d.ts.map +1 -1
  106. package/dist/render-context.js +1 -0
  107. package/dist/render-context.js.map +1 -1
  108. package/dist/retired-codegen-attrs.d.ts +32 -0
  109. package/dist/retired-codegen-attrs.d.ts.map +1 -0
  110. package/dist/retired-codegen-attrs.js +47 -0
  111. package/dist/retired-codegen-attrs.js.map +1 -0
  112. package/dist/routes-expose.d.ts +63 -0
  113. package/dist/routes-expose.d.ts.map +1 -0
  114. package/dist/routes-expose.js +57 -0
  115. package/dist/routes-expose.js.map +1 -0
  116. package/dist/runner.d.ts +18 -0
  117. package/dist/runner.d.ts.map +1 -1
  118. package/dist/runner.js +109 -7
  119. package/dist/runner.js.map +1 -1
  120. package/dist/templates/callable-file.d.ts +7 -2
  121. package/dist/templates/callable-file.d.ts.map +1 -1
  122. package/dist/templates/callable-file.js +54 -9
  123. package/dist/templates/callable-file.js.map +1 -1
  124. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  125. package/dist/templates/drizzle-schema.js +236 -65
  126. package/dist/templates/drizzle-schema.js.map +1 -1
  127. package/dist/templates/entity-constants.d.ts +15 -6
  128. package/dist/templates/entity-constants.d.ts.map +1 -1
  129. package/dist/templates/entity-constants.js +85 -161
  130. package/dist/templates/entity-constants.js.map +1 -1
  131. package/dist/templates/entity-file.d.ts.map +1 -1
  132. package/dist/templates/entity-file.js +38 -4
  133. package/dist/templates/entity-file.js.map +1 -1
  134. package/dist/templates/entity-meta-file.d.ts +15 -1
  135. package/dist/templates/entity-meta-file.d.ts.map +1 -1
  136. package/dist/templates/entity-meta-file.js +12 -2
  137. package/dist/templates/entity-meta-file.js.map +1 -1
  138. package/dist/templates/entity-ui-descriptor.d.ts +112 -0
  139. package/dist/templates/entity-ui-descriptor.d.ts.map +1 -0
  140. package/dist/templates/entity-ui-descriptor.js +238 -0
  141. package/dist/templates/entity-ui-descriptor.js.map +1 -0
  142. package/dist/templates/field-meta.d.ts +46 -9
  143. package/dist/templates/field-meta.d.ts.map +1 -1
  144. package/dist/templates/field-meta.js +95 -21
  145. package/dist/templates/field-meta.js.map +1 -1
  146. package/dist/templates/names-decl.d.ts +38 -0
  147. package/dist/templates/names-decl.d.ts.map +1 -0
  148. package/dist/templates/names-decl.js +104 -0
  149. package/dist/templates/names-decl.js.map +1 -0
  150. package/dist/templates/projection-decl.d.ts +21 -0
  151. package/dist/templates/projection-decl.d.ts.map +1 -1
  152. package/dist/templates/projection-decl.js +53 -29
  153. package/dist/templates/projection-decl.js.map +1 -1
  154. package/dist/templates/queries-file.d.ts.map +1 -1
  155. package/dist/templates/queries-file.js +17 -2
  156. package/dist/templates/queries-file.js.map +1 -1
  157. package/dist/templates/requirement-test.d.ts.map +1 -1
  158. package/dist/templates/requirement-test.js +13 -2
  159. package/dist/templates/requirement-test.js.map +1 -1
  160. package/dist/templates/routes-file-hono.d.ts +2 -1
  161. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  162. package/dist/templates/routes-file-hono.js +11 -2
  163. package/dist/templates/routes-file-hono.js.map +1 -1
  164. package/dist/templates/routes-file.d.ts +2 -1
  165. package/dist/templates/routes-file.d.ts.map +1 -1
  166. package/dist/templates/routes-file.js +54 -21
  167. package/dist/templates/routes-file.js.map +1 -1
  168. package/dist/templates/value-object-file.d.ts.map +1 -1
  169. package/dist/templates/value-object-file.js +15 -1
  170. package/dist/templates/value-object-file.js.map +1 -1
  171. package/dist/templates/view-decl.d.ts +37 -1
  172. package/dist/templates/view-decl.d.ts.map +1 -1
  173. package/dist/templates/view-decl.js +27 -3
  174. package/dist/templates/view-decl.js.map +1 -1
  175. package/dist/templates/zod-validators.d.ts +21 -0
  176. package/dist/templates/zod-validators.d.ts.map +1 -1
  177. package/dist/templates/zod-validators.js +40 -5
  178. package/dist/templates/zod-validators.js.map +1 -1
  179. package/dist/view-context.d.ts +24 -0
  180. package/dist/view-context.d.ts.map +1 -0
  181. package/dist/view-context.js +81 -0
  182. package/dist/view-context.js.map +1 -0
  183. package/package.json +6 -6
  184. package/src/api-surface.ts +53 -0
  185. package/src/column-mapper.ts +13 -0
  186. package/src/constants.ts +91 -16
  187. package/src/db-emitting.ts +31 -0
  188. package/src/generator-registry.ts +14 -4
  189. package/src/generator.ts +5 -0
  190. package/src/generators/agent-docs-file.ts +297 -0
  191. package/src/generators/agent-requirements-page.ts +199 -0
  192. package/src/generators/agent-schema-input.ts +88 -0
  193. package/src/generators/agent-schema-page.ts +297 -0
  194. package/src/generators/agent-ui-page.ts +290 -0
  195. package/src/generators/api-docs-file.ts +13 -6
  196. package/src/generators/api-model.ts +63 -34
  197. package/src/generators/callable-file.ts +1 -1
  198. package/src/generators/docs-data-builder.ts +46 -2
  199. package/src/generators/docs-data.ts +6 -2
  200. package/src/generators/index.ts +25 -14
  201. package/src/generators/names-file.ts +98 -0
  202. package/src/generators/requirements-markdown.ts +36 -2
  203. package/src/generators/routes-file-hono.ts +19 -10
  204. package/src/generators/routes-file.ts +20 -7
  205. package/src/generators/template-generator.ts +20 -6
  206. package/src/index.ts +64 -4
  207. package/src/metaobjects-config.ts +48 -2
  208. package/src/names.ts +629 -0
  209. package/src/overwrite-policy.ts +15 -3
  210. package/src/projection/extract-view-spec.ts +85 -6
  211. package/src/reference/entity.ts +21 -2
  212. package/src/reference/names.ts +125 -0
  213. package/src/reference/queries.ts +19 -3
  214. package/src/reference/routes-hono.ts +19 -9
  215. package/src/reference/routes.ts +25 -9
  216. package/src/reference-templates.ts +1 -1
  217. package/src/render-context.ts +13 -1
  218. package/src/retired-codegen-attrs.ts +52 -0
  219. package/src/routes-expose.ts +86 -0
  220. package/src/runner.ts +122 -11
  221. package/src/templates/callable-file.ts +60 -8
  222. package/src/templates/drizzle-schema.ts +248 -60
  223. package/src/templates/entity-constants.ts +95 -184
  224. package/src/templates/entity-file.ts +46 -4
  225. package/src/templates/entity-meta-file.ts +16 -3
  226. package/src/templates/entity-ui-descriptor.ts +305 -0
  227. package/src/templates/field-meta.ts +100 -19
  228. package/src/templates/names-decl.ts +165 -0
  229. package/src/templates/projection-decl.ts +72 -31
  230. package/src/templates/queries-file.ts +17 -2
  231. package/src/templates/requirement-test.ts +13 -2
  232. package/src/templates/routes-file-hono.ts +14 -2
  233. package/src/templates/routes-file.ts +69 -24
  234. package/src/templates/value-object-file.ts +15 -1
  235. package/src/templates/view-decl.ts +53 -3
  236. package/src/templates/zod-validators.ts +41 -5
  237. package/src/view-context.ts +88 -0
@@ -3,10 +3,11 @@
3
3
  // plus the relations() block auto-emitted at the end.
4
4
 
5
5
  import { code, imp, joinCode, type Code } from "ts-poet";
6
- import { MetaObject, MetaField, MetaIndex, stripPackage } from "@metaobjectsdev/metadata";
6
+ import { MetaObject, MetaField, MetaIndex, isMetaObject, stripPackage } from "@metaobjectsdev/metadata";
7
7
  import {
8
8
  FIELD_SUBTYPE_LONG,
9
9
  IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION,
10
+ IDENTITY_ATTR_EXPR, IDENTITY_ATTR_USING, IDENTITY_ATTR_ORDERS, IDENTITY_ATTR_WHERE,
10
11
  GENERATION_INCREMENT, GENERATION_UUID,
11
12
  FIELD_ATTR_AUTO_SET,
12
13
  FIELD_ATTR_OBJECT_REF,
@@ -14,7 +15,11 @@ import {
14
15
  import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
15
16
  import { crossEntitySpecifier, valueObjectModuleSpecifier } from "../import-path.js";
16
17
  import { mapColumnType, type ColumnSpec, type EnumIntCustomType } from "../column-mapper.js";
17
- import { tableNameFromEntity, columnNameFromField } from "../naming.js";
18
+ import { tableNameFromEntity } from "../naming.js";
19
+ import {
20
+ namesRef, physicalNameExpr, sourceSchemaExpr, indexNameExpr, columnExpr,
21
+ } from "../names.js";
22
+ import { resolveTableSchema } from "@metaobjectsdev/metadata";
18
23
  import { renderRelationsBlock } from "./relations-block.js";
19
24
  import { renderDocsFor } from "./jsdoc.js";
20
25
  import { collectTphSubtypeFields } from "./tph-discriminator.js";
@@ -36,6 +41,81 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
36
41
  const tableName = obj.dbTable ?? tableNameFromEntity(obj.name, ctx.columnNamingStrategy);
37
42
  const varName = ctx.collectionName(obj.name);
38
43
 
44
+ // §A6/§B2 — reference `<Entity>Names` instead of embedding the physical names a SECOND
45
+ // time. `namesRef` is undefined whenever the names generator is not active in this run
46
+ // (opt-in under ADR-0034 scaffold-and-own — an unconditional import would break every
47
+ // project that has not enabled it) or the object has no primary source — both PRESENCE
48
+ // guards. No per-site equality check: whenever the constant exists, it IS the name;
49
+ // `primaryRdbSource` (@metaobjectsdev/metadata) already refuses (throws) any object
50
+ // whose `@role: primary` sources disagree on a name, and BOTH branches reach it — the
51
+ // constant through `resolveObjectNames`, and `obj.dbTable` above on its own — so this
52
+ // file never has to re-check, and the literal arm is refused too rather than silently
53
+ // binding the inherited parent's table when the names generator is out of the run.
54
+ const names = namesRef(obj, ctx);
55
+ const tableNameExpr: Code = physicalNameExpr(names, tableName, obj);
56
+
57
+ // @schema — the table binding must land in the SAME schema the migration creates the table
58
+ // in. migrate-ts qualifies every table, view, index and FK it emits (`CREATE TABLE
59
+ // "sales"."widgets"`), and under ADR-0015 it owns where a table lives; codegen dropping the
60
+ // schema meant generated queries hit `public.widgets` while the migration built
61
+ // `sales.widgets`. Whether that surfaced depended entirely on the adopter's `search_path`,
62
+ // which is the worst kind of bug — it works on the machine that has it configured.
63
+ //
64
+ // Drizzle expresses this with a DIFFERENT call shape rather than a third argument:
65
+ // `pgSchema("sales").table(name, …)`. It is built inline rather than hoisted to a `const`
66
+ // so two tables in one file sharing a schema cannot collide on a generated identifier; a
67
+ // schema object is just a descriptor, so a second call costs nothing.
68
+ //
69
+ // sqlite/d1 are excluded because SQLite has no schema concept at all — migrate already
70
+ // REFUSES a non-default @schema there (expected-schema.ts), so there is nothing to honour
71
+ // and no divergence to close.
72
+ const tableSchema = dialect === "sqlite" ? undefined : resolveTableSchema(obj);
73
+ const schemaExpr: Code | undefined =
74
+ tableSchema === undefined ? undefined
75
+ : sourceSchemaExpr(names) ?? code`${JSON.stringify(tableSchema)}`;
76
+ const tableCall: Code = schemaExpr === undefined
77
+ ? code`${tableFnSym}`
78
+ : code`${imp(`pgSchema@${importModule}`)}(${schemaExpr}).table`;
79
+ // A column's constant, on the same terms — but resolved against the entity that DECLARES
80
+ // the field, which is not always `obj`. Under TPH the fold below emits a subtype's own
81
+ // columns into this base's table, and those columns live in the SUBTYPE's names artifact;
82
+ // looking them up in the base's finds nothing.
83
+ //
84
+ // This used to pass `names` for every column and call the resulting miss "normal". It was
85
+ // not: the base's artifact genuinely does not carry a subtype's column, so every TPH
86
+ // subtype column fell through to the literal while `<Sub>Names.fields.<f>.column` sat
87
+ // right there, already imported by the subtype's own artifact. C# never had the bug
88
+ // because its emitter passes the subtype entity to `ColumnRef`; this is that shape.
89
+ //
90
+ // `fromPackage` stays `obj.package` so the import path is computed relative to the file
91
+ // being emitted (this base's), not to the subtype's own package.
92
+ // THIS entity's artifact first. It already carries every INHERITED field — a concrete
93
+ // entity's artifact spreads its abstract parent's `fields` — so an `extends` chain needs
94
+ // no redirect, and taking one would be a regression: an abstract base has no source, so
95
+ // it has no artifact of its own to redirect TO, and the column would fall back to a
96
+ // literal that `obj`'s artifact was carrying all along.
97
+ //
98
+ // A miss here means the field belongs to a different entity ENTIRELY, which under TPH is
99
+ // exactly the subtype whose columns this base's table absorbs. Only then resolve against
100
+ // the declaring entity, found through the field's own parent link.
101
+ //
102
+ // `isMetaObject`, never `instanceof`: two physical copies of @metaobjectsdev/metadata in
103
+ // one process give the class object and the instance different identities, so the check
104
+ // would return false for a real node — silently.
105
+ const columnNameExpr = (field: MetaField, dbName: string): Code => {
106
+ if (names === undefined || names.resolved.fields[field.name] !== undefined) {
107
+ return columnExpr(names, field.name, dbName);
108
+ }
109
+ const owner = field.parent;
110
+ // `fromPackage` is obj's: the import path is relative to the file being emitted (this
111
+ // base's), not to the subtype's own package.
112
+ const ownerNames =
113
+ owner !== undefined && isMetaObject(owner) && owner !== obj
114
+ ? namesRef(owner, ctx, obj.package)
115
+ : undefined;
116
+ return columnExpr(ownerNames, field.name, dbName);
117
+ };
118
+
39
119
  const primary = obj.primaryIdentity();
40
120
  const rawPkFields = primary?.attr(IDENTITY_ATTR_FIELDS);
41
121
  const pkFieldsList: string[] = Array.isArray(rawPkFields)
@@ -52,20 +132,58 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
52
132
 
53
133
  // Collect secondary identities and the field names that need .unique() on
54
134
  // their column. identity.secondary is ALWAYS unique (no @unique attr) — only
55
- // single-column identities propagate .unique() to the column; multi-column ones
56
- // emit a callback uniqueIndex() entry instead.
57
- // index.lookup is non-unique and must NOT stamp .unique() on its columns.
135
+ // An `identity.secondary` reaches the table ONLY as the `uniqueIndex(...)` callback
136
+ // emitted further down — never additionally as `.unique()` on its column.
137
+ //
138
+ // It used to do both for a single-column identity, under a comment describing an
139
+ // either/or ("single-column identities propagate .unique() to the column; multi-column
140
+ // ones emit a callback uniqueIndex() entry instead") that the code did not implement:
141
+ // the callback loop has no column-count condition, so a one-column secondary got a
142
+ // column-level unique AND a table-level unique index. `migrate-ts` emits only the index
143
+ // for that shape — it mirrors a `<table>_<col>_unique` constraint for `@unique` fields
144
+ // and nothing extra for a secondary — so the generated schema declared a constraint the
145
+ // database does not have, and `drizzle-kit push` run against it proposes creating one.
146
+ //
147
+ // `.unique()` still comes from the FIELD's own `@unique`, via the column mapper, which
148
+ // is exactly the arm migrate mirrors.
58
149
  const secondaryIdentities = obj.secondaryIdentities();
59
- const uniqueFieldNames = new Set<string>();
60
- for (const sec of secondaryIdentities) {
61
- const fields = sec.attr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
62
- if (!Array.isArray(fields) || fields.length !== 1) continue; // multi-col uniques use a callback uniqueIndex, not a column flag
63
- uniqueFieldNames.add(fields[0]!);
64
- }
65
150
 
66
151
  const columnLines: Code[] = [];
67
152
  // Collect CHECK constraints for enum columns; emitted as table-level check() callbacks.
68
- const checkConstraints: Array<{ name: string; expr: string }> = [];
153
+ const checkConstraints: Array<{ name: Code; expr: Code }> = [];
154
+ /**
155
+ * One CHECK entry, composed against `<Entity>Names` when the artifact is in the run.
156
+ *
157
+ * #293 — migrate's `<table>_<col>_chk` is the shared convention and it is the AUTHORITY:
158
+ * its suffix form is systematic across five constraint kinds and those names are already
159
+ * in live databases, so flipping migrate instead would emit DROP/ADD CONSTRAINT churn
160
+ * against production for a cosmetic difference. Referencing the constants therefore
161
+ * composes the IDENTICAL string at runtime rather than changing the convention — the
162
+ * template literal below evaluates to exactly what the literal arm spells.
163
+ *
164
+ * The expression's column goes through `sql.raw`, not `sql.identifier`: the latter QUOTES
165
+ * the name, which changes the constraint text migrate compares against.
166
+ */
167
+ const checkEntry = (field: MetaField, spec: ColumnSpec): { name: Code; expr: Code } => {
168
+ const colExpr = columnNameExpr(field, spec.dbName);
169
+ const values = spec.checkConstraintValues;
170
+ const sqlSym = imp("sql@drizzle-orm");
171
+ // No artifact in this run (the documented ADR-0034 opt-out) — spell both halves, which
172
+ // is what this file did unconditionally before.
173
+ if (names === undefined || values === undefined) {
174
+ return {
175
+ name: code`${JSON.stringify(`${tableName}_${spec.dbName}_chk`)}`,
176
+ expr: code`${spec.checkConstraint ?? ""}`,
177
+ };
178
+ }
179
+ // `\${` emits a literal `${` into the generated source: the name becomes a template
180
+ // literal and the expression an interpolation, both evaluated at run time in the
181
+ // consumer's code, where they produce the identical strings the literal arm spells.
182
+ return {
183
+ name: code`\`\${${tableNameExpr}}_\${${colExpr}}_chk\``,
184
+ expr: code`\${${sqlSym}.raw(${colExpr})} IN (${values})`,
185
+ };
186
+ };
69
187
  // Int-backed field.enum customType helpers, emitted ahead of the table. Keyed by
70
188
  // const name so a shared enum used by two fields of the SAME entity emits once.
71
189
  const enumIntTypes = new Map<string, EnumIntCustomType>();
@@ -76,7 +194,6 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
76
194
  // no longer creates.
77
195
  if (child.isDerived()) continue;
78
196
  const isPk = pkFieldNames.has(child.name);
79
- const isUnique = uniqueFieldNames.has(child.name) && !isPk;
80
197
  const fkInfo = fkMap.get(child.name);
81
198
  // Compute the column spec once per field and reuse it for both the column
82
199
  // line and the CHECK collection.
@@ -85,18 +202,9 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
85
202
  enumIntTypes.set(spec.enumIntCustomType.fnConstName, spec.enumIntCustomType);
86
203
  }
87
204
  const fieldDocs = renderDocsFor(child);
88
- const columnLine = renderColumn(spec, child, ctx, isPk, pkGeneration, fkInfo, isComposite, isUnique, obj.package, obj.name);
205
+ const columnLine = renderColumn(spec, columnNameExpr(child, spec.dbName), child, ctx, isPk, pkGeneration, fkInfo, isComposite, obj.package, obj.name);
89
206
  columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
90
- if (spec.checkConstraint !== undefined) {
91
- checkConstraints.push({
92
- // #293 — migrate's `<table>_<col>_chk` is the shared convention, and it is the
93
- // authority: its suffix form is systematic across five constraint kinds and those
94
- // names are already in live databases, so flipping migrate instead would emit
95
- // DROP/ADD CONSTRAINT churn against production for a cosmetic difference.
96
- name: `${tableName}_${spec.dbName}_chk`,
97
- expr: spec.checkConstraint,
98
- });
99
- }
207
+ if (spec.checkConstraint !== undefined) checkConstraints.push(checkEntry(child, spec));
100
208
  }
101
209
 
102
210
  // FR-017 Tier 2 — TPH single-table inheritance. When this entity is a
@@ -114,21 +222,12 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
114
222
  }
115
223
  const fieldDocs = renderDocsFor(child);
116
224
  const columnLine = renderColumn(
117
- spec, child, ctx, false, undefined, fkMap.get(child.name), isComposite, false, obj.package, obj.name, true,
225
+ spec, columnNameExpr(child, spec.dbName), child, ctx, false, undefined, fkMap.get(child.name), isComposite, obj.package, obj.name, true,
118
226
  );
119
227
  columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
120
228
  // Enum CHECK constraints stay valid under TPH: `NULL IN (...)` is NULL
121
229
  // (not false), so other-subtype rows with NULL pass the check.
122
- if (spec.checkConstraint !== undefined) {
123
- checkConstraints.push({
124
- // #293 — migrate's `<table>_<col>_chk` is the shared convention, and it is the
125
- // authority: its suffix form is systematic across five constraint kinds and those
126
- // names are already in live databases, so flipping migrate instead would emit
127
- // DROP/ADD CONSTRAINT churn against production for a cosmetic difference.
128
- name: `${tableName}_${spec.dbName}_chk`,
129
- expr: spec.checkConstraint,
130
- });
131
- }
230
+ if (spec.checkConstraint !== undefined) checkConstraints.push(checkEntry(child, spec));
132
231
  }
133
232
 
134
233
  // Build all table callback entries
@@ -138,38 +237,122 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
138
237
  callbackEntries.push(buildCompositeKeyCallback(pkFieldNames, importModule));
139
238
  }
140
239
 
240
+ /**
241
+ * One index callback entry, for BOTH `identity.secondary` (unique) and `index.lookup`
242
+ * (non-unique). The two shapes differ only in that boolean, and migrate treats their
243
+ * physical attributes identically — `@expr`/`@using`/`@orders`/`@where` are registered on
244
+ * both by the same provider — so they are emitted by ONE function here. They used to be
245
+ * two near-identical loops, which is how the physical attributes came to be honoured by
246
+ * neither: a rule written twice is a rule that can be forgotten twice.
247
+ *
248
+ * What was missing is a SHAPE divergence, not a naming one (the names were reconciled
249
+ * separately). An `@expr`-only index emitted NOTHING at all here, because both loops
250
+ * required a non-empty `@fields` — while migrate emits it, keying off `@expr`. And
251
+ * `@where` never reached this file, so a PARTIAL unique index read as a FULLY unique one
252
+ * in the generated schema: `drizzle-kit push` would propose replacing the database's
253
+ * partial index with a total one, and any diff written from generated source disagreed
254
+ * with the database over a difference that was not real. Same class as the sqlite
255
+ * emitter dropping these in 0.15.21.
256
+ */
257
+ const indexEntry = (
258
+ // `type`/`subType` as well as `name`: the index-name resolver and the artifact's
259
+ // `identities` / `indexes` collections both key off which KIND of node this is, and
260
+ // ADR-0040 put that distinction in the type rather than in an attribute.
261
+ node: { name: string; type: string; subType: string; attr: (n: string) => unknown },
262
+ fieldNames: string[],
263
+ unique: boolean,
264
+ ): Code | undefined => {
265
+ const str = (n: string): string | undefined => {
266
+ const raw = node.attr(n);
267
+ return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : undefined;
268
+ };
269
+ const expr = str(IDENTITY_ATTR_EXPR);
270
+ // An expression index keys off @expr; a plain one needs @fields. Mirrors migrate's
271
+ // `if (fieldNames.length === 0 && expr === undefined) continue`.
272
+ if (fieldNames.length === 0 && expr === undefined) return undefined;
273
+
274
+ const sqlSym = imp("sql@drizzle-orm");
275
+ const indexSym = imp(`${unique ? "uniqueIndex" : "index"}@${importModule}`);
276
+
277
+ // @orders is positional against @fields; migrate only serializes it when at least one
278
+ // entry is `desc`, because an all-ascending array must be indistinguishable from no
279
+ // orders at all for diff stability. Match that exactly or the two disagree on a
280
+ // no-op.
281
+ const ordersRaw = node.attr(IDENTITY_ATTR_ORDERS);
282
+ const orders = Array.isArray(ordersRaw)
283
+ ? fieldNames.map((_, i) => (ordersRaw[i] === "desc" ? "desc" : "asc"))
284
+ : [];
285
+ const anyDesc = orders.some((o) => o === "desc");
286
+ const colRefs = fieldNames.map((f, i) =>
287
+ anyDesc ? `table.${f}.${orders[i] === "desc" ? "desc" : "asc"}()` : `table.${f}`);
288
+
289
+ // sqlite's `.on()` accepts only columns — no SQL — so an expression index is not
290
+ // expressible there. It is skipped rather than emitted wrong; migrate still creates it,
291
+ // and a Drizzle declaration that silently indexed a COLUMN instead of the expression
292
+ // would be a worse answer than an absent one.
293
+ if (expr !== undefined && dialect === "sqlite") return undefined;
294
+
295
+ // @using selects the access method and takes the columns itself, so it REPLACES .on().
296
+ // Postgres-only: SQLite has no USING clause. migrate omits a `btree` value as the
297
+ // default, and so does this — emitting it would be a spurious difference.
298
+ const using = dialect === "sqlite" ? undefined : str(IDENTITY_ATTR_USING);
299
+ const target: Code =
300
+ expr !== undefined ? code`${sqlSym}\`${expr}\``
301
+ : code`${colRefs.join(", ")}`;
302
+ // The CONSTANT when the artifact is in the run, the literal otherwise — and the
303
+ // literal arm goes through `resolveIndexName` too, so the two paths answer with one
304
+ // function. Both halves matter: migrate's expected-schema resolves the same name and
305
+ // the two spelled it independently until now, which is how codegen came to declare
306
+ // `idx_<table>_<col>` against an index the database actually held under
307
+ // `identity.name` (fdb4118f1); and an empty `index.lookup` name — which the loader
308
+ // accepts, unlike an identity — is refused here rather than emitting `index("")`.
309
+ const indexName = indexNameExpr(names, node);
310
+ const head: Code =
311
+ using !== undefined && using !== "btree"
312
+ ? code`${indexSym}(${indexName}).using(${JSON.stringify(using)}, ${target})`
313
+ : code`${indexSym}(${indexName}).on(${target})`;
314
+
315
+ // @where — the partial-index predicate, available on both dialects.
316
+ const where = str(IDENTITY_ATTR_WHERE);
317
+ return where === undefined ? head : code`${head}.where(${sqlSym}\`${where}\`)`;
318
+ };
319
+
141
320
  // identity.secondary — always unique (uniqueness is in the type, no boolean).
142
321
  for (const sec of secondaryIdentities) {
143
322
  const fields = sec.attr(IDENTITY_ATTR_FIELDS) as string[] | undefined;
144
- if (!Array.isArray(fields) || fields.length === 0) continue;
145
- const indexName = `idx_${tableName}_${fields.map((f) => columnNameFromField(f, ctx.columnNamingStrategy)).join("_")}`;
146
- const indexSym = imp(`uniqueIndex@${importModule}`);
147
- // Use the callback param `table` (not the outer varName) so TS doesn't see
323
+ // The identity's OWN name, which is what migrate puts in the database
324
+ // (`expected-schema.ts`, secondary-identity pass) and what `index.lookup` three
325
+ // blocks below already emits. This used to compose `idx_<table>_<col>...` by running
326
+ // the naming strategy over the FIELD names two defects in one expression: the name
327
+ // disagreed with migrate's (so a DROP INDEX written from generated source failed, and
328
+ // any schema diff reported a difference that was not real), and running the strategy
329
+ // over a field NAME cannot see `@column`, so a declared physical column was invisible
330
+ // to it. Gated by `secondary-index-name-parity.test.ts`, whose fixture is de-blinded
331
+ // against exactly that second defect.
332
+ // Column refs use the callback param `table` (not the outer varName) so TS does not see
148
333
  // the table referencing itself inside its own initializer (TS7022/TS7024).
149
- const cols = fields.map((f) => `table.${f}`).join(", ");
150
- callbackEntries.push(code`${indexSym}(${JSON.stringify(indexName)}).on(${cols})`);
334
+ const entry = indexEntry(sec, Array.isArray(fields) ? fields : [], /* unique */ true);
335
+ if (entry !== undefined) callbackEntries.push(entry);
151
336
  }
152
337
 
153
338
  // index.lookup — always non-unique. Use MetaIndex.fields() per ADR-0039.
154
339
  for (const lookup of obj.lookupIndexes()) {
155
- const fields = lookup.fields();
156
- if (fields.length === 0) continue;
157
- const indexSym = imp(`index@${importModule}`);
158
- const cols = fields.map((f) => `table.${f}`).join(", ");
159
- callbackEntries.push(code`${indexSym}(${JSON.stringify(lookup.name)}).on(${cols})`);
340
+ const entry = indexEntry(lookup, lookup.fields(), /* unique */ false);
341
+ if (entry !== undefined) callbackEntries.push(entry);
160
342
  }
161
343
 
162
- // Emit table-level CHECK constraints for enum fields.
344
+ // Emit table-level CHECK constraints for enum fields. Both halves are composed by
345
+ // `checkEntry` above, which owns the constant-vs-literal decision for the pair.
163
346
  for (const { name, expr } of checkConstraints) {
164
347
  const checkSym = imp(`check@${importModule}`);
165
348
  const sqlSym = imp("sql@drizzle-orm");
166
- callbackEntries.push(code`${checkSym}(${JSON.stringify(name)}, ${sqlSym}\`${expr}\`)`);
349
+ callbackEntries.push(code`${checkSym}(${name}, ${sqlSym}\`${expr}\`)`);
167
350
  }
168
351
 
169
352
  let tableBlock: Code;
170
353
  if (callbackEntries.length > 0) {
171
354
  tableBlock = code`
172
- export const ${varName} = ${tableFnSym}(${JSON.stringify(tableName)}, {
355
+ export const ${varName} = ${tableCall}(${tableNameExpr}, {
173
356
  ${joinCode(columnLines, { on: ",\n", trim: false })}
174
357
  }, (table) => [
175
358
  ${joinCode(callbackEntries, { on: ",\n ", trim: false })}
@@ -177,7 +360,7 @@ ${joinCode(columnLines, { on: ",\n", trim: false })}
177
360
  `;
178
361
  } else {
179
362
  tableBlock = code`
180
- export const ${varName} = ${tableFnSym}(${JSON.stringify(tableName)}, {
363
+ export const ${varName} = ${tableCall}(${tableNameExpr}, {
181
364
  ${joinCode(columnLines, { on: ",\n", trim: false })}
182
365
  });
183
366
  `;
@@ -309,13 +492,16 @@ function inlineObjectLiteral(obj: Record<string, unknown>): string {
309
492
  /** Render one column line (field name + Drizzle column expression). */
310
493
  function renderColumn(
311
494
  spec: ColumnSpec,
495
+ // §A6 — the column's physical name as an EXPRESSION: either the `<Entity>Names` constant
496
+ // or the same literal as before. Passed in rather than derived here so one call site owns
497
+ // the decision (and the equality check that makes it byte-safe).
498
+ dbNameExpr: Code,
312
499
  field: MetaField,
313
500
  ctx: RenderContext,
314
501
  isPk: boolean,
315
502
  pkGeneration: string | undefined,
316
503
  fkInfo: FkInfo | undefined,
317
504
  isComposite: boolean,
318
- isUnique: boolean = false,
319
505
  entityPackage: string | undefined = undefined,
320
506
  // Name of the entity this column belongs to — used to detect a self-referential
321
507
  // FK (target entity === this entity), which Drizzle emits without a self-import.
@@ -332,12 +518,11 @@ function renderColumn(
332
518
  ? spec.enumIntCustomType.fnConstName
333
519
  : imp(`${spec.fnName}@${spec.importModule}`);
334
520
 
335
- const dbNameLit = JSON.stringify(spec.dbName);
336
521
  let baseCall: Code;
337
522
  if (spec.fnOptions !== undefined && Object.keys(spec.fnOptions).length > 0) {
338
- baseCall = code`${fnSym}(${dbNameLit}, ${inlineObjectLiteral(spec.fnOptions)})`;
523
+ baseCall = code`${fnSym}(${dbNameExpr}, ${inlineObjectLiteral(spec.fnOptions)})`;
339
524
  } else {
340
- baseCall = code`${fnSym}(${dbNameLit})`;
525
+ baseCall = code`${fnSym}(${dbNameExpr})`;
341
526
  }
342
527
 
343
528
  let pkSuffix = "";
@@ -350,10 +535,10 @@ function renderColumn(
350
535
  // Postgres: bigserial for long (8-byte), serial for int (4-byte).
351
536
  if (field.subType === FIELD_SUBTYPE_LONG) {
352
537
  const bigserialSym = imp(`bigserial@${spec.importModule}`);
353
- baseCall = code`${bigserialSym}(${dbNameLit}, { mode: "number" })`;
538
+ baseCall = code`${bigserialSym}(${dbNameExpr}, { mode: "number" })`;
354
539
  } else {
355
540
  const serialSym = imp(`serial@${spec.importModule}`);
356
- baseCall = code`${serialSym}(${dbNameLit})`;
541
+ baseCall = code`${serialSym}(${dbNameExpr})`;
357
542
  }
358
543
  pkSuffix = isComposite ? "" : ".primaryKey()";
359
544
  }
@@ -370,14 +555,17 @@ function renderColumn(
370
555
  }
371
556
 
372
557
  let modifiersStr = pkSuffix;
373
- // Append .unique() for secondary-identity fields (isPk guard already excluded PKs upstream).
374
- if (isUnique) modifiersStr += ".unique()";
558
+ // No `isUnique` parameter here any more, and its absence is the fix rather than a tidy-up.
559
+ // A single-column `identity.secondary` used to append `.unique()` on top of the constraint
560
+ // migrate already emits under the identity's own name, so codegen declared one constraint
561
+ // the database does not have. When that producer was removed both call sites started
562
+ // passing a literal `false`, leaving a parameter whose every branch was unreachable — a
563
+ // dead switch that reads like a live one. `spec.modifiers` carries whatever uniqueness the
564
+ // column genuinely has, and the loop below is the only thing that applies it.
375
565
  for (const m of spec.modifiers) {
376
566
  // Single-column PKs imply notNull/unique; avoid emitting them twice.
377
567
  // Composite-PK columns are NOT declared with .primaryKey(), so they DO need .notNull().
378
568
  if (isPk && !isComposite && (m === ".notNull()" || m === ".unique()")) continue;
379
- // Avoid double-emitting .unique() if it was already appended above.
380
- if (isUnique && m === ".unique()") continue;
381
569
  // TPH subtype-only column: never .notNull() / .unique() — rows of other
382
570
  // subtypes store NULL, so neither constraint can hold across the table.
383
571
  if (forceNullable && (m === ".notNull()" || m === ".unique()")) continue;