shapecraft 1.0.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 (112) hide show
  1. package/CLAUDE.md +227 -0
  2. package/README.md +22 -0
  3. package/apps/cli/node_modules/.bin/prettier +21 -0
  4. package/apps/cli/node_modules/.bin/tsc +21 -0
  5. package/apps/cli/node_modules/.bin/tsserver +21 -0
  6. package/apps/cli/node_modules/.bin/tsx +21 -0
  7. package/apps/cli/node_modules/.bin/vitest +21 -0
  8. package/apps/cli/package.json +47 -0
  9. package/apps/cli/src/index.ts +98 -0
  10. package/apps/cli/tsconfig.cjs.json +10 -0
  11. package/apps/cli/tsconfig.esm.json +10 -0
  12. package/apps/cli/tsconfig.json +22 -0
  13. package/package.json +16 -0
  14. package/packages/core/node_modules/.bin/prettier +21 -0
  15. package/packages/core/node_modules/.bin/tsc +21 -0
  16. package/packages/core/node_modules/.bin/tsserver +21 -0
  17. package/packages/core/node_modules/.bin/tsx +21 -0
  18. package/packages/core/node_modules/.bin/vitest +21 -0
  19. package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  20. package/packages/core/package.json +44 -0
  21. package/packages/core/src/common/array.test.ts +19 -0
  22. package/packages/core/src/common/array.ts +15 -0
  23. package/packages/core/src/common/index.ts +5 -0
  24. package/packages/core/src/common/is.ts +23 -0
  25. package/packages/core/src/common/object.ts +35 -0
  26. package/packages/core/src/common/phantom.ts +1 -0
  27. package/packages/core/src/common/result.ts +43 -0
  28. package/packages/core/src/common/string.ts +28 -0
  29. package/packages/core/src/common/types.ts +34 -0
  30. package/packages/core/src/index.ts +1 -0
  31. package/packages/core/src/shape/annotate.ts +139 -0
  32. package/packages/core/src/shape/annotation.ts +47 -0
  33. package/packages/core/src/shape/base.ts +71 -0
  34. package/packages/core/src/shape/builder.test.ts +728 -0
  35. package/packages/core/src/shape/builder.ts +475 -0
  36. package/packages/core/src/shape/error.ts +4 -0
  37. package/packages/core/src/shape/index.ts +3 -0
  38. package/packages/core/src/shape/number.ts +118 -0
  39. package/packages/core/src/shape/shape.test.ts +792 -0
  40. package/packages/core/src/shape/shape.ts +377 -0
  41. package/packages/core/src/shape/tags.ts +14 -0
  42. package/packages/core/src/shape/transforms/index.ts +3 -0
  43. package/packages/core/src/shape/transforms/json-schema/index.ts +2 -0
  44. package/packages/core/src/shape/transforms/json-schema/transform.test.ts +850 -0
  45. package/packages/core/src/shape/transforms/json-schema/transform.ts +882 -0
  46. package/packages/core/src/shape/transforms/json-schema/types.ts +132 -0
  47. package/packages/core/src/shape/transforms/sql/dialects/dialect.ts +89 -0
  48. package/packages/core/src/shape/transforms/sql/dialects/index.ts +14 -0
  49. package/packages/core/src/shape/transforms/sql/dialects/postgres.ts +392 -0
  50. package/packages/core/src/shape/transforms/sql/dialects/sqlite.ts +333 -0
  51. package/packages/core/src/shape/transforms/sql/from-sql.test.ts +704 -0
  52. package/packages/core/src/shape/transforms/sql/from-sql.ts +210 -0
  53. package/packages/core/src/shape/transforms/sql/index.ts +3 -0
  54. package/packages/core/src/shape/transforms/sql/options.ts +6 -0
  55. package/packages/core/src/shape/transforms/sql/parser/check-decoder.ts +457 -0
  56. package/packages/core/src/shape/transforms/sql/parser/create-domain.ts +105 -0
  57. package/packages/core/src/shape/transforms/sql/parser/create-table.ts +809 -0
  58. package/packages/core/src/shape/transforms/sql/parser/create-type.ts +91 -0
  59. package/packages/core/src/shape/transforms/sql/parser/cursor.ts +179 -0
  60. package/packages/core/src/shape/transforms/sql/parser/default-decoder.ts +129 -0
  61. package/packages/core/src/shape/transforms/sql/parser/lexer.ts +289 -0
  62. package/packages/core/src/shape/transforms/sql/parser/pg-types.ts +247 -0
  63. package/packages/core/src/shape/transforms/sql/parser/sqlite-types.ts +103 -0
  64. package/packages/core/src/shape/transforms/sql/parser/statements.ts +127 -0
  65. package/packages/core/src/shape/transforms/sql/parser/type-spec.ts +159 -0
  66. package/packages/core/src/shape/transforms/sql/transform.sqlite.test.ts +448 -0
  67. package/packages/core/src/shape/transforms/sql/transform.test.ts +880 -0
  68. package/packages/core/src/shape/transforms/sql/transform.ts +295 -0
  69. package/packages/core/src/shape/transforms/typescript/index.ts +1 -0
  70. package/packages/core/src/shape/transforms/typescript/transform.ts +211 -0
  71. package/packages/core/src/shape/tuple.test.ts +171 -0
  72. package/packages/core/src/shape/validate.ts +413 -0
  73. package/packages/core/tsconfig.cjs.json +11 -0
  74. package/packages/core/tsconfig.esm.json +10 -0
  75. package/packages/core/tsconfig.json +23 -0
  76. package/packages/samples/node_modules/.bin/prettier +21 -0
  77. package/packages/samples/node_modules/.bin/tsc +21 -0
  78. package/packages/samples/node_modules/.bin/tsserver +21 -0
  79. package/packages/samples/node_modules/.bin/tsx +21 -0
  80. package/packages/samples/node_modules/.bin/vitest +21 -0
  81. package/packages/samples/package.json +47 -0
  82. package/packages/samples/src/blog.ts +49 -0
  83. package/packages/samples/src/config.ts +50 -0
  84. package/packages/samples/src/ecommerce.ts +65 -0
  85. package/packages/samples/src/embeddings.ts +43 -0
  86. package/packages/samples/src/events.ts +52 -0
  87. package/packages/samples/src/geometry.ts +62 -0
  88. package/packages/samples/src/index.ts +9 -0
  89. package/packages/samples/src/relational.ts +17 -0
  90. package/packages/samples/src/tuples.ts +67 -0
  91. package/packages/samples/src/user.ts +9 -0
  92. package/packages/samples/tsconfig.cjs.json +11 -0
  93. package/packages/samples/tsconfig.esm.json +10 -0
  94. package/packages/samples/tsconfig.json +23 -0
  95. package/pnpm-workspace.yaml +3 -0
  96. package/test-data/json-schema/address.json +35 -0
  97. package/test-data/json-schema/array-of-things.json +36 -0
  98. package/test-data/json-schema/basic.json +21 -0
  99. package/test-data/json-schema/blog-post.json +29 -0
  100. package/test-data/json-schema/calendar.json +48 -0
  101. package/test-data/json-schema/complex-object-with-nested-properties.json +41 -0
  102. package/test-data/json-schema/ecommerce-complex.json +344 -0
  103. package/test-data/json-schema/ecommerce-system.json +27 -0
  104. package/test-data/json-schema/enumerated-values.json +11 -0
  105. package/test-data/json-schema/fstab-entry.json +92 -0
  106. package/test-data/json-schema/geographical-location.json +20 -0
  107. package/test-data/json-schema/health-record.json +41 -0
  108. package/test-data/json-schema/job-posting.json +33 -0
  109. package/test-data/json-schema/movie.json +35 -0
  110. package/test-data/json-schema/regular-expression-pattern.json +12 -0
  111. package/test-data/json-schema/user-profile.json +33 -0
  112. package/test-data/sql/ecommerce.sql +641 -0
@@ -0,0 +1,809 @@
1
+ import { ReferentialAction } from "../../../annotation";
2
+ import { annotate, shapes } from "../../../shape";
3
+ import { SQLDialect } from "../options";
4
+ import { checkExprToSql, decodeCheck } from "./check-decoder";
5
+ import { ParseError, TokenCursor } from "./cursor";
6
+ import { decodeDefault } from "./default-decoder";
7
+ import { Token } from "./lexer";
8
+ import { Registries, resolvePgType } from "./pg-types";
9
+ import { resolveSqliteType } from "./sqlite-types";
10
+ import { parseTypeSpec, ParsedTypeSpec } from "./type-spec";
11
+
12
+ export type ParsedTable = {
13
+ schema?: string;
14
+ name: string;
15
+ columns: Array<{ key: string; shape: shapes.Shape }>;
16
+ uniqueConstraints?: Array<{ columns: string[]; name?: string }>;
17
+ mappingAnno: {
18
+ title: string;
19
+ schema?: string;
20
+ check?: string;
21
+ description?: string;
22
+ };
23
+ };
24
+
25
+ export type ParseTableOptions = {
26
+ registries: Registries;
27
+ dialect: SQLDialect;
28
+ };
29
+
30
+ const qualifiedName = (parts: string[]): { schema?: string; name: string } => {
31
+ if (parts.length === 1) return { name: parts[0]! };
32
+ return { schema: parts[0]!, name: parts.slice(1).join(".") };
33
+ };
34
+
35
+ const splitAtTopLevelCommas = (tokens: Token[]): Token[][] => {
36
+ const out: Token[][] = [];
37
+ let buf: Token[] = [];
38
+ let depth = 0;
39
+ for (const t of tokens) {
40
+ if (t.kind === "punct" && t.value === "(") depth += 1;
41
+ else if (t.kind === "punct" && t.value === ")") depth -= 1;
42
+ if (t.kind === "punct" && t.value === "," && depth === 0) {
43
+ if (buf.length > 0) out.push(buf);
44
+ buf = [];
45
+ continue;
46
+ }
47
+ buf.push(t);
48
+ }
49
+ if (buf.length > 0) out.push(buf);
50
+ return out;
51
+ };
52
+
53
+ const REF_ACTION_MAP: Record<string, ReferentialAction> = {
54
+ cascade: "cascade",
55
+ restrict: "restrict",
56
+ // "set null" / "set default" are two-word — handled in parsing
57
+ };
58
+
59
+ const parseReferentialAction = (c: TokenCursor): ReferentialAction | null => {
60
+ if (c.consumeIdent("cascade")) return "cascade";
61
+ if (c.consumeIdent("restrict")) return "restrict";
62
+ if (c.consumeIdent("no")) {
63
+ c.consumeIdent("action");
64
+ return "noAction";
65
+ }
66
+ if (c.consumeIdent("set")) {
67
+ if (c.consumeIdent("null")) return "setNull";
68
+ if (c.consumeIdent("default")) return "setDefault";
69
+ return null;
70
+ }
71
+ return null;
72
+ };
73
+
74
+ // Read the type spec tokens up to where a column constraint keyword begins
75
+ // (or end of column-def). Returns the consumed tokens.
76
+ const readTypeTokens = (c: TokenCursor): Token[] => {
77
+ const out: Token[] = [];
78
+ // first ident (the base type or first part of qualified name)
79
+ if (c.peek().kind !== "ident") {
80
+ // unexpected — caller should error
81
+ return out;
82
+ }
83
+ out.push(c.consume());
84
+ // dotted continuation
85
+ while (c.isPunct(".")) {
86
+ out.push(c.consume()); // '.'
87
+ if (c.peek().kind !== "ident") break;
88
+ out.push(c.consume());
89
+ }
90
+ // multi-word types: peek for known continuations
91
+ const firstTwoExtend = (a: string, b: string): boolean => {
92
+ const combos = [
93
+ "double precision",
94
+ "character varying",
95
+ "bit varying",
96
+ "timestamp with",
97
+ "time with",
98
+ "timestamp without",
99
+ "time without",
100
+ ];
101
+ return combos.includes(`${a} ${b}`);
102
+ };
103
+ const first = out[0]?.value;
104
+ const next = c.peek();
105
+ if (
106
+ typeof first === "string" &&
107
+ next.kind === "ident" &&
108
+ !next.quoted &&
109
+ firstTwoExtend(first, next.value)
110
+ ) {
111
+ out.push(c.consume());
112
+ // possibly "time zone" suffix
113
+ if (c.isIdent("time") && c.peek(1).kind === "ident") {
114
+ const t1 = c.peek(1);
115
+ if (t1.kind === "ident" && t1.value === "zone") {
116
+ out.push(c.consume());
117
+ out.push(c.consume());
118
+ }
119
+ }
120
+ }
121
+ // optional (args)
122
+ if (c.isPunct("(")) {
123
+ out.push(c.consume());
124
+ let depth = 1;
125
+ while (!c.done() && depth > 0) {
126
+ const t = c.peek();
127
+ if (t.kind === "punct" && t.value === "(") depth += 1;
128
+ if (t.kind === "punct" && t.value === ")") depth -= 1;
129
+ out.push(c.consume());
130
+ }
131
+ }
132
+ // optional [] array suffix
133
+ while (
134
+ c.isPunct("[") &&
135
+ c.peek(1).kind === "punct" &&
136
+ c.peek(1).value === "]"
137
+ ) {
138
+ out.push(c.consume());
139
+ out.push(c.consume());
140
+ }
141
+ return out;
142
+ };
143
+
144
+ type ColMeta = {
145
+ notNull: boolean;
146
+ primary: boolean;
147
+ unique: boolean;
148
+ autoIncrement: boolean;
149
+ default?: { kind: "literal"; value: unknown } | { kind: "expr"; sql: string };
150
+ foreign?: {
151
+ table: string;
152
+ column: string;
153
+ onDelete?: ReferentialAction;
154
+ onUpdate?: ReferentialAction;
155
+ };
156
+ generated?: { expression: string; stored: boolean };
157
+ checks: Array<Token[]>;
158
+ };
159
+
160
+ // Convert generated-as-expression tokens to a clean SQL string.
161
+ const tokensToExprSql = (tokens: Token[]): string => checkExprToSql(tokens);
162
+
163
+ const parseColumnConstraints = (c: TokenCursor): ColMeta => {
164
+ const meta: ColMeta = {
165
+ notNull: false,
166
+ primary: false,
167
+ unique: false,
168
+ autoIncrement: false,
169
+ checks: [],
170
+ };
171
+ while (!c.done()) {
172
+ if (c.consumeIdent("constraint")) {
173
+ // anonymous constraint name — consume it
174
+ if (c.peek().kind === "ident") c.consume();
175
+ continue;
176
+ }
177
+ if (c.consumeIdent("not")) {
178
+ if (c.consumeIdent("null")) {
179
+ meta.notNull = true;
180
+ continue;
181
+ }
182
+ // unknown form — bail
183
+ break;
184
+ }
185
+ if (c.consumeIdent("null")) {
186
+ // explicit NULL — nothing to do
187
+ continue;
188
+ }
189
+ if (c.consumeIdent("primary")) {
190
+ c.consumeIdent("key");
191
+ meta.primary = true;
192
+ continue;
193
+ }
194
+ if (c.consumeIdent("autoincrement")) {
195
+ // SQLite-only keyword, but accepting it in any dialect is harmless.
196
+ meta.autoIncrement = true;
197
+ continue;
198
+ }
199
+ if (c.consumeIdent("unique")) {
200
+ meta.unique = true;
201
+ continue;
202
+ }
203
+ if (c.consumeIdent("default")) {
204
+ const defTokens = readDefaultExpr(c);
205
+ meta.default = decodeDefault(defTokens, null);
206
+ continue;
207
+ }
208
+ if (c.consumeIdent("references")) {
209
+ meta.foreign = parseReferencesClause(c);
210
+ continue;
211
+ }
212
+ if (c.consumeIdent("check")) {
213
+ const inner = c.skipParens();
214
+ meta.checks.push(inner);
215
+ continue;
216
+ }
217
+ if (c.consumeIdent("generated")) {
218
+ // GENERATED [ALWAYS|BY DEFAULT] AS (...) STORED
219
+ // GENERATED [ALWAYS|BY DEFAULT] AS IDENTITY [(seq opts)]
220
+ let byDefault = false;
221
+ if (c.consumeIdent("always")) {
222
+ /* */
223
+ } else if (c.consumeIdent("by")) {
224
+ c.consumeIdent("default");
225
+ byDefault = true;
226
+ }
227
+ c.expectIdent("as");
228
+ if (c.consumeIdent("identity")) {
229
+ meta.autoIncrement = true;
230
+ // optional (sequence options)
231
+ if (c.isPunct("(")) c.skipParens();
232
+ continue;
233
+ }
234
+ // expression
235
+ const inner = c.skipParens();
236
+ // STORED keyword (optional but typical)
237
+ let stored = true;
238
+ if (c.consumeIdent("stored")) stored = true;
239
+ if (c.consumeIdent("virtual")) stored = false;
240
+ meta.generated = { expression: tokensToExprSql(inner), stored };
241
+ if (byDefault) {
242
+ // not really meaningful for expression-generated columns; ignore
243
+ }
244
+ continue;
245
+ }
246
+ if (c.consumeIdent("collate")) {
247
+ // collate name — consume one ident
248
+ if (c.peek().kind === "ident") c.consume();
249
+ continue;
250
+ }
251
+ if (c.consumeIdent("deferrable")) continue;
252
+ if (c.consumeIdent("not") && c.consumeIdent("deferrable")) continue;
253
+ if (c.consumeIdent("initially")) {
254
+ if (c.peek().kind === "ident") c.consume();
255
+ continue;
256
+ }
257
+ // Unrecognized constraint — bail
258
+ break;
259
+ }
260
+ return meta;
261
+ };
262
+
263
+ const parseReferencesClause = (
264
+ c: TokenCursor,
265
+ ): {
266
+ table: string;
267
+ column: string;
268
+ onDelete?: ReferentialAction;
269
+ onUpdate?: ReferentialAction;
270
+ } => {
271
+ const parts = c.readDottedName();
272
+ const qn = qualifiedName(parts);
273
+ let column = "id";
274
+ if (c.consumePunct("(")) {
275
+ const colTok = c.expectAnyIdent();
276
+ column = colTok.name;
277
+ c.expectPunct(")");
278
+ }
279
+ const out: {
280
+ table: string;
281
+ column: string;
282
+ onDelete?: ReferentialAction;
283
+ onUpdate?: ReferentialAction;
284
+ } = { table: qn.name, column };
285
+ while (c.consumeIdent("on")) {
286
+ if (c.consumeIdent("delete")) {
287
+ const a = parseReferentialAction(c);
288
+ if (a !== null) out.onDelete = a;
289
+ } else if (c.consumeIdent("update")) {
290
+ const a = parseReferentialAction(c);
291
+ if (a !== null) out.onUpdate = a;
292
+ } else break;
293
+ }
294
+ // MATCH FULL / SIMPLE / PARTIAL — consume
295
+ if (c.consumeIdent("match")) {
296
+ if (c.peek().kind === "ident") c.consume();
297
+ }
298
+ return out;
299
+ };
300
+
301
+ // Read a DEFAULT expression — terminate on a known constraint-start keyword
302
+ // or end of input.
303
+ const readDefaultExpr = (c: TokenCursor): Token[] => {
304
+ const out: Token[] = [];
305
+ let depth = 0;
306
+ const STOP = new Set([
307
+ "not",
308
+ "null",
309
+ "primary",
310
+ "unique",
311
+ "references",
312
+ "check",
313
+ "default",
314
+ "generated",
315
+ "constraint",
316
+ "collate",
317
+ "deferrable",
318
+ "initially",
319
+ ]);
320
+ while (!c.done()) {
321
+ const t = c.peek();
322
+ if (t.kind === "punct" && t.value === "(") depth += 1;
323
+ if (t.kind === "punct" && t.value === ")") depth -= 1;
324
+ if (depth === 0 && t.kind === "ident" && !t.quoted && STOP.has(t.value))
325
+ break;
326
+ out.push(t);
327
+ c.consume();
328
+ }
329
+ return out;
330
+ };
331
+
332
+ // Apply a column shape with the column metadata to produce a final shape with
333
+ // annotations folded in. `ctx.isString/isInteger/isArray` are derived from the
334
+ // resolved base shape.
335
+ const applyColumnMeta = (
336
+ baseShape: shapes.Shape,
337
+ meta: ColMeta,
338
+ pgType: string,
339
+ columnName: string,
340
+ ): shapes.Shape => {
341
+ // First pass: collect all decoded CHECK updates and the leftover raw
342
+ // expressions. Aggregating before applying lets us swap the base shape
343
+ // (e.g. INTEGER → boolean, TEXT → array) based on the union of hints
344
+ // rather than the order they appeared.
345
+ const ctx = {
346
+ columnName,
347
+ isInteger: baseShape.type === "number",
348
+ isString: baseShape.type === "string",
349
+ isArray: baseShape.type === "array",
350
+ };
351
+ const combined: {
352
+ min?: number;
353
+ max?: number;
354
+ pattern?: string;
355
+ uniqueItems?: boolean;
356
+ unionLiterals?: string[];
357
+ isBooleanInt?: boolean;
358
+ isJsonValid?: boolean;
359
+ tupleLength?: number;
360
+ } = {};
361
+ const rawChecks: string[] = [];
362
+ for (const chkTokens of meta.checks) {
363
+ const dec = decodeCheck(chkTokens, ctx);
364
+ if (!dec.fullyDecoded) {
365
+ rawChecks.push(checkExprToSql(chkTokens));
366
+ continue;
367
+ }
368
+ const u = dec.updates;
369
+ if (u.min !== undefined) combined.min = u.min;
370
+ if (u.max !== undefined) combined.max = u.max;
371
+ if (u.pattern !== undefined) combined.pattern = u.pattern;
372
+ if (u.uniqueItems === true) combined.uniqueItems = true;
373
+ if (u.unionLiterals !== undefined) combined.unionLiterals = u.unionLiterals;
374
+ if (u.isBooleanInt === true) combined.isBooleanInt = true;
375
+ if (u.isJsonValid === true) combined.isJsonValid = true;
376
+ if (u.tupleLength !== undefined) combined.tupleLength = u.tupleLength;
377
+ }
378
+
379
+ // Swap the base shape if CHECKs reveal a different underlying type.
380
+ let shape: shapes.Shape = baseShape;
381
+ if (combined.isBooleanInt === true && baseShape.type === "number") {
382
+ shape = shapes.boolean();
383
+ } else if (combined.isJsonValid === true && baseShape.type === "string") {
384
+ if (combined.tupleLength !== undefined) {
385
+ const items: shapes.Shape[] = [];
386
+ for (let i = 0; i < combined.tupleLength; i += 1)
387
+ items.push(shapes.unknown());
388
+ shape = shapes.tuple(...items);
389
+ } else if (
390
+ combined.min !== undefined ||
391
+ combined.max !== undefined ||
392
+ combined.uniqueItems === true
393
+ ) {
394
+ shape = shapes.array(shapes.unknown());
395
+ } else {
396
+ shape = shapes.unknown();
397
+ }
398
+ } else if (
399
+ combined.unionLiterals !== undefined &&
400
+ baseShape.type === "string"
401
+ ) {
402
+ const literals = combined.unionLiterals.map((v) => shapes.literal(v));
403
+ shape = shapes.union(literals);
404
+ }
405
+
406
+ if (!meta.notNull && !meta.primary) {
407
+ shape = annotate.optional(shape);
408
+ }
409
+ if (meta.primary) {
410
+ shape = annotate.primary(shape);
411
+ }
412
+ if (meta.unique) {
413
+ shape = annotate.unique(shape);
414
+ }
415
+ if (meta.autoIncrement) {
416
+ shape = annotate.autoIncremented(shape);
417
+ }
418
+ if (meta.foreign !== undefined) {
419
+ shape = annotate.as(shape, { foreign: meta.foreign });
420
+ }
421
+ if (meta.generated !== undefined) {
422
+ shape = annotate.as(shape, { generated: meta.generated });
423
+ }
424
+ // On a date column, re-interpret a bare string literal as a Date (SQLite
425
+ // emits `DEFAULT '2024-01-01T…'` without a `::timestamptz` cast, so the
426
+ // base decoder leaves it as a string).
427
+ if (
428
+ meta.default !== undefined &&
429
+ meta.default.kind === "literal" &&
430
+ typeof meta.default.value === "string" &&
431
+ baseShape.type === "date"
432
+ ) {
433
+ const d = new Date(meta.default.value);
434
+ const t = d.getTime();
435
+ if (!isNaN(t) && isFinite(t)) {
436
+ meta.default = { kind: "literal", value: d };
437
+ }
438
+ }
439
+ // If the column was swapped to boolean (SQLite INTEGER + IN(0,1)),
440
+ // re-interpret 0/1 numeric defaults as boolean.
441
+ if (
442
+ meta.default !== undefined &&
443
+ meta.default.kind === "literal" &&
444
+ typeof meta.default.value === "number" &&
445
+ shape.type === "boolean" &&
446
+ (meta.default.value === 0 || meta.default.value === 1)
447
+ ) {
448
+ meta.default = { kind: "literal", value: meta.default.value === 1 };
449
+ }
450
+
451
+ if (meta.default !== undefined) {
452
+ if (meta.default.kind === "literal") {
453
+ shape = annotate.as(shape, { default: meta.default.value });
454
+ } else {
455
+ // decode might have given an empty string for malformed input — skip in that case
456
+ if (meta.default.sql.length > 0) {
457
+ shape = annotate.as(shape, { defaultExpr: meta.default.sql });
458
+ }
459
+ }
460
+ }
461
+ // Re-decode defaults that need the pg type context (specifically for `'{}'` on T[])
462
+ if (
463
+ meta.default !== undefined &&
464
+ meta.default.kind === "expr" &&
465
+ pgType.endsWith("[]") &&
466
+ /^'\{}'$/.test(meta.default.sql.trim())
467
+ ) {
468
+ shape = annotate.as(shape, { default: [] });
469
+ }
470
+
471
+ if (combined.min !== undefined) {
472
+ shape = annotate.as(shape, { min: combined.min });
473
+ }
474
+ if (combined.max !== undefined) {
475
+ shape = annotate.as(shape, { max: combined.max });
476
+ }
477
+ if (combined.pattern !== undefined) {
478
+ shape = annotate.as(shape, { pattern: combined.pattern });
479
+ }
480
+ if (combined.uniqueItems === true) {
481
+ shape = annotate.as(shape, { uniqueItems: true });
482
+ }
483
+ for (const raw of rawChecks) {
484
+ shape = annotate.as(shape, { check: raw });
485
+ }
486
+
487
+ return shape;
488
+ };
489
+
490
+ export const parseCreateTable = (
491
+ tokens: Token[],
492
+ opts: ParseTableOptions,
493
+ ): ParsedTable => {
494
+ const c = new TokenCursor(tokens);
495
+ c.expectIdent("create");
496
+ if (c.consumeIdent("or")) c.expectIdent("replace");
497
+ // optional UNLOGGED / TEMPORARY etc.
498
+ c.consumeIdent("global");
499
+ c.consumeIdent("local");
500
+ c.consumeIdent("temporary");
501
+ c.consumeIdent("temp");
502
+ c.consumeIdent("unlogged");
503
+ c.expectIdent("table");
504
+ if (c.consumeIdent("if")) {
505
+ c.expectIdent("not");
506
+ c.expectIdent("exists");
507
+ }
508
+ const nameParts = c.readDottedName();
509
+ const qn = qualifiedName(nameParts);
510
+
511
+ c.expectPunct("(");
512
+ // collect everything inside the outer parens as defs
513
+ const defsTokens: Token[] = [];
514
+ let depth = 1;
515
+ while (!c.done() && depth > 0) {
516
+ const t = c.peek();
517
+ if (t.kind === "punct" && t.value === "(") depth += 1;
518
+ if (t.kind === "punct" && t.value === ")") {
519
+ depth -= 1;
520
+ if (depth === 0) {
521
+ c.consume();
522
+ break;
523
+ }
524
+ }
525
+ defsTokens.push(t);
526
+ c.consume();
527
+ }
528
+ // anything after the closing paren (PARTITION BY, WITH, etc.) is dropped
529
+
530
+ const items = splitAtTopLevelCommas(defsTokens);
531
+
532
+ const columns: Array<{ key: string; shape: shapes.Shape }> = [];
533
+ // Maps to apply table constraints retroactively
534
+ const colIndex = new Map<string, number>();
535
+ const colMetas = new Map<string, ColMeta>();
536
+ const baseShapeByName = new Map<string, shapes.Shape>();
537
+ const pgTypeByName = new Map<string, string>();
538
+ let tableCheck: string | undefined;
539
+ const uniqueConstraints: Array<{ columns: string[]; name?: string }> = [];
540
+
541
+ for (const item of items) {
542
+ if (isTableConstraint(item)) continue; // process in second pass
543
+ const ic = new TokenCursor(item);
544
+ const ident = ic.peek();
545
+ if (ident.kind !== "ident") {
546
+ throw new ParseError(
547
+ `expected column definition but found ${ident.kind}`,
548
+ ident.pos,
549
+ );
550
+ }
551
+ const colTok = ic.expectAnyIdent();
552
+ const typeTokens = readTypeTokens(ic);
553
+ const spec = parseTypeSpec(typeTokens);
554
+ const resolved =
555
+ opts.dialect === "sqlite" ?
556
+ resolveSqliteType(spec)
557
+ : resolvePgType(spec, opts.registries);
558
+ let baseShape = resolved.shape;
559
+ const meta = parseColumnConstraints(ic);
560
+ if (resolved.autoIncrement === true) {
561
+ meta.autoIncrement = true;
562
+ }
563
+ const pgType = typeSpecToString(spec);
564
+ const finalShape = applyColumnMeta(baseShape, meta, pgType, colTok.name);
565
+ columns.push({ key: colTok.name, shape: finalShape });
566
+ colIndex.set(colTok.name, columns.length - 1);
567
+ colMetas.set(colTok.name, meta);
568
+ baseShapeByName.set(colTok.name, baseShape);
569
+ pgTypeByName.set(colTok.name, pgType);
570
+ }
571
+
572
+ // Second pass: table constraints
573
+ for (const item of items) {
574
+ if (!isTableConstraint(item)) continue;
575
+ applyTableConstraint(item, {
576
+ columns,
577
+ colIndex,
578
+ pgTypeByName,
579
+ setTableCheck: (chk) => {
580
+ tableCheck = chk;
581
+ },
582
+ pushUniqueConstraint: (entry) => {
583
+ uniqueConstraints.push(entry);
584
+ },
585
+ });
586
+ }
587
+
588
+ const mappingAnno: ParsedTable["mappingAnno"] = { title: qn.name };
589
+ // In SQLite, a `main.foo`/`temp.foo` prefix is a database alias, not a
590
+ // schema in the PG sense. Drop it so re-emission doesn't try to write a
591
+ // `CREATE SCHEMA` prelude (which SQLite doesn't have).
592
+ if (qn.schema !== undefined && opts.dialect !== "sqlite") {
593
+ mappingAnno.schema = qn.schema;
594
+ }
595
+ if (tableCheck !== undefined) mappingAnno.check = tableCheck;
596
+
597
+ const out: ParsedTable = {
598
+ name: qn.name,
599
+ columns,
600
+ mappingAnno,
601
+ };
602
+ if (qn.schema !== undefined && opts.dialect !== "sqlite") {
603
+ out.schema = qn.schema;
604
+ }
605
+ if (uniqueConstraints.length > 0) out.uniqueConstraints = uniqueConstraints;
606
+ return out;
607
+ };
608
+
609
+ const typeSpecToString = (spec: ParsedTypeSpec): string => {
610
+ const base = spec.qualified ?? spec.baseName;
611
+ const args = spec.args.length > 0 ? `(${spec.args.join(",")})` : "";
612
+ const arr = spec.isArray ? "[]" : "";
613
+ return `${base}${args}${arr}`;
614
+ };
615
+
616
+ const isTableConstraint = (item: Token[]): boolean => {
617
+ const t0 = item[0];
618
+ if (t0 === undefined || t0.kind !== "ident") return false;
619
+ if (t0.value === "constraint") return true;
620
+ if (
621
+ t0.value === "primary" &&
622
+ item[1]?.kind === "ident" &&
623
+ item[1].value === "key" &&
624
+ item[2]?.kind === "punct" &&
625
+ item[2].value === "("
626
+ )
627
+ return true;
628
+ if (
629
+ t0.value === "unique" &&
630
+ item[1]?.kind === "punct" &&
631
+ item[1].value === "("
632
+ )
633
+ return true;
634
+ if (
635
+ t0.value === "foreign" &&
636
+ item[1]?.kind === "ident" &&
637
+ item[1].value === "key"
638
+ )
639
+ return true;
640
+ if (
641
+ t0.value === "check" &&
642
+ item[1]?.kind === "punct" &&
643
+ item[1].value === "("
644
+ )
645
+ return true;
646
+ if (t0.value === "exclude") return true;
647
+ if (t0.value === "like" && item[1]?.kind === "ident") return true;
648
+ return false;
649
+ };
650
+
651
+ type ApplyTableConstraintCtx = {
652
+ columns: Array<{ key: string; shape: shapes.Shape }>;
653
+ colIndex: Map<string, number>;
654
+ pgTypeByName: Map<string, string>;
655
+ setTableCheck: (chk: string) => void;
656
+ pushUniqueConstraint: (entry: { columns: string[]; name?: string }) => void;
657
+ };
658
+
659
+ const applyTableConstraint = (
660
+ item: Token[],
661
+ ctx: ApplyTableConstraintCtx,
662
+ ): void => {
663
+ const c = new TokenCursor(item);
664
+ let constraintName: string | undefined;
665
+ if (c.consumeIdent("constraint")) {
666
+ if (c.peek().kind === "ident") {
667
+ constraintName = c.consume().value;
668
+ }
669
+ }
670
+ if (c.consumeIdent("primary")) {
671
+ c.consumeIdent("key");
672
+ c.expectPunct("(");
673
+ const cols = readColList(c);
674
+ for (const col of cols) {
675
+ const idx = ctx.colIndex.get(col);
676
+ if (idx === undefined) continue;
677
+ const entry = ctx.columns[idx]!;
678
+ entry.shape = annotate.primary(entry.shape);
679
+ // primary implies not-null; remove optional
680
+ entry.shape = { ...entry.shape, anno: stripOptional(entry.shape.anno) };
681
+ }
682
+ return;
683
+ }
684
+ if (c.consumeIdent("unique")) {
685
+ c.expectPunct("(");
686
+ const cols = readColList(c);
687
+ if (cols.length === 1) {
688
+ const col = cols[0]!;
689
+ const idx = ctx.colIndex.get(col);
690
+ if (idx !== undefined) {
691
+ const entry = ctx.columns[idx]!;
692
+ entry.shape = annotate.unique(entry.shape);
693
+ }
694
+ return;
695
+ }
696
+ const entry: { columns: string[]; name?: string } = { columns: cols };
697
+ if (constraintName !== undefined) entry.name = constraintName;
698
+ ctx.pushUniqueConstraint(entry);
699
+ return;
700
+ }
701
+ if (c.consumeIdent("foreign")) {
702
+ c.expectIdent("key");
703
+ c.expectPunct("(");
704
+ const localCols = readColList(c);
705
+ c.expectIdent("references");
706
+ const parts = c.readDottedName();
707
+ const qn = qualifiedName(parts);
708
+ let refCols: string[] = [];
709
+ if (c.consumePunct("(")) {
710
+ refCols = readColList(c);
711
+ }
712
+ if (refCols.length === 0) refCols = ["id"];
713
+ let onDelete: ReferentialAction | undefined;
714
+ let onUpdate: ReferentialAction | undefined;
715
+ while (c.consumeIdent("on")) {
716
+ if (c.consumeIdent("delete")) {
717
+ const a = parseReferentialAction(c);
718
+ if (a !== null) onDelete = a;
719
+ } else if (c.consumeIdent("update")) {
720
+ const a = parseReferentialAction(c);
721
+ if (a !== null) onUpdate = a;
722
+ } else break;
723
+ }
724
+ if (c.consumeIdent("match")) {
725
+ if (c.peek().kind === "ident") c.consume();
726
+ }
727
+ for (let i = 0; i < localCols.length; i += 1) {
728
+ const local = localCols[i]!;
729
+ const ref = refCols[i] ?? refCols[refCols.length - 1]!;
730
+ const idx = ctx.colIndex.get(local);
731
+ if (idx === undefined) continue;
732
+ const entry = ctx.columns[idx]!;
733
+ const foreign: {
734
+ table: string;
735
+ column: string;
736
+ onDelete?: ReferentialAction;
737
+ onUpdate?: ReferentialAction;
738
+ } = {
739
+ table: qn.name,
740
+ column: ref,
741
+ };
742
+ if (onDelete !== undefined) foreign.onDelete = onDelete;
743
+ if (onUpdate !== undefined) foreign.onUpdate = onUpdate;
744
+ entry.shape = annotate.as(entry.shape, { foreign });
745
+ }
746
+ return;
747
+ }
748
+ if (c.consumeIdent("check")) {
749
+ const inner = c.skipParens();
750
+ // Try to decode as a per-column constraint matching any column in this table
751
+ for (const col of ctx.columns) {
752
+ const base = col.shape;
753
+ const dec = decodeCheck(inner, {
754
+ columnName: col.key,
755
+ isInteger: base.type === "number",
756
+ isString: base.type === "string",
757
+ isArray: base.type === "array",
758
+ });
759
+ if (dec.fullyDecoded) {
760
+ const u = dec.updates;
761
+ if (u.min !== undefined)
762
+ col.shape = annotate.as(col.shape, { min: u.min });
763
+ if (u.max !== undefined)
764
+ col.shape = annotate.as(col.shape, { max: u.max });
765
+ if (u.pattern !== undefined)
766
+ col.shape = annotate.as(col.shape, { pattern: u.pattern });
767
+ if (u.uniqueItems === true)
768
+ col.shape = annotate.as(col.shape, { uniqueItems: true });
769
+ if (u.unionLiterals !== undefined && col.shape.type === "string") {
770
+ const preserved = col.shape.anno;
771
+ const literals = u.unionLiterals.map((v) => shapes.literal(v));
772
+ const u2 = shapes.union(literals);
773
+ col.shape = {
774
+ ...u2,
775
+ anno: { ...u2.anno, ...preserved },
776
+ } as shapes.Shape;
777
+ }
778
+ return;
779
+ }
780
+ }
781
+ ctx.setTableCheck(checkExprToSql(inner));
782
+ return;
783
+ }
784
+ // exclude, like — dropped
785
+ };
786
+
787
+ const stripOptional = (anno: shapes.Shape["anno"]): shapes.Shape["anno"] => {
788
+ const out = { ...anno };
789
+ delete out.optional;
790
+ return out;
791
+ };
792
+
793
+ const readColList = (c: TokenCursor): string[] => {
794
+ const out: string[] = [];
795
+ while (!c.isPunct(")")) {
796
+ const t = c.expectAnyIdent();
797
+ out.push(t.name);
798
+ // optional ordering / nulls clauses
799
+ if (c.consumeIdent("asc") || c.consumeIdent("desc")) {
800
+ /* */
801
+ }
802
+ if (c.consumeIdent("nulls")) {
803
+ if (c.peek().kind === "ident") c.consume();
804
+ }
805
+ if (!c.consumePunct(",")) break;
806
+ }
807
+ c.expectPunct(")");
808
+ return out;
809
+ };