@syncular/typegen 0.15.32 → 0.15.34
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 +12 -1
- package/dist/generate.js +2 -1
- package/dist/sql.d.ts +7 -0
- package/dist/sql.js +48 -10
- package/package.json +5 -3
- package/src/generate.ts +6 -1
- package/src/sql.ts +67 -8
package/README.md
CHANGED
|
@@ -278,6 +278,14 @@ is bare column names: **ASC/DESC**, **expression** columns (`lower(a)`), and
|
|
|
278
278
|
**partial** (`WHERE …`) indexes are hard errors (the IR models column names
|
|
279
279
|
only, so accepting a direction/expression would silently drop it).
|
|
280
280
|
|
|
281
|
+
Synced table, column, and secondary-index identifiers are checked against the
|
|
282
|
+
same portable relational rules used by server schema compilation. Names use a
|
|
283
|
+
maximum of **63 UTF-8 bytes** (not JavaScript characters), and `sync_`/`_sync`
|
|
284
|
+
remain reserved for Syncular storage. Generation names the migration, object,
|
|
285
|
+
actual byte count, and portable limit before writing the migration lock or any
|
|
286
|
+
generated artifact. Exactly 63 bytes is accepted. Bare Unicode identifiers are
|
|
287
|
+
supported; quoted identifiers remain outside the migration subset.
|
|
288
|
+
|
|
281
289
|
**Index replacement** (`DROP INDEX`). A previously declared secondary index
|
|
282
290
|
may be removed with `DROP INDEX name` or conditionally with `IF EXISTS`.
|
|
283
291
|
Generation removes it from the head IR; a later `CREATE INDEX` may reuse the
|
|
@@ -285,7 +293,10 @@ name with a different uniqueness or column definition. On a server schema
|
|
|
285
293
|
bump, Syncular rebuilds the declared secondary indexes for its owned
|
|
286
294
|
relational projection tables before advancing the version marker. Client
|
|
287
295
|
mirrors already wipe and re-bootstrap application tables on every schema
|
|
288
|
-
version change.
|
|
296
|
+
version change. Portability validation applies to the final head schema: an
|
|
297
|
+
already-locked historical index with a non-portable name may therefore be
|
|
298
|
+
dropped and replaced in a forward migration, while a non-portable name that
|
|
299
|
+
remains in the head still fails generation.
|
|
289
300
|
|
|
290
301
|
**Table retirement** (`DROP TABLE`). A table created earlier in migration
|
|
291
302
|
history and dropped before the head version is absent from the IR and must be
|
package/dist/generate.js
CHANGED
|
@@ -21,7 +21,7 @@ import { buildMigrationLock, LEGACY_MIGRATION_LOCK_FORMAT_VERSION, MIGRATION_LOC
|
|
|
21
21
|
import { buildNamingMap } from './naming.js';
|
|
22
22
|
import { analyzeQueryFile, synthesizeDdl, } from './query.js';
|
|
23
23
|
import { serializeQueryIr } from './query-ir.js';
|
|
24
|
-
import { applyMigrationSql } from './sql.js';
|
|
24
|
+
import { applyMigrationSql, validateFinalSchemaIdentifiers, } from './sql.js';
|
|
25
25
|
import { lowerSyqlQuery } from './syql-lowering.js';
|
|
26
26
|
import { buildSyqlModuleGraph } from './syql-modules.js';
|
|
27
27
|
import { analyzeSyqlSemantics } from './syql-semantics.js';
|
|
@@ -159,6 +159,7 @@ export function buildIr(manifest, migrations) {
|
|
|
159
159
|
for (const migration of migrations) {
|
|
160
160
|
applyMigrationSql(parsedTables, migration.sql, `${migration.name}/up.sql`, droppedTables);
|
|
161
161
|
}
|
|
162
|
+
validateFinalSchemaIdentifiers(parsedTables);
|
|
162
163
|
const schemaVersions = buildSchemaVersions(manifest, migrations);
|
|
163
164
|
const tables = manifest.tables.map((manifestTable) => {
|
|
164
165
|
const parsed = parsedTables.get(manifestTable.name);
|
package/dist/sql.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ export interface ParsedTable {
|
|
|
8
8
|
/** Client-local contentful FTS5 projections. */
|
|
9
9
|
readonly ftsIndexes: IrFtsIndex[];
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Validate the accumulated head schema after every migration has been
|
|
13
|
+
* applied. This deliberately permits a locked historical migration to create
|
|
14
|
+
* an invalid identifier when a later forward migration drops it; only final
|
|
15
|
+
* generated objects must be portable.
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateFinalSchemaIdentifiers(tables: ReadonlyMap<string, ParsedTable>): void;
|
|
11
18
|
/**
|
|
12
19
|
* Apply one migration file's SQL to the accumulated table map. Columns
|
|
13
20
|
* accumulate in declaration order — the §2.4 row-codec positional order.
|
package/dist/sql.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* primary keys, ASC/DESC or expression index columns, partial (`WHERE`)
|
|
22
22
|
* indexes — is a hard error naming the unsupported construct.
|
|
23
23
|
*/
|
|
24
|
+
import { validatePortableRelationalIdentifier } from '@syncular/core';
|
|
24
25
|
import { TypegenError } from './errors.js';
|
|
25
26
|
/** SQL type keyword → the §2.4 column types. Case-insensitive. */
|
|
26
27
|
const TYPE_MAP = {
|
|
@@ -50,9 +51,38 @@ const TYPE_MAP = {
|
|
|
50
51
|
};
|
|
51
52
|
/** Default `crdtType` for a bare `CRDT` keyword (§5.10.1). */
|
|
52
53
|
const DEFAULT_CRDT_TYPE = 'yjs-doc';
|
|
53
|
-
const WORD_START = /[
|
|
54
|
-
const WORD_PART = /[
|
|
54
|
+
const WORD_START = /[\p{L}_]/u;
|
|
55
|
+
const WORD_PART = /[\p{L}\p{M}\p{N}_]/u;
|
|
55
56
|
const DIGIT = /[0-9]/;
|
|
57
|
+
const tableIdentifierSources = new WeakMap();
|
|
58
|
+
const columnIdentifierSources = new WeakMap();
|
|
59
|
+
const indexIdentifierSources = new WeakMap();
|
|
60
|
+
function validateIdentifier(source, kind, identifier) {
|
|
61
|
+
try {
|
|
62
|
+
validatePortableRelationalIdentifier(kind, identifier);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new TypegenError(source, error instanceof Error ? error.message : 'invalid relational identifier');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validate the accumulated head schema after every migration has been
|
|
70
|
+
* applied. This deliberately permits a locked historical migration to create
|
|
71
|
+
* an invalid identifier when a later forward migration drops it; only final
|
|
72
|
+
* generated objects must be portable.
|
|
73
|
+
*/
|
|
74
|
+
export function validateFinalSchemaIdentifiers(tables) {
|
|
75
|
+
for (const table of tables.values()) {
|
|
76
|
+
const tableSource = tableIdentifierSources.get(table) ?? 'migrations';
|
|
77
|
+
validateIdentifier(tableSource, 'table', table.name);
|
|
78
|
+
for (const column of table.columns) {
|
|
79
|
+
validateIdentifier(columnIdentifierSources.get(column) ?? tableSource, `table ${table.name}: column`, column.name);
|
|
80
|
+
}
|
|
81
|
+
for (const index of table.indexes) {
|
|
82
|
+
validateIdentifier(indexIdentifierSources.get(index) ?? tableSource, `table ${table.name}: index`, index.name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
56
86
|
function tokenizeStatements(sql, source) {
|
|
57
87
|
const statements = [];
|
|
58
88
|
let current = [];
|
|
@@ -262,11 +292,11 @@ function parseCreate(cursor, tables, droppedTables, source) {
|
|
|
262
292
|
}
|
|
263
293
|
if (cursor.eatWord('UNIQUE')) {
|
|
264
294
|
cursor.expectWord('INDEX', 'after CREATE UNIQUE');
|
|
265
|
-
parseCreateIndex(cursor, tables, true);
|
|
295
|
+
parseCreateIndex(cursor, tables, true, source);
|
|
266
296
|
return;
|
|
267
297
|
}
|
|
268
298
|
if (cursor.eatWord('INDEX')) {
|
|
269
|
-
parseCreateIndex(cursor, tables, false);
|
|
299
|
+
parseCreateIndex(cursor, tables, false, source);
|
|
270
300
|
return;
|
|
271
301
|
}
|
|
272
302
|
const token = cursor.peek();
|
|
@@ -451,13 +481,18 @@ function parseCreateTable(cursor, tables, droppedTables, source) {
|
|
|
451
481
|
throw new TypegenError(source, `table ${name}: primary key ${JSON.stringify(primaryKey)} is not a column`);
|
|
452
482
|
}
|
|
453
483
|
const finalColumns = columns.map((c) => c.name === primaryKey ? { ...c, nullable: false } : c);
|
|
454
|
-
|
|
484
|
+
const table = {
|
|
455
485
|
name,
|
|
456
486
|
primaryKey,
|
|
457
487
|
columns: finalColumns,
|
|
458
488
|
indexes: [],
|
|
459
489
|
ftsIndexes: [],
|
|
460
|
-
}
|
|
490
|
+
};
|
|
491
|
+
tables.set(name, table);
|
|
492
|
+
tableIdentifierSources.set(table, source);
|
|
493
|
+
for (const column of finalColumns) {
|
|
494
|
+
columnIdentifierSources.set(column, source);
|
|
495
|
+
}
|
|
461
496
|
}
|
|
462
497
|
/**
|
|
463
498
|
* `CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (col [, col…])`.
|
|
@@ -467,7 +502,7 @@ function parseCreateTable(cursor, tables, droppedTables, source) {
|
|
|
467
502
|
* (partial index) clause are hard errors naming the construct. Index names
|
|
468
503
|
* are unique per accumulated schema.
|
|
469
504
|
*/
|
|
470
|
-
function parseCreateIndex(cursor, tables, unique) {
|
|
505
|
+
function parseCreateIndex(cursor, tables, unique, source) {
|
|
471
506
|
// `INDEX` already matched by the dispatcher.
|
|
472
507
|
if (cursor.eatWord('IF')) {
|
|
473
508
|
cursor.expectWord('NOT', 'after IF');
|
|
@@ -527,9 +562,11 @@ function parseCreateIndex(cursor, tables, unique) {
|
|
|
527
562
|
}
|
|
528
563
|
cursor.fail(`index ${name}: unsupported trailing SQL ${JSON.stringify(trailing.text)}`);
|
|
529
564
|
}
|
|
530
|
-
|
|
565
|
+
const index = { name, columns, unique };
|
|
566
|
+
table.indexes.push(index);
|
|
567
|
+
indexIdentifierSources.set(index, source);
|
|
531
568
|
}
|
|
532
|
-
function parseAlterTable(cursor, tables) {
|
|
569
|
+
function parseAlterTable(cursor, tables, source) {
|
|
533
570
|
cursor.expectWord('TABLE', 'after ALTER');
|
|
534
571
|
const name = cursor.identifier('a table name');
|
|
535
572
|
const table = tables.get(name);
|
|
@@ -550,6 +587,7 @@ function parseAlterTable(cursor, tables) {
|
|
|
550
587
|
cursor.fail(`ALTER TABLE ${name}: duplicate column ${JSON.stringify(def.column.name)}`);
|
|
551
588
|
}
|
|
552
589
|
table.columns.push(def.column);
|
|
590
|
+
columnIdentifierSources.set(def.column, source);
|
|
553
591
|
}
|
|
554
592
|
/** `DROP TABLE [IF EXISTS] name`; table-name reuse remains unsupported. */
|
|
555
593
|
function parseDropTable(cursor, tables, droppedTables) {
|
|
@@ -613,7 +651,7 @@ export function applyMigrationSql(tables, sql, source, droppedTables = new Set()
|
|
|
613
651
|
parseCreate(cursor, tables, droppedTables, source);
|
|
614
652
|
}
|
|
615
653
|
else if (word === 'ALTER') {
|
|
616
|
-
parseAlterTable(cursor, tables);
|
|
654
|
+
parseAlterTable(cursor, tables, source);
|
|
617
655
|
}
|
|
618
656
|
else if (word === 'DROP') {
|
|
619
657
|
parseDrop(cursor, tables, droppedTables);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/typegen",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.34",
|
|
4
4
|
"description": "Syncular schema-to-TypeScript type generator and the `syncular` CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -55,8 +55,10 @@
|
|
|
55
55
|
"!dist/**/*.test.js",
|
|
56
56
|
"!dist/**/*.test.d.ts"
|
|
57
57
|
],
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@syncular/core": "0.15.34"
|
|
60
|
+
},
|
|
58
61
|
"devDependencies": {
|
|
59
|
-
"@syncular/
|
|
60
|
-
"@syncular/server": "0.15.32"
|
|
62
|
+
"@syncular/server": "0.15.34"
|
|
61
63
|
}
|
|
62
64
|
}
|
package/src/generate.ts
CHANGED
|
@@ -58,7 +58,11 @@ import {
|
|
|
58
58
|
synthesizeDdl,
|
|
59
59
|
} from './query';
|
|
60
60
|
import { serializeQueryIr } from './query-ir';
|
|
61
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
applyMigrationSql,
|
|
63
|
+
type ParsedTable,
|
|
64
|
+
validateFinalSchemaIdentifiers,
|
|
65
|
+
} from './sql';
|
|
62
66
|
import { lowerSyqlQuery } from './syql-lowering';
|
|
63
67
|
import { buildSyqlModuleGraph } from './syql-modules';
|
|
64
68
|
import { analyzeSyqlSemantics } from './syql-semantics';
|
|
@@ -282,6 +286,7 @@ export function buildIr(
|
|
|
282
286
|
droppedTables,
|
|
283
287
|
);
|
|
284
288
|
}
|
|
289
|
+
validateFinalSchemaIdentifiers(parsedTables);
|
|
285
290
|
const schemaVersions = buildSchemaVersions(manifest, migrations);
|
|
286
291
|
const tables = manifest.tables.map((manifestTable) => {
|
|
287
292
|
const parsed = parsedTables.get(manifestTable.name);
|
package/src/sql.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* primary keys, ASC/DESC or expression index columns, partial (`WHERE`)
|
|
22
22
|
* indexes — is a hard error naming the unsupported construct.
|
|
23
23
|
*/
|
|
24
|
+
import { validatePortableRelationalIdentifier } from '@syncular/core';
|
|
24
25
|
import { TypegenError } from './errors';
|
|
25
26
|
import type { IrColumn, IrColumnType, IrFtsIndex, IrIndex } from './ir';
|
|
26
27
|
|
|
@@ -69,10 +70,58 @@ interface Token {
|
|
|
69
70
|
readonly text: string;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
const WORD_START = /[
|
|
73
|
-
const WORD_PART = /[
|
|
73
|
+
const WORD_START = /[\p{L}_]/u;
|
|
74
|
+
const WORD_PART = /[\p{L}\p{M}\p{N}_]/u;
|
|
74
75
|
const DIGIT = /[0-9]/;
|
|
75
76
|
|
|
77
|
+
const tableIdentifierSources = new WeakMap<ParsedTable, string>();
|
|
78
|
+
const columnIdentifierSources = new WeakMap<IrColumn, string>();
|
|
79
|
+
const indexIdentifierSources = new WeakMap<IrIndex, string>();
|
|
80
|
+
|
|
81
|
+
function validateIdentifier(
|
|
82
|
+
source: string,
|
|
83
|
+
kind: string,
|
|
84
|
+
identifier: string,
|
|
85
|
+
): void {
|
|
86
|
+
try {
|
|
87
|
+
validatePortableRelationalIdentifier(kind, identifier);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new TypegenError(
|
|
90
|
+
source,
|
|
91
|
+
error instanceof Error ? error.message : 'invalid relational identifier',
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Validate the accumulated head schema after every migration has been
|
|
98
|
+
* applied. This deliberately permits a locked historical migration to create
|
|
99
|
+
* an invalid identifier when a later forward migration drops it; only final
|
|
100
|
+
* generated objects must be portable.
|
|
101
|
+
*/
|
|
102
|
+
export function validateFinalSchemaIdentifiers(
|
|
103
|
+
tables: ReadonlyMap<string, ParsedTable>,
|
|
104
|
+
): void {
|
|
105
|
+
for (const table of tables.values()) {
|
|
106
|
+
const tableSource = tableIdentifierSources.get(table) ?? 'migrations';
|
|
107
|
+
validateIdentifier(tableSource, 'table', table.name);
|
|
108
|
+
for (const column of table.columns) {
|
|
109
|
+
validateIdentifier(
|
|
110
|
+
columnIdentifierSources.get(column) ?? tableSource,
|
|
111
|
+
`table ${table.name}: column`,
|
|
112
|
+
column.name,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
for (const index of table.indexes) {
|
|
116
|
+
validateIdentifier(
|
|
117
|
+
indexIdentifierSources.get(index) ?? tableSource,
|
|
118
|
+
`table ${table.name}: index`,
|
|
119
|
+
index.name,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
76
125
|
function tokenizeStatements(sql: string, source: string): Token[][] {
|
|
77
126
|
const statements: Token[][] = [];
|
|
78
127
|
let current: Token[] = [];
|
|
@@ -310,11 +359,11 @@ function parseCreate(
|
|
|
310
359
|
}
|
|
311
360
|
if (cursor.eatWord('UNIQUE')) {
|
|
312
361
|
cursor.expectWord('INDEX', 'after CREATE UNIQUE');
|
|
313
|
-
parseCreateIndex(cursor, tables, true);
|
|
362
|
+
parseCreateIndex(cursor, tables, true, source);
|
|
314
363
|
return;
|
|
315
364
|
}
|
|
316
365
|
if (cursor.eatWord('INDEX')) {
|
|
317
|
-
parseCreateIndex(cursor, tables, false);
|
|
366
|
+
parseCreateIndex(cursor, tables, false, source);
|
|
318
367
|
return;
|
|
319
368
|
}
|
|
320
369
|
const token = cursor.peek();
|
|
@@ -549,13 +598,18 @@ function parseCreateTable(
|
|
|
549
598
|
const finalColumns = columns.map((c) =>
|
|
550
599
|
c.name === primaryKey ? { ...c, nullable: false } : c,
|
|
551
600
|
);
|
|
552
|
-
|
|
601
|
+
const table: ParsedTable = {
|
|
553
602
|
name,
|
|
554
603
|
primaryKey,
|
|
555
604
|
columns: finalColumns,
|
|
556
605
|
indexes: [],
|
|
557
606
|
ftsIndexes: [],
|
|
558
|
-
}
|
|
607
|
+
};
|
|
608
|
+
tables.set(name, table);
|
|
609
|
+
tableIdentifierSources.set(table, source);
|
|
610
|
+
for (const column of finalColumns) {
|
|
611
|
+
columnIdentifierSources.set(column, source);
|
|
612
|
+
}
|
|
559
613
|
}
|
|
560
614
|
|
|
561
615
|
/**
|
|
@@ -570,6 +624,7 @@ function parseCreateIndex(
|
|
|
570
624
|
cursor: Cursor,
|
|
571
625
|
tables: Map<string, ParsedTable>,
|
|
572
626
|
unique: boolean,
|
|
627
|
+
source: string,
|
|
573
628
|
): void {
|
|
574
629
|
// `INDEX` already matched by the dispatcher.
|
|
575
630
|
if (cursor.eatWord('IF')) {
|
|
@@ -646,12 +701,15 @@ function parseCreateIndex(
|
|
|
646
701
|
`index ${name}: unsupported trailing SQL ${JSON.stringify(trailing.text)}`,
|
|
647
702
|
);
|
|
648
703
|
}
|
|
649
|
-
|
|
704
|
+
const index: IrIndex = { name, columns, unique };
|
|
705
|
+
table.indexes.push(index);
|
|
706
|
+
indexIdentifierSources.set(index, source);
|
|
650
707
|
}
|
|
651
708
|
|
|
652
709
|
function parseAlterTable(
|
|
653
710
|
cursor: Cursor,
|
|
654
711
|
tables: Map<string, ParsedTable>,
|
|
712
|
+
source: string,
|
|
655
713
|
): void {
|
|
656
714
|
cursor.expectWord('TABLE', 'after ALTER');
|
|
657
715
|
const name = cursor.identifier('a table name');
|
|
@@ -679,6 +737,7 @@ function parseAlterTable(
|
|
|
679
737
|
);
|
|
680
738
|
}
|
|
681
739
|
table.columns.push(def.column);
|
|
740
|
+
columnIdentifierSources.set(def.column, source);
|
|
682
741
|
}
|
|
683
742
|
|
|
684
743
|
/** `DROP TABLE [IF EXISTS] name`; table-name reuse remains unsupported. */
|
|
@@ -763,7 +822,7 @@ export function applyMigrationSql(
|
|
|
763
822
|
if (word === 'CREATE') {
|
|
764
823
|
parseCreate(cursor, tables, droppedTables, source);
|
|
765
824
|
} else if (word === 'ALTER') {
|
|
766
|
-
parseAlterTable(cursor, tables);
|
|
825
|
+
parseAlterTable(cursor, tables, source);
|
|
767
826
|
} else if (word === 'DROP') {
|
|
768
827
|
parseDrop(cursor, tables, droppedTables);
|
|
769
828
|
} else {
|