@syncular/typegen 0.15.0 → 0.15.2
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 +7 -2
- package/dist/query.js +22 -2
- package/dist/syql-validator.js +26 -7
- package/package.json +3 -3
- package/src/query.ts +29 -2
- package/src/syql-validator.ts +43 -7
package/README.md
CHANGED
|
@@ -470,8 +470,13 @@ For FTS-backed named queries the synthesized type-check database creates the
|
|
|
470
470
|
same declared columns plus `_syncular_source_id UNINDEXED`. `MATCH :query`
|
|
471
471
|
infers a string parameter, and the FTS table reference is folded into its
|
|
472
472
|
owning synced table for dependency and scope-coverage metadata. `bm25`,
|
|
473
|
-
`highlight`, and `snippet`
|
|
474
|
-
|
|
473
|
+
`highlight`, and `snippet` are accepted deterministic FTS5 auxiliary
|
|
474
|
+
functions only when the query references a schema-declared FTS projection;
|
|
475
|
+
they use the normal computed-expression fidelity rules. Arbitrary extension
|
|
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.
|
|
475
480
|
|
|
476
481
|
**Typing fidelity** (what `bun:sqlite` actually exposes, and its honest
|
|
477
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
|
@@ -121,6 +121,16 @@ const SQLITE_346_PORTABLE_FUNCTIONS = new Set([
|
|
|
121
121
|
'jsonb_replace',
|
|
122
122
|
'jsonb_set',
|
|
123
123
|
]);
|
|
124
|
+
/**
|
|
125
|
+
* Deterministic FTS5 auxiliary functions available only when the query reads
|
|
126
|
+
* a schema-declared FTS projection. FTS5 is an explicit Syncular client
|
|
127
|
+
* requirement in that case, not an arbitrary host extension.
|
|
128
|
+
*/
|
|
129
|
+
const SQLITE_FTS5_AUXILIARY_FUNCTIONS = new Set([
|
|
130
|
+
'bm25',
|
|
131
|
+
'highlight',
|
|
132
|
+
'snippet',
|
|
133
|
+
]);
|
|
124
134
|
const SQL_PAREN_KEYWORDS = new Set([
|
|
125
135
|
'and',
|
|
126
136
|
'as',
|
|
@@ -248,8 +258,8 @@ class Validator {
|
|
|
248
258
|
const activeStructure = this.#inspect(activeSql, location);
|
|
249
259
|
this.#validateStatementShape(activeSql, activeStructure, logical.declaration, location);
|
|
250
260
|
this.#validateDeterminism(activeSql, logical.declaration.statement.span);
|
|
251
|
-
this.#validatePortableProfile(activeSql, logical.declaration.statement.span);
|
|
252
261
|
const refs = scanTableRefs(activeSql, this.#ir);
|
|
262
|
+
this.#validatePortableProfile(activeSql, logical.declaration.statement.span, refs);
|
|
253
263
|
const bindSymbols = this.#bindSymbols(logical.declaration);
|
|
254
264
|
const bindTypes = this.#resolveBindTypes(logical, activeSql, refs, bindSymbols);
|
|
255
265
|
const sort = this.#validateSort(logical.declaration, activeStructure, location);
|
|
@@ -512,8 +522,11 @@ class Validator {
|
|
|
512
522
|
}
|
|
513
523
|
}
|
|
514
524
|
}
|
|
515
|
-
#validatePortableProfile(sql, span) {
|
|
525
|
+
#validatePortableProfile(sql, span, refs) {
|
|
516
526
|
const tokens = significant(lexSyqlSqlSource(span.file, sql));
|
|
527
|
+
const declaredFtsTables = new Set(this.#ir.tables.flatMap((table) => table.ftsIndexes.map((index) => index.name)));
|
|
528
|
+
const queryRefs = refs ?? scanTableRefs(sql, this.#ir);
|
|
529
|
+
const readsDeclaredFts = queryRefs.some((ref) => declaredFtsTables.has(ref.table));
|
|
517
530
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
518
531
|
const token = tokens[index];
|
|
519
532
|
const lower = tokenLower(token);
|
|
@@ -537,8 +550,10 @@ class Validator {
|
|
|
537
550
|
tokens[closeIndex + 2]?.text === '(') {
|
|
538
551
|
continue;
|
|
539
552
|
}
|
|
540
|
-
|
|
541
|
-
|
|
553
|
+
const allowedFtsAuxiliary = readsDeclaredFts && SQLITE_FTS5_AUXILIARY_FUNCTIONS.has(lower ?? '');
|
|
554
|
+
if (!SQLITE_346_PORTABLE_FUNCTIONS.has(lower ?? '') &&
|
|
555
|
+
!allowedFtsAuxiliary) {
|
|
556
|
+
this.#fail('SYQL6002_INVALID_SQL', token.span, `${token.text}() is not a core built-in in the portable SQLite 3.46.0 profile or an auxiliary function of a referenced schema-declared FTS5 projection`);
|
|
542
557
|
}
|
|
543
558
|
const argumentCount = functionArgumentCount(tokens, index + 1);
|
|
544
559
|
if (lower === 'iif' && argumentCount !== 3) {
|
|
@@ -892,9 +907,13 @@ class Validator {
|
|
|
892
907
|
const inferred = [];
|
|
893
908
|
for (const ref of refs) {
|
|
894
909
|
const table = this.#ir.tables.find((item) => item.name === ref.table);
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
910
|
+
const primaryKey = table?.primaryKey ??
|
|
911
|
+
(this.#ir.tables.some((item) => item.ftsIndexes.some((index) => index.name === ref.table))
|
|
912
|
+
? '_syncular_source_id'
|
|
913
|
+
: undefined);
|
|
914
|
+
const column = analysis.columns.find((candidate) => primaryKey !== undefined &&
|
|
915
|
+
candidate.origin?.table === ref.table &&
|
|
916
|
+
candidate.origin.column === primaryKey &&
|
|
898
917
|
!candidate.nullable);
|
|
899
918
|
if (column === undefined)
|
|
900
919
|
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.2",
|
|
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.2",
|
|
52
|
+
"@syncular/server": "0.15.2"
|
|
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
|
@@ -221,6 +221,16 @@ const SQLITE_346_PORTABLE_FUNCTIONS = new Set([
|
|
|
221
221
|
'jsonb_replace',
|
|
222
222
|
'jsonb_set',
|
|
223
223
|
]);
|
|
224
|
+
/**
|
|
225
|
+
* Deterministic FTS5 auxiliary functions available only when the query reads
|
|
226
|
+
* a schema-declared FTS projection. FTS5 is an explicit Syncular client
|
|
227
|
+
* requirement in that case, not an arbitrary host extension.
|
|
228
|
+
*/
|
|
229
|
+
const SQLITE_FTS5_AUXILIARY_FUNCTIONS = new Set([
|
|
230
|
+
'bm25',
|
|
231
|
+
'highlight',
|
|
232
|
+
'snippet',
|
|
233
|
+
]);
|
|
224
234
|
const SQL_PAREN_KEYWORDS = new Set([
|
|
225
235
|
'and',
|
|
226
236
|
'as',
|
|
@@ -376,12 +386,13 @@ class Validator {
|
|
|
376
386
|
location,
|
|
377
387
|
);
|
|
378
388
|
this.#validateDeterminism(activeSql, logical.declaration.statement.span);
|
|
389
|
+
const refs = scanTableRefs(activeSql, this.#ir);
|
|
379
390
|
this.#validatePortableProfile(
|
|
380
391
|
activeSql,
|
|
381
392
|
logical.declaration.statement.span,
|
|
393
|
+
refs,
|
|
382
394
|
);
|
|
383
395
|
|
|
384
|
-
const refs = scanTableRefs(activeSql, this.#ir);
|
|
385
396
|
const bindSymbols = this.#bindSymbols(logical.declaration);
|
|
386
397
|
const bindTypes = this.#resolveBindTypes(
|
|
387
398
|
logical,
|
|
@@ -792,8 +803,21 @@ class Validator {
|
|
|
792
803
|
}
|
|
793
804
|
}
|
|
794
805
|
|
|
795
|
-
#validatePortableProfile(
|
|
806
|
+
#validatePortableProfile(
|
|
807
|
+
sql: string,
|
|
808
|
+
span: SyqlSourceSpan,
|
|
809
|
+
refs?: readonly TableRef[],
|
|
810
|
+
): void {
|
|
796
811
|
const tokens = significant(lexSyqlSqlSource(span.file, sql));
|
|
812
|
+
const declaredFtsTables = new Set(
|
|
813
|
+
this.#ir.tables.flatMap((table) =>
|
|
814
|
+
table.ftsIndexes.map((index) => index.name),
|
|
815
|
+
),
|
|
816
|
+
);
|
|
817
|
+
const queryRefs = refs ?? scanTableRefs(sql, this.#ir);
|
|
818
|
+
const readsDeclaredFts = queryRefs.some((ref) =>
|
|
819
|
+
declaredFtsTables.has(ref.table),
|
|
820
|
+
);
|
|
797
821
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
798
822
|
const token = tokens[index] as SyqlToken;
|
|
799
823
|
const lower = tokenLower(token);
|
|
@@ -828,11 +852,16 @@ class Validator {
|
|
|
828
852
|
) {
|
|
829
853
|
continue;
|
|
830
854
|
}
|
|
831
|
-
|
|
855
|
+
const allowedFtsAuxiliary =
|
|
856
|
+
readsDeclaredFts && SQLITE_FTS5_AUXILIARY_FUNCTIONS.has(lower ?? '');
|
|
857
|
+
if (
|
|
858
|
+
!SQLITE_346_PORTABLE_FUNCTIONS.has(lower ?? '') &&
|
|
859
|
+
!allowedFtsAuxiliary
|
|
860
|
+
) {
|
|
832
861
|
this.#fail(
|
|
833
862
|
'SYQL6002_INVALID_SQL',
|
|
834
863
|
token.span,
|
|
835
|
-
`${token.text}() is not a core built-in in the portable SQLite 3.46.0 profile`,
|
|
864
|
+
`${token.text}() is not a core built-in in the portable SQLite 3.46.0 profile or an auxiliary function of a referenced schema-declared FTS5 projection`,
|
|
836
865
|
);
|
|
837
866
|
}
|
|
838
867
|
|
|
@@ -1335,11 +1364,18 @@ class Validator {
|
|
|
1335
1364
|
const inferred: string[] = [];
|
|
1336
1365
|
for (const ref of refs) {
|
|
1337
1366
|
const table = this.#ir.tables.find((item) => item.name === ref.table);
|
|
1367
|
+
const primaryKey =
|
|
1368
|
+
table?.primaryKey ??
|
|
1369
|
+
(this.#ir.tables.some((item) =>
|
|
1370
|
+
item.ftsIndexes.some((index) => index.name === ref.table),
|
|
1371
|
+
)
|
|
1372
|
+
? '_syncular_source_id'
|
|
1373
|
+
: undefined);
|
|
1338
1374
|
const column = analysis.columns.find(
|
|
1339
1375
|
(candidate) =>
|
|
1340
|
-
|
|
1341
|
-
candidate.origin?.table === table
|
|
1342
|
-
candidate.origin.column ===
|
|
1376
|
+
primaryKey !== undefined &&
|
|
1377
|
+
candidate.origin?.table === ref.table &&
|
|
1378
|
+
candidate.origin.column === primaryKey &&
|
|
1343
1379
|
!candidate.nullable,
|
|
1344
1380
|
);
|
|
1345
1381
|
if (column === undefined) return undefined;
|