@kequtech/tenon-orm 0.4.6 → 0.4.7
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/CHANGELOG.md +11 -0
- package/dist/check/contracts.d.ts +2 -2
- package/dist/check/contracts.js +5 -25
- package/dist/check/output.d.ts +3 -0
- package/dist/check/output.js +31 -0
- package/dist/check/source.d.ts +2 -1
- package/dist/check/source.js +81 -85
- package/dist/check.js +1 -1
- package/dist/cli.js +3 -5
- package/docs/query-checking.md +20 -7
- package/docs/reference.md +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.7
|
|
4
|
+
|
|
5
|
+
- Remove routine parameter-value and result-nullability checker notes. General limits are stated
|
|
6
|
+
once; remaining readable findings are grouped per query with their variant numbers. JSON retains
|
|
7
|
+
individual variant findings, without the removed routine notes.
|
|
8
|
+
- Reject enum result declarations whose string literals are disjoint from every catalog label.
|
|
9
|
+
Overlapping refinements and custom decoders retain conservative handling.
|
|
10
|
+
- Recover actual fields and value types from direct mutation object literals and fixed literal
|
|
11
|
+
batches, reducing needless expansion under broad generated shapes. Source checking and runtime
|
|
12
|
+
mutations share SQL assembly; dynamic inputs retain bounded shape analysis.
|
|
13
|
+
|
|
3
14
|
## 0.4.6
|
|
4
15
|
|
|
5
16
|
- Add `inspectSchema` and `tenon schema` for versioned, codec-independent catalog snapshots and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { QueryDescription } from '../inspection/types.js';
|
|
2
2
|
import { type Project } from './project.js';
|
|
3
|
-
import type { QueryNode
|
|
3
|
+
import type { QueryNode } from './source.js';
|
|
4
4
|
import type { CheckDiagnostic } from './types.js';
|
|
5
5
|
/** Compare non-NULL representations; nullability and value refinements remain explicit limitations. */
|
|
6
|
-
export declare function compareContracts(project: Project, query: QueryNode,
|
|
6
|
+
export declare function compareContracts(project: Project, query: QueryNode, description: QueryDescription, readTypes: Readonly<Record<number, string>>): CheckDiagnostic[];
|
package/dist/check/contracts.js
CHANGED
|
@@ -27,6 +27,10 @@ function kinds(project, type, seen = new Set()) {
|
|
|
27
27
|
function disjoint(project, actual, expected, depth = 0) {
|
|
28
28
|
if (depth > 8)
|
|
29
29
|
return false;
|
|
30
|
+
if (expected.isUnion())
|
|
31
|
+
return expected.types.every((member) => disjoint(project, actual, member, depth + 1));
|
|
32
|
+
if (actual.isStringLiteral() && expected.isStringLiteral())
|
|
33
|
+
return actual.value !== expected.value;
|
|
30
34
|
const a = kinds(project, actual), b = kinds(project, expected);
|
|
31
35
|
if (!b.size)
|
|
32
36
|
return true;
|
|
@@ -69,34 +73,10 @@ function rowType(project, query) {
|
|
|
69
73
|
: undefined;
|
|
70
74
|
}
|
|
71
75
|
/** Compare non-NULL representations; nullability and value refinements remain explicit limitations. */
|
|
72
|
-
export function compareContracts(project, query,
|
|
76
|
+
export function compareContracts(project, query, description, readTypes) {
|
|
73
77
|
const checker = project.checker;
|
|
74
78
|
const at = location(query);
|
|
75
79
|
const diagnostics = [];
|
|
76
|
-
for (const [index, parameter] of variant.parameters.entries()) {
|
|
77
|
-
const expected = description.parameters[index];
|
|
78
|
-
if (!expected)
|
|
79
|
-
continue;
|
|
80
|
-
const inputKinds = kinds(project, parameter.type);
|
|
81
|
-
if (inputKinds.has('string') || inputKinds.has('number')) {
|
|
82
|
-
diagnostics.push({
|
|
83
|
-
...location(parameter.node),
|
|
84
|
-
kind: 'unproven',
|
|
85
|
-
code: 'parameter-value-unproven',
|
|
86
|
-
parameter: index + 1,
|
|
87
|
-
expected: expected.postgres,
|
|
88
|
-
observed: checker.typeToString(parameter.type),
|
|
89
|
-
message: `Parameter ${index + 1}${parameter.field ? ` (${parameter.field})` : ''}: ${checker.typeToString(parameter.type)} does not prove PostgreSQL ranges or textual formats for ${expected.postgres}.`,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (description.columns.length)
|
|
94
|
-
diagnostics.push({
|
|
95
|
-
...at,
|
|
96
|
-
kind: 'unproven',
|
|
97
|
-
code: 'nullability-unproven',
|
|
98
|
-
message: 'Describe does not establish result nullability; column representation checks exclude SQL NULL.',
|
|
99
|
-
});
|
|
100
80
|
for (const issue of description.issues) {
|
|
101
81
|
if (issue.code === 'duplicate-column')
|
|
102
82
|
diagnostics.push({
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function group(diagnostics) {
|
|
2
|
+
const groups = new Map();
|
|
3
|
+
for (const diagnostic of diagnostics) {
|
|
4
|
+
const { variant, ...finding } = diagnostic;
|
|
5
|
+
const key = JSON.stringify(finding);
|
|
6
|
+
let entry = groups.get(key);
|
|
7
|
+
if (!entry) {
|
|
8
|
+
entry = { diagnostic, variants: new Set() };
|
|
9
|
+
groups.set(key, entry);
|
|
10
|
+
}
|
|
11
|
+
if (variant !== undefined)
|
|
12
|
+
entry.variants.add(variant);
|
|
13
|
+
}
|
|
14
|
+
return [...groups.values()];
|
|
15
|
+
}
|
|
16
|
+
/** Group presentation only; JSON retains each finding's SQL variant. */
|
|
17
|
+
export function formatCheckReport(report) {
|
|
18
|
+
const queryFindings = new Set(report.queries.flatMap((query) => query.diagnostics));
|
|
19
|
+
const findings = [
|
|
20
|
+
...group(report.diagnostics.filter((finding) => !queryFindings.has(finding))),
|
|
21
|
+
...report.queries.flatMap((query) => group(query.diagnostics)),
|
|
22
|
+
];
|
|
23
|
+
const lines = findings.map(({ diagnostic: d, variants }) => {
|
|
24
|
+
const scope = variants.size
|
|
25
|
+
? ` [variant${variants.size === 1 ? '' : 's'} ${[...variants].join(', ')}]`
|
|
26
|
+
: '';
|
|
27
|
+
return `${d.file}:${d.line}:${d.column} [${d.kind}] ${d.code}${scope}: ${d.message}`;
|
|
28
|
+
});
|
|
29
|
+
lines.push(`${report.coverage.total} queries, ${report.coverage.analysed} analysed, ${report.coverage.unchecked} unchecked; ${findings.filter(({ diagnostic }) => diagnostic.kind === 'mismatch').length} definite mismatches, ${findings.filter(({ diagnostic }) => diagnostic.kind === 'unproven').length} unproven findings.`, 'Checks exclude result nullability and runtime value validation.');
|
|
30
|
+
return lines.join('\n') + '\n';
|
|
31
|
+
}
|
package/dist/check/source.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ export interface Binding {
|
|
|
10
10
|
}
|
|
11
11
|
export interface QueryVariant {
|
|
12
12
|
readonly text: string;
|
|
13
|
-
readonly parameters: readonly Binding[];
|
|
14
13
|
readonly parameterTypes: readonly number[];
|
|
15
14
|
readonly uncertainParameters: readonly number[];
|
|
16
15
|
}
|
|
@@ -39,6 +38,8 @@ export declare class SourceAnalysis {
|
|
|
39
38
|
private combine;
|
|
40
39
|
private conditional;
|
|
41
40
|
private template;
|
|
41
|
+
/** Only direct data-property literals establish actual own keys and value expressions. */
|
|
42
|
+
private literalFields;
|
|
42
43
|
private mutation;
|
|
43
44
|
private expression;
|
|
44
45
|
private codecOid;
|
package/dist/check/source.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { builtin } from '@kequtech/pgclear';
|
|
2
2
|
import ts from 'typescript';
|
|
3
|
+
import { assignmentParts, insertParts } from '../mutations.js';
|
|
3
4
|
import { joinSeparator, quote } from '../sql.js';
|
|
4
5
|
import { location, symbol } from './project.js';
|
|
5
6
|
export const variantLimit = 16;
|
|
@@ -247,6 +248,30 @@ export class SourceAnalysis {
|
|
|
247
248
|
}
|
|
248
249
|
return variants;
|
|
249
250
|
}
|
|
251
|
+
/** Only direct data-property literals establish actual own keys and value expressions. */
|
|
252
|
+
literalFields(input) {
|
|
253
|
+
const node = unwrap(input);
|
|
254
|
+
if (!ts.isObjectLiteralExpression(node))
|
|
255
|
+
return undefined;
|
|
256
|
+
const fields = new Map();
|
|
257
|
+
for (const property of node.properties) {
|
|
258
|
+
if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property))
|
|
259
|
+
return undefined;
|
|
260
|
+
const name = property.name;
|
|
261
|
+
if (!ts.isIdentifier(name) && !ts.isStringLiteral(name) && !ts.isNumericLiteral(name))
|
|
262
|
+
return undefined;
|
|
263
|
+
// A colon-form __proto__ property changes the prototype rather than defining an own key.
|
|
264
|
+
if (name.text === '__proto__' && ts.isPropertyAssignment(property))
|
|
265
|
+
return undefined;
|
|
266
|
+
const value = ts.isPropertyAssignment(property) ? property.initializer : property.name;
|
|
267
|
+
fields.set(name.text, {
|
|
268
|
+
node: value,
|
|
269
|
+
type: this.project.checker.getTypeAtLocation(value),
|
|
270
|
+
field: name.text,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return fields;
|
|
274
|
+
}
|
|
250
275
|
mutation(node, helper) {
|
|
251
276
|
const shapeNode = node.typeArguments?.[0];
|
|
252
277
|
if (!shapeNode)
|
|
@@ -273,44 +298,14 @@ export class SourceAnalysis {
|
|
|
273
298
|
const input = node.arguments[0];
|
|
274
299
|
if (!input)
|
|
275
300
|
return this.fail(node, 'expansion-input-required', 'Object expansion requires an input.');
|
|
276
|
-
|
|
277
|
-
this.issues.push({
|
|
278
|
-
...location(node),
|
|
279
|
-
kind: 'unproven',
|
|
280
|
-
code: 'expansion-shape-assumed',
|
|
281
|
-
message: 'The explicit shape is trusted to describe exactly the expanded own fields and their order. An explicit column list removes that assumption.',
|
|
282
|
-
});
|
|
283
|
-
const columns = node.arguments[1]
|
|
301
|
+
const explicit = node.arguments[1]
|
|
284
302
|
? this.array(node.arguments[1]).map((key) => this.text(key))
|
|
285
|
-
:
|
|
286
|
-
if (
|
|
287
|
-
return this.fail(node, 'expansion-columns', '
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
for (const name of columns) {
|
|
291
|
-
try {
|
|
292
|
-
quote(name);
|
|
293
|
-
}
|
|
294
|
-
catch {
|
|
295
|
-
return this.fail(node, 'expansion-columns', 'Expansion columns must be valid PostgreSQL identifiers.');
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
const fields = columns.map((name) => {
|
|
299
|
-
const property = shape.getProperty(name);
|
|
300
|
-
if (!property)
|
|
303
|
+
: undefined;
|
|
304
|
+
if (explicit && (!explicit.length || new Set(explicit).size !== explicit.length))
|
|
305
|
+
return this.fail(node, 'expansion-columns', 'Expansion columns must be nonempty and unique.');
|
|
306
|
+
for (const name of explicit ?? [])
|
|
307
|
+
if (!shape.getProperty(name))
|
|
301
308
|
return this.fail(node, 'expansion-columns', `Column ${name} is absent from the expansion shape.`);
|
|
302
|
-
const type = this.project.checker.getTypeOfSymbolAtLocation(property, shapeNode);
|
|
303
|
-
// Presence follows the same constrained value type used to derive bind hints.
|
|
304
|
-
const valueType = this.project.checker.getBaseConstraintOfType(type) ?? type;
|
|
305
|
-
const members = valueType.isUnion() ? valueType.types : [valueType];
|
|
306
|
-
return {
|
|
307
|
-
name,
|
|
308
|
-
type,
|
|
309
|
-
optional: !!(property.flags & ts.SymbolFlags.Optional) ||
|
|
310
|
-
members.some((member) => !!(member.flags & ts.TypeFlags.Undefined)),
|
|
311
|
-
defined: members.some((member) => !(member.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Never))),
|
|
312
|
-
};
|
|
313
|
-
});
|
|
314
309
|
const inputType = this.project.checker.getTypeAtLocation(input);
|
|
315
310
|
const rows = helper === 'sql.insertValues' &&
|
|
316
311
|
(this.project.checker.isArrayType(inputType) || this.project.checker.isTupleType(inputType))
|
|
@@ -318,16 +313,46 @@ export class SourceAnalysis {
|
|
|
318
313
|
: [input];
|
|
319
314
|
if (!rows.length)
|
|
320
315
|
return this.fail(node, 'empty-expansion', 'Expansion requires a nonempty batch.');
|
|
316
|
+
const literals = rows.map((row) => this.literalFields(row));
|
|
317
|
+
if (!explicit && literals.some((literal) => !literal))
|
|
318
|
+
this.issues.push({
|
|
319
|
+
...location(node),
|
|
320
|
+
kind: 'unproven',
|
|
321
|
+
code: 'expansion-shape-assumed',
|
|
322
|
+
message: 'The explicit shape is trusted to describe exactly the expanded own fields and their order. An explicit column list removes that assumption.',
|
|
323
|
+
});
|
|
321
324
|
let variants = [[]];
|
|
322
|
-
for (const row of rows) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
+
for (const [index, row] of rows.entries()) {
|
|
326
|
+
const literal = literals[index];
|
|
327
|
+
const columns = explicit ?? (literal ? [...literal.keys()] : shape.getProperties().map((p) => p.name));
|
|
328
|
+
let choices = [{}];
|
|
329
|
+
for (const name of columns) {
|
|
330
|
+
try {
|
|
331
|
+
quote(name);
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
return this.fail(node, 'expansion-columns', 'Expansion columns must be valid PostgreSQL identifiers.');
|
|
335
|
+
}
|
|
336
|
+
const property = shape.getProperty(name);
|
|
337
|
+
const binding = literal
|
|
338
|
+
? literal.get(name)
|
|
339
|
+
: property && {
|
|
340
|
+
node: row,
|
|
341
|
+
type: this.project.checker.getTypeOfSymbolAtLocation(property, shapeNode),
|
|
342
|
+
field: name,
|
|
343
|
+
};
|
|
344
|
+
const valueType = binding && (this.project.checker.getBaseConstraintOfType(binding.type) ?? binding.type);
|
|
345
|
+
const members = valueType ? (valueType.isUnion() ? valueType.types : [valueType]) : [];
|
|
346
|
+
const defined = members.some((member) => !(member.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Never)));
|
|
347
|
+
const optional = !binding ||
|
|
348
|
+
(!literal && !!(property && property.flags & ts.SymbolFlags.Optional)) ||
|
|
349
|
+
members.some((member) => !!(member.flags & ts.TypeFlags.Undefined));
|
|
325
350
|
const values = [];
|
|
326
|
-
if (
|
|
327
|
-
values.push(
|
|
328
|
-
if (
|
|
351
|
+
if (binding && defined)
|
|
352
|
+
values.push(binding);
|
|
353
|
+
if (optional || !defined)
|
|
329
354
|
values.push(undefined);
|
|
330
|
-
choices = choices.flatMap((choice) => values.map((value) =>
|
|
355
|
+
choices = choices.flatMap((choice) => values.map((value) => ({ ...choice, [name]: value })));
|
|
331
356
|
if (choices.length * variants.length > variantLimit)
|
|
332
357
|
return this.fail(node, 'variant-limit', `Mutation expansion exceeds the ${variantLimit}-variant budget.`);
|
|
333
358
|
}
|
|
@@ -336,47 +361,19 @@ export class SourceAnalysis {
|
|
|
336
361
|
const result = [];
|
|
337
362
|
let rejected = false;
|
|
338
363
|
for (const variant of variants) {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
];
|
|
344
|
-
const parts = [];
|
|
345
|
-
if (helper === 'sql.assignments') {
|
|
346
|
-
for (const [index, cell] of variant[0].entries()) {
|
|
347
|
-
if (!cell)
|
|
348
|
-
continue;
|
|
349
|
-
if (parts.length)
|
|
350
|
-
parts.push(', ');
|
|
351
|
-
parts.push(`${quote(columns[index])} = `, cell);
|
|
352
|
-
}
|
|
353
|
-
if (!parts.length) {
|
|
354
|
-
rejected = true;
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
else if (!selected.length) {
|
|
359
|
-
if (rows.length > 1) {
|
|
360
|
-
rejected = true;
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
parts.push('DEFAULT VALUES');
|
|
364
|
+
const hasValues = variant.some((row) => Object.values(row).some((value) => value !== undefined));
|
|
365
|
+
if (!hasValues && (helper === 'sql.assignments' || (!explicit && rows.length > 1))) {
|
|
366
|
+
rejected = true;
|
|
367
|
+
continue;
|
|
364
368
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
parts.push(', ');
|
|
374
|
-
parts.push(row[column] ?? 'DEFAULT');
|
|
375
|
-
}
|
|
376
|
-
parts.push(')');
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
result.push({ parts, choices: new Map() });
|
|
369
|
+
// These builders see only compiler-created bindings, never application objects or values.
|
|
370
|
+
const parts = helper === 'sql.assignments'
|
|
371
|
+
? assignmentParts(variant[0], explicit)
|
|
372
|
+
: insertParts(variant, explicit);
|
|
373
|
+
result.push({
|
|
374
|
+
parts: parts.map((part) => (typeof part === 'string' ? part : part.value)),
|
|
375
|
+
choices: new Map(),
|
|
376
|
+
});
|
|
380
377
|
}
|
|
381
378
|
if (!result.length)
|
|
382
379
|
return this.fail(node, 'empty-expansion', 'Every expansion is rejected by the runtime helper: assignments need a defined field and default-only inserts need one row.');
|
|
@@ -555,7 +552,7 @@ export class SourceAnalysis {
|
|
|
555
552
|
if (checker.isTypeAssignableTo(type, this.project.parameterType)) {
|
|
556
553
|
let oid;
|
|
557
554
|
try {
|
|
558
|
-
oid =
|
|
555
|
+
oid = this.encoderOid(binding.node);
|
|
559
556
|
}
|
|
560
557
|
catch (error) {
|
|
561
558
|
if (!(error instanceof Unsupported))
|
|
@@ -667,7 +664,6 @@ export class SourceAnalysis {
|
|
|
667
664
|
}
|
|
668
665
|
return lists.map((parameterTypes) => ({
|
|
669
666
|
text,
|
|
670
|
-
parameters: bindings,
|
|
671
667
|
parameterTypes,
|
|
672
668
|
uncertainParameters,
|
|
673
669
|
}));
|
package/dist/check.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function checkQueries(options) {
|
|
|
111
111
|
});
|
|
112
112
|
const description = queryDescription(metadata, context, codecs);
|
|
113
113
|
described++;
|
|
114
|
-
findings.push(...compareContracts(project, entry.query,
|
|
114
|
+
findings.push(...compareContracts(project, entry.query, description, readTypes).map((diagnostic) => variant.uncertainParameters.length && diagnostic.kind === 'mismatch'
|
|
115
115
|
? {
|
|
116
116
|
...diagnostic,
|
|
117
117
|
kind: 'unproven',
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { parseArgs } from 'node:util';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { defaultCodecs } from '@kequtech/pgclear';
|
|
6
|
+
import { formatCheckReport } from './check/output.js';
|
|
6
7
|
import { adoptBaseline, captureBaseline } from './baseline.js';
|
|
7
8
|
import { describeFlags, inspectionSettings } from './inspection/cli.js';
|
|
8
9
|
import { loadConfig } from './config/load.js';
|
|
@@ -197,11 +198,8 @@ async function main() {
|
|
|
197
198
|
});
|
|
198
199
|
if (values.json)
|
|
199
200
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
200
|
-
else
|
|
201
|
-
|
|
202
|
-
process.stdout.write(`${diagnostic.file}:${diagnostic.line}:${diagnostic.column} [${diagnostic.kind}] ${diagnostic.code}: ${diagnostic.message}\n`);
|
|
203
|
-
process.stdout.write(`${report.coverage.total} queries, ${report.coverage.analysed} analysed, ${report.coverage.unchecked} unchecked, ${report.queries.reduce((count, query) => count + query.variants, 0)} variants described; ${report.diagnostics.filter((diagnostic) => diagnostic.kind === 'mismatch').length} mismatches, ${report.diagnostics.filter((diagnostic) => diagnostic.kind === 'unproven').length} unproven findings.\n`);
|
|
204
|
-
}
|
|
201
|
+
else
|
|
202
|
+
process.stdout.write(formatCheckReport(report));
|
|
205
203
|
if (!report.ok)
|
|
206
204
|
process.exitCode = 1;
|
|
207
205
|
return;
|
package/docs/query-checking.md
CHANGED
|
@@ -65,13 +65,16 @@ type alone does not reject a narrower application declaration.
|
|
|
65
65
|
For enum results with unchanged default decoding, catalog labels establish exact or superset string
|
|
66
66
|
unions, including when unrelated Date/bigint codecs change. Narrower label subsets and branded
|
|
67
67
|
strings remain unproven. Custom enum decoders keep their registry representations; catalog labels
|
|
68
|
-
never narrow a decoder that transforms them.
|
|
68
|
+
never narrow a decoder that transforms them. Disjoint string-literal declarations fail as definite
|
|
69
|
+
mismatches; a declaration with some matching labels remains unproven. Result nullability is outside
|
|
70
|
+
the check.
|
|
69
71
|
|
|
70
72
|
Input and output contracts are separate. For example, an int8 input accepts exact text, bigint, or
|
|
71
73
|
an explicit encoder; its default output is a string. Strings, numbers, bigint, Date, and supported
|
|
72
74
|
Temporal inputs use OID 0/server inference. Booleans, bytes, and recoverable `json`/`param` calls
|
|
73
75
|
supply the same OID hints as execution. Unions with different hints create variants. Input types do
|
|
74
|
-
not prove value ranges, valid Dates, or textual formats.
|
|
76
|
+
not prove value ranges, valid Dates, or textual formats. These general limits are stated once in
|
|
77
|
+
readable output, rather than repeated as diagnostics for ordinary queries.
|
|
75
78
|
|
|
76
79
|
A plain object, array, or undefined input contradicts the driver's bind API. An opaque `Parameter`
|
|
77
80
|
is trusted, with its unknown OID reported as unproven. Describe uses server inference in that case;
|
|
@@ -108,7 +111,10 @@ Exit 0 means no mismatch or unchecked site in the selected scope. It does not es
|
|
|
108
111
|
nullability, cardinality, or every runtime branch. No query sites also fails. Independent queries
|
|
109
112
|
continue after an unsupported site, and a recoverable branch can expose a mismatch even when another
|
|
110
113
|
branch is unchecked. Diagnostics include locations, codes, parameter/column identifiers, expected
|
|
111
|
-
and observed types, and variant numbers.
|
|
114
|
+
and observed types, and variant numbers. Readable output groups equivalent findings within each
|
|
115
|
+
query and counts those groups, preserving distinct columns, parameters and types. `--json` retains
|
|
116
|
+
the individual variant findings in the structured report. Ordinary scalar bindings and unknown
|
|
117
|
+
result nullability do not generate per-query findings.
|
|
112
118
|
|
|
113
119
|
## Supported composition
|
|
114
120
|
|
|
@@ -146,10 +152,17 @@ function createUser(input: NewUser) {
|
|
|
146
152
|
}
|
|
147
153
|
```
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
For a direct object literal, the checker recovers its actual own fields and value expressions without
|
|
156
|
+
evaluating them. This includes shorthand properties and literals within fixed batches. A broad
|
|
157
|
+
generated shape therefore does not multiply variants for fields absent from the literal. An
|
|
158
|
+
undefined-valued expression still creates defined/omitted branches when its type permits both.
|
|
159
|
+
|
|
160
|
+
For other inputs, the shape is a trusted expansion contract. Prefer an explicit column list so
|
|
161
|
+
runtime and checking select the same keys/order. Without one, the declared shape is assumed to
|
|
162
|
+
describe exactly the expanded fields, unless the input is a recovered literal. Structural typing
|
|
163
|
+
cannot prove the absence of extra own keys or their order, so that assumption is unproven. Object
|
|
164
|
+
aliases, spreads, computed keys and accessors retain this conservative shape analysis; the checker
|
|
165
|
+
does not reconstruct object mutation or evaluate getters.
|
|
153
166
|
|
|
154
167
|
Optional or undefined-valued fields create bounded SQL variants. With explicit insert columns, an
|
|
155
168
|
absent/undefined cell becomes `DEFAULT`; with inferred columns, a field absent from every row is
|
package/docs/reference.md
CHANGED
|
@@ -265,6 +265,8 @@ The bundled TypeScript compiler loads only through that path or the check comman
|
|
|
265
265
|
Options are `{ connection, target?, project?, session?, timeouts?, codecs?, readTypes? }`.
|
|
266
266
|
Connection is a URI or pool-option values; connection codecs are inherited unless `codecs` is explicit. The report contains `ok`, `project`, `queries`, `diagnostics`, and available
|
|
267
267
|
inspection `context`. Mismatches, unchecked sites, and no sites fail; unproven findings alone do not.
|
|
268
|
+
Readable output groups equivalent findings per query; JSON retains their variant records. General
|
|
269
|
+
limits on runtime values and result nullability do not generate individual diagnostics.
|
|
268
270
|
See [query checking](query-checking.md) for selection, supported composition, and custom decoders.
|
|
269
271
|
|
|
270
272
|
## Query inspection
|