@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/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,98 @@ 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
|
+
/** SYQL public inputs. These are deliberately separate from SQL binds: groups
|
|
71
|
+
* and compiler-generated parameters have distinct public/runtime shapes. */
|
|
72
|
+
export type QuerySyqlPublicInput = {
|
|
73
|
+
readonly kind: 'value';
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly langName: string;
|
|
76
|
+
readonly type: QueryParamType;
|
|
77
|
+
readonly nullable: boolean;
|
|
78
|
+
readonly required: boolean;
|
|
79
|
+
readonly default?: false;
|
|
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: 'sort';
|
|
92
|
+
readonly name: string;
|
|
93
|
+
readonly langName: string;
|
|
94
|
+
readonly defaultProfile: string;
|
|
95
|
+
readonly profiles: readonly {
|
|
96
|
+
readonly name: string;
|
|
97
|
+
readonly langName: string;
|
|
98
|
+
}[];
|
|
99
|
+
} | {
|
|
100
|
+
readonly kind: 'limit';
|
|
101
|
+
readonly name: string;
|
|
102
|
+
readonly langName: string;
|
|
103
|
+
readonly defaultSize: number;
|
|
104
|
+
readonly maxSize: number;
|
|
105
|
+
};
|
|
106
|
+
/** One distinct SQLite bind in a physical revision-1 statement. */
|
|
107
|
+
export type QuerySyqlPlanBind = {
|
|
108
|
+
readonly kind: 'value';
|
|
109
|
+
readonly name: string;
|
|
110
|
+
readonly type: QueryParamType;
|
|
111
|
+
readonly input: string;
|
|
112
|
+
} | {
|
|
113
|
+
readonly kind: 'group-member';
|
|
114
|
+
readonly name: string;
|
|
115
|
+
readonly type: QueryParamType;
|
|
116
|
+
readonly input: string;
|
|
117
|
+
readonly member: string;
|
|
118
|
+
} | {
|
|
119
|
+
readonly kind: 'condition-active';
|
|
120
|
+
readonly name: string;
|
|
121
|
+
readonly type: 'boolean';
|
|
122
|
+
readonly condition: number;
|
|
123
|
+
readonly controls: readonly string[];
|
|
124
|
+
} | {
|
|
125
|
+
readonly kind: 'limit';
|
|
126
|
+
readonly name: string;
|
|
127
|
+
readonly type: 'integer';
|
|
128
|
+
readonly input: string;
|
|
129
|
+
};
|
|
130
|
+
/** One SQLite-checked statement selected by sort profile and, for the
|
|
131
|
+
* enumerated backend, the activation bitmask. */
|
|
132
|
+
export interface QuerySyqlStatement {
|
|
133
|
+
readonly sortProfile?: string;
|
|
134
|
+
readonly activationMask?: number;
|
|
135
|
+
readonly sql: string;
|
|
136
|
+
readonly positionalSql: string;
|
|
137
|
+
readonly binds: readonly QuerySyqlPlanBind[];
|
|
138
|
+
}
|
|
139
|
+
/** The target-neutral physical plan every emitter must implement exactly. */
|
|
140
|
+
export interface QuerySyqlExecutionPlan {
|
|
141
|
+
readonly backend: Exclude<QueryBackend, 'auto'>;
|
|
142
|
+
/** One bit per conditional activation control, in declaration order. */
|
|
143
|
+
readonly activationControls: readonly string[];
|
|
144
|
+
readonly conditions: readonly {
|
|
145
|
+
readonly controls: readonly string[];
|
|
146
|
+
/** Present only for neutralized plans. */
|
|
147
|
+
readonly bind?: string;
|
|
148
|
+
}[];
|
|
149
|
+
readonly statements: readonly QuerySyqlStatement[];
|
|
150
|
+
}
|
|
151
|
+
export interface QuerySyqlMetadata {
|
|
152
|
+
readonly revision: 1;
|
|
153
|
+
readonly inputs: readonly QuerySyqlPublicInput[];
|
|
154
|
+
readonly plan: QuerySyqlExecutionPlan;
|
|
155
|
+
readonly identity?: readonly string[];
|
|
92
156
|
}
|
|
93
157
|
export interface AnalyzedQuery {
|
|
94
158
|
/** camelCase function name (path-derived, or a `-- name:` override). */
|
|
@@ -111,41 +175,9 @@ export interface AnalyzedQuery {
|
|
|
111
175
|
/** IR tables this query reads (the useRawSql `{tables}` set), sorted. */
|
|
112
176
|
readonly tables: readonly string[];
|
|
113
177
|
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
|
-
}[];
|
|
178
|
+
/** Revision-1 frontend/public-input and physical execution contract. When
|
|
179
|
+
* present, emitters use this instead of treating `params` as the API. */
|
|
180
|
+
readonly syql?: QuerySyqlMetadata;
|
|
149
181
|
}
|
|
150
182
|
/**
|
|
151
183
|
* Path-relative default name. `billing/invoices/list.sql` → `billingInvoicesList`;
|
|
@@ -171,6 +203,9 @@ export interface SplitStatement {
|
|
|
171
203
|
export declare function splitStatements(content: string): SplitStatement[];
|
|
172
204
|
/** The `-- name:` override in a statement's leading comment block, or null. */
|
|
173
205
|
export declare function parseStatementName(statementText: string, location: string): string | null;
|
|
206
|
+
/** Strip `--` and block comments so identifier scans don't hit commented SQL,
|
|
207
|
+
* and string literals so `':x'` inside a string isn't read as a param. */
|
|
208
|
+
export declare function stripCommentsAndStrings(sql: string): string;
|
|
174
209
|
/** Rewrite `:name` → positional placeholders AND strip comments + collapse
|
|
175
210
|
* whitespace, producing a clean single-line SQL string suitable for
|
|
176
211
|
* embedding in a generated string literal. String literals are preserved
|
|
@@ -182,6 +217,18 @@ export declare function parseStatementName(statementText: string, location: stri
|
|
|
182
217
|
* form — one bound value per DISTINCT param, reuse resolved by SQLite —
|
|
183
218
|
* so the generated bind array stays one-entry-per-param. */
|
|
184
219
|
export declare function toPositionalSql(sql: string): string;
|
|
220
|
+
export interface TableRef {
|
|
221
|
+
readonly table: string;
|
|
222
|
+
/** Alias (or the table name when un-aliased). */
|
|
223
|
+
readonly alias: string;
|
|
224
|
+
}
|
|
225
|
+
export declare function scanTableRefs(sql: string, ir: IrDocument): TableRef[];
|
|
226
|
+
/**
|
|
227
|
+
* Return every revision-1 column/LIKE type constraint for one bind. Unlike
|
|
228
|
+
* `inferParamType` (the legacy first-match helper), SYQL uses the complete set
|
|
229
|
+
* so conflicting checked uses cannot be silently accepted.
|
|
230
|
+
*/
|
|
231
|
+
export declare function inferParamTypeEvidence(paramName: string, sql: string, refs: readonly TableRef[], ir: IrDocument): readonly IrColumnType[];
|
|
185
232
|
export interface QueryDb {
|
|
186
233
|
/** Prepare + return the result columns / declared types / param count.
|
|
187
234
|
* Throws (with SQLite's message) when the SQL is invalid against the DDL. */
|
package/dist/query.js
CHANGED
|
@@ -84,6 +84,8 @@ const DECLTYPE_MAP = {
|
|
|
84
84
|
BLOBREF: 'blob_ref',
|
|
85
85
|
CRDT: 'crdt',
|
|
86
86
|
};
|
|
87
|
+
/** Local client protocol column; query-only and never part of schema IR. */
|
|
88
|
+
const SYNC_VERSION_COLUMN = '_sync_version';
|
|
87
89
|
const DEFAULT_NAMING = { naming: 'camel', targets: ['ts'] };
|
|
88
90
|
// -- path → name --------------------------------------------------------------
|
|
89
91
|
/** A single kebab-case path segment (folder) or filename stem. */
|
|
@@ -290,7 +292,7 @@ function parseCommentParams(file, raw) {
|
|
|
290
292
|
}
|
|
291
293
|
/** Strip `--` and block comments so identifier scans don't hit commented SQL,
|
|
292
294
|
* and string literals so `':x'` inside a string isn't read as a param. */
|
|
293
|
-
function stripCommentsAndStrings(sql) {
|
|
295
|
+
export function stripCommentsAndStrings(sql) {
|
|
294
296
|
let out = '';
|
|
295
297
|
let i = 0;
|
|
296
298
|
while (i < sql.length) {
|
|
@@ -453,7 +455,7 @@ const RESERVED_ALIAS = new Set([
|
|
|
453
455
|
'limit',
|
|
454
456
|
'having',
|
|
455
457
|
]);
|
|
456
|
-
function scanTableRefs(sql, ir) {
|
|
458
|
+
export function scanTableRefs(sql, ir) {
|
|
457
459
|
const cleaned = stripCommentsAndStrings(sql);
|
|
458
460
|
const known = new Map(ir.tables.map((t) => [t.name, t]));
|
|
459
461
|
const refs = [];
|
|
@@ -533,6 +535,17 @@ function resolveSource(item, refs, ir) {
|
|
|
533
535
|
}
|
|
534
536
|
return null;
|
|
535
537
|
}
|
|
538
|
+
/** Resolve the one supported query-only protocol column. SQLite still proves
|
|
539
|
+
* qualifier/ambiguity against the synthesized local tables. */
|
|
540
|
+
function isSyncVersionSource(item, refs) {
|
|
541
|
+
const match = PLAIN_REF_RE.exec(item.expr);
|
|
542
|
+
if (match === null || match[2] !== SYNC_VERSION_COLUMN)
|
|
543
|
+
return false;
|
|
544
|
+
const qualifier = match[1];
|
|
545
|
+
return qualifier === undefined
|
|
546
|
+
? refs.length === 1
|
|
547
|
+
: refs.some((ref) => ref.alias === qualifier);
|
|
548
|
+
}
|
|
536
549
|
// -- param type inference -----------------------------------------------------
|
|
537
550
|
//
|
|
538
551
|
// Infer a param's type from `col <op> :name` or `col IN (:name, …)` where `col`
|
|
@@ -603,6 +616,57 @@ function inferParamType(paramName, sql, refs, ir) {
|
|
|
603
616
|
return 'string';
|
|
604
617
|
return null;
|
|
605
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* Return every revision-1 column/LIKE type constraint for one bind. Unlike
|
|
621
|
+
* `inferParamType` (the legacy first-match helper), SYQL uses the complete set
|
|
622
|
+
* so conflicting checked uses cannot be silently accepted.
|
|
623
|
+
*/
|
|
624
|
+
export function inferParamTypeEvidence(paramName, sql, refs, ir) {
|
|
625
|
+
const cleaned = stripCommentsAndStrings(sql);
|
|
626
|
+
const byName = new Map(ir.tables.map((table) => [table.name, table]));
|
|
627
|
+
const evidence = [];
|
|
628
|
+
const add = (qualifier, column) => {
|
|
629
|
+
if (qualifier !== undefined) {
|
|
630
|
+
const ref = refs.find((candidate) => candidate.alias === qualifier);
|
|
631
|
+
const table = ref === undefined ? undefined : byName.get(ref.table);
|
|
632
|
+
const type = table?.columns.find((item) => item.name === column)?.type;
|
|
633
|
+
if (type !== undefined)
|
|
634
|
+
evidence.push(type);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const matches = refs.flatMap((ref) => {
|
|
638
|
+
const table = byName.get(ref.table);
|
|
639
|
+
const type = table?.columns.find((item) => item.name === column)?.type;
|
|
640
|
+
return type === undefined ? [] : [type];
|
|
641
|
+
});
|
|
642
|
+
if (matches.length === 1)
|
|
643
|
+
evidence.push(matches[0]);
|
|
644
|
+
};
|
|
645
|
+
const comparison = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s*(?:=|==|!=|<>|<=|>=|<|>|\\bLIKE\\b|\\bIS\\b)\\s*:${paramName}\\b`, 'gi');
|
|
646
|
+
for (const match of cleaned.matchAll(comparison)) {
|
|
647
|
+
add(match[1], match[2]);
|
|
648
|
+
}
|
|
649
|
+
const reversed = new RegExp(`:${paramName}\\b\\s*(?:=|==|!=|<>|<=|>=|<|>)\\s*(?:(${IDENT})\\.)?(${IDENT})`, 'gi');
|
|
650
|
+
for (const match of cleaned.matchAll(reversed)) {
|
|
651
|
+
add(match[1], match[2]);
|
|
652
|
+
}
|
|
653
|
+
const inList = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+IN\\s*\\(([^)]*:${paramName}\\b[^)]*)\\)`, 'gi');
|
|
654
|
+
for (const match of cleaned.matchAll(inList)) {
|
|
655
|
+
add(match[1], match[2]);
|
|
656
|
+
}
|
|
657
|
+
const betweenLeft = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+:${paramName}\\b`, 'gi');
|
|
658
|
+
for (const match of cleaned.matchAll(betweenLeft)) {
|
|
659
|
+
add(match[1], match[2]);
|
|
660
|
+
}
|
|
661
|
+
const betweenRight = new RegExp(`(?:(${IDENT})\\.)?(${IDENT})\\s+BETWEEN\\s+\\S+\\s+AND\\s+:${paramName}\\b`, 'gi');
|
|
662
|
+
for (const match of cleaned.matchAll(betweenRight)) {
|
|
663
|
+
add(match[1], match[2]);
|
|
664
|
+
}
|
|
665
|
+
const like = new RegExp(`\\bLIKE\\b[^()]*:${paramName}\\b`, 'gi');
|
|
666
|
+
if (like.test(cleaned))
|
|
667
|
+
evidence.push('string');
|
|
668
|
+
return evidence;
|
|
669
|
+
}
|
|
606
670
|
// -- fallback column typing (computed expressions) ----------------------------
|
|
607
671
|
const AGG_RE = /\b(count|sum|total|avg|min|max)\s*\(/i;
|
|
608
672
|
/** decltype-null column: the documented honest fallback. Aggregates and
|
|
@@ -665,9 +729,7 @@ function inferReactiveMetadata(sql, refs, ir, params, columns) {
|
|
|
665
729
|
continue;
|
|
666
730
|
const name = match[1] ?? match[2];
|
|
667
731
|
const param = name === undefined ? undefined : byParam.get(name);
|
|
668
|
-
if (param !== undefined
|
|
669
|
-
param.optional !== true &&
|
|
670
|
-
param.flag !== true) {
|
|
732
|
+
if (param !== undefined) {
|
|
671
733
|
found.push(param.name);
|
|
672
734
|
}
|
|
673
735
|
}
|
|
@@ -677,9 +739,7 @@ function inferReactiveMetadata(sql, refs, ir, params, columns) {
|
|
|
677
739
|
continue;
|
|
678
740
|
for (const paramMatch of (match[1] ?? '').matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
679
741
|
const param = byParam.get(paramMatch[1]);
|
|
680
|
-
if (param !== undefined
|
|
681
|
-
param.optional !== true &&
|
|
682
|
-
param.flag !== true) {
|
|
742
|
+
if (param !== undefined) {
|
|
683
743
|
found.push(param.name);
|
|
684
744
|
}
|
|
685
745
|
}
|
|
@@ -750,6 +810,10 @@ export function synthesizeDdl(ir) {
|
|
|
750
810
|
const pk = c.name === table.primaryKey ? ' PRIMARY KEY' : '';
|
|
751
811
|
return ` ${c.name} ${SQL_TYPE[c.type]}${pk}${notNull}`;
|
|
752
812
|
});
|
|
813
|
+
// The materialized local client table always carries this hidden protocol
|
|
814
|
+
// token. It is absent from IR mutation types but may be explicitly aliased
|
|
815
|
+
// by a named read query for optimistic concurrency.
|
|
816
|
+
cols.push(` ${SYNC_VERSION_COLUMN} INTEGER NOT NULL DEFAULT -1`);
|
|
753
817
|
lines.push(`CREATE TABLE ${table.name} (\n${cols.join(',\n')}\n);`);
|
|
754
818
|
// Create the declared indexes too — harmless for prepare()/decltype, and
|
|
755
819
|
// keeps the type-check DB's shape honest with what the client materializes.
|
|
@@ -834,6 +898,18 @@ export function analyzeStatement(name, location, statementText, ir, db, naming =
|
|
|
834
898
|
const source = item !== undefined ? resolveSource(item, refs, ir) : null;
|
|
835
899
|
const sqlName = sqlNames[index] ?? colName;
|
|
836
900
|
const langName = langNames[index] ?? colName;
|
|
901
|
+
if (item !== undefined && isSyncVersionSource(item, refs)) {
|
|
902
|
+
if (sqlName === SYNC_VERSION_COLUMN) {
|
|
903
|
+
throw new TypegenError(file, `${SYNC_VERSION_COLUMN} is stripped from app-facing query rows unless explicitly aliased (for example \`${SYNC_VERSION_COLUMN} AS server_version\`)`);
|
|
904
|
+
}
|
|
905
|
+
return {
|
|
906
|
+
name: sqlName,
|
|
907
|
+
langName,
|
|
908
|
+
type: 'integer',
|
|
909
|
+
nullable: false,
|
|
910
|
+
fidelity: 'exact',
|
|
911
|
+
};
|
|
912
|
+
}
|
|
837
913
|
if (source !== null) {
|
|
838
914
|
// Exact: IR column type + IR nullability. (decltype agrees; we prefer
|
|
839
915
|
// the IR type so blob_ref/crdt/json semantic types survive.)
|
|
@@ -867,10 +943,12 @@ export function analyzeStatement(name, location, statementText, ir, db, naming =
|
|
|
867
943
|
// Defensive: our scan and SQLite disagree (shouldn't happen for the subset).
|
|
868
944
|
throw new TypegenError(file, `internal: scanned ${paramNames.length} params (${paramNames.join(', ')}) but SQLite reports ${described.paramsCount}`);
|
|
869
945
|
}
|
|
870
|
-
const
|
|
946
|
+
const internalParams = new Set(naming.internalParams ?? []);
|
|
947
|
+
const publicParamLangNames = buildNamingMap(paramNames.filter((name) => !internalParams.has(name)), naming.naming, file, 'params', naming.targets);
|
|
948
|
+
const langNameByParam = new Map(publicParamLangNames.map((mapping) => [mapping.sqlName, mapping.langName]));
|
|
871
949
|
const commentByName = new Map(commentParams.map((p) => [p.name, p.type]));
|
|
872
|
-
const params = paramNames.map((paramName
|
|
873
|
-
const langName =
|
|
950
|
+
const params = paramNames.map((paramName) => {
|
|
951
|
+
const langName = langNameByParam.get(paramName) ?? paramName;
|
|
874
952
|
const commented = commentByName.get(paramName);
|
|
875
953
|
if (commented !== undefined) {
|
|
876
954
|
return { name: paramName, langName, type: commented, source: 'comment' };
|
package/dist/syql-ast.d.ts
CHANGED
|
@@ -19,15 +19,8 @@ export type SyqlSemanticTemplateNode = {
|
|
|
19
19
|
} | {
|
|
20
20
|
readonly kind: 'when';
|
|
21
21
|
readonly controls: readonly string[];
|
|
22
|
+
readonly explicitPresence: readonly boolean[];
|
|
22
23
|
readonly body: readonly SyqlSemanticTemplateNode[];
|
|
23
|
-
} | {
|
|
24
|
-
readonly kind: 'scope' | 'cover';
|
|
25
|
-
readonly bindings: readonly {
|
|
26
|
-
readonly qualifier: string;
|
|
27
|
-
readonly column: string;
|
|
28
|
-
readonly operator: 'equal' | 'in';
|
|
29
|
-
readonly values: readonly string[];
|
|
30
|
-
}[];
|
|
31
24
|
};
|
|
32
25
|
export interface SyqlSemanticTemplate {
|
|
33
26
|
readonly mode: SyqlTemplate['tree']['mode'];
|
|
@@ -38,10 +31,12 @@ export type SyqlSemanticQueryParameter = {
|
|
|
38
31
|
readonly name: string;
|
|
39
32
|
readonly optional: boolean;
|
|
40
33
|
readonly type?: SyqlSemanticType;
|
|
34
|
+
readonly default?: false;
|
|
41
35
|
} | {
|
|
42
|
-
readonly kind: '
|
|
36
|
+
readonly kind: 'range';
|
|
43
37
|
readonly name: string;
|
|
44
|
-
readonly optional:
|
|
38
|
+
readonly optional: boolean;
|
|
39
|
+
readonly type?: SyqlSemanticType;
|
|
45
40
|
} | {
|
|
46
41
|
readonly kind: 'group';
|
|
47
42
|
readonly name: string;
|
|
@@ -62,8 +57,13 @@ export type SyqlSemanticDeclaration = {
|
|
|
62
57
|
} | {
|
|
63
58
|
readonly kind: 'query';
|
|
64
59
|
readonly name: string;
|
|
60
|
+
readonly sync: boolean;
|
|
61
|
+
readonly syncBy?: {
|
|
62
|
+
readonly qualifier: string;
|
|
63
|
+
readonly column: string;
|
|
64
|
+
};
|
|
65
65
|
readonly parameters: readonly SyqlSemanticQueryParameter[];
|
|
66
|
-
readonly
|
|
66
|
+
readonly statement: SyqlSemanticTemplate;
|
|
67
67
|
readonly sort?: {
|
|
68
68
|
readonly control: string;
|
|
69
69
|
readonly defaultProfile: string;
|
|
@@ -72,12 +72,11 @@ export type SyqlSemanticDeclaration = {
|
|
|
72
72
|
readonly order: SyqlSemanticTemplate;
|
|
73
73
|
}[];
|
|
74
74
|
};
|
|
75
|
-
readonly
|
|
75
|
+
readonly limit?: {
|
|
76
76
|
readonly control: string;
|
|
77
77
|
readonly defaultSize: number;
|
|
78
78
|
readonly maxSize: number;
|
|
79
79
|
};
|
|
80
|
-
readonly identity?: readonly string[];
|
|
81
80
|
};
|
|
82
81
|
export interface SyqlSemanticFile {
|
|
83
82
|
readonly kind: 'syql-file';
|
package/dist/syql-ast.js
CHANGED
|
@@ -10,8 +10,14 @@ function semanticMember(member) {
|
|
|
10
10
|
return { name: member.name, ...(type === undefined ? {} : { type }) };
|
|
11
11
|
}
|
|
12
12
|
function semanticParameter(parameter) {
|
|
13
|
-
if (parameter.kind === '
|
|
14
|
-
|
|
13
|
+
if (parameter.kind === 'range') {
|
|
14
|
+
const type = semanticType(parameter.type);
|
|
15
|
+
return {
|
|
16
|
+
kind: 'range',
|
|
17
|
+
name: parameter.name,
|
|
18
|
+
optional: parameter.optional,
|
|
19
|
+
...(type === undefined ? {} : { type }),
|
|
20
|
+
};
|
|
15
21
|
}
|
|
16
22
|
if (parameter.kind === 'group') {
|
|
17
23
|
return {
|
|
@@ -27,6 +33,7 @@ function semanticParameter(parameter) {
|
|
|
27
33
|
name: parameter.name,
|
|
28
34
|
optional: parameter.optional,
|
|
29
35
|
...(type === undefined ? {} : { type }),
|
|
36
|
+
...(parameter.default === undefined ? {} : { default: parameter.default }),
|
|
30
37
|
};
|
|
31
38
|
}
|
|
32
39
|
function semanticRawTokens(tokens) {
|
|
@@ -50,18 +57,11 @@ function semanticNode(node) {
|
|
|
50
57
|
return {
|
|
51
58
|
kind: 'when',
|
|
52
59
|
controls: node.controls,
|
|
60
|
+
explicitPresence: node.explicitPresence,
|
|
53
61
|
body: semanticNodes(node.body.nodes),
|
|
54
62
|
};
|
|
55
63
|
}
|
|
56
|
-
return
|
|
57
|
-
kind: node.kind,
|
|
58
|
-
bindings: node.bindings.map((binding) => ({
|
|
59
|
-
qualifier: binding.column.qualifier,
|
|
60
|
-
column: binding.column.name,
|
|
61
|
-
operator: binding.operator,
|
|
62
|
-
values: binding.values.map((value) => value.name),
|
|
63
|
-
})),
|
|
64
|
-
};
|
|
64
|
+
return undefined;
|
|
65
65
|
}
|
|
66
66
|
function semanticNodes(nodes) {
|
|
67
67
|
return nodes.flatMap((node) => {
|
|
@@ -93,8 +93,17 @@ function semanticDeclaration(declaration) {
|
|
|
93
93
|
return {
|
|
94
94
|
kind: 'query',
|
|
95
95
|
name: declaration.name,
|
|
96
|
+
sync: declaration.sync,
|
|
97
|
+
...(declaration.syncBy === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: {
|
|
100
|
+
syncBy: {
|
|
101
|
+
qualifier: declaration.syncBy.qualifier,
|
|
102
|
+
column: declaration.syncBy.column,
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
96
105
|
parameters: declaration.parameters.map(semanticParameter),
|
|
97
|
-
|
|
106
|
+
statement: semanticTemplate(declaration.statement),
|
|
98
107
|
...(declaration.sort === undefined
|
|
99
108
|
? {}
|
|
100
109
|
: {
|
|
@@ -107,18 +116,15 @@ function semanticDeclaration(declaration) {
|
|
|
107
116
|
})),
|
|
108
117
|
},
|
|
109
118
|
}),
|
|
110
|
-
...(declaration.
|
|
119
|
+
...(declaration.limit === undefined
|
|
111
120
|
? {}
|
|
112
121
|
: {
|
|
113
|
-
|
|
114
|
-
control: declaration.
|
|
115
|
-
defaultSize: declaration.
|
|
116
|
-
maxSize: declaration.
|
|
122
|
+
limit: {
|
|
123
|
+
control: declaration.limit.control,
|
|
124
|
+
defaultSize: declaration.limit.defaultSize,
|
|
125
|
+
maxSize: declaration.limit.maxSize,
|
|
117
126
|
},
|
|
118
127
|
}),
|
|
119
|
-
...(declaration.identity === undefined
|
|
120
|
-
? {}
|
|
121
|
-
: { identity: declaration.identity.fields }),
|
|
122
128
|
};
|
|
123
129
|
}
|
|
124
130
|
/** Return the stable syntax/semantic AST pinned by `spec/syql` fixtures. */
|
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[];
|