@syncular/typegen 0.5.0 → 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.
Files changed (47) hide show
  1. package/README.md +75 -33
  2. package/dist/cli.js +43 -17
  3. package/dist/emit-queries-dart.js +167 -55
  4. package/dist/emit-queries-kotlin.js +166 -55
  5. package/dist/emit-queries-swift.js +182 -57
  6. package/dist/emit-queries.js +289 -123
  7. package/dist/fmt.d.ts +2 -2
  8. package/dist/fmt.js +323 -316
  9. package/dist/generate.d.ts +1 -1
  10. package/dist/generate.js +28 -9
  11. package/dist/index.d.ts +3 -1
  12. package/dist/index.js +3 -1
  13. package/dist/lsp.d.ts +1 -3
  14. package/dist/lsp.js +359 -185
  15. package/dist/manifest.d.ts +1 -3
  16. package/dist/manifest.js +1 -1
  17. package/dist/query-ir.js +53 -42
  18. package/dist/query.d.ts +115 -63
  19. package/dist/query.js +60 -11
  20. package/dist/syql-lexer.d.ts +2 -0
  21. package/dist/syql-lexer.js +9 -2
  22. package/dist/syql-lowering.d.ts +22 -0
  23. package/dist/syql-lowering.js +411 -0
  24. package/dist/syql-semantics.d.ts +76 -0
  25. package/dist/syql-semantics.js +551 -0
  26. package/dist/syql-validator.d.ts +35 -0
  27. package/dist/syql-validator.js +966 -0
  28. package/package.json +3 -3
  29. package/src/cli.ts +51 -21
  30. package/src/emit-queries-dart.ts +195 -69
  31. package/src/emit-queries-kotlin.ts +197 -69
  32. package/src/emit-queries-swift.ts +224 -68
  33. package/src/emit-queries.ts +413 -165
  34. package/src/fmt.ts +377 -303
  35. package/src/generate.ts +43 -9
  36. package/src/index.ts +3 -1
  37. package/src/lsp.ts +425 -215
  38. package/src/manifest.ts +2 -4
  39. package/src/query-ir.ts +52 -42
  40. package/src/query.ts +199 -79
  41. package/src/syql-lexer.ts +12 -1
  42. package/src/syql-lowering.ts +638 -0
  43. package/src/syql-semantics.ts +859 -0
  44. package/src/syql-validator.ts +1492 -0
  45. package/dist/syql.d.ts +0 -102
  46. package/dist/syql.js +0 -1141
  47. 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 isOptionalParam(query, p) {
70
- return (p.optional === true ||
71
- p.flag === true ||
72
- (query.limit !== undefined && p.name === 'limit'));
73
- }
74
- /** Per-query orderBy allowlist enum (column = the checked SQL column). */
75
- function emitOrderByEnum(query) {
76
- if (query.orderBy === undefined)
77
- return [];
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
- lines.push(`/** §6 orderBy allowlist for ${query.name} checked at generate time. */`);
80
- lines.push(`enum class ${typeName(query.name)}OrderBy(val column: String) {`);
81
- lines.push(`${query.orderBy.allowed
82
- .map((col) => ` ${camelCase(col.langName)}(${quote(col.name)})`)
83
- .join(',\n')};`);
84
- lines.push('}');
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
- if (query.orderBy !== undefined) {
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
- if (isOptionalParam(query, p)) {
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
- if (query.orderBy !== undefined) {
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 isOptionalParam(query, p)
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 isOptionalParam(query, p) {
73
- return (p.optional === true ||
74
- p.flag === true ||
75
- (query.limit !== undefined && p.name === 'limit'));
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
- if (query.orderBy !== undefined) {
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
- if (isOptionalParam(query, p)) {
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 sqlExpr = query.orderBy !== undefined
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 isOptionalParam(query, p)
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).');