@syncular/typegen 0.15.1 → 0.15.3
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 +4 -1
- package/dist/query.js +22 -2
- package/dist/syql-validator.js +11 -4
- package/package.json +3 -3
- package/src/query.ts +29 -2
- package/src/syql-validator.ts +18 -4
package/README.md
CHANGED
|
@@ -473,7 +473,10 @@ owning synced table for dependency and scope-coverage metadata. `bm25`,
|
|
|
473
473
|
`highlight`, and `snippet` are accepted deterministic FTS5 auxiliary
|
|
474
474
|
functions only when the query references a schema-declared FTS projection;
|
|
475
475
|
they use the normal computed-expression fidelity rules. Arbitrary extension
|
|
476
|
-
functions remain outside the portable profile.
|
|
476
|
+
functions remain outside the portable profile. A bounded FTS join must project
|
|
477
|
+
the FTS table's `_syncular_source_id` as well as every joined table's primary
|
|
478
|
+
key and end its total order in those identity fields. Typegen then proves the
|
|
479
|
+
composite row key instead of weakening bounded-query stability rules.
|
|
477
480
|
|
|
478
481
|
**Typing fidelity** (what `bun:sqlite` actually exposes, and its honest
|
|
479
482
|
boundary):
|
package/dist/query.js
CHANGED
|
@@ -86,6 +86,8 @@ const DECLTYPE_MAP = {
|
|
|
86
86
|
};
|
|
87
87
|
/** Local client protocol column; query-only and never part of schema IR. */
|
|
88
88
|
const SYNC_VERSION_COLUMN = '_sync_version';
|
|
89
|
+
/** Stable private identity shared by every declared local FTS5 projection. */
|
|
90
|
+
const FTS_SOURCE_ID_COLUMN = '_syncular_source_id';
|
|
89
91
|
const DEFAULT_NAMING = { naming: 'camel', targets: ['ts'] };
|
|
90
92
|
// -- path → name --------------------------------------------------------------
|
|
91
93
|
/** A single kebab-case path segment (folder) or filename stem. */
|
|
@@ -513,6 +515,14 @@ function splitSelectList(sql) {
|
|
|
513
515
|
return items;
|
|
514
516
|
}
|
|
515
517
|
const PLAIN_REF_RE = new RegExp(`^(?:(${IDENT})\\.)?(${IDENT})$`);
|
|
518
|
+
function declaredFtsProjection(ir, tableName) {
|
|
519
|
+
return ir.tables.some((table) => table.ftsIndexes.some((index) => index.name === tableName));
|
|
520
|
+
}
|
|
521
|
+
const FTS_SOURCE_ID_IR_COLUMN = {
|
|
522
|
+
name: FTS_SOURCE_ID_COLUMN,
|
|
523
|
+
type: 'string',
|
|
524
|
+
nullable: false,
|
|
525
|
+
};
|
|
516
526
|
/** Resolve a SELECT item's expression to an IR column, if it is a plain ref. */
|
|
517
527
|
function resolveSource(item, refs, ir) {
|
|
518
528
|
const m = PLAIN_REF_RE.exec(item.expr);
|
|
@@ -527,7 +537,13 @@ function resolveSource(item, refs, ir) {
|
|
|
527
537
|
return null;
|
|
528
538
|
const table = byName.get(ref.table);
|
|
529
539
|
const col = table?.columns.find((c) => c.name === columnName);
|
|
530
|
-
|
|
540
|
+
if (col !== undefined)
|
|
541
|
+
return { table: ref.table, column: col };
|
|
542
|
+
if (columnName === FTS_SOURCE_ID_COLUMN &&
|
|
543
|
+
declaredFtsProjection(ir, ref.table)) {
|
|
544
|
+
return { table: ref.table, column: FTS_SOURCE_ID_IR_COLUMN };
|
|
545
|
+
}
|
|
546
|
+
return null;
|
|
531
547
|
}
|
|
532
548
|
// Unqualified: search every FROM/JOIN table (SQLite already resolved
|
|
533
549
|
// ambiguity; a single-table query is the common case).
|
|
@@ -536,6 +552,10 @@ function resolveSource(item, refs, ir) {
|
|
|
536
552
|
const col = table?.columns.find((c) => c.name === columnName);
|
|
537
553
|
if (col !== undefined)
|
|
538
554
|
return { table: ref.table, column: col };
|
|
555
|
+
if (columnName === FTS_SOURCE_ID_COLUMN &&
|
|
556
|
+
declaredFtsProjection(ir, ref.table)) {
|
|
557
|
+
return { table: ref.table, column: FTS_SOURCE_ID_IR_COLUMN };
|
|
558
|
+
}
|
|
539
559
|
}
|
|
540
560
|
return null;
|
|
541
561
|
}
|
|
@@ -836,7 +856,7 @@ export function synthesizeDdl(ir) {
|
|
|
836
856
|
}
|
|
837
857
|
for (const index of table.ftsIndexes) {
|
|
838
858
|
const tokenize = index.tokenize.replaceAll("'", "''");
|
|
839
|
-
lines.push(`CREATE VIRTUAL TABLE ${index.name} USING fts5(
|
|
859
|
+
lines.push(`CREATE VIRTUAL TABLE ${index.name} USING fts5(${FTS_SOURCE_ID_COLUMN} UNINDEXED, ${index.columns.join(', ')}, tokenize='${tokenize}');`);
|
|
840
860
|
}
|
|
841
861
|
}
|
|
842
862
|
return lines.join('\n');
|
package/dist/syql-validator.js
CHANGED
|
@@ -762,7 +762,10 @@ class Validator {
|
|
|
762
762
|
}
|
|
763
763
|
candidates.push({ ref, scopes: inferred });
|
|
764
764
|
}
|
|
765
|
-
const
|
|
765
|
+
const ftsOwners = new Map(this.#ir.tables.flatMap((table) => table.ftsIndexes.map((index) => [index.name, table.name])));
|
|
766
|
+
const dependencies = [
|
|
767
|
+
...new Set(refs.map((ref) => ftsOwners.get(ref.table) ?? ref.table)),
|
|
768
|
+
]
|
|
766
769
|
.sort()
|
|
767
770
|
.map((table) => {
|
|
768
771
|
const instances = candidates.filter((item) => item.ref.table === table);
|
|
@@ -907,9 +910,13 @@ class Validator {
|
|
|
907
910
|
const inferred = [];
|
|
908
911
|
for (const ref of refs) {
|
|
909
912
|
const table = this.#ir.tables.find((item) => item.name === ref.table);
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
+
const primaryKey = table?.primaryKey ??
|
|
914
|
+
(this.#ir.tables.some((item) => item.ftsIndexes.some((index) => index.name === ref.table))
|
|
915
|
+
? '_syncular_source_id'
|
|
916
|
+
: undefined);
|
|
917
|
+
const column = analysis.columns.find((candidate) => primaryKey !== undefined &&
|
|
918
|
+
candidate.origin?.table === ref.table &&
|
|
919
|
+
candidate.origin.column === primaryKey &&
|
|
913
920
|
!candidate.nullable);
|
|
914
921
|
if (column === undefined)
|
|
915
922
|
return undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/typegen",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
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.15.
|
|
52
|
-
"@syncular/server": "0.15.
|
|
51
|
+
"@syncular/core": "0.15.3",
|
|
52
|
+
"@syncular/server": "0.15.3"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/query.ts
CHANGED
|
@@ -89,6 +89,8 @@ const DECLTYPE_MAP: Readonly<Record<string, IrColumnType>> = {
|
|
|
89
89
|
|
|
90
90
|
/** Local client protocol column; query-only and never part of schema IR. */
|
|
91
91
|
const SYNC_VERSION_COLUMN = '_sync_version';
|
|
92
|
+
/** Stable private identity shared by every declared local FTS5 projection. */
|
|
93
|
+
const FTS_SOURCE_ID_COLUMN = '_syncular_source_id';
|
|
92
94
|
|
|
93
95
|
/** A param type is one of the §2.4 types (the columns params compare to). */
|
|
94
96
|
export type QueryParamType = IrColumnType;
|
|
@@ -793,6 +795,18 @@ interface ResolvedSource {
|
|
|
793
795
|
readonly column: IrTable['columns'][number];
|
|
794
796
|
}
|
|
795
797
|
|
|
798
|
+
function declaredFtsProjection(ir: IrDocument, tableName: string): boolean {
|
|
799
|
+
return ir.tables.some((table) =>
|
|
800
|
+
table.ftsIndexes.some((index) => index.name === tableName),
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const FTS_SOURCE_ID_IR_COLUMN: IrTable['columns'][number] = {
|
|
805
|
+
name: FTS_SOURCE_ID_COLUMN,
|
|
806
|
+
type: 'string',
|
|
807
|
+
nullable: false,
|
|
808
|
+
};
|
|
809
|
+
|
|
796
810
|
/** Resolve a SELECT item's expression to an IR column, if it is a plain ref. */
|
|
797
811
|
function resolveSource(
|
|
798
812
|
item: SelectItem,
|
|
@@ -809,7 +823,14 @@ function resolveSource(
|
|
|
809
823
|
if (ref === undefined) return null;
|
|
810
824
|
const table = byName.get(ref.table);
|
|
811
825
|
const col = table?.columns.find((c) => c.name === columnName);
|
|
812
|
-
|
|
826
|
+
if (col !== undefined) return { table: ref.table, column: col };
|
|
827
|
+
if (
|
|
828
|
+
columnName === FTS_SOURCE_ID_COLUMN &&
|
|
829
|
+
declaredFtsProjection(ir, ref.table)
|
|
830
|
+
) {
|
|
831
|
+
return { table: ref.table, column: FTS_SOURCE_ID_IR_COLUMN };
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
813
834
|
}
|
|
814
835
|
// Unqualified: search every FROM/JOIN table (SQLite already resolved
|
|
815
836
|
// ambiguity; a single-table query is the common case).
|
|
@@ -817,6 +838,12 @@ function resolveSource(
|
|
|
817
838
|
const table = byName.get(ref.table);
|
|
818
839
|
const col = table?.columns.find((c) => c.name === columnName);
|
|
819
840
|
if (col !== undefined) return { table: ref.table, column: col };
|
|
841
|
+
if (
|
|
842
|
+
columnName === FTS_SOURCE_ID_COLUMN &&
|
|
843
|
+
declaredFtsProjection(ir, ref.table)
|
|
844
|
+
) {
|
|
845
|
+
return { table: ref.table, column: FTS_SOURCE_ID_IR_COLUMN };
|
|
846
|
+
}
|
|
820
847
|
}
|
|
821
848
|
return null;
|
|
822
849
|
}
|
|
@@ -1211,7 +1238,7 @@ export function synthesizeDdl(ir: IrDocument): string {
|
|
|
1211
1238
|
for (const index of table.ftsIndexes) {
|
|
1212
1239
|
const tokenize = index.tokenize.replaceAll("'", "''");
|
|
1213
1240
|
lines.push(
|
|
1214
|
-
`CREATE VIRTUAL TABLE ${index.name} USING fts5(
|
|
1241
|
+
`CREATE VIRTUAL TABLE ${index.name} USING fts5(${FTS_SOURCE_ID_COLUMN} UNINDEXED, ${index.columns.join(', ')}, tokenize='${tokenize}');`,
|
|
1215
1242
|
);
|
|
1216
1243
|
}
|
|
1217
1244
|
}
|
package/src/syql-validator.ts
CHANGED
|
@@ -1149,7 +1149,14 @@ class Validator {
|
|
|
1149
1149
|
candidates.push({ ref, scopes: inferred });
|
|
1150
1150
|
}
|
|
1151
1151
|
|
|
1152
|
-
const
|
|
1152
|
+
const ftsOwners = new Map(
|
|
1153
|
+
this.#ir.tables.flatMap((table) =>
|
|
1154
|
+
table.ftsIndexes.map((index) => [index.name, table.name] as const),
|
|
1155
|
+
),
|
|
1156
|
+
);
|
|
1157
|
+
const dependencies = [
|
|
1158
|
+
...new Set(refs.map((ref) => ftsOwners.get(ref.table) ?? ref.table)),
|
|
1159
|
+
]
|
|
1153
1160
|
.sort()
|
|
1154
1161
|
.map((table) => {
|
|
1155
1162
|
const instances = candidates.filter((item) => item.ref.table === table);
|
|
@@ -1364,11 +1371,18 @@ class Validator {
|
|
|
1364
1371
|
const inferred: string[] = [];
|
|
1365
1372
|
for (const ref of refs) {
|
|
1366
1373
|
const table = this.#ir.tables.find((item) => item.name === ref.table);
|
|
1374
|
+
const primaryKey =
|
|
1375
|
+
table?.primaryKey ??
|
|
1376
|
+
(this.#ir.tables.some((item) =>
|
|
1377
|
+
item.ftsIndexes.some((index) => index.name === ref.table),
|
|
1378
|
+
)
|
|
1379
|
+
? '_syncular_source_id'
|
|
1380
|
+
: undefined);
|
|
1367
1381
|
const column = analysis.columns.find(
|
|
1368
1382
|
(candidate) =>
|
|
1369
|
-
|
|
1370
|
-
candidate.origin?.table === table
|
|
1371
|
-
candidate.origin.column ===
|
|
1383
|
+
primaryKey !== undefined &&
|
|
1384
|
+
candidate.origin?.table === ref.table &&
|
|
1385
|
+
candidate.origin.column === primaryKey &&
|
|
1372
1386
|
!candidate.nullable,
|
|
1373
1387
|
);
|
|
1374
1388
|
if (column === undefined) return undefined;
|