@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/src/manifest.ts
CHANGED
|
@@ -128,9 +128,7 @@ export interface Manifest {
|
|
|
128
128
|
* generated row types/params (queries lower with `AS` aliases); "preserve"
|
|
129
129
|
* keeps SQL-truth names everywhere. */
|
|
130
130
|
readonly naming: NamingMode;
|
|
131
|
-
/** §7 `.syql` backend: "
|
|
132
|
-
* whenever eligible), or "auto" (enumerate at ≤ 2 optional groups). The
|
|
133
|
-
* per-query `variants` knob always forces enumeration. */
|
|
131
|
+
/** §7 `.syql` backend: "auto" (default), "neutralize", or "variants". */
|
|
134
132
|
readonly queryBackend: ManifestQueryBackend;
|
|
135
133
|
readonly output: ManifestOutput;
|
|
136
134
|
readonly schemaVersions: readonly ManifestSchemaVersion[];
|
|
@@ -389,7 +387,7 @@ export function parseManifest(raw: unknown): Manifest {
|
|
|
389
387
|
}
|
|
390
388
|
naming = obj.naming;
|
|
391
389
|
}
|
|
392
|
-
let queryBackend: ManifestQueryBackend = '
|
|
390
|
+
let queryBackend: ManifestQueryBackend = 'auto';
|
|
393
391
|
if (obj.queryBackend !== undefined) {
|
|
394
392
|
if (
|
|
395
393
|
obj.queryBackend !== 'neutralize' &&
|
package/src/query-ir.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { AnalyzedQuery } from './query';
|
|
|
14
14
|
/** Serialize analyzed queries as the deterministic QueryIR JSON document. */
|
|
15
15
|
export function serializeQueryIr(queries: readonly AnalyzedQuery[]): string {
|
|
16
16
|
const doc = {
|
|
17
|
-
queryIrVersion:
|
|
17
|
+
queryIrVersion: 3,
|
|
18
18
|
queries: queries.map((query) => ({
|
|
19
19
|
name: query.name,
|
|
20
20
|
file: query.file,
|
|
@@ -25,11 +25,6 @@ export function serializeQueryIr(queries: readonly AnalyzedQuery[]): string {
|
|
|
25
25
|
name: param.name,
|
|
26
26
|
langName: param.langName,
|
|
27
27
|
type: param.type,
|
|
28
|
-
// §4 metadata — emitted only when set, so `.sql`-tier IR bytes are
|
|
29
|
-
// unchanged from before the DSL existed.
|
|
30
|
-
...(param.optional === true ? { optional: true } : {}),
|
|
31
|
-
...(param.group !== undefined ? { group: param.group } : {}),
|
|
32
|
-
...(param.flag === true ? { flag: true } : {}),
|
|
33
28
|
})),
|
|
34
29
|
columns: query.columns.map((column) => ({
|
|
35
30
|
name: column.name,
|
|
@@ -62,43 +57,58 @@ export function serializeQueryIr(queries: readonly AnalyzedQuery[]): string {
|
|
|
62
57
|
? { rowKey: query.reactive.rowKey }
|
|
63
58
|
: {}),
|
|
64
59
|
},
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
60
|
+
...(query.syql === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: {
|
|
63
|
+
syql: {
|
|
64
|
+
revision: query.syql.revision,
|
|
65
|
+
inputs: query.syql.inputs.map((input) => {
|
|
66
|
+
if (input.kind === 'value') return { ...input };
|
|
67
|
+
if (input.kind === 'group') {
|
|
68
|
+
return {
|
|
69
|
+
kind: input.kind,
|
|
70
|
+
name: input.name,
|
|
71
|
+
langName: input.langName,
|
|
72
|
+
members: input.members.map((member) => ({ ...member })),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (input.kind === 'sort') {
|
|
76
|
+
return {
|
|
77
|
+
kind: input.kind,
|
|
78
|
+
name: input.name,
|
|
79
|
+
langName: input.langName,
|
|
80
|
+
defaultProfile: input.defaultProfile,
|
|
81
|
+
profiles: input.profiles.map((profile) => ({ ...profile })),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return { ...input };
|
|
85
|
+
}),
|
|
86
|
+
plan: {
|
|
87
|
+
backend: query.syql.plan.backend,
|
|
88
|
+
activationControls: query.syql.plan.activationControls,
|
|
89
|
+
conditions: query.syql.plan.conditions.map((condition) => ({
|
|
90
|
+
controls: condition.controls,
|
|
91
|
+
...(condition.bind === undefined
|
|
92
|
+
? {}
|
|
93
|
+
: { bind: condition.bind }),
|
|
94
|
+
})),
|
|
95
|
+
statements: query.syql.plan.statements.map((statement) => ({
|
|
96
|
+
...(statement.sortProfile === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { sortProfile: statement.sortProfile }),
|
|
99
|
+
...(statement.activationMask === undefined
|
|
100
|
+
? {}
|
|
101
|
+
: { activationMask: statement.activationMask }),
|
|
102
|
+
sql: statement.sql,
|
|
103
|
+
positionalSql: statement.positionalSql,
|
|
104
|
+
binds: statement.binds.map((bind) => ({ ...bind })),
|
|
105
|
+
})),
|
|
106
|
+
},
|
|
107
|
+
...(query.syql.identity === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { identity: query.syql.identity }),
|
|
75
110
|
},
|
|
76
|
-
}
|
|
77
|
-
: {}),
|
|
78
|
-
...(query.limit !== undefined ? { limit: query.limit } : {}),
|
|
79
|
-
...(query.positionalSqlBase !== undefined
|
|
80
|
-
? { positionalSqlBase: query.positionalSqlBase }
|
|
81
|
-
: {}),
|
|
82
|
-
// §7 variant backend — emitted only when the query opts in.
|
|
83
|
-
...(query.variantGroups !== undefined
|
|
84
|
-
? {
|
|
85
|
-
variantGroups: query.variantGroups.map((g) => ({
|
|
86
|
-
key: g.key,
|
|
87
|
-
params: g.params,
|
|
88
|
-
flag: g.flag,
|
|
89
|
-
})),
|
|
90
|
-
}
|
|
91
|
-
: {}),
|
|
92
|
-
...(query.variants !== undefined
|
|
93
|
-
? {
|
|
94
|
-
variants: query.variants.map((v) => ({
|
|
95
|
-
when: v.when,
|
|
96
|
-
sql: v.sql,
|
|
97
|
-
positionalSql: v.positionalSql,
|
|
98
|
-
params: v.params,
|
|
99
|
-
})),
|
|
100
|
-
}
|
|
101
|
-
: {}),
|
|
111
|
+
}),
|
|
102
112
|
})),
|
|
103
113
|
};
|
|
104
114
|
return `${JSON.stringify(doc, null, 2)}\n`;
|
package/src/query.ts
CHANGED
|
@@ -98,30 +98,6 @@ export interface QueryParam {
|
|
|
98
98
|
readonly type: QueryParamType;
|
|
99
99
|
/** How the type was resolved — for the docs/tests, not emitted. */
|
|
100
100
|
readonly source: 'inferred' | 'comment';
|
|
101
|
-
/** §4 (DESIGN-queries.md): an optional param — its auto-guarded conjunct
|
|
102
|
-
* applies only when it is provided. Always false for the `.sql` tier. */
|
|
103
|
-
readonly optional?: boolean;
|
|
104
|
-
/** §4: the `from+to` pairing — params sharing a group apply together. */
|
|
105
|
-
readonly group?: string;
|
|
106
|
-
/** §3: a `: flag` boolean guard param (never in a predicate as written;
|
|
107
|
-
* the lowering binds it). Implies optional. */
|
|
108
|
-
readonly flag?: boolean;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/** §6 orderBy knob: a generate-time allowlist (identifiers cannot bind). */
|
|
112
|
-
export interface QueryOrderBy {
|
|
113
|
-
/** Allowed columns: authored SQL name + the language-facing key the
|
|
114
|
-
* generated signature accepts. Each is SQLite-checked at generate time. */
|
|
115
|
-
readonly allowed: readonly { name: string; langName: string }[];
|
|
116
|
-
/** Default column (authored SQL name). */
|
|
117
|
-
readonly defaultColumn: string;
|
|
118
|
-
readonly defaultDir: 'asc' | 'desc';
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** §6 limit knob: a bound value with a codegen clamp + default. */
|
|
122
|
-
export interface QueryLimit {
|
|
123
|
-
readonly max?: number;
|
|
124
|
-
readonly default?: number;
|
|
125
101
|
}
|
|
126
102
|
|
|
127
103
|
export interface QueryColumn {
|
|
@@ -166,7 +142,7 @@ export interface QueryReactiveMetadata {
|
|
|
166
142
|
readonly rowKey?: readonly string[];
|
|
167
143
|
}
|
|
168
144
|
|
|
169
|
-
/** §7/§8 backend selection: neutralization
|
|
145
|
+
/** §7/§8 backend selection: neutralization, always-variants, or
|
|
170
146
|
* the small-N heuristic (`auto`: enumerate at ≤ 2 optional groups). */
|
|
171
147
|
export type QueryBackend = 'neutralize' | 'variants' | 'auto';
|
|
172
148
|
|
|
@@ -176,13 +152,121 @@ export interface QueryNamingOptions {
|
|
|
176
152
|
/** Emitter targets this run generates — keyword hazards are only real on
|
|
177
153
|
* targets that exist. */
|
|
178
154
|
readonly targets: readonly NamingTarget[];
|
|
179
|
-
/** `.syql` conditional-lowering
|
|
180
|
-
* per-query `variants` knob always forces enumeration. */
|
|
155
|
+
/** `.syql` conditional-lowering policy (default `auto`). */
|
|
181
156
|
readonly backend?: QueryBackend;
|
|
157
|
+
/** Compiler-reserved physical binds. They retain their exact names and do
|
|
158
|
+
* not participate in public target-language keyword/private-name checks. */
|
|
159
|
+
readonly internalParams?: readonly string[];
|
|
182
160
|
}
|
|
183
161
|
|
|
184
162
|
const DEFAULT_NAMING: QueryNamingOptions = { naming: 'camel', targets: ['ts'] };
|
|
185
163
|
|
|
164
|
+
/** Revision-1 SYQL public inputs. These are deliberately separate from SQL
|
|
165
|
+
* binds: groups and switches are public values without corresponding SQLite
|
|
166
|
+
* parameters, while compiler-generated parameters are never public. */
|
|
167
|
+
export type QuerySyqlPublicInput =
|
|
168
|
+
| {
|
|
169
|
+
readonly kind: 'value';
|
|
170
|
+
readonly name: string;
|
|
171
|
+
readonly langName: string;
|
|
172
|
+
readonly type: QueryParamType;
|
|
173
|
+
readonly nullable: boolean;
|
|
174
|
+
readonly required: boolean;
|
|
175
|
+
}
|
|
176
|
+
| {
|
|
177
|
+
readonly kind: 'group';
|
|
178
|
+
readonly name: string;
|
|
179
|
+
readonly langName: string;
|
|
180
|
+
readonly members: readonly {
|
|
181
|
+
readonly name: string;
|
|
182
|
+
readonly langName: string;
|
|
183
|
+
readonly type: QueryParamType;
|
|
184
|
+
readonly nullable: boolean;
|
|
185
|
+
}[];
|
|
186
|
+
}
|
|
187
|
+
| {
|
|
188
|
+
readonly kind: 'switch';
|
|
189
|
+
readonly name: string;
|
|
190
|
+
readonly langName: string;
|
|
191
|
+
readonly default: false;
|
|
192
|
+
}
|
|
193
|
+
| {
|
|
194
|
+
readonly kind: 'sort';
|
|
195
|
+
readonly name: string;
|
|
196
|
+
readonly langName: string;
|
|
197
|
+
readonly defaultProfile: string;
|
|
198
|
+
readonly profiles: readonly {
|
|
199
|
+
readonly name: string;
|
|
200
|
+
readonly langName: string;
|
|
201
|
+
}[];
|
|
202
|
+
}
|
|
203
|
+
| {
|
|
204
|
+
readonly kind: 'page';
|
|
205
|
+
readonly name: string;
|
|
206
|
+
readonly langName: string;
|
|
207
|
+
readonly defaultSize: number;
|
|
208
|
+
readonly maxSize: number;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
/** One distinct SQLite bind in a physical revision-1 statement. */
|
|
212
|
+
export type QuerySyqlPlanBind =
|
|
213
|
+
| {
|
|
214
|
+
readonly kind: 'value';
|
|
215
|
+
readonly name: string;
|
|
216
|
+
readonly type: QueryParamType;
|
|
217
|
+
readonly input: string;
|
|
218
|
+
}
|
|
219
|
+
| {
|
|
220
|
+
readonly kind: 'group-member';
|
|
221
|
+
readonly name: string;
|
|
222
|
+
readonly type: QueryParamType;
|
|
223
|
+
readonly input: string;
|
|
224
|
+
readonly member: string;
|
|
225
|
+
}
|
|
226
|
+
| {
|
|
227
|
+
readonly kind: 'condition-active';
|
|
228
|
+
readonly name: string;
|
|
229
|
+
readonly type: 'boolean';
|
|
230
|
+
readonly condition: number;
|
|
231
|
+
readonly controls: readonly string[];
|
|
232
|
+
}
|
|
233
|
+
| {
|
|
234
|
+
readonly kind: 'page';
|
|
235
|
+
readonly name: string;
|
|
236
|
+
readonly type: 'integer';
|
|
237
|
+
readonly input: string;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/** One SQLite-checked statement selected by sort profile and, for the
|
|
241
|
+
* enumerated backend, the activation bitmask. */
|
|
242
|
+
export interface QuerySyqlStatement {
|
|
243
|
+
readonly sortProfile?: string;
|
|
244
|
+
readonly activationMask?: number;
|
|
245
|
+
readonly sql: string;
|
|
246
|
+
readonly positionalSql: string;
|
|
247
|
+
readonly binds: readonly QuerySyqlPlanBind[];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The target-neutral physical plan every emitter must implement exactly. */
|
|
251
|
+
export interface QuerySyqlExecutionPlan {
|
|
252
|
+
readonly backend: Exclude<QueryBackend, 'auto'>;
|
|
253
|
+
/** One bit per optional scalar/group/switch, in declaration order. */
|
|
254
|
+
readonly activationControls: readonly string[];
|
|
255
|
+
readonly conditions: readonly {
|
|
256
|
+
readonly controls: readonly string[];
|
|
257
|
+
/** Present only for neutralized plans. */
|
|
258
|
+
readonly bind?: string;
|
|
259
|
+
}[];
|
|
260
|
+
readonly statements: readonly QuerySyqlStatement[];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface QuerySyqlMetadata {
|
|
264
|
+
readonly revision: 1;
|
|
265
|
+
readonly inputs: readonly QuerySyqlPublicInput[];
|
|
266
|
+
readonly plan: QuerySyqlExecutionPlan;
|
|
267
|
+
readonly identity?: readonly string[];
|
|
268
|
+
}
|
|
269
|
+
|
|
186
270
|
export interface AnalyzedQuery {
|
|
187
271
|
/** camelCase function name (path-derived, or a `-- name:` override). */
|
|
188
272
|
readonly name: string;
|
|
@@ -204,41 +288,9 @@ export interface AnalyzedQuery {
|
|
|
204
288
|
/** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
|
|
205
289
|
readonly tables: readonly string[];
|
|
206
290
|
readonly reactive: QueryReactiveMetadata;
|
|
207
|
-
/**
|
|
208
|
-
*
|
|
209
|
-
|
|
210
|
-
readonly orderBy?: QueryOrderBy;
|
|
211
|
-
/** §6 limit knob (`.syql` tier): the trailing `LIMIT ?` bind's clamp. */
|
|
212
|
-
readonly limit?: QueryLimit;
|
|
213
|
-
/** positional SQL WITHOUT the dynamic ORDER BY/LIMIT tail; present only
|
|
214
|
-
* when `orderBy` is (emitters build `base + ORDER BY <baked> + limit
|
|
215
|
-
* tail`). */
|
|
216
|
-
readonly positionalSqlBase?: string;
|
|
217
|
-
/** The positional limit tail (e.g. ` limit min(coalesce(?, 50), 100)` —
|
|
218
|
-
* the default+clamp live IN the SQL, so runtimes bind `limit ?? null`).
|
|
219
|
-
* Present only when BOTH knobs are declared (composers need it verbatim). */
|
|
220
|
-
readonly positionalLimitTail?: string;
|
|
221
|
-
/** §7 variant-enumeration backend (opt-in `variants` knob): one checked
|
|
222
|
-
* statement per combination of provided optional groups. Semantically
|
|
223
|
-
* identical to the neutralization `sql` by construction; the TS emitter
|
|
224
|
-
* dispatches on provided-ness for perfect index use. `when` lists the
|
|
225
|
-
* provided group keys; `params` are the DISTINCT param names this variant
|
|
226
|
-
* binds, positional order. */
|
|
227
|
-
readonly variants?: readonly {
|
|
228
|
-
readonly when: readonly string[];
|
|
229
|
-
readonly sql: string;
|
|
230
|
-
readonly positionalSql: string;
|
|
231
|
-
readonly params: readonly string[];
|
|
232
|
-
}[];
|
|
233
|
-
/** The optional-group keys, in dispatch-bit order (bit i of the variant
|
|
234
|
-
* index = group i provided). Present iff `variants` is. */
|
|
235
|
-
readonly variantGroups?: readonly {
|
|
236
|
-
readonly key: string;
|
|
237
|
-
/** Params whose provision defines the group (all must be non-null; a
|
|
238
|
-
* flag group is "on" when its single param is TRUE). */
|
|
239
|
-
readonly params: readonly string[];
|
|
240
|
-
readonly flag: boolean;
|
|
241
|
-
}[];
|
|
291
|
+
/** Revision-1 frontend/public-input and physical execution contract. When
|
|
292
|
+
* present, emitters use this instead of treating `params` as the API. */
|
|
293
|
+
readonly syql?: QuerySyqlMetadata;
|
|
242
294
|
}
|
|
243
295
|
|
|
244
296
|
// -- path → name --------------------------------------------------------------
|
|
@@ -502,7 +554,7 @@ function parseCommentParams(file: string, raw: string): CommentParam[] {
|
|
|
502
554
|
|
|
503
555
|
/** Strip `--` and block comments so identifier scans don't hit commented SQL,
|
|
504
556
|
* and string literals so `':x'` inside a string isn't read as a param. */
|
|
505
|
-
function stripCommentsAndStrings(sql: string): string {
|
|
557
|
+
export function stripCommentsAndStrings(sql: string): string {
|
|
506
558
|
let out = '';
|
|
507
559
|
let i = 0;
|
|
508
560
|
while (i < sql.length) {
|
|
@@ -638,7 +690,7 @@ export function toPositionalSql(sql: string): string {
|
|
|
638
690
|
|
|
639
691
|
// -- FROM/JOIN table resolution ----------------------------------------------
|
|
640
692
|
|
|
641
|
-
interface TableRef {
|
|
693
|
+
export interface TableRef {
|
|
642
694
|
readonly table: string;
|
|
643
695
|
/** Alias (or the table name when un-aliased). */
|
|
644
696
|
readonly alias: string;
|
|
@@ -668,7 +720,7 @@ const RESERVED_ALIAS = new Set([
|
|
|
668
720
|
'having',
|
|
669
721
|
]);
|
|
670
722
|
|
|
671
|
-
function scanTableRefs(sql: string, ir: IrDocument): TableRef[] {
|
|
723
|
+
export function scanTableRefs(sql: string, ir: IrDocument): TableRef[] {
|
|
672
724
|
const cleaned = stripCommentsAndStrings(sql);
|
|
673
725
|
const known = new Map(ir.tables.map((t) => [t.name, t] as const));
|
|
674
726
|
const refs: TableRef[] = [];
|
|
@@ -855,6 +907,78 @@ function inferParamType(
|
|
|
855
907
|
return null;
|
|
856
908
|
}
|
|
857
909
|
|
|
910
|
+
/**
|
|
911
|
+
* Return every revision-1 column/LIKE type constraint for one bind. Unlike
|
|
912
|
+
* `inferParamType` (the legacy first-match helper), SYQL uses the complete set
|
|
913
|
+
* so conflicting checked uses cannot be silently accepted.
|
|
914
|
+
*/
|
|
915
|
+
export function inferParamTypeEvidence(
|
|
916
|
+
paramName: string,
|
|
917
|
+
sql: string,
|
|
918
|
+
refs: readonly TableRef[],
|
|
919
|
+
ir: IrDocument,
|
|
920
|
+
): readonly IrColumnType[] {
|
|
921
|
+
const cleaned = stripCommentsAndStrings(sql);
|
|
922
|
+
const byName = new Map(
|
|
923
|
+
ir.tables.map((table) => [table.name, table] as const),
|
|
924
|
+
);
|
|
925
|
+
const evidence: IrColumnType[] = [];
|
|
926
|
+
const add = (qualifier: string | undefined, column: string): void => {
|
|
927
|
+
if (qualifier !== undefined) {
|
|
928
|
+
const ref = refs.find((candidate) => candidate.alias === qualifier);
|
|
929
|
+
const table = ref === undefined ? undefined : byName.get(ref.table);
|
|
930
|
+
const type = table?.columns.find((item) => item.name === column)?.type;
|
|
931
|
+
if (type !== undefined) evidence.push(type);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const matches = refs.flatMap((ref) => {
|
|
935
|
+
const table = byName.get(ref.table);
|
|
936
|
+
const type = table?.columns.find((item) => item.name === column)?.type;
|
|
937
|
+
return type === undefined ? [] : [type];
|
|
938
|
+
});
|
|
939
|
+
if (matches.length === 1) evidence.push(matches[0] as IrColumnType);
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const comparison = new RegExp(
|
|
943
|
+
`(?:(${IDENT})\\.)?(${IDENT})\\s*(?:=|==|!=|<>|<=|>=|<|>|\\bLIKE\\b|\\bIS\\b)\\s*:${paramName}\\b`,
|
|
944
|
+
'gi',
|
|
945
|
+
);
|
|
946
|
+
for (const match of cleaned.matchAll(comparison)) {
|
|
947
|
+
add(match[1], match[2] as string);
|
|
948
|
+
}
|
|
949
|
+
const reversed = new RegExp(
|
|
950
|
+
`:${paramName}\\b\\s*(?:=|==|!=|<>|<=|>=|<|>)\\s*(?:(${IDENT})\\.)?(${IDENT})`,
|
|
951
|
+
'gi',
|
|
952
|
+
);
|
|
953
|
+
for (const match of cleaned.matchAll(reversed)) {
|
|
954
|
+
add(match[1], match[2] as string);
|
|
955
|
+
}
|
|
956
|
+
const inList = new RegExp(
|
|
957
|
+
`(?:(${IDENT})\\.)?(${IDENT})\\s+IN\\s*\\(([^)]*:${paramName}\\b[^)]*)\\)`,
|
|
958
|
+
'gi',
|
|
959
|
+
);
|
|
960
|
+
for (const match of cleaned.matchAll(inList)) {
|
|
961
|
+
add(match[1], match[2] as string);
|
|
962
|
+
}
|
|
963
|
+
const betweenLeft = new RegExp(
|
|
964
|
+
`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+:${paramName}\\b`,
|
|
965
|
+
'gi',
|
|
966
|
+
);
|
|
967
|
+
for (const match of cleaned.matchAll(betweenLeft)) {
|
|
968
|
+
add(match[1], match[2] as string);
|
|
969
|
+
}
|
|
970
|
+
const betweenRight = new RegExp(
|
|
971
|
+
`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+\\S+\\s+AND\\s+:${paramName}\\b`,
|
|
972
|
+
'gi',
|
|
973
|
+
);
|
|
974
|
+
for (const match of cleaned.matchAll(betweenRight)) {
|
|
975
|
+
add(match[1], match[2] as string);
|
|
976
|
+
}
|
|
977
|
+
const like = new RegExp(`\\bLIKE\\b[^()]*:${paramName}\\b`, 'gi');
|
|
978
|
+
if (like.test(cleaned)) evidence.push('string');
|
|
979
|
+
return evidence;
|
|
980
|
+
}
|
|
981
|
+
|
|
858
982
|
// -- fallback column typing (computed expressions) ----------------------------
|
|
859
983
|
|
|
860
984
|
const AGG_RE = /\b(count|sum|total|avg|min|max)\s*\(/i;
|
|
@@ -941,11 +1065,7 @@ function inferReactiveMetadata(
|
|
|
941
1065
|
if (!requiredAt(match.index)) continue;
|
|
942
1066
|
const name = match[1] ?? match[2];
|
|
943
1067
|
const param = name === undefined ? undefined : byParam.get(name);
|
|
944
|
-
if (
|
|
945
|
-
param !== undefined &&
|
|
946
|
-
param.optional !== true &&
|
|
947
|
-
param.flag !== true
|
|
948
|
-
) {
|
|
1068
|
+
if (param !== undefined) {
|
|
949
1069
|
found.push(param.name);
|
|
950
1070
|
}
|
|
951
1071
|
}
|
|
@@ -956,11 +1076,7 @@ function inferReactiveMetadata(
|
|
|
956
1076
|
/:([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
957
1077
|
)) {
|
|
958
1078
|
const param = byParam.get(paramMatch[1] as string);
|
|
959
|
-
if (
|
|
960
|
-
param !== undefined &&
|
|
961
|
-
param.optional !== true &&
|
|
962
|
-
param.flag !== true
|
|
963
|
-
) {
|
|
1079
|
+
if (param !== undefined) {
|
|
964
1080
|
found.push(param.name);
|
|
965
1081
|
}
|
|
966
1082
|
}
|
|
@@ -1214,16 +1330,20 @@ export function analyzeStatement(
|
|
|
1214
1330
|
`internal: scanned ${paramNames.length} params (${paramNames.join(', ')}) but SQLite reports ${described.paramsCount}`,
|
|
1215
1331
|
);
|
|
1216
1332
|
}
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1333
|
+
const internalParams = new Set(naming.internalParams ?? []);
|
|
1334
|
+
const publicParamLangNames = buildNamingMap(
|
|
1335
|
+
paramNames.filter((name) => !internalParams.has(name)),
|
|
1219
1336
|
naming.naming,
|
|
1220
1337
|
file,
|
|
1221
1338
|
'params',
|
|
1222
1339
|
naming.targets,
|
|
1223
1340
|
);
|
|
1341
|
+
const langNameByParam = new Map(
|
|
1342
|
+
publicParamLangNames.map((mapping) => [mapping.sqlName, mapping.langName]),
|
|
1343
|
+
);
|
|
1224
1344
|
const commentByName = new Map(commentParams.map((p) => [p.name, p.type]));
|
|
1225
|
-
const params: QueryParam[] = paramNames.map((paramName
|
|
1226
|
-
const langName =
|
|
1345
|
+
const params: QueryParam[] = paramNames.map((paramName) => {
|
|
1346
|
+
const langName = langNameByParam.get(paramName) ?? paramName;
|
|
1227
1347
|
const commented = commentByName.get(paramName);
|
|
1228
1348
|
if (commented !== undefined) {
|
|
1229
1349
|
return { name: paramName, langName, type: commented, source: 'comment' };
|
package/src/syql-lexer.ts
CHANGED
|
@@ -163,10 +163,12 @@ class Lexer {
|
|
|
163
163
|
#column = 1;
|
|
164
164
|
#braceDepth = 0;
|
|
165
165
|
#expectImportPath = false;
|
|
166
|
+
readonly #recognizeImportPaths: boolean;
|
|
166
167
|
|
|
167
|
-
constructor(file: string, source: string) {
|
|
168
|
+
constructor(file: string, source: string, recognizeImportPaths = true) {
|
|
168
169
|
this.#file = file;
|
|
169
170
|
this.#source = source;
|
|
171
|
+
this.#recognizeImportPaths = recognizeImportPaths;
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
lex(): readonly SyqlToken[] {
|
|
@@ -237,6 +239,7 @@ class Lexer {
|
|
|
237
239
|
}
|
|
238
240
|
|
|
239
241
|
if (
|
|
242
|
+
this.#recognizeImportPaths &&
|
|
240
243
|
token.kind === 'identifier' &&
|
|
241
244
|
token.text === 'from' &&
|
|
242
245
|
this.#braceDepth === 0
|
|
@@ -512,3 +515,11 @@ export function lexSyqlSource(
|
|
|
512
515
|
): readonly SyqlToken[] {
|
|
513
516
|
return new Lexer(file, source).lex();
|
|
514
517
|
}
|
|
518
|
+
|
|
519
|
+
/** Lex an isolated SQL/template string without import-path contextual rules. */
|
|
520
|
+
export function lexSyqlSqlSource(
|
|
521
|
+
file: string,
|
|
522
|
+
source: string,
|
|
523
|
+
): readonly SyqlToken[] {
|
|
524
|
+
return new Lexer(file, source, false).lex();
|
|
525
|
+
}
|