@syncular/typegen 0.15.12 → 0.15.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,27 @@ const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
18
18
  function propertyKey(name) {
19
19
  return IDENTIFIER_RE.test(name) ? name : quote(name);
20
20
  }
21
+ function emitResultMapper(query, Row) {
22
+ const booleans = query.columns.filter((column) => column.type === 'boolean');
23
+ const lines = [
24
+ `/** Decode one storage-shaped result row for ${quote(query.name)}. */`,
25
+ `export function ${query.name}MapRow(row: Readonly<Record<string, unknown>>): ${Row} {`,
26
+ ];
27
+ if (booleans.length === 0) {
28
+ lines.push(` return row as unknown as ${Row};`);
29
+ }
30
+ else {
31
+ lines.push(' return {', ' ...row,');
32
+ for (const column of booleans) {
33
+ const access = `row[${quote(column.langName)}]`;
34
+ const decoded = `decodeQueryBoolean(${access}, ${quote(query.name)}, ${quote(column.langName)})`;
35
+ lines.push(` ${propertyKey(column.langName)}: ${column.nullable ? `${access} === null ? null : ${decoded}` : decoded},`);
36
+ }
37
+ lines.push(` } as unknown as ${Row};`);
38
+ }
39
+ lines.push('}');
40
+ return lines;
41
+ }
21
42
  /** A named-query param value is the SqlValue subset its type maps to. */
22
43
  const PARAM_TS_TYPE = TS_TYPE;
23
44
  const SYQL_PARAM_TS_TYPE = {
@@ -161,6 +182,7 @@ function emitSyqlQuery(query, hash) {
161
182
  lines.push(` ${propertyKey(column.langName)}: ${TS_TYPE[column.type]}${column.nullable ? ' | null' : ''};`);
162
183
  }
163
184
  lines.push('}', '');
185
+ lines.push(...emitResultMapper(query, Row), '');
164
186
  for (const input of inputs) {
165
187
  if (input.kind === 'group') {
166
188
  lines.push(`export interface ${pascalCase(query.name)}${pascalCase(input.langName)} {`);
@@ -240,12 +262,12 @@ function emitSyqlQuery(query, hash) {
240
262
  : requiresParams
241
263
  ? `, params: ${Params}`
242
264
  : `, params?: ${Params}`;
243
- lines.push(`/** Run the ${quote(query.name)} named query (SELECT-only). */`, `export async function ${query.name}(client: QueryClient${runnerParam}): Promise<${Row}[]> {`, ` const selected = ${query.name}Select(${hasParams ? 'params' : ''});`, ' const rows = await client.query(selected.sql, selected.bind);', ` return rows as unknown as ${Row}[];`, '}', '');
265
+ lines.push(`/** Run the ${quote(query.name)} named query (SELECT-only). */`, `export async function ${query.name}(client: QueryClient${runnerParam}): Promise<${Row}[]> {`, ` const selected = ${query.name}Select(${hasParams ? 'params' : ''});`, ' const rows = await client.query(selected.sql, selected.bind);', ` return rows.map((row) => ${query.name}MapRow(row as Readonly<Record<string, unknown>>));`, '}', '');
244
266
  const paramsType = hasParams ? Params : 'undefined';
245
267
  const defaultStatement = metadata.plan.statements.find((statement) => (statement.activationMask === undefined ||
246
268
  statement.activationMask === 0) &&
247
269
  (sort?.kind !== 'sort' || statement.sortProfile === sort.defaultProfile));
248
- lines.push(`/** Revisioned reactive descriptor for \`useQuery(${query.name}Query${hasParams ? ', params' : ''})\`. */`, `export const ${query.name}Query: NamedQuery<${Row}, ${paramsType}> = {`, ` id: ${quote(`${hash}/${query.name}`)},`, ` hasParams: ${hasParams},`, ` sql: ${quote(defaultStatement?.positionalSql ?? query.positionalSql)},`);
270
+ lines.push(`/** Revisioned reactive descriptor for \`useQuery(${query.name}Query${hasParams ? ', params' : ''})\`. */`, `export const ${query.name}Query: NamedQuery<${Row}, ${paramsType}> = {`, ` id: ${quote(`${hash}/${query.name}`)},`, ` hasParams: ${hasParams},`, ` sql: ${quote(defaultStatement?.positionalSql ?? query.positionalSql)},`, ` mapRow: ${query.name}MapRow,`);
249
271
  if (hasParams) {
250
272
  lines.push(` sqlFor: (params: ${Params}) => ${query.name}Select(params).sql,`);
251
273
  }
@@ -297,6 +319,8 @@ function emitQuery(query, hash) {
297
319
  }
298
320
  lines.push('}');
299
321
  lines.push('');
322
+ lines.push(...emitResultMapper(query, Row));
323
+ lines.push('');
300
324
  const hasParams = query.params.length > 0;
301
325
  if (hasParams) {
302
326
  lines.push(`/** Named parameters for ${quote(query.name)}. */`);
@@ -324,7 +348,7 @@ function emitQuery(query, hash) {
324
348
  else {
325
349
  lines.push(` const rows = await client.query(${sqlConst});`);
326
350
  }
327
- lines.push(` return rows as unknown as ${Row}[];`);
351
+ lines.push(` return rows.map((row) => ${query.name}MapRow(row as Readonly<Record<string, unknown>>));`);
328
352
  lines.push('}');
329
353
  lines.push('');
330
354
  lines.push(`/** Revisioned reactive descriptor for \`useQuery(${query.name}Query${hasParams ? ', params' : ''})\`. */`);
@@ -333,6 +357,7 @@ function emitQuery(query, hash) {
333
357
  lines.push(` id: ${quote(`${hash}/${query.name}`)},`);
334
358
  lines.push(` hasParams: ${hasParams},`);
335
359
  lines.push(` sql: ${sqlConst},`);
360
+ lines.push(` mapRow: ${query.name}MapRow,`);
336
361
  lines.push(` tables: ${query.name}Tables,`);
337
362
  const reactiveUsesParams = query.reactive.dependencies.some((dependency) => dependency.scopes.some((scope) => scope.params.length > 0));
338
363
  lines.push(` dependencies: (${reactiveUsesParams ? 'params' : ''}) => [`);
@@ -398,6 +423,24 @@ export function emitQueriesModule(queries, hash, irVersion) {
398
423
  '}',
399
424
  ].join('\n'));
400
425
  }
426
+ if (queries.some((query) => query.columns.some((column) => column.type === 'boolean'))) {
427
+ parts.push([
428
+ '/** A generated named-query row did not match its analyzed result type. */',
429
+ 'export class QueryResultDecodeError extends TypeError {',
430
+ " readonly name = 'QueryResultDecodeError';",
431
+ ' constructor(readonly query: string, readonly column: string) {',
432
+ " super(query + ': result column ' + column + ' is not a SQLite boolean');",
433
+ ' }',
434
+ '}',
435
+ '',
436
+ '/** Lift a SQLite boolean: preserve booleans, map 0 to false and finite non-zero numbers to true. */',
437
+ 'function decodeQueryBoolean(value: unknown, query: string, column: string): boolean {',
438
+ " if (typeof value === 'boolean') return value;",
439
+ " if (typeof value === 'number' && Number.isFinite(value)) return value !== 0;",
440
+ ' throw new QueryResultDecodeError(query, column);',
441
+ '}',
442
+ ].join('\n'));
443
+ }
401
444
  parts.push([
402
445
  "/** A bindable SQL param/row value (the wrapper's SqlValue subset). */",
403
446
  'export type QueryValue =',
@@ -427,6 +470,7 @@ export function emitQueriesModule(queries, hash, irVersion) {
427
470
  ' readonly id: string;',
428
471
  ' readonly hasParams: boolean;',
429
472
  ' readonly sql: string;',
473
+ ' readonly mapRow: (row: Readonly<Record<string, unknown>>) => Row;',
430
474
  ' readonly tables: readonly string[];',
431
475
  ' readonly bind: (params: Params) => readonly QueryValue[];',
432
476
  ' readonly sqlFor?: (params: Params) => string;',
package/dist/generate.js CHANGED
@@ -11,6 +11,7 @@ import { emitKotlinModule } from './emit-kotlin.js';
11
11
  import { emitQueriesModule } from './emit-queries.js';
12
12
  import { emitQueriesDartModule } from './emit-queries-dart.js';
13
13
  import { emitQueriesKotlinModule } from './emit-queries-kotlin.js';
14
+ import { emitQueriesRustModule } from './emit-queries-rust.js';
14
15
  import { emitQueriesSwiftModule } from './emit-queries-swift.js';
15
16
  import { emitSwiftModule } from './emit-swift.js';
16
17
  import { TypegenError } from './errors.js';
@@ -347,6 +348,8 @@ export function generate(manifestDir) {
347
348
  targets.push('kotlin');
348
349
  if (manifest.output.dart !== undefined)
349
350
  targets.push('dart');
351
+ if (manifest.output.rust !== undefined)
352
+ targets.push('rust');
350
353
  const naming = {
351
354
  naming: manifest.naming,
352
355
  targets,
@@ -365,7 +368,7 @@ export function generate(manifestDir) {
365
368
  { path: modulePath, content: module },
366
369
  ];
367
370
  // Opt-in native emitters — each present only when the manifest requests it.
368
- const { queryIr: queryIrPath, queries: tsQueriesPath, swift, kotlin, dart, } = manifest.output;
371
+ const { queryIr: queryIrPath, queries: tsQueriesPath, swift, kotlin, dart, rust, } = manifest.output;
369
372
  // Named queries: analyzed once (SELECT-only, typed against the IR via
370
373
  // SQLite), then emitted per-language into its OWN file so schema-only
371
374
  // consumers never churn. Only analyzed when SOME query output is requested.
@@ -373,7 +376,8 @@ export function generate(manifestDir) {
373
376
  tsQueriesPath !== undefined ||
374
377
  swift?.queriesPath !== undefined ||
375
378
  kotlin?.queriesPath !== undefined ||
376
- dart?.queriesPath !== undefined;
379
+ dart?.queriesPath !== undefined ||
380
+ rust?.queriesPath !== undefined;
377
381
  const analyzedQueries = wantsQueries
378
382
  ? analyzeQueries(ir, loadQueries(resolve(manifestDir, manifest.queries)), naming, resolve(manifestDir, manifest.queries))
379
383
  : [];
@@ -430,6 +434,12 @@ export function generate(manifestDir) {
430
434
  });
431
435
  }
432
436
  }
437
+ if (rust !== undefined) {
438
+ outputs.push({
439
+ path: resolve(manifestDir, rust.queriesPath),
440
+ content: emitQueriesRustModule(analyzedQueries, queryHash, ir.irVersion, rust.clientCrate),
441
+ });
442
+ }
433
443
  return {
434
444
  ir,
435
445
  irJson,
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export * from './emit-kotlin.js';
9
9
  export * from './emit-queries.js';
10
10
  export * from './emit-queries-dart.js';
11
11
  export * from './emit-queries-kotlin.js';
12
+ export * from './emit-queries-rust.js';
12
13
  export * from './emit-queries-swift.js';
13
14
  export * from './emit-swift.js';
14
15
  export * from './errors.js';
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ export * from './emit-kotlin.js';
9
9
  export * from './emit-queries.js';
10
10
  export * from './emit-queries-dart.js';
11
11
  export * from './emit-queries-kotlin.js';
12
+ export * from './emit-queries-rust.js';
12
13
  export * from './emit-queries-swift.js';
13
14
  export * from './emit-swift.js';
14
15
  export * from './errors.js';
package/dist/lsp.js CHANGED
@@ -186,6 +186,8 @@ export class SyqlLanguageServer {
186
186
  targets.push('kotlin');
187
187
  if (manifest.output.dart !== undefined)
188
188
  targets.push('dart');
189
+ if (manifest.output.rust !== undefined)
190
+ targets.push('rust');
189
191
  const lookup = {
190
192
  kind: 'ready',
191
193
  context: {
@@ -1,4 +1,4 @@
1
- import type { NamingMode } from './naming.js';
1
+ import { type NamingMode } from './naming.js';
2
2
  /** §7/§8 `.syql` conditional-lowering backend selection. */
3
3
  export type ManifestQueryBackend = 'neutralize' | 'variants' | 'auto';
4
4
  export declare const MANIFEST_FILENAME = "syncular.json";
@@ -53,6 +53,13 @@ export interface DartOutput {
53
53
  /** Opt-in named-queries output path (a sibling `.dart` file). */
54
54
  readonly queriesPath?: string;
55
55
  }
56
+ /** Rust named-query output. Rust consumes neutral schema IR at runtime, so
57
+ * revision 1 has no separate generated schema source path. */
58
+ export interface RustOutput {
59
+ readonly queriesPath: string;
60
+ /** Rust module identifier for the `syncular-client` Cargo dependency. */
61
+ readonly clientCrate: string;
62
+ }
56
63
  export interface ManifestOutput {
57
64
  readonly ir: string;
58
65
  readonly module: string;
@@ -66,6 +73,7 @@ export interface ManifestOutput {
66
73
  readonly swift?: SwiftOutput;
67
74
  readonly kotlin?: KotlinOutput;
68
75
  readonly dart?: DartOutput;
76
+ readonly rust?: RustOutput;
69
77
  }
70
78
  export interface Manifest {
71
79
  readonly manifestVersion: 1;
package/dist/manifest.js CHANGED
@@ -28,19 +28,21 @@
28
28
  * placeholders (partial templates are unsupported).
29
29
  * - `output.ir` / `output.module` are the always-emitted TS defaults;
30
30
  * `output.queryIr` opts into the deterministic analyzed-query document.
31
- * `output.swift` / `output.kotlin` / `output.dart` are OPT-IN native
31
+ * `output.swift` / `output.kotlin` / `output.dart` are OPT-IN native schema
32
32
  * emitters — a bare string is the output path; an object carries
33
33
  * language-appropriate options (Kotlin `package`/`objectName`, Swift
34
34
  * `enumName`; Dart takes `path` only). This is additive within the `output`
35
35
  * object (the same forward-extension shape `output.ir`/`output.module`
36
36
  * already use); no `manifestVersion` bump — new *recognized* keys, not
37
37
  * tolerated-unknown ones. Absent → that language is not generated (TS
38
- * stays the default).
38
+ * stays the default). `output.rust.queriesPath` opts into Rust named-query
39
+ * source over the neutral schema IR consumed by `syncular-client`.
39
40
  * - Unknown keys are hard errors everywhere (fail loud; growth happens by
40
41
  * bumping `manifestVersion`), except inside `extensions`, the reserved
41
42
  * WP-49 passthrough slot copied verbatim into the IR.
42
43
  */
43
44
  import { TypegenError } from './errors.js';
45
+ import { isRustKeyword } from './naming.js';
44
46
  export const MANIFEST_FILENAME = 'syncular.json';
45
47
  const SOURCE = MANIFEST_FILENAME;
46
48
  function fail(message) {
@@ -137,6 +139,21 @@ function parseDartOutput(value) {
137
139
  : {}),
138
140
  };
139
141
  }
142
+ const RUST_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
143
+ function parseRustOutput(value) {
144
+ const obj = asObject(value, 'output.rust');
145
+ rejectUnknownKeys(obj, ['queriesPath', 'clientCrate'], 'output.rust');
146
+ const clientCrate = obj.clientCrate === undefined
147
+ ? 'syncular_client'
148
+ : asString(obj.clientCrate, 'output.rust.clientCrate');
149
+ if (!RUST_IDENTIFIER_RE.test(clientCrate) || isRustKeyword(clientCrate)) {
150
+ fail(`output.rust.clientCrate must be one Rust identifier, got ${JSON.stringify(clientCrate)}`);
151
+ }
152
+ return {
153
+ queriesPath: asString(obj.queriesPath, 'output.rust.queriesPath'),
154
+ clientCrate,
155
+ };
156
+ }
140
157
  function parseTable(value, index) {
141
158
  const context = `tables[${index}]`;
142
159
  const obj = asObject(value, context);
@@ -248,9 +265,10 @@ export function parseManifest(raw) {
248
265
  let swift;
249
266
  let kotlin;
250
267
  let dart;
268
+ let rust;
251
269
  if (obj.output !== undefined) {
252
270
  const output = asObject(obj.output, 'output');
253
- rejectUnknownKeys(output, ['ir', 'module', 'queryIr', 'queries', 'swift', 'kotlin', 'dart'], 'output');
271
+ rejectUnknownKeys(output, ['ir', 'module', 'queryIr', 'queries', 'swift', 'kotlin', 'dart', 'rust'], 'output');
254
272
  if (output.ir !== undefined)
255
273
  ir = asString(output.ir, 'output.ir');
256
274
  if (output.module !== undefined) {
@@ -268,6 +286,8 @@ export function parseManifest(raw) {
268
286
  kotlin = parseKotlinOutput(output.kotlin);
269
287
  if (output.dart !== undefined)
270
288
  dart = parseDartOutput(output.dart);
289
+ if (output.rust !== undefined)
290
+ rust = parseRustOutput(output.rust);
271
291
  }
272
292
  // Build `output` with only the keys that are set — `exactOptionalPropertyTypes`
273
293
  // forbids explicit `undefined` on optional properties.
@@ -279,6 +299,7 @@ export function parseManifest(raw) {
279
299
  ...(swift !== undefined ? { swift } : {}),
280
300
  ...(kotlin !== undefined ? { kotlin } : {}),
281
301
  ...(dart !== undefined ? { dart } : {}),
302
+ ...(rust !== undefined ? { rust } : {}),
282
303
  };
283
304
  if (!Array.isArray(obj.schemaVersions) || obj.schemaVersions.length === 0) {
284
305
  fail('schemaVersions must be a non-empty array (§1.5 version history)');
package/dist/naming.d.ts CHANGED
@@ -6,7 +6,17 @@ export declare function snakeToCamel(name: string): string;
6
6
  /** The emitter targets whose keyword sets we police. `ts` is included for
7
7
  * completeness (its emitters can quote any property, so its list is the
8
8
  * small set that breaks generated FUNCTION/const identifiers). */
9
- export type NamingTarget = 'ts' | 'swift' | 'kotlin' | 'dart';
9
+ export type NamingTarget = 'ts' | 'swift' | 'kotlin' | 'dart' | 'rust';
10
+ export declare function isRustKeyword(name: string): boolean;
11
+ /** Pinned Rust identifier conversion from RFC 0006. */
12
+ export declare function rustSnakeCase(name: string): string;
13
+ export declare function rustPascalCase(name: string): string;
14
+ export interface RustNameMapping {
15
+ readonly langName: string;
16
+ readonly rustName: string;
17
+ }
18
+ /** Validate and map QueryIR runtime names to emitted Rust identifiers. */
19
+ export declare function buildRustNamingMap(langNames: readonly string[], context: string, scope: string): RustNameMapping[];
10
20
  /** One naming-map entry: the SQL-truth name and its language-facing name. */
11
21
  export interface NameMapping {
12
22
  readonly sqlName: string;
package/dist/naming.js CHANGED
@@ -202,7 +202,130 @@ const TARGET_KEYWORDS = {
202
202
  'while',
203
203
  'with',
204
204
  ]),
205
+ rust: new Set([
206
+ 'abstract',
207
+ 'as',
208
+ 'async',
209
+ 'await',
210
+ 'become',
211
+ 'box',
212
+ 'break',
213
+ 'const',
214
+ 'continue',
215
+ 'crate',
216
+ 'do',
217
+ 'dyn',
218
+ 'else',
219
+ 'enum',
220
+ 'extern',
221
+ 'false',
222
+ 'final',
223
+ 'fn',
224
+ 'for',
225
+ 'gen',
226
+ 'if',
227
+ 'impl',
228
+ 'in',
229
+ 'let',
230
+ 'loop',
231
+ 'macro',
232
+ 'match',
233
+ 'mod',
234
+ 'move',
235
+ 'mut',
236
+ 'override',
237
+ 'priv',
238
+ 'pub',
239
+ 'ref',
240
+ 'return',
241
+ 'self',
242
+ 'static',
243
+ 'struct',
244
+ 'super',
245
+ 'trait',
246
+ 'true',
247
+ 'try',
248
+ 'type',
249
+ 'typeof',
250
+ 'unsafe',
251
+ 'unsized',
252
+ 'use',
253
+ 'virtual',
254
+ 'where',
255
+ 'while',
256
+ 'yield',
257
+ ]),
205
258
  };
259
+ export function isRustKeyword(name) {
260
+ return TARGET_KEYWORDS.rust.has(name);
261
+ }
262
+ /** Pinned Rust identifier conversion from RFC 0006. */
263
+ export function rustSnakeCase(name) {
264
+ if (!/^_*[A-Za-z][A-Za-z0-9_]*$/.test(name))
265
+ return name;
266
+ const lead = /^_*/.exec(name)?.[0] ?? '';
267
+ const withoutLead = name.slice(lead.length);
268
+ const trail = /_*$/.exec(withoutLead)?.[0] ?? '';
269
+ const middle = withoutLead.slice(0, withoutLead.length - trail.length);
270
+ const out = [];
271
+ for (let index = 0; index < middle.length; index++) {
272
+ const char = middle[index];
273
+ if (char === '_') {
274
+ if (out.length > 0 && out[out.length - 1] !== '_')
275
+ out.push('_');
276
+ continue;
277
+ }
278
+ const previous = index > 0 ? middle[index - 1] : undefined;
279
+ const next = index + 1 < middle.length ? middle[index + 1] : undefined;
280
+ const uppercase = char >= 'A' && char <= 'Z';
281
+ const previousLowerOrDigit = previous !== undefined && /[a-z0-9]/.test(previous);
282
+ const acronymBoundary = uppercase &&
283
+ previous !== undefined &&
284
+ /[A-Z]/.test(previous) &&
285
+ next !== undefined &&
286
+ /[a-z]/.test(next);
287
+ if (uppercase &&
288
+ (previousLowerOrDigit || acronymBoundary) &&
289
+ out.length > 0 &&
290
+ out[out.length - 1] !== '_') {
291
+ out.push('_');
292
+ }
293
+ out.push(char.toLowerCase());
294
+ }
295
+ return lead + out.join('') + trail;
296
+ }
297
+ export function rustPascalCase(name) {
298
+ const snake = rustSnakeCase(name);
299
+ const lead = /^_*/.exec(snake)?.[0] ?? '';
300
+ const bare = snake.slice(lead.length).replace(/_*$/, '');
301
+ const suffix = snake.slice(lead.length + bare.length);
302
+ return (lead +
303
+ bare
304
+ .split('_')
305
+ .filter((part) => part.length > 0)
306
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
307
+ .join('') +
308
+ suffix);
309
+ }
310
+ /** Validate and map QueryIR runtime names to emitted Rust identifiers. */
311
+ export function buildRustNamingMap(langNames, context, scope) {
312
+ const seen = new Map();
313
+ return langNames.map((langName) => {
314
+ const rustName = rustSnakeCase(langName);
315
+ if (!/^_*[a-z][a-z0-9_]*$/.test(rustName)) {
316
+ throw new TypegenError(context, `${scope}: ${JSON.stringify(langName)} cannot be emitted as a Rust identifier — alias it in SQL (AS)`);
317
+ }
318
+ if (TARGET_KEYWORDS.rust.has(rustName)) {
319
+ throw new TypegenError(context, `${scope}: ${JSON.stringify(langName)} maps to ${JSON.stringify(rustName)}, a reserved word on the rust target — alias it in SQL (AS)`);
320
+ }
321
+ const clash = seen.get(rustName);
322
+ if (clash !== undefined && clash !== langName) {
323
+ throw new TypegenError(context, `${scope}: ${JSON.stringify(clash)} and ${JSON.stringify(langName)} both map to ${JSON.stringify(rustName)} on the rust target — alias one in SQL (AS)`);
324
+ }
325
+ seen.set(rustName, langName);
326
+ return { langName, rustName };
327
+ });
328
+ }
206
329
  /**
207
330
  * Map a set of SQL names to language names under `mode`, enforcing the §12
208
331
  * hard errors within the scope (one table's columns / one query's projection
@@ -223,8 +346,9 @@ export function buildNamingMap(sqlNames, mode, context, scope, targets) {
223
346
  throw new TypegenError(context, `${scope}: ${JSON.stringify(clash)} and ${JSON.stringify(sqlName)} both map to ${JSON.stringify(langName)} under camelCase naming — rename one, alias it in SQL (AS), or set "naming": "preserve" in syncular.json`);
224
347
  }
225
348
  for (const target of targets) {
226
- if (TARGET_KEYWORDS[target].has(langName)) {
227
- throw new TypegenError(context, `${scope}: ${JSON.stringify(sqlName)} maps to ${JSON.stringify(langName)}, a reserved word on the ${target} target — rename it, alias it in SQL (AS), or set "naming": "preserve" in syncular.json`);
349
+ const targetName = target === 'rust' ? rustSnakeCase(langName) : langName;
350
+ if (TARGET_KEYWORDS[target].has(targetName)) {
351
+ throw new TypegenError(context, `${scope}: ${JSON.stringify(sqlName)} maps to ${JSON.stringify(targetName)}, a reserved word on the ${target} target — rename it, alias it in SQL (AS), or set "naming": "preserve" in syncular.json`);
228
352
  }
229
353
  if (target === 'dart' && langName.startsWith('_')) {
230
354
  throw new TypegenError(context, `${scope}: ${JSON.stringify(sqlName)} maps to ${JSON.stringify(langName)} — a leading underscore is library-private on the dart target. Alias it in SQL (AS) or set "naming": "preserve" in syncular.json`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/typegen",
3
- "version": "0.15.12",
3
+ "version": "0.15.14",
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.15.12",
52
- "@syncular/server": "0.15.12"
51
+ "@syncular/core": "0.15.14",
52
+ "@syncular/server": "0.15.14"
53
53
  }
54
54
  }