@syncular/typegen 0.6.0 → 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 +47 -42
- package/dist/cli.js +1 -4
- package/dist/emit-queries-dart.js +9 -8
- package/dist/emit-queries-kotlin.js +9 -8
- package/dist/emit-queries-swift.js +9 -8
- package/dist/emit-queries.js +10 -13
- package/dist/fmt.js +29 -4
- package/dist/lsp.js +16 -28
- package/dist/query.d.ts +6 -11
- package/dist/query.js +29 -0
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- package/dist/syql-lowering.js +65 -42
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +4 -7
- package/dist/syql-semantics.js +121 -65
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +2 -2
- package/dist/syql-validator.js +251 -224
- package/package.json +3 -3
- package/src/cli.ts +1 -3
- package/src/emit-queries-dart.ts +7 -7
- package/src/emit-queries-kotlin.ts +7 -7
- package/src/emit-queries-swift.ts +7 -7
- package/src/emit-queries.ts +9 -12
- package/src/fmt.ts +32 -4
- package/src/lsp.ts +16 -28
- package/src/query.ts +42 -12
- package/src/syql-ast.ts +38 -34
- package/src/syql-lowering.ts +69 -43
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +158 -87
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +330 -336
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. */
|
|
@@ -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`
|
|
@@ -797,6 +810,10 @@ export function synthesizeDdl(ir) {
|
|
|
797
810
|
const pk = c.name === table.primaryKey ? ' PRIMARY KEY' : '';
|
|
798
811
|
return ` ${c.name} ${SQL_TYPE[c.type]}${pk}${notNull}`;
|
|
799
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`);
|
|
800
817
|
lines.push(`CREATE TABLE ${table.name} (\n${cols.join(',\n')}\n);`);
|
|
801
818
|
// Create the declared indexes too — harmless for prepare()/decltype, and
|
|
802
819
|
// keeps the type-check DB's shape honest with what the client materializes.
|
|
@@ -881,6 +898,18 @@ export function analyzeStatement(name, location, statementText, ir, db, naming =
|
|
|
881
898
|
const source = item !== undefined ? resolveSource(item, refs, ir) : null;
|
|
882
899
|
const sqlName = sqlNames[index] ?? colName;
|
|
883
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
|
+
}
|
|
884
913
|
if (source !== null) {
|
|
885
914
|
// Exact: IR column type + IR nullability. (decltype agrees; we prefer
|
|
886
915
|
// the IR type so blob_ref/crdt/json semantic types survive.)
|
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-lowering.js
CHANGED
|
@@ -3,26 +3,17 @@ import { TypegenError } from './errors.js';
|
|
|
3
3
|
import { buildNamingMap } from './naming.js';
|
|
4
4
|
import { analyzeStatement, } from './query.js';
|
|
5
5
|
import { lexSyqlSqlSource } from './syql-lexer.js';
|
|
6
|
+
import { syqlRangeEndBind, syqlRangeStartBind } from './syql-semantics.js';
|
|
6
7
|
const DEFAULT_NAMING = {
|
|
7
8
|
naming: 'camel',
|
|
8
9
|
targets: ['ts'],
|
|
9
10
|
backend: 'auto',
|
|
10
11
|
};
|
|
11
12
|
const DEFAULT_ENUMERATION_LIMIT = 256;
|
|
12
|
-
const
|
|
13
|
+
const LIMIT_BIND = '__syqlLimit';
|
|
13
14
|
function fail(code, query, message) {
|
|
14
15
|
throw new TypegenError(`${query.module.file} (query ${query.declaration.name})`, `${code}: ${message}`);
|
|
15
16
|
}
|
|
16
|
-
function renderDirective(node) {
|
|
17
|
-
const predicates = node.directive.bindings.map((binding) => {
|
|
18
|
-
const column = `${binding.column.qualifier}.${binding.column.name}`;
|
|
19
|
-
const values = binding.values.map((value) => `:${value.name}`);
|
|
20
|
-
return binding.operator === 'equal'
|
|
21
|
-
? `${column} = ${values[0]}`
|
|
22
|
-
: `${column} in (${values.join(', ')})`;
|
|
23
|
-
});
|
|
24
|
-
return `(${predicates.join(' and ')})`;
|
|
25
|
-
}
|
|
26
17
|
function renderTemplate(nodes, conditions, mode) {
|
|
27
18
|
return nodes
|
|
28
19
|
.map((node) => {
|
|
@@ -34,9 +25,6 @@ function renderTemplate(nodes, conditions, mode) {
|
|
|
34
25
|
if (node.kind === 'predicate') {
|
|
35
26
|
return `(${renderTemplate(node.body, conditions, mode)})`;
|
|
36
27
|
}
|
|
37
|
-
if (node.kind === 'scope' || node.kind === 'cover') {
|
|
38
|
-
return renderDirective(node);
|
|
39
|
-
}
|
|
40
28
|
if (node.kind !== 'when') {
|
|
41
29
|
throw new Error('revision-1 lowering found an unknown logical node');
|
|
42
30
|
}
|
|
@@ -75,7 +63,7 @@ function outerLimitOffset(sql, file) {
|
|
|
75
63
|
}
|
|
76
64
|
return sql.length;
|
|
77
65
|
}
|
|
78
|
-
function composeControls(sql, file, sortSql,
|
|
66
|
+
function composeControls(sql, file, sortSql, limit) {
|
|
79
67
|
let out = sql.trim();
|
|
80
68
|
if (sortSql !== undefined) {
|
|
81
69
|
const insertion = outerLimitOffset(out, file);
|
|
@@ -83,10 +71,10 @@ function composeControls(sql, file, sortSql, page) {
|
|
|
83
71
|
.slice(insertion)
|
|
84
72
|
.trimStart()}`.trim();
|
|
85
73
|
}
|
|
86
|
-
if (
|
|
74
|
+
if (limit !== undefined) {
|
|
87
75
|
// `coalesce` gives the generator's metadata-only execution a valid value;
|
|
88
76
|
// runtimes still validate and bind the effective size before execution.
|
|
89
|
-
out = `${out.trimEnd()} limit min(coalesce(:${
|
|
77
|
+
out = `${out.trimEnd()} limit min(coalesce(:${LIMIT_BIND}, ${limit.defaultSize}), ${limit.maxSize})`;
|
|
90
78
|
}
|
|
91
79
|
return out;
|
|
92
80
|
}
|
|
@@ -101,7 +89,7 @@ function distinctBindNames(sql, file) {
|
|
|
101
89
|
}
|
|
102
90
|
return names;
|
|
103
91
|
}
|
|
104
|
-
function headersFor(names, valueOwners, conditionBinds,
|
|
92
|
+
function headersFor(names, valueOwners, conditionBinds, limit, query) {
|
|
105
93
|
return names
|
|
106
94
|
.map((name) => {
|
|
107
95
|
const value = valueOwners.get(name);
|
|
@@ -109,13 +97,13 @@ function headersFor(names, valueOwners, conditionBinds, page, query) {
|
|
|
109
97
|
return `-- param :${name} ${value.type}`;
|
|
110
98
|
if (conditionBinds.has(name))
|
|
111
99
|
return `-- param :${name} boolean`;
|
|
112
|
-
if (name ===
|
|
100
|
+
if (name === LIMIT_BIND && limit !== undefined)
|
|
113
101
|
return `-- param :${name} integer`;
|
|
114
102
|
return fail('SYQL7002_INTERNAL_LOWERING', query, `generated SQL contains unresolved bind :${name}`);
|
|
115
103
|
})
|
|
116
104
|
.join('\n');
|
|
117
105
|
}
|
|
118
|
-
function planBind(name, valueOwners, conditionBinds, conditions,
|
|
106
|
+
function planBind(name, valueOwners, conditionBinds, conditions, limit, query) {
|
|
119
107
|
const value = valueOwners.get(name);
|
|
120
108
|
if (value?.kind === 'value') {
|
|
121
109
|
return { kind: 'value', name, type: value.type, input: value.input };
|
|
@@ -139,12 +127,12 @@ function planBind(name, valueOwners, conditionBinds, conditions, page, query) {
|
|
|
139
127
|
controls: conditions[condition].controls,
|
|
140
128
|
};
|
|
141
129
|
}
|
|
142
|
-
if (name ===
|
|
130
|
+
if (name === LIMIT_BIND && limit !== undefined) {
|
|
143
131
|
return {
|
|
144
|
-
kind: '
|
|
132
|
+
kind: 'limit',
|
|
145
133
|
name,
|
|
146
134
|
type: 'integer',
|
|
147
|
-
input:
|
|
135
|
+
input: limit.control,
|
|
148
136
|
};
|
|
149
137
|
}
|
|
150
138
|
return fail('SYQL7002_INTERNAL_LOWERING', query, `cannot describe generated bind :${name}`);
|
|
@@ -155,17 +143,34 @@ function publicInputs(validated, naming) {
|
|
|
155
143
|
const publicNames = [
|
|
156
144
|
...declaration.parameters.map((parameter) => parameter.name),
|
|
157
145
|
...(declaration.sort === undefined ? [] : [declaration.sort.control]),
|
|
158
|
-
...(declaration.
|
|
146
|
+
...(declaration.limit === undefined ? [] : [declaration.limit.control]),
|
|
159
147
|
];
|
|
160
148
|
const mapped = new Map(buildNamingMap(publicNames, naming.naming, query.module.file, `query ${declaration.name} public inputs`, naming.targets).map((entry) => [entry.sqlName, entry.langName]));
|
|
161
149
|
const result = declaration.parameters.map((parameter) => {
|
|
162
150
|
const langName = mapped.get(parameter.name);
|
|
163
|
-
if (parameter.kind === '
|
|
151
|
+
if (parameter.kind === 'range') {
|
|
152
|
+
const type = validated.bindTypes.get(syqlRangeStartBind(parameter.name));
|
|
153
|
+
if (type === undefined) {
|
|
154
|
+
return fail('SYQL7002_INTERNAL_LOWERING', query, `range ${parameter.name} has no resolved element type`);
|
|
155
|
+
}
|
|
164
156
|
return {
|
|
165
|
-
kind: '
|
|
157
|
+
kind: 'group',
|
|
166
158
|
name: parameter.name,
|
|
167
159
|
langName,
|
|
168
|
-
|
|
160
|
+
members: [
|
|
161
|
+
{
|
|
162
|
+
name: 'start',
|
|
163
|
+
langName: 'start',
|
|
164
|
+
type: type.base,
|
|
165
|
+
nullable: type.nullable,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: 'end',
|
|
169
|
+
langName: 'end',
|
|
170
|
+
type: type.base,
|
|
171
|
+
nullable: type.nullable,
|
|
172
|
+
},
|
|
173
|
+
],
|
|
169
174
|
};
|
|
170
175
|
}
|
|
171
176
|
if (parameter.kind === 'group') {
|
|
@@ -199,7 +204,10 @@ function publicInputs(validated, naming) {
|
|
|
199
204
|
langName,
|
|
200
205
|
type: type.base,
|
|
201
206
|
nullable: type.nullable,
|
|
202
|
-
required: !parameter.optional,
|
|
207
|
+
required: !parameter.optional && parameter.default === undefined,
|
|
208
|
+
...(parameter.default === undefined
|
|
209
|
+
? {}
|
|
210
|
+
: { default: parameter.default }),
|
|
203
211
|
};
|
|
204
212
|
});
|
|
205
213
|
if (validated.sort !== undefined) {
|
|
@@ -215,13 +223,13 @@ function publicInputs(validated, naming) {
|
|
|
215
223
|
})),
|
|
216
224
|
});
|
|
217
225
|
}
|
|
218
|
-
if (validated.
|
|
226
|
+
if (validated.limit !== undefined) {
|
|
219
227
|
result.push({
|
|
220
|
-
kind: '
|
|
221
|
-
name: validated.
|
|
222
|
-
langName: mapped.get(validated.
|
|
223
|
-
defaultSize: validated.
|
|
224
|
-
maxSize: validated.
|
|
228
|
+
kind: 'limit',
|
|
229
|
+
name: validated.limit.control,
|
|
230
|
+
langName: mapped.get(validated.limit.control),
|
|
231
|
+
defaultSize: validated.limit.defaultSize,
|
|
232
|
+
maxSize: validated.limit.maxSize,
|
|
225
233
|
});
|
|
226
234
|
}
|
|
227
235
|
return result;
|
|
@@ -229,9 +237,23 @@ function publicInputs(validated, naming) {
|
|
|
229
237
|
function valueOwners(validated) {
|
|
230
238
|
const owners = new Map();
|
|
231
239
|
for (const parameter of validated.logical.declaration.parameters) {
|
|
232
|
-
if (parameter.kind === '
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
if (parameter.kind === 'range') {
|
|
241
|
+
for (const [name, member] of [
|
|
242
|
+
[syqlRangeStartBind(parameter.name), 'start'],
|
|
243
|
+
[syqlRangeEndBind(parameter.name), 'end'],
|
|
244
|
+
]) {
|
|
245
|
+
const type = validated.bindTypes.get(name);
|
|
246
|
+
if (type === undefined)
|
|
247
|
+
continue;
|
|
248
|
+
owners.set(name, {
|
|
249
|
+
kind: 'group-member',
|
|
250
|
+
input: parameter.name,
|
|
251
|
+
member,
|
|
252
|
+
type: type.base,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (parameter.kind === 'group') {
|
|
235
257
|
for (const member of parameter.members) {
|
|
236
258
|
const type = validated.bindTypes.get(member.name);
|
|
237
259
|
if (type === undefined)
|
|
@@ -267,13 +289,14 @@ function lowerStatement(validated, ir, db, naming, sql, sortProfile, activationM
|
|
|
267
289
|
const query = validated.logical;
|
|
268
290
|
const location = `${query.module.file} (query ${query.declaration.name}, generated)`;
|
|
269
291
|
const names = distinctBindNames(sql, location);
|
|
270
|
-
const headers = headersFor(names, owners, conditionBinds, validated.
|
|
292
|
+
const headers = headersFor(names, owners, conditionBinds, validated.limit, query);
|
|
271
293
|
const candidate = headers.length === 0 ? sql : `${headers}\n${sql}`;
|
|
272
294
|
const analyzed = analyzeStatement(query.declaration.name, location, candidate, ir, db, {
|
|
273
295
|
...naming,
|
|
274
296
|
internalParams: [
|
|
275
297
|
...conditionBinds.keys(),
|
|
276
|
-
...(validated.
|
|
298
|
+
...(validated.limit === undefined ? [] : [LIMIT_BIND]),
|
|
299
|
+
...[...owners.keys()].filter((name) => name.startsWith('__syqlRange')),
|
|
277
300
|
],
|
|
278
301
|
});
|
|
279
302
|
const namedSql = headers.length === 0
|
|
@@ -286,7 +309,7 @@ function lowerStatement(validated, ir, db, naming, sql, sortProfile, activationM
|
|
|
286
309
|
...(activationMask === undefined ? {} : { activationMask }),
|
|
287
310
|
sql: namedSql,
|
|
288
311
|
positionalSql: analyzed.positionalSql,
|
|
289
|
-
binds: bindNames.map((name) => planBind(name, owners, conditionBinds, query.conditions, validated.
|
|
312
|
+
binds: bindNames.map((name) => planBind(name, owners, conditionBinds, query.conditions, validated.limit, query)),
|
|
290
313
|
},
|
|
291
314
|
analysis: { ...analyzed, sourceSql: namedSql, sql: namedSql },
|
|
292
315
|
};
|
|
@@ -315,7 +338,7 @@ export function lowerSyqlQuery(validated, ir, db, naming = DEFAULT_NAMING, optio
|
|
|
315
338
|
const body = renderTemplate(logical.template, conditions, {
|
|
316
339
|
kind: 'neutralized',
|
|
317
340
|
});
|
|
318
|
-
const sql = composeControls(body, logical.module.file, profile.sql, validated.
|
|
341
|
+
const sql = composeControls(body, logical.module.file, profile.sql, validated.limit);
|
|
319
342
|
const lowered = lowerStatement(validated, ir, db, naming, sql, profile.name, undefined, owners, conditionBinds);
|
|
320
343
|
neutralStatements.push(lowered.statement);
|
|
321
344
|
if (neutralDefault === undefined ||
|
|
@@ -347,7 +370,7 @@ export function lowerSyqlQuery(validated, ir, db, naming = DEFAULT_NAMING, optio
|
|
|
347
370
|
kind: 'enumerated',
|
|
348
371
|
active,
|
|
349
372
|
});
|
|
350
|
-
const sql = composeControls(body, logical.module.file, profile.sql, validated.
|
|
373
|
+
const sql = composeControls(body, logical.module.file, profile.sql, validated.limit);
|
|
351
374
|
const lowered = lowerStatement(validated, ir, db, naming, sql, profile.name, mask, owners, new Map());
|
|
352
375
|
statements.push(lowered.statement);
|
|
353
376
|
if (mask === 0 &&
|
package/dist/syql-parser.d.ts
CHANGED
|
@@ -12,13 +12,17 @@ export interface SyqlValueParameter {
|
|
|
12
12
|
readonly name: string;
|
|
13
13
|
readonly optional: boolean;
|
|
14
14
|
readonly type?: SyqlValueType;
|
|
15
|
+
/** A source-level default. Revision 1 currently permits only `bool = false`. */
|
|
16
|
+
readonly default?: false;
|
|
15
17
|
readonly span: SyqlSourceSpan;
|
|
16
18
|
readonly nameSpan: SyqlSourceSpan;
|
|
17
19
|
}
|
|
18
|
-
export interface
|
|
19
|
-
readonly kind: '
|
|
20
|
+
export interface SyqlRangeParameter {
|
|
21
|
+
readonly kind: 'range';
|
|
20
22
|
readonly name: string;
|
|
21
|
-
readonly optional:
|
|
23
|
+
readonly optional: boolean;
|
|
24
|
+
/** Element type, inferred from the left side of BETWEEN when omitted. */
|
|
25
|
+
readonly type?: SyqlValueType;
|
|
22
26
|
readonly span: SyqlSourceSpan;
|
|
23
27
|
readonly nameSpan: SyqlSourceSpan;
|
|
24
28
|
}
|
|
@@ -36,7 +40,7 @@ export interface SyqlGroupParameter {
|
|
|
36
40
|
readonly span: SyqlSourceSpan;
|
|
37
41
|
readonly nameSpan: SyqlSourceSpan;
|
|
38
42
|
}
|
|
39
|
-
export type SyqlQueryParameter = SyqlValueParameter |
|
|
43
|
+
export type SyqlQueryParameter = SyqlValueParameter | SyqlRangeParameter | SyqlGroupParameter;
|
|
40
44
|
export interface SyqlPredicateParameter {
|
|
41
45
|
readonly name: string;
|
|
42
46
|
readonly type?: SyqlValueType;
|
|
@@ -72,11 +76,6 @@ export interface SyqlPredicateDeclaration {
|
|
|
72
76
|
readonly span: SyqlSourceSpan;
|
|
73
77
|
readonly nameSpan: SyqlSourceSpan;
|
|
74
78
|
}
|
|
75
|
-
export interface SyqlSqlSection {
|
|
76
|
-
readonly kind: 'sql';
|
|
77
|
-
readonly body: SyqlTemplate;
|
|
78
|
-
readonly span: SyqlSourceSpan;
|
|
79
|
-
}
|
|
80
79
|
export interface SyqlSortProfile {
|
|
81
80
|
readonly name: string;
|
|
82
81
|
readonly order: SyqlTemplate;
|
|
@@ -91,27 +90,29 @@ export interface SyqlSortSection {
|
|
|
91
90
|
readonly span: SyqlSourceSpan;
|
|
92
91
|
readonly controlSpan: SyqlSourceSpan;
|
|
93
92
|
}
|
|
94
|
-
export interface
|
|
95
|
-
readonly kind: '
|
|
93
|
+
export interface SyqlLimitDeclaration {
|
|
94
|
+
readonly kind: 'limit';
|
|
96
95
|
readonly control: string;
|
|
97
96
|
readonly defaultSize: number;
|
|
98
97
|
readonly maxSize: number;
|
|
99
98
|
readonly span: SyqlSourceSpan;
|
|
100
99
|
readonly controlSpan: SyqlSourceSpan;
|
|
101
100
|
}
|
|
102
|
-
export interface
|
|
103
|
-
readonly
|
|
104
|
-
readonly
|
|
101
|
+
export interface SyqlSyncDimension {
|
|
102
|
+
readonly qualifier: string;
|
|
103
|
+
readonly column: string;
|
|
105
104
|
readonly span: SyqlSourceSpan;
|
|
106
105
|
}
|
|
107
106
|
export interface SyqlQueryDeclaration {
|
|
108
107
|
readonly kind: 'query';
|
|
109
108
|
readonly name: string;
|
|
109
|
+
readonly sync: boolean;
|
|
110
|
+
readonly syncBy?: SyqlSyncDimension;
|
|
110
111
|
readonly parameters: readonly SyqlQueryParameter[];
|
|
111
|
-
|
|
112
|
+
/** The SQL-shaped statement body, excluding its terminating semicolon. */
|
|
113
|
+
readonly statement: SyqlTemplate;
|
|
112
114
|
readonly sort?: SyqlSortSection;
|
|
113
|
-
readonly
|
|
114
|
-
readonly identity?: SyqlIdentityDeclaration;
|
|
115
|
+
readonly limit?: SyqlLimitDeclaration;
|
|
115
116
|
readonly span: SyqlSourceSpan;
|
|
116
117
|
readonly nameSpan: SyqlSourceSpan;
|
|
117
118
|
}
|
|
@@ -127,6 +128,6 @@ export interface SyqlSyntaxFile {
|
|
|
127
128
|
readonly queries: readonly SyqlQueryDeclaration[];
|
|
128
129
|
readonly span: SyqlSourceSpan;
|
|
129
130
|
}
|
|
130
|
-
export type SyqlParseErrorCode = 'SYQL2001_EXPECTED_TOKEN' | 'SYQL2002_INVALID_NAME' | 'SYQL2003_RESERVED_NAME' | 'SYQL2004_DUPLICATE_NAME' | 'SYQL2005_INVALID_IMPORT' | 'SYQL2006_EMPTY_TEMPLATE' | 'SYQL2007_FORBIDDEN_SEMICOLON' | 'SYQL2008_INVALID_MEMBER' | 'SYQL2009_INVALID_INTEGER' | 'SYQL2010_INVALID_PAGE_RANGE' | 'SYQL2011_INVALID_PARAMETER';
|
|
131
|
+
export type SyqlParseErrorCode = 'SYQL2001_EXPECTED_TOKEN' | 'SYQL2002_INVALID_NAME' | 'SYQL2003_RESERVED_NAME' | 'SYQL2004_DUPLICATE_NAME' | 'SYQL2005_INVALID_IMPORT' | 'SYQL2006_EMPTY_TEMPLATE' | 'SYQL2007_FORBIDDEN_SEMICOLON' | 'SYQL2008_INVALID_MEMBER' | 'SYQL2009_INVALID_INTEGER' | 'SYQL2010_INVALID_PAGE_RANGE' | 'SYQL2011_INVALID_PARAMETER' | 'SYQL2012_INVALID_QUERY_BODY';
|
|
131
132
|
/** Parse one `.syql` file using the destructive revision-1 grammar. */
|
|
132
133
|
export declare function parseSyqlSyntaxFile(file: string, source: string): SyqlSyntaxFile;
|