@syncular/typegen 0.15.47 → 0.16.1

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.
@@ -0,0 +1,77 @@
1
+ import type { SyqlLexErrorCode } from './syql-lexer.js';
2
+ import type { SyqlLoweringErrorCode } from './syql-lowering.js';
3
+ import type { SyqlModuleErrorCode } from './syql-modules.js';
4
+ import type { SyqlParseErrorCode } from './syql-parser.js';
5
+ import type { SyqlSemanticErrorCode } from './syql-semantics.js';
6
+ import type { SyqlTemplateParseErrorCode } from './syql-template-parser.js';
7
+ import type { SyqlValidationErrorCode } from './syql-validator.js';
8
+ export type SyqlRuntimeErrorCode = 'SYQL_RUNTIME_MISSING_REQUIRED_INPUT' | 'SYQL_RUNTIME_UNKNOWN_INPUT' | 'SYQL_RUNTIME_INVALID_INPUT' | 'SYQL_RUNTIME_INVALID_GROUP' | 'SYQL_RUNTIME_INVALID_SORT' | 'SYQL_RUNTIME_INVALID_LIMIT';
9
+ export type SyqlDiagnosticCode = SyqlLexErrorCode | SyqlParseErrorCode | SyqlTemplateParseErrorCode | SyqlModuleErrorCode | SyqlSemanticErrorCode | SyqlValidationErrorCode | SyqlLoweringErrorCode | 'SYQL9001_PROJECT_CONTEXT' | SyqlRuntimeErrorCode;
10
+ /** Stable compiler and generated-runtime diagnostic remedies keyed by code. */
11
+ export declare const SYQL_DIAGNOSTIC_REMEDIES: {
12
+ readonly SYQL1001_UNTERMINATED_STRING: "Close the string literal with a matching single quote.";
13
+ readonly SYQL1002_UNTERMINATED_IDENTIFIER: "Close the quoted identifier with its matching delimiter.";
14
+ readonly SYQL1003_UNTERMINATED_COMMENT: "Close the block comment with */.";
15
+ readonly SYQL1004_UNTERMINATED_IMPORT_PATH: "Close the import path with a matching double quote.";
16
+ readonly SYQL2001_EXPECTED_TOKEN: "Insert the token named by the diagnostic at the reported source position.";
17
+ readonly SYQL2002_INVALID_NAME: "Rename the declaration or parameter to a valid lower-camel identifier.";
18
+ readonly SYQL2003_RESERVED_NAME: "Rename the declaration or parameter so it does not use a reserved SYQL name.";
19
+ readonly SYQL2004_DUPLICATE_NAME: "Give each declaration, parameter, group member, and profile a unique name in its scope.";
20
+ readonly SYQL2005_INVALID_IMPORT: "Use a relative .syql import with explicit imported predicate names.";
21
+ readonly SYQL2006_EMPTY_TEMPLATE: "Add a SQL expression or statement inside the braces.";
22
+ readonly SYQL2007_FORBIDDEN_SEMICOLON: "Remove the semicolon from the embedded SQL template.";
23
+ readonly SYQL2008_INVALID_MEMBER: "Use a member supported by the surrounding SYQL declaration.";
24
+ readonly SYQL2009_INVALID_INTEGER: "Use a decimal integer within the range named by the diagnostic.";
25
+ readonly SYQL2010_INVALID_PAGE_RANGE: "Choose default and maximum page sizes that satisfy the declared range.";
26
+ readonly SYQL2011_INVALID_PARAMETER: "Rewrite the parameter using a supported value, optional, range, group, sort, or limit form.";
27
+ readonly SYQL2012_INVALID_QUERY_BODY: "Rewrite the query body using the clause order and forms defined by the SYQL grammar.";
28
+ readonly SYQL3001_EXPECTED_EMBEDDED_TOKEN: "Insert the required token in the embedded SQL construct.";
29
+ readonly SYQL3002_INVALID_BIND: "Bind a declared input with the :name form in a supported SQL position.";
30
+ readonly SYQL3003_INVALID_PREDICATE_CALL: "Call the predicate with declared bind arguments and matching parentheses.";
31
+ readonly SYQL3004_INVALID_WHEN: "Use when with declared controls and a nonempty conditional SQL body.";
32
+ readonly SYQL3005_INVALID_REACTIVE_DIRECTIVE: "Rewrite the reactive directive using a supported revision-1 form.";
33
+ readonly SYQL3006_FORBIDDEN_TEMPLATE_NODE: "Remove the conditional or predicate construct from this SQL context.";
34
+ readonly SYQL3007_UNEXPECTED_BRACE: "Remove the unmatched brace or close the surrounding embedded construct.";
35
+ readonly SYQL3008_FORBIDDEN_PARAMETER_FORM: "Use a value bind in this context instead of a range, group, sort, or limit control.";
36
+ readonly SYQL4001_IMPORT_OUTSIDE_ROOT: "Move the imported file under the configured query root and use a relative path.";
37
+ readonly SYQL4002_MODULE_NOT_FOUND: "Create the imported .syql file or correct the relative import path.";
38
+ readonly SYQL4003_IMPORT_CYCLE: "Remove one import edge so predicate modules form an acyclic graph.";
39
+ readonly SYQL4004_UNKNOWN_PREDICATE: "Export and import the predicate under the referenced name.";
40
+ readonly SYQL4005_DUPLICATE_IMPORT_TARGET: "Import each local predicate name once in the module.";
41
+ readonly SYQL4006_DUPLICATE_QUERY: "Give every query in the project a unique public name.";
42
+ readonly SYQL5001_UNKNOWN_PREDICATE: "Define the predicate locally or import it under the referenced name.";
43
+ readonly SYQL5002_PREDICATE_CYCLE: "Remove the recursive predicate call so expansion terminates.";
44
+ readonly SYQL5003_PREDICATE_ARITY: "Pass exactly the number of bind arguments declared by the predicate.";
45
+ readonly SYQL5004_CLOSED_PREDICATE: "Declare each external bind as a predicate parameter and pass it at the call site.";
46
+ readonly SYQL5005_UNUSED_PREDICATE_PARAMETER: "Use the predicate parameter in its body or remove it from the signature.";
47
+ readonly SYQL5006_UNDECLARED_BIND: "Declare the bind as a query input or predicate parameter.";
48
+ readonly SYQL5007_UNUSED_INPUT: "Use the query input in SQL or remove it from the public signature.";
49
+ readonly SYQL5008_INVALID_CONTROL: "Use an optional input, group, sort, or limit name as the control.";
50
+ readonly SYQL5009_MISSING_DOMINANCE: "Guard every optional bind with a when condition that proves its presence.";
51
+ readonly SYQL5010_UNUSED_CONTROL: "Use the control in the query body or remove it from the signature.";
52
+ readonly SYQL5011_TYPE_CONFLICT: "Make every declaration and SQL use of the bind agree on one SYQL type.";
53
+ readonly SYQL6001_INVALID_PLACEMENT: "Move the SYQL construct to the SQL clause named by the diagnostic.";
54
+ readonly SYQL6002_INVALID_SQL: "Correct the reported SQLite syntax, table, column, function, or bind error.";
55
+ readonly SYQL6003_NONDETERMINISTIC_SQL: "Replace nondeterministic SQL with values supplied through query inputs or stored columns.";
56
+ readonly SYQL6004_TYPE_CONFLICT: "Align the declared input type with the schema column and SQL expression types.";
57
+ readonly SYQL6005_INVALID_SYNC_QUERY: "Select stable row identity and preserve the scope coverage required for reactive sync.";
58
+ readonly SYQL6006_INVALID_SORT: "Use declared sort profiles with deterministic ORDER BY terms and a stable tie-breaker.";
59
+ readonly SYQL6007_INVALID_LIMIT: "Use the declared limit control and keep its default and maximum within the supported range.";
60
+ readonly SYQL6008_INVALID_IDENTITY: "Declare identity columns that are selected, non-null, and sufficient to identify each result row.";
61
+ readonly SYQL7001_ENUMERATION_LIMIT: "Use the auto or neutralize backend, reduce control combinations, or raise the explicit compiler limit.";
62
+ readonly SYQL7002_INTERNAL_LOWERING: "Report the query and diagnostic to the Syncular maintainers.";
63
+ readonly SYQL9001_PROJECT_CONTEXT: "Open the file under a project with a valid syncular.json, migrations, schema output, and query root.";
64
+ readonly SYQL_RUNTIME_MISSING_REQUIRED_INPUT: "Pass the required generated-query input.";
65
+ readonly SYQL_RUNTIME_UNKNOWN_INPUT: "Remove the unknown key from the generated-query input object.";
66
+ readonly SYQL_RUNTIME_INVALID_INPUT: "Pass a value that matches the generated input type and nullability.";
67
+ readonly SYQL_RUNTIME_INVALID_GROUP: "Pass either the complete generated group shape or omit the optional group.";
68
+ readonly SYQL_RUNTIME_INVALID_SORT: "Pass one of the sort profile names exported for the generated query.";
69
+ readonly SYQL_RUNTIME_INVALID_LIMIT: "Pass an integer within the generated query limit range.";
70
+ };
71
+ /** Generate a deterministic JSON-ready code-to-remedy catalog. */
72
+ export declare function generateSyqlDiagnosticCatalog(): Readonly<Array<{
73
+ readonly code: SyqlDiagnosticCode;
74
+ readonly remedy: string;
75
+ }>>;
76
+ /** Resolve a diagnostic remedy without requiring a narrowed code type. */
77
+ export declare function syqlDiagnosticRemedy(code: string): string | undefined;
@@ -0,0 +1,72 @@
1
+ /** Stable compiler and generated-runtime diagnostic remedies keyed by code. */
2
+ export const SYQL_DIAGNOSTIC_REMEDIES = {
3
+ SYQL1001_UNTERMINATED_STRING: 'Close the string literal with a matching single quote.',
4
+ SYQL1002_UNTERMINATED_IDENTIFIER: 'Close the quoted identifier with its matching delimiter.',
5
+ SYQL1003_UNTERMINATED_COMMENT: 'Close the block comment with */.',
6
+ SYQL1004_UNTERMINATED_IMPORT_PATH: 'Close the import path with a matching double quote.',
7
+ SYQL2001_EXPECTED_TOKEN: 'Insert the token named by the diagnostic at the reported source position.',
8
+ SYQL2002_INVALID_NAME: 'Rename the declaration or parameter to a valid lower-camel identifier.',
9
+ SYQL2003_RESERVED_NAME: 'Rename the declaration or parameter so it does not use a reserved SYQL name.',
10
+ SYQL2004_DUPLICATE_NAME: 'Give each declaration, parameter, group member, and profile a unique name in its scope.',
11
+ SYQL2005_INVALID_IMPORT: 'Use a relative .syql import with explicit imported predicate names.',
12
+ SYQL2006_EMPTY_TEMPLATE: 'Add a SQL expression or statement inside the braces.',
13
+ SYQL2007_FORBIDDEN_SEMICOLON: 'Remove the semicolon from the embedded SQL template.',
14
+ SYQL2008_INVALID_MEMBER: 'Use a member supported by the surrounding SYQL declaration.',
15
+ SYQL2009_INVALID_INTEGER: 'Use a decimal integer within the range named by the diagnostic.',
16
+ SYQL2010_INVALID_PAGE_RANGE: 'Choose default and maximum page sizes that satisfy the declared range.',
17
+ SYQL2011_INVALID_PARAMETER: 'Rewrite the parameter using a supported value, optional, range, group, sort, or limit form.',
18
+ SYQL2012_INVALID_QUERY_BODY: 'Rewrite the query body using the clause order and forms defined by the SYQL grammar.',
19
+ SYQL3001_EXPECTED_EMBEDDED_TOKEN: 'Insert the required token in the embedded SQL construct.',
20
+ SYQL3002_INVALID_BIND: 'Bind a declared input with the :name form in a supported SQL position.',
21
+ SYQL3003_INVALID_PREDICATE_CALL: 'Call the predicate with declared bind arguments and matching parentheses.',
22
+ SYQL3004_INVALID_WHEN: 'Use when with declared controls and a nonempty conditional SQL body.',
23
+ SYQL3005_INVALID_REACTIVE_DIRECTIVE: 'Rewrite the reactive directive using a supported revision-1 form.',
24
+ SYQL3006_FORBIDDEN_TEMPLATE_NODE: 'Remove the conditional or predicate construct from this SQL context.',
25
+ SYQL3007_UNEXPECTED_BRACE: 'Remove the unmatched brace or close the surrounding embedded construct.',
26
+ SYQL3008_FORBIDDEN_PARAMETER_FORM: 'Use a value bind in this context instead of a range, group, sort, or limit control.',
27
+ SYQL4001_IMPORT_OUTSIDE_ROOT: 'Move the imported file under the configured query root and use a relative path.',
28
+ SYQL4002_MODULE_NOT_FOUND: 'Create the imported .syql file or correct the relative import path.',
29
+ SYQL4003_IMPORT_CYCLE: 'Remove one import edge so predicate modules form an acyclic graph.',
30
+ SYQL4004_UNKNOWN_PREDICATE: 'Export and import the predicate under the referenced name.',
31
+ SYQL4005_DUPLICATE_IMPORT_TARGET: 'Import each local predicate name once in the module.',
32
+ SYQL4006_DUPLICATE_QUERY: 'Give every query in the project a unique public name.',
33
+ SYQL5001_UNKNOWN_PREDICATE: 'Define the predicate locally or import it under the referenced name.',
34
+ SYQL5002_PREDICATE_CYCLE: 'Remove the recursive predicate call so expansion terminates.',
35
+ SYQL5003_PREDICATE_ARITY: 'Pass exactly the number of bind arguments declared by the predicate.',
36
+ SYQL5004_CLOSED_PREDICATE: 'Declare each external bind as a predicate parameter and pass it at the call site.',
37
+ SYQL5005_UNUSED_PREDICATE_PARAMETER: 'Use the predicate parameter in its body or remove it from the signature.',
38
+ SYQL5006_UNDECLARED_BIND: 'Declare the bind as a query input or predicate parameter.',
39
+ SYQL5007_UNUSED_INPUT: 'Use the query input in SQL or remove it from the public signature.',
40
+ SYQL5008_INVALID_CONTROL: 'Use an optional input, group, sort, or limit name as the control.',
41
+ SYQL5009_MISSING_DOMINANCE: 'Guard every optional bind with a when condition that proves its presence.',
42
+ SYQL5010_UNUSED_CONTROL: 'Use the control in the query body or remove it from the signature.',
43
+ SYQL5011_TYPE_CONFLICT: 'Make every declaration and SQL use of the bind agree on one SYQL type.',
44
+ SYQL6001_INVALID_PLACEMENT: 'Move the SYQL construct to the SQL clause named by the diagnostic.',
45
+ SYQL6002_INVALID_SQL: 'Correct the reported SQLite syntax, table, column, function, or bind error.',
46
+ SYQL6003_NONDETERMINISTIC_SQL: 'Replace nondeterministic SQL with values supplied through query inputs or stored columns.',
47
+ SYQL6004_TYPE_CONFLICT: 'Align the declared input type with the schema column and SQL expression types.',
48
+ SYQL6005_INVALID_SYNC_QUERY: 'Select stable row identity and preserve the scope coverage required for reactive sync.',
49
+ SYQL6006_INVALID_SORT: 'Use declared sort profiles with deterministic ORDER BY terms and a stable tie-breaker.',
50
+ SYQL6007_INVALID_LIMIT: 'Use the declared limit control and keep its default and maximum within the supported range.',
51
+ SYQL6008_INVALID_IDENTITY: 'Declare identity columns that are selected, non-null, and sufficient to identify each result row.',
52
+ SYQL7001_ENUMERATION_LIMIT: 'Use the auto or neutralize backend, reduce control combinations, or raise the explicit compiler limit.',
53
+ SYQL7002_INTERNAL_LOWERING: 'Report the query and diagnostic to the Syncular maintainers.',
54
+ SYQL9001_PROJECT_CONTEXT: 'Open the file under a project with a valid syncular.json, migrations, schema output, and query root.',
55
+ SYQL_RUNTIME_MISSING_REQUIRED_INPUT: 'Pass the required generated-query input.',
56
+ SYQL_RUNTIME_UNKNOWN_INPUT: 'Remove the unknown key from the generated-query input object.',
57
+ SYQL_RUNTIME_INVALID_INPUT: 'Pass a value that matches the generated input type and nullability.',
58
+ SYQL_RUNTIME_INVALID_GROUP: 'Pass either the complete generated group shape or omit the optional group.',
59
+ SYQL_RUNTIME_INVALID_SORT: 'Pass one of the sort profile names exported for the generated query.',
60
+ SYQL_RUNTIME_INVALID_LIMIT: 'Pass an integer within the generated query limit range.',
61
+ };
62
+ /** Generate a deterministic JSON-ready code-to-remedy catalog. */
63
+ export function generateSyqlDiagnosticCatalog() {
64
+ return Object.keys(SYQL_DIAGNOSTIC_REMEDIES)
65
+ .sort()
66
+ .map((code) => ({ code, remedy: SYQL_DIAGNOSTIC_REMEDIES[code] }));
67
+ }
68
+ /** Resolve a diagnostic remedy without requiring a narrowed code type. */
69
+ export function syqlDiagnosticRemedy(code) {
70
+ const remedies = SYQL_DIAGNOSTIC_REMEDIES;
71
+ return remedies[code];
72
+ }
@@ -309,6 +309,7 @@ function lowerStatement(validated, ir, db, naming, sql, sortProfile, activationM
309
309
  ...(activationMask === undefined ? {} : { activationMask }),
310
310
  sql: namedSql,
311
311
  positionalSql: analyzed.positionalSql,
312
+ relations: analyzed.relations,
312
313
  binds: bindNames.map((name) => planBind(name, owners, conditionBinds, query.conditions, validated.limit, query)),
313
314
  },
314
315
  analysis: { ...analyzed, sourceSql: namedSql, sql: namedSql },
@@ -303,7 +303,15 @@ class Validator {
303
303
  const activeStructure = this.#inspect(activeSql, location);
304
304
  this.#validateStatementShape(activeSql, activeStructure, logical.declaration, location);
305
305
  this.#validateDeterminism(activeSql, logical.declaration.statement.span);
306
- const refs = scanTableRefs(activeSql, this.#ir);
306
+ let refs;
307
+ try {
308
+ refs = scanTableRefs(activeSql, this.#ir);
309
+ }
310
+ catch (error) {
311
+ // Preserve SQLite's source-spanned diagnostics for invalid relations.
312
+ this.#validateSqlite(activeSql, logical, []);
313
+ this.#fail('SYQL6002_INVALID_SQL', logical.declaration.statement.span, error instanceof Error ? error.message : String(error));
314
+ }
307
315
  this.#validatePortableProfile(activeSql, logical.declaration.statement.span, refs);
308
316
  this.#validateSqlite(activeSql, logical, refs);
309
317
  const bindSymbols = this.#bindSymbols(logical.declaration);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/typegen",
3
- "version": "0.15.47",
3
+ "version": "0.16.1",
4
4
  "description": "Syncular schema-to-TypeScript type generator and the `syncular` CLI",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -56,9 +56,9 @@
56
56
  "!dist/**/*.test.d.ts"
57
57
  ],
58
58
  "dependencies": {
59
- "@syncular/core": "0.15.47"
59
+ "@syncular/core": "0.16.1"
60
60
  },
61
61
  "devDependencies": {
62
- "@syncular/server": "0.15.47"
62
+ "@syncular/server": "0.16.1"
63
63
  }
64
64
  }
@@ -892,7 +892,7 @@ function emitSupport(clientCrate: string): string {
892
892
  ' ));',
893
893
  ' }',
894
894
  ' hex.as_bytes()',
895
- ' .chunks_exact(2)',
895
+ ' .chunks(2)',
896
896
  ' .map(|pair| {',
897
897
  ' let pair = std::str::from_utf8(pair)',
898
898
  ' .map_err(|_| decode_error(query, column, "bytes", "non-ASCII $bytes envelope"))?;',
@@ -314,15 +314,15 @@ function emitSyqlQuery(query: AnalyzedQuery, hash: string): string {
314
314
  : `raw?: ${Params}`;
315
315
  const validated = hasParams ? `${query.name}Validate(raw)` : undefined;
316
316
  const statementType = hasParams
317
- ? `{ sql: string; bind: (params: ${Params}) => QueryValue[] }`
318
- : '{ sql: string; bind: () => QueryValue[] }';
319
- lines.push(`const ${query.name}Statements: ${statementType}[] = [`);
317
+ ? `QueryRelationPlan & { bind: (params: ${Params}) => QueryValue[] }`
318
+ : 'QueryRelationPlan & { bind: () => QueryValue[] }';
319
+ lines.push(`const ${query.name}Statements: (${statementType})[] = [`);
320
320
  for (const statement of metadata.plan.statements) {
321
321
  const binds = statement.binds
322
322
  .map((bind) => syqlBindExpr(query, bind, 'params'))
323
323
  .join(', ');
324
324
  lines.push(
325
- ` { sql: ${quote(statement.positionalSql)}, bind: (${hasParams ? 'params' : ''}) => [${binds}] },`,
325
+ ` { sql: ${quote(statement.positionalSql)}, relations: ${JSON.stringify(statement.relations)}, bind: (${hasParams ? 'params' : ''}) => [${binds}] },`,
326
326
  );
327
327
  }
328
328
  lines.push('];');
@@ -399,6 +399,7 @@ function emitSyqlQuery(query: AnalyzedQuery, hash: string): string {
399
399
  );
400
400
  }
401
401
  lines.push(` tables: ${query.name}Tables,`);
402
+ lines.push(` relationPlans: ${query.name}Statements,`);
402
403
  lines.push(
403
404
  ` resultColumns: [${query.columns.map((column) => `{ name: ${quote(column.langName)}, type: ${quote(column.type)}, nullable: ${column.nullable} }`).join(', ')}],`,
404
405
  );
@@ -540,6 +541,9 @@ function emitQuery(query: AnalyzedQuery, hash: string): string {
540
541
  lines.push(` sql: ${sqlConst},`);
541
542
  lines.push(` mapRow: ${query.name}MapRow,`);
542
543
  lines.push(` tables: ${query.name}Tables,`);
544
+ lines.push(
545
+ ` relationPlans: [{ sql: ${sqlConst}, relations: ${JSON.stringify(query.relations)} }],`,
546
+ );
543
547
  lines.push(
544
548
  ` resultColumns: [${query.columns.map((column) => `{ name: ${quote(column.langName)}, type: ${quote(column.type)}, nullable: ${column.nullable} }`).join(', ')}],`,
545
549
  );
@@ -683,6 +687,11 @@ export function emitQueriesModule(
683
687
  " * `@syncular/react`'s `useQuery`. `Row` is the projection row",
684
688
  ' * type; `Params` is `undefined` for a param-less query. `sqlFor`',
685
689
  ' * selects a checked revision-1 SYQL physical statement when needed. */',
690
+ 'export interface QueryRelationPlan {',
691
+ ' readonly sql: string;',
692
+ ' readonly relations: readonly { readonly table: string; readonly start: number; readonly end: number; readonly alias?: string }[];',
693
+ '}',
694
+ '',
686
695
  'export interface NamedQuery<Row, Params = undefined> {',
687
696
  ' readonly id: string;',
688
697
  ' readonly hasParams: boolean;',
@@ -690,6 +699,7 @@ export function emitQueriesModule(
690
699
  ' readonly mapRow: (row: Readonly<Record<string, unknown>>) => Row;',
691
700
  ' readonly tables: readonly string[];',
692
701
  ' readonly resultColumns: readonly QueryResultColumn[];',
702
+ ' readonly relationPlans: readonly QueryRelationPlan[];',
693
703
  ' readonly bind: (params: Params) => readonly QueryValue[];',
694
704
  ' readonly sqlFor?: (params: Params) => string;',
695
705
  ' readonly dependencies: (params: Params) => readonly QueryDependency[];',
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export * from './query';
25
25
  export * from './query-ir';
26
26
  export * from './sql';
27
27
  export * from './syql-ast';
28
+ export * from './syql-diagnostics';
28
29
  export * from './syql-lexer';
29
30
  export * from './syql-lowering';
30
31
  export * from './syql-modules';
package/src/lsp.ts CHANGED
@@ -9,6 +9,7 @@ import type { IrDocument } from './ir';
9
9
  import { MANIFEST_FILENAME, parseManifest } from './manifest';
10
10
  import { lockedMigrationNames, readMigrationLock } from './migration-lock';
11
11
  import type { QueryDb, QueryNamingOptions } from './query';
12
+ import { syqlDiagnosticRemedy } from './syql-diagnostics';
12
13
  import { SyqlFrontendError, type SyqlSourceSpan } from './syql-lexer';
13
14
  import type { SyqlLoweredQuery } from './syql-lowering';
14
15
  import { lowerSyqlQuery } from './syql-lowering';
@@ -49,6 +50,7 @@ interface Diagnostic {
49
50
  severity: number;
50
51
  source: string;
51
52
  code?: string;
53
+ data?: { readonly remedy: string };
52
54
  message: string;
53
55
  }
54
56
 
@@ -384,14 +386,25 @@ export class SyqlLanguageServer {
384
386
  error instanceof SyqlFrontendError &&
385
387
  resolve(error.sourceFile) === file
386
388
  ) {
389
+ const remedy = syqlDiagnosticRemedy(error.code);
387
390
  return {
388
391
  range: spanRange(text, error.span),
389
392
  severity: 1,
390
393
  source: 'syncular',
391
394
  code: error.code,
395
+ ...(remedy === undefined ? {} : { data: { remedy } }),
392
396
  message: error.detail,
393
397
  };
394
398
  }
399
+ const projectContextError =
400
+ this.#context(pathToFileURL(file).href).kind === 'error';
401
+ const code =
402
+ error instanceof SyqlFrontendError
403
+ ? error.code
404
+ : projectContextError
405
+ ? 'SYQL9001_PROJECT_CONTEXT'
406
+ : undefined;
407
+ const remedy = code === undefined ? undefined : syqlDiagnosticRemedy(code);
395
408
  return {
396
409
  range: {
397
410
  start: { line: 0, character: 0 },
@@ -402,11 +415,11 @@ export class SyqlLanguageServer {
402
415
  },
403
416
  severity: 1,
404
417
  source: 'syncular',
405
- ...(error instanceof SyqlFrontendError ? { code: error.code } : {}),
406
- message:
407
- this.#context(pathToFileURL(file).href).kind === 'error'
408
- ? `SYQL9001_PROJECT_CONTEXT: ${message}`
409
- : message,
418
+ ...(code === undefined ? {} : { code }),
419
+ ...(remedy === undefined ? {} : { data: { remedy } }),
420
+ message: projectContextError
421
+ ? `SYQL9001_PROJECT_CONTEXT: ${message}`
422
+ : message,
410
423
  };
411
424
  }
412
425
 
package/src/query-ir.ts CHANGED
@@ -14,13 +14,14 @@ import type { AnalyzedQuery } from './query';
14
14
  /** Serialize analyzed queries as the deterministic QueryIR JSON document. */
15
15
  export function serializeQueryIr(queries: readonly AnalyzedQuery[]): string {
16
16
  const doc = {
17
- queryIrVersion: 3,
17
+ queryIrVersion: 4,
18
18
  queries: queries.map((query) => ({
19
19
  name: query.name,
20
20
  file: query.file,
21
21
  sourceSql: query.sourceSql,
22
22
  sql: query.sql,
23
23
  positionalSql: query.positionalSql,
24
+ relations: query.relations,
24
25
  params: query.params.map((param) => ({
25
26
  name: param.name,
26
27
  langName: param.langName,
@@ -101,6 +102,7 @@ export function serializeQueryIr(queries: readonly AnalyzedQuery[]): string {
101
102
  : { activationMask: statement.activationMask }),
102
103
  sql: statement.sql,
103
104
  positionalSql: statement.positionalSql,
105
+ relations: statement.relations,
104
106
  binds: statement.binds.map((bind) => ({ ...bind })),
105
107
  })),
106
108
  },