@syncular/typegen 0.5.0 → 0.6.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 +75 -33
- package/dist/cli.js +43 -17
- package/dist/emit-queries-dart.js +167 -55
- package/dist/emit-queries-kotlin.js +166 -55
- package/dist/emit-queries-swift.js +182 -57
- package/dist/emit-queries.js +289 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +323 -316
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +28 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lsp.d.ts +1 -3
- package/dist/lsp.js +359 -185
- package/dist/manifest.d.ts +1 -3
- package/dist/manifest.js +1 -1
- package/dist/query-ir.js +53 -42
- package/dist/query.d.ts +115 -63
- package/dist/query.js +60 -11
- package/dist/syql-lexer.d.ts +2 -0
- package/dist/syql-lexer.js +9 -2
- package/dist/syql-lowering.d.ts +22 -0
- package/dist/syql-lowering.js +411 -0
- package/dist/syql-semantics.d.ts +76 -0
- package/dist/syql-semantics.js +551 -0
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +966 -0
- package/package.json +3 -3
- package/src/cli.ts +51 -21
- package/src/emit-queries-dart.ts +195 -69
- package/src/emit-queries-kotlin.ts +197 -69
- package/src/emit-queries-swift.ts +224 -68
- package/src/emit-queries.ts +413 -165
- package/src/fmt.ts +377 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +425 -215
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +199 -79
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +638 -0
- package/src/syql-semantics.ts +859 -0
- package/src/syql-validator.ts +1492 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- package/src/syql.ts +0 -1470
package/dist/manifest.d.ts
CHANGED
|
@@ -77,9 +77,7 @@ export interface Manifest {
|
|
|
77
77
|
* generated row types/params (queries lower with `AS` aliases); "preserve"
|
|
78
78
|
* keeps SQL-truth names everywhere. */
|
|
79
79
|
readonly naming: NamingMode;
|
|
80
|
-
/** §7 `.syql` backend: "
|
|
81
|
-
* whenever eligible), or "auto" (enumerate at ≤ 2 optional groups). The
|
|
82
|
-
* per-query `variants` knob always forces enumeration. */
|
|
80
|
+
/** §7 `.syql` backend: "auto" (default), "neutralize", or "variants". */
|
|
83
81
|
readonly queryBackend: ManifestQueryBackend;
|
|
84
82
|
readonly output: ManifestOutput;
|
|
85
83
|
readonly schemaVersions: readonly ManifestSchemaVersion[];
|
package/dist/manifest.js
CHANGED
|
@@ -232,7 +232,7 @@ export function parseManifest(raw) {
|
|
|
232
232
|
}
|
|
233
233
|
naming = obj.naming;
|
|
234
234
|
}
|
|
235
|
-
let queryBackend = '
|
|
235
|
+
let queryBackend = 'auto';
|
|
236
236
|
if (obj.queryBackend !== undefined) {
|
|
237
237
|
if (obj.queryBackend !== 'neutralize' &&
|
|
238
238
|
obj.queryBackend !== 'variants' &&
|
package/dist/query-ir.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Serialize analyzed queries as the deterministic QueryIR JSON document. */
|
|
2
2
|
export function serializeQueryIr(queries) {
|
|
3
3
|
const doc = {
|
|
4
|
-
queryIrVersion:
|
|
4
|
+
queryIrVersion: 3,
|
|
5
5
|
queries: queries.map((query) => ({
|
|
6
6
|
name: query.name,
|
|
7
7
|
file: query.file,
|
|
@@ -12,11 +12,6 @@ export function serializeQueryIr(queries) {
|
|
|
12
12
|
name: param.name,
|
|
13
13
|
langName: param.langName,
|
|
14
14
|
type: param.type,
|
|
15
|
-
// §4 metadata — emitted only when set, so `.sql`-tier IR bytes are
|
|
16
|
-
// unchanged from before the DSL existed.
|
|
17
|
-
...(param.optional === true ? { optional: true } : {}),
|
|
18
|
-
...(param.group !== undefined ? { group: param.group } : {}),
|
|
19
|
-
...(param.flag === true ? { flag: true } : {}),
|
|
20
15
|
})),
|
|
21
16
|
columns: query.columns.map((column) => ({
|
|
22
17
|
name: column.name,
|
|
@@ -49,43 +44,59 @@ export function serializeQueryIr(queries) {
|
|
|
49
44
|
? { rowKey: query.reactive.rowKey }
|
|
50
45
|
: {}),
|
|
51
46
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
...(query.syql === undefined
|
|
48
|
+
? {}
|
|
49
|
+
: {
|
|
50
|
+
syql: {
|
|
51
|
+
revision: query.syql.revision,
|
|
52
|
+
inputs: query.syql.inputs.map((input) => {
|
|
53
|
+
if (input.kind === 'value')
|
|
54
|
+
return { ...input };
|
|
55
|
+
if (input.kind === 'group') {
|
|
56
|
+
return {
|
|
57
|
+
kind: input.kind,
|
|
58
|
+
name: input.name,
|
|
59
|
+
langName: input.langName,
|
|
60
|
+
members: input.members.map((member) => ({ ...member })),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (input.kind === 'sort') {
|
|
64
|
+
return {
|
|
65
|
+
kind: input.kind,
|
|
66
|
+
name: input.name,
|
|
67
|
+
langName: input.langName,
|
|
68
|
+
defaultProfile: input.defaultProfile,
|
|
69
|
+
profiles: input.profiles.map((profile) => ({ ...profile })),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return { ...input };
|
|
73
|
+
}),
|
|
74
|
+
plan: {
|
|
75
|
+
backend: query.syql.plan.backend,
|
|
76
|
+
activationControls: query.syql.plan.activationControls,
|
|
77
|
+
conditions: query.syql.plan.conditions.map((condition) => ({
|
|
78
|
+
controls: condition.controls,
|
|
79
|
+
...(condition.bind === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { bind: condition.bind }),
|
|
82
|
+
})),
|
|
83
|
+
statements: query.syql.plan.statements.map((statement) => ({
|
|
84
|
+
...(statement.sortProfile === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: { sortProfile: statement.sortProfile }),
|
|
87
|
+
...(statement.activationMask === undefined
|
|
88
|
+
? {}
|
|
89
|
+
: { activationMask: statement.activationMask }),
|
|
90
|
+
sql: statement.sql,
|
|
91
|
+
positionalSql: statement.positionalSql,
|
|
92
|
+
binds: statement.binds.map((bind) => ({ ...bind })),
|
|
93
|
+
})),
|
|
94
|
+
},
|
|
95
|
+
...(query.syql.identity === undefined
|
|
96
|
+
? {}
|
|
97
|
+
: { identity: query.syql.identity }),
|
|
62
98
|
},
|
|
63
|
-
}
|
|
64
|
-
: {}),
|
|
65
|
-
...(query.limit !== undefined ? { limit: query.limit } : {}),
|
|
66
|
-
...(query.positionalSqlBase !== undefined
|
|
67
|
-
? { positionalSqlBase: query.positionalSqlBase }
|
|
68
|
-
: {}),
|
|
69
|
-
// §7 variant backend — emitted only when the query opts in.
|
|
70
|
-
...(query.variantGroups !== undefined
|
|
71
|
-
? {
|
|
72
|
-
variantGroups: query.variantGroups.map((g) => ({
|
|
73
|
-
key: g.key,
|
|
74
|
-
params: g.params,
|
|
75
|
-
flag: g.flag,
|
|
76
|
-
})),
|
|
77
|
-
}
|
|
78
|
-
: {}),
|
|
79
|
-
...(query.variants !== undefined
|
|
80
|
-
? {
|
|
81
|
-
variants: query.variants.map((v) => ({
|
|
82
|
-
when: v.when,
|
|
83
|
-
sql: v.sql,
|
|
84
|
-
positionalSql: v.positionalSql,
|
|
85
|
-
params: v.params,
|
|
86
|
-
})),
|
|
87
|
-
}
|
|
88
|
-
: {}),
|
|
99
|
+
}),
|
|
89
100
|
})),
|
|
90
101
|
};
|
|
91
102
|
return `${JSON.stringify(doc, null, 2)}\n`;
|
package/dist/query.d.ts
CHANGED
|
@@ -10,31 +10,6 @@ export interface QueryParam {
|
|
|
10
10
|
readonly type: QueryParamType;
|
|
11
11
|
/** How the type was resolved — for the docs/tests, not emitted. */
|
|
12
12
|
readonly source: 'inferred' | 'comment';
|
|
13
|
-
/** §4 (DESIGN-queries.md): an optional param — its auto-guarded conjunct
|
|
14
|
-
* applies only when it is provided. Always false for the `.sql` tier. */
|
|
15
|
-
readonly optional?: boolean;
|
|
16
|
-
/** §4: the `from+to` pairing — params sharing a group apply together. */
|
|
17
|
-
readonly group?: string;
|
|
18
|
-
/** §3: a `: flag` boolean guard param (never in a predicate as written;
|
|
19
|
-
* the lowering binds it). Implies optional. */
|
|
20
|
-
readonly flag?: boolean;
|
|
21
|
-
}
|
|
22
|
-
/** §6 orderBy knob: a generate-time allowlist (identifiers cannot bind). */
|
|
23
|
-
export interface QueryOrderBy {
|
|
24
|
-
/** Allowed columns: authored SQL name + the language-facing key the
|
|
25
|
-
* generated signature accepts. Each is SQLite-checked at generate time. */
|
|
26
|
-
readonly allowed: readonly {
|
|
27
|
-
name: string;
|
|
28
|
-
langName: string;
|
|
29
|
-
}[];
|
|
30
|
-
/** Default column (authored SQL name). */
|
|
31
|
-
readonly defaultColumn: string;
|
|
32
|
-
readonly defaultDir: 'asc' | 'desc';
|
|
33
|
-
}
|
|
34
|
-
/** §6 limit knob: a bound value with a codegen clamp + default. */
|
|
35
|
-
export interface QueryLimit {
|
|
36
|
-
readonly max?: number;
|
|
37
|
-
readonly default?: number;
|
|
38
13
|
}
|
|
39
14
|
export interface QueryColumn {
|
|
40
15
|
/** SQL-truth result name (pre-lowering, as authored). */
|
|
@@ -77,7 +52,7 @@ export interface QueryReactiveMetadata {
|
|
|
77
52
|
/** Language-facing projection fields forming a proven unique row key. */
|
|
78
53
|
readonly rowKey?: readonly string[];
|
|
79
54
|
}
|
|
80
|
-
/** §7/§8 backend selection: neutralization
|
|
55
|
+
/** §7/§8 backend selection: neutralization, always-variants, or
|
|
81
56
|
* the small-N heuristic (`auto`: enumerate at ≤ 2 optional groups). */
|
|
82
57
|
export type QueryBackend = 'neutralize' | 'variants' | 'auto';
|
|
83
58
|
/** Naming/backend options threaded from the manifest into query analysis. */
|
|
@@ -86,9 +61,103 @@ export interface QueryNamingOptions {
|
|
|
86
61
|
/** Emitter targets this run generates — keyword hazards are only real on
|
|
87
62
|
* targets that exist. */
|
|
88
63
|
readonly targets: readonly NamingTarget[];
|
|
89
|
-
/** `.syql` conditional-lowering
|
|
90
|
-
* per-query `variants` knob always forces enumeration. */
|
|
64
|
+
/** `.syql` conditional-lowering policy (default `auto`). */
|
|
91
65
|
readonly backend?: QueryBackend;
|
|
66
|
+
/** Compiler-reserved physical binds. They retain their exact names and do
|
|
67
|
+
* not participate in public target-language keyword/private-name checks. */
|
|
68
|
+
readonly internalParams?: readonly string[];
|
|
69
|
+
}
|
|
70
|
+
/** Revision-1 SYQL public inputs. These are deliberately separate from SQL
|
|
71
|
+
* binds: groups and switches are public values without corresponding SQLite
|
|
72
|
+
* parameters, while compiler-generated parameters are never public. */
|
|
73
|
+
export type QuerySyqlPublicInput = {
|
|
74
|
+
readonly kind: 'value';
|
|
75
|
+
readonly name: string;
|
|
76
|
+
readonly langName: string;
|
|
77
|
+
readonly type: QueryParamType;
|
|
78
|
+
readonly nullable: boolean;
|
|
79
|
+
readonly required: boolean;
|
|
80
|
+
} | {
|
|
81
|
+
readonly kind: 'group';
|
|
82
|
+
readonly name: string;
|
|
83
|
+
readonly langName: string;
|
|
84
|
+
readonly members: readonly {
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly langName: string;
|
|
87
|
+
readonly type: QueryParamType;
|
|
88
|
+
readonly nullable: boolean;
|
|
89
|
+
}[];
|
|
90
|
+
} | {
|
|
91
|
+
readonly kind: 'switch';
|
|
92
|
+
readonly name: string;
|
|
93
|
+
readonly langName: string;
|
|
94
|
+
readonly default: false;
|
|
95
|
+
} | {
|
|
96
|
+
readonly kind: 'sort';
|
|
97
|
+
readonly name: string;
|
|
98
|
+
readonly langName: string;
|
|
99
|
+
readonly defaultProfile: string;
|
|
100
|
+
readonly profiles: readonly {
|
|
101
|
+
readonly name: string;
|
|
102
|
+
readonly langName: string;
|
|
103
|
+
}[];
|
|
104
|
+
} | {
|
|
105
|
+
readonly kind: 'page';
|
|
106
|
+
readonly name: string;
|
|
107
|
+
readonly langName: string;
|
|
108
|
+
readonly defaultSize: number;
|
|
109
|
+
readonly maxSize: number;
|
|
110
|
+
};
|
|
111
|
+
/** One distinct SQLite bind in a physical revision-1 statement. */
|
|
112
|
+
export type QuerySyqlPlanBind = {
|
|
113
|
+
readonly kind: 'value';
|
|
114
|
+
readonly name: string;
|
|
115
|
+
readonly type: QueryParamType;
|
|
116
|
+
readonly input: string;
|
|
117
|
+
} | {
|
|
118
|
+
readonly kind: 'group-member';
|
|
119
|
+
readonly name: string;
|
|
120
|
+
readonly type: QueryParamType;
|
|
121
|
+
readonly input: string;
|
|
122
|
+
readonly member: string;
|
|
123
|
+
} | {
|
|
124
|
+
readonly kind: 'condition-active';
|
|
125
|
+
readonly name: string;
|
|
126
|
+
readonly type: 'boolean';
|
|
127
|
+
readonly condition: number;
|
|
128
|
+
readonly controls: readonly string[];
|
|
129
|
+
} | {
|
|
130
|
+
readonly kind: 'page';
|
|
131
|
+
readonly name: string;
|
|
132
|
+
readonly type: 'integer';
|
|
133
|
+
readonly input: string;
|
|
134
|
+
};
|
|
135
|
+
/** One SQLite-checked statement selected by sort profile and, for the
|
|
136
|
+
* enumerated backend, the activation bitmask. */
|
|
137
|
+
export interface QuerySyqlStatement {
|
|
138
|
+
readonly sortProfile?: string;
|
|
139
|
+
readonly activationMask?: number;
|
|
140
|
+
readonly sql: string;
|
|
141
|
+
readonly positionalSql: string;
|
|
142
|
+
readonly binds: readonly QuerySyqlPlanBind[];
|
|
143
|
+
}
|
|
144
|
+
/** The target-neutral physical plan every emitter must implement exactly. */
|
|
145
|
+
export interface QuerySyqlExecutionPlan {
|
|
146
|
+
readonly backend: Exclude<QueryBackend, 'auto'>;
|
|
147
|
+
/** One bit per optional scalar/group/switch, in declaration order. */
|
|
148
|
+
readonly activationControls: readonly string[];
|
|
149
|
+
readonly conditions: readonly {
|
|
150
|
+
readonly controls: readonly string[];
|
|
151
|
+
/** Present only for neutralized plans. */
|
|
152
|
+
readonly bind?: string;
|
|
153
|
+
}[];
|
|
154
|
+
readonly statements: readonly QuerySyqlStatement[];
|
|
155
|
+
}
|
|
156
|
+
export interface QuerySyqlMetadata {
|
|
157
|
+
readonly revision: 1;
|
|
158
|
+
readonly inputs: readonly QuerySyqlPublicInput[];
|
|
159
|
+
readonly plan: QuerySyqlExecutionPlan;
|
|
160
|
+
readonly identity?: readonly string[];
|
|
92
161
|
}
|
|
93
162
|
export interface AnalyzedQuery {
|
|
94
163
|
/** camelCase function name (path-derived, or a `-- name:` override). */
|
|
@@ -111,41 +180,9 @@ export interface AnalyzedQuery {
|
|
|
111
180
|
/** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
|
|
112
181
|
readonly tables: readonly string[];
|
|
113
182
|
readonly reactive: QueryReactiveMetadata;
|
|
114
|
-
/**
|
|
115
|
-
*
|
|
116
|
-
|
|
117
|
-
readonly orderBy?: QueryOrderBy;
|
|
118
|
-
/** §6 limit knob (`.syql` tier): the trailing `LIMIT ?` bind's clamp. */
|
|
119
|
-
readonly limit?: QueryLimit;
|
|
120
|
-
/** positional SQL WITHOUT the dynamic ORDER BY/LIMIT tail; present only
|
|
121
|
-
* when `orderBy` is (emitters build `base + ORDER BY <baked> + limit
|
|
122
|
-
* tail`). */
|
|
123
|
-
readonly positionalSqlBase?: string;
|
|
124
|
-
/** The positional limit tail (e.g. ` limit min(coalesce(?, 50), 100)` —
|
|
125
|
-
* the default+clamp live IN the SQL, so runtimes bind `limit ?? null`).
|
|
126
|
-
* Present only when BOTH knobs are declared (composers need it verbatim). */
|
|
127
|
-
readonly positionalLimitTail?: string;
|
|
128
|
-
/** §7 variant-enumeration backend (opt-in `variants` knob): one checked
|
|
129
|
-
* statement per combination of provided optional groups. Semantically
|
|
130
|
-
* identical to the neutralization `sql` by construction; the TS emitter
|
|
131
|
-
* dispatches on provided-ness for perfect index use. `when` lists the
|
|
132
|
-
* provided group keys; `params` are the DISTINCT param names this variant
|
|
133
|
-
* binds, positional order. */
|
|
134
|
-
readonly variants?: readonly {
|
|
135
|
-
readonly when: readonly string[];
|
|
136
|
-
readonly sql: string;
|
|
137
|
-
readonly positionalSql: string;
|
|
138
|
-
readonly params: readonly string[];
|
|
139
|
-
}[];
|
|
140
|
-
/** The optional-group keys, in dispatch-bit order (bit i of the variant
|
|
141
|
-
* index = group i provided). Present iff `variants` is. */
|
|
142
|
-
readonly variantGroups?: readonly {
|
|
143
|
-
readonly key: string;
|
|
144
|
-
/** Params whose provision defines the group (all must be non-null; a
|
|
145
|
-
* flag group is "on" when its single param is TRUE). */
|
|
146
|
-
readonly params: readonly string[];
|
|
147
|
-
readonly flag: boolean;
|
|
148
|
-
}[];
|
|
183
|
+
/** Revision-1 frontend/public-input and physical execution contract. When
|
|
184
|
+
* present, emitters use this instead of treating `params` as the API. */
|
|
185
|
+
readonly syql?: QuerySyqlMetadata;
|
|
149
186
|
}
|
|
150
187
|
/**
|
|
151
188
|
* Path-relative default name. `billing/invoices/list.sql` → `billingInvoicesList`;
|
|
@@ -171,6 +208,9 @@ export interface SplitStatement {
|
|
|
171
208
|
export declare function splitStatements(content: string): SplitStatement[];
|
|
172
209
|
/** The `-- name:` override in a statement's leading comment block, or null. */
|
|
173
210
|
export declare function parseStatementName(statementText: string, location: string): string | null;
|
|
211
|
+
/** Strip `--` and block comments so identifier scans don't hit commented SQL,
|
|
212
|
+
* and string literals so `':x'` inside a string isn't read as a param. */
|
|
213
|
+
export declare function stripCommentsAndStrings(sql: string): string;
|
|
174
214
|
/** Rewrite `:name` → positional placeholders AND strip comments + collapse
|
|
175
215
|
* whitespace, producing a clean single-line SQL string suitable for
|
|
176
216
|
* embedding in a generated string literal. String literals are preserved
|
|
@@ -182,6 +222,18 @@ export declare function parseStatementName(statementText: string, location: stri
|
|
|
182
222
|
* form — one bound value per DISTINCT param, reuse resolved by SQLite —
|
|
183
223
|
* so the generated bind array stays one-entry-per-param. */
|
|
184
224
|
export declare function toPositionalSql(sql: string): string;
|
|
225
|
+
export interface TableRef {
|
|
226
|
+
readonly table: string;
|
|
227
|
+
/** Alias (or the table name when un-aliased). */
|
|
228
|
+
readonly alias: string;
|
|
229
|
+
}
|
|
230
|
+
export declare function scanTableRefs(sql: string, ir: IrDocument): TableRef[];
|
|
231
|
+
/**
|
|
232
|
+
* Return every revision-1 column/LIKE type constraint for one bind. Unlike
|
|
233
|
+
* `inferParamType` (the legacy first-match helper), SYQL uses the complete set
|
|
234
|
+
* so conflicting checked uses cannot be silently accepted.
|
|
235
|
+
*/
|
|
236
|
+
export declare function inferParamTypeEvidence(paramName: string, sql: string, refs: readonly TableRef[], ir: IrDocument): readonly IrColumnType[];
|
|
185
237
|
export interface QueryDb {
|
|
186
238
|
/** Prepare + return the result columns / declared types / param count.
|
|
187
239
|
* Throws (with SQLite's message) when the SQL is invalid against the DDL. */
|
package/dist/query.js
CHANGED
|
@@ -290,7 +290,7 @@ function parseCommentParams(file, raw) {
|
|
|
290
290
|
}
|
|
291
291
|
/** Strip `--` and block comments so identifier scans don't hit commented SQL,
|
|
292
292
|
* and string literals so `':x'` inside a string isn't read as a param. */
|
|
293
|
-
function stripCommentsAndStrings(sql) {
|
|
293
|
+
export function stripCommentsAndStrings(sql) {
|
|
294
294
|
let out = '';
|
|
295
295
|
let i = 0;
|
|
296
296
|
while (i < sql.length) {
|
|
@@ -453,7 +453,7 @@ const RESERVED_ALIAS = new Set([
|
|
|
453
453
|
'limit',
|
|
454
454
|
'having',
|
|
455
455
|
]);
|
|
456
|
-
function scanTableRefs(sql, ir) {
|
|
456
|
+
export function scanTableRefs(sql, ir) {
|
|
457
457
|
const cleaned = stripCommentsAndStrings(sql);
|
|
458
458
|
const known = new Map(ir.tables.map((t) => [t.name, t]));
|
|
459
459
|
const refs = [];
|
|
@@ -603,6 +603,57 @@ function inferParamType(paramName, sql, refs, ir) {
|
|
|
603
603
|
return 'string';
|
|
604
604
|
return null;
|
|
605
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* Return every revision-1 column/LIKE type constraint for one bind. Unlike
|
|
608
|
+
* `inferParamType` (the legacy first-match helper), SYQL uses the complete set
|
|
609
|
+
* so conflicting checked uses cannot be silently accepted.
|
|
610
|
+
*/
|
|
611
|
+
export function inferParamTypeEvidence(paramName, sql, refs, ir) {
|
|
612
|
+
const cleaned = stripCommentsAndStrings(sql);
|
|
613
|
+
const byName = new Map(ir.tables.map((table) => [table.name, table]));
|
|
614
|
+
const evidence = [];
|
|
615
|
+
const add = (qualifier, column) => {
|
|
616
|
+
if (qualifier !== undefined) {
|
|
617
|
+
const ref = refs.find((candidate) => candidate.alias === qualifier);
|
|
618
|
+
const table = ref === undefined ? undefined : byName.get(ref.table);
|
|
619
|
+
const type = table?.columns.find((item) => item.name === column)?.type;
|
|
620
|
+
if (type !== undefined)
|
|
621
|
+
evidence.push(type);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const matches = refs.flatMap((ref) => {
|
|
625
|
+
const table = byName.get(ref.table);
|
|
626
|
+
const type = table?.columns.find((item) => item.name === column)?.type;
|
|
627
|
+
return type === undefined ? [] : [type];
|
|
628
|
+
});
|
|
629
|
+
if (matches.length === 1)
|
|
630
|
+
evidence.push(matches[0]);
|
|
631
|
+
};
|
|
632
|
+
const comparison = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s*(?:=|==|!=|<>|<=|>=|<|>|\\bLIKE\\b|\\bIS\\b)\\s*:${paramName}\\b`, 'gi');
|
|
633
|
+
for (const match of cleaned.matchAll(comparison)) {
|
|
634
|
+
add(match[1], match[2]);
|
|
635
|
+
}
|
|
636
|
+
const reversed = new RegExp(`:${paramName}\\b\\s*(?:=|==|!=|<>|<=|>=|<|>)\\s*(?:(${IDENT})\\.)?(${IDENT})`, 'gi');
|
|
637
|
+
for (const match of cleaned.matchAll(reversed)) {
|
|
638
|
+
add(match[1], match[2]);
|
|
639
|
+
}
|
|
640
|
+
const inList = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+IN\\s*\\(([^)]*:${paramName}\\b[^)]*)\\)`, 'gi');
|
|
641
|
+
for (const match of cleaned.matchAll(inList)) {
|
|
642
|
+
add(match[1], match[2]);
|
|
643
|
+
}
|
|
644
|
+
const betweenLeft = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+:${paramName}\\b`, 'gi');
|
|
645
|
+
for (const match of cleaned.matchAll(betweenLeft)) {
|
|
646
|
+
add(match[1], match[2]);
|
|
647
|
+
}
|
|
648
|
+
const betweenRight = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+\\S+\\s+AND\\s+:${paramName}\\b`, 'gi');
|
|
649
|
+
for (const match of cleaned.matchAll(betweenRight)) {
|
|
650
|
+
add(match[1], match[2]);
|
|
651
|
+
}
|
|
652
|
+
const like = new RegExp(`\\bLIKE\\b[^()]*:${paramName}\\b`, 'gi');
|
|
653
|
+
if (like.test(cleaned))
|
|
654
|
+
evidence.push('string');
|
|
655
|
+
return evidence;
|
|
656
|
+
}
|
|
606
657
|
// -- fallback column typing (computed expressions) ----------------------------
|
|
607
658
|
const AGG_RE = /\b(count|sum|total|avg|min|max)\s*\(/i;
|
|
608
659
|
/** decltype-null column: the documented honest fallback. Aggregates and
|
|
@@ -665,9 +716,7 @@ function inferReactiveMetadata(sql, refs, ir, params, columns) {
|
|
|
665
716
|
continue;
|
|
666
717
|
const name = match[1] ?? match[2];
|
|
667
718
|
const param = name === undefined ? undefined : byParam.get(name);
|
|
668
|
-
if (param !== undefined
|
|
669
|
-
param.optional !== true &&
|
|
670
|
-
param.flag !== true) {
|
|
719
|
+
if (param !== undefined) {
|
|
671
720
|
found.push(param.name);
|
|
672
721
|
}
|
|
673
722
|
}
|
|
@@ -677,9 +726,7 @@ function inferReactiveMetadata(sql, refs, ir, params, columns) {
|
|
|
677
726
|
continue;
|
|
678
727
|
for (const paramMatch of (match[1] ?? '').matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
679
728
|
const param = byParam.get(paramMatch[1]);
|
|
680
|
-
if (param !== undefined
|
|
681
|
-
param.optional !== true &&
|
|
682
|
-
param.flag !== true) {
|
|
729
|
+
if (param !== undefined) {
|
|
683
730
|
found.push(param.name);
|
|
684
731
|
}
|
|
685
732
|
}
|
|
@@ -867,10 +914,12 @@ export function analyzeStatement(name, location, statementText, ir, db, naming =
|
|
|
867
914
|
// Defensive: our scan and SQLite disagree (shouldn't happen for the subset).
|
|
868
915
|
throw new TypegenError(file, `internal: scanned ${paramNames.length} params (${paramNames.join(', ')}) but SQLite reports ${described.paramsCount}`);
|
|
869
916
|
}
|
|
870
|
-
const
|
|
917
|
+
const internalParams = new Set(naming.internalParams ?? []);
|
|
918
|
+
const publicParamLangNames = buildNamingMap(paramNames.filter((name) => !internalParams.has(name)), naming.naming, file, 'params', naming.targets);
|
|
919
|
+
const langNameByParam = new Map(publicParamLangNames.map((mapping) => [mapping.sqlName, mapping.langName]));
|
|
871
920
|
const commentByName = new Map(commentParams.map((p) => [p.name, p.type]));
|
|
872
|
-
const params = paramNames.map((paramName
|
|
873
|
-
const langName =
|
|
921
|
+
const params = paramNames.map((paramName) => {
|
|
922
|
+
const langName = langNameByParam.get(paramName) ?? paramName;
|
|
874
923
|
const commented = commentByName.get(paramName);
|
|
875
924
|
if (commented !== undefined) {
|
|
876
925
|
return { name: paramName, langName, type: commented, source: 'comment' };
|
package/dist/syql-lexer.d.ts
CHANGED
|
@@ -38,3 +38,5 @@ export declare class SyqlFrontendError extends TypegenError {
|
|
|
38
38
|
export declare function isSyqlTrivia(token: SyqlToken): boolean;
|
|
39
39
|
/** Lex one complete `.syql` source file, including trivia and an EOF token. */
|
|
40
40
|
export declare function lexSyqlSource(file: string, source: string): readonly SyqlToken[];
|
|
41
|
+
/** Lex an isolated SQL/template string without import-path contextual rules. */
|
|
42
|
+
export declare function lexSyqlSqlSource(file: string, source: string): readonly SyqlToken[];
|
package/dist/syql-lexer.js
CHANGED
|
@@ -92,9 +92,11 @@ class Lexer {
|
|
|
92
92
|
#column = 1;
|
|
93
93
|
#braceDepth = 0;
|
|
94
94
|
#expectImportPath = false;
|
|
95
|
-
|
|
95
|
+
#recognizeImportPaths;
|
|
96
|
+
constructor(file, source, recognizeImportPaths = true) {
|
|
96
97
|
this.#file = file;
|
|
97
98
|
this.#source = source;
|
|
99
|
+
this.#recognizeImportPaths = recognizeImportPaths;
|
|
98
100
|
}
|
|
99
101
|
lex() {
|
|
100
102
|
while (this.#offset < this.#source.length)
|
|
@@ -161,7 +163,8 @@ class Lexer {
|
|
|
161
163
|
else if (token.text === '}')
|
|
162
164
|
this.#braceDepth = Math.max(0, this.#braceDepth - 1);
|
|
163
165
|
}
|
|
164
|
-
if (
|
|
166
|
+
if (this.#recognizeImportPaths &&
|
|
167
|
+
token.kind === 'identifier' &&
|
|
165
168
|
token.text === 'from' &&
|
|
166
169
|
this.#braceDepth === 0) {
|
|
167
170
|
this.#expectImportPath = true;
|
|
@@ -365,3 +368,7 @@ class Lexer {
|
|
|
365
368
|
export function lexSyqlSource(file, source) {
|
|
366
369
|
return new Lexer(file, source).lex();
|
|
367
370
|
}
|
|
371
|
+
/** Lex an isolated SQL/template string without import-path contextual rules. */
|
|
372
|
+
export function lexSyqlSqlSource(file, source) {
|
|
373
|
+
return new Lexer(file, source, false).lex();
|
|
374
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { IrDocument } from './ir.js';
|
|
2
|
+
import { type AnalyzedQuery, type QueryBackend, type QueryDb, type QueryNamingOptions, type QuerySyqlExecutionPlan } from './query.js';
|
|
3
|
+
import type { SyqlValidatedQuery } from './syql-validator.js';
|
|
4
|
+
export type SyqlLoweringErrorCode = 'SYQL7001_ENUMERATION_LIMIT' | 'SYQL7002_INTERNAL_LOWERING';
|
|
5
|
+
export interface SyqlLoweringOptions {
|
|
6
|
+
readonly backend?: QueryBackend;
|
|
7
|
+
/** Maximum physical activation variants generated for diagnostics and a
|
|
8
|
+
* forced variants backend. Sort profiles multiply this count. */
|
|
9
|
+
readonly maxEnumeratedStatements?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface SyqlLoweredQuery {
|
|
12
|
+
readonly validated: SyqlValidatedQuery;
|
|
13
|
+
readonly analysis: AnalyzedQuery;
|
|
14
|
+
readonly selected: QuerySyqlExecutionPlan;
|
|
15
|
+
readonly neutralized: QuerySyqlExecutionPlan;
|
|
16
|
+
/** Omitted only when enumeration exceeds compiler resource policy. */
|
|
17
|
+
readonly enumerated?: QuerySyqlExecutionPlan;
|
|
18
|
+
}
|
|
19
|
+
/** Lower one validated revision-1 query and prepare every generated physical
|
|
20
|
+
* statement. The selected plan is embedded into the returned shared QueryIR
|
|
21
|
+
* query; the second plan is retained for equivalence conformance tests. */
|
|
22
|
+
export declare function lowerSyqlQuery(validated: SyqlValidatedQuery, ir: IrDocument, db: QueryDb, naming?: QueryNamingOptions, options?: SyqlLoweringOptions): SyqlLoweredQuery;
|