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.
- package/CLAUDE.md +227 -0
- package/README.md +22 -0
- package/apps/cli/node_modules/.bin/prettier +21 -0
- package/apps/cli/node_modules/.bin/tsc +21 -0
- package/apps/cli/node_modules/.bin/tsserver +21 -0
- package/apps/cli/node_modules/.bin/tsx +21 -0
- package/apps/cli/node_modules/.bin/vitest +21 -0
- package/apps/cli/package.json +47 -0
- package/apps/cli/src/index.ts +98 -0
- package/apps/cli/tsconfig.cjs.json +10 -0
- package/apps/cli/tsconfig.esm.json +10 -0
- package/apps/cli/tsconfig.json +22 -0
- package/package.json +16 -0
- package/packages/core/node_modules/.bin/prettier +21 -0
- package/packages/core/node_modules/.bin/tsc +21 -0
- package/packages/core/node_modules/.bin/tsserver +21 -0
- package/packages/core/node_modules/.bin/tsx +21 -0
- package/packages/core/node_modules/.bin/vitest +21 -0
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/core/package.json +44 -0
- package/packages/core/src/common/array.test.ts +19 -0
- package/packages/core/src/common/array.ts +15 -0
- package/packages/core/src/common/index.ts +5 -0
- package/packages/core/src/common/is.ts +23 -0
- package/packages/core/src/common/object.ts +35 -0
- package/packages/core/src/common/phantom.ts +1 -0
- package/packages/core/src/common/result.ts +43 -0
- package/packages/core/src/common/string.ts +28 -0
- package/packages/core/src/common/types.ts +34 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/shape/annotate.ts +139 -0
- package/packages/core/src/shape/annotation.ts +47 -0
- package/packages/core/src/shape/base.ts +71 -0
- package/packages/core/src/shape/builder.test.ts +728 -0
- package/packages/core/src/shape/builder.ts +475 -0
- package/packages/core/src/shape/error.ts +4 -0
- package/packages/core/src/shape/index.ts +3 -0
- package/packages/core/src/shape/number.ts +118 -0
- package/packages/core/src/shape/shape.test.ts +792 -0
- package/packages/core/src/shape/shape.ts +377 -0
- package/packages/core/src/shape/tags.ts +14 -0
- package/packages/core/src/shape/transforms/index.ts +3 -0
- package/packages/core/src/shape/transforms/json-schema/index.ts +2 -0
- package/packages/core/src/shape/transforms/json-schema/transform.test.ts +850 -0
- package/packages/core/src/shape/transforms/json-schema/transform.ts +882 -0
- package/packages/core/src/shape/transforms/json-schema/types.ts +132 -0
- package/packages/core/src/shape/transforms/sql/dialects/dialect.ts +89 -0
- package/packages/core/src/shape/transforms/sql/dialects/index.ts +14 -0
- package/packages/core/src/shape/transforms/sql/dialects/postgres.ts +392 -0
- package/packages/core/src/shape/transforms/sql/dialects/sqlite.ts +333 -0
- package/packages/core/src/shape/transforms/sql/from-sql.test.ts +704 -0
- package/packages/core/src/shape/transforms/sql/from-sql.ts +210 -0
- package/packages/core/src/shape/transforms/sql/index.ts +3 -0
- package/packages/core/src/shape/transforms/sql/options.ts +6 -0
- package/packages/core/src/shape/transforms/sql/parser/check-decoder.ts +457 -0
- package/packages/core/src/shape/transforms/sql/parser/create-domain.ts +105 -0
- package/packages/core/src/shape/transforms/sql/parser/create-table.ts +809 -0
- package/packages/core/src/shape/transforms/sql/parser/create-type.ts +91 -0
- package/packages/core/src/shape/transforms/sql/parser/cursor.ts +179 -0
- package/packages/core/src/shape/transforms/sql/parser/default-decoder.ts +129 -0
- package/packages/core/src/shape/transforms/sql/parser/lexer.ts +289 -0
- package/packages/core/src/shape/transforms/sql/parser/pg-types.ts +247 -0
- package/packages/core/src/shape/transforms/sql/parser/sqlite-types.ts +103 -0
- package/packages/core/src/shape/transforms/sql/parser/statements.ts +127 -0
- package/packages/core/src/shape/transforms/sql/parser/type-spec.ts +159 -0
- package/packages/core/src/shape/transforms/sql/transform.sqlite.test.ts +448 -0
- package/packages/core/src/shape/transforms/sql/transform.test.ts +880 -0
- package/packages/core/src/shape/transforms/sql/transform.ts +295 -0
- package/packages/core/src/shape/transforms/typescript/index.ts +1 -0
- package/packages/core/src/shape/transforms/typescript/transform.ts +211 -0
- package/packages/core/src/shape/tuple.test.ts +171 -0
- package/packages/core/src/shape/validate.ts +413 -0
- package/packages/core/tsconfig.cjs.json +11 -0
- package/packages/core/tsconfig.esm.json +10 -0
- package/packages/core/tsconfig.json +23 -0
- package/packages/samples/node_modules/.bin/prettier +21 -0
- package/packages/samples/node_modules/.bin/tsc +21 -0
- package/packages/samples/node_modules/.bin/tsserver +21 -0
- package/packages/samples/node_modules/.bin/tsx +21 -0
- package/packages/samples/node_modules/.bin/vitest +21 -0
- package/packages/samples/package.json +47 -0
- package/packages/samples/src/blog.ts +49 -0
- package/packages/samples/src/config.ts +50 -0
- package/packages/samples/src/ecommerce.ts +65 -0
- package/packages/samples/src/embeddings.ts +43 -0
- package/packages/samples/src/events.ts +52 -0
- package/packages/samples/src/geometry.ts +62 -0
- package/packages/samples/src/index.ts +9 -0
- package/packages/samples/src/relational.ts +17 -0
- package/packages/samples/src/tuples.ts +67 -0
- package/packages/samples/src/user.ts +9 -0
- package/packages/samples/tsconfig.cjs.json +11 -0
- package/packages/samples/tsconfig.esm.json +10 -0
- package/packages/samples/tsconfig.json +23 -0
- package/pnpm-workspace.yaml +3 -0
- package/test-data/json-schema/address.json +35 -0
- package/test-data/json-schema/array-of-things.json +36 -0
- package/test-data/json-schema/basic.json +21 -0
- package/test-data/json-schema/blog-post.json +29 -0
- package/test-data/json-schema/calendar.json +48 -0
- package/test-data/json-schema/complex-object-with-nested-properties.json +41 -0
- package/test-data/json-schema/ecommerce-complex.json +344 -0
- package/test-data/json-schema/ecommerce-system.json +27 -0
- package/test-data/json-schema/enumerated-values.json +11 -0
- package/test-data/json-schema/fstab-entry.json +92 -0
- package/test-data/json-schema/geographical-location.json +20 -0
- package/test-data/json-schema/health-record.json +41 -0
- package/test-data/json-schema/job-posting.json +33 -0
- package/test-data/json-schema/movie.json +35 -0
- package/test-data/json-schema/regular-expression-pattern.json +12 -0
- package/test-data/json-schema/user-profile.json +33 -0
- package/test-data/sql/ecommerce.sql +641 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { describe, expect } from "vitest";
|
|
2
|
+
import { R } from "../../../common/result";
|
|
3
|
+
import { B } from "../../builder";
|
|
4
|
+
import { annotate, shapes } from "../../shape";
|
|
5
|
+
import { fromSQL } from "./from-sql";
|
|
6
|
+
import { toSQL } from "./transform";
|
|
7
|
+
|
|
8
|
+
const sqlite = { dialect: "sqlite" as const };
|
|
9
|
+
|
|
10
|
+
const ok = (res: R.Result<string, string[]>): string => {
|
|
11
|
+
if (!R.isOk(res))
|
|
12
|
+
throw new Error(`expected ok, got err: ${res.error.join("; ")}`);
|
|
13
|
+
return res.value;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const err = (res: R.Result<string, string[]>): string[] => {
|
|
17
|
+
if (R.isOk(res)) throw new Error(`expected err, got ok: ${res.value}`);
|
|
18
|
+
return res.error;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("toSQL sqlite — basics", (it) => {
|
|
22
|
+
it("trails every CREATE TABLE with STRICT", () => {
|
|
23
|
+
const m = annotate.titled(shapes.mapping({ id: shapes.int32() }), "t");
|
|
24
|
+
expect(ok(toSQL(m, sqlite))).toContain(") STRICT;");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("string max → TEXT + length CHECK (not varchar)", () => {
|
|
28
|
+
const m = annotate.titled(
|
|
29
|
+
shapes.mapping({ name: annotate.max(shapes.string(), 50) }),
|
|
30
|
+
"t",
|
|
31
|
+
);
|
|
32
|
+
expect(ok(toSQL(m, sqlite))).toBe(
|
|
33
|
+
'CREATE TABLE IF NOT EXISTS "t" (\n' +
|
|
34
|
+
' "name" TEXT NOT NULL CHECK (length("name") <= 50)\n' +
|
|
35
|
+
") STRICT;",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("string min → length() check (not char_length)", () => {
|
|
40
|
+
const m = annotate.titled(
|
|
41
|
+
shapes.mapping({ name: annotate.min(shapes.string(), 1) }),
|
|
42
|
+
"t",
|
|
43
|
+
);
|
|
44
|
+
expect(ok(toSQL(m, sqlite))).toContain('length("name") >= 1');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("string pattern → REGEXP operator", () => {
|
|
48
|
+
const m = annotate.titled(
|
|
49
|
+
shapes.mapping({
|
|
50
|
+
slug: annotate.pattern(shapes.string(), /^[a-z]+$/),
|
|
51
|
+
}),
|
|
52
|
+
"t",
|
|
53
|
+
);
|
|
54
|
+
expect(ok(toSQL(m, sqlite))).toContain("\"slug\" REGEXP '^[a-z]+$'");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("boolean → INTEGER + CHECK IN (0,1)", () => {
|
|
58
|
+
const m = annotate.titled(
|
|
59
|
+
shapes.mapping({ active: shapes.boolean() }),
|
|
60
|
+
"t",
|
|
61
|
+
);
|
|
62
|
+
expect(ok(toSQL(m, sqlite))).toContain(
|
|
63
|
+
'"active" INTEGER NOT NULL CHECK ("active" IN (0, 1))',
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("boolean default → 1/0 (no TRUE/FALSE)", () => {
|
|
68
|
+
const m = annotate.titled(
|
|
69
|
+
shapes.mapping({
|
|
70
|
+
a: annotate.defaulted(shapes.boolean(), true),
|
|
71
|
+
b: annotate.defaulted(shapes.boolean(), false),
|
|
72
|
+
}),
|
|
73
|
+
"t",
|
|
74
|
+
);
|
|
75
|
+
const sql = ok(toSQL(m, sqlite));
|
|
76
|
+
expect(sql).toContain('"a" INTEGER NOT NULL DEFAULT 1');
|
|
77
|
+
expect(sql).toContain('"b" INTEGER NOT NULL DEFAULT 0');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("int8 → INTEGER + range CHECKs", () => {
|
|
81
|
+
const m = annotate.titled(shapes.mapping({ x: shapes.int8() }), "t");
|
|
82
|
+
expect(ok(toSQL(m, sqlite))).toBe(
|
|
83
|
+
'CREATE TABLE IF NOT EXISTS "t" (\n' +
|
|
84
|
+
' "x" INTEGER NOT NULL CHECK ("x" >= -128) CHECK ("x" <= 127)\n' +
|
|
85
|
+
") STRICT;",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("uint64 → INTEGER + >= 0 (range-lossy, no error)", () => {
|
|
90
|
+
const m = annotate.titled(shapes.mapping({ x: shapes.uint64() }), "t");
|
|
91
|
+
expect(ok(toSQL(m, sqlite))).toContain(
|
|
92
|
+
'"x" INTEGER NOT NULL CHECK ("x" >= 0)',
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("int64 → INTEGER (no range CHECKs)", () => {
|
|
97
|
+
const m = annotate.titled(shapes.mapping({ x: shapes.int64() }), "t");
|
|
98
|
+
const sql = ok(toSQL(m, sqlite));
|
|
99
|
+
expect(sql).not.toContain('CHECK ("x"');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("float → REAL", () => {
|
|
103
|
+
const m = annotate.titled(shapes.mapping({ x: shapes.float() }), "t");
|
|
104
|
+
expect(ok(toSQL(m, sqlite))).toContain('"x" REAL NOT NULL');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("date → TEXT (ISO 8601)", () => {
|
|
108
|
+
const m = annotate.titled(shapes.mapping({ t: shapes.date() }), "t");
|
|
109
|
+
expect(ok(toSQL(m, sqlite))).toContain('"t" TEXT NOT NULL');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("binary → BLOB", () => {
|
|
113
|
+
const m = annotate.titled(shapes.mapping({ b: shapes.binary() }), "t");
|
|
114
|
+
expect(ok(toSQL(m, sqlite))).toContain('"b" BLOB NOT NULL');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("unknown / mapping / record → TEXT + json_valid", () => {
|
|
118
|
+
const m = annotate.titled(
|
|
119
|
+
shapes.mapping({
|
|
120
|
+
u: shapes.unknown(),
|
|
121
|
+
m: shapes.mapping({ k: shapes.string() }),
|
|
122
|
+
r: shapes.record(shapes.string(), shapes.int()),
|
|
123
|
+
}),
|
|
124
|
+
"t",
|
|
125
|
+
);
|
|
126
|
+
const sql = ok(toSQL(m, sqlite));
|
|
127
|
+
expect(sql).toContain('"u" TEXT NOT NULL CHECK (json_valid("u"))');
|
|
128
|
+
expect(sql).toContain('"m" TEXT NOT NULL CHECK (json_valid("m"))');
|
|
129
|
+
expect(sql).toContain('"r" TEXT NOT NULL CHECK (json_valid("r"))');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("tuple → TEXT + json_valid + array length check", () => {
|
|
133
|
+
const m = annotate.titled(
|
|
134
|
+
shapes.mapping({
|
|
135
|
+
p: shapes.tuple(shapes.float(), shapes.float()),
|
|
136
|
+
}),
|
|
137
|
+
"t",
|
|
138
|
+
);
|
|
139
|
+
expect(ok(toSQL(m, sqlite))).toContain(
|
|
140
|
+
'CHECK (json_valid("p") AND json_array_length("p") = 2)',
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("array → TEXT + json_valid + json_array_length min/max", () => {
|
|
145
|
+
const m = annotate.titled(
|
|
146
|
+
shapes.mapping({
|
|
147
|
+
xs: annotate.max(annotate.min(shapes.array(shapes.string()), 1), 20),
|
|
148
|
+
}),
|
|
149
|
+
"t",
|
|
150
|
+
);
|
|
151
|
+
const sql = ok(toSQL(m, sqlite));
|
|
152
|
+
expect(sql).toContain('"xs" TEXT NOT NULL CHECK (json_valid("xs"))');
|
|
153
|
+
expect(sql).toContain('CHECK (json_array_length("xs") <= 20)');
|
|
154
|
+
expect(sql).toContain('CHECK (json_array_length("xs") >= 1)');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("array uniqueItems → inline json_each CHECK (no prelude function)", () => {
|
|
158
|
+
const m = annotate.titled(
|
|
159
|
+
shapes.mapping({
|
|
160
|
+
xs: annotate.as(shapes.array(shapes.string()), { uniqueItems: true }),
|
|
161
|
+
}),
|
|
162
|
+
"t",
|
|
163
|
+
);
|
|
164
|
+
const sql = ok(toSQL(m, sqlite));
|
|
165
|
+
expect(sql).toContain(
|
|
166
|
+
'CHECK ((SELECT COUNT(*) FROM json_each("xs")) = (SELECT COUNT(DISTINCT value) FROM json_each("xs")))',
|
|
167
|
+
);
|
|
168
|
+
// No prelude function emitted
|
|
169
|
+
expect(sql).not.toContain("CREATE OR REPLACE FUNCTION");
|
|
170
|
+
expect(sql).not.toContain("shapecraft_array_is_unique");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("vector → BLOB + length check (sqlite-vec layout)", () => {
|
|
174
|
+
const m = annotate.titled(
|
|
175
|
+
shapes.mapping({ embedding: shapes.vector(384) }),
|
|
176
|
+
"t",
|
|
177
|
+
);
|
|
178
|
+
expect(ok(toSQL(m, sqlite))).toContain(
|
|
179
|
+
'"embedding" BLOB NOT NULL CHECK (length("embedding") = 1536)',
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("union of string literals → inline CHECK IN (no CREATE TYPE)", () => {
|
|
184
|
+
const m = annotate.titled(
|
|
185
|
+
shapes.mapping({
|
|
186
|
+
status: annotate.titled(
|
|
187
|
+
shapes.union(shapes.literal("draft"), shapes.literal("published")),
|
|
188
|
+
"Status",
|
|
189
|
+
),
|
|
190
|
+
}),
|
|
191
|
+
"t",
|
|
192
|
+
);
|
|
193
|
+
const sql = ok(toSQL(m, sqlite));
|
|
194
|
+
expect(sql).not.toContain("CREATE TYPE");
|
|
195
|
+
expect(sql).toContain(
|
|
196
|
+
`"status" TEXT NOT NULL CHECK ("status" IN ('draft', 'published'))`,
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("autoIncrement + integer → INTEGER PRIMARY KEY AUTOINCREMENT", () => {
|
|
201
|
+
const m = annotate.titled(
|
|
202
|
+
shapes.mapping({
|
|
203
|
+
id: annotate.autoIncremented(annotate.primary(shapes.uint32())),
|
|
204
|
+
}),
|
|
205
|
+
"t",
|
|
206
|
+
);
|
|
207
|
+
expect(ok(toSQL(m, sqlite))).toContain(
|
|
208
|
+
'"id" INTEGER PRIMARY KEY AUTOINCREMENT',
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("autoIncrement auto-promotes to primary when not yet primary", () => {
|
|
213
|
+
const m = annotate.titled(
|
|
214
|
+
shapes.mapping({
|
|
215
|
+
id: annotate.autoIncremented(shapes.uint32()),
|
|
216
|
+
}),
|
|
217
|
+
"t",
|
|
218
|
+
);
|
|
219
|
+
expect(ok(toSQL(m, sqlite))).toContain(
|
|
220
|
+
'"id" INTEGER PRIMARY KEY AUTOINCREMENT',
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("toSQL sqlite — errors", (it) => {
|
|
226
|
+
it("anno.schema → err", () => {
|
|
227
|
+
const m = annotate.as(
|
|
228
|
+
annotate.titled(shapes.mapping({ id: shapes.int32() }), "t"),
|
|
229
|
+
{ schema: "public" },
|
|
230
|
+
);
|
|
231
|
+
expect(err(toSQL(m, sqlite))).toEqual([
|
|
232
|
+
"table 't': sqlite has no schemas; remove anno.schema or use dialect='postgres'",
|
|
233
|
+
]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("autoIncrement on uint64 → err", () => {
|
|
237
|
+
const m = annotate.titled(
|
|
238
|
+
shapes.mapping({ x: annotate.autoIncremented(shapes.uint64()) }),
|
|
239
|
+
"t",
|
|
240
|
+
);
|
|
241
|
+
expect(err(toSQL(m, sqlite))).toEqual([
|
|
242
|
+
"field 'x': autoIncrement is not supported on uint64 in sqlite",
|
|
243
|
+
]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("autoIncrement on non-integer → err", () => {
|
|
247
|
+
const m = annotate.titled(
|
|
248
|
+
shapes.mapping({ s: annotate.autoIncremented(shapes.string()) }),
|
|
249
|
+
"t",
|
|
250
|
+
);
|
|
251
|
+
expect(err(toSQL(m, sqlite))).toEqual([
|
|
252
|
+
"field 's': autoIncrement requires an integer type",
|
|
253
|
+
]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("autoIncrement column conflicts with another primary col", () => {
|
|
257
|
+
const m = annotate.titled(
|
|
258
|
+
shapes.mapping({
|
|
259
|
+
id: annotate.autoIncremented(annotate.primary(shapes.int32())),
|
|
260
|
+
other: annotate.primary(shapes.string()),
|
|
261
|
+
}),
|
|
262
|
+
"t",
|
|
263
|
+
);
|
|
264
|
+
expect(err(toSQL(m, sqlite))[0]).toMatch(
|
|
265
|
+
/autoIncrement column 'id' must be the sole primary key/,
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("toSQL sqlite — cross-dialect divergence", (it) => {
|
|
271
|
+
it("PG emits ENUM, SQLite emits inline CHECK", () => {
|
|
272
|
+
const m = annotate.titled(
|
|
273
|
+
shapes.mapping({
|
|
274
|
+
status: annotate.titled(
|
|
275
|
+
shapes.union(shapes.literal("a"), shapes.literal("b")),
|
|
276
|
+
"Status",
|
|
277
|
+
),
|
|
278
|
+
}),
|
|
279
|
+
"t",
|
|
280
|
+
);
|
|
281
|
+
const pg = ok(toSQL(m));
|
|
282
|
+
const sl = ok(toSQL(m, sqlite));
|
|
283
|
+
expect(pg).toContain("CREATE TYPE");
|
|
284
|
+
expect(sl).not.toContain("CREATE TYPE");
|
|
285
|
+
expect(pg).not.toEqual(sl);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("PG emits varchar(N), SQLite emits TEXT + length CHECK", () => {
|
|
289
|
+
const m = annotate.titled(
|
|
290
|
+
shapes.mapping({ name: annotate.max(shapes.string(), 50) }),
|
|
291
|
+
"t",
|
|
292
|
+
);
|
|
293
|
+
expect(ok(toSQL(m))).toContain("varchar(50)");
|
|
294
|
+
expect(ok(toSQL(m, sqlite))).toContain(
|
|
295
|
+
'TEXT NOT NULL CHECK (length("name") <= 50)',
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("toSQL sqlite — round-trip", (it) => {
|
|
301
|
+
it("comprehensive table round-trips byte-equal", () => {
|
|
302
|
+
const Tag = B.mapping({
|
|
303
|
+
id: B.uint32().primary().autoIncrement(),
|
|
304
|
+
slug: B.string()
|
|
305
|
+
.regex(/^[a-z0-9-]+$/)
|
|
306
|
+
.unique(),
|
|
307
|
+
label: B.string().min(1).max(40),
|
|
308
|
+
tags: B.array(B.string()).max(20).uniqueItems(),
|
|
309
|
+
embedding: B.vector(384),
|
|
310
|
+
active: B.boolean().default(true),
|
|
311
|
+
metadata: B.mapping({ key: B.string() }).optional(),
|
|
312
|
+
}).title("Tag");
|
|
313
|
+
|
|
314
|
+
const emitted = ok(toSQL(Tag.shape, sqlite));
|
|
315
|
+
const parsed = fromSQL(emitted, sqlite);
|
|
316
|
+
if (!parsed.ok) throw new Error("parse failed: " + parsed.error.join("; "));
|
|
317
|
+
const reEmitted = ok(toSQL(parsed.value, sqlite));
|
|
318
|
+
expect(reEmitted).toBe(emitted);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("module with FK + multi-table round-trips", () => {
|
|
322
|
+
const User = B.mapping({
|
|
323
|
+
id: B.uint32().primary().autoIncrement(),
|
|
324
|
+
email: B.string().unique(),
|
|
325
|
+
}).title("User");
|
|
326
|
+
const Post = B.mapping({
|
|
327
|
+
id: B.uint32().primary().autoIncrement(),
|
|
328
|
+
author: B.uint32().references(User, "id"),
|
|
329
|
+
title: B.string().min(1).max(200),
|
|
330
|
+
publishedAt: B.date().optional(),
|
|
331
|
+
}).title("Post");
|
|
332
|
+
const mod = B.module({ User, Post });
|
|
333
|
+
|
|
334
|
+
const emitted = ok(toSQL(mod.shape, sqlite));
|
|
335
|
+
const parsed = fromSQL(emitted, sqlite);
|
|
336
|
+
if (!parsed.ok) throw new Error("parse failed: " + parsed.error.join("; "));
|
|
337
|
+
const reEmitted = ok(toSQL(parsed.value, sqlite));
|
|
338
|
+
expect(reEmitted).toBe(emitted);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe("fromSQL sqlite — parsing", (it) => {
|
|
343
|
+
it("INTEGER PRIMARY KEY AUTOINCREMENT → primary + autoIncrement on int64", () => {
|
|
344
|
+
const r = fromSQL(
|
|
345
|
+
`CREATE TABLE "t" ("id" INTEGER PRIMARY KEY AUTOINCREMENT) STRICT;`,
|
|
346
|
+
sqlite,
|
|
347
|
+
);
|
|
348
|
+
if (!r.ok) throw new Error(r.error.join("; "));
|
|
349
|
+
const m = r.value as shapes.ShapeMapping;
|
|
350
|
+
const id = m.input.id!;
|
|
351
|
+
expect(id.type).toBe("number");
|
|
352
|
+
expect((id as shapes.ShapeNumber).tag).toBe("int64");
|
|
353
|
+
expect(id.anno.primary).toBe(true);
|
|
354
|
+
expect(id.anno.autoIncrement).toBe(true);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("INTEGER + CHECK IN (0,1) + DEFAULT 1 → boolean with default true", () => {
|
|
358
|
+
const r = fromSQL(
|
|
359
|
+
`CREATE TABLE "t" ("a" INTEGER NOT NULL DEFAULT 1 CHECK ("a" IN (0,1))) STRICT;`,
|
|
360
|
+
sqlite,
|
|
361
|
+
);
|
|
362
|
+
if (!r.ok) throw new Error(r.error.join("; "));
|
|
363
|
+
const m = r.value as shapes.ShapeMapping;
|
|
364
|
+
const a = m.input.a!;
|
|
365
|
+
expect(a.type).toBe("boolean");
|
|
366
|
+
expect(a.anno.default).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("TEXT + json_valid + json_array_length(col) = N → tuple", () => {
|
|
370
|
+
const r = fromSQL(
|
|
371
|
+
`CREATE TABLE "t" ("p" TEXT NOT NULL CHECK (json_valid("p") AND json_array_length("p") = 2)) STRICT;`,
|
|
372
|
+
sqlite,
|
|
373
|
+
);
|
|
374
|
+
if (!r.ok) throw new Error(r.error.join("; "));
|
|
375
|
+
const m = r.value as shapes.ShapeMapping;
|
|
376
|
+
expect(m.input.p!.type).toBe("tuple");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("TEXT + json_valid only → unknown", () => {
|
|
380
|
+
const r = fromSQL(
|
|
381
|
+
`CREATE TABLE "t" ("u" TEXT NOT NULL CHECK (json_valid("u"))) STRICT;`,
|
|
382
|
+
sqlite,
|
|
383
|
+
);
|
|
384
|
+
if (!r.ok) throw new Error(r.error.join("; "));
|
|
385
|
+
const m = r.value as shapes.ShapeMapping;
|
|
386
|
+
expect(m.input.u!.type).toBe("unknown");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("TEXT + json_valid + array length CHECKs → array(unknown)", () => {
|
|
390
|
+
const r = fromSQL(
|
|
391
|
+
`CREATE TABLE "t" ("xs" TEXT NOT NULL CHECK (json_valid("xs")) CHECK (json_array_length("xs") <= 5)) STRICT;`,
|
|
392
|
+
sqlite,
|
|
393
|
+
);
|
|
394
|
+
if (!r.ok) throw new Error(r.error.join("; "));
|
|
395
|
+
const m = r.value as shapes.ShapeMapping;
|
|
396
|
+
expect(m.input.xs!.type).toBe("array");
|
|
397
|
+
expect(m.input.xs!.anno.max).toBe(5);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("backtick-quoted identifiers parse", () => {
|
|
401
|
+
const r = fromSQL(
|
|
402
|
+
"CREATE TABLE `t` (`id` INTEGER PRIMARY KEY, `name` TEXT) STRICT;",
|
|
403
|
+
sqlite,
|
|
404
|
+
);
|
|
405
|
+
if (!r.ok) throw new Error(r.error.join("; "));
|
|
406
|
+
const m = r.value as shapes.ShapeMapping;
|
|
407
|
+
expect(m.anno.title).toBe("t");
|
|
408
|
+
expect(Object.keys(m.input)).toEqual(["id", "name"]);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("WITHOUT ROWID is tolerated", () => {
|
|
412
|
+
const r = fromSQL(
|
|
413
|
+
`CREATE TABLE "t" ("id" INTEGER PRIMARY KEY) WITHOUT ROWID;`,
|
|
414
|
+
sqlite,
|
|
415
|
+
);
|
|
416
|
+
expect(r.ok).toBe(true);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("main.<table> qualified name drops the database prefix", () => {
|
|
420
|
+
const r = fromSQL(
|
|
421
|
+
`CREATE TABLE main."t" ("id" INTEGER PRIMARY KEY) STRICT;`,
|
|
422
|
+
sqlite,
|
|
423
|
+
);
|
|
424
|
+
if (!r.ok) throw new Error(r.error.join("; "));
|
|
425
|
+
const m = r.value as shapes.ShapeMapping;
|
|
426
|
+
expect(m.anno.title).toBe("t");
|
|
427
|
+
expect(m.anno.schema).toBeUndefined();
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("CREATE VIRTUAL TABLE is skipped without error", () => {
|
|
431
|
+
const r = fromSQL(
|
|
432
|
+
`CREATE VIRTUAL TABLE "vec_idx" USING vec0(embedding float[3]);` +
|
|
433
|
+
`\nCREATE TABLE "t" ("id" INTEGER PRIMARY KEY) STRICT;`,
|
|
434
|
+
sqlite,
|
|
435
|
+
);
|
|
436
|
+
if (!r.ok) throw new Error(r.error.join("; "));
|
|
437
|
+
const m = r.value as shapes.ShapeMapping;
|
|
438
|
+
expect(m.anno.title).toBe("t");
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("varchar(N) → string with max", () => {
|
|
442
|
+
const r = fromSQL(`CREATE TABLE "t" ("s" VARCHAR(50)) STRICT;`, sqlite);
|
|
443
|
+
if (!r.ok) throw new Error(r.error.join("; "));
|
|
444
|
+
const m = r.value as shapes.ShapeMapping;
|
|
445
|
+
expect(m.input.s!.type).toBe("string");
|
|
446
|
+
expect(m.input.s!.anno.max).toBe(50);
|
|
447
|
+
});
|
|
448
|
+
});
|