@syncular/typegen 0.2.1 → 0.3.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 +25 -10
- package/dist/cli.js +155 -4
- package/dist/emit-dart.js +3 -2
- package/dist/emit-kotlin.js +3 -2
- package/dist/emit-queries-dart.js +87 -15
- package/dist/emit-queries-kotlin.js +89 -15
- package/dist/emit-queries-swift.js +92 -14
- package/dist/emit-queries.js +131 -22
- package/dist/emit-swift.js +3 -2
- package/dist/emit.d.ts +2 -1
- package/dist/emit.js +13 -27
- package/dist/fmt.d.ts +3 -0
- package/dist/fmt.js +356 -0
- package/dist/generate.d.ts +16 -6
- package/dist/generate.js +34 -9
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lower.d.ts +54 -0
- package/dist/lower.js +283 -0
- package/dist/lsp.d.ts +20 -0
- package/dist/lsp.js +376 -0
- package/dist/manifest.d.ts +11 -0
- package/dist/manifest.js +20 -0
- package/dist/naming.d.ts +22 -0
- package/dist/naming.js +240 -0
- package/dist/query-ir.d.ts +14 -0
- package/dist/query-ir.js +69 -0
- package/dist/query.d.ts +102 -6
- package/dist/query.js +126 -34
- package/dist/syql.d.ts +83 -0
- package/dist/syql.js +945 -0
- package/package.json +2 -2
- package/src/cli.ts +155 -4
- package/src/emit-dart.ts +3 -2
- package/src/emit-kotlin.ts +3 -2
- package/src/emit-queries-dart.ts +109 -16
- package/src/emit-queries-kotlin.ts +111 -18
- package/src/emit-queries-swift.ts +106 -17
- package/src/emit-queries.ts +179 -28
- package/src/emit-swift.ts +3 -2
- package/src/emit.ts +18 -28
- package/src/fmt.ts +351 -0
- package/src/generate.ts +54 -11
- package/src/index.ts +6 -0
- package/src/lower.ts +331 -0
- package/src/lsp.ts +445 -0
- package/src/manifest.ts +38 -0
- package/src/naming.ts +271 -0
- package/src/query-ir.ts +82 -0
- package/src/query.ts +241 -34
- package/src/syql.ts +1171 -0
package/dist/query.js
CHANGED
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
* error listing the fix. bun:sqlite can't name params, so we parse `:name`
|
|
49
49
|
* tokens from the SQL ourselves (first-occurrence order = positional order).
|
|
50
50
|
*
|
|
51
|
-
* ## The tables set (for
|
|
51
|
+
* ## The tables set (for useRawSql `{tables}` — exact invalidation)
|
|
52
52
|
*
|
|
53
53
|
* bun:sqlite exposes no authorizer and no statement table-list; EXPLAIN
|
|
54
54
|
* opcodes are fragile. The honest mechanism: the FROM/JOIN table set we
|
|
@@ -61,6 +61,8 @@
|
|
|
61
61
|
* for the query shapes this tier supports.
|
|
62
62
|
*/
|
|
63
63
|
import { TypegenError } from './errors.js';
|
|
64
|
+
import { lowerProjection, mainVerbAfterWith } from './lower.js';
|
|
65
|
+
import { buildNamingMap } from './naming.js';
|
|
64
66
|
/** The SQL decltype keyword → §2.4 type map (mirrors sql.ts TYPE_MAP so a
|
|
65
67
|
* plain column ref's decltype resolves to the exact IR type). */
|
|
66
68
|
const DECLTYPE_MAP = {
|
|
@@ -82,6 +84,7 @@ const DECLTYPE_MAP = {
|
|
|
82
84
|
BLOBREF: 'blob_ref',
|
|
83
85
|
CRDT: 'crdt',
|
|
84
86
|
};
|
|
87
|
+
const DEFAULT_NAMING = { naming: 'camel', targets: ['ts'] };
|
|
85
88
|
// -- path → name --------------------------------------------------------------
|
|
86
89
|
/** A single kebab-case path segment (folder) or filename stem. */
|
|
87
90
|
const KEBAB_SEGMENT_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
@@ -328,11 +331,17 @@ function scanParamNames(sql) {
|
|
|
328
331
|
}
|
|
329
332
|
return seen;
|
|
330
333
|
}
|
|
331
|
-
/** Rewrite `:name` → positional
|
|
332
|
-
*
|
|
334
|
+
/** Rewrite `:name` → positional placeholders AND strip comments + collapse
|
|
335
|
+
* whitespace, producing a clean single-line SQL string suitable for
|
|
333
336
|
* embedding in a generated string literal. String literals are preserved
|
|
334
|
-
* verbatim (their inner whitespace is not collapsed).
|
|
335
|
-
|
|
337
|
+
* verbatim (their inner whitespace is not collapsed).
|
|
338
|
+
*
|
|
339
|
+
* When every param occurs exactly once, plain `?` is emitted. When ANY
|
|
340
|
+
* param repeats (the §7 neutralization guards mention a param in the guard
|
|
341
|
+
* AND the predicate), the WHOLE statement uses SQLite's numbered `?N`
|
|
342
|
+
* form — one bound value per DISTINCT param, reuse resolved by SQLite —
|
|
343
|
+
* so the generated bind array stays one-entry-per-param. */
|
|
344
|
+
export function toPositionalSql(sql) {
|
|
336
345
|
const tokens = [];
|
|
337
346
|
let i = 0;
|
|
338
347
|
while (i < sql.length) {
|
|
@@ -379,7 +388,7 @@ function toPositionalSql(sql) {
|
|
|
379
388
|
while (end < sql.length && /[A-Za-z0-9_]/.test(sql[end])) {
|
|
380
389
|
end += 1;
|
|
381
390
|
}
|
|
382
|
-
tokens.push(
|
|
391
|
+
tokens.push({ param: sql.slice(i + 1, end) });
|
|
383
392
|
i = end;
|
|
384
393
|
}
|
|
385
394
|
else if (/\s/.test(ch)) {
|
|
@@ -391,8 +400,25 @@ function toPositionalSql(sql) {
|
|
|
391
400
|
i += 1;
|
|
392
401
|
}
|
|
393
402
|
}
|
|
403
|
+
const order = [];
|
|
404
|
+
let repeats = false;
|
|
405
|
+
for (const token of tokens) {
|
|
406
|
+
if (typeof token === 'string')
|
|
407
|
+
continue;
|
|
408
|
+
if (order.includes(token.param))
|
|
409
|
+
repeats = true;
|
|
410
|
+
else
|
|
411
|
+
order.push(token.param);
|
|
412
|
+
}
|
|
413
|
+
const rendered = tokens
|
|
414
|
+
.map((token) => {
|
|
415
|
+
if (typeof token === 'string')
|
|
416
|
+
return token;
|
|
417
|
+
return repeats ? `?${order.indexOf(token.param) + 1}` : '?';
|
|
418
|
+
})
|
|
419
|
+
.join('');
|
|
394
420
|
// Collapse runs of the whitespace placeholders; trim.
|
|
395
|
-
return
|
|
421
|
+
return rendered.replace(/\s+/g, ' ').trim();
|
|
396
422
|
}
|
|
397
423
|
const IDENT = '[A-Za-z_][A-Za-z0-9_]*';
|
|
398
424
|
// `FROM tbl [AS] a`, `JOIN tbl [AS] a`. We only need the referenced base
|
|
@@ -539,6 +565,28 @@ function inferParamType(paramName, sql, refs, ir) {
|
|
|
539
565
|
if (t !== null)
|
|
540
566
|
return t;
|
|
541
567
|
}
|
|
568
|
+
// `col BETWEEN :name AND …` / `col BETWEEN … AND :name` — both endpoints
|
|
569
|
+
// take the column's type (the §4 from+to group shape).
|
|
570
|
+
const btwLeft = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+:${paramName}\\b`, 'i');
|
|
571
|
+
const btwLeftM = btwLeft.exec(cleaned);
|
|
572
|
+
if (btwLeftM !== null) {
|
|
573
|
+
const t = lookup(btwLeftM[1], btwLeftM[2]);
|
|
574
|
+
if (t !== null)
|
|
575
|
+
return t;
|
|
576
|
+
}
|
|
577
|
+
const btwRight = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+\\S+\\s+AND\\s+:${paramName}\\b`, 'i');
|
|
578
|
+
const btwRightM = btwRight.exec(cleaned);
|
|
579
|
+
if (btwRightM !== null) {
|
|
580
|
+
const t = lookup(btwRightM[1], btwRightM[2]);
|
|
581
|
+
if (t !== null)
|
|
582
|
+
return t;
|
|
583
|
+
}
|
|
584
|
+
// `… LIKE <expr mentioning :name>` — a LIKE operand is TEXT, even inside
|
|
585
|
+
// a concatenation (`title like '%' || :q || '%'`, the search-fragment
|
|
586
|
+
// shape).
|
|
587
|
+
const like = new RegExp(`\\bLIKE\\b[^()]*:${paramName}\\b`, 'i');
|
|
588
|
+
if (like.test(cleaned))
|
|
589
|
+
return 'string';
|
|
542
590
|
return null;
|
|
543
591
|
}
|
|
544
592
|
// -- fallback column typing (computed expressions) ----------------------------
|
|
@@ -591,45 +639,79 @@ export function synthesizeDdl(ir) {
|
|
|
591
639
|
* top-level `;`). This is the per-statement core; {@link analyzeQueryFile}
|
|
592
640
|
* splits a file and drives it.
|
|
593
641
|
*/
|
|
594
|
-
export function analyzeStatement(name, location, statementText, ir, db) {
|
|
642
|
+
export function analyzeStatement(name, location, statementText, ir, db, naming = DEFAULT_NAMING) {
|
|
595
643
|
const file = location;
|
|
596
644
|
const commentParams = parseCommentParams(file, statementText);
|
|
597
|
-
const
|
|
598
|
-
if (
|
|
645
|
+
const sourceSql = statementText.trim();
|
|
646
|
+
if (sourceSql.length === 0) {
|
|
599
647
|
throw new TypegenError(file, 'query file is empty');
|
|
600
648
|
}
|
|
601
|
-
// SELECT-only (the read tier).
|
|
649
|
+
// SELECT-only (the read tier). A `WITH` is allowed when its main statement
|
|
650
|
+
// is a SELECT (SQLite also allows WITH … INSERT/UPDATE/DELETE — writes).
|
|
602
651
|
const firstKeyword = /^\s*([A-Za-z]+)/.exec(stripCommentsAndStrings(statementText).trimStart())?.[1];
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
try {
|
|
610
|
-
described = db.analyze(sql);
|
|
652
|
+
const upperKeyword = firstKeyword?.toUpperCase();
|
|
653
|
+
if (upperKeyword === 'WITH') {
|
|
654
|
+
const main = mainVerbAfterWith(stripCommentsAndStrings(statementText));
|
|
655
|
+
if (main !== 'SELECT') {
|
|
656
|
+
throw new TypegenError(file, `named queries are SELECT-only (the read tier); this WITH statement's main verb is ${JSON.stringify(main ?? '')}. Writes go through mutate() (SPEC §7.1).`);
|
|
657
|
+
}
|
|
611
658
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
throw new TypegenError(file, `SQL rejected by SQLite: ${message}`);
|
|
659
|
+
else if (upperKeyword !== 'SELECT') {
|
|
660
|
+
throw new TypegenError(file, `named queries are SELECT-only (the read tier); found ${JSON.stringify(firstKeyword ?? '')}. Writes go through mutate() (SPEC §7.1).`);
|
|
615
661
|
}
|
|
616
|
-
|
|
662
|
+
// Let SQLite validate + describe the AUTHORED query first. Any bad
|
|
663
|
+
// reference throws here with SQLite's own message.
|
|
664
|
+
const analyze = (candidate) => {
|
|
665
|
+
try {
|
|
666
|
+
return db.analyze(candidate);
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
670
|
+
throw new TypegenError(file, `SQL rejected by SQLite: ${message}`);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
let described = analyze(sourceSql);
|
|
674
|
+
const refs = scanTableRefs(sourceSql, ir);
|
|
617
675
|
const tableSet = new Set(refs.map((r) => r.table));
|
|
618
676
|
const tables = [...tableSet].sort();
|
|
619
677
|
if (tables.length === 0) {
|
|
620
678
|
throw new TypegenError(file, 'query reads no synced table — every named query must read at least one IR table (for exact invalidation)');
|
|
621
679
|
}
|
|
622
|
-
//
|
|
680
|
+
// §5 lowering: rewrite the projection so runtime result keys are the
|
|
681
|
+
// language-facing names ("camel"); a no-op pass-through under "preserve".
|
|
682
|
+
let sql = sourceSql;
|
|
683
|
+
let sqlNames = described.columnNames;
|
|
684
|
+
let langNames = described.columnNames;
|
|
685
|
+
if (naming.naming === 'camel') {
|
|
686
|
+
const lowered = lowerProjection(sourceSql, refs, ir, file, (candidate) => analyze(candidate).columnNames, (names) => buildNamingMap(names, 'camel', file, 'projection', naming.targets).map((m) => m.langName));
|
|
687
|
+
sqlNames = lowered.sqlNames;
|
|
688
|
+
langNames = lowered.langNames;
|
|
689
|
+
if (lowered.changed) {
|
|
690
|
+
sql = lowered.sql;
|
|
691
|
+
described = analyze(sql);
|
|
692
|
+
// Defensive: the rewritten projection's runtime keys must BE the
|
|
693
|
+
// language names — this is the whole point of the lowering.
|
|
694
|
+
if (described.columnNames.length !== langNames.length ||
|
|
695
|
+
described.columnNames.some((n, i) => n !== langNames[i])) {
|
|
696
|
+
throw new TypegenError(file, `internal: lowered projection keys [${described.columnNames.join(', ')}] do not match the naming map [${langNames.join(', ')}]`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// Columns: pair bun's column names + decltypes with our source resolution
|
|
701
|
+
// (against the LOWERED sql — aliased plain refs still resolve exactly).
|
|
623
702
|
const items = splitSelectList(sql);
|
|
624
703
|
const columns = described.columnNames.map((colName, index) => {
|
|
625
704
|
const decl = described.declaredTypes[index];
|
|
626
705
|
const item = items?.[index];
|
|
627
706
|
const source = item !== undefined ? resolveSource(item, refs, ir) : null;
|
|
707
|
+
const sqlName = sqlNames[index] ?? colName;
|
|
708
|
+
const langName = langNames[index] ?? colName;
|
|
628
709
|
if (source !== null) {
|
|
629
710
|
// Exact: IR column type + IR nullability. (decltype agrees; we prefer
|
|
630
711
|
// the IR type so blob_ref/crdt/json semantic types survive.)
|
|
631
712
|
return {
|
|
632
|
-
name:
|
|
713
|
+
name: sqlName,
|
|
714
|
+
langName,
|
|
633
715
|
type: source.column.type,
|
|
634
716
|
nullable: source.column.nullable,
|
|
635
717
|
fidelity: 'exact',
|
|
@@ -640,24 +722,33 @@ export function analyzeStatement(name, location, statementText, ir, db) {
|
|
|
640
722
|
? DECLTYPE_MAP[decl.toUpperCase()]
|
|
641
723
|
: undefined;
|
|
642
724
|
const type = mapped ?? (item !== undefined ? fallbackColumnType(item.expr) : 'string');
|
|
643
|
-
return {
|
|
725
|
+
return {
|
|
726
|
+
name: sqlName,
|
|
727
|
+
langName,
|
|
728
|
+
type,
|
|
729
|
+
nullable: true,
|
|
730
|
+
fidelity: 'fallback',
|
|
731
|
+
};
|
|
644
732
|
});
|
|
645
733
|
// Params: names from the text (positional order), types from inference or
|
|
646
|
-
// the `-- param` comment; every param must resolve.
|
|
734
|
+
// the `-- param` comment; every param must resolve. Param langNames go
|
|
735
|
+
// through the same §12 map (authored camelCase params are identity).
|
|
647
736
|
const paramNames = scanParamNames(sql);
|
|
648
737
|
if (paramNames.length !== described.paramsCount) {
|
|
649
738
|
// Defensive: our scan and SQLite disagree (shouldn't happen for the subset).
|
|
650
739
|
throw new TypegenError(file, `internal: scanned ${paramNames.length} params (${paramNames.join(', ')}) but SQLite reports ${described.paramsCount}`);
|
|
651
740
|
}
|
|
741
|
+
const paramLangNames = buildNamingMap(paramNames, naming.naming, file, 'params', naming.targets);
|
|
652
742
|
const commentByName = new Map(commentParams.map((p) => [p.name, p.type]));
|
|
653
|
-
const params = paramNames.map((paramName) => {
|
|
743
|
+
const params = paramNames.map((paramName, index) => {
|
|
744
|
+
const langName = paramLangNames[index]?.langName ?? paramName;
|
|
654
745
|
const commented = commentByName.get(paramName);
|
|
655
746
|
if (commented !== undefined) {
|
|
656
|
-
return { name: paramName, type: commented, source: 'comment' };
|
|
747
|
+
return { name: paramName, langName, type: commented, source: 'comment' };
|
|
657
748
|
}
|
|
658
749
|
const inferred = inferParamType(paramName, sql, refs, ir);
|
|
659
750
|
if (inferred !== null) {
|
|
660
|
-
return { name: paramName, type: inferred, source: 'inferred' };
|
|
751
|
+
return { name: paramName, langName, type: inferred, source: 'inferred' };
|
|
661
752
|
}
|
|
662
753
|
throw new TypegenError(file, `cannot infer a type for param :${paramName} (it is not compared to a plain column). Add a header comment: \`-- param :${paramName} <type>\` (one of ${[...PARAM_TYPES].join(', ')}).`);
|
|
663
754
|
});
|
|
@@ -670,6 +761,7 @@ export function analyzeStatement(name, location, statementText, ir, db) {
|
|
|
670
761
|
return {
|
|
671
762
|
name,
|
|
672
763
|
file,
|
|
764
|
+
sourceSql,
|
|
673
765
|
sql,
|
|
674
766
|
positionalSql: toPositionalSql(sql),
|
|
675
767
|
params,
|
|
@@ -688,7 +780,7 @@ export function analyzeStatement(name, location, statementText, ir, db) {
|
|
|
688
780
|
* - A file with MULTIPLE statements requires `-- name:` on EVERY statement; a
|
|
689
781
|
* missing one errors, naming the file + the statement's position/first line.
|
|
690
782
|
*/
|
|
691
|
-
export function analyzeQueryFile(relPath, content, ir, db) {
|
|
783
|
+
export function analyzeQueryFile(relPath, content, ir, db, naming = DEFAULT_NAMING) {
|
|
692
784
|
const statements = splitStatements(content);
|
|
693
785
|
if (statements.length === 0) {
|
|
694
786
|
throw new TypegenError(relPath, 'query file is empty');
|
|
@@ -703,17 +795,17 @@ export function analyzeQueryFile(relPath, content, ir, db) {
|
|
|
703
795
|
throw new TypegenError(location, `${relPath} holds ${statements.length} statements, so each requires a \`-- name: <camelCase>\` marker; statement #${index + 1} (line ${stmt.startLine}: ${JSON.stringify(firstLine.slice(0, 60))}) has none`);
|
|
704
796
|
}
|
|
705
797
|
const name = override ?? defaultName;
|
|
706
|
-
return analyzeStatement(name, location, stmt.text, ir, db);
|
|
798
|
+
return analyzeStatement(name, location, stmt.text, ir, db, naming);
|
|
707
799
|
});
|
|
708
800
|
}
|
|
709
801
|
/** Back-compat single-statement analyzer: resolve the name from the file path
|
|
710
802
|
* (or a `-- name:` override) and analyze. Rejects a `;`-separated file loudly
|
|
711
803
|
* — use {@link analyzeQueryFile} for the multi-statement contract. */
|
|
712
|
-
export function analyzeQuery(file, raw, ir, db) {
|
|
804
|
+
export function analyzeQuery(file, raw, ir, db, naming = DEFAULT_NAMING) {
|
|
713
805
|
const statements = splitStatements(raw);
|
|
714
806
|
if (statements.length > 1) {
|
|
715
807
|
throw new TypegenError(file, 'a query file holds exactly one SELECT statement here (found a `;` separating statements) — use analyzeQueryFile for multi-statement files');
|
|
716
808
|
}
|
|
717
|
-
const results = analyzeQueryFile(file, raw, ir, db);
|
|
809
|
+
const results = analyzeQueryFile(file, raw, ir, db, naming);
|
|
718
810
|
return results[0];
|
|
719
811
|
}
|
package/dist/syql.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { IrDocument } from './ir.js';
|
|
2
|
+
import { type AnalyzedQuery, type QueryDb, type QueryNamingOptions } from './query.js';
|
|
3
|
+
export interface SyqlParamDecl {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly optional: boolean;
|
|
6
|
+
/** `from+to?` pairing: both params share the group key. */
|
|
7
|
+
readonly group?: string;
|
|
8
|
+
/** `name?: flag` — a boolean guard param (§3). */
|
|
9
|
+
readonly flag: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface SyqlOrderBy {
|
|
12
|
+
readonly allowed: readonly string[];
|
|
13
|
+
readonly defaultColumn: string;
|
|
14
|
+
readonly defaultDir: 'asc' | 'desc';
|
|
15
|
+
}
|
|
16
|
+
export interface SyqlLimit {
|
|
17
|
+
readonly max?: number;
|
|
18
|
+
readonly default?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface SyqlFragmentDecl {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly params: readonly SyqlParamDecl[];
|
|
23
|
+
/** The predicate body (raw SQL expression text). */
|
|
24
|
+
readonly body: string;
|
|
25
|
+
/** Source offset of the `fragment` keyword (editor tooling). */
|
|
26
|
+
readonly offset: number;
|
|
27
|
+
}
|
|
28
|
+
export interface SyqlQueryDecl {
|
|
29
|
+
readonly name: string;
|
|
30
|
+
readonly params: readonly SyqlParamDecl[];
|
|
31
|
+
readonly orderBy?: SyqlOrderBy;
|
|
32
|
+
readonly limit?: SyqlLimit;
|
|
33
|
+
/** §7 opt-in: lower to 2^N enumerated statements (perfect index use)
|
|
34
|
+
* instead of one neutralized statement. API-invisible. */
|
|
35
|
+
readonly variants?: boolean;
|
|
36
|
+
/** The SQL-shaped body (may contain @fragment refs and if-guards). */
|
|
37
|
+
readonly body: string;
|
|
38
|
+
/** Source offset of the `query` keyword (editor tooling). */
|
|
39
|
+
readonly offset: number;
|
|
40
|
+
}
|
|
41
|
+
export interface SyqlFile {
|
|
42
|
+
readonly fragments: readonly SyqlFragmentDecl[];
|
|
43
|
+
readonly queries: readonly SyqlQueryDecl[];
|
|
44
|
+
}
|
|
45
|
+
/** Parse one `.syql` file into its declarations (no lowering yet). */
|
|
46
|
+
export declare function parseSyqlFile(file: string, content: string): SyqlFile;
|
|
47
|
+
interface ParamInfo {
|
|
48
|
+
optional: boolean;
|
|
49
|
+
group?: string;
|
|
50
|
+
flag: boolean;
|
|
51
|
+
declared: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** One top-level WHERE conjunct after guard analysis: `governing` is the
|
|
54
|
+
* set of OPTIONAL param names gating it (empty = required conjunct). */
|
|
55
|
+
export interface LoweredConjunct {
|
|
56
|
+
readonly pred: string;
|
|
57
|
+
readonly governing: readonly string[];
|
|
58
|
+
}
|
|
59
|
+
export interface LoweredSyqlQuery {
|
|
60
|
+
/** The lowered single SQL statement (named params, no knob tails). */
|
|
61
|
+
readonly sql: string;
|
|
62
|
+
/** Params discovered/declared, in signature-then-first-use order. */
|
|
63
|
+
readonly paramInfo: ReadonlyMap<string, ParamInfo>;
|
|
64
|
+
/** Statement text before the `where` keyword (whole statement when there
|
|
65
|
+
* is no WHERE). */
|
|
66
|
+
readonly prefix: string;
|
|
67
|
+
/** Statement text after the WHERE clause (may be empty). */
|
|
68
|
+
readonly suffix: string;
|
|
69
|
+
/** The structured conjuncts (empty when there is no WHERE) — the §7
|
|
70
|
+
* variant backend re-assembles per provided-combination from these. */
|
|
71
|
+
readonly conjuncts: readonly LoweredConjunct[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Lower one query body: splice fragments, auto-guard optional conjuncts,
|
|
75
|
+
* expand `if` guards, enforce B1. Returns plain SQL (knob tails are the
|
|
76
|
+
* caller's job).
|
|
77
|
+
*/
|
|
78
|
+
export declare function lowerSyqlBody(decl: SyqlQueryDecl, fragments: ReadonlyMap<string, SyqlFragmentDecl>, location: string): LoweredSyqlQuery;
|
|
79
|
+
/** Analyze every query of one `.syql` file into `AnalyzedQuery` units —
|
|
80
|
+
* byte-compatible with the `.sql` frontend's output (§1: nothing below the
|
|
81
|
+
* IR knows the frontend). */
|
|
82
|
+
export declare function analyzeSyqlFile(relPath: string, content: string, ir: IrDocument, db: QueryDb, naming?: QueryNamingOptions): AnalyzedQuery[];
|
|
83
|
+
export {};
|