@syncular/typegen 0.15.33 → 0.15.35

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 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 = /[A-Za-z_]/;
54
- const WORD_PART = /[A-Za-z0-9_]/;
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
- tables.set(name, {
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
- table.indexes.push({ name, columns, unique });
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.33",
3
+ "version": "0.15.35",
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.35"
60
+ },
58
61
  "devDependencies": {
59
- "@syncular/core": "0.15.33",
60
- "@syncular/server": "0.15.33"
62
+ "@syncular/server": "0.15.35"
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 { applyMigrationSql, type ParsedTable } from './sql';
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 = /[A-Za-z_]/;
73
- const WORD_PART = /[A-Za-z0-9_]/;
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
- tables.set(name, {
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
- table.indexes.push({ name, columns, unique });
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 {