Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
'@objectstack/driver-turso': patch
'@objectstack/driver-sql': patch
---

driver-turso refusals and log lines, and driver-sql's last three aggregate refusals, no longer cite tracker numbers; each states the reason in words

Clause-②: no

Many messages the Turso driver shows to authors and operators ended with an issue-tracker number where
the reason belonged. Most of the Turso remote transport's numbers were bare ids from the repository that
file used to live in, so here they pointed at unrelated cards. The number goes, and where the sentence
did not already say what was decided, it now does:

- Aggregate refusals, on both drivers: the undeclared-function, `count_distinct`-without-`field` and
per-aggregation `filter` refusals lose their citation on the SQL driver and the Turso remote
transport together, so the two faces still read one sentence. The remote transport's
declared-but-uncompiled and date-bucket refusals lose theirs too, and read exactly like the SQL
driver's again.
- Turso remote filter refusals (`INVALID_FILTER`): the withheld cross-field and unbindable-comparand
wording, and the full diagnostics behind every filter refusal (unsupported operator, unlowered
`$between`, undeclared or non-list combinator, non-node operand, non-object `where`, empty operator
map, undefined comparand, non-boolean `$exists`) lose only the citation, because their sentences
already said it. The non-boolean `$null` diagnostic now says every driver refuses it, so one filter
no longer gets a different answer per backend.
- The Turso remote `auto_number` refusal (`NOT_IMPLEMENTED`) now says why it refuses rather than
resolves: resolving would write NULL into the slot and persist the row without its record number.
- Log lines: the unnumbered-upsert warning loses its citation; the remote canonical backfill's info line
says what a conversion buys (the column drops the unindexable read-side repair only once a pass finds
nothing left to convert); the unresolvable-remainder warning says a value that cannot be read as an
instant is counted and reported, never guessed at.

Text only: no error code, field name, status or behaviour changes.
6 changes: 3 additions & 3 deletions packages/drivers/driver-sql/src/sql-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1696,7 +1696,7 @@ function undeclaredAggregateFunctionError(func: string): Error {
`Declared functions: ${DECLARED_AGGREGATE_FUNCTIONS.join(', ')} ` +
`(@objectstack/spec AggregationFunction). Fix the "function" key of the aggregations[] ` +
`entry — the Query Protocol has no such function, so this is a query no backend can run, ` +
`not a gap in this one (#5907).`,
`not a gap in this one.`,
) as Error & { code?: string; status?: number };
err.code = StandardErrorCode.enum.INVALID_QUERY;
err.status = 400;
Expand Down Expand Up @@ -1800,7 +1800,7 @@ function unsupportedAggregationFilterError(alias: string, backend: string): Erro
`Per-aggregation \`filter\` on "${alias}" is not supported by this backend (${backend}). ` +
`The query is spelled correctly and @objectstack/spec AggregationNodeSchema declares the key — ` +
`this backend compiles no conditional-aggregate (SQL FILTER (WHERE …) / CASE WHEN) expression ` +
`for it, so it is refused rather than silently aggregating the UNFILTERED rows (#10413), which ` +
`for it, so it is refused rather than silently aggregating the UNFILTERED rows, which ` +
`is why it answers NOT_IMPLEMENTED/501 rather than a 400. \`engine.aggregate\` lowers filtered ` +
`aggregations in memory for every driver without native support — route the query through the ` +
`engine, or drop the \`filter\` key.`,
Expand Down Expand Up @@ -1834,7 +1834,7 @@ function refuseDistinctAggregateWithoutField(func: string): never {
const err = new Error(
`Aggregate function "${func}" needs a "field" — there is nothing to deduplicate. ` +
`COUNT(*) counts rows and is the spelling that takes no field; a distinct count has to name ` +
`the column whose values are deduplicated. Add "field" to the aggregations[] entry (#6409).`,
`the column whose values are deduplicated. Add "field" to the aggregations[] entry.`,
) as Error & { code?: string; status?: number };
err.code = StandardErrorCode.enum.INVALID_QUERY;
err.status = 400;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,9 @@ export async function backfillRemoteCanonicalColumns(
);
} else if (report.rowsConverted || report.epochTextRowsConverted) {
logger?.info?.(
`[driver-turso] canonicalised remote ${column.kind} storage (#5770) for ${where}`,
`[driver-turso] canonicalised remote ${column.kind} storage for ${where}: the converted ` +
`rows now hold the one canonical text form, and the column drops the unindexable ` +
`read-side repair only once a pass finds nothing left to convert`,
{
rowsConverted: report.rowsConverted,
epochTextRowsConverted: report.epochTextRowsConverted,
Expand All @@ -833,8 +835,8 @@ export async function backfillRemoteCanonicalColumns(
if (report.unresolvedEpochTextRows > 0) {
logger?.warn(
`[driver-turso] ${report.unresolvedEpochTextRows} row(s) in ${where} hold digits-only ` +
`text outside the interpretable epoch-millisecond band and were left untouched ` +
`(cloud#1005 后果 B, unresolvable remainder)`,
`text outside the interpretable epoch-millisecond band and were left untouched: a value ` +
`that cannot be read as an instant is counted and reported, never guessed at`,
{ table: column.table, field: column.field },
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ describe('RemoteTransport comparand refusal — the value half of #1004', () =>
const { t } = transportWithCapturingClient();
const err = await t.find('deal', { where: { payload: { $eq: { a: 1 } } } }).catch((e) => e);
expect(err.message).toMatch(/must be a string, number, bigint, boolean, null or Date/);
expect(err.message).toMatch(/#1004, #1058/);
expect(err.message).toContain('indistinguishable from "no rows matched".');
});

it('truncates a large comparand rather than pasting it into the log', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,12 @@ describe('RemoteTransport $null comparand refusal (#1116)', () => {
expect(err.message).toContain('the side opposite the false it was written to mean');
});

it('cites the ruling and the framework landing so both repos are findable', async () => {
it('states the ruling and its landing in words: refused, not coerced, by every driver', async () => {
const err = await refusalOf({ stage: { $null: 'yes' } });
expect(err.message).toContain('objectstack#5347');
expect(err.message).toContain('objectstack#5368');
expect(err.message).toContain('#1116');
expect(err.message).toContain('It is refused rather than coerced');
expect(err.message).toContain(
'Every driver now refuses it, so one filter no longer gets a different answer per backend.',
);
});

it('names the spec declaration rather than only complaining', async () => {
Expand Down
53 changes: 26 additions & 27 deletions packages/drivers/driver-turso/src/remote-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ function undeclaredAggregateFunctionError(func: string): Error {
`Declared functions: ${DECLARED_AGGREGATE_FUNCTIONS.join(', ')} ` +
`(@objectstack/spec AggregationFunction). Fix the "function" key of the aggregations[] ` +
`entry — the Query Protocol has no such function, so this is a query no backend can run, ` +
`not a gap in this one (#5907).`,
`not a gap in this one.`,
) as Error & { code?: string; status?: number };
err.code = StandardErrorCode.enum.INVALID_QUERY;
err.status = 400;
Expand Down Expand Up @@ -984,7 +984,7 @@ function uncompilableAggregateFunctionError(func: string): Error {
`correctly and @objectstack/spec AggregationFunction declares it — this is a capability gap ` +
`in the backend, not a mistake in the query, which is why it answers NOT_IMPLEMENTED/501 ` +
`rather than a 400. Aggregate with a function this backend compiles; whether the declaration ` +
`itself should stand is ADR-0049's enforce-or-remove question (#5907).`,
`itself should stand is ADR-0049's enforce-or-remove question.`,
) as Error & { code?: string; status?: number };
err.code = StandardErrorCode.enum.NOT_IMPLEMENTED;
err.status = 501;
Expand All @@ -1011,7 +1011,7 @@ function refuseDistinctAggregateWithoutField(func: string): never {
const err = new Error(
`Aggregate function "${func}" needs a "field" — there is nothing to deduplicate. ` +
`COUNT(*) counts rows and is the spelling that takes no field; a distinct count has to name ` +
`the column whose values are deduplicated. Add "field" to the aggregations[] entry (#6409).`,
`the column whose values are deduplicated. Add "field" to the aggregations[] entry.`,
) as Error & { code?: string; status?: number };
err.code = StandardErrorCode.enum.INVALID_QUERY;
err.status = 400;
Expand Down Expand Up @@ -1068,7 +1068,7 @@ function refusePerAggregationFilter(alias: string): never {
`Per-aggregation \`filter\` on "${alias}" is not supported by this backend (Turso remote transport). ` +
`The query is spelled correctly and @objectstack/spec AggregationNodeSchema declares the key — ` +
`this backend compiles no conditional-aggregate (SQL FILTER (WHERE …) / CASE WHEN) expression ` +
`for it, so it is refused rather than silently aggregating the UNFILTERED rows (#10413), which ` +
`for it, so it is refused rather than silently aggregating the UNFILTERED rows, which ` +
`is why it answers NOT_IMPLEMENTED/501 rather than a 400. \`engine.aggregate\` lowers filtered ` +
`aggregations in memory for every driver without native support — route the query through the ` +
`engine, or drop the \`filter\` key.`,
Expand Down Expand Up @@ -1105,7 +1105,7 @@ function refuseDateBucketedGroupBy(granularity: string): never {
`a capability gap in the backend, not a mistake in the query, which is why it answers ` +
`NOT_IMPLEMENTED/501 rather than a 400. A driver publishes the granularities it buckets ` +
`natively as \`supports.queryDateGranularity\`; the engine reads that record and buckets ` +
`in memory for every granularity absent from it, which is always correct (#6212).`,
`in memory for every granularity absent from it, which is always correct.`,
) as Error & { code?: string; status?: number };
err.code = StandardErrorCode.enum.NOT_IMPLEMENTED;
err.status = 501;
Expand Down Expand Up @@ -3853,8 +3853,8 @@ export class RemoteTransport {
`(with driver-sql, driver-sqlite-wasm and Turso local) compiled IS NULL (anything but false), ` +
`driver-memory's query path and driver-mongodb compiled IS NOT NULL (anything but true), and ` +
`driver-memory's matcher dropped the constraint entirely. Note "false" the STRING is truthy, ` +
`so it landed on the side opposite the false it was written to mean ` +
`(objectstack#5347, objectstack#5368, #1116).`,
`so it landed on the side opposite the false it was written to mean. Every driver now ` +
`refuses it, so one filter no longer gets a different answer per backend.`,
);
}

Expand Down Expand Up @@ -3946,12 +3946,12 @@ export class RemoteTransport {
`[RemoteTransport] Operator "$exists" on field "${field}" requires a boolean comparand (true or ` +
`false). Received ${shown} (${preview(value)}) at '${object}.${field}'.$exists. ` +
`@objectstack/spec FieldOperatorsSchema declares $exists as a boolean. It is refused rather ` +
`than coerced for the same reason $null is (objectstack#5347): a non-boolean lands on ` +
`than coerced for the same reason $null is: a non-boolean lands on ` +
`whichever side the backend's two-branch conditional happens to default to, and those ` +
`defaults point in OPPOSITE directions — this transport's \`=== false\` test compiled ` +
`IS NOT NULL for anything but false, a \`=== true\` test compiles IS NULL for anything but ` +
`true. Note "false" the STRING is truthy, so it landed on the side opposite the false it was ` +
`written to mean (objectstack#5369, objectstack#5903).`,
`written to mean.`,
);
}

Expand Down Expand Up @@ -4156,7 +4156,7 @@ export class RemoteTransport {
subtree,
`[RemoteTransport] $between on ${target} must be lowered to $gte/$lte before it reaches the ` +
`transport — TursoDriver.toRemoteFieldSpec does that so the calendar-day upper-bound rule is ` +
`applied exactly once (#1003). Refusing rather than compiling a second, rule-free range.`,
`applied exactly once. Refusing rather than compiling a second, rule-free range.`,
);
}
// [#20020, the #8220 contract] The two arms below name the target and the
Expand All @@ -4182,7 +4182,7 @@ export class RemoteTransport {
subtree,
`[RemoteTransport] Unsupported filter operator "${op}" on ${target} in remote mode. Supported: ` +
`${SUPPORTED_FILTER_OPERATORS.join(', ')}. Refusing rather than compiling it to an equality — a ` +
`silent degradation is indistinguishable from "no rows matched" (#1004).`,
`silent degradation is indistinguishable from "no rows matched".`,
);
}

Expand Down Expand Up @@ -4239,16 +4239,15 @@ export class RemoteTransport {
subtree,
`${shared}. "${key}" IS a field operator, one level down: write ` +
`{ <field>: { "${key}": <value> } } — e.g. { stage: { "${key}": … } } — rather than putting ` +
`it at the condition level (objectstack#5769, objectstack#5348).`,
`it at the condition level.`,
);
}
return this.withheldRefusal(
withheld,
subtree,
`${shared}. The Filter Protocol declares no "${key}" at any level; on a read it cost an empty ` +
`page, and on deleteMany/updateMany the predicate is constantly false or constantly true ` +
`depending on which side of the comparison the literal lands (objectstack#5769, ` +
`objectstack#5348).`,
`depending on which side of the comparison the literal lands.`,
);
}

Expand Down Expand Up @@ -4291,7 +4290,7 @@ export class RemoteTransport {
`filter conditions, but received ${shown} (${preview(value)}). @objectstack/spec ` +
`FilterConditionSchema declares "${key}" as FilterCondition[]. Refusing rather than falling ` +
`through to the field path, which read '${key}' as a COLUMN NAME and compiled a predicate ` +
`against a column that cannot exist (objectstack#5769).`,
`against a column that cannot exist.`,
);
}

Expand Down Expand Up @@ -4439,7 +4438,7 @@ export class RemoteTransport {
`because the backends disagreed: driver-sql handed it to knex and got a bare "Undefined ` +
`binding(s)" Error carrying no code, while Turso's remote transport compiled it to IS NULL ` +
`— so \`{ owner_id: ctx.user?.id }\` with a missing id silently matched every env-wide row ` +
`instead of failing (#6050).`,
`instead of failing.`,
);
}

Expand Down Expand Up @@ -4490,12 +4489,12 @@ export class RemoteTransport {
`FilterConditionSchema declares \`$not: FilterConditionSchema\`). Refusing rather than ` +
`reading '${branch}' as a COLUMN NAME — that is the bug this branch exists to close: it ` +
`compiled a predicate against a column that cannot exist, and named it as a field of ` +
`'${object}' when it complained (#1051, #1076). A negation of "no condition" is written ` +
`'${object}' when it complained. A negation of "no condition" is written ` +
`\`{ $not: {} }\`, which matches zero rows.`
: `Every element of ${branch} must be a plain object of conditions (spec ` +
`FilterConditionSchema). Refusing rather than skipping it — a dropped element leaves ` +
`valid SQL that answers a DIFFERENT question: narrower in $or, wider in $and (#1004, ` +
`#1058, #1066, #1073). An intentionally unconstrained branch is written \`{}\`.`;
`valid SQL that answers a DIFFERENT question: narrower in $or, wider in $and. An ` +
`intentionally unconstrained branch is written \`{}\`.`;
// [#20039, the #8220 contract] The position, the kind and the preview are
// the predicate's (a policy's literal, measured) — and so is which of the
// three combinators it sat under, since the requirement sentence names it.
Expand Down Expand Up @@ -4562,8 +4561,8 @@ export class RemoteTransport {
`[RemoteTransport] where on '${object}' is ${describeValue(filters)}, not a filter condition: ` +
`${preview(filters)}. ${requirement} Refusing rather than compiling it to NO WHERE clause — ` +
`a filter that vanishes WIDENS the statement to the whole table, so a read returns exactly ` +
`the rows the filter was written to exclude and deleteMany/updateMany touch every row ` +
`(#1004, #1058, #1066, #1073, #1075). "No filter" is written \`{}\`, or by omitting \`where\`.`,
`the rows the filter was written to exclude and deleteMany/updateMany touch every row. ` +
`"No filter" is written \`{}\`, or by omitting \`where\`.`,
);
}

Expand Down Expand Up @@ -4601,7 +4600,7 @@ export class RemoteTransport {
`(${SUPPORTED_FILTER_OPERATORS.join(', ')}); to compare against a value, write the value ` +
`itself. Refusing rather than dropping the condition — a predicate that vanishes WIDENS the ` +
`statement to the whole table, so the caller gets back exactly the rows the filter was ` +
`written to exclude (#1004, #1058, #1066).`,
`written to exclude.`,
);
}

Expand Down Expand Up @@ -4687,9 +4686,9 @@ export class RemoteTransport {
`compares a column against another column instead of against a value. The query DSL ` +
`declares this form (spec FieldReferenceSchema) but no executor compiles it, so it is ` +
`refused here rather than bound as the marker's JSON text — which is valid SQL that ` +
`matches nothing (#1058). Compare against a literal, or select both columns and compare ` +
`matches nothing. Compare against a literal, or select both columns and compare ` +
`after retrieval. The columns and the operator this filter used are withheld from the ` +
`message (#7929); the full diagnostic is in the server log.`,
`message; the full diagnostic is in the server log.`,
diagnostic,
value,
`${diagnostic} No executor compiles this form in remote mode — compare against a ` +
Expand All @@ -4712,14 +4711,14 @@ export class RemoteTransport {
`transport cannot bind. A comparison value must be ` +
`${ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE}. Refusing rather than binding its JSON text ` +
`— that compiles to valid SQL matching zero rows, which is indistinguishable from ` +
`"no rows matched" (#1004, #1058).`;
`"no rows matched".`;
this.diagnosticSink?.(diagnostic);
return withheldInvalidFilterError(
`[RemoteTransport] A filter comparand in this query is a value this transport cannot bind. ` +
`A comparison value must be ${ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE}. Refusing rather ` +
`than binding its JSON text — that compiles to valid SQL matching zero rows, which is ` +
`indistinguishable from "no rows matched" (#1004, #1058). The field, the operator and the ` +
`value this filter used are withheld from the message (#8197); the full diagnostic is in ` +
`indistinguishable from "no rows matched". The field, the operator and the ` +
`value this filter used are withheld from the message; the full diagnostic is in ` +
`the server log.`,
diagnostic,
value,
Expand Down
Loading
Loading