@syncular/typegen 0.15.13 → 0.15.14
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 +41 -7
- package/dist/emit-queries-rust.d.ts +2 -0
- package/dist/emit-queries-rust.js +748 -0
- package/dist/emit-queries.js +47 -3
- package/dist/generate.js +12 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lsp.js +2 -0
- package/dist/manifest.d.ts +9 -1
- package/dist/manifest.js +24 -3
- package/dist/naming.d.ts +11 -1
- package/dist/naming.js +126 -2
- package/package.json +3 -3
- package/src/emit-queries-rust.ts +975 -0
- package/src/emit-queries.ts +57 -2
- package/src/generate.ts +16 -1
- package/src/index.ts +1 -0
- package/src/lsp.ts +1 -0
- package/src/manifest.ts +37 -4
- package/src/naming.ts +153 -3
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
import { buildRustNamingMap, rustPascalCase, rustSnakeCase } from './naming.js';
|
|
2
|
+
const RUST_TYPE = {
|
|
3
|
+
string: 'String',
|
|
4
|
+
integer: 'i64',
|
|
5
|
+
float: 'f64',
|
|
6
|
+
boolean: 'bool',
|
|
7
|
+
json: 'String',
|
|
8
|
+
bytes: 'Vec<u8>',
|
|
9
|
+
blob_ref: 'String',
|
|
10
|
+
crdt: 'Vec<u8>',
|
|
11
|
+
};
|
|
12
|
+
function quote(value) {
|
|
13
|
+
let out = '"';
|
|
14
|
+
for (const char of value) {
|
|
15
|
+
const point = char.codePointAt(0);
|
|
16
|
+
if (char === '"')
|
|
17
|
+
out += '\\"';
|
|
18
|
+
else if (char === '\\')
|
|
19
|
+
out += '\\\\';
|
|
20
|
+
else if (char === '\n')
|
|
21
|
+
out += '\\n';
|
|
22
|
+
else if (char === '\r')
|
|
23
|
+
out += '\\r';
|
|
24
|
+
else if (char === '\t')
|
|
25
|
+
out += '\\t';
|
|
26
|
+
else if (point < 0x20 || point === 0x7f)
|
|
27
|
+
out += `\\u{${point.toString(16)}}`;
|
|
28
|
+
else
|
|
29
|
+
out += char;
|
|
30
|
+
}
|
|
31
|
+
return `${out}"`;
|
|
32
|
+
}
|
|
33
|
+
function field(name) {
|
|
34
|
+
return rustSnakeCase(name);
|
|
35
|
+
}
|
|
36
|
+
function typeName(name) {
|
|
37
|
+
return rustPascalCase(name);
|
|
38
|
+
}
|
|
39
|
+
function queryId(query, hash) {
|
|
40
|
+
return `${hash}/${query.name}`;
|
|
41
|
+
}
|
|
42
|
+
function rustType(type, nullable = false) {
|
|
43
|
+
const base = RUST_TYPE[type];
|
|
44
|
+
return nullable ? `Option<${base}>` : base;
|
|
45
|
+
}
|
|
46
|
+
function syqlInput(query, name) {
|
|
47
|
+
const input = query.syql?.inputs.find((candidate) => candidate.name === name);
|
|
48
|
+
if (input === undefined)
|
|
49
|
+
throw new Error(`unknown SYQL input ${name}`);
|
|
50
|
+
return input;
|
|
51
|
+
}
|
|
52
|
+
function validateQueryNames(query) {
|
|
53
|
+
buildRustNamingMap([query.name], query.file, 'query module');
|
|
54
|
+
buildRustNamingMap(query.columns.map((column) => column.langName), query.file, `query ${query.name} projection`);
|
|
55
|
+
if (query.syql === undefined) {
|
|
56
|
+
buildRustNamingMap(query.params.map((param) => param.langName), query.file, `query ${query.name} params`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
buildRustNamingMap(query.syql.inputs.map((input) => input.langName), query.file, `query ${query.name} inputs`);
|
|
60
|
+
for (const input of query.syql.inputs) {
|
|
61
|
+
if (input.kind === 'group') {
|
|
62
|
+
buildRustNamingMap(input.members.map((member) => member.langName), query.file, `query ${query.name} group ${input.name}`);
|
|
63
|
+
}
|
|
64
|
+
else if (input.kind === 'sort') {
|
|
65
|
+
buildRustNamingMap(input.profiles.map((profile) => profile.langName), query.file, `query ${query.name} sort ${input.name}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function bindValue(type, reference, input) {
|
|
70
|
+
switch (type) {
|
|
71
|
+
case 'string':
|
|
72
|
+
case 'json':
|
|
73
|
+
case 'blob_ref':
|
|
74
|
+
return `bind_string(${reference})`;
|
|
75
|
+
case 'integer':
|
|
76
|
+
return `bind_integer(${reference})`;
|
|
77
|
+
case 'float':
|
|
78
|
+
return `bind_float(${reference}, ID, ${quote(input)})?`;
|
|
79
|
+
case 'boolean':
|
|
80
|
+
return `bind_boolean(${reference})`;
|
|
81
|
+
case 'bytes':
|
|
82
|
+
case 'crdt':
|
|
83
|
+
return `bind_bytes(${reference})`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function optionalBindValue(type, option, input) {
|
|
87
|
+
return `match ${option} { Some(value) => ${bindValue(type, 'value', input)}, None => QueryValue::Null }`;
|
|
88
|
+
}
|
|
89
|
+
function syqlControlActive(query, name) {
|
|
90
|
+
const input = syqlInput(query, name);
|
|
91
|
+
const access = `params.${field(input.langName)}`;
|
|
92
|
+
if (input.kind === 'value' && input.default === false)
|
|
93
|
+
return access;
|
|
94
|
+
if (input.kind === 'value' && input.nullable) {
|
|
95
|
+
return `matches!(&${access}, SyqlPresence::Present(_))`;
|
|
96
|
+
}
|
|
97
|
+
if (input.kind === 'value' || input.kind === 'group') {
|
|
98
|
+
return `${access}.is_some()`;
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`${name} is not an activation control`);
|
|
101
|
+
}
|
|
102
|
+
function syqlBindExpr(query, bind) {
|
|
103
|
+
if (bind.kind === 'condition-active') {
|
|
104
|
+
const active = bind.controls
|
|
105
|
+
.map((control) => syqlControlActive(query, control))
|
|
106
|
+
.join(' && ');
|
|
107
|
+
return `bind_boolean(&(${active}))`;
|
|
108
|
+
}
|
|
109
|
+
const input = syqlInput(query, bind.input);
|
|
110
|
+
const access = `params.${field(input.langName)}`;
|
|
111
|
+
if (bind.kind === 'limit') {
|
|
112
|
+
return `bind_integer(&effective_${field(input.langName)})`;
|
|
113
|
+
}
|
|
114
|
+
if (bind.kind === 'group-member') {
|
|
115
|
+
if (input.kind !== 'group')
|
|
116
|
+
throw new Error('group bind/input mismatch');
|
|
117
|
+
const member = input.members.find((candidate) => candidate.name === bind.member);
|
|
118
|
+
if (member === undefined) {
|
|
119
|
+
throw new Error(`unknown group member ${bind.member}`);
|
|
120
|
+
}
|
|
121
|
+
const memberAccess = `&group.${field(member.langName)}`;
|
|
122
|
+
const present = member.nullable
|
|
123
|
+
? optionalBindValue(member.type, memberAccess, `${input.langName}.${member.langName}`)
|
|
124
|
+
: bindValue(member.type, memberAccess, `${input.langName}.${member.langName}`);
|
|
125
|
+
return `match &${access} { Some(group) => ${present}, None => QueryValue::Null }`;
|
|
126
|
+
}
|
|
127
|
+
if (input.kind !== 'value')
|
|
128
|
+
throw new Error('value bind/input mismatch');
|
|
129
|
+
if (input.default === false || (input.required && !input.nullable)) {
|
|
130
|
+
return bindValue(input.type, `&${access}`, input.langName);
|
|
131
|
+
}
|
|
132
|
+
if (input.required) {
|
|
133
|
+
return optionalBindValue(input.type, `&${access}`, input.langName);
|
|
134
|
+
}
|
|
135
|
+
if (input.nullable) {
|
|
136
|
+
return `match &${access} { SyqlPresence::Absent | SyqlPresence::Present(None) => QueryValue::Null, SyqlPresence::Present(Some(value)) => ${bindValue(input.type, 'value', input.langName)} }`;
|
|
137
|
+
}
|
|
138
|
+
return optionalBindValue(input.type, `&${access}`, input.langName);
|
|
139
|
+
}
|
|
140
|
+
function plainBindExpr(param) {
|
|
141
|
+
return bindValue(param.type, `¶ms.${field(param.langName)}`, param.langName);
|
|
142
|
+
}
|
|
143
|
+
function decodeFunction(type) {
|
|
144
|
+
switch (type) {
|
|
145
|
+
case 'string':
|
|
146
|
+
case 'json':
|
|
147
|
+
case 'blob_ref':
|
|
148
|
+
return 'decode_string';
|
|
149
|
+
case 'integer':
|
|
150
|
+
return 'decode_integer';
|
|
151
|
+
case 'float':
|
|
152
|
+
return 'decode_float';
|
|
153
|
+
case 'boolean':
|
|
154
|
+
return 'decode_boolean';
|
|
155
|
+
case 'bytes':
|
|
156
|
+
case 'crdt':
|
|
157
|
+
return 'decode_bytes';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function emitRow(query) {
|
|
161
|
+
const lines = [
|
|
162
|
+
` /// One strictly decoded row of the ${query.name} query.`,
|
|
163
|
+
' #[derive(Debug, Clone, PartialEq)]',
|
|
164
|
+
' pub struct Row {',
|
|
165
|
+
];
|
|
166
|
+
for (const column of query.columns) {
|
|
167
|
+
lines.push(` pub ${field(column.langName)}: ${rustType(column.type, column.nullable)},`);
|
|
168
|
+
}
|
|
169
|
+
lines.push(' }', '', ' pub(crate) fn decode(row: QueryRow) -> Result<Row, QueryError> {');
|
|
170
|
+
for (const column of query.columns) {
|
|
171
|
+
const name = field(column.langName);
|
|
172
|
+
const value = `required_value(&row, ID, ${quote(column.langName)})?`;
|
|
173
|
+
const decoder = decodeFunction(column.type);
|
|
174
|
+
if (column.nullable) {
|
|
175
|
+
lines.push(` let ${name} = match ${value} {`, ' QueryValue::Null => None,', ` value => Some(${decoder}(value, ID, ${quote(column.langName)})?),`, ' };');
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
lines.push(` let ${name} = ${decoder}(${value}, ID, ${quote(column.langName)})?;`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
lines.push(' Ok(Row {');
|
|
182
|
+
for (const column of query.columns) {
|
|
183
|
+
lines.push(` ${field(column.langName)},`);
|
|
184
|
+
}
|
|
185
|
+
lines.push(' })', ' }');
|
|
186
|
+
return lines;
|
|
187
|
+
}
|
|
188
|
+
function syqlFieldType(input) {
|
|
189
|
+
if (input.kind === 'value') {
|
|
190
|
+
const valueType = rustType(input.type, input.nullable);
|
|
191
|
+
if (input.required || input.default === false)
|
|
192
|
+
return valueType;
|
|
193
|
+
return input.nullable
|
|
194
|
+
? `SyqlPresence<${valueType}>`
|
|
195
|
+
: `Option<${valueType}>`;
|
|
196
|
+
}
|
|
197
|
+
if (input.kind === 'group')
|
|
198
|
+
return `Option<${typeName(input.langName)}>`;
|
|
199
|
+
if (input.kind === 'sort')
|
|
200
|
+
return typeName(input.langName);
|
|
201
|
+
return 'Option<i64>';
|
|
202
|
+
}
|
|
203
|
+
function emitParams(query) {
|
|
204
|
+
if (query.syql === undefined)
|
|
205
|
+
return emitPlainParams(query);
|
|
206
|
+
const lines = [];
|
|
207
|
+
for (const input of query.syql.inputs) {
|
|
208
|
+
if (input.kind === 'group') {
|
|
209
|
+
lines.push(' #[derive(Debug, Clone, PartialEq)]', ` pub struct ${typeName(input.langName)} {`);
|
|
210
|
+
for (const member of input.members) {
|
|
211
|
+
lines.push(` pub ${field(member.langName)}: ${rustType(member.type, member.nullable)},`);
|
|
212
|
+
}
|
|
213
|
+
lines.push(' }', '');
|
|
214
|
+
}
|
|
215
|
+
else if (input.kind === 'sort') {
|
|
216
|
+
lines.push(' #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]', ` pub enum ${typeName(input.langName)} {`);
|
|
217
|
+
for (const profile of input.profiles) {
|
|
218
|
+
if (profile.name === input.defaultProfile)
|
|
219
|
+
lines.push(' #[default]');
|
|
220
|
+
lines.push(` ${typeName(profile.langName)},`);
|
|
221
|
+
}
|
|
222
|
+
lines.push(' }', '');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (query.syql.inputs.length === 0) {
|
|
226
|
+
lines.push(' pub type Params = ();');
|
|
227
|
+
return lines;
|
|
228
|
+
}
|
|
229
|
+
const required = query.syql.inputs.filter((input) => input.kind === 'value' && input.required);
|
|
230
|
+
lines.push(` #[derive(Debug, Clone, PartialEq${required.length === 0 ? ', Default' : ''})]`, ' pub struct Params {');
|
|
231
|
+
for (const input of query.syql.inputs) {
|
|
232
|
+
lines.push(` pub ${field(input.langName)}: ${syqlFieldType(input)},`);
|
|
233
|
+
}
|
|
234
|
+
lines.push(' }', '', ' impl Params {');
|
|
235
|
+
const args = required.map((input) => `${field(input.langName)}: ${syqlFieldType(input)}`);
|
|
236
|
+
lines.push(` pub fn new(${args.join(', ')}) -> Self {`, ' Self {');
|
|
237
|
+
for (const input of query.syql.inputs) {
|
|
238
|
+
const name = field(input.langName);
|
|
239
|
+
if (input.kind === 'value' && input.required)
|
|
240
|
+
lines.push(` ${name},`);
|
|
241
|
+
else if (input.kind === 'value' && input.default === false) {
|
|
242
|
+
lines.push(` ${name}: false,`);
|
|
243
|
+
}
|
|
244
|
+
else if (input.kind === 'value' && input.nullable) {
|
|
245
|
+
lines.push(` ${name}: SyqlPresence::Absent,`);
|
|
246
|
+
}
|
|
247
|
+
else if (input.kind === 'sort') {
|
|
248
|
+
const profile = input.profiles.find((candidate) => candidate.name === input.defaultProfile);
|
|
249
|
+
if (profile === undefined)
|
|
250
|
+
throw new Error('missing default sort profile');
|
|
251
|
+
lines.push(` ${name}: ${typeName(input.langName)}::${typeName(profile.langName)},`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
lines.push(` ${name}: None,`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
lines.push(' }', ' }', ' }');
|
|
258
|
+
return lines;
|
|
259
|
+
}
|
|
260
|
+
function emitPlainParams(query) {
|
|
261
|
+
if (query.params.length === 0)
|
|
262
|
+
return [' pub type Params = ();'];
|
|
263
|
+
const lines = [
|
|
264
|
+
' #[derive(Debug, Clone, PartialEq)]',
|
|
265
|
+
' pub struct Params {',
|
|
266
|
+
];
|
|
267
|
+
for (const param of query.params) {
|
|
268
|
+
lines.push(` pub ${field(param.langName)}: ${rustType(param.type)},`);
|
|
269
|
+
}
|
|
270
|
+
lines.push(' }', '', ' impl Params {');
|
|
271
|
+
const args = query.params
|
|
272
|
+
.map((param) => `${field(param.langName)}: ${rustType(param.type)}`)
|
|
273
|
+
.join(', ');
|
|
274
|
+
lines.push(` pub fn new(${args}) -> Self {`, ' Self {');
|
|
275
|
+
for (const param of query.params) {
|
|
276
|
+
lines.push(` ${field(param.langName)},`);
|
|
277
|
+
}
|
|
278
|
+
lines.push(' }', ' }', ' }');
|
|
279
|
+
return lines;
|
|
280
|
+
}
|
|
281
|
+
function emitSelect(query) {
|
|
282
|
+
if (query.syql === undefined) {
|
|
283
|
+
const binds = query.params.map((param) => plainBindExpr(param)).join(', ');
|
|
284
|
+
return [
|
|
285
|
+
` pub fn select(${query.params.length === 0 ? '_params' : 'params'}: &Params) -> Result<SelectedQuery, QueryError> {`,
|
|
286
|
+
' Ok(SelectedQuery {',
|
|
287
|
+
` sql: ${quote(query.positionalSql)},`,
|
|
288
|
+
` params: vec![${binds}],`,
|
|
289
|
+
' })',
|
|
290
|
+
' }',
|
|
291
|
+
];
|
|
292
|
+
}
|
|
293
|
+
const metadata = query.syql;
|
|
294
|
+
const lines = [
|
|
295
|
+
` pub fn select(${metadata.inputs.length === 0 ? '_params' : 'params'}: &Params) -> Result<SelectedQuery, QueryError> {`,
|
|
296
|
+
];
|
|
297
|
+
const limit = metadata.inputs.find((input) => input.kind === 'limit');
|
|
298
|
+
if (limit?.kind === 'limit') {
|
|
299
|
+
const name = field(limit.langName);
|
|
300
|
+
lines.push(` let effective_${name} = params.${name}.unwrap_or(${limit.defaultSize});`, ` if !(1..=${limit.maxSize}).contains(&effective_${name}) {`, ' return Err(QueryError::Input {', ' code: "SYQL_RUNTIME_INVALID_LIMIT",', ' query: ID,', ` message: ${quote(`${query.name}: limit must be an integer from 1 through ${limit.maxSize}`)}.to_owned(),`, ' });', ' }');
|
|
301
|
+
}
|
|
302
|
+
if (metadata.plan.backend === 'variants') {
|
|
303
|
+
lines.push(' let mut activation_mask = 0usize;');
|
|
304
|
+
metadata.plan.activationControls.forEach((control, index) => {
|
|
305
|
+
lines.push(` if ${syqlControlActive(query, control)} {`, ` activation_mask |= ${2 ** index};`, ' }');
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
const sort = metadata.inputs.find((input) => input.kind === 'sort');
|
|
309
|
+
if (sort?.kind === 'sort') {
|
|
310
|
+
lines.push(` let sort_index = match params.${field(sort.langName)} {`);
|
|
311
|
+
sort.profiles.forEach((profile, index) => {
|
|
312
|
+
lines.push(` ${typeName(sort.langName)}::${typeName(profile.langName)} => ${index},`);
|
|
313
|
+
});
|
|
314
|
+
lines.push(' };');
|
|
315
|
+
}
|
|
316
|
+
const sortIndex = sort?.kind === 'sort' ? 'sort_index' : '0usize';
|
|
317
|
+
const profileCount = sort?.kind === 'sort' ? sort.profiles.length : 1;
|
|
318
|
+
const index = metadata.plan.backend === 'variants'
|
|
319
|
+
? profileCount === 1
|
|
320
|
+
? 'activation_mask'
|
|
321
|
+
: `activation_mask * ${profileCount} + ${sortIndex}`
|
|
322
|
+
: sortIndex;
|
|
323
|
+
lines.push(` let statement_index = ${index};`, ' match statement_index {');
|
|
324
|
+
metadata.plan.statements.forEach((statement, statementIndex) => {
|
|
325
|
+
const binds = statement.binds
|
|
326
|
+
.map((bind) => syqlBindExpr(query, bind))
|
|
327
|
+
.join(', ');
|
|
328
|
+
lines.push(` ${statementIndex} => Ok(SelectedQuery {`, ` sql: ${quote(statement.positionalSql)},`, ` params: vec![${binds}],`, ' }),');
|
|
329
|
+
});
|
|
330
|
+
lines.push(' _ => Err(QueryError::Input {', ' code: "SYQL_RUNTIME_INVALID_SORT",', ' query: ID,', ` message: ${quote(`${query.name}: invalid generated SYQL statement index`)}.to_owned(),`, ' }),', ' }', ' }');
|
|
331
|
+
return lines;
|
|
332
|
+
}
|
|
333
|
+
function reactiveParam(query, name) {
|
|
334
|
+
if (query.syql !== undefined) {
|
|
335
|
+
const input = query.syql.inputs.find((candidate) => candidate.kind === 'value' && candidate.name === name);
|
|
336
|
+
if (input?.kind !== 'value' || !input.required || input.nullable) {
|
|
337
|
+
throw new Error(`reactive input ${name} is not required and non-null`);
|
|
338
|
+
}
|
|
339
|
+
return scopeValue(input.type, `params.${field(input.langName)}`);
|
|
340
|
+
}
|
|
341
|
+
const param = query.params.find((candidate) => candidate.name === name);
|
|
342
|
+
if (param === undefined)
|
|
343
|
+
throw new Error(`unknown reactive param ${name}`);
|
|
344
|
+
return scopeValue(param.type, `params.${field(param.langName)}`);
|
|
345
|
+
}
|
|
346
|
+
function scopeValue(type, reference) {
|
|
347
|
+
switch (type) {
|
|
348
|
+
case 'string':
|
|
349
|
+
case 'json':
|
|
350
|
+
case 'blob_ref':
|
|
351
|
+
return `${reference}.clone()`;
|
|
352
|
+
case 'bytes':
|
|
353
|
+
case 'crdt':
|
|
354
|
+
return `encode_hex(&${reference})`;
|
|
355
|
+
default:
|
|
356
|
+
return `${reference}.to_string()`;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function scopeKey(query, pattern, variable, param) {
|
|
360
|
+
const marker = `{${variable}}`;
|
|
361
|
+
const index = pattern.indexOf(marker);
|
|
362
|
+
if (index < 0)
|
|
363
|
+
throw new Error(`scope pattern ${pattern} lacks ${marker}`);
|
|
364
|
+
const prefix = pattern.slice(0, index);
|
|
365
|
+
const suffix = pattern.slice(index + marker.length);
|
|
366
|
+
return `format!("{}{}{}", ${quote(prefix)}, ${reactiveParam(query, param)}, ${quote(suffix)})`;
|
|
367
|
+
}
|
|
368
|
+
function emitDependencies(query) {
|
|
369
|
+
const usesParams = query.reactive.dependencies.some((dependency) => dependency.scopes.some((scope) => scope.params.length > 0));
|
|
370
|
+
const lines = [
|
|
371
|
+
` pub fn dependencies(${usesParams ? 'params' : '_params'}: &Params) -> Vec<QueryDependency> {`,
|
|
372
|
+
' vec![',
|
|
373
|
+
];
|
|
374
|
+
for (const dependency of query.reactive.dependencies) {
|
|
375
|
+
const keys = dependency.scopes.flatMap((scope) => scope.params.map((param) => scopeKey(query, scope.pattern, scope.variable, param)));
|
|
376
|
+
lines.push(' QueryDependency {', ` table: ${quote(dependency.table)},`, keys.length === 0
|
|
377
|
+
? ' scope_keys: None,'
|
|
378
|
+
: ` scope_keys: Some(vec![${keys.join(', ')}]),`, ' },');
|
|
379
|
+
}
|
|
380
|
+
lines.push(' ]', ' }');
|
|
381
|
+
return lines;
|
|
382
|
+
}
|
|
383
|
+
function emitCoverage(query) {
|
|
384
|
+
const usesParams = query.reactive.coverage.some((coverage) => coverage.units.length > 0 ||
|
|
385
|
+
coverage.fixedScopes.some((scope) => scope.params.length > 0));
|
|
386
|
+
const lines = [
|
|
387
|
+
` pub fn coverage(${usesParams ? 'params' : '_params'}: &Params) -> Vec<WindowCoverage> {`,
|
|
388
|
+
' vec![',
|
|
389
|
+
];
|
|
390
|
+
for (const coverage of query.reactive.coverage) {
|
|
391
|
+
const fixed = coverage.fixedScopes.map((scope) => {
|
|
392
|
+
const values = scope.params
|
|
393
|
+
.map((param) => reactiveParam(query, param))
|
|
394
|
+
.join(', ');
|
|
395
|
+
return `(${quote(scope.variable)}.to_owned(), vec![${values}])`;
|
|
396
|
+
});
|
|
397
|
+
const units = coverage.units
|
|
398
|
+
.map((param) => reactiveParam(query, param))
|
|
399
|
+
.join(', ');
|
|
400
|
+
lines.push(' WindowCoverage {', ' base: WindowBase {', ` table: ${quote(coverage.table)}.to_owned(),`, ` variable: ${quote(coverage.variable)}.to_owned(),`, ` fixed_scopes: vec![${fixed.join(', ')}],`, ' params: None,', ' },', ` units: vec![${units}],`, ' },');
|
|
401
|
+
}
|
|
402
|
+
lines.push(' ]', ' }');
|
|
403
|
+
return lines;
|
|
404
|
+
}
|
|
405
|
+
function keyValue(column, reference) {
|
|
406
|
+
const value = (() => {
|
|
407
|
+
switch (column.type) {
|
|
408
|
+
case 'string':
|
|
409
|
+
case 'json':
|
|
410
|
+
case 'blob_ref':
|
|
411
|
+
return `bind_string(${reference})`;
|
|
412
|
+
case 'integer':
|
|
413
|
+
return `bind_integer(${reference})`;
|
|
414
|
+
case 'float':
|
|
415
|
+
return `QueryValue::from(*${reference})`;
|
|
416
|
+
case 'boolean':
|
|
417
|
+
return `bind_boolean(${reference})`;
|
|
418
|
+
case 'bytes':
|
|
419
|
+
case 'crdt':
|
|
420
|
+
return `bind_bytes(${reference})`;
|
|
421
|
+
}
|
|
422
|
+
})();
|
|
423
|
+
if (!column.nullable)
|
|
424
|
+
return value;
|
|
425
|
+
return `match ${reference} { Some(value) => ${keyValue({ ...column, nullable: false }, 'value')}, None => QueryValue::Null }`;
|
|
426
|
+
}
|
|
427
|
+
function emitRowKey(query) {
|
|
428
|
+
if (query.reactive.rowKey === undefined)
|
|
429
|
+
return [];
|
|
430
|
+
const byName = new Map(query.columns.map((column) => [column.langName, column]));
|
|
431
|
+
const values = query.reactive.rowKey.map((name) => {
|
|
432
|
+
const column = byName.get(name);
|
|
433
|
+
if (column === undefined)
|
|
434
|
+
throw new Error(`row key column ${name} missing`);
|
|
435
|
+
return keyValue(column, `&row.${field(column.langName)}`);
|
|
436
|
+
});
|
|
437
|
+
return [
|
|
438
|
+
' pub fn row_key(row: &Row) -> Vec<QueryValue> {',
|
|
439
|
+
` vec![${values.join(', ')}]`,
|
|
440
|
+
' }',
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
function emitDescriptorAndRunners(query, hash) {
|
|
444
|
+
const hasRowKey = query.reactive.rowKey !== undefined;
|
|
445
|
+
const noParams = query.syql === undefined
|
|
446
|
+
? query.params.length === 0
|
|
447
|
+
: query.syql.inputs.length === 0;
|
|
448
|
+
const lines = [
|
|
449
|
+
` pub const ID: &str = ${quote(queryId(query, hash))};`,
|
|
450
|
+
` pub const TABLES: &[&str] = &[${query.tables.map(quote).join(', ')}];`,
|
|
451
|
+
' pub const DESCRIPTOR: QueryDescriptor<Params, Row> = QueryDescriptor {',
|
|
452
|
+
' id: ID,',
|
|
453
|
+
' tables: TABLES,',
|
|
454
|
+
' select,',
|
|
455
|
+
' dependencies,',
|
|
456
|
+
' coverage,',
|
|
457
|
+
` row_key: ${hasRowKey ? 'Some(row_key)' : 'None'},`,
|
|
458
|
+
' };',
|
|
459
|
+
'',
|
|
460
|
+
];
|
|
461
|
+
lines.push(...emitSelect(query), '', ...emitDependencies(query), '', ...emitCoverage(query));
|
|
462
|
+
const rowKey = emitRowKey(query);
|
|
463
|
+
if (rowKey.length > 0)
|
|
464
|
+
lines.push('', ...rowKey);
|
|
465
|
+
lines.push('');
|
|
466
|
+
if (noParams) {
|
|
467
|
+
lines.push(' pub fn run(client: &SyncClient) -> Result<Vec<Row>, QueryError> {', ' run_with(client, &())', ' }', '', ' pub fn snapshot(', ' client: &mut SyncClient,', ' ) -> Result<TypedQuerySnapshot<Row>, QueryError> {', ' snapshot_with(client, &())', ' }', '', ' fn run_with(client: &SyncClient, params: &Params) -> Result<Vec<Row>, QueryError> {');
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
lines.push(' pub fn run(client: &SyncClient, params: &Params) -> Result<Vec<Row>, QueryError> {');
|
|
471
|
+
}
|
|
472
|
+
lines.push(' let selected = select(params)?;', ' let rows = client', ' .query(selected.sql, &selected.params)', ' .map_err(|message| QueryError::Client { query: ID, message })?;', ' rows.into_iter().map(decode).collect()', ' }', '');
|
|
473
|
+
if (noParams) {
|
|
474
|
+
lines.push(' fn snapshot_with(', ' client: &mut SyncClient,', ' params: &Params,', ' ) -> Result<TypedQuerySnapshot<Row>, QueryError> {');
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
lines.push(' pub fn snapshot(', ' client: &mut SyncClient,', ' params: &Params,', ' ) -> Result<TypedQuerySnapshot<Row>, QueryError> {');
|
|
478
|
+
}
|
|
479
|
+
lines.push(' let selected = select(params)?;', ' let required_coverage = coverage(params);', ' let snapshot = client', ' .query_snapshot(selected.sql, &selected.params, &required_coverage)', ' .map_err(|message| QueryError::Client { query: ID, message })?;', ' let rows = snapshot.rows.into_iter().map(decode).collect::<Result<_, _>>()?;', ' Ok(TypedQuerySnapshot {', ' revision: snapshot.revision,', ' rows,', ' coverage: snapshot.coverage,', ' })', ' }');
|
|
480
|
+
return lines;
|
|
481
|
+
}
|
|
482
|
+
function emitQuery(query, hash) {
|
|
483
|
+
validateQueryNames(query);
|
|
484
|
+
const lines = [
|
|
485
|
+
'#[rustfmt::skip]',
|
|
486
|
+
`pub mod ${field(query.name)} {`,
|
|
487
|
+
' use super::*;',
|
|
488
|
+
'',
|
|
489
|
+
];
|
|
490
|
+
lines.push(...emitRow(query), '', ...emitParams(query), '');
|
|
491
|
+
lines.push(...emitDescriptorAndRunners(query, hash), '}');
|
|
492
|
+
return lines.join('\n');
|
|
493
|
+
}
|
|
494
|
+
function emitSupport(clientCrate) {
|
|
495
|
+
return [
|
|
496
|
+
'use std::error::Error;',
|
|
497
|
+
'use std::fmt;',
|
|
498
|
+
`use ${clientCrate}::{`,
|
|
499
|
+
' CoverageSnapshot, QueryRow, QueryValue, SyncClient, WindowBase, WindowCoverage,',
|
|
500
|
+
'};',
|
|
501
|
+
'',
|
|
502
|
+
'#[derive(Debug, Clone, PartialEq, Eq, Default)]',
|
|
503
|
+
'pub enum SyqlPresence<T> {',
|
|
504
|
+
' #[default]',
|
|
505
|
+
' Absent,',
|
|
506
|
+
' Present(T),',
|
|
507
|
+
'}',
|
|
508
|
+
'',
|
|
509
|
+
'#[derive(Debug, Clone, PartialEq)]',
|
|
510
|
+
'pub struct SelectedQuery {',
|
|
511
|
+
" pub sql: &'static str,",
|
|
512
|
+
' pub params: Vec<QueryValue>,',
|
|
513
|
+
'}',
|
|
514
|
+
'',
|
|
515
|
+
'#[derive(Debug, Clone, PartialEq, Eq)]',
|
|
516
|
+
'pub struct QueryDependency {',
|
|
517
|
+
" pub table: &'static str,",
|
|
518
|
+
' pub scope_keys: Option<Vec<String>>,',
|
|
519
|
+
'}',
|
|
520
|
+
'',
|
|
521
|
+
'#[derive(Debug, Clone)]',
|
|
522
|
+
'pub struct TypedQuerySnapshot<Row> {',
|
|
523
|
+
' pub revision: String,',
|
|
524
|
+
' pub rows: Vec<Row>,',
|
|
525
|
+
' pub coverage: CoverageSnapshot,',
|
|
526
|
+
'}',
|
|
527
|
+
'',
|
|
528
|
+
'#[derive(Clone, Copy)]',
|
|
529
|
+
'pub struct QueryDescriptor<Params, Row> {',
|
|
530
|
+
" pub id: &'static str,",
|
|
531
|
+
" pub tables: &'static [&'static str],",
|
|
532
|
+
' pub select: fn(&Params) -> Result<SelectedQuery, QueryError>,',
|
|
533
|
+
' pub dependencies: fn(&Params) -> Vec<QueryDependency>,',
|
|
534
|
+
' pub coverage: fn(&Params) -> Vec<WindowCoverage>,',
|
|
535
|
+
' pub row_key: Option<fn(&Row) -> Vec<QueryValue>>,',
|
|
536
|
+
'}',
|
|
537
|
+
'',
|
|
538
|
+
'#[derive(Debug, Clone, PartialEq, Eq)]',
|
|
539
|
+
'pub enum QueryError {',
|
|
540
|
+
' Input {',
|
|
541
|
+
" code: &'static str,",
|
|
542
|
+
" query: &'static str,",
|
|
543
|
+
' message: String,',
|
|
544
|
+
' },',
|
|
545
|
+
' Client {',
|
|
546
|
+
" query: &'static str,",
|
|
547
|
+
' message: String,',
|
|
548
|
+
' },',
|
|
549
|
+
' Decode {',
|
|
550
|
+
" query: &'static str,",
|
|
551
|
+
" column: &'static str,",
|
|
552
|
+
" expected: &'static str,",
|
|
553
|
+
" reason: &'static str,",
|
|
554
|
+
' },',
|
|
555
|
+
'}',
|
|
556
|
+
'',
|
|
557
|
+
'impl fmt::Display for QueryError {',
|
|
558
|
+
" fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {",
|
|
559
|
+
' match self {',
|
|
560
|
+
' Self::Input {',
|
|
561
|
+
' code,',
|
|
562
|
+
' query,',
|
|
563
|
+
' message,',
|
|
564
|
+
' } => write!(formatter, "{code} ({query}): {message}"),',
|
|
565
|
+
' Self::Client { query, message } => {',
|
|
566
|
+
' write!(formatter, "query {query} failed: {message}")',
|
|
567
|
+
' }',
|
|
568
|
+
' Self::Decode {',
|
|
569
|
+
' query,',
|
|
570
|
+
' column,',
|
|
571
|
+
' expected,',
|
|
572
|
+
' reason,',
|
|
573
|
+
' } => write!(',
|
|
574
|
+
' formatter,',
|
|
575
|
+
' "query {query} column {column} expected {expected}: {reason}"',
|
|
576
|
+
' ),',
|
|
577
|
+
' }',
|
|
578
|
+
' }',
|
|
579
|
+
'}',
|
|
580
|
+
'',
|
|
581
|
+
'impl Error for QueryError {}',
|
|
582
|
+
'',
|
|
583
|
+
'fn decode_error(',
|
|
584
|
+
" query: &'static str,",
|
|
585
|
+
" column: &'static str,",
|
|
586
|
+
" expected: &'static str,",
|
|
587
|
+
" reason: &'static str,",
|
|
588
|
+
') -> QueryError {',
|
|
589
|
+
' QueryError::Decode {',
|
|
590
|
+
' query,',
|
|
591
|
+
' column,',
|
|
592
|
+
' expected,',
|
|
593
|
+
' reason,',
|
|
594
|
+
' }',
|
|
595
|
+
'}',
|
|
596
|
+
'',
|
|
597
|
+
"fn required_value<'a>(",
|
|
598
|
+
" row: &'a QueryRow,",
|
|
599
|
+
" query: &'static str,",
|
|
600
|
+
" column: &'static str,",
|
|
601
|
+
") -> Result<&'a QueryValue, QueryError> {",
|
|
602
|
+
' row.get(column)',
|
|
603
|
+
' .ok_or_else(|| decode_error(query, column, "projected value", "missing column"))',
|
|
604
|
+
'}',
|
|
605
|
+
'',
|
|
606
|
+
'fn decode_string(',
|
|
607
|
+
' value: &QueryValue,',
|
|
608
|
+
" query: &'static str,",
|
|
609
|
+
" column: &'static str,",
|
|
610
|
+
') -> Result<String, QueryError> {',
|
|
611
|
+
' value',
|
|
612
|
+
' .as_str()',
|
|
613
|
+
' .map(str::to_owned)',
|
|
614
|
+
' .ok_or_else(|| decode_error(query, column, "string", "wrong dynamic type"))',
|
|
615
|
+
'}',
|
|
616
|
+
'',
|
|
617
|
+
'fn decode_integer(',
|
|
618
|
+
' value: &QueryValue,',
|
|
619
|
+
" query: &'static str,",
|
|
620
|
+
" column: &'static str,",
|
|
621
|
+
') -> Result<i64, QueryError> {',
|
|
622
|
+
' if let Some(integer) = value.as_i64() {',
|
|
623
|
+
' return Ok(integer);',
|
|
624
|
+
' }',
|
|
625
|
+
' if let Some(decimal) = value.get("$bigint").and_then(QueryValue::as_str) {',
|
|
626
|
+
' return decimal',
|
|
627
|
+
' .parse::<i64>()',
|
|
628
|
+
' .map_err(|_| decode_error(query, column, "integer", "invalid $bigint envelope"));',
|
|
629
|
+
' }',
|
|
630
|
+
' Err(decode_error(query, column, "integer", "wrong dynamic type"))',
|
|
631
|
+
'}',
|
|
632
|
+
'',
|
|
633
|
+
'fn decode_float(',
|
|
634
|
+
' value: &QueryValue,',
|
|
635
|
+
" query: &'static str,",
|
|
636
|
+
" column: &'static str,",
|
|
637
|
+
') -> Result<f64, QueryError> {',
|
|
638
|
+
' value',
|
|
639
|
+
' .as_f64()',
|
|
640
|
+
' .filter(|number| number.is_finite())',
|
|
641
|
+
' .ok_or_else(|| decode_error(query, column, "finite float", "wrong dynamic type"))',
|
|
642
|
+
'}',
|
|
643
|
+
'',
|
|
644
|
+
'fn decode_boolean(',
|
|
645
|
+
' value: &QueryValue,',
|
|
646
|
+
" query: &'static str,",
|
|
647
|
+
" column: &'static str,",
|
|
648
|
+
') -> Result<bool, QueryError> {',
|
|
649
|
+
' if let Some(boolean) = value.as_bool() {',
|
|
650
|
+
' return Ok(boolean);',
|
|
651
|
+
' }',
|
|
652
|
+
' if let Some(number) = value.as_f64().filter(|number| number.is_finite()) {',
|
|
653
|
+
' return Ok(number != 0.0);',
|
|
654
|
+
' }',
|
|
655
|
+
' Err(decode_error(query, column, "boolean", "wrong dynamic type"))',
|
|
656
|
+
'}',
|
|
657
|
+
'',
|
|
658
|
+
'fn decode_bytes(',
|
|
659
|
+
' value: &QueryValue,',
|
|
660
|
+
" query: &'static str,",
|
|
661
|
+
" column: &'static str,",
|
|
662
|
+
') -> Result<Vec<u8>, QueryError> {',
|
|
663
|
+
' let hex = value',
|
|
664
|
+
' .get("$bytes")',
|
|
665
|
+
' .and_then(QueryValue::as_str)',
|
|
666
|
+
' .ok_or_else(|| decode_error(query, column, "bytes", "missing $bytes envelope"))?;',
|
|
667
|
+
' if hex.len() % 2 != 0 {',
|
|
668
|
+
' return Err(decode_error(',
|
|
669
|
+
' query,',
|
|
670
|
+
' column,',
|
|
671
|
+
' "bytes",',
|
|
672
|
+
' "odd-length $bytes envelope",',
|
|
673
|
+
' ));',
|
|
674
|
+
' }',
|
|
675
|
+
' hex.as_bytes()',
|
|
676
|
+
' .chunks_exact(2)',
|
|
677
|
+
' .map(|pair| {',
|
|
678
|
+
' let pair = std::str::from_utf8(pair)',
|
|
679
|
+
' .map_err(|_| decode_error(query, column, "bytes", "non-ASCII $bytes envelope"))?;',
|
|
680
|
+
' u8::from_str_radix(pair, 16).map_err(|_| {',
|
|
681
|
+
' decode_error(',
|
|
682
|
+
' query,',
|
|
683
|
+
' column,',
|
|
684
|
+
' "bytes",',
|
|
685
|
+
' "invalid hexadecimal $bytes envelope",',
|
|
686
|
+
' )',
|
|
687
|
+
' })',
|
|
688
|
+
' })',
|
|
689
|
+
' .collect()',
|
|
690
|
+
'}',
|
|
691
|
+
'',
|
|
692
|
+
'fn bind_string(value: &str) -> QueryValue {',
|
|
693
|
+
' QueryValue::String(value.to_owned())',
|
|
694
|
+
'}',
|
|
695
|
+
'',
|
|
696
|
+
'fn bind_integer(value: &i64) -> QueryValue {',
|
|
697
|
+
' QueryValue::from(*value)',
|
|
698
|
+
'}',
|
|
699
|
+
'',
|
|
700
|
+
'fn bind_float(',
|
|
701
|
+
' value: &f64,',
|
|
702
|
+
" query: &'static str,",
|
|
703
|
+
" input: &'static str,",
|
|
704
|
+
') -> Result<QueryValue, QueryError> {',
|
|
705
|
+
' if !value.is_finite() {',
|
|
706
|
+
' return Err(QueryError::Input {',
|
|
707
|
+
' code: "SYQL_RUNTIME_INVALID_INPUT",',
|
|
708
|
+
' query,',
|
|
709
|
+
' message: format!("{query}: input {input} must be finite"),',
|
|
710
|
+
' });',
|
|
711
|
+
' }',
|
|
712
|
+
' Ok(QueryValue::from(*value))',
|
|
713
|
+
'}',
|
|
714
|
+
'',
|
|
715
|
+
'fn bind_boolean(value: &bool) -> QueryValue {',
|
|
716
|
+
' QueryValue::Bool(*value)',
|
|
717
|
+
'}',
|
|
718
|
+
'',
|
|
719
|
+
'fn encode_hex(value: &[u8]) -> String {',
|
|
720
|
+
' const HEX: &[u8; 16] = b"0123456789abcdef";',
|
|
721
|
+
' let mut out = String::with_capacity(value.len() * 2);',
|
|
722
|
+
' for byte in value {',
|
|
723
|
+
' out.push(HEX[(byte >> 4) as usize] as char);',
|
|
724
|
+
' out.push(HEX[(byte & 0x0f) as usize] as char);',
|
|
725
|
+
' }',
|
|
726
|
+
' out',
|
|
727
|
+
'}',
|
|
728
|
+
'',
|
|
729
|
+
'fn bind_bytes(value: &[u8]) -> QueryValue {',
|
|
730
|
+
' let mut envelope = QueryRow::new();',
|
|
731
|
+
' envelope.insert("$bytes".to_owned(), QueryValue::String(encode_hex(value)));',
|
|
732
|
+
' QueryValue::Object(envelope)',
|
|
733
|
+
'}',
|
|
734
|
+
].join('\n');
|
|
735
|
+
}
|
|
736
|
+
export function emitQueriesRustModule(queries, hash, irVersion, clientCrate = 'syncular_client') {
|
|
737
|
+
buildRustNamingMap(queries.map((query) => query.name), 'queries', 'query modules');
|
|
738
|
+
const parts = [
|
|
739
|
+
[
|
|
740
|
+
'// Generated by @syncular/typegen — DO NOT EDIT.',
|
|
741
|
+
`// irVersion: ${irVersion}`,
|
|
742
|
+
`// irHash: ${hash}`,
|
|
743
|
+
].join('\n'),
|
|
744
|
+
emitSupport(clientCrate),
|
|
745
|
+
...queries.map((query) => emitQuery(query, hash)),
|
|
746
|
+
];
|
|
747
|
+
return `${parts.join('\n\n')}\n`;
|
|
748
|
+
}
|