@syncular/typegen 0.6.0 → 0.8.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syncular/typegen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Syncular schema-to-TypeScript type generator and the `syncular` CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Benjamin Kniffler",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"!dist/**/*.test.d.ts"
|
|
49
49
|
],
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@syncular/core": "0.
|
|
52
|
-
"@syncular/server": "0.
|
|
51
|
+
"@syncular/core": "0.8.0",
|
|
52
|
+
"@syncular/server": "0.8.0"
|
|
53
53
|
}
|
|
54
54
|
}
|
package/src/cli.ts
CHANGED
|
@@ -131,15 +131,13 @@ function runGeneratePrint(args: GenerateArgs, name: string): void {
|
|
|
131
131
|
console.log(
|
|
132
132
|
`-- ${input.name}: optional group (${input.members.map((member) => `${member.name}: ${member.type}${member.nullable ? ' | null' : ''}`).join(', ')})`,
|
|
133
133
|
);
|
|
134
|
-
} else if (input.kind === 'switch') {
|
|
135
|
-
console.log(`-- ${input.name}: switch (default false)`);
|
|
136
134
|
} else if (input.kind === 'sort') {
|
|
137
135
|
console.log(
|
|
138
136
|
`-- ${input.name}: sort [${input.profiles.map((profile) => profile.name).join(', ')}] (default ${input.defaultProfile})`,
|
|
139
137
|
);
|
|
140
138
|
} else {
|
|
141
139
|
console.log(
|
|
142
|
-
`-- ${input.name}:
|
|
140
|
+
`-- ${input.name}: limit 1..${input.maxSize} (default ${input.defaultSize})`,
|
|
143
141
|
);
|
|
144
142
|
}
|
|
145
143
|
}
|
package/src/emit-queries-dart.ts
CHANGED
|
@@ -103,7 +103,7 @@ function syqlDartType(type: IrColumnType, nullable: boolean): string {
|
|
|
103
103
|
function syqlControlActive(query: AnalyzedQuery, name: string): string {
|
|
104
104
|
const input = syqlInput(query, name);
|
|
105
105
|
const access = camelCase(input.langName);
|
|
106
|
-
if (input.kind === '
|
|
106
|
+
if (input.kind === 'value' && input.default === false) return access;
|
|
107
107
|
if (input.kind === 'value' && input.nullable) return `${access}.isPresent`;
|
|
108
108
|
if (input.kind === 'value' || input.kind === 'group') {
|
|
109
109
|
return `${access} != null`;
|
|
@@ -119,7 +119,7 @@ function syqlBindExpr(query: AnalyzedQuery, bind: QuerySyqlPlanBind): string {
|
|
|
119
119
|
}
|
|
120
120
|
const input = syqlInput(query, bind.input);
|
|
121
121
|
const access = camelCase(input.langName);
|
|
122
|
-
if (bind.kind === '
|
|
122
|
+
if (bind.kind === 'limit') return `effective${typeName(access)}`;
|
|
123
123
|
if (bind.kind === 'group-member') {
|
|
124
124
|
if (input.kind !== 'group') throw new Error('group bind/input mismatch');
|
|
125
125
|
const member = input.members.find(
|
|
@@ -134,6 +134,7 @@ function syqlBindExpr(query: AnalyzedQuery, bind: QuerySyqlPlanBind): string {
|
|
|
134
134
|
return `${access} == null ? null : ${value}`;
|
|
135
135
|
}
|
|
136
136
|
if (input.kind !== 'value') throw new Error('value bind/input mismatch');
|
|
137
|
+
if (input.default === false) return paramValue(input.type, access);
|
|
137
138
|
if (input.required) {
|
|
138
139
|
return input.nullable
|
|
139
140
|
? optionalParamValue(input.type, access)
|
|
@@ -189,6 +190,7 @@ function emitSyqlDartRunner(query: AnalyzedQuery): string[] {
|
|
|
189
190
|
if (input.kind === 'value') {
|
|
190
191
|
const type = syqlDartType(input.type, input.nullable);
|
|
191
192
|
if (input.required) args.push(`required ${type} ${name}`);
|
|
193
|
+
else if (input.default === false) args.push(`bool ${name} = false`);
|
|
192
194
|
else if (input.nullable) {
|
|
193
195
|
args.push(
|
|
194
196
|
`SyqlQueryPresence<${type}> ${name} = const SyqlQueryPresence.absent()`,
|
|
@@ -196,8 +198,6 @@ function emitSyqlDartRunner(query: AnalyzedQuery): string[] {
|
|
|
196
198
|
} else args.push(`${type}? ${name}`);
|
|
197
199
|
} else if (input.kind === 'group') {
|
|
198
200
|
args.push(`${typeName(query.name)}${typeName(input.langName)}? ${name}`);
|
|
199
|
-
} else if (input.kind === 'switch') {
|
|
200
|
-
args.push(`bool ${name} = false`);
|
|
201
201
|
} else if (input.kind === 'sort') {
|
|
202
202
|
const defaultCase =
|
|
203
203
|
input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
@@ -212,13 +212,13 @@ function emitSyqlDartRunner(query: AnalyzedQuery): string[] {
|
|
|
212
212
|
lines.push(
|
|
213
213
|
`List<${Row}> syncular${Pascal}Query(SyncularClient client${argsClause}) {`,
|
|
214
214
|
);
|
|
215
|
-
const page = metadata.inputs.find((input) => input.kind === '
|
|
216
|
-
if (page?.kind === '
|
|
215
|
+
const page = metadata.inputs.find((input) => input.kind === 'limit');
|
|
216
|
+
if (page?.kind === 'limit') {
|
|
217
217
|
const name = camelCase(page.langName);
|
|
218
218
|
lines.push(
|
|
219
219
|
` final effective${typeName(name)} = ${name} ?? ${page.defaultSize};`,
|
|
220
220
|
` if (effective${typeName(name)} < 1 || effective${typeName(name)} > ${page.maxSize}) {`,
|
|
221
|
-
` throw SyqlQueryInputException('
|
|
221
|
+
` throw SyqlQueryInputException('SYQL_RUNTIME_INVALID_LIMIT', ${quote(`${query.name}: invalid limit`)});`,
|
|
222
222
|
' }',
|
|
223
223
|
);
|
|
224
224
|
}
|
|
@@ -107,7 +107,7 @@ function syqlKotlinType(type: IrColumnType, nullable: boolean): string {
|
|
|
107
107
|
function syqlControlActive(query: AnalyzedQuery, name: string): string {
|
|
108
108
|
const input = syqlInput(query, name);
|
|
109
109
|
const access = camelCase(input.langName);
|
|
110
|
-
if (input.kind === '
|
|
110
|
+
if (input.kind === 'value' && input.default === false) return access;
|
|
111
111
|
if (input.kind === 'value' && input.nullable) {
|
|
112
112
|
return `${access} is SyncularQueryPresence.Present`;
|
|
113
113
|
}
|
|
@@ -130,7 +130,7 @@ function syqlBindExpr(query: AnalyzedQuery, bind: QuerySyqlPlanBind): string {
|
|
|
130
130
|
}
|
|
131
131
|
const input = syqlInput(query, bind.input);
|
|
132
132
|
const access = camelCase(input.langName);
|
|
133
|
-
if (bind.kind === '
|
|
133
|
+
if (bind.kind === 'limit')
|
|
134
134
|
return `JsonValue.of(effective${typeName(access)}.toDouble())`;
|
|
135
135
|
if (bind.kind === 'group-member') {
|
|
136
136
|
if (input.kind !== 'group') throw new Error('group bind/input mismatch');
|
|
@@ -146,6 +146,7 @@ function syqlBindExpr(query: AnalyzedQuery, bind: QuerySyqlPlanBind): string {
|
|
|
146
146
|
return `${access}?.let { value -> ${present} } ?: JsonValue.Null`;
|
|
147
147
|
}
|
|
148
148
|
if (input.kind !== 'value') throw new Error('value bind/input mismatch');
|
|
149
|
+
if (input.default === false) return paramValue(input.type, access);
|
|
149
150
|
if (input.required) {
|
|
150
151
|
return input.nullable
|
|
151
152
|
? optionalParamValue(input.type, access)
|
|
@@ -201,6 +202,7 @@ function emitSyqlKotlinRunner(query: AnalyzedQuery): string[] {
|
|
|
201
202
|
if (input.kind === 'value') {
|
|
202
203
|
const type = syqlKotlinType(input.type, input.nullable);
|
|
203
204
|
if (input.required) args.push(`${name}: ${type}`);
|
|
205
|
+
else if (input.default === false) args.push(`${name}: Boolean = false`);
|
|
204
206
|
else if (input.nullable) {
|
|
205
207
|
args.push(
|
|
206
208
|
`${name}: SyncularQueryPresence<${type}> = SyncularQueryPresence.Absent`,
|
|
@@ -210,8 +212,6 @@ function emitSyqlKotlinRunner(query: AnalyzedQuery): string[] {
|
|
|
210
212
|
args.push(
|
|
211
213
|
`${name}: ${typeName(query.name)}${typeName(input.langName)}? = null`,
|
|
212
214
|
);
|
|
213
|
-
} else if (input.kind === 'switch') {
|
|
214
|
-
args.push(`${name}: Boolean = false`);
|
|
215
215
|
} else if (input.kind === 'sort') {
|
|
216
216
|
const defaultCase =
|
|
217
217
|
input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
@@ -223,13 +223,13 @@ function emitSyqlKotlinRunner(query: AnalyzedQuery): string[] {
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
lines.push(` fun ${query.name}(${args.join(', ')}): List<${Row}> {`);
|
|
226
|
-
const page = metadata.inputs.find((input) => input.kind === '
|
|
227
|
-
if (page?.kind === '
|
|
226
|
+
const page = metadata.inputs.find((input) => input.kind === 'limit');
|
|
227
|
+
if (page?.kind === 'limit') {
|
|
228
228
|
const name = camelCase(page.langName);
|
|
229
229
|
lines.push(
|
|
230
230
|
` val effective${typeName(name)} = ${name} ?: ${page.defaultSize}L`,
|
|
231
231
|
` if (effective${typeName(name)} < 1L || effective${typeName(name)} > ${page.maxSize}L) {`,
|
|
232
|
-
` throw SyncularQueryInputException("
|
|
232
|
+
` throw SyncularQueryInputException("SYQL_RUNTIME_INVALID_LIMIT", ${quote(`${query.name}: invalid limit`)})`,
|
|
233
233
|
' }',
|
|
234
234
|
);
|
|
235
235
|
}
|
|
@@ -115,7 +115,7 @@ function syqlSwiftType(type: IrColumnType, nullable: boolean): string {
|
|
|
115
115
|
function syqlControlActive(query: AnalyzedQuery, name: string): string {
|
|
116
116
|
const input = syqlInput(query, name);
|
|
117
117
|
const access = camelCase(input.langName);
|
|
118
|
-
if (input.kind === '
|
|
118
|
+
if (input.kind === 'value' && input.default === false) return access;
|
|
119
119
|
if (input.kind === 'value' && input.nullable) {
|
|
120
120
|
return `{ if case .present = ${access} { return true }; return false }()`;
|
|
121
121
|
}
|
|
@@ -138,7 +138,7 @@ function syqlBindExpr(query: AnalyzedQuery, bind: QuerySyqlPlanBind): string {
|
|
|
138
138
|
}
|
|
139
139
|
const input = syqlInput(query, bind.input);
|
|
140
140
|
const access = camelCase(input.langName);
|
|
141
|
-
if (bind.kind === '
|
|
141
|
+
if (bind.kind === 'limit')
|
|
142
142
|
return `.number(Double(effective${typeName(access)}))`;
|
|
143
143
|
if (bind.kind === 'group-member') {
|
|
144
144
|
if (input.kind !== 'group') throw new Error('group bind/input mismatch');
|
|
@@ -154,6 +154,7 @@ function syqlBindExpr(query: AnalyzedQuery, bind: QuerySyqlPlanBind): string {
|
|
|
154
154
|
return `${access}.map { value in ${present} } ?? .null`;
|
|
155
155
|
}
|
|
156
156
|
if (input.kind !== 'value') throw new Error('value bind/input mismatch');
|
|
157
|
+
if (input.default === false) return paramValue(input.type, access);
|
|
157
158
|
if (input.required) {
|
|
158
159
|
return input.nullable
|
|
159
160
|
? optionalParamValue(input.type, access)
|
|
@@ -216,6 +217,7 @@ function emitSyqlSwiftRunner(query: AnalyzedQuery): string[] {
|
|
|
216
217
|
if (input.kind === 'value') {
|
|
217
218
|
const type = syqlSwiftType(input.type, input.nullable);
|
|
218
219
|
if (input.required) args.push(`${name}: ${type}`);
|
|
220
|
+
else if (input.default === false) args.push(`${name}: Bool = false`);
|
|
219
221
|
else if (input.nullable)
|
|
220
222
|
args.push(`${name}: SyncularQueryPresence<${type}> = .absent`);
|
|
221
223
|
else args.push(`${name}: ${type}? = nil`);
|
|
@@ -223,8 +225,6 @@ function emitSyqlSwiftRunner(query: AnalyzedQuery): string[] {
|
|
|
223
225
|
args.push(
|
|
224
226
|
`${name}: ${typeName(query.name)}${typeName(input.langName)}? = nil`,
|
|
225
227
|
);
|
|
226
|
-
} else if (input.kind === 'switch') {
|
|
227
|
-
args.push(`${name}: Bool = false`);
|
|
228
228
|
} else if (input.kind === 'sort') {
|
|
229
229
|
const defaultCase =
|
|
230
230
|
input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
@@ -239,13 +239,13 @@ function emitSyqlSwiftRunner(query: AnalyzedQuery): string[] {
|
|
|
239
239
|
lines.push(
|
|
240
240
|
` public static func ${query.name}(${args.join(', ')}) throws -> [${Row}] {`,
|
|
241
241
|
);
|
|
242
|
-
const page = metadata.inputs.find((input) => input.kind === '
|
|
243
|
-
if (page?.kind === '
|
|
242
|
+
const page = metadata.inputs.find((input) => input.kind === 'limit');
|
|
243
|
+
if (page?.kind === 'limit') {
|
|
244
244
|
const name = camelCase(page.langName);
|
|
245
245
|
lines.push(
|
|
246
246
|
` let effective${typeName(name)} = ${name} ?? ${page.defaultSize}`,
|
|
247
247
|
` guard effective${typeName(name)} >= 1 && effective${typeName(name)} <= ${page.maxSize} else {`,
|
|
248
|
-
` throw SyncularQueryInputError(code: "
|
|
248
|
+
` throw SyncularQueryInputError(code: "SYQL_RUNTIME_INVALID_LIMIT", message: ${quote(`${query.name}: invalid limit`)})`,
|
|
249
249
|
' }',
|
|
250
250
|
);
|
|
251
251
|
}
|
package/src/emit-queries.ts
CHANGED
|
@@ -67,7 +67,8 @@ function reactiveParam(query: AnalyzedQuery, name: string): string {
|
|
|
67
67
|
);
|
|
68
68
|
if (input?.kind !== 'value')
|
|
69
69
|
throw new Error(`unknown reactive input ${name}`);
|
|
70
|
-
|
|
70
|
+
const access = `params.${propertyKey(input.langName)}`;
|
|
71
|
+
return `String(${input.default === false ? `${access} ?? false` : access})`;
|
|
71
72
|
}
|
|
72
73
|
const param = query.params.find((candidate) => candidate.name === name);
|
|
73
74
|
if (param === undefined) throw new Error(`unknown reactive param ${name}`);
|
|
@@ -117,7 +118,8 @@ function syqlControlActive(
|
|
|
117
118
|
): string {
|
|
118
119
|
const input = syqlInput(query, control);
|
|
119
120
|
const access = `${params}.${propertyKey(input.langName)}`;
|
|
120
|
-
if (input.kind === '
|
|
121
|
+
if (input.kind === 'value' && input.default === false)
|
|
122
|
+
return `${access} === true`;
|
|
121
123
|
if (input.kind === 'value' || input.kind === 'group') {
|
|
122
124
|
return `${access} !== undefined`;
|
|
123
125
|
}
|
|
@@ -136,8 +138,8 @@ function syqlBindExpr(
|
|
|
136
138
|
}
|
|
137
139
|
const input = syqlInput(query, bind.input);
|
|
138
140
|
const access = `${params}.${propertyKey(input.langName)}`;
|
|
139
|
-
if (bind.kind === '
|
|
140
|
-
if (input.kind !== '
|
|
141
|
+
if (bind.kind === 'limit') {
|
|
142
|
+
if (input.kind !== 'limit') throw new Error('limit bind/input mismatch');
|
|
141
143
|
return `${access} ?? ${input.defaultSize}`;
|
|
142
144
|
}
|
|
143
145
|
if (bind.kind === 'group-member') {
|
|
@@ -150,6 +152,7 @@ function syqlBindExpr(
|
|
|
150
152
|
return `${access}?.${propertyKey(member.langName)} ?? null`;
|
|
151
153
|
}
|
|
152
154
|
if (input.kind !== 'value') throw new Error('value bind/input mismatch');
|
|
155
|
+
if (input.default === false) return `${access} ?? false`;
|
|
153
156
|
if (input.required) return access;
|
|
154
157
|
return input.nullable ? `${access}?.value ?? null` : `${access} ?? null`;
|
|
155
158
|
}
|
|
@@ -197,17 +200,13 @@ function emitSyqlValidation(query: AnalyzedQuery, Params: string): string[] {
|
|
|
197
200
|
);
|
|
198
201
|
}
|
|
199
202
|
lines.push(' }');
|
|
200
|
-
} else if (input.kind === 'switch') {
|
|
201
|
-
lines.push(
|
|
202
|
-
` if (${access} !== undefined && typeof ${access} !== 'boolean') throw new SyqlInputError('SYQL_RUNTIME_INVALID_INPUT', ${quote(`${query.name}: invalid switch ${input.name}`)});`,
|
|
203
|
-
);
|
|
204
203
|
} else if (input.kind === 'sort') {
|
|
205
204
|
lines.push(
|
|
206
205
|
` if (${access} !== undefined && ![${input.profiles.map((profile) => quote(profile.langName)).join(', ')}].includes(${access})) throw new SyqlInputError('SYQL_RUNTIME_INVALID_SORT', ${quote(`${query.name}: invalid sort profile`)});`,
|
|
207
206
|
);
|
|
208
207
|
} else {
|
|
209
208
|
lines.push(
|
|
210
|
-
` if (${access} !== undefined && (!Number.isSafeInteger(${access}) || ${access} < 1 || ${access} > ${input.maxSize})) throw new SyqlInputError('
|
|
209
|
+
` if (${access} !== undefined && (!Number.isSafeInteger(${access}) || ${access} < 1 || ${access} > ${input.maxSize})) throw new SyqlInputError('SYQL_RUNTIME_INVALID_LIMIT', ${quote(`${query.name}: limit must be an integer from 1 through ${input.maxSize}`)});`,
|
|
211
210
|
);
|
|
212
211
|
}
|
|
213
212
|
}
|
|
@@ -267,8 +266,6 @@ function emitSyqlQuery(query: AnalyzedQuery, hash: string): string {
|
|
|
267
266
|
lines.push(
|
|
268
267
|
` ${key}?: ${pascalCase(query.name)}${pascalCase(input.langName)};`,
|
|
269
268
|
);
|
|
270
|
-
} else if (input.kind === 'switch') {
|
|
271
|
-
lines.push(` ${key}?: boolean;`);
|
|
272
269
|
} else if (input.kind === 'sort') {
|
|
273
270
|
lines.push(
|
|
274
271
|
` ${key}?: ${input.profiles.map((profile) => quote(profile.langName)).join(' | ')};`,
|
|
@@ -580,7 +577,7 @@ export function emitQueriesModule(
|
|
|
580
577
|
" | 'SYQL_RUNTIME_INVALID_INPUT'",
|
|
581
578
|
" | 'SYQL_RUNTIME_INVALID_GROUP'",
|
|
582
579
|
" | 'SYQL_RUNTIME_INVALID_SORT'",
|
|
583
|
-
" | '
|
|
580
|
+
" | 'SYQL_RUNTIME_INVALID_LIMIT';",
|
|
584
581
|
'',
|
|
585
582
|
'export class SyqlInputError extends Error {',
|
|
586
583
|
" readonly name = 'SyqlInputError';",
|
package/src/fmt.ts
CHANGED
|
@@ -53,7 +53,7 @@ function templates(file: SyqlSyntaxFile): readonly SyqlTemplate[] {
|
|
|
53
53
|
return file.declarations.flatMap((declaration) => {
|
|
54
54
|
if (declaration.kind === 'predicate') return [declaration.body];
|
|
55
55
|
return [
|
|
56
|
-
declaration.
|
|
56
|
+
declaration.statement,
|
|
57
57
|
...(declaration.sort?.profiles.map((profile) => profile.order) ?? []),
|
|
58
58
|
];
|
|
59
59
|
});
|
|
@@ -268,6 +268,8 @@ function formatTokens(parsed: SyqlSyntaxFile): string {
|
|
|
268
268
|
let justClosedImportList = false;
|
|
269
269
|
let seenTopLevel = false;
|
|
270
270
|
let pendingBetween = 0;
|
|
271
|
+
let inlineParameterRecord = false;
|
|
272
|
+
let justClosedInlineParameterRecord = false;
|
|
271
273
|
|
|
272
274
|
let declarationParameters: DeclarationParameters | undefined;
|
|
273
275
|
let awaitsDeclarationParameters = false;
|
|
@@ -279,15 +281,26 @@ function formatTokens(parsed: SyqlSyntaxFile): string {
|
|
|
279
281
|
const topLevelDeclaration =
|
|
280
282
|
braceDepth === 0 &&
|
|
281
283
|
token.kind === 'identifier' &&
|
|
282
|
-
(lower === 'import' ||
|
|
284
|
+
(lower === 'import' ||
|
|
285
|
+
lower === 'predicate' ||
|
|
286
|
+
lower === 'query' ||
|
|
287
|
+
lower === 'sync') &&
|
|
288
|
+
!(lower === 'query' && previous?.text === 'sync');
|
|
283
289
|
if (topLevelDeclaration) {
|
|
284
290
|
if (seenTopLevel) writer.blankline();
|
|
285
291
|
seenTopLevel = true;
|
|
286
292
|
previous = undefined;
|
|
287
293
|
awaitsDeclarationParameters = lower === 'predicate' || lower === 'query';
|
|
294
|
+
} else if (
|
|
295
|
+
braceDepth === 0 &&
|
|
296
|
+
lower === 'query' &&
|
|
297
|
+
previous?.text === 'sync'
|
|
298
|
+
) {
|
|
299
|
+
awaitsDeclarationParameters = true;
|
|
288
300
|
} else if (
|
|
289
301
|
previous?.text === '}' &&
|
|
290
302
|
!justClosedImportList &&
|
|
303
|
+
!justClosedInlineParameterRecord &&
|
|
291
304
|
token.text !== ';' &&
|
|
292
305
|
token.text !== ','
|
|
293
306
|
) {
|
|
@@ -326,6 +339,12 @@ function formatTokens(parsed: SyqlSyntaxFile): string {
|
|
|
326
339
|
if (previous?.kind === 'identifier' && previous.text === 'import') {
|
|
327
340
|
writer.write('{', true);
|
|
328
341
|
importList = true;
|
|
342
|
+
} else if (
|
|
343
|
+
declarationParameters !== undefined &&
|
|
344
|
+
previous?.text === ':'
|
|
345
|
+
) {
|
|
346
|
+
writer.write('{', true);
|
|
347
|
+
inlineParameterRecord = true;
|
|
329
348
|
} else {
|
|
330
349
|
writer.openBlock();
|
|
331
350
|
braceDepth += 1;
|
|
@@ -334,7 +353,11 @@ function formatTokens(parsed: SyqlSyntaxFile): string {
|
|
|
334
353
|
continue;
|
|
335
354
|
}
|
|
336
355
|
if (token.text === '}') {
|
|
337
|
-
if (
|
|
356
|
+
if (inlineParameterRecord) {
|
|
357
|
+
writer.write('}', true);
|
|
358
|
+
inlineParameterRecord = false;
|
|
359
|
+
justClosedInlineParameterRecord = true;
|
|
360
|
+
} else if (importList && braceDepth === 0) {
|
|
338
361
|
writer.write('}', true);
|
|
339
362
|
importList = false;
|
|
340
363
|
justClosedImportList = true;
|
|
@@ -345,6 +368,7 @@ function formatTokens(parsed: SyqlSyntaxFile): string {
|
|
|
345
368
|
previous = token;
|
|
346
369
|
continue;
|
|
347
370
|
}
|
|
371
|
+
justClosedInlineParameterRecord = false;
|
|
348
372
|
justClosedImportList = false;
|
|
349
373
|
|
|
350
374
|
if (token.text === '(') {
|
|
@@ -365,7 +389,11 @@ function formatTokens(parsed: SyqlSyntaxFile): string {
|
|
|
365
389
|
continue;
|
|
366
390
|
}
|
|
367
391
|
|
|
368
|
-
if (
|
|
392
|
+
if (
|
|
393
|
+
token.text === ',' &&
|
|
394
|
+
declarationParameters?.depth === parenDepth &&
|
|
395
|
+
!inlineParameterRecord
|
|
396
|
+
) {
|
|
369
397
|
const next = tokens[index + 1];
|
|
370
398
|
if (!(declarationParameters.multiline === false && next?.text === ')')) {
|
|
371
399
|
writer.write(',', false);
|
package/src/lsp.ts
CHANGED
|
@@ -148,19 +148,19 @@ function spanRange(text: string, span: SyqlSourceSpan): Range {
|
|
|
148
148
|
function typeText(type: SyqlValueType | undefined): string {
|
|
149
149
|
return type === undefined
|
|
150
150
|
? 'inferred'
|
|
151
|
-
: `${type.base}${type.nullable ? ' | null' : ''}`;
|
|
151
|
+
: `${type.base === 'boolean' ? 'bool' : type.base}${type.nullable ? ' | null' : ''}`;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
function parameterHover(parameter: SyqlQueryParameter): string {
|
|
155
|
-
if (parameter.kind === '
|
|
156
|
-
return
|
|
155
|
+
if (parameter.kind === 'range') {
|
|
156
|
+
return `${parameter.optional ? 'optional' : 'required'} inclusive range \`${parameter.name}: range<${typeText(parameter.type)}>\``;
|
|
157
157
|
}
|
|
158
158
|
if (parameter.kind === 'group') {
|
|
159
159
|
return `optional group \`${parameter.name}\`\n\n${parameter.members
|
|
160
160
|
.map((member) => `- \`${member.name}: ${typeText(member.type)}\``)
|
|
161
161
|
.join('\n')}`;
|
|
162
162
|
}
|
|
163
|
-
return `${parameter.optional ? 'optional' : 'required'} input \`${parameter.name}: ${typeText(parameter.type)}\``;
|
|
163
|
+
return `${parameter.optional ? 'optional' : parameter.default === false ? 'default-false' : 'required'} input \`${parameter.name}: ${typeText(parameter.type)}\``;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
export class SyqlLanguageServer {
|
|
@@ -432,22 +432,10 @@ export class SyqlLanguageServer {
|
|
|
432
432
|
const request = this.#request(params);
|
|
433
433
|
if (request === null) return null;
|
|
434
434
|
const { found, view, offset } = request;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
value:
|
|
440
|
-
found.word === '@scope'
|
|
441
|
-
? '`@scope` constructs exact reactive dependency keys and the same SQL predicate.'
|
|
442
|
-
: '`@cover` constructs exact dependencies plus checked window coverage.',
|
|
443
|
-
},
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
if (found.word.startsWith('@')) {
|
|
447
|
-
const predicate = view.semantic.predicateScopes
|
|
448
|
-
.get(view.file)
|
|
449
|
-
?.get(found.word.slice(1));
|
|
450
|
-
if (predicate === undefined) return null;
|
|
435
|
+
const predicate = view.semantic.predicateScopes
|
|
436
|
+
.get(view.file)
|
|
437
|
+
?.get(found.word);
|
|
438
|
+
if (predicate !== undefined) {
|
|
451
439
|
return {
|
|
452
440
|
contents: {
|
|
453
441
|
kind: 'markdown',
|
|
@@ -488,11 +476,11 @@ export class SyqlLanguageServer {
|
|
|
488
476
|
},
|
|
489
477
|
};
|
|
490
478
|
}
|
|
491
|
-
if (query.
|
|
479
|
+
if (query.limit?.control === found.word) {
|
|
492
480
|
return {
|
|
493
481
|
contents: {
|
|
494
482
|
kind: 'markdown',
|
|
495
|
-
value: `
|
|
483
|
+
value: `limit control \`${found.word}\`: default ${query.limit.defaultSize}, maximum ${query.limit.maxSize}`,
|
|
496
484
|
},
|
|
497
485
|
};
|
|
498
486
|
}
|
|
@@ -529,10 +517,10 @@ export class SyqlLanguageServer {
|
|
|
529
517
|
|
|
530
518
|
#definition(params: unknown): unknown {
|
|
531
519
|
const request = this.#request(params);
|
|
532
|
-
if (request === null
|
|
520
|
+
if (request === null) return null;
|
|
533
521
|
const predicate = request.view.semantic.predicateScopes
|
|
534
522
|
.get(request.view.file)
|
|
535
|
-
?.get(request.found.word
|
|
523
|
+
?.get(request.found.word);
|
|
536
524
|
if (predicate === undefined) return null;
|
|
537
525
|
const text =
|
|
538
526
|
this.#openText(predicate.module.file) ?? predicate.module.source;
|
|
@@ -544,17 +532,17 @@ export class SyqlLanguageServer {
|
|
|
544
532
|
|
|
545
533
|
#references(params: unknown): unknown {
|
|
546
534
|
const request = this.#request(params);
|
|
547
|
-
if (request === null
|
|
535
|
+
if (request === null) return [];
|
|
548
536
|
const target = request.view.semantic.predicateScopes
|
|
549
537
|
.get(request.view.file)
|
|
550
|
-
?.get(request.found.word
|
|
538
|
+
?.get(request.found.word);
|
|
551
539
|
if (target === undefined) return [];
|
|
552
540
|
const locations: unknown[] = [];
|
|
553
541
|
for (const module of request.view.semantic.graph.modules) {
|
|
554
542
|
const scope = request.view.semantic.predicateScopes.get(module.file);
|
|
555
543
|
for (const token of module.tokens) {
|
|
556
|
-
if (token.kind !== '
|
|
557
|
-
const resolved = scope?.get(token.text
|
|
544
|
+
if (token.kind !== 'identifier') continue;
|
|
545
|
+
const resolved = scope?.get(token.text);
|
|
558
546
|
if (resolved?.id !== target.id) continue;
|
|
559
547
|
locations.push({
|
|
560
548
|
uri: pathToFileURL(module.file).href,
|
package/src/query.ts
CHANGED
|
@@ -87,6 +87,9 @@ const DECLTYPE_MAP: Readonly<Record<string, IrColumnType>> = {
|
|
|
87
87
|
CRDT: 'crdt',
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
+
/** Local client protocol column; query-only and never part of schema IR. */
|
|
91
|
+
const SYNC_VERSION_COLUMN = '_sync_version';
|
|
92
|
+
|
|
90
93
|
/** A param type is one of the §2.4 types (the columns params compare to). */
|
|
91
94
|
export type QueryParamType = IrColumnType;
|
|
92
95
|
|
|
@@ -161,9 +164,8 @@ export interface QueryNamingOptions {
|
|
|
161
164
|
|
|
162
165
|
const DEFAULT_NAMING: QueryNamingOptions = { naming: 'camel', targets: ['ts'] };
|
|
163
166
|
|
|
164
|
-
/**
|
|
165
|
-
*
|
|
166
|
-
* parameters, while compiler-generated parameters are never public. */
|
|
167
|
+
/** SYQL public inputs. These are deliberately separate from SQL binds: groups
|
|
168
|
+
* and compiler-generated parameters have distinct public/runtime shapes. */
|
|
167
169
|
export type QuerySyqlPublicInput =
|
|
168
170
|
| {
|
|
169
171
|
readonly kind: 'value';
|
|
@@ -172,6 +174,7 @@ export type QuerySyqlPublicInput =
|
|
|
172
174
|
readonly type: QueryParamType;
|
|
173
175
|
readonly nullable: boolean;
|
|
174
176
|
readonly required: boolean;
|
|
177
|
+
readonly default?: false;
|
|
175
178
|
}
|
|
176
179
|
| {
|
|
177
180
|
readonly kind: 'group';
|
|
@@ -184,12 +187,6 @@ export type QuerySyqlPublicInput =
|
|
|
184
187
|
readonly nullable: boolean;
|
|
185
188
|
}[];
|
|
186
189
|
}
|
|
187
|
-
| {
|
|
188
|
-
readonly kind: 'switch';
|
|
189
|
-
readonly name: string;
|
|
190
|
-
readonly langName: string;
|
|
191
|
-
readonly default: false;
|
|
192
|
-
}
|
|
193
190
|
| {
|
|
194
191
|
readonly kind: 'sort';
|
|
195
192
|
readonly name: string;
|
|
@@ -201,7 +198,7 @@ export type QuerySyqlPublicInput =
|
|
|
201
198
|
}[];
|
|
202
199
|
}
|
|
203
200
|
| {
|
|
204
|
-
readonly kind: '
|
|
201
|
+
readonly kind: 'limit';
|
|
205
202
|
readonly name: string;
|
|
206
203
|
readonly langName: string;
|
|
207
204
|
readonly defaultSize: number;
|
|
@@ -231,7 +228,7 @@ export type QuerySyqlPlanBind =
|
|
|
231
228
|
readonly controls: readonly string[];
|
|
232
229
|
}
|
|
233
230
|
| {
|
|
234
|
-
readonly kind: '
|
|
231
|
+
readonly kind: 'limit';
|
|
235
232
|
readonly name: string;
|
|
236
233
|
readonly type: 'integer';
|
|
237
234
|
readonly input: string;
|
|
@@ -250,7 +247,7 @@ export interface QuerySyqlStatement {
|
|
|
250
247
|
/** The target-neutral physical plan every emitter must implement exactly. */
|
|
251
248
|
export interface QuerySyqlExecutionPlan {
|
|
252
249
|
readonly backend: Exclude<QueryBackend, 'auto'>;
|
|
253
|
-
/** One bit per
|
|
250
|
+
/** One bit per conditional activation control, in declaration order. */
|
|
254
251
|
readonly activationControls: readonly string[];
|
|
255
252
|
readonly conditions: readonly {
|
|
256
253
|
readonly controls: readonly string[];
|
|
@@ -819,6 +816,20 @@ function resolveSource(
|
|
|
819
816
|
return null;
|
|
820
817
|
}
|
|
821
818
|
|
|
819
|
+
/** Resolve the one supported query-only protocol column. SQLite still proves
|
|
820
|
+
* qualifier/ambiguity against the synthesized local tables. */
|
|
821
|
+
function isSyncVersionSource(
|
|
822
|
+
item: SelectItem,
|
|
823
|
+
refs: readonly TableRef[],
|
|
824
|
+
): boolean {
|
|
825
|
+
const match = PLAIN_REF_RE.exec(item.expr);
|
|
826
|
+
if (match === null || match[2] !== SYNC_VERSION_COLUMN) return false;
|
|
827
|
+
const qualifier = match[1];
|
|
828
|
+
return qualifier === undefined
|
|
829
|
+
? refs.length === 1
|
|
830
|
+
: refs.some((ref) => ref.alias === qualifier);
|
|
831
|
+
}
|
|
832
|
+
|
|
822
833
|
// -- param type inference -----------------------------------------------------
|
|
823
834
|
//
|
|
824
835
|
// Infer a param's type from `col <op> :name` or `col IN (:name, …)` where `col`
|
|
@@ -1168,6 +1179,10 @@ export function synthesizeDdl(ir: IrDocument): string {
|
|
|
1168
1179
|
const pk = c.name === table.primaryKey ? ' PRIMARY KEY' : '';
|
|
1169
1180
|
return ` ${c.name} ${SQL_TYPE[c.type]}${pk}${notNull}`;
|
|
1170
1181
|
});
|
|
1182
|
+
// The materialized local client table always carries this hidden protocol
|
|
1183
|
+
// token. It is absent from IR mutation types but may be explicitly aliased
|
|
1184
|
+
// by a named read query for optimistic concurrency.
|
|
1185
|
+
cols.push(` ${SYNC_VERSION_COLUMN} INTEGER NOT NULL DEFAULT -1`);
|
|
1171
1186
|
lines.push(`CREATE TABLE ${table.name} (\n${cols.join(',\n')}\n);`);
|
|
1172
1187
|
// Create the declared indexes too — harmless for prepare()/decltype, and
|
|
1173
1188
|
// keeps the type-check DB's shape honest with what the client materializes.
|
|
@@ -1291,6 +1306,21 @@ export function analyzeStatement(
|
|
|
1291
1306
|
const source = item !== undefined ? resolveSource(item, refs, ir) : null;
|
|
1292
1307
|
const sqlName = sqlNames[index] ?? colName;
|
|
1293
1308
|
const langName = langNames[index] ?? colName;
|
|
1309
|
+
if (item !== undefined && isSyncVersionSource(item, refs)) {
|
|
1310
|
+
if (sqlName === SYNC_VERSION_COLUMN) {
|
|
1311
|
+
throw new TypegenError(
|
|
1312
|
+
file,
|
|
1313
|
+
`${SYNC_VERSION_COLUMN} is stripped from app-facing query rows unless explicitly aliased (for example \`${SYNC_VERSION_COLUMN} AS server_version\`)`,
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
return {
|
|
1317
|
+
name: sqlName,
|
|
1318
|
+
langName,
|
|
1319
|
+
type: 'integer',
|
|
1320
|
+
nullable: false,
|
|
1321
|
+
fidelity: 'exact',
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1294
1324
|
if (source !== null) {
|
|
1295
1325
|
// Exact: IR column type + IR nullability. (decltype agrees; we prefer
|
|
1296
1326
|
// the IR type so blob_ref/crdt/json semantic types survive.)
|