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,880 @@
1
+ import { describe, expect } from "vitest";
2
+ import { R } from "../../../common/result";
3
+ import { annotate, shapes } from "../../shape";
4
+ import { toSQL } from "./transform";
5
+
6
+ const ok = (res: R.Result<string, string[]>): string => {
7
+ if (!R.isOk(res))
8
+ throw new Error(`expected ok, got err: ${res.error.join("; ")}`);
9
+ return res.value;
10
+ };
11
+
12
+ const err = (res: R.Result<string, string[]>): string[] => {
13
+ if (R.isOk(res)) throw new Error(`expected err, got ok: ${res.value}`);
14
+ return res.error;
15
+ };
16
+
17
+ describe("toSQL — top-level errors", (it) => {
18
+ it("non-mapping top-level → err", () => {
19
+ expect(err(toSQL(shapes.string()))).toEqual([
20
+ "toSQL expects a top-level mapping (table) or module",
21
+ ]);
22
+ });
23
+
24
+ it("mapping without title → err", () => {
25
+ const m = shapes.mapping({ id: shapes.int() });
26
+ expect(err(toSQL(m))).toEqual([
27
+ "mapping is missing required title (table name)",
28
+ ]);
29
+ });
30
+
31
+ it("nil/undefined field → err", () => {
32
+ const m = annotate.titled(
33
+ shapes.mapping({ a: shapes.nil(), b: shapes.notDefined() }),
34
+ "t",
35
+ );
36
+ expect(err(toSQL(m))).toEqual([
37
+ "field 'a': nil cannot be a column",
38
+ "field 'b': undefined cannot be a column",
39
+ ]);
40
+ });
41
+
42
+ it("autoIncrement on non-integer → err", () => {
43
+ const m = annotate.titled(
44
+ shapes.mapping({ s: annotate.autoIncremented(shapes.string()) }),
45
+ "t",
46
+ );
47
+ expect(err(toSQL(m))).toEqual([
48
+ "field 's': autoIncrement requires an integer type",
49
+ ]);
50
+ });
51
+
52
+ it("autoIncrement on uint64 → err", () => {
53
+ const m = annotate.titled(
54
+ shapes.mapping({ x: annotate.autoIncremented(shapes.uint64()) }),
55
+ "t",
56
+ );
57
+ expect(err(toSQL(m))).toEqual([
58
+ "field 'x': autoIncrement is not supported on uint64 (numeric column)",
59
+ ]);
60
+ });
61
+
62
+ it("multiple errors collected", () => {
63
+ const m = annotate.titled(
64
+ shapes.mapping({
65
+ a: shapes.nil(),
66
+ b: annotate.autoIncremented(shapes.string()),
67
+ }),
68
+ "t",
69
+ );
70
+ expect(err(toSQL(m))).toEqual([
71
+ "field 'a': nil cannot be a column",
72
+ "field 'b': autoIncrement requires an integer type",
73
+ ]);
74
+ });
75
+ });
76
+
77
+ describe("toSQL — scalars", (it) => {
78
+ it("string → text NOT NULL", () => {
79
+ const m = annotate.titled(shapes.mapping({ name: shapes.string() }), "t");
80
+ expect(ok(toSQL(m))).toBe(
81
+ `CREATE TABLE IF NOT EXISTS "t" (\n "name" text NOT NULL\n);`,
82
+ );
83
+ });
84
+
85
+ it("optional string → no NOT NULL", () => {
86
+ const m = annotate.titled(
87
+ shapes.mapping({ name: annotate.optional(shapes.string()) }),
88
+ "t",
89
+ );
90
+ expect(ok(toSQL(m))).toBe(
91
+ `CREATE TABLE IF NOT EXISTS "t" (\n "name" text\n);`,
92
+ );
93
+ });
94
+
95
+ it("boolean → boolean NOT NULL", () => {
96
+ const m = annotate.titled(shapes.mapping({ flag: shapes.boolean() }), "t");
97
+ expect(ok(toSQL(m))).toBe(
98
+ `CREATE TABLE IF NOT EXISTS "t" (\n "flag" boolean NOT NULL\n);`,
99
+ );
100
+ });
101
+
102
+ it("date → timestamptz", () => {
103
+ const m = annotate.titled(shapes.mapping({ when: shapes.date() }), "t");
104
+ expect(ok(toSQL(m))).toBe(
105
+ `CREATE TABLE IF NOT EXISTS "t" (\n "when" timestamptz NOT NULL\n);`,
106
+ );
107
+ });
108
+
109
+ it("binary → bytea", () => {
110
+ const m = annotate.titled(shapes.mapping({ blob: shapes.binary() }), "t");
111
+ expect(ok(toSQL(m))).toBe(
112
+ `CREATE TABLE IF NOT EXISTS "t" (\n "blob" bytea NOT NULL\n);`,
113
+ );
114
+ });
115
+
116
+ it("unknown → jsonb", () => {
117
+ const m = annotate.titled(
118
+ shapes.mapping({ payload: shapes.unknown() }),
119
+ "t",
120
+ );
121
+ expect(ok(toSQL(m))).toBe(
122
+ `CREATE TABLE IF NOT EXISTS "t" (\n "payload" jsonb NOT NULL\n);`,
123
+ );
124
+ });
125
+ });
126
+
127
+ describe("toSQL — strings", (it) => {
128
+ it("varchar(N) when max set; no min check", () => {
129
+ const m = annotate.titled(
130
+ shapes.mapping({ n: annotate.max(shapes.string(), 32) }),
131
+ "t",
132
+ );
133
+ expect(ok(toSQL(m))).toBe(
134
+ `CREATE TABLE IF NOT EXISTS "t" (\n "n" varchar(32) NOT NULL\n);`,
135
+ );
136
+ });
137
+
138
+ it("text + CHECK char_length when only min set", () => {
139
+ const m = annotate.titled(
140
+ shapes.mapping({ n: annotate.min(shapes.string(), 3) }),
141
+ "t",
142
+ );
143
+ expect(ok(toSQL(m))).toBe(
144
+ `CREATE TABLE IF NOT EXISTS "t" (\n "n" text NOT NULL CHECK (char_length("n") >= 3)\n);`,
145
+ );
146
+ });
147
+
148
+ it("RegExp pattern → ~ check using .source", () => {
149
+ const m = annotate.titled(
150
+ shapes.mapping({ n: annotate.pattern(shapes.string(), /^[a-z]+$/) }),
151
+ "t",
152
+ );
153
+ expect(ok(toSQL(m))).toBe(
154
+ `CREATE TABLE IF NOT EXISTS "t" (\n "n" text NOT NULL CHECK ("n" ~ '^[a-z]+$')\n);`,
155
+ );
156
+ });
157
+
158
+ it("string pattern stored verbatim; single quotes escaped", () => {
159
+ const m = annotate.titled(
160
+ shapes.mapping({ n: annotate.pattern(shapes.string(), "it's") }),
161
+ "t",
162
+ );
163
+ expect(ok(toSQL(m))).toBe(
164
+ `CREATE TABLE IF NOT EXISTS "t" (\n "n" text NOT NULL CHECK ("n" ~ 'it''s')\n);`,
165
+ );
166
+ });
167
+ });
168
+
169
+ describe("toSQL — numbers", (it) => {
170
+ it("number() → double precision (no checks)", () => {
171
+ const m = annotate.titled(shapes.mapping({ x: shapes.number() }), "t");
172
+ expect(ok(toSQL(m))).toBe(
173
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" double precision NOT NULL\n);`,
174
+ );
175
+ });
176
+
177
+ it("int() → bigint (no checks)", () => {
178
+ const m = annotate.titled(shapes.mapping({ x: shapes.int() }), "t");
179
+ expect(ok(toSQL(m))).toBe(
180
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" bigint NOT NULL\n);`,
181
+ );
182
+ });
183
+
184
+ it("int8 → smallint + tag-range CHECKs", () => {
185
+ const m = annotate.titled(shapes.mapping({ x: shapes.int8() }), "t");
186
+ expect(ok(toSQL(m))).toBe(
187
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" smallint NOT NULL CHECK ("x" >= -128) CHECK ("x" <= 127)\n);`,
188
+ );
189
+ });
190
+
191
+ it("uint8 → smallint + [0, 255] CHECKs", () => {
192
+ const m = annotate.titled(shapes.mapping({ x: shapes.uint8() }), "t");
193
+ expect(ok(toSQL(m))).toBe(
194
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" smallint NOT NULL CHECK ("x" >= 0) CHECK ("x" <= 255)\n);`,
195
+ );
196
+ });
197
+
198
+ it("int16 → smallint, no CHECKs (range matches pg type)", () => {
199
+ const m = annotate.titled(shapes.mapping({ x: shapes.int16() }), "t");
200
+ expect(ok(toSQL(m))).toBe(
201
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" smallint NOT NULL\n);`,
202
+ );
203
+ });
204
+
205
+ it("uint16 → integer + [0, 65535] CHECKs", () => {
206
+ const m = annotate.titled(shapes.mapping({ x: shapes.uint16() }), "t");
207
+ expect(ok(toSQL(m))).toBe(
208
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" integer NOT NULL CHECK ("x" >= 0) CHECK ("x" <= 65535)\n);`,
209
+ );
210
+ });
211
+
212
+ it("int32 → integer, no CHECKs", () => {
213
+ const m = annotate.titled(shapes.mapping({ x: shapes.int32() }), "t");
214
+ expect(ok(toSQL(m))).toBe(
215
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" integer NOT NULL\n);`,
216
+ );
217
+ });
218
+
219
+ it("uint32 → bigint + [0, 4294967295] CHECKs", () => {
220
+ const m = annotate.titled(shapes.mapping({ x: shapes.uint32() }), "t");
221
+ expect(ok(toSQL(m))).toBe(
222
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" bigint NOT NULL CHECK ("x" >= 0) CHECK ("x" <= 4294967295)\n);`,
223
+ );
224
+ });
225
+
226
+ it("int64 → bigint, no CHECKs", () => {
227
+ const m = annotate.titled(shapes.mapping({ x: shapes.int64() }), "t");
228
+ expect(ok(toSQL(m))).toBe(
229
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" bigint NOT NULL\n);`,
230
+ );
231
+ });
232
+
233
+ it("uint64 → numeric(20,0) + >= 0 CHECK", () => {
234
+ const m = annotate.titled(shapes.mapping({ x: shapes.uint64() }), "t");
235
+ expect(ok(toSQL(m))).toBe(
236
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" numeric(20,0) NOT NULL CHECK ("x" >= 0)\n);`,
237
+ );
238
+ });
239
+
240
+ it("float / float32 → real", () => {
241
+ const m = annotate.titled(
242
+ shapes.mapping({ a: shapes.float(), b: shapes.float32() }),
243
+ "t",
244
+ );
245
+ expect(ok(toSQL(m))).toBe(
246
+ `CREATE TABLE IF NOT EXISTS "t" (\n "a" real NOT NULL,\n "b" real NOT NULL\n);`,
247
+ );
248
+ });
249
+
250
+ it("float64 → double precision", () => {
251
+ const m = annotate.titled(shapes.mapping({ x: shapes.float64() }), "t");
252
+ expect(ok(toSQL(m))).toBe(
253
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" double precision NOT NULL\n);`,
254
+ );
255
+ });
256
+
257
+ it("tighter anno.min/max replaces tag bound", () => {
258
+ const m = annotate.titled(
259
+ shapes.mapping({ x: annotate.as(shapes.int16(), { min: 0, max: 100 }) }),
260
+ "t",
261
+ );
262
+ expect(ok(toSQL(m))).toBe(
263
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" smallint NOT NULL CHECK ("x" >= 0) CHECK ("x" <= 100)\n);`,
264
+ );
265
+ });
266
+
267
+ it("wider anno.min/max keeps the tag bound (intersection)", () => {
268
+ const m = annotate.titled(
269
+ shapes.mapping({
270
+ x: annotate.as(shapes.uint8(), { min: -1000, max: 1000 }),
271
+ }),
272
+ "t",
273
+ );
274
+ expect(ok(toSQL(m))).toBe(
275
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" smallint NOT NULL CHECK ("x" >= 0) CHECK ("x" <= 255)\n);`,
276
+ );
277
+ });
278
+
279
+ it("anno.min/max on plain number → CHECKs", () => {
280
+ const m = annotate.titled(
281
+ shapes.mapping({ x: annotate.as(shapes.number(), { min: 0, max: 1 }) }),
282
+ "t",
283
+ );
284
+ expect(ok(toSQL(m))).toBe(
285
+ `CREATE TABLE IF NOT EXISTS "t" (\n "x" double precision NOT NULL CHECK ("x" >= 0) CHECK ("x" <= 1)\n);`,
286
+ );
287
+ });
288
+ });
289
+
290
+ describe("toSQL — arrays", (it) => {
291
+ it("array(string) → text[]", () => {
292
+ const m = annotate.titled(
293
+ shapes.mapping({ tags: shapes.array(shapes.string()) }),
294
+ "t",
295
+ );
296
+ expect(ok(toSQL(m))).toBe(
297
+ `CREATE TABLE IF NOT EXISTS "t" (\n "tags" text[] NOT NULL\n);`,
298
+ );
299
+ });
300
+
301
+ it("array(int32) → integer[]", () => {
302
+ const m = annotate.titled(
303
+ shapes.mapping({ ns: shapes.array(shapes.int32()) }),
304
+ "t",
305
+ );
306
+ expect(ok(toSQL(m))).toBe(
307
+ `CREATE TABLE IF NOT EXISTS "t" (\n "ns" integer[] NOT NULL\n);`,
308
+ );
309
+ });
310
+
311
+ it("array(varchar) element honors element max", () => {
312
+ const m = annotate.titled(
313
+ shapes.mapping({ ns: shapes.array(annotate.max(shapes.string(), 8)) }),
314
+ "t",
315
+ );
316
+ expect(ok(toSQL(m))).toBe(
317
+ `CREATE TABLE IF NOT EXISTS "t" (\n "ns" varchar(8)[] NOT NULL\n);`,
318
+ );
319
+ });
320
+
321
+ it("array(mapping) → jsonb", () => {
322
+ const m = annotate.titled(
323
+ shapes.mapping({
324
+ rows: shapes.array(shapes.mapping({ a: shapes.string() })),
325
+ }),
326
+ "t",
327
+ );
328
+ expect(ok(toSQL(m))).toBe(
329
+ `CREATE TABLE IF NOT EXISTS "t" (\n "rows" jsonb NOT NULL\n);`,
330
+ );
331
+ });
332
+
333
+ it("array min/max → cardinality CHECKs", () => {
334
+ const m = annotate.titled(
335
+ shapes.mapping({
336
+ tags: annotate.as(shapes.array(shapes.string()), { min: 1, max: 3 }),
337
+ }),
338
+ "t",
339
+ );
340
+ expect(ok(toSQL(m))).toBe(
341
+ `CREATE TABLE IF NOT EXISTS "t" (\n "tags" text[] NOT NULL CHECK (cardinality("tags") >= 1) CHECK (cardinality("tags") <= 3)\n);`,
342
+ );
343
+ });
344
+
345
+ it("anno.unique on array → column-level UNIQUE (cross-row)", () => {
346
+ const m = annotate.titled(
347
+ shapes.mapping({ tags: annotate.unique(shapes.array(shapes.string())) }),
348
+ "t",
349
+ );
350
+ expect(ok(toSQL(m))).toBe(
351
+ `CREATE TABLE IF NOT EXISTS "t" (\n "tags" text[] NOT NULL UNIQUE\n);`,
352
+ );
353
+ });
354
+
355
+ it("anno.uniqueItems on array → CHECK via helper + prelude function", () => {
356
+ const m = annotate.titled(
357
+ shapes.mapping({
358
+ tags: annotate.uniqueItems(shapes.array(shapes.string())),
359
+ }),
360
+ "t",
361
+ );
362
+ expect(ok(toSQL(m))).toBe(
363
+ [
364
+ `CREATE OR REPLACE FUNCTION shapecraft_array_is_unique(anyarray) RETURNS boolean`,
365
+ ` LANGUAGE sql IMMUTABLE PARALLEL SAFE`,
366
+ ` AS $$ SELECT cardinality($1) = cardinality(ARRAY(SELECT DISTINCT unnest($1))) $$;`,
367
+ ``,
368
+ `CREATE TABLE IF NOT EXISTS "t" (`,
369
+ ` "tags" text[] NOT NULL CHECK (shapecraft_array_is_unique("tags"))`,
370
+ `);`,
371
+ ].join("\n"),
372
+ );
373
+ });
374
+
375
+ it("anno.unique + anno.uniqueItems → both UNIQUE and element CHECK", () => {
376
+ const m = annotate.titled(
377
+ shapes.mapping({
378
+ tags: annotate.unique(
379
+ annotate.uniqueItems(shapes.array(shapes.string())),
380
+ ),
381
+ }),
382
+ "t",
383
+ );
384
+ expect(ok(toSQL(m))).toBe(
385
+ [
386
+ `CREATE OR REPLACE FUNCTION shapecraft_array_is_unique(anyarray) RETURNS boolean`,
387
+ ` LANGUAGE sql IMMUTABLE PARALLEL SAFE`,
388
+ ` AS $$ SELECT cardinality($1) = cardinality(ARRAY(SELECT DISTINCT unnest($1))) $$;`,
389
+ ``,
390
+ `CREATE TABLE IF NOT EXISTS "t" (`,
391
+ ` "tags" text[] NOT NULL UNIQUE CHECK (shapecraft_array_is_unique("tags"))`,
392
+ `);`,
393
+ ].join("\n"),
394
+ );
395
+ });
396
+
397
+ it("anno.uniqueItems on two arrays → only one prelude function emitted", () => {
398
+ const m = annotate.titled(
399
+ shapes.mapping({
400
+ a: annotate.uniqueItems(shapes.array(shapes.string())),
401
+ b: annotate.uniqueItems(shapes.array(shapes.int32())),
402
+ }),
403
+ "t",
404
+ );
405
+ const sql = ok(toSQL(m));
406
+ const fnOccurrences =
407
+ sql.split("CREATE OR REPLACE FUNCTION shapecraft_array_is_unique")
408
+ .length - 1;
409
+ expect(fnOccurrences).toBe(1);
410
+ });
411
+
412
+ it("anno.uniqueItems on jsonb-backed array → err", () => {
413
+ const m = annotate.titled(
414
+ shapes.mapping({
415
+ rows: annotate.uniqueItems(
416
+ shapes.array(shapes.mapping({ a: shapes.string() })),
417
+ ),
418
+ }),
419
+ "t",
420
+ );
421
+ expect(err(toSQL(m))).toEqual([
422
+ "field 'rows': uniqueItems is not supported on jsonb array columns",
423
+ ]);
424
+ });
425
+ });
426
+
427
+ describe("toSQL — vector", (it) => {
428
+ it("vector(3, float32) → vector(3)", () => {
429
+ const m = annotate.titled(
430
+ shapes.mapping({ emb: shapes.vector(3, shapes.float32()) }),
431
+ "t",
432
+ );
433
+ expect(ok(toSQL(m))).toBe(
434
+ `CREATE TABLE IF NOT EXISTS "t" (\n "emb" vector(3) NOT NULL\n);`,
435
+ );
436
+ });
437
+
438
+ it("vector(384) → vector(384) (default format ignored)", () => {
439
+ const m = annotate.titled(shapes.mapping({ emb: shapes.vector(384) }), "t");
440
+ expect(ok(toSQL(m))).toBe(
441
+ `CREATE TABLE IF NOT EXISTS "t" (\n "emb" vector(384) NOT NULL\n);`,
442
+ );
443
+ });
444
+ });
445
+
446
+ describe("toSQL — mappings / records as fields → jsonb", (it) => {
447
+ it("nested mapping → jsonb", () => {
448
+ const m = annotate.titled(
449
+ shapes.mapping({
450
+ profile: shapes.mapping({ a: shapes.string() }),
451
+ }),
452
+ "t",
453
+ );
454
+ expect(ok(toSQL(m))).toBe(
455
+ `CREATE TABLE IF NOT EXISTS "t" (\n "profile" jsonb NOT NULL\n);`,
456
+ );
457
+ });
458
+
459
+ it("record → jsonb", () => {
460
+ const m = annotate.titled(
461
+ shapes.mapping({
462
+ counters: shapes.record(shapes.string(), shapes.int32()),
463
+ }),
464
+ "t",
465
+ );
466
+ expect(ok(toSQL(m))).toBe(
467
+ `CREATE TABLE IF NOT EXISTS "t" (\n "counters" jsonb NOT NULL\n);`,
468
+ );
469
+ });
470
+ });
471
+
472
+ describe("toSQL — unions", (it) => {
473
+ it("string-literal union with title → CREATE TYPE enum + column type ref", () => {
474
+ const role = annotate.titled(
475
+ shapes.union(shapes.literal("admin"), shapes.literal("user")),
476
+ "user_role",
477
+ );
478
+ const m = annotate.titled(shapes.mapping({ role }), "users");
479
+ expect(ok(toSQL(m))).toBe(
480
+ `CREATE TYPE "user_role" AS ENUM ('admin', 'user');\n\nCREATE TABLE IF NOT EXISTS "users" (\n "role" "user_role" NOT NULL\n);`,
481
+ );
482
+ });
483
+
484
+ it("string-literal union without title → text + IN check", () => {
485
+ const u = shapes.union(shapes.literal("a"), shapes.literal("b"));
486
+ const m = annotate.titled(shapes.mapping({ k: u }), "t");
487
+ expect(ok(toSQL(m))).toBe(
488
+ `CREATE TABLE IF NOT EXISTS "t" (\n "k" text NOT NULL CHECK ("k" IN ('a', 'b'))\n);`,
489
+ );
490
+ });
491
+
492
+ it("mixed / non-literal union → jsonb", () => {
493
+ const u = shapes.union(shapes.literal("a"), shapes.number());
494
+ const m = annotate.titled(shapes.mapping({ k: u }), "t");
495
+ expect(ok(toSQL(m))).toBe(
496
+ `CREATE TABLE IF NOT EXISTS "t" (\n "k" jsonb NOT NULL\n);`,
497
+ );
498
+ });
499
+
500
+ it("same titled enum used twice → only one CREATE TYPE", () => {
501
+ const role = annotate.titled(
502
+ shapes.union(shapes.literal("admin"), shapes.literal("user")),
503
+ "user_role",
504
+ );
505
+ const m = annotate.titled(
506
+ shapes.mapping({ role_a: role, role_b: role }),
507
+ "t",
508
+ );
509
+ expect(ok(toSQL(m))).toBe(
510
+ `CREATE TYPE "user_role" AS ENUM ('admin', 'user');\n\nCREATE TABLE IF NOT EXISTS "t" (\n "role_a" "user_role" NOT NULL,\n "role_b" "user_role" NOT NULL\n);`,
511
+ );
512
+ });
513
+ });
514
+
515
+ describe("toSQL — annotations", (it) => {
516
+ it("primary → PRIMARY KEY (always NOT NULL)", () => {
517
+ const m = annotate.titled(
518
+ shapes.mapping({ id: annotate.primary(shapes.int()) }),
519
+ "t",
520
+ );
521
+ expect(ok(toSQL(m))).toBe(
522
+ `CREATE TABLE IF NOT EXISTS "t" (\n "id" bigint NOT NULL PRIMARY KEY\n);`,
523
+ );
524
+ });
525
+
526
+ it("unique on scalar → UNIQUE", () => {
527
+ const m = annotate.titled(
528
+ shapes.mapping({ slug: annotate.unique(shapes.string()) }),
529
+ "t",
530
+ );
531
+ expect(ok(toSQL(m))).toBe(
532
+ `CREATE TABLE IF NOT EXISTS "t" (\n "slug" text NOT NULL UNIQUE\n);`,
533
+ );
534
+ });
535
+
536
+ it("autoIncrement on int → GENERATED ALWAYS AS IDENTITY", () => {
537
+ const m = annotate.titled(
538
+ shapes.mapping({
539
+ id: annotate.primary(annotate.autoIncremented(shapes.int())),
540
+ }),
541
+ "t",
542
+ );
543
+ expect(ok(toSQL(m))).toBe(
544
+ `CREATE TABLE IF NOT EXISTS "t" (\n "id" bigint NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY\n);`,
545
+ );
546
+ });
547
+
548
+ it("foreign → REFERENCES", () => {
549
+ const m = annotate.titled(
550
+ shapes.mapping({
551
+ user_id: annotate.foreign(shapes.int(), "users", "id"),
552
+ }),
553
+ "t",
554
+ );
555
+ expect(ok(toSQL(m))).toBe(
556
+ `CREATE TABLE IF NOT EXISTS "t" (\n "user_id" bigint NOT NULL REFERENCES "users"("id")\n);`,
557
+ );
558
+ });
559
+
560
+ it("string default", () => {
561
+ const m = annotate.titled(
562
+ shapes.mapping({
563
+ s: annotate.defaulted(shapes.string(), "hi"),
564
+ }),
565
+ "t",
566
+ );
567
+ expect(ok(toSQL(m))).toBe(
568
+ `CREATE TABLE IF NOT EXISTS "t" (\n "s" text NOT NULL DEFAULT 'hi'\n);`,
569
+ );
570
+ });
571
+
572
+ it("number default", () => {
573
+ const m = annotate.titled(
574
+ shapes.mapping({
575
+ n: annotate.defaulted(shapes.int32(), 42),
576
+ }),
577
+ "t",
578
+ );
579
+ expect(ok(toSQL(m))).toBe(
580
+ `CREATE TABLE IF NOT EXISTS "t" (\n "n" integer NOT NULL DEFAULT 42\n);`,
581
+ );
582
+ });
583
+
584
+ it("boolean default", () => {
585
+ const m = annotate.titled(
586
+ shapes.mapping({
587
+ b: annotate.defaulted(shapes.boolean(), true),
588
+ }),
589
+ "t",
590
+ );
591
+ expect(ok(toSQL(m))).toBe(
592
+ `CREATE TABLE IF NOT EXISTS "t" (\n "b" boolean NOT NULL DEFAULT TRUE\n);`,
593
+ );
594
+ });
595
+
596
+ it("null default on optional column", () => {
597
+ const m = annotate.titled(
598
+ shapes.mapping({
599
+ s: annotate.as(annotate.optional(shapes.string()), { default: null }),
600
+ }),
601
+ "t",
602
+ );
603
+ expect(ok(toSQL(m))).toBe(
604
+ `CREATE TABLE IF NOT EXISTS "t" (\n "s" text DEFAULT NULL\n);`,
605
+ );
606
+ });
607
+
608
+ it("object default on jsonb column", () => {
609
+ const m = annotate.titled(
610
+ shapes.mapping({
611
+ meta: annotate.as(shapes.unknown(), { default: { a: 1 } }),
612
+ }),
613
+ "t",
614
+ );
615
+ expect(ok(toSQL(m))).toBe(
616
+ `CREATE TABLE IF NOT EXISTS "t" (\n "meta" jsonb NOT NULL DEFAULT '{"a":1}'::jsonb\n);`,
617
+ );
618
+ });
619
+
620
+ it("unsupported object default on non-jsonb column → err", () => {
621
+ const m = annotate.titled(
622
+ shapes.mapping({
623
+ s: annotate.as(shapes.string(), { default: { a: 1 } }),
624
+ }),
625
+ "t",
626
+ );
627
+ expect(err(toSQL(m))).toEqual([
628
+ "field 's': default value of type object is not supported for column type text",
629
+ ]);
630
+ });
631
+ });
632
+
633
+ describe("toSQL — end-to-end users table", (it) => {
634
+ it("realistic users table with enum, vector, jsonb, default", () => {
635
+ const role = annotate.titled(
636
+ shapes.union(shapes.literal("admin"), shapes.literal("user")),
637
+ "user_role",
638
+ );
639
+ const m = annotate.titled(
640
+ shapes.mapping({
641
+ id: annotate.primary(annotate.autoIncremented(shapes.int())),
642
+ email: annotate.unique(
643
+ annotate.pattern(annotate.max(shapes.string(), 255), /.+@.+/),
644
+ ),
645
+ age: shapes.uint8(),
646
+ role,
647
+ embedding: shapes.vector(384, shapes.float32()),
648
+ profile: shapes.mapping({ bio: shapes.string() }),
649
+ created_at: annotate.as(shapes.date(), { default: null }),
650
+ }),
651
+ "users",
652
+ );
653
+
654
+ const sql = ok(toSQL(m));
655
+ expect(sql).toBe(
656
+ [
657
+ `CREATE TYPE "user_role" AS ENUM ('admin', 'user');`,
658
+ "",
659
+ `CREATE TABLE IF NOT EXISTS "users" (`,
660
+ ` "id" bigint NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,`,
661
+ ` "email" varchar(255) NOT NULL UNIQUE CHECK ("email" ~ '.+@.+'),`,
662
+ ` "age" smallint NOT NULL CHECK ("age" >= 0) CHECK ("age" <= 255),`,
663
+ ` "role" "user_role" NOT NULL,`,
664
+ ` "embedding" vector(384) NOT NULL,`,
665
+ ` "profile" jsonb NOT NULL,`,
666
+ ` "created_at" timestamptz NOT NULL DEFAULT NULL`,
667
+ `);`,
668
+ ].join("\n"),
669
+ );
670
+ });
671
+ });
672
+
673
+ describe("toSQL — comments", (it) => {
674
+ it("column with description → single -- comment above it", () => {
675
+ const m = annotate.titled(
676
+ shapes.mapping({
677
+ name: annotate.described(shapes.string(), "the user's name"),
678
+ }),
679
+ "t",
680
+ );
681
+ expect(ok(toSQL(m))).toBe(
682
+ [
683
+ `CREATE TABLE IF NOT EXISTS "t" (`,
684
+ ` -- the user's name`,
685
+ ` "name" text NOT NULL`,
686
+ `);`,
687
+ ].join("\n"),
688
+ );
689
+ });
690
+
691
+ it("column with description + comment → /* */ block above it", () => {
692
+ const m = annotate.titled(
693
+ shapes.mapping({
694
+ name: annotate.commented(
695
+ annotate.described(shapes.string(), "display name"),
696
+ "indexed",
697
+ ),
698
+ }),
699
+ "t",
700
+ );
701
+ expect(ok(toSQL(m))).toBe(
702
+ [
703
+ `CREATE TABLE IF NOT EXISTS "t" (`,
704
+ ` /*`,
705
+ ` * display name`,
706
+ ` * indexed`,
707
+ ` */`,
708
+ ` "name" text NOT NULL`,
709
+ `);`,
710
+ ].join("\n"),
711
+ );
712
+ });
713
+
714
+ it("column with format → -- (format: ...) above it", () => {
715
+ const m = annotate.titled(
716
+ shapes.mapping({ email: annotate.format(shapes.string(), "email") }),
717
+ "t",
718
+ );
719
+ expect(ok(toSQL(m))).toBe(
720
+ [
721
+ `CREATE TABLE IF NOT EXISTS "t" (`,
722
+ ` -- (format: email)`,
723
+ ` "email" text NOT NULL`,
724
+ `);`,
725
+ ].join("\n"),
726
+ );
727
+ });
728
+
729
+ it("table-level description → comment before CREATE TABLE", () => {
730
+ const m = annotate.described(
731
+ annotate.titled(shapes.mapping({ id: shapes.int() }), "t"),
732
+ "rows of stuff",
733
+ );
734
+ expect(ok(toSQL(m))).toBe(
735
+ [
736
+ `-- rows of stuff`,
737
+ `CREATE TABLE IF NOT EXISTS "t" (`,
738
+ ` "id" bigint NOT NULL`,
739
+ `);`,
740
+ ].join("\n"),
741
+ );
742
+ });
743
+
744
+ it("comments coexist with native column constraints", () => {
745
+ const m = annotate.titled(
746
+ shapes.mapping({
747
+ id: annotate.described(
748
+ annotate.primary(shapes.int()),
749
+ "the primary key",
750
+ ),
751
+ }),
752
+ "t",
753
+ );
754
+ expect(ok(toSQL(m))).toBe(
755
+ [
756
+ `CREATE TABLE IF NOT EXISTS "t" (`,
757
+ ` -- the primary key`,
758
+ ` "id" bigint NOT NULL PRIMARY KEY`,
759
+ `);`,
760
+ ].join("\n"),
761
+ );
762
+ });
763
+ });
764
+
765
+ describe("toSQL — module / multi-table", (it) => {
766
+ it("emits CREATE TABLE for each table in module", () => {
767
+ const m = shapes.module({
768
+ product: annotate.titled(
769
+ shapes.mapping({
770
+ id: annotate.autoIncremented(annotate.primary(shapes.int64())),
771
+ name: shapes.string(),
772
+ }),
773
+ "product",
774
+ ),
775
+ order: annotate.titled(
776
+ shapes.mapping({
777
+ id: annotate.autoIncremented(annotate.primary(shapes.int64())),
778
+ }),
779
+ "order",
780
+ ),
781
+ });
782
+ const sql = ok(toSQL(m));
783
+ expect(sql).toContain(`CREATE TABLE IF NOT EXISTS "product"`);
784
+ expect(sql).toContain(`CREATE TABLE IF NOT EXISTS "order"`);
785
+ });
786
+
787
+ it("emits tables in topological order (referenced first)", () => {
788
+ const m = shapes.module({
789
+ post: annotate.titled(
790
+ shapes.mapping({
791
+ id: annotate.autoIncremented(annotate.primary(shapes.int64())),
792
+ author_id: annotate.foreign(shapes.int64(), "user", "id"),
793
+ }),
794
+ "post",
795
+ ),
796
+ user: annotate.titled(
797
+ shapes.mapping({
798
+ id: annotate.autoIncremented(annotate.primary(shapes.int64())),
799
+ }),
800
+ "user",
801
+ ),
802
+ });
803
+ const sql = ok(toSQL(m));
804
+ const userIdx = sql.indexOf(`CREATE TABLE IF NOT EXISTS "user"`);
805
+ const postIdx = sql.indexOf(`CREATE TABLE IF NOT EXISTS "post"`);
806
+ expect(userIdx).toBeGreaterThanOrEqual(0);
807
+ expect(postIdx).toBeGreaterThan(userIdx);
808
+ });
809
+
810
+ it("composite PK → table-level PRIMARY KEY constraint", () => {
811
+ const m = shapes.module({
812
+ ab: annotate.titled(
813
+ shapes.mapping({
814
+ a_id: annotate.primary(annotate.foreign(shapes.int64(), "a", "id")),
815
+ b_id: annotate.primary(annotate.foreign(shapes.int64(), "b", "id")),
816
+ }),
817
+ "ab",
818
+ ),
819
+ a: annotate.titled(
820
+ shapes.mapping({
821
+ id: annotate.autoIncremented(annotate.primary(shapes.int64())),
822
+ }),
823
+ "a",
824
+ ),
825
+ b: annotate.titled(
826
+ shapes.mapping({
827
+ id: annotate.autoIncremented(annotate.primary(shapes.int64())),
828
+ }),
829
+ "b",
830
+ ),
831
+ });
832
+ const sql = ok(toSQL(m));
833
+ expect(sql).toContain(`PRIMARY KEY ("a_id", "b_id")`);
834
+ expect(sql).not.toContain(
835
+ `"a_id" bigint NOT NULL REFERENCES "a"("id") PRIMARY KEY`,
836
+ );
837
+ });
838
+
839
+ it("FK cycle → err", () => {
840
+ const m = shapes.module({
841
+ a: annotate.titled(
842
+ shapes.mapping({
843
+ b_ref: annotate.foreign(shapes.int64(), "b", "id"),
844
+ }),
845
+ "a",
846
+ ),
847
+ b: annotate.titled(
848
+ shapes.mapping({
849
+ a_ref: annotate.foreign(shapes.int64(), "a", "id"),
850
+ }),
851
+ "b",
852
+ ),
853
+ });
854
+ expect(err(toSQL(m))[0]).toMatch(/FK cycle/);
855
+ });
856
+
857
+ it("self-reference allowed (1-node cycle)", () => {
858
+ const m = shapes.module({
859
+ category: annotate.titled(
860
+ shapes.mapping({
861
+ id: annotate.autoIncremented(annotate.primary(shapes.int64())),
862
+ parent: annotate.foreign(shapes.int64(), "category", "id"),
863
+ }),
864
+ "category",
865
+ ),
866
+ });
867
+ const sql = ok(toSQL(m));
868
+ expect(sql).toContain(`CREATE TABLE IF NOT EXISTS "category"`);
869
+ expect(sql).toContain(`REFERENCES "category"("id")`);
870
+ });
871
+
872
+ it("module errors are prefixed by table name", () => {
873
+ const m = shapes.module({
874
+ bad: annotate.titled(shapes.mapping({ x: shapes.nil() }), "bad"),
875
+ });
876
+ expect(err(toSQL(m))).toEqual([
877
+ "table 'bad': field 'x': nil cannot be a column",
878
+ ]);
879
+ });
880
+ });