@syncular/typegen 0.5.1 → 0.7.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 +79 -32
- package/dist/cli.js +40 -17
- package/dist/emit-queries-dart.js +168 -55
- package/dist/emit-queries-kotlin.js +167 -55
- package/dist/emit-queries-swift.js +183 -57
- package/dist/emit-queries.js +286 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +348 -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 +349 -187
- 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 +110 -63
- package/dist/query.js +89 -11
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- 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 +434 -0
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +73 -0
- package/dist/syql-semantics.js +607 -0
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +993 -0
- package/package.json +3 -3
- package/src/cli.ts +49 -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 +410 -165
- package/src/fmt.ts +405 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +414 -216
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +229 -79
- package/src/syql-ast.ts +38 -34
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +664 -0
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +930 -0
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +1486 -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
|
@@ -87,6 +87,9 @@ const DECLTYPE_MAP: Readonly<Record<string, IrColumnType>> = {
|
|
|
87
87
|
CRDT: 'crdt',
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
+
/** Local client protocol column; query-only and never part of schema IR. */
|
|
91
|
+
const SYNC_VERSION_COLUMN = '_sync_version';
|
|
92
|
+
|
|
90
93
|
/** A param type is one of the §2.4 types (the columns params compare to). */
|
|
91
94
|
export type QueryParamType = IrColumnType;
|
|
92
95
|
|
|
@@ -98,30 +101,6 @@ export interface QueryParam {
|
|
|
98
101
|
readonly type: QueryParamType;
|
|
99
102
|
/** How the type was resolved — for the docs/tests, not emitted. */
|
|
100
103
|
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
104
|
}
|
|
126
105
|
|
|
127
106
|
export interface QueryColumn {
|
|
@@ -166,7 +145,7 @@ export interface QueryReactiveMetadata {
|
|
|
166
145
|
readonly rowKey?: readonly string[];
|
|
167
146
|
}
|
|
168
147
|
|
|
169
|
-
/** §7/§8 backend selection: neutralization
|
|
148
|
+
/** §7/§8 backend selection: neutralization, always-variants, or
|
|
170
149
|
* the small-N heuristic (`auto`: enumerate at ≤ 2 optional groups). */
|
|
171
150
|
export type QueryBackend = 'neutralize' | 'variants' | 'auto';
|
|
172
151
|
|
|
@@ -176,13 +155,115 @@ export interface QueryNamingOptions {
|
|
|
176
155
|
/** Emitter targets this run generates — keyword hazards are only real on
|
|
177
156
|
* targets that exist. */
|
|
178
157
|
readonly targets: readonly NamingTarget[];
|
|
179
|
-
/** `.syql` conditional-lowering
|
|
180
|
-
* per-query `variants` knob always forces enumeration. */
|
|
158
|
+
/** `.syql` conditional-lowering policy (default `auto`). */
|
|
181
159
|
readonly backend?: QueryBackend;
|
|
160
|
+
/** Compiler-reserved physical binds. They retain their exact names and do
|
|
161
|
+
* not participate in public target-language keyword/private-name checks. */
|
|
162
|
+
readonly internalParams?: readonly string[];
|
|
182
163
|
}
|
|
183
164
|
|
|
184
165
|
const DEFAULT_NAMING: QueryNamingOptions = { naming: 'camel', targets: ['ts'] };
|
|
185
166
|
|
|
167
|
+
/** SYQL public inputs. These are deliberately separate from SQL binds: groups
|
|
168
|
+
* and compiler-generated parameters have distinct public/runtime shapes. */
|
|
169
|
+
export type QuerySyqlPublicInput =
|
|
170
|
+
| {
|
|
171
|
+
readonly kind: 'value';
|
|
172
|
+
readonly name: string;
|
|
173
|
+
readonly langName: string;
|
|
174
|
+
readonly type: QueryParamType;
|
|
175
|
+
readonly nullable: boolean;
|
|
176
|
+
readonly required: boolean;
|
|
177
|
+
readonly default?: false;
|
|
178
|
+
}
|
|
179
|
+
| {
|
|
180
|
+
readonly kind: 'group';
|
|
181
|
+
readonly name: string;
|
|
182
|
+
readonly langName: string;
|
|
183
|
+
readonly members: readonly {
|
|
184
|
+
readonly name: string;
|
|
185
|
+
readonly langName: string;
|
|
186
|
+
readonly type: QueryParamType;
|
|
187
|
+
readonly nullable: boolean;
|
|
188
|
+
}[];
|
|
189
|
+
}
|
|
190
|
+
| {
|
|
191
|
+
readonly kind: 'sort';
|
|
192
|
+
readonly name: string;
|
|
193
|
+
readonly langName: string;
|
|
194
|
+
readonly defaultProfile: string;
|
|
195
|
+
readonly profiles: readonly {
|
|
196
|
+
readonly name: string;
|
|
197
|
+
readonly langName: string;
|
|
198
|
+
}[];
|
|
199
|
+
}
|
|
200
|
+
| {
|
|
201
|
+
readonly kind: 'limit';
|
|
202
|
+
readonly name: string;
|
|
203
|
+
readonly langName: string;
|
|
204
|
+
readonly defaultSize: number;
|
|
205
|
+
readonly maxSize: number;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/** One distinct SQLite bind in a physical revision-1 statement. */
|
|
209
|
+
export type QuerySyqlPlanBind =
|
|
210
|
+
| {
|
|
211
|
+
readonly kind: 'value';
|
|
212
|
+
readonly name: string;
|
|
213
|
+
readonly type: QueryParamType;
|
|
214
|
+
readonly input: string;
|
|
215
|
+
}
|
|
216
|
+
| {
|
|
217
|
+
readonly kind: 'group-member';
|
|
218
|
+
readonly name: string;
|
|
219
|
+
readonly type: QueryParamType;
|
|
220
|
+
readonly input: string;
|
|
221
|
+
readonly member: string;
|
|
222
|
+
}
|
|
223
|
+
| {
|
|
224
|
+
readonly kind: 'condition-active';
|
|
225
|
+
readonly name: string;
|
|
226
|
+
readonly type: 'boolean';
|
|
227
|
+
readonly condition: number;
|
|
228
|
+
readonly controls: readonly string[];
|
|
229
|
+
}
|
|
230
|
+
| {
|
|
231
|
+
readonly kind: 'limit';
|
|
232
|
+
readonly name: string;
|
|
233
|
+
readonly type: 'integer';
|
|
234
|
+
readonly input: string;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/** One SQLite-checked statement selected by sort profile and, for the
|
|
238
|
+
* enumerated backend, the activation bitmask. */
|
|
239
|
+
export interface QuerySyqlStatement {
|
|
240
|
+
readonly sortProfile?: string;
|
|
241
|
+
readonly activationMask?: number;
|
|
242
|
+
readonly sql: string;
|
|
243
|
+
readonly positionalSql: string;
|
|
244
|
+
readonly binds: readonly QuerySyqlPlanBind[];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The target-neutral physical plan every emitter must implement exactly. */
|
|
248
|
+
export interface QuerySyqlExecutionPlan {
|
|
249
|
+
readonly backend: Exclude<QueryBackend, 'auto'>;
|
|
250
|
+
/** One bit per conditional activation control, in declaration order. */
|
|
251
|
+
readonly activationControls: readonly string[];
|
|
252
|
+
readonly conditions: readonly {
|
|
253
|
+
readonly controls: readonly string[];
|
|
254
|
+
/** Present only for neutralized plans. */
|
|
255
|
+
readonly bind?: string;
|
|
256
|
+
}[];
|
|
257
|
+
readonly statements: readonly QuerySyqlStatement[];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface QuerySyqlMetadata {
|
|
261
|
+
readonly revision: 1;
|
|
262
|
+
readonly inputs: readonly QuerySyqlPublicInput[];
|
|
263
|
+
readonly plan: QuerySyqlExecutionPlan;
|
|
264
|
+
readonly identity?: readonly string[];
|
|
265
|
+
}
|
|
266
|
+
|
|
186
267
|
export interface AnalyzedQuery {
|
|
187
268
|
/** camelCase function name (path-derived, or a `-- name:` override). */
|
|
188
269
|
readonly name: string;
|
|
@@ -204,41 +285,9 @@ export interface AnalyzedQuery {
|
|
|
204
285
|
/** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
|
|
205
286
|
readonly tables: readonly string[];
|
|
206
287
|
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
|
-
}[];
|
|
288
|
+
/** Revision-1 frontend/public-input and physical execution contract. When
|
|
289
|
+
* present, emitters use this instead of treating `params` as the API. */
|
|
290
|
+
readonly syql?: QuerySyqlMetadata;
|
|
242
291
|
}
|
|
243
292
|
|
|
244
293
|
// -- path → name --------------------------------------------------------------
|
|
@@ -502,7 +551,7 @@ function parseCommentParams(file: string, raw: string): CommentParam[] {
|
|
|
502
551
|
|
|
503
552
|
/** Strip `--` and block comments so identifier scans don't hit commented SQL,
|
|
504
553
|
* and string literals so `':x'` inside a string isn't read as a param. */
|
|
505
|
-
function stripCommentsAndStrings(sql: string): string {
|
|
554
|
+
export function stripCommentsAndStrings(sql: string): string {
|
|
506
555
|
let out = '';
|
|
507
556
|
let i = 0;
|
|
508
557
|
while (i < sql.length) {
|
|
@@ -638,7 +687,7 @@ export function toPositionalSql(sql: string): string {
|
|
|
638
687
|
|
|
639
688
|
// -- FROM/JOIN table resolution ----------------------------------------------
|
|
640
689
|
|
|
641
|
-
interface TableRef {
|
|
690
|
+
export interface TableRef {
|
|
642
691
|
readonly table: string;
|
|
643
692
|
/** Alias (or the table name when un-aliased). */
|
|
644
693
|
readonly alias: string;
|
|
@@ -668,7 +717,7 @@ const RESERVED_ALIAS = new Set([
|
|
|
668
717
|
'having',
|
|
669
718
|
]);
|
|
670
719
|
|
|
671
|
-
function scanTableRefs(sql: string, ir: IrDocument): TableRef[] {
|
|
720
|
+
export function scanTableRefs(sql: string, ir: IrDocument): TableRef[] {
|
|
672
721
|
const cleaned = stripCommentsAndStrings(sql);
|
|
673
722
|
const known = new Map(ir.tables.map((t) => [t.name, t] as const));
|
|
674
723
|
const refs: TableRef[] = [];
|
|
@@ -767,6 +816,20 @@ function resolveSource(
|
|
|
767
816
|
return null;
|
|
768
817
|
}
|
|
769
818
|
|
|
819
|
+
/** Resolve the one supported query-only protocol column. SQLite still proves
|
|
820
|
+
* qualifier/ambiguity against the synthesized local tables. */
|
|
821
|
+
function isSyncVersionSource(
|
|
822
|
+
item: SelectItem,
|
|
823
|
+
refs: readonly TableRef[],
|
|
824
|
+
): boolean {
|
|
825
|
+
const match = PLAIN_REF_RE.exec(item.expr);
|
|
826
|
+
if (match === null || match[2] !== SYNC_VERSION_COLUMN) return false;
|
|
827
|
+
const qualifier = match[1];
|
|
828
|
+
return qualifier === undefined
|
|
829
|
+
? refs.length === 1
|
|
830
|
+
: refs.some((ref) => ref.alias === qualifier);
|
|
831
|
+
}
|
|
832
|
+
|
|
770
833
|
// -- param type inference -----------------------------------------------------
|
|
771
834
|
//
|
|
772
835
|
// Infer a param's type from `col <op> :name` or `col IN (:name, …)` where `col`
|
|
@@ -855,6 +918,78 @@ function inferParamType(
|
|
|
855
918
|
return null;
|
|
856
919
|
}
|
|
857
920
|
|
|
921
|
+
/**
|
|
922
|
+
* Return every revision-1 column/LIKE type constraint for one bind. Unlike
|
|
923
|
+
* `inferParamType` (the legacy first-match helper), SYQL uses the complete set
|
|
924
|
+
* so conflicting checked uses cannot be silently accepted.
|
|
925
|
+
*/
|
|
926
|
+
export function inferParamTypeEvidence(
|
|
927
|
+
paramName: string,
|
|
928
|
+
sql: string,
|
|
929
|
+
refs: readonly TableRef[],
|
|
930
|
+
ir: IrDocument,
|
|
931
|
+
): readonly IrColumnType[] {
|
|
932
|
+
const cleaned = stripCommentsAndStrings(sql);
|
|
933
|
+
const byName = new Map(
|
|
934
|
+
ir.tables.map((table) => [table.name, table] as const),
|
|
935
|
+
);
|
|
936
|
+
const evidence: IrColumnType[] = [];
|
|
937
|
+
const add = (qualifier: string | undefined, column: string): void => {
|
|
938
|
+
if (qualifier !== undefined) {
|
|
939
|
+
const ref = refs.find((candidate) => candidate.alias === qualifier);
|
|
940
|
+
const table = ref === undefined ? undefined : byName.get(ref.table);
|
|
941
|
+
const type = table?.columns.find((item) => item.name === column)?.type;
|
|
942
|
+
if (type !== undefined) evidence.push(type);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
const matches = refs.flatMap((ref) => {
|
|
946
|
+
const table = byName.get(ref.table);
|
|
947
|
+
const type = table?.columns.find((item) => item.name === column)?.type;
|
|
948
|
+
return type === undefined ? [] : [type];
|
|
949
|
+
});
|
|
950
|
+
if (matches.length === 1) evidence.push(matches[0] as IrColumnType);
|
|
951
|
+
};
|
|
952
|
+
|
|
953
|
+
const comparison = new RegExp(
|
|
954
|
+
`(?:(${IDENT})\\.)?(${IDENT})\\s*(?:=|==|!=|<>|<=|>=|<|>|\\bLIKE\\b|\\bIS\\b)\\s*:${paramName}\\b`,
|
|
955
|
+
'gi',
|
|
956
|
+
);
|
|
957
|
+
for (const match of cleaned.matchAll(comparison)) {
|
|
958
|
+
add(match[1], match[2] as string);
|
|
959
|
+
}
|
|
960
|
+
const reversed = new RegExp(
|
|
961
|
+
`:${paramName}\\b\\s*(?:=|==|!=|<>|<=|>=|<|>)\\s*(?:(${IDENT})\\.)?(${IDENT})`,
|
|
962
|
+
'gi',
|
|
963
|
+
);
|
|
964
|
+
for (const match of cleaned.matchAll(reversed)) {
|
|
965
|
+
add(match[1], match[2] as string);
|
|
966
|
+
}
|
|
967
|
+
const inList = new RegExp(
|
|
968
|
+
`(?:(${IDENT})\\.)?(${IDENT})\\s+IN\\s*\\(([^)]*:${paramName}\\b[^)]*)\\)`,
|
|
969
|
+
'gi',
|
|
970
|
+
);
|
|
971
|
+
for (const match of cleaned.matchAll(inList)) {
|
|
972
|
+
add(match[1], match[2] as string);
|
|
973
|
+
}
|
|
974
|
+
const betweenLeft = new RegExp(
|
|
975
|
+
`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+:${paramName}\\b`,
|
|
976
|
+
'gi',
|
|
977
|
+
);
|
|
978
|
+
for (const match of cleaned.matchAll(betweenLeft)) {
|
|
979
|
+
add(match[1], match[2] as string);
|
|
980
|
+
}
|
|
981
|
+
const betweenRight = new RegExp(
|
|
982
|
+
`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+\\S+\\s+AND\\s+:${paramName}\\b`,
|
|
983
|
+
'gi',
|
|
984
|
+
);
|
|
985
|
+
for (const match of cleaned.matchAll(betweenRight)) {
|
|
986
|
+
add(match[1], match[2] as string);
|
|
987
|
+
}
|
|
988
|
+
const like = new RegExp(`\\bLIKE\\b[^()]*:${paramName}\\b`, 'gi');
|
|
989
|
+
if (like.test(cleaned)) evidence.push('string');
|
|
990
|
+
return evidence;
|
|
991
|
+
}
|
|
992
|
+
|
|
858
993
|
// -- fallback column typing (computed expressions) ----------------------------
|
|
859
994
|
|
|
860
995
|
const AGG_RE = /\b(count|sum|total|avg|min|max)\s*\(/i;
|
|
@@ -941,11 +1076,7 @@ function inferReactiveMetadata(
|
|
|
941
1076
|
if (!requiredAt(match.index)) continue;
|
|
942
1077
|
const name = match[1] ?? match[2];
|
|
943
1078
|
const param = name === undefined ? undefined : byParam.get(name);
|
|
944
|
-
if (
|
|
945
|
-
param !== undefined &&
|
|
946
|
-
param.optional !== true &&
|
|
947
|
-
param.flag !== true
|
|
948
|
-
) {
|
|
1079
|
+
if (param !== undefined) {
|
|
949
1080
|
found.push(param.name);
|
|
950
1081
|
}
|
|
951
1082
|
}
|
|
@@ -956,11 +1087,7 @@ function inferReactiveMetadata(
|
|
|
956
1087
|
/:([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
957
1088
|
)) {
|
|
958
1089
|
const param = byParam.get(paramMatch[1] as string);
|
|
959
|
-
if (
|
|
960
|
-
param !== undefined &&
|
|
961
|
-
param.optional !== true &&
|
|
962
|
-
param.flag !== true
|
|
963
|
-
) {
|
|
1090
|
+
if (param !== undefined) {
|
|
964
1091
|
found.push(param.name);
|
|
965
1092
|
}
|
|
966
1093
|
}
|
|
@@ -1052,6 +1179,10 @@ export function synthesizeDdl(ir: IrDocument): string {
|
|
|
1052
1179
|
const pk = c.name === table.primaryKey ? ' PRIMARY KEY' : '';
|
|
1053
1180
|
return ` ${c.name} ${SQL_TYPE[c.type]}${pk}${notNull}`;
|
|
1054
1181
|
});
|
|
1182
|
+
// The materialized local client table always carries this hidden protocol
|
|
1183
|
+
// token. It is absent from IR mutation types but may be explicitly aliased
|
|
1184
|
+
// by a named read query for optimistic concurrency.
|
|
1185
|
+
cols.push(` ${SYNC_VERSION_COLUMN} INTEGER NOT NULL DEFAULT -1`);
|
|
1055
1186
|
lines.push(`CREATE TABLE ${table.name} (\n${cols.join(',\n')}\n);`);
|
|
1056
1187
|
// Create the declared indexes too — harmless for prepare()/decltype, and
|
|
1057
1188
|
// keeps the type-check DB's shape honest with what the client materializes.
|
|
@@ -1175,6 +1306,21 @@ export function analyzeStatement(
|
|
|
1175
1306
|
const source = item !== undefined ? resolveSource(item, refs, ir) : null;
|
|
1176
1307
|
const sqlName = sqlNames[index] ?? colName;
|
|
1177
1308
|
const langName = langNames[index] ?? colName;
|
|
1309
|
+
if (item !== undefined && isSyncVersionSource(item, refs)) {
|
|
1310
|
+
if (sqlName === SYNC_VERSION_COLUMN) {
|
|
1311
|
+
throw new TypegenError(
|
|
1312
|
+
file,
|
|
1313
|
+
`${SYNC_VERSION_COLUMN} is stripped from app-facing query rows unless explicitly aliased (for example \`${SYNC_VERSION_COLUMN} AS server_version\`)`,
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
name: sqlName,
|
|
1318
|
+
langName,
|
|
1319
|
+
type: 'integer',
|
|
1320
|
+
nullable: false,
|
|
1321
|
+
fidelity: 'exact',
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1178
1324
|
if (source !== null) {
|
|
1179
1325
|
// Exact: IR column type + IR nullability. (decltype agrees; we prefer
|
|
1180
1326
|
// the IR type so blob_ref/crdt/json semantic types survive.)
|
|
@@ -1214,16 +1360,20 @@ export function analyzeStatement(
|
|
|
1214
1360
|
`internal: scanned ${paramNames.length} params (${paramNames.join(', ')}) but SQLite reports ${described.paramsCount}`,
|
|
1215
1361
|
);
|
|
1216
1362
|
}
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1363
|
+
const internalParams = new Set(naming.internalParams ?? []);
|
|
1364
|
+
const publicParamLangNames = buildNamingMap(
|
|
1365
|
+
paramNames.filter((name) => !internalParams.has(name)),
|
|
1219
1366
|
naming.naming,
|
|
1220
1367
|
file,
|
|
1221
1368
|
'params',
|
|
1222
1369
|
naming.targets,
|
|
1223
1370
|
);
|
|
1371
|
+
const langNameByParam = new Map(
|
|
1372
|
+
publicParamLangNames.map((mapping) => [mapping.sqlName, mapping.langName]),
|
|
1373
|
+
);
|
|
1224
1374
|
const commentByName = new Map(commentParams.map((p) => [p.name, p.type]));
|
|
1225
|
-
const params: QueryParam[] = paramNames.map((paramName
|
|
1226
|
-
const langName =
|
|
1375
|
+
const params: QueryParam[] = paramNames.map((paramName) => {
|
|
1376
|
+
const langName = langNameByParam.get(paramName) ?? paramName;
|
|
1227
1377
|
const commented = commentByName.get(paramName);
|
|
1228
1378
|
if (commented !== undefined) {
|
|
1229
1379
|
return { name: paramName, langName, type: commented, source: 'comment' };
|