@syncular/typegen 0.1.2 → 0.2.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/README.md +436 -94
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +159 -73
- package/dist/emit-dart.d.ts +17 -0
- package/dist/emit-dart.js +201 -0
- package/dist/emit-kotlin.d.ts +16 -0
- package/dist/emit-kotlin.js +201 -0
- package/dist/emit-queries-dart.d.ts +2 -0
- package/dist/emit-queries-dart.js +160 -0
- package/dist/emit-queries-kotlin.d.ts +2 -0
- package/dist/emit-queries-kotlin.js +162 -0
- package/dist/emit-queries-swift.d.ts +2 -0
- package/dist/emit-queries-swift.js +146 -0
- package/dist/emit-queries.d.ts +2 -0
- package/dist/emit-queries.js +129 -0
- package/dist/emit-swift.d.ts +18 -0
- package/dist/emit-swift.js +235 -0
- package/dist/emit.d.ts +16 -0
- package/dist/emit.js +191 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +10 -0
- package/dist/generate.d.ts +53 -18
- package/dist/generate.js +391 -77
- package/dist/index.d.ts +17 -14
- package/dist/index.js +16 -13
- package/dist/init.d.ts +12 -0
- package/dist/init.js +85 -0
- package/dist/ir.d.ts +90 -0
- package/dist/ir.js +94 -0
- package/dist/manifest.d.ts +76 -0
- package/dist/manifest.js +303 -0
- package/dist/query.d.ts +96 -0
- package/dist/query.js +719 -0
- package/dist/sql.d.ts +13 -0
- package/dist/sql.js +440 -0
- package/package.json +14 -34
- package/src/cli.ts +161 -82
- package/src/emit-dart.ts +245 -0
- package/src/emit-kotlin.ts +257 -0
- package/src/emit-queries-dart.ts +203 -0
- package/src/emit-queries-kotlin.ts +209 -0
- package/src/emit-queries-swift.ts +198 -0
- package/src/emit-queries.ts +183 -0
- package/src/emit-swift.ts +295 -0
- package/src/emit.ts +239 -0
- package/src/errors.ts +11 -0
- package/src/generate.ts +574 -105
- package/src/index.ts +16 -13
- package/src/init.ts +100 -0
- package/src/ir.ts +197 -0
- package/src/manifest.ts +445 -0
- package/src/query.ts +918 -0
- package/src/sql.ts +533 -0
- package/dist/app-contract.d.ts +0 -154
- package/dist/app-contract.d.ts.map +0 -1
- package/dist/app-contract.js +0 -250
- package/dist/app-contract.js.map +0 -1
- package/dist/checksums.d.ts +0 -6
- package/dist/checksums.d.ts.map +0 -1
- package/dist/checksums.js +0 -173
- package/dist/checksums.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/generate.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/introspect-postgres.d.ts +0 -8
- package/dist/introspect-postgres.d.ts.map +0 -1
- package/dist/introspect-postgres.js +0 -94
- package/dist/introspect-postgres.js.map +0 -1
- package/dist/introspect-sqlite.d.ts +0 -10
- package/dist/introspect-sqlite.d.ts.map +0 -1
- package/dist/introspect-sqlite.js +0 -97
- package/dist/introspect-sqlite.js.map +0 -1
- package/dist/introspect.d.ts +0 -8
- package/dist/introspect.d.ts.map +0 -1
- package/dist/introspect.js +0 -18
- package/dist/introspect.js.map +0 -1
- package/dist/map-types.d.ts +0 -20
- package/dist/map-types.d.ts.map +0 -1
- package/dist/map-types.js +0 -151
- package/dist/map-types.js.map +0 -1
- package/dist/render.d.ts +0 -23
- package/dist/render.d.ts.map +0 -1
- package/dist/render.js +0 -140
- package/dist/render.js.map +0 -1
- package/dist/types.d.ts +0 -122
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +0 -1
- package/src/app-contract.ts +0 -531
- package/src/checksums.ts +0 -257
- package/src/introspect-postgres.ts +0 -149
- package/src/introspect-sqlite.ts +0 -156
- package/src/introspect.ts +0 -36
- package/src/map-types.ts +0 -189
- package/src/render.ts +0 -196
- package/src/types.ts +0 -137
package/dist/sql.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IrColumn, IrIndex } from './ir.js';
|
|
2
|
+
export interface ParsedTable {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
primaryKey: string;
|
|
5
|
+
readonly columns: IrColumn[];
|
|
6
|
+
/** Local secondary indexes, in declaration order (CREATE INDEX subset). */
|
|
7
|
+
readonly indexes: IrIndex[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Apply one migration file's SQL to the accumulated table map. Columns
|
|
11
|
+
* accumulate in declaration order — the §2.4 row-codec positional order.
|
|
12
|
+
*/
|
|
13
|
+
export declare function applyMigrationSql(tables: Map<string, ParsedTable>, sql: string, source: string): void;
|
package/dist/sql.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL migration subset parser (REVISE B5).
|
|
3
|
+
*
|
|
4
|
+
* Parses exactly the subset v1-style migrations need:
|
|
5
|
+
*
|
|
6
|
+
* - `CREATE TABLE [IF NOT EXISTS] name (columns…, [PRIMARY KEY (col)])
|
|
7
|
+
* [WITHOUT ROWID]`
|
|
8
|
+
* - `ALTER TABLE name ADD [COLUMN] coldef`
|
|
9
|
+
* - `CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (col [, col…])`
|
|
10
|
+
* - column defs: `name TYPE [PRIMARY KEY] [NOT NULL] [NULL]
|
|
11
|
+
* [DEFAULT literal]`
|
|
12
|
+
* - `--` and C-style comments
|
|
13
|
+
*
|
|
14
|
+
* Anything else — other statements, table constraints, parameterized or
|
|
15
|
+
* unknown types, DEFAULT expressions, quoted identifiers, composite
|
|
16
|
+
* primary keys, ASC/DESC or expression index columns, partial (`WHERE`)
|
|
17
|
+
* indexes — is a hard error naming the unsupported construct.
|
|
18
|
+
*/
|
|
19
|
+
import { TypegenError } from './errors.js';
|
|
20
|
+
/** SQL type keyword → the §2.4 column types. Case-insensitive. */
|
|
21
|
+
const TYPE_MAP = {
|
|
22
|
+
TEXT: 'string',
|
|
23
|
+
INTEGER: 'integer',
|
|
24
|
+
INT: 'integer',
|
|
25
|
+
BIGINT: 'integer',
|
|
26
|
+
SMALLINT: 'integer',
|
|
27
|
+
REAL: 'float',
|
|
28
|
+
FLOAT: 'float',
|
|
29
|
+
DOUBLE: 'float',
|
|
30
|
+
BOOLEAN: 'boolean',
|
|
31
|
+
BOOL: 'boolean',
|
|
32
|
+
JSON: 'json',
|
|
33
|
+
JSONB: 'json',
|
|
34
|
+
BLOB: 'bytes',
|
|
35
|
+
BYTEA: 'bytes',
|
|
36
|
+
// §5.9 tag 7: a stored-as-TEXT column carrying a canonical BlobRef
|
|
37
|
+
// document. The synthetic keyword designates the semantic type; the local
|
|
38
|
+
// column is TEXT-shaped like `json`.
|
|
39
|
+
BLOB_REF: 'blob_ref',
|
|
40
|
+
BLOBREF: 'blob_ref',
|
|
41
|
+
// §5.10 tag 8: a stored-as-BLOB column carrying opaque server-merged CRDT
|
|
42
|
+
// bytes. The synthetic keyword designates the semantic type; crdtType
|
|
43
|
+
// defaults to the one built-in merger (`yjs-doc`, §5.10.1).
|
|
44
|
+
CRDT: 'crdt',
|
|
45
|
+
};
|
|
46
|
+
/** Default `crdtType` for a bare `CRDT` keyword (§5.10.1). */
|
|
47
|
+
const DEFAULT_CRDT_TYPE = 'yjs-doc';
|
|
48
|
+
const WORD_START = /[A-Za-z_]/;
|
|
49
|
+
const WORD_PART = /[A-Za-z0-9_]/;
|
|
50
|
+
const DIGIT = /[0-9]/;
|
|
51
|
+
function tokenizeStatements(sql, source) {
|
|
52
|
+
const statements = [];
|
|
53
|
+
let current = [];
|
|
54
|
+
let i = 0;
|
|
55
|
+
while (i < sql.length) {
|
|
56
|
+
const ch = sql[i];
|
|
57
|
+
if (/\s/.test(ch)) {
|
|
58
|
+
i += 1;
|
|
59
|
+
}
|
|
60
|
+
else if (ch === '-' && sql[i + 1] === '-') {
|
|
61
|
+
const end = sql.indexOf('\n', i);
|
|
62
|
+
i = end === -1 ? sql.length : end + 1;
|
|
63
|
+
}
|
|
64
|
+
else if (ch === '/' && sql[i + 1] === '*') {
|
|
65
|
+
const end = sql.indexOf('*/', i + 2);
|
|
66
|
+
if (end === -1)
|
|
67
|
+
throw new TypegenError(source, 'unterminated comment');
|
|
68
|
+
i = end + 2;
|
|
69
|
+
}
|
|
70
|
+
else if (ch === ';') {
|
|
71
|
+
if (current.length > 0)
|
|
72
|
+
statements.push(current);
|
|
73
|
+
current = [];
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
else if (ch === "'") {
|
|
77
|
+
let text = '';
|
|
78
|
+
i += 1;
|
|
79
|
+
for (;;) {
|
|
80
|
+
if (i >= sql.length) {
|
|
81
|
+
throw new TypegenError(source, 'unterminated string literal');
|
|
82
|
+
}
|
|
83
|
+
if (sql[i] === "'") {
|
|
84
|
+
if (sql[i + 1] === "'") {
|
|
85
|
+
text += "'";
|
|
86
|
+
i += 2;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
i += 1;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
text += sql[i];
|
|
95
|
+
i += 1;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
current.push({ kind: 'string', text });
|
|
99
|
+
}
|
|
100
|
+
else if (ch === '"' || ch === '`' || ch === '[') {
|
|
101
|
+
throw new TypegenError(source, `quoted identifiers are unsupported (found ${JSON.stringify(ch)})`);
|
|
102
|
+
}
|
|
103
|
+
else if (WORD_START.test(ch)) {
|
|
104
|
+
let end = i + 1;
|
|
105
|
+
while (end < sql.length && WORD_PART.test(sql[end]))
|
|
106
|
+
end += 1;
|
|
107
|
+
current.push({ kind: 'word', text: sql.slice(i, end) });
|
|
108
|
+
i = end;
|
|
109
|
+
}
|
|
110
|
+
else if (DIGIT.test(ch)) {
|
|
111
|
+
let end = i + 1;
|
|
112
|
+
while (end < sql.length && /[0-9.]/.test(sql[end]))
|
|
113
|
+
end += 1;
|
|
114
|
+
current.push({ kind: 'number', text: sql.slice(i, end) });
|
|
115
|
+
i = end;
|
|
116
|
+
}
|
|
117
|
+
else if (ch === '(' || ch === ')' || ch === ',') {
|
|
118
|
+
current.push({ kind: 'punct', text: ch });
|
|
119
|
+
i += 1;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
throw new TypegenError(source, `unsupported SQL syntax: unexpected character ${JSON.stringify(ch)}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (current.length > 0)
|
|
126
|
+
statements.push(current);
|
|
127
|
+
return statements;
|
|
128
|
+
}
|
|
129
|
+
class Cursor {
|
|
130
|
+
tokens;
|
|
131
|
+
source;
|
|
132
|
+
index = 0;
|
|
133
|
+
constructor(tokens, source) {
|
|
134
|
+
this.tokens = tokens;
|
|
135
|
+
this.source = source;
|
|
136
|
+
}
|
|
137
|
+
peek() {
|
|
138
|
+
return this.tokens[this.index];
|
|
139
|
+
}
|
|
140
|
+
next() {
|
|
141
|
+
const token = this.tokens[this.index];
|
|
142
|
+
if (token === undefined) {
|
|
143
|
+
throw new TypegenError(this.source, 'unexpected end of SQL statement');
|
|
144
|
+
}
|
|
145
|
+
this.index += 1;
|
|
146
|
+
return token;
|
|
147
|
+
}
|
|
148
|
+
/** Consume a word token if it matches (case-insensitive). */
|
|
149
|
+
eatWord(word) {
|
|
150
|
+
const token = this.peek();
|
|
151
|
+
if (token?.kind === 'word' && token.text.toUpperCase() === word) {
|
|
152
|
+
this.index += 1;
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
expectWord(word, context) {
|
|
158
|
+
const token = this.next();
|
|
159
|
+
if (token.kind !== 'word' || token.text.toUpperCase() !== word) {
|
|
160
|
+
throw new TypegenError(this.source, `expected ${word} ${context}, found ${JSON.stringify(token.text)}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
expectPunct(punct, context) {
|
|
164
|
+
const token = this.next();
|
|
165
|
+
if (token.kind !== 'punct' || token.text !== punct) {
|
|
166
|
+
throw new TypegenError(this.source, `expected ${JSON.stringify(punct)} ${context}, found ${JSON.stringify(token.text)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
identifier(context) {
|
|
170
|
+
const token = this.next();
|
|
171
|
+
if (token.kind !== 'word') {
|
|
172
|
+
throw new TypegenError(this.source, `expected ${context}, found ${JSON.stringify(token.text)}`);
|
|
173
|
+
}
|
|
174
|
+
return token.text;
|
|
175
|
+
}
|
|
176
|
+
expectEnd() {
|
|
177
|
+
const token = this.peek();
|
|
178
|
+
if (token !== undefined) {
|
|
179
|
+
throw new TypegenError(this.source, `unsupported trailing SQL ${JSON.stringify(token.text)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
fail(message) {
|
|
183
|
+
throw new TypegenError(this.source, message);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function parseColumnType(cursor, columnName) {
|
|
187
|
+
const typeWord = cursor.identifier(`a column type for ${columnName}`);
|
|
188
|
+
if (cursor.peek()?.text === '(') {
|
|
189
|
+
cursor.fail(`column ${columnName}: parameterized column types are unsupported (${typeWord}(…))`);
|
|
190
|
+
}
|
|
191
|
+
const mapped = TYPE_MAP[typeWord.toUpperCase()];
|
|
192
|
+
if (mapped === undefined) {
|
|
193
|
+
cursor.fail(`column ${columnName}: unsupported column type ${JSON.stringify(typeWord)}`);
|
|
194
|
+
}
|
|
195
|
+
return mapped;
|
|
196
|
+
}
|
|
197
|
+
function parseColumnDef(cursor, allowPrimaryKey) {
|
|
198
|
+
const name = cursor.identifier('a column name');
|
|
199
|
+
const type = parseColumnType(cursor, name);
|
|
200
|
+
let nullable = true;
|
|
201
|
+
let primaryKey = false;
|
|
202
|
+
for (;;) {
|
|
203
|
+
const token = cursor.peek();
|
|
204
|
+
if (token === undefined || token.kind === 'punct')
|
|
205
|
+
break;
|
|
206
|
+
const word = token.text.toUpperCase();
|
|
207
|
+
if (word === 'PRIMARY') {
|
|
208
|
+
cursor.next();
|
|
209
|
+
cursor.expectWord('KEY', 'after PRIMARY');
|
|
210
|
+
if (!allowPrimaryKey) {
|
|
211
|
+
cursor.fail(`column ${name}: PRIMARY KEY is not supported on ADD COLUMN`);
|
|
212
|
+
}
|
|
213
|
+
primaryKey = true;
|
|
214
|
+
nullable = false;
|
|
215
|
+
}
|
|
216
|
+
else if (word === 'NOT') {
|
|
217
|
+
cursor.next();
|
|
218
|
+
cursor.expectWord('NULL', 'after NOT');
|
|
219
|
+
nullable = false;
|
|
220
|
+
}
|
|
221
|
+
else if (word === 'NULL') {
|
|
222
|
+
cursor.next();
|
|
223
|
+
}
|
|
224
|
+
else if (word === 'DEFAULT') {
|
|
225
|
+
cursor.next();
|
|
226
|
+
const value = cursor.next();
|
|
227
|
+
if (value.kind === 'punct') {
|
|
228
|
+
cursor.fail(`column ${name}: DEFAULT expressions are unsupported (only literals)`);
|
|
229
|
+
}
|
|
230
|
+
// Literal defaults are accepted and ignored: typegen extracts the
|
|
231
|
+
// schema shape; running migrations is the host's job.
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
cursor.fail(`column ${name}: unsupported column constraint ${JSON.stringify(token.text)}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (primaryKey)
|
|
238
|
+
nullable = false;
|
|
239
|
+
// §5.10.1: a crdt column carries a crdtType (default `yjs-doc`).
|
|
240
|
+
const column = type === 'crdt'
|
|
241
|
+
? { name, type, nullable, crdtType: DEFAULT_CRDT_TYPE }
|
|
242
|
+
: { name, type, nullable };
|
|
243
|
+
return { column, primaryKey };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Dispatch a `CREATE …` statement: `CREATE TABLE`, `CREATE INDEX`, or
|
|
247
|
+
* `CREATE UNIQUE INDEX`. Anything else is a hard error naming the construct.
|
|
248
|
+
*/
|
|
249
|
+
function parseCreate(cursor, tables, source) {
|
|
250
|
+
if (cursor.eatWord('TABLE')) {
|
|
251
|
+
parseCreateTable(cursor, tables, source);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (cursor.eatWord('UNIQUE')) {
|
|
255
|
+
cursor.expectWord('INDEX', 'after CREATE UNIQUE');
|
|
256
|
+
parseCreateIndex(cursor, tables, true);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (cursor.eatWord('INDEX')) {
|
|
260
|
+
parseCreateIndex(cursor, tables, false);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const token = cursor.peek();
|
|
264
|
+
cursor.fail(`unsupported CREATE statement (only CREATE TABLE and CREATE [UNIQUE] INDEX), found ${JSON.stringify(token?.text ?? '')}`);
|
|
265
|
+
}
|
|
266
|
+
function parseCreateTable(cursor, tables, source) {
|
|
267
|
+
if (cursor.eatWord('IF')) {
|
|
268
|
+
cursor.expectWord('NOT', 'after IF');
|
|
269
|
+
cursor.expectWord('EXISTS', 'after IF NOT');
|
|
270
|
+
}
|
|
271
|
+
const name = cursor.identifier('a table name');
|
|
272
|
+
if (tables.has(name)) {
|
|
273
|
+
cursor.fail(`table ${name} is created twice`);
|
|
274
|
+
}
|
|
275
|
+
cursor.expectPunct('(', `after CREATE TABLE ${name}`);
|
|
276
|
+
const columns = [];
|
|
277
|
+
const primaryKeys = [];
|
|
278
|
+
for (;;) {
|
|
279
|
+
const head = cursor.peek();
|
|
280
|
+
if (head?.kind === 'word') {
|
|
281
|
+
const word = head.text.toUpperCase();
|
|
282
|
+
if (word === 'PRIMARY') {
|
|
283
|
+
cursor.next();
|
|
284
|
+
cursor.expectWord('KEY', 'after PRIMARY');
|
|
285
|
+
cursor.expectPunct('(', 'after PRIMARY KEY');
|
|
286
|
+
primaryKeys.push(cursor.identifier('a primary-key column name'));
|
|
287
|
+
if (cursor.peek()?.text === ',') {
|
|
288
|
+
cursor.fail(`table ${name}: composite primary keys are unsupported`);
|
|
289
|
+
}
|
|
290
|
+
cursor.expectPunct(')', 'after the PRIMARY KEY column');
|
|
291
|
+
}
|
|
292
|
+
else if (word === 'FOREIGN' ||
|
|
293
|
+
word === 'UNIQUE' ||
|
|
294
|
+
word === 'CHECK' ||
|
|
295
|
+
word === 'CONSTRAINT') {
|
|
296
|
+
cursor.fail(`table ${name}: unsupported table constraint ${JSON.stringify(head.text)}`);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const def = parseColumnDef(cursor, true);
|
|
300
|
+
if (columns.some((c) => c.name === def.column.name)) {
|
|
301
|
+
cursor.fail(`table ${name}: duplicate column ${JSON.stringify(def.column.name)}`);
|
|
302
|
+
}
|
|
303
|
+
columns.push(def.column);
|
|
304
|
+
if (def.primaryKey)
|
|
305
|
+
primaryKeys.push(def.column.name);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
cursor.fail(`table ${name}: expected a column definition`);
|
|
310
|
+
}
|
|
311
|
+
const sep = cursor.next();
|
|
312
|
+
if (sep.kind === 'punct' && sep.text === ',')
|
|
313
|
+
continue;
|
|
314
|
+
if (sep.kind === 'punct' && sep.text === ')')
|
|
315
|
+
break;
|
|
316
|
+
cursor.fail(`table ${name}: expected "," or ")", found ${JSON.stringify(sep.text)}`);
|
|
317
|
+
}
|
|
318
|
+
if (cursor.eatWord('WITHOUT')) {
|
|
319
|
+
cursor.expectWord('ROWID', 'after WITHOUT');
|
|
320
|
+
}
|
|
321
|
+
cursor.expectEnd();
|
|
322
|
+
if (primaryKeys.length === 0) {
|
|
323
|
+
throw new TypegenError(source, `table ${name} declares no primary key`);
|
|
324
|
+
}
|
|
325
|
+
if (primaryKeys.length > 1) {
|
|
326
|
+
throw new TypegenError(source, `table ${name}: composite primary keys are unsupported (${primaryKeys.join(', ')})`);
|
|
327
|
+
}
|
|
328
|
+
const primaryKey = primaryKeys[0];
|
|
329
|
+
const pkColumn = columns.find((c) => c.name === primaryKey);
|
|
330
|
+
if (pkColumn === undefined) {
|
|
331
|
+
throw new TypegenError(source, `table ${name}: primary key ${JSON.stringify(primaryKey)} is not a column`);
|
|
332
|
+
}
|
|
333
|
+
const finalColumns = columns.map((c) => c.name === primaryKey ? { ...c, nullable: false } : c);
|
|
334
|
+
tables.set(name, { name, primaryKey, columns: finalColumns, indexes: [] });
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* `CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (col [, col…])`.
|
|
338
|
+
* The `UNIQUE` keyword is already consumed by the dispatcher (it precedes
|
|
339
|
+
* `INDEX`), so `unique` is passed in. The subset keeps the column list to
|
|
340
|
+
* bare column names of the target table: ASC/DESC, expressions, and a `WHERE`
|
|
341
|
+
* (partial index) clause are hard errors naming the construct. Index names
|
|
342
|
+
* are unique per accumulated schema.
|
|
343
|
+
*/
|
|
344
|
+
function parseCreateIndex(cursor, tables, unique) {
|
|
345
|
+
// `INDEX` already matched by the dispatcher.
|
|
346
|
+
if (cursor.eatWord('IF')) {
|
|
347
|
+
cursor.expectWord('NOT', 'after IF');
|
|
348
|
+
cursor.expectWord('EXISTS', 'after IF NOT');
|
|
349
|
+
}
|
|
350
|
+
const name = cursor.identifier('an index name');
|
|
351
|
+
for (const table of tables.values()) {
|
|
352
|
+
if (table.indexes.some((i) => i.name === name)) {
|
|
353
|
+
cursor.fail(`index ${name} is created twice`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
cursor.expectWord('ON', `after CREATE INDEX ${name}`);
|
|
357
|
+
const tableName = cursor.identifier('a table name');
|
|
358
|
+
const table = tables.get(tableName);
|
|
359
|
+
if (table === undefined) {
|
|
360
|
+
cursor.fail(`CREATE INDEX ${name}: table ${tableName} does not exist at this point`);
|
|
361
|
+
}
|
|
362
|
+
cursor.expectPunct('(', `after CREATE INDEX ${name} ON ${tableName}`);
|
|
363
|
+
const columns = [];
|
|
364
|
+
for (;;) {
|
|
365
|
+
const col = cursor.identifier(`an index column for ${name}`);
|
|
366
|
+
// Reject ASC/DESC and any expression tail: the IR stores bare column names
|
|
367
|
+
// only, so accepting a direction would silently drop it. A following word
|
|
368
|
+
// (not "," or ")") is such an unsupported construct.
|
|
369
|
+
const after = cursor.peek();
|
|
370
|
+
if (after !== undefined &&
|
|
371
|
+
!(after.kind === 'punct' && (after.text === ',' || after.text === ')'))) {
|
|
372
|
+
const dir = after.text.toUpperCase();
|
|
373
|
+
if (dir === 'ASC' || dir === 'DESC') {
|
|
374
|
+
cursor.fail(`index ${name}: ASC/DESC index columns are unsupported (found ${JSON.stringify(after.text)} after ${JSON.stringify(col)})`);
|
|
375
|
+
}
|
|
376
|
+
cursor.fail(`index ${name}: expression index columns are unsupported (found ${JSON.stringify(after.text)} after ${JSON.stringify(col)})`);
|
|
377
|
+
}
|
|
378
|
+
if (table.columns.every((c) => c.name !== col)) {
|
|
379
|
+
cursor.fail(`index ${name}: column ${JSON.stringify(col)} does not exist on table ${tableName}`);
|
|
380
|
+
}
|
|
381
|
+
if (columns.includes(col)) {
|
|
382
|
+
cursor.fail(`index ${name}: column ${JSON.stringify(col)} appears twice`);
|
|
383
|
+
}
|
|
384
|
+
columns.push(col);
|
|
385
|
+
const sep = cursor.next();
|
|
386
|
+
if (sep.kind === 'punct' && sep.text === ',')
|
|
387
|
+
continue;
|
|
388
|
+
if (sep.kind === 'punct' && sep.text === ')')
|
|
389
|
+
break;
|
|
390
|
+
cursor.fail(`index ${name}: expected "," or ")", found ${JSON.stringify(sep.text)}`);
|
|
391
|
+
}
|
|
392
|
+
// A trailing token here is a partial-index `WHERE`, `COLLATE`, etc.
|
|
393
|
+
const trailing = cursor.peek();
|
|
394
|
+
if (trailing !== undefined) {
|
|
395
|
+
if (trailing.kind === 'word' && trailing.text.toUpperCase() === 'WHERE') {
|
|
396
|
+
cursor.fail(`index ${name}: partial indexes (WHERE …) are unsupported`);
|
|
397
|
+
}
|
|
398
|
+
cursor.fail(`index ${name}: unsupported trailing SQL ${JSON.stringify(trailing.text)}`);
|
|
399
|
+
}
|
|
400
|
+
table.indexes.push({ name, columns, unique });
|
|
401
|
+
}
|
|
402
|
+
function parseAlterTable(cursor, tables) {
|
|
403
|
+
cursor.expectWord('TABLE', 'after ALTER');
|
|
404
|
+
const name = cursor.identifier('a table name');
|
|
405
|
+
const table = tables.get(name);
|
|
406
|
+
if (table === undefined) {
|
|
407
|
+
cursor.fail(`ALTER TABLE ${name}: table does not exist at this point`);
|
|
408
|
+
}
|
|
409
|
+
const action = cursor.identifier('an ALTER TABLE action');
|
|
410
|
+
if (action.toUpperCase() !== 'ADD') {
|
|
411
|
+
cursor.fail(`ALTER TABLE ${name}: unsupported action ${JSON.stringify(action)} (only ADD COLUMN)`);
|
|
412
|
+
}
|
|
413
|
+
cursor.eatWord('COLUMN');
|
|
414
|
+
const def = parseColumnDef(cursor, false);
|
|
415
|
+
cursor.expectEnd();
|
|
416
|
+
if (table.columns.some((c) => c.name === def.column.name)) {
|
|
417
|
+
cursor.fail(`ALTER TABLE ${name}: duplicate column ${JSON.stringify(def.column.name)}`);
|
|
418
|
+
}
|
|
419
|
+
table.columns.push(def.column);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Apply one migration file's SQL to the accumulated table map. Columns
|
|
423
|
+
* accumulate in declaration order — the §2.4 row-codec positional order.
|
|
424
|
+
*/
|
|
425
|
+
export function applyMigrationSql(tables, sql, source) {
|
|
426
|
+
for (const tokens of tokenizeStatements(sql, source)) {
|
|
427
|
+
const cursor = new Cursor(tokens, source);
|
|
428
|
+
const head = cursor.next();
|
|
429
|
+
const word = head.kind === 'word' ? head.text.toUpperCase() : '';
|
|
430
|
+
if (word === 'CREATE') {
|
|
431
|
+
parseCreate(cursor, tables, source);
|
|
432
|
+
}
|
|
433
|
+
else if (word === 'ALTER') {
|
|
434
|
+
parseAlterTable(cursor, tables);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
throw new TypegenError(source, `unsupported SQL statement starting with ${JSON.stringify(head.text)} (only CREATE TABLE, CREATE [UNIQUE] INDEX, and ALTER TABLE … ADD COLUMN)`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/typegen",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "TypeScript type generator
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Syncular schema-to-TypeScript type generator and the `syncular` CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
7
7
|
"homepage": "https://syncular.dev",
|
|
@@ -18,57 +18,37 @@
|
|
|
18
18
|
"offline-first",
|
|
19
19
|
"realtime",
|
|
20
20
|
"database",
|
|
21
|
-
"typescript",
|
|
22
|
-
"codegen",
|
|
23
21
|
"typescript"
|
|
24
22
|
],
|
|
25
|
-
"
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
26
25
|
"publishConfig": {
|
|
27
26
|
"access": "public"
|
|
28
27
|
},
|
|
29
|
-
"type": "module",
|
|
30
28
|
"bin": {
|
|
31
|
-
"syncular
|
|
29
|
+
"syncular": "./dist/cli.js"
|
|
32
30
|
},
|
|
33
31
|
"exports": {
|
|
34
32
|
".": {
|
|
35
33
|
"bun": "./src/index.ts",
|
|
34
|
+
"browser": "./src/index.ts",
|
|
36
35
|
"import": {
|
|
37
36
|
"types": "./dist/index.d.ts",
|
|
38
37
|
"default": "./dist/index.js"
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
},
|
|
42
|
-
"scripts": {
|
|
43
|
-
"test": "bun test --pass-with-no-tests",
|
|
44
|
-
"tsgo": "tsgo --noEmit",
|
|
45
|
-
"build": "tsgo",
|
|
46
|
-
"release": "syncular-publish"
|
|
47
|
-
},
|
|
48
|
-
"dependencies": {
|
|
49
|
-
"@electric-sql/pglite": "^0.3.15",
|
|
50
|
-
"@syncular/migrations": "0.1.1",
|
|
51
|
-
"better-sqlite3": "^12.8.0",
|
|
52
|
-
"kysely-bun-sqlite": "^0.4.0",
|
|
53
|
-
"kysely-pglite-dialect": "^1.2.0"
|
|
54
|
-
},
|
|
55
|
-
"peerDependencies": {
|
|
56
|
-
"kysely": "^0.28.0"
|
|
57
|
-
},
|
|
58
|
-
"devDependencies": {
|
|
59
|
-
"@syncular/config": "0.0.0",
|
|
60
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
61
|
-
"kysely": "*"
|
|
62
|
-
},
|
|
63
41
|
"files": [
|
|
64
42
|
"dist",
|
|
65
43
|
"src",
|
|
44
|
+
"README.md",
|
|
66
45
|
"!src/**/*.test.ts",
|
|
67
46
|
"!src/**/*.test.tsx",
|
|
68
|
-
"!
|
|
69
|
-
"!dist/**/*.test
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
|
|
47
|
+
"!dist/**/*.test.js",
|
|
48
|
+
"!dist/**/*.test.d.ts"
|
|
49
|
+
],
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@syncular/core": "0.2.0",
|
|
52
|
+
"@syncular/server": "0.2.0"
|
|
53
|
+
}
|
|
74
54
|
}
|