@syncular/typegen 0.5.1 → 0.6.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 +75 -33
- package/dist/cli.js +43 -17
- package/dist/emit-queries-dart.js +167 -55
- package/dist/emit-queries-kotlin.js +166 -55
- package/dist/emit-queries-swift.js +182 -57
- package/dist/emit-queries.js +289 -123
- package/dist/fmt.d.ts +2 -2
- package/dist/fmt.js +323 -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 +359 -185
- 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 +115 -63
- package/dist/query.js +60 -11
- 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 +411 -0
- package/dist/syql-semantics.d.ts +76 -0
- package/dist/syql-semantics.js +551 -0
- package/dist/syql-validator.d.ts +35 -0
- package/dist/syql-validator.js +966 -0
- package/package.json +3 -3
- package/src/cli.ts +51 -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 +413 -165
- package/src/fmt.ts +377 -303
- package/src/generate.ts +43 -9
- package/src/index.ts +3 -1
- package/src/lsp.ts +425 -215
- package/src/manifest.ts +2 -4
- package/src/query-ir.ts +52 -42
- package/src/query.ts +199 -79
- package/src/syql-lexer.ts +12 -1
- package/src/syql-lowering.ts +638 -0
- package/src/syql-semantics.ts +859 -0
- package/src/syql-validator.ts +1492 -0
- package/dist/syql.d.ts +0 -102
- package/dist/syql.js +0 -1141
- package/src/syql.ts +0 -1470
|
@@ -66,22 +66,151 @@ 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 === 'switch')
|
|
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 === 'page')
|
|
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.required) {
|
|
120
|
+
return input.nullable
|
|
121
|
+
? optionalParamValue(input.type, access)
|
|
122
|
+
: paramValue(input.type, access);
|
|
123
|
+
}
|
|
124
|
+
return input.nullable
|
|
125
|
+
? syqlNullablePresenceBind(input.type, access)
|
|
126
|
+
: optionalParamValue(input.type, access);
|
|
127
|
+
}
|
|
128
|
+
function emitSyqlKotlinTypes(query) {
|
|
78
129
|
const lines = [];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
130
|
+
for (const input of query.syql?.inputs ?? []) {
|
|
131
|
+
if (input.kind === 'group') {
|
|
132
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
133
|
+
lines.push(`data class ${name}(`);
|
|
134
|
+
for (const member of input.members) {
|
|
135
|
+
lines.push(` val ${camelCase(member.langName)}: ${syqlKotlinType(member.type, member.nullable)},`);
|
|
136
|
+
}
|
|
137
|
+
lines.push(')', '');
|
|
138
|
+
}
|
|
139
|
+
else if (input.kind === 'sort') {
|
|
140
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
141
|
+
lines.push(`enum class ${name}(val index: Int) {`);
|
|
142
|
+
lines.push(`${input.profiles
|
|
143
|
+
.map((profile, index) => ` ${camelCase(profile.langName)}(${index})`)
|
|
144
|
+
.join(',\n')};`);
|
|
145
|
+
lines.push('}', '');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (lines[lines.length - 1] === '')
|
|
149
|
+
lines.pop();
|
|
150
|
+
return lines;
|
|
151
|
+
}
|
|
152
|
+
function emitSyqlKotlinRunner(query) {
|
|
153
|
+
const metadata = query.syql;
|
|
154
|
+
if (metadata === undefined)
|
|
155
|
+
throw new Error('missing SYQL metadata');
|
|
156
|
+
const Row = `${typeName(query.name)}Row`;
|
|
157
|
+
const lines = [];
|
|
158
|
+
lines.push(` val ${query.name}Tables = listOf(${query.tables.map(quote).join(', ')})`, '', ` /** Run the ${query.name} revision-1 SYQL query. */`);
|
|
159
|
+
const args = ['client: SyncularClient'];
|
|
160
|
+
for (const input of metadata.inputs) {
|
|
161
|
+
const name = camelCase(input.langName);
|
|
162
|
+
if (input.kind === 'value') {
|
|
163
|
+
const type = syqlKotlinType(input.type, input.nullable);
|
|
164
|
+
if (input.required)
|
|
165
|
+
args.push(`${name}: ${type}`);
|
|
166
|
+
else if (input.nullable) {
|
|
167
|
+
args.push(`${name}: SyncularQueryPresence<${type}> = SyncularQueryPresence.Absent`);
|
|
168
|
+
}
|
|
169
|
+
else
|
|
170
|
+
args.push(`${name}: ${type}? = null`);
|
|
171
|
+
}
|
|
172
|
+
else if (input.kind === 'group') {
|
|
173
|
+
args.push(`${name}: ${typeName(query.name)}${typeName(input.langName)}? = null`);
|
|
174
|
+
}
|
|
175
|
+
else if (input.kind === 'switch') {
|
|
176
|
+
args.push(`${name}: Boolean = false`);
|
|
177
|
+
}
|
|
178
|
+
else if (input.kind === 'sort') {
|
|
179
|
+
const defaultCase = input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
180
|
+
?.langName ?? input.defaultProfile;
|
|
181
|
+
const type = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
182
|
+
args.push(`${name}: ${type} = ${type}.${camelCase(defaultCase)}`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
args.push(`${name}: Long? = null`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
lines.push(` fun ${query.name}(${args.join(', ')}): List<${Row}> {`);
|
|
189
|
+
const page = metadata.inputs.find((input) => input.kind === 'page');
|
|
190
|
+
if (page?.kind === 'page') {
|
|
191
|
+
const name = camelCase(page.langName);
|
|
192
|
+
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_PAGE", ${quote(`${query.name}: invalid page size`)})`, ' }');
|
|
193
|
+
}
|
|
194
|
+
if (metadata.plan.backend === 'variants') {
|
|
195
|
+
lines.push(' var activationMask = 0');
|
|
196
|
+
metadata.plan.activationControls.forEach((control, index) => {
|
|
197
|
+
lines.push(` if (${syqlControlActive(query, control)}) activationMask = activationMask or ${2 ** index}`);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const sort = metadata.inputs.find((input) => input.kind === 'sort');
|
|
201
|
+
const profileCount = sort?.kind === 'sort' ? sort.profiles.length : 1;
|
|
202
|
+
const sortIndex = sort?.kind === 'sort' ? `${camelCase(sort.langName)}.index` : '0';
|
|
203
|
+
const index = metadata.plan.backend === 'variants'
|
|
204
|
+
? `activationMask * ${profileCount} + ${sortIndex}`
|
|
205
|
+
: sortIndex;
|
|
206
|
+
lines.push(` val statementIndex = ${index}`, ' val selected: Pair<String, List<JsonValue>> = when (statementIndex) {');
|
|
207
|
+
metadata.plan.statements.forEach((statement, statementIndex) => {
|
|
208
|
+
const binds = statement.binds
|
|
209
|
+
.map((bind) => syqlBindExpr(query, bind))
|
|
210
|
+
.join(', ');
|
|
211
|
+
lines.push(` ${statementIndex} -> Pair(${quote(statement.positionalSql)}, listOf(${binds}))`);
|
|
212
|
+
});
|
|
213
|
+
lines.push(' else -> error("invalid generated SYQL statement index")', ' }', ` return client.query(selected.first, selected.second).mapNotNull { ${Row}.fromRow(it) }`, ' }');
|
|
85
214
|
return lines;
|
|
86
215
|
}
|
|
87
216
|
function emitDataClass(query) {
|
|
@@ -114,51 +243,29 @@ function emitDataClass(query) {
|
|
|
114
243
|
return lines;
|
|
115
244
|
}
|
|
116
245
|
function emitRunner(query) {
|
|
246
|
+
if (query.syql !== undefined)
|
|
247
|
+
return emitSyqlKotlinRunner(query);
|
|
117
248
|
const Row = `${typeName(query.name)}Row`;
|
|
118
249
|
const lines = [];
|
|
119
250
|
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
|
-
}
|
|
251
|
+
lines.push(` private const val ${query.name}Sql = ${quote(query.positionalSql)}`);
|
|
126
252
|
lines.push('');
|
|
127
253
|
lines.push(` /** Run the ${query.name} named query (SELECT-only). */`);
|
|
128
254
|
const args = [];
|
|
129
255
|
for (const p of query.params) {
|
|
130
256
|
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()}`);
|
|
257
|
+
args.push(`${name}: ${KOTLIN_TYPE[p.type]}`);
|
|
143
258
|
}
|
|
144
259
|
const signature = args.length > 0
|
|
145
260
|
? `client: SyncularClient, ${args.join(', ')}`
|
|
146
261
|
: 'client: SyncularClient';
|
|
147
262
|
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`;
|
|
263
|
+
const sqlRef = `${query.name}Sql`;
|
|
155
264
|
if (query.params.length > 0) {
|
|
156
265
|
const binds = query.params
|
|
157
266
|
.map((p) => {
|
|
158
267
|
const name = camelCase(p.langName);
|
|
159
|
-
return
|
|
160
|
-
? optionalParamValue(p.type, name)
|
|
161
|
-
: paramValue(p.type, name);
|
|
268
|
+
return paramValue(p.type, name);
|
|
162
269
|
})
|
|
163
270
|
.join(', ');
|
|
164
271
|
lines.push(` val params = listOf(${binds})`);
|
|
@@ -182,6 +289,19 @@ export function emitQueriesKotlinModule(queries, hash, irVersion, packageName, o
|
|
|
182
289
|
'import dev.syncular.JsonValue',
|
|
183
290
|
'import dev.syncular.SyncularClient',
|
|
184
291
|
].join('\n'));
|
|
292
|
+
if (queries.some((query) => query.syql !== undefined)) {
|
|
293
|
+
parts.push([
|
|
294
|
+
'sealed class SyncularQueryPresence<out T> {',
|
|
295
|
+
' object Absent : SyncularQueryPresence<Nothing>()',
|
|
296
|
+
' data class Present<T>(val value: T) : SyncularQueryPresence<T>()',
|
|
297
|
+
'}',
|
|
298
|
+
'',
|
|
299
|
+
'class SyncularQueryInputException(',
|
|
300
|
+
' val code: String,',
|
|
301
|
+
' override val message: String,',
|
|
302
|
+
') : IllegalArgumentException(message)',
|
|
303
|
+
].join('\n'));
|
|
304
|
+
}
|
|
185
305
|
// Row-decode + bind helpers (package-private; distinct names so they don't
|
|
186
306
|
// clash with the schema file's rowBool/rowBytes in the same package).
|
|
187
307
|
parts.push([
|
|
@@ -204,20 +324,11 @@ export function emitQueriesKotlinModule(queries, hash, irVersion, packageName, o
|
|
|
204
324
|
' return JsonValue.obj("\\$bytes" to JsonValue.of(hex))',
|
|
205
325
|
'}',
|
|
206
326
|
].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
327
|
for (const query of queries) {
|
|
328
|
+
const syqlTypes = emitSyqlKotlinTypes(query);
|
|
329
|
+
if (syqlTypes.length > 0)
|
|
330
|
+
parts.push(syqlTypes.join('\n'));
|
|
217
331
|
parts.push(emitDataClass(query).join('\n'));
|
|
218
|
-
const orderByEnum = emitOrderByEnum(query);
|
|
219
|
-
if (orderByEnum.length > 0)
|
|
220
|
-
parts.push(orderByEnum.join('\n'));
|
|
221
332
|
}
|
|
222
333
|
const objLines = [];
|
|
223
334
|
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,158 @@ 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 === 'switch')
|
|
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 === 'page')
|
|
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.required) {
|
|
127
|
+
return input.nullable
|
|
128
|
+
? optionalParamValue(input.type, access)
|
|
129
|
+
: paramValue(input.type, access);
|
|
130
|
+
}
|
|
131
|
+
return input.nullable
|
|
132
|
+
? syqlNullablePresenceBind(input.type, access)
|
|
133
|
+
: optionalParamValue(input.type, access);
|
|
134
|
+
}
|
|
135
|
+
function emitSyqlSwiftTypes(query) {
|
|
136
|
+
const lines = [];
|
|
137
|
+
for (const input of query.syql?.inputs ?? []) {
|
|
138
|
+
if (input.kind === 'group') {
|
|
139
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
140
|
+
lines.push(`public struct ${name}: Sendable {`);
|
|
141
|
+
for (const member of input.members) {
|
|
142
|
+
lines.push(` public let ${camelCase(member.langName)}: ${syqlSwiftType(member.type, member.nullable)}`);
|
|
143
|
+
}
|
|
144
|
+
const args = input.members
|
|
145
|
+
.map((member) => `${camelCase(member.langName)}: ${syqlSwiftType(member.type, member.nullable)}`)
|
|
146
|
+
.join(', ');
|
|
147
|
+
lines.push(` public init(${args}) {`);
|
|
148
|
+
for (const member of input.members) {
|
|
149
|
+
const name = camelCase(member.langName);
|
|
150
|
+
lines.push(` self.${name} = ${name}`);
|
|
151
|
+
}
|
|
152
|
+
lines.push(' }', '}', '');
|
|
153
|
+
}
|
|
154
|
+
else if (input.kind === 'sort') {
|
|
155
|
+
const name = `${typeName(query.name)}${typeName(input.langName)}`;
|
|
156
|
+
lines.push(`public enum ${name}: Int, Sendable {`);
|
|
157
|
+
input.profiles.forEach((profile, index) => {
|
|
158
|
+
lines.push(` case ${camelCase(profile.langName)} = ${index}`);
|
|
159
|
+
});
|
|
160
|
+
lines.push('}', '');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (lines[lines.length - 1] === '')
|
|
164
|
+
lines.pop();
|
|
165
|
+
return lines;
|
|
166
|
+
}
|
|
167
|
+
function emitSyqlSwiftRunner(query) {
|
|
168
|
+
const metadata = query.syql;
|
|
169
|
+
if (metadata === undefined)
|
|
170
|
+
throw new Error('missing SYQL metadata');
|
|
171
|
+
const Row = `${typeName(query.name)}Row`;
|
|
172
|
+
const lines = [];
|
|
173
|
+
lines.push(` public static let ${query.name}Tables = [${query.tables.map(quote).join(', ')}]`, '', ` /// Run the ${query.name} revision-1 SYQL query.`);
|
|
174
|
+
const args = ['client: SyncularClient'];
|
|
175
|
+
for (const input of metadata.inputs) {
|
|
176
|
+
const name = camelCase(input.langName);
|
|
177
|
+
if (input.kind === 'value') {
|
|
178
|
+
const type = syqlSwiftType(input.type, input.nullable);
|
|
179
|
+
if (input.required)
|
|
180
|
+
args.push(`${name}: ${type}`);
|
|
181
|
+
else if (input.nullable)
|
|
182
|
+
args.push(`${name}: SyncularQueryPresence<${type}> = .absent`);
|
|
183
|
+
else
|
|
184
|
+
args.push(`${name}: ${type}? = nil`);
|
|
185
|
+
}
|
|
186
|
+
else if (input.kind === 'group') {
|
|
187
|
+
args.push(`${name}: ${typeName(query.name)}${typeName(input.langName)}? = nil`);
|
|
188
|
+
}
|
|
189
|
+
else if (input.kind === 'switch') {
|
|
190
|
+
args.push(`${name}: Bool = false`);
|
|
191
|
+
}
|
|
192
|
+
else if (input.kind === 'sort') {
|
|
193
|
+
const defaultCase = input.profiles.find((profile) => profile.name === input.defaultProfile)
|
|
194
|
+
?.langName ?? input.defaultProfile;
|
|
195
|
+
args.push(`${name}: ${typeName(query.name)}${typeName(input.langName)} = .${camelCase(defaultCase)}`);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
args.push(`${name}: Int? = nil`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
lines.push(` public static func ${query.name}(${args.join(', ')}) throws -> [${Row}] {`);
|
|
202
|
+
const page = metadata.inputs.find((input) => input.kind === 'page');
|
|
203
|
+
if (page?.kind === 'page') {
|
|
204
|
+
const name = camelCase(page.langName);
|
|
205
|
+
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_PAGE", message: ${quote(`${query.name}: invalid page size`)})`, ' }');
|
|
206
|
+
}
|
|
207
|
+
if (metadata.plan.backend === 'variants') {
|
|
208
|
+
lines.push(' var activationMask = 0');
|
|
209
|
+
metadata.plan.activationControls.forEach((control, index) => {
|
|
210
|
+
lines.push(` if ${syqlControlActive(query, control)} { activationMask |= ${2 ** index} }`);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const sort = metadata.inputs.find((input) => input.kind === 'sort');
|
|
214
|
+
const profileCount = sort?.kind === 'sort' ? sort.profiles.length : 1;
|
|
215
|
+
const sortIndex = sort?.kind === 'sort' ? `${camelCase(sort.langName)}.rawValue` : '0';
|
|
216
|
+
const index = metadata.plan.backend === 'variants'
|
|
217
|
+
? `activationMask * ${profileCount} + ${sortIndex}`
|
|
218
|
+
: sortIndex;
|
|
219
|
+
lines.push(` let statementIndex = ${index}`, ' let selected: (String, [JSONValue])', ' switch statementIndex {');
|
|
220
|
+
metadata.plan.statements.forEach((statement, statementIndex) => {
|
|
221
|
+
const binds = statement.binds
|
|
222
|
+
.map((bind) => syqlBindExpr(query, bind))
|
|
223
|
+
.join(', ');
|
|
224
|
+
lines.push(` case ${statementIndex}: selected = (${quote(statement.positionalSql)}, [${binds}])`);
|
|
225
|
+
});
|
|
226
|
+
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)`, ' }', ' }');
|
|
227
|
+
return lines;
|
|
76
228
|
}
|
|
77
229
|
function emitRowStruct(query) {
|
|
78
230
|
const Row = `${typeName(query.name)}Row`;
|
|
@@ -100,65 +252,30 @@ function emitRowStruct(query) {
|
|
|
100
252
|
lines.push('}');
|
|
101
253
|
return lines;
|
|
102
254
|
}
|
|
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
255
|
function emitRunner(query) {
|
|
256
|
+
if (query.syql !== undefined)
|
|
257
|
+
return emitSyqlSwiftRunner(query);
|
|
117
258
|
const Row = `${typeName(query.name)}Row`;
|
|
118
259
|
const lines = [];
|
|
119
260
|
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
|
-
}
|
|
261
|
+
lines.push(` private static let ${query.name}Sql = ${quote(query.positionalSql)}`);
|
|
126
262
|
lines.push('');
|
|
127
263
|
lines.push(` /// Run the ${query.name} named query (SELECT-only).`);
|
|
128
264
|
const args = [];
|
|
129
265
|
for (const p of query.params) {
|
|
130
266
|
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}`);
|
|
267
|
+
args.push(`${name}: ${SWIFT_TYPE[p.type]}`);
|
|
143
268
|
}
|
|
144
269
|
const signature = args.length > 0
|
|
145
270
|
? `client: SyncularClient, ${args.join(', ')}`
|
|
146
271
|
: 'client: SyncularClient';
|
|
147
272
|
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`;
|
|
273
|
+
const sqlRef = `${query.name}Sql`;
|
|
155
274
|
if (query.params.length > 0) {
|
|
156
275
|
const binds = query.params
|
|
157
276
|
.map((p) => {
|
|
158
277
|
const name = camelCase(p.langName);
|
|
159
|
-
return
|
|
160
|
-
? optionalParamValue(p.type, name)
|
|
161
|
-
: paramValue(p.type, name);
|
|
278
|
+
return paramValue(p.type, name);
|
|
162
279
|
})
|
|
163
280
|
.join(', ');
|
|
164
281
|
lines.push(` let params: [JSONValue] = [${binds}]`);
|
|
@@ -183,6 +300,23 @@ export function emitQueriesSwiftModule(queries, hash, irVersion, enumName) {
|
|
|
183
300
|
'import Foundation',
|
|
184
301
|
'import Syncular',
|
|
185
302
|
].join('\n'));
|
|
303
|
+
if (queries.some((query) => query.syql !== undefined)) {
|
|
304
|
+
parts.push([
|
|
305
|
+
'public enum SyncularQueryPresence<Value: Sendable>: Sendable {',
|
|
306
|
+
' case absent',
|
|
307
|
+
' case present(Value)',
|
|
308
|
+
'}',
|
|
309
|
+
'',
|
|
310
|
+
'public struct SyncularQueryInputError: Error, Sendable {',
|
|
311
|
+
' public let code: String',
|
|
312
|
+
' public let message: String',
|
|
313
|
+
' public init(code: String, message: String) {',
|
|
314
|
+
' self.code = code',
|
|
315
|
+
' self.message = message',
|
|
316
|
+
' }',
|
|
317
|
+
'}',
|
|
318
|
+
].join('\n'));
|
|
319
|
+
}
|
|
186
320
|
parts.push([
|
|
187
321
|
'/// Param-bind helpers shared by the generated query runners.',
|
|
188
322
|
'enum SyncularSchemaQueryBind {',
|
|
@@ -192,20 +326,11 @@ export function emitQueriesSwiftModule(queries, hash, irVersion, enumName) {
|
|
|
192
326
|
' }',
|
|
193
327
|
'}',
|
|
194
328
|
].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
329
|
for (const query of queries) {
|
|
330
|
+
const syqlTypes = emitSyqlSwiftTypes(query);
|
|
331
|
+
if (syqlTypes.length > 0)
|
|
332
|
+
parts.push(syqlTypes.join('\n'));
|
|
205
333
|
parts.push(emitRowStruct(query).join('\n'));
|
|
206
|
-
const orderByEnum = emitOrderByEnum(query);
|
|
207
|
-
if (orderByEnum.length > 0)
|
|
208
|
-
parts.push(orderByEnum.join('\n'));
|
|
209
334
|
}
|
|
210
335
|
const enumLines = [];
|
|
211
336
|
enumLines.push('/// Typed named queries (the sqlc/SQLDelight tier).');
|