@syncular/typegen 0.11.0 → 0.13.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 CHANGED
@@ -184,6 +184,7 @@ duplicate ordinals are errors). The parser accepts exactly:
184
184
  - `CREATE TABLE [IF NOT EXISTS] name ( column-defs…, [PRIMARY KEY (col)] ) [WITHOUT ROWID]`
185
185
  - `ALTER TABLE name ADD [COLUMN] column-def`
186
186
  - `CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table ( col [, col…] )`
187
+ - `DROP TABLE [IF EXISTS] name`
187
188
  - column-def: `name TYPE [PRIMARY KEY] [NOT NULL] [NULL] [DEFAULT literal]`
188
189
  - `--` line comments and `/* … */` block comments
189
190
 
@@ -206,21 +207,27 @@ inline or table-level.
206
207
  secondary indexes on an already-created table: `CREATE INDEX name ON table
207
208
  (col)`, a compound `… (a, b)` (order preserved), the `UNIQUE` variant, and
208
209
  `IF NOT EXISTS`. Each index becomes an `Ir​Table.indexes` entry
209
- (`{ name, columns, unique }`, declaration order) and is materialized as a real
210
- SQLite index by the **clients** (the TS web-client mirror + the Rust core's
211
- base/visible table pair) and by typegen's own named-query type-check DB. Index
212
- columns must exist on the table; index names must be unique across the schema;
213
- a column must not repeat within one index. **Indexes are client-side only**:
214
- the server stores rows in a generic `sync_rows` table with an opaque payload
215
- (no per-user-table SQL columns exist server-side), so a user-column index has
216
- nothing to attach to there the scope inverted-index already covers server
217
- reads. The column list is bare column names: **ASC/DESC**, **expression**
218
- columns (`lower(a)`), and **partial** (`WHERE …`) indexes are hard errors (the
219
- IR models column names only, so accepting a direction/expression would silently
220
- drop it).
210
+ (`{ name, columns, unique }`, declaration order) and is materialized by the
211
+ clients (the TS web-client mirror + the Rust core's base/visible table pair),
212
+ typegen's named-query type-check DB, and the server's relational app-table
213
+ projection. Index columns must exist on the table; index names must be unique
214
+ across the schema; a column must not repeat within one index. The column list
215
+ is bare column names: **ASC/DESC**, **expression** columns (`lower(a)`), and
216
+ **partial** (`WHERE …`) indexes are hard errors (the IR models column names
217
+ only, so accepting a direction/expression would silently drop it).
218
+
219
+ **Table retirement** (`DROP TABLE`). A table created earlier in migration
220
+ history and dropped before the head version is absent from the IR and must be
221
+ omitted from the manifest's `tables` list. `IF EXISTS` is supported. Reusing a
222
+ dropped table name is rejected: the head-only IR cannot distinguish a new
223
+ table from an incompatible in-place rewrite on an upgrading server. On a
224
+ schema-version bump, clients wipe and re-bootstrap their synced tables; the
225
+ reference relational server drops the retired current-row table and its live
226
+ scope index. Append-only commit history follows the configured retention
227
+ policy—`DROP TABLE` is schema retirement, not a compliance erasure API.
221
228
 
222
229
  **Hard errors** (each names the construct and source file): any other
223
- statement (`CREATE TRIGGER/VIEW`, `DROP`, DML, `ALTER … RENAME`, …); unknown
230
+ statement (`CREATE TRIGGER/VIEW`, `DROP INDEX`, DML, `ALTER … RENAME`, …); unknown
224
231
  or parameterized types (`VARCHAR(36)`); quoted identifiers
225
232
  (`"t"`, `` `t` ``, `[t]`); table constraints (`FOREIGN KEY`, `UNIQUE`,
226
233
  `CHECK`, `CONSTRAINT`); column constraints beyond the list above
package/dist/generate.js CHANGED
@@ -152,8 +152,9 @@ export function buildIr(manifest, migrations) {
152
152
  throw new TypegenError(MANIFEST_FILENAME, 'no migrations found');
153
153
  }
154
154
  const parsedTables = new Map();
155
+ const droppedTables = new Set();
155
156
  for (const migration of migrations) {
156
- applyMigrationSql(parsedTables, migration.sql, `${migration.name}/up.sql`);
157
+ applyMigrationSql(parsedTables, migration.sql, `${migration.name}/up.sql`, droppedTables);
157
158
  }
158
159
  const schemaVersions = buildSchemaVersions(manifest, migrations);
159
160
  const tables = manifest.tables.map((manifestTable) => {
package/dist/sql.d.ts CHANGED
@@ -10,4 +10,4 @@ export interface ParsedTable {
10
10
  * Apply one migration file's SQL to the accumulated table map. Columns
11
11
  * accumulate in declaration order — the §2.4 row-codec positional order.
12
12
  */
13
- export declare function applyMigrationSql(tables: Map<string, ParsedTable>, sql: string, source: string): void;
13
+ export declare function applyMigrationSql(tables: Map<string, ParsedTable>, sql: string, source: string, droppedTables?: Set<string>): void;
package/dist/sql.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * [WITHOUT ROWID]`
8
8
  * - `ALTER TABLE name ADD [COLUMN] coldef`
9
9
  * - `CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (col [, col…])`
10
+ * - `DROP TABLE [IF EXISTS] name`
10
11
  * - column defs: `name TYPE [PRIMARY KEY] [NOT NULL] [NULL]
11
12
  * [DEFAULT literal]`
12
13
  * - `--` and C-style comments
@@ -246,9 +247,9 @@ function parseColumnDef(cursor, allowPrimaryKey) {
246
247
  * Dispatch a `CREATE …` statement: `CREATE TABLE`, `CREATE INDEX`, or
247
248
  * `CREATE UNIQUE INDEX`. Anything else is a hard error naming the construct.
248
249
  */
249
- function parseCreate(cursor, tables, source) {
250
+ function parseCreate(cursor, tables, droppedTables, source) {
250
251
  if (cursor.eatWord('TABLE')) {
251
- parseCreateTable(cursor, tables, source);
252
+ parseCreateTable(cursor, tables, droppedTables, source);
252
253
  return;
253
254
  }
254
255
  if (cursor.eatWord('UNIQUE')) {
@@ -263,12 +264,15 @@ function parseCreate(cursor, tables, source) {
263
264
  const token = cursor.peek();
264
265
  cursor.fail(`unsupported CREATE statement (only CREATE TABLE and CREATE [UNIQUE] INDEX), found ${JSON.stringify(token?.text ?? '')}`);
265
266
  }
266
- function parseCreateTable(cursor, tables, source) {
267
+ function parseCreateTable(cursor, tables, droppedTables, source) {
267
268
  if (cursor.eatWord('IF')) {
268
269
  cursor.expectWord('NOT', 'after IF');
269
270
  cursor.expectWord('EXISTS', 'after IF NOT');
270
271
  }
271
272
  const name = cursor.identifier('a table name');
273
+ if (droppedTables.has(name)) {
274
+ cursor.fail(`table ${name} cannot be re-created after DROP TABLE — table-name reuse is unsupported`);
275
+ }
272
276
  if (tables.has(name)) {
273
277
  cursor.fail(`table ${name} is created twice`);
274
278
  }
@@ -418,23 +422,44 @@ function parseAlterTable(cursor, tables) {
418
422
  }
419
423
  table.columns.push(def.column);
420
424
  }
425
+ /** `DROP TABLE [IF EXISTS] name`; table-name reuse remains unsupported. */
426
+ function parseDropTable(cursor, tables, droppedTables) {
427
+ cursor.expectWord('TABLE', 'after DROP');
428
+ let ifExists = false;
429
+ if (cursor.eatWord('IF')) {
430
+ cursor.expectWord('EXISTS', 'after IF');
431
+ ifExists = true;
432
+ }
433
+ const name = cursor.identifier('a table name');
434
+ cursor.expectEnd();
435
+ if (!tables.has(name)) {
436
+ if (ifExists)
437
+ return;
438
+ cursor.fail(`DROP TABLE ${name}: table does not exist at this point`);
439
+ }
440
+ tables.delete(name);
441
+ droppedTables.add(name);
442
+ }
421
443
  /**
422
444
  * Apply one migration file's SQL to the accumulated table map. Columns
423
445
  * accumulate in declaration order — the §2.4 row-codec positional order.
424
446
  */
425
- export function applyMigrationSql(tables, sql, source) {
447
+ export function applyMigrationSql(tables, sql, source, droppedTables = new Set()) {
426
448
  for (const tokens of tokenizeStatements(sql, source)) {
427
449
  const cursor = new Cursor(tokens, source);
428
450
  const head = cursor.next();
429
451
  const word = head.kind === 'word' ? head.text.toUpperCase() : '';
430
452
  if (word === 'CREATE') {
431
- parseCreate(cursor, tables, source);
453
+ parseCreate(cursor, tables, droppedTables, source);
432
454
  }
433
455
  else if (word === 'ALTER') {
434
456
  parseAlterTable(cursor, tables);
435
457
  }
458
+ else if (word === 'DROP') {
459
+ parseDropTable(cursor, tables, droppedTables);
460
+ }
436
461
  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)`);
462
+ throw new TypegenError(source, `unsupported SQL statement starting with ${JSON.stringify(head.text)} (only CREATE TABLE, CREATE [UNIQUE] INDEX, ALTER TABLE … ADD COLUMN, and DROP TABLE)`);
438
463
  }
439
464
  }
440
465
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/typegen",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Syncular schema-to-TypeScript type generator and the `syncular` CLI",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -48,7 +48,7 @@
48
48
  "!dist/**/*.test.d.ts"
49
49
  ],
50
50
  "devDependencies": {
51
- "@syncular/core": "0.11.0",
52
- "@syncular/server": "0.11.0"
51
+ "@syncular/core": "0.13.0",
52
+ "@syncular/server": "0.13.0"
53
53
  }
54
54
  }
package/src/generate.ts CHANGED
@@ -263,8 +263,14 @@ export function buildIr(
263
263
  throw new TypegenError(MANIFEST_FILENAME, 'no migrations found');
264
264
  }
265
265
  const parsedTables = new Map<string, ParsedTable>();
266
+ const droppedTables = new Set<string>();
266
267
  for (const migration of migrations) {
267
- applyMigrationSql(parsedTables, migration.sql, `${migration.name}/up.sql`);
268
+ applyMigrationSql(
269
+ parsedTables,
270
+ migration.sql,
271
+ `${migration.name}/up.sql`,
272
+ droppedTables,
273
+ );
268
274
  }
269
275
  const schemaVersions = buildSchemaVersions(manifest, migrations);
270
276
  const tables = manifest.tables.map((manifestTable) => {
package/src/sql.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * [WITHOUT ROWID]`
8
8
  * - `ALTER TABLE name ADD [COLUMN] coldef`
9
9
  * - `CREATE [UNIQUE] INDEX [IF NOT EXISTS] name ON table (col [, col…])`
10
+ * - `DROP TABLE [IF EXISTS] name`
10
11
  * - column defs: `name TYPE [PRIMARY KEY] [NOT NULL] [NULL]
11
12
  * [DEFAULT literal]`
12
13
  * - `--` and C-style comments
@@ -290,10 +291,11 @@ function parseColumnDef(cursor: Cursor, allowPrimaryKey: boolean): ColumnDef {
290
291
  function parseCreate(
291
292
  cursor: Cursor,
292
293
  tables: Map<string, ParsedTable>,
294
+ droppedTables: ReadonlySet<string>,
293
295
  source: string,
294
296
  ): void {
295
297
  if (cursor.eatWord('TABLE')) {
296
- parseCreateTable(cursor, tables, source);
298
+ parseCreateTable(cursor, tables, droppedTables, source);
297
299
  return;
298
300
  }
299
301
  if (cursor.eatWord('UNIQUE')) {
@@ -314,6 +316,7 @@ function parseCreate(
314
316
  function parseCreateTable(
315
317
  cursor: Cursor,
316
318
  tables: Map<string, ParsedTable>,
319
+ droppedTables: ReadonlySet<string>,
317
320
  source: string,
318
321
  ): void {
319
322
  if (cursor.eatWord('IF')) {
@@ -321,6 +324,11 @@ function parseCreateTable(
321
324
  cursor.expectWord('EXISTS', 'after IF NOT');
322
325
  }
323
326
  const name = cursor.identifier('a table name');
327
+ if (droppedTables.has(name)) {
328
+ cursor.fail(
329
+ `table ${name} cannot be re-created after DROP TABLE — table-name reuse is unsupported`,
330
+ );
331
+ }
324
332
  if (tables.has(name)) {
325
333
  cursor.fail(`table ${name} is created twice`);
326
334
  }
@@ -506,6 +514,28 @@ function parseAlterTable(
506
514
  table.columns.push(def.column);
507
515
  }
508
516
 
517
+ /** `DROP TABLE [IF EXISTS] name`; table-name reuse remains unsupported. */
518
+ function parseDropTable(
519
+ cursor: Cursor,
520
+ tables: Map<string, ParsedTable>,
521
+ droppedTables: Set<string>,
522
+ ): void {
523
+ cursor.expectWord('TABLE', 'after DROP');
524
+ let ifExists = false;
525
+ if (cursor.eatWord('IF')) {
526
+ cursor.expectWord('EXISTS', 'after IF');
527
+ ifExists = true;
528
+ }
529
+ const name = cursor.identifier('a table name');
530
+ cursor.expectEnd();
531
+ if (!tables.has(name)) {
532
+ if (ifExists) return;
533
+ cursor.fail(`DROP TABLE ${name}: table does not exist at this point`);
534
+ }
535
+ tables.delete(name);
536
+ droppedTables.add(name);
537
+ }
538
+
509
539
  /**
510
540
  * Apply one migration file's SQL to the accumulated table map. Columns
511
541
  * accumulate in declaration order — the §2.4 row-codec positional order.
@@ -514,19 +544,22 @@ export function applyMigrationSql(
514
544
  tables: Map<string, ParsedTable>,
515
545
  sql: string,
516
546
  source: string,
547
+ droppedTables: Set<string> = new Set<string>(),
517
548
  ): void {
518
549
  for (const tokens of tokenizeStatements(sql, source)) {
519
550
  const cursor = new Cursor(tokens, source);
520
551
  const head = cursor.next();
521
552
  const word = head.kind === 'word' ? head.text.toUpperCase() : '';
522
553
  if (word === 'CREATE') {
523
- parseCreate(cursor, tables, source);
554
+ parseCreate(cursor, tables, droppedTables, source);
524
555
  } else if (word === 'ALTER') {
525
556
  parseAlterTable(cursor, tables);
557
+ } else if (word === 'DROP') {
558
+ parseDropTable(cursor, tables, droppedTables);
526
559
  } else {
527
560
  throw new TypegenError(
528
561
  source,
529
- `unsupported SQL statement starting with ${JSON.stringify(head.text)} (only CREATE TABLE, CREATE [UNIQUE] INDEX, and ALTER TABLE … ADD COLUMN)`,
562
+ `unsupported SQL statement starting with ${JSON.stringify(head.text)} (only CREATE TABLE, CREATE [UNIQUE] INDEX, ALTER TABLE … ADD COLUMN, and DROP TABLE)`,
530
563
  );
531
564
  }
532
565
  }