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,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
|
+
};
|