@syncular/typegen 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -32
- package/dist/cli.js +40 -17
- package/dist/emit-queries-dart.js +168 -55
- package/dist/emit-queries-kotlin.js +167 -55
- package/dist/emit-queries-swift.js +183 -57
- package/dist/emit-queries.js +286 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +348 -316
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +28 -9
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/lsp.d.ts +1 -3
- package/dist/lsp.js +349 -187
- package/dist/manifest.d.ts +1 -3
- package/dist/manifest.js +1 -1
- package/dist/query-ir.js +53 -42
- package/dist/query.d.ts +110 -63
- package/dist/query.js +89 -11
- package/dist/syql-ast.d.ts +12 -13
- package/dist/syql-ast.js +26 -20
- package/dist/syql-lexer.d.ts +2 -0
- package/dist/syql-lexer.js +9 -2
- package/dist/syql-lowering.d.ts +22 -0
- package/dist/syql-lowering.js +434 -0
- package/dist/syql-parser.d.ts +19 -18
- package/dist/syql-parser.js +341 -93
- package/dist/syql-semantics.d.ts +73 -0
- package/dist/syql-semantics.js +607 -0
- package/dist/syql-template-parser.d.ts +5 -20
- package/dist/syql-template-parser.js +116 -109
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +993 -0
- package/package.json +3 -3
- package/src/cli.ts +49 -21
- package/src/emit-queries-dart.ts +195 -69
- package/src/emit-queries-kotlin.ts +197 -69
- package/src/emit-queries-swift.ts +224 -68
- package/src/emit-queries.ts +410 -165
- package/src/fmt.ts +405 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +414 -216
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +229 -79
- package/src/syql-ast.ts +38 -34
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +664 -0
- package/src/syql-parser.ts +472 -134
- package/src/syql-semantics.ts +930 -0
- package/src/syql-template-parser.ts +119 -163
- package/src/syql-validator.ts +1486 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- package/src/syql.ts +0 -1470
|
@@ -66,22 +66,152 @@ function optionalParamValue(type, name) {
|
|
|
66
66
|
return `${name}?.let { JsonValue.of(it) } ?: JsonValue.Null`;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
function syqlInput(query, name) {
|
|
70
|
+
const input = query.syql?.inputs.find((candidate) => candidate.name === name);
|
|
71
|
+
if (input === undefined)
|
|
72
|
+
throw new Error(`unknown SYQL input ${name}`);
|
|
73
|
+
return input;
|
|
74
|
+
}
|
|
75
|
+
function syqlKotlinType(type, nullable) {
|
|
76
|
+
return `${KOTLIN_TYPE[type]}${nullable ? '?' : ''}`;
|
|
77
|
+
}
|
|
78
|
+
function syqlControlActive(query, name) {
|
|
79
|
+
const input = syqlInput(query, name);
|
|
80
|
+
const access = camelCase(input.langName);
|
|
81
|
+
if (input.kind === 'value' && input.default === false)
|
|
82
|
+
return access;
|
|
83
|
+
if (input.kind === 'value' && input.nullable) {
|
|
84
|
+
return `${access} is SyncularQueryPresence.Present`;
|
|
85
|
+
}
|
|
86
|
+
if (input.kind === 'value' || input.kind === 'group') {
|
|
87
|
+
return `${access} != null`;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`${name} is not an activation control`);
|
|
90
|
+
}
|
|
91
|
+
function syqlNullablePresenceBind(type, access) {
|
|
92
|
+
return `when (val presence = ${access}) { is SyncularQueryPresence.Present -> ${optionalParamValue(type, 'presence.value')}; SyncularQueryPresence.Absent -> JsonValue.Null }`;
|
|
93
|
+
}
|
|
94
|
+
function syqlBindExpr(query, bind) {
|
|
95
|
+
if (bind.kind === 'condition-active') {
|
|
96
|
+
const active = bind.controls
|
|
97
|
+
.map((control) => syqlControlActive(query, control))
|
|
98
|
+
.join(' && ');
|
|
99
|
+
return `JsonValue.of(${active})`;
|
|
100
|
+
}
|
|
101
|
+
const input = syqlInput(query, bind.input);
|
|
102
|
+
const access = camelCase(input.langName);
|
|
103
|
+
if (bind.kind === 'limit')
|
|
104
|
+
return `JsonValue.of(effective${typeName(access)}.toDouble())`;
|
|
105
|
+
if (bind.kind === 'group-member') {
|
|
106
|
+
if (input.kind !== 'group')
|
|
107
|
+
throw new Error('group bind/input mismatch');
|
|
108
|
+
const member = input.members.find((candidate) => candidate.name === bind.member);
|
|
109
|
+
if (member === undefined)
|
|
110
|
+
throw new Error(`unknown group member ${bind.member}`);
|
|
111
|
+
const memberName = camelCase(member.langName);
|
|
112
|
+
const present = member.nullable
|
|
113
|
+
? optionalParamValue(member.type, `value.${memberName}`)
|
|
114
|
+
: paramValue(member.type, `value.${memberName}`);
|
|
115
|
+
return `${access}?.let { value -> ${present} } ?: JsonValue.Null`;
|
|
116
|
+
}
|
|
117
|
+
if (input.kind !== 'value')
|
|
118
|
+
throw new Error('value bind/input mismatch');
|
|
119
|
+
if (input.default === false)
|
|
120
|
+
return paramValue(input.type, access);
|
|
121
|
+
if (input.required) {
|
|
122
|
+
return input.nullable
|
|
123
|
+
? optionalParamValue(input.type, access)
|
|
124
|
+
: paramValue(input.type, access);
|
|
125
|
+
}
|
|
126
|
+
return input.nullable
|
|
127
|
+
? syqlNullablePresenceBind(input.type, access)
|
|
128
|
+
: optionalParamValue(input.type, access);
|
|
129
|
+
}
|
|
130
|
+
function emitSyqlKotlinTypes(query) {
|
|
78
131
|
const lines = [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
132
|
+
for (const input of query.syql?.inputs ?? []) {
|
|
133
|
+
if (input.kind === 'group') {
|
|
134
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
135
|
+
lines.push(`data class ${name}(`);
|
|
136
|
+
for (const member of input.members) {
|
|
137
|
+
lines.push(` val ${camelCase(member.langName)}: ${syqlKotlinType(member.type, member.nullable)},`);
|
|
138
|
+
}
|
|
139
|
+
lines.push(')', '');
|
|
140
|
+
}
|
|
141
|
+
else if (input.kind === 'sort') {
|
|
142
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
143
|
+
lines.push(`enum class ${name}(val index: Int) {`);
|
|
144
|
+
lines.push(`${input.profiles
|
|
145
|
+
.map((profile, index) => ` ${camelCase(profile.langName)}(${index})`)
|
|
146
|
+
.join(',\n')};`);
|
|
147
|
+
lines.push('}', '');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (lines[lines.length - 1] === '')
|
|
151
|
+
lines.pop();
|
|
152
|
+
return lines;
|
|
153
|
+
}
|
|
154
|
+
function emitSyqlKotlinRunner(query) {
|
|
155
|
+
const metadata = query.syql;
|
|
156
|
+
if (metadata === undefined)
|
|
157
|
+
throw new Error('missing SYQL metadata');
|
|
158
|
+
const Row = `${typeName(query.name)}Row`;
|
|
159
|
+
const lines = [];
|
|
160
|
+
lines.push(` val ${query.name}Tables = listOf(${query.tables.map(quote).join(', ')})`, '', ` /** Run the ${query.name} revision-1 SYQL query. */`);
|
|
161
|
+
const args = ['client: SyncularClient'];
|
|
162
|
+
for (const input of metadata.inputs) {
|
|
163
|
+
const name = camelCase(input.langName);
|
|
164
|
+
if (input.kind === 'value') {
|
|
165
|
+
const type = syqlKotlinType(input.type, input.nullable);
|
|
166
|
+
if (input.required)
|
|
167
|
+
args.push(`${name}: ${type}`);
|
|
168
|
+
else if (input.default === false)
|
|
169
|
+
args.push(`${name}: Boolean = false`);
|
|
170
|
+
else if (input.nullable) {
|
|
171
|
+
args.push(`${name}: SyncularQueryPresence<${type}> = SyncularQueryPresence.Absent`);
|
|
172
|
+
}
|
|
173
|
+
else
|
|
174
|
+
args.push(`${name}: ${type}? = null`);
|
|
175
|
+
}
|
|
176
|
+
else if (input.kind === 'group') {
|
|
177
|
+
args.push(`${name}: ${typeName(query.name)}${typeName(input.langName)}? = null`);
|
|
178
|
+
}
|
|
179
|
+
else if (input.kind === 'sort') {
|
|
180
|
+
const defaultCase = input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
181
|
+
?.langName ?? input.defaultProfile;
|
|
182
|
+
const type = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
183
|
+
args.push(`${name}: ${type} = ${type}.${camelCase(defaultCase)}`);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
args.push(`${name}: Long? = null`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
lines.push(` fun ${query.name}(${args.join(', ')}): List<${Row}> {`);
|
|
190
|
+
const page = metadata.inputs.find((input) => input.kind === 'limit');
|
|
191
|
+
if (page?.kind === 'limit') {
|
|
192
|
+
const name = camelCase(page.langName);
|
|
193
|
+
lines.push(` val effective${typeName(name)} = ${name} ?: ${page.defaultSize}L`, ` if (effective${typeName(name)} < 1L || effective${typeName(name)} > ${page.maxSize}L) {`, ` throw SyncularQueryInputException("SYQL_RUNTIME_INVALID_LIMIT", ${quote(`${query.name}: invalid limit`)})`, ' }');
|
|
194
|
+
}
|
|
195
|
+
if (metadata.plan.backend === 'variants') {
|
|
196
|
+
lines.push(' var activationMask = 0');
|
|
197
|
+
metadata.plan.activationControls.forEach((control, index) => {
|
|
198
|
+
lines.push(` if (${syqlControlActive(query, control)}) activationMask = activationMask or ${2 ** index}`);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const sort = metadata.inputs.find((input) => input.kind === 'sort');
|
|
202
|
+
const profileCount = sort?.kind === 'sort' ? sort.profiles.length : 1;
|
|
203
|
+
const sortIndex = sort?.kind === 'sort' ? `${camelCase(sort.langName)}.index` : '0';
|
|
204
|
+
const index = metadata.plan.backend === 'variants'
|
|
205
|
+
? `activationMask * ${profileCount} + ${sortIndex}`
|
|
206
|
+
: sortIndex;
|
|
207
|
+
lines.push(` val statementIndex = ${index}`, ' val selected: Pair<String, List<JsonValue>> = when (statementIndex) {');
|
|
208
|
+
metadata.plan.statements.forEach((statement, statementIndex) => {
|
|
209
|
+
const binds = statement.binds
|
|
210
|
+
.map((bind) => syqlBindExpr(query, bind))
|
|
211
|
+
.join(', ');
|
|
212
|
+
lines.push(` ${statementIndex} -> Pair(${quote(statement.positionalSql)}, listOf(${binds}))`);
|
|
213
|
+
});
|
|
214
|
+
lines.push(' else -> error("invalid generated SYQL statement index")', ' }', ` return client.query(selected.first, selected.second).mapNotNull { ${Row}.fromRow(it) }`, ' }');
|
|
85
215
|
return lines;
|
|
86
216
|
}
|
|
87
217
|
function emitDataClass(query) {
|
|
@@ -114,51 +244,29 @@ function emitDataClass(query) {
|
|
|
114
244
|
return lines;
|
|
115
245
|
}
|
|
116
246
|
function emitRunner(query) {
|
|
247
|
+
if (query.syql !== undefined)
|
|
248
|
+
return emitSyqlKotlinRunner(query);
|
|
117
249
|
const Row = `${typeName(query.name)}Row`;
|
|
118
250
|
const lines = [];
|
|
119
251
|
lines.push(` val ${query.name}Tables = listOf(${query.tables.map(quote).join(', ')})`);
|
|
120
|
-
|
|
121
|
-
lines.push(` private const val ${query.name}SqlBase = ${quote(query.positionalSqlBase ?? '')}`);
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
lines.push(` private const val ${query.name}Sql = ${quote(query.positionalSql)}`);
|
|
125
|
-
}
|
|
252
|
+
lines.push(` private const val ${query.name}Sql = ${quote(query.positionalSql)}`);
|
|
126
253
|
lines.push('');
|
|
127
254
|
lines.push(` /** Run the ${query.name} named query (SELECT-only). */`);
|
|
128
255
|
const args = [];
|
|
129
256
|
for (const p of query.params) {
|
|
130
257
|
const name = camelCase(p.langName);
|
|
131
|
-
|
|
132
|
-
args.push(`${name}: ${KOTLIN_TYPE[p.type]}? = null`);
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
args.push(`${name}: ${KOTLIN_TYPE[p.type]}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (query.orderBy !== undefined) {
|
|
139
|
-
const defaultCase = camelCase(query.orderBy.allowed.find((c) => c.name === query.orderBy?.defaultColumn)
|
|
140
|
-
?.langName ?? query.orderBy.defaultColumn);
|
|
141
|
-
args.push(`orderBy: ${typeName(query.name)}OrderBy = ${typeName(query.name)}OrderBy.${defaultCase}`);
|
|
142
|
-
args.push(`dir: SyncularQueryDir = SyncularQueryDir.${query.orderBy.defaultDir.toUpperCase()}`);
|
|
258
|
+
args.push(`${name}: ${KOTLIN_TYPE[p.type]}`);
|
|
143
259
|
}
|
|
144
260
|
const signature = args.length > 0
|
|
145
261
|
? `client: SyncularClient, ${args.join(', ')}`
|
|
146
262
|
: 'client: SyncularClient';
|
|
147
263
|
lines.push(` fun ${query.name}(${signature}): List<${Row}> {`);
|
|
148
|
-
|
|
149
|
-
const limitTail = query.positionalLimitTail !== undefined
|
|
150
|
-
? ` + ${quote(query.positionalLimitTail)}`
|
|
151
|
-
: '';
|
|
152
|
-
lines.push(` val sql = ${query.name}SqlBase + " order by " + orderBy.column + " " + dir.sql${limitTail}`);
|
|
153
|
-
}
|
|
154
|
-
const sqlRef = query.orderBy !== undefined ? 'sql' : `${query.name}Sql`;
|
|
264
|
+
const sqlRef = `${query.name}Sql`;
|
|
155
265
|
if (query.params.length > 0) {
|
|
156
266
|
const binds = query.params
|
|
157
267
|
.map((p) => {
|
|
158
268
|
const name = camelCase(p.langName);
|
|
159
|
-
return
|
|
160
|
-
? optionalParamValue(p.type, name)
|
|
161
|
-
: paramValue(p.type, name);
|
|
269
|
+
return paramValue(p.type, name);
|
|
162
270
|
})
|
|
163
271
|
.join(', ');
|
|
164
272
|
lines.push(` val params = listOf(${binds})`);
|
|
@@ -182,6 +290,19 @@ export function emitQueriesKotlinModule(queries, hash, irVersion, packageName, o
|
|
|
182
290
|
'import dev.syncular.JsonValue',
|
|
183
291
|
'import dev.syncular.SyncularClient',
|
|
184
292
|
].join('\n'));
|
|
293
|
+
if (queries.some((query) => query.syql !== undefined)) {
|
|
294
|
+
parts.push([
|
|
295
|
+
'sealed class SyncularQueryPresence<out T> {',
|
|
296
|
+
' object Absent : SyncularQueryPresence<Nothing>()',
|
|
297
|
+
' data class Present<T>(val value: T) : SyncularQueryPresence<T>()',
|
|
298
|
+
'}',
|
|
299
|
+
'',
|
|
300
|
+
'class SyncularQueryInputException(',
|
|
301
|
+
' val code: String,',
|
|
302
|
+
' override val message: String,',
|
|
303
|
+
') : IllegalArgumentException(message)',
|
|
304
|
+
].join('\n'));
|
|
305
|
+
}
|
|
185
306
|
// Row-decode + bind helpers (package-private; distinct names so they don't
|
|
186
307
|
// clash with the schema file's rowBool/rowBytes in the same package).
|
|
187
308
|
parts.push([
|
|
@@ -204,20 +325,11 @@ export function emitQueriesKotlinModule(queries, hash, irVersion, packageName, o
|
|
|
204
325
|
' return JsonValue.obj("\\$bytes" to JsonValue.of(hex))',
|
|
205
326
|
'}',
|
|
206
327
|
].join('\n'));
|
|
207
|
-
if (queries.some((q) => q.orderBy !== undefined)) {
|
|
208
|
-
parts.push([
|
|
209
|
-
'/** §6 orderBy direction (shared by every orderBy-knob query). */',
|
|
210
|
-
'enum class SyncularQueryDir(val sql: String) {',
|
|
211
|
-
' ASC("asc"),',
|
|
212
|
-
' DESC("desc");',
|
|
213
|
-
'}',
|
|
214
|
-
].join('\n'));
|
|
215
|
-
}
|
|
216
328
|
for (const query of queries) {
|
|
329
|
+
const syqlTypes = emitSyqlKotlinTypes(query);
|
|
330
|
+
if (syqlTypes.length > 0)
|
|
331
|
+
parts.push(syqlTypes.join('\n'));
|
|
217
332
|
parts.push(emitDataClass(query).join('\n'));
|
|
218
|
-
const orderByEnum = emitOrderByEnum(query);
|
|
219
|
-
if (orderByEnum.length > 0)
|
|
220
|
-
parts.push(orderByEnum.join('\n'));
|
|
221
333
|
}
|
|
222
334
|
const objLines = [];
|
|
223
335
|
objLines.push('/** Typed named queries (the sqlc/SQLDelight tier). */');
|
|
@@ -9,6 +9,10 @@ const SWIFT_TYPE = {
|
|
|
9
9
|
blob_ref: 'String',
|
|
10
10
|
crdt: '[UInt8]',
|
|
11
11
|
};
|
|
12
|
+
const SYQL_SWIFT_TYPE = {
|
|
13
|
+
...SWIFT_TYPE,
|
|
14
|
+
integer: 'Int64',
|
|
15
|
+
};
|
|
12
16
|
/** Language-facing field name — the pinned §12 naming map. */
|
|
13
17
|
function camelCase(name) {
|
|
14
18
|
return snakeToCamel(name);
|
|
@@ -69,10 +73,159 @@ function optionalParamValue(type, name) {
|
|
|
69
73
|
return `${name}.map { JSONValue.string($0) } ?? .null`;
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
(
|
|
76
|
+
function syqlInput(query, name) {
|
|
77
|
+
const input = query.syql?.inputs.find((candidate) => candidate.name === name);
|
|
78
|
+
if (input === undefined)
|
|
79
|
+
throw new Error(`unknown SYQL input ${name}`);
|
|
80
|
+
return input;
|
|
81
|
+
}
|
|
82
|
+
function syqlSwiftType(type, nullable) {
|
|
83
|
+
return `${SYQL_SWIFT_TYPE[type]}${nullable ? '?' : ''}`;
|
|
84
|
+
}
|
|
85
|
+
function syqlControlActive(query, name) {
|
|
86
|
+
const input = syqlInput(query, name);
|
|
87
|
+
const access = camelCase(input.langName);
|
|
88
|
+
if (input.kind === 'value' && input.default === false)
|
|
89
|
+
return access;
|
|
90
|
+
if (input.kind === 'value' && input.nullable) {
|
|
91
|
+
return `{ if case .present = ${access} { return true }; return false }()`;
|
|
92
|
+
}
|
|
93
|
+
if (input.kind === 'value' || input.kind === 'group') {
|
|
94
|
+
return `${access} != nil`;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(`${name} is not an activation control`);
|
|
97
|
+
}
|
|
98
|
+
function syqlNullablePresenceBind(type, access) {
|
|
99
|
+
return `{ () -> JSONValue in switch ${access} { case .absent: return .null; case .present(let value): return ${optionalParamValue(type, 'value')} } }()`;
|
|
100
|
+
}
|
|
101
|
+
function syqlBindExpr(query, bind) {
|
|
102
|
+
if (bind.kind === 'condition-active') {
|
|
103
|
+
const active = bind.controls
|
|
104
|
+
.map((control) => syqlControlActive(query, control))
|
|
105
|
+
.join(' && ');
|
|
106
|
+
return `.bool(${active})`;
|
|
107
|
+
}
|
|
108
|
+
const input = syqlInput(query, bind.input);
|
|
109
|
+
const access = camelCase(input.langName);
|
|
110
|
+
if (bind.kind === 'limit')
|
|
111
|
+
return `.number(Double(effective${typeName(access)}))`;
|
|
112
|
+
if (bind.kind === 'group-member') {
|
|
113
|
+
if (input.kind !== 'group')
|
|
114
|
+
throw new Error('group bind/input mismatch');
|
|
115
|
+
const member = input.members.find((candidate) => candidate.name === bind.member);
|
|
116
|
+
if (member === undefined)
|
|
117
|
+
throw new Error(`unknown group member ${bind.member}`);
|
|
118
|
+
const memberName = camelCase(member.langName);
|
|
119
|
+
const present = member.nullable
|
|
120
|
+
? optionalParamValue(member.type, `value.${memberName}`)
|
|
121
|
+
: paramValue(member.type, `value.${memberName}`);
|
|
122
|
+
return `${access}.map { value in ${present} } ?? .null`;
|
|
123
|
+
}
|
|
124
|
+
if (input.kind !== 'value')
|
|
125
|
+
throw new Error('value bind/input mismatch');
|
|
126
|
+
if (input.default === false)
|
|
127
|
+
return paramValue(input.type, access);
|
|
128
|
+
if (input.required) {
|
|
129
|
+
return input.nullable
|
|
130
|
+
? optionalParamValue(input.type, access)
|
|
131
|
+
: paramValue(input.type, access);
|
|
132
|
+
}
|
|
133
|
+
return input.nullable
|
|
134
|
+
? syqlNullablePresenceBind(input.type, access)
|
|
135
|
+
: optionalParamValue(input.type, access);
|
|
136
|
+
}
|
|
137
|
+
function emitSyqlSwiftTypes(query) {
|
|
138
|
+
const lines = [];
|
|
139
|
+
for (const input of query.syql?.inputs ?? []) {
|
|
140
|
+
if (input.kind === 'group') {
|
|
141
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
142
|
+
lines.push(`public struct ${name}: Sendable {`);
|
|
143
|
+
for (const member of input.members) {
|
|
144
|
+
lines.push(` public let ${camelCase(member.langName)}: ${syqlSwiftType(member.type, member.nullable)}`);
|
|
145
|
+
}
|
|
146
|
+
const args = input.members
|
|
147
|
+
.map((member) => `${camelCase(member.langName)}: ${syqlSwiftType(member.type, member.nullable)}`)
|
|
148
|
+
.join(', ');
|
|
149
|
+
lines.push(` public init(${args}) {`);
|
|
150
|
+
for (const member of input.members) {
|
|
151
|
+
const name = camelCase(member.langName);
|
|
152
|
+
lines.push(` self.${name} = ${name}`);
|
|
153
|
+
}
|
|
154
|
+
lines.push(' }', '}', '');
|
|
155
|
+
}
|
|
156
|
+
else if (input.kind === 'sort') {
|
|
157
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
158
|
+
lines.push(`public enum ${name}: Int, Sendable {`);
|
|
159
|
+
input.profiles.forEach((profile, index) => {
|
|
160
|
+
lines.push(` case ${camelCase(profile.langName)} = ${index}`);
|
|
161
|
+
});
|
|
162
|
+
lines.push('}', '');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (lines[lines.length - 1] === '')
|
|
166
|
+
lines.pop();
|
|
167
|
+
return lines;
|
|
168
|
+
}
|
|
169
|
+
function emitSyqlSwiftRunner(query) {
|
|
170
|
+
const metadata = query.syql;
|
|
171
|
+
if (metadata === undefined)
|
|
172
|
+
throw new Error('missing SYQL metadata');
|
|
173
|
+
const Row = `${typeName(query.name)}Row`;
|
|
174
|
+
const lines = [];
|
|
175
|
+
lines.push(` public static let ${query.name}Tables = [${query.tables.map(quote).join(', ')}]`, '', ` /// Run the ${query.name} revision-1 SYQL query.`);
|
|
176
|
+
const args = ['client: SyncularClient'];
|
|
177
|
+
for (const input of metadata.inputs) {
|
|
178
|
+
const name = camelCase(input.langName);
|
|
179
|
+
if (input.kind === 'value') {
|
|
180
|
+
const type = syqlSwiftType(input.type, input.nullable);
|
|
181
|
+
if (input.required)
|
|
182
|
+
args.push(`${name}: ${type}`);
|
|
183
|
+
else if (input.default === false)
|
|
184
|
+
args.push(`${name}: Bool = false`);
|
|
185
|
+
else if (input.nullable)
|
|
186
|
+
args.push(`${name}: SyncularQueryPresence<${type}> = .absent`);
|
|
187
|
+
else
|
|
188
|
+
args.push(`${name}: ${type}? = nil`);
|
|
189
|
+
}
|
|
190
|
+
else if (input.kind === 'group') {
|
|
191
|
+
args.push(`${name}: ${typeName(query.name)}${typeName(input.langName)}? = nil`);
|
|
192
|
+
}
|
|
193
|
+
else if (input.kind === 'sort') {
|
|
194
|
+
const defaultCase = input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
195
|
+
?.langName ?? input.defaultProfile;
|
|
196
|
+
args.push(`${name}: ${typeName(query.name)}${typeName(input.langName)} = .${camelCase(defaultCase)}`);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
args.push(`${name}: Int? = nil`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
lines.push(` public static func ${query.name}(${args.join(', ')}) throws -> [${Row}] {`);
|
|
203
|
+
const page = metadata.inputs.find((input) => input.kind === 'limit');
|
|
204
|
+
if (page?.kind === 'limit') {
|
|
205
|
+
const name = camelCase(page.langName);
|
|
206
|
+
lines.push(` let effective${typeName(name)} = ${name} ?? ${page.defaultSize}`, ` guard effective${typeName(name)} >= 1 && effective${typeName(name)} <= ${page.maxSize} else {`, ` throw SyncularQueryInputError(code: "SYQL_RUNTIME_INVALID_LIMIT", message: ${quote(`${query.name}: invalid limit`)})`, ' }');
|
|
207
|
+
}
|
|
208
|
+
if (metadata.plan.backend === 'variants') {
|
|
209
|
+
lines.push(' var activationMask = 0');
|
|
210
|
+
metadata.plan.activationControls.forEach((control, index) => {
|
|
211
|
+
lines.push(` if ${syqlControlActive(query, control)} { activationMask |= ${2 ** index} }`);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const sort = metadata.inputs.find((input) => input.kind === 'sort');
|
|
215
|
+
const profileCount = sort?.kind === 'sort' ? sort.profiles.length : 1;
|
|
216
|
+
const sortIndex = sort?.kind === 'sort' ? `${camelCase(sort.langName)}.rawValue` : '0';
|
|
217
|
+
const index = metadata.plan.backend === 'variants'
|
|
218
|
+
? `activationMask * ${profileCount} + ${sortIndex}`
|
|
219
|
+
: sortIndex;
|
|
220
|
+
lines.push(` let statementIndex = ${index}`, ' let selected: (String, [JSONValue])', ' switch statementIndex {');
|
|
221
|
+
metadata.plan.statements.forEach((statement, statementIndex) => {
|
|
222
|
+
const binds = statement.binds
|
|
223
|
+
.map((bind) => syqlBindExpr(query, bind))
|
|
224
|
+
.join(', ');
|
|
225
|
+
lines.push(` case ${statementIndex}: selected = (${quote(statement.positionalSql)}, [${binds}])`);
|
|
226
|
+
});
|
|
227
|
+
lines.push(' default: preconditionFailure("invalid generated SYQL statement index")', ' }', ' return try client.query(selected.0, params: selected.1).compactMap { row in', ' guard case let .object(fields) = row else { return nil }', ` return ${Row}(row: fields)`, ' }', ' }');
|
|
228
|
+
return lines;
|
|
76
229
|
}
|
|
77
230
|
function emitRowStruct(query) {
|
|
78
231
|
const Row = `${typeName(query.name)}Row`;
|
|
@@ -100,65 +253,30 @@ function emitRowStruct(query) {
|
|
|
100
253
|
lines.push('}');
|
|
101
254
|
return lines;
|
|
102
255
|
}
|
|
103
|
-
/** Per-query orderBy allowlist enum (rawValue = the checked SQL column). */
|
|
104
|
-
function emitOrderByEnum(query) {
|
|
105
|
-
if (query.orderBy === undefined)
|
|
106
|
-
return [];
|
|
107
|
-
const lines = [];
|
|
108
|
-
lines.push(`/// §6 orderBy allowlist for ${query.name} — checked at generate time.`);
|
|
109
|
-
lines.push(`public enum ${typeName(query.name)}OrderBy: String, Sendable {`);
|
|
110
|
-
for (const col of query.orderBy.allowed) {
|
|
111
|
-
lines.push(` case ${camelCase(col.langName)} = ${quote(col.name)}`);
|
|
112
|
-
}
|
|
113
|
-
lines.push('}');
|
|
114
|
-
return lines;
|
|
115
|
-
}
|
|
116
256
|
function emitRunner(query) {
|
|
257
|
+
if (query.syql !== undefined)
|
|
258
|
+
return emitSyqlSwiftRunner(query);
|
|
117
259
|
const Row = `${typeName(query.name)}Row`;
|
|
118
260
|
const lines = [];
|
|
119
261
|
lines.push(` public static let ${query.name}Tables = [${query.tables.map(quote).join(', ')}]`);
|
|
120
|
-
|
|
121
|
-
lines.push(` private static let ${query.name}SqlBase = ${quote(query.positionalSqlBase ?? '')}`);
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
lines.push(` private static let ${query.name}Sql = ${quote(query.positionalSql)}`);
|
|
125
|
-
}
|
|
262
|
+
lines.push(` private static let ${query.name}Sql = ${quote(query.positionalSql)}`);
|
|
126
263
|
lines.push('');
|
|
127
264
|
lines.push(` /// Run the ${query.name} named query (SELECT-only).`);
|
|
128
265
|
const args = [];
|
|
129
266
|
for (const p of query.params) {
|
|
130
267
|
const name = camelCase(p.langName);
|
|
131
|
-
|
|
132
|
-
args.push(`${name}: ${SWIFT_TYPE[p.type]}? = nil`);
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
args.push(`${name}: ${SWIFT_TYPE[p.type]}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (query.orderBy !== undefined) {
|
|
139
|
-
const defaultCase = camelCase(query.orderBy.allowed.find((c) => c.name === query.orderBy?.defaultColumn)
|
|
140
|
-
?.langName ?? query.orderBy.defaultColumn);
|
|
141
|
-
args.push(`orderBy: ${typeName(query.name)}OrderBy = .${defaultCase}`);
|
|
142
|
-
args.push(`dir: SyncularQueryDir = .${query.orderBy.defaultDir}`);
|
|
268
|
+
args.push(`${name}: ${SWIFT_TYPE[p.type]}`);
|
|
143
269
|
}
|
|
144
270
|
const signature = args.length > 0
|
|
145
271
|
? `client: SyncularClient, ${args.join(', ')}`
|
|
146
272
|
: 'client: SyncularClient';
|
|
147
273
|
lines.push(` public static func ${query.name}(${signature}) throws -> [${Row}] {`);
|
|
148
|
-
const
|
|
149
|
-
? `${query.name}SqlBase + " order by " + orderBy.rawValue + " " + dir.rawValue${query.positionalLimitTail !== undefined ? ` + ${quote(query.positionalLimitTail)}` : ''}`
|
|
150
|
-
: `${query.name}Sql`;
|
|
151
|
-
if (query.orderBy !== undefined) {
|
|
152
|
-
lines.push(` let sql = ${sqlExpr}`);
|
|
153
|
-
}
|
|
154
|
-
const sqlRef = query.orderBy !== undefined ? 'sql' : `${query.name}Sql`;
|
|
274
|
+
const sqlRef = `${query.name}Sql`;
|
|
155
275
|
if (query.params.length > 0) {
|
|
156
276
|
const binds = query.params
|
|
157
277
|
.map((p) => {
|
|
158
278
|
const name = camelCase(p.langName);
|
|
159
|
-
return
|
|
160
|
-
? optionalParamValue(p.type, name)
|
|
161
|
-
: paramValue(p.type, name);
|
|
279
|
+
return paramValue(p.type, name);
|
|
162
280
|
})
|
|
163
281
|
.join(', ');
|
|
164
282
|
lines.push(` let params: [JSONValue] = [${binds}]`);
|
|
@@ -183,6 +301,23 @@ export function emitQueriesSwiftModule(queries, hash, irVersion, enumName) {
|
|
|
183
301
|
'import Foundation',
|
|
184
302
|
'import Syncular',
|
|
185
303
|
].join('\n'));
|
|
304
|
+
if (queries.some((query) => query.syql !== undefined)) {
|
|
305
|
+
parts.push([
|
|
306
|
+
'public enum SyncularQueryPresence<Value: Sendable>: Sendable {',
|
|
307
|
+
' case absent',
|
|
308
|
+
' case present(Value)',
|
|
309
|
+
'}',
|
|
310
|
+
'',
|
|
311
|
+
'public struct SyncularQueryInputError: Error, Sendable {',
|
|
312
|
+
' public let code: String',
|
|
313
|
+
' public let message: String',
|
|
314
|
+
' public init(code: String, message: String) {',
|
|
315
|
+
' self.code = code',
|
|
316
|
+
' self.message = message',
|
|
317
|
+
' }',
|
|
318
|
+
'}',
|
|
319
|
+
].join('\n'));
|
|
320
|
+
}
|
|
186
321
|
parts.push([
|
|
187
322
|
'/// Param-bind helpers shared by the generated query runners.',
|
|
188
323
|
'enum SyncularSchemaQueryBind {',
|
|
@@ -192,20 +327,11 @@ export function emitQueriesSwiftModule(queries, hash, irVersion, enumName) {
|
|
|
192
327
|
' }',
|
|
193
328
|
'}',
|
|
194
329
|
].join('\n'));
|
|
195
|
-
if (queries.some((q) => q.orderBy !== undefined)) {
|
|
196
|
-
parts.push([
|
|
197
|
-
'/// §6 orderBy direction (shared by every orderBy-knob query).',
|
|
198
|
-
'public enum SyncularQueryDir: String, Sendable {',
|
|
199
|
-
' case asc = "asc"',
|
|
200
|
-
' case desc = "desc"',
|
|
201
|
-
'}',
|
|
202
|
-
].join('\n'));
|
|
203
|
-
}
|
|
204
330
|
for (const query of queries) {
|
|
331
|
+
const syqlTypes = emitSyqlSwiftTypes(query);
|
|
332
|
+
if (syqlTypes.length > 0)
|
|
333
|
+
parts.push(syqlTypes.join('\n'));
|
|
205
334
|
parts.push(emitRowStruct(query).join('\n'));
|
|
206
|
-
const orderByEnum = emitOrderByEnum(query);
|
|
207
|
-
if (orderByEnum.length > 0)
|
|
208
|
-
parts.push(orderByEnum.join('\n'));
|
|
209
335
|
}
|
|
210
336
|
const enumLines = [];
|
|
211
337
|
enumLines.push('/// Typed named queries (the sqlc/SQLDelight tier).');
|