431645ae60
Closes the placeholder-substitution gap reported during manual
testing: FK violations were rendering `<value>` and `<column>`
literally because the App had no schema awareness. With this
change the runtime resolves the schema-dependent facts before
the App ever sees the failure.
## Architecture
- **Database** gains two public methods backed by new worker
Request variants:
- `read_relationships(table)` → (outbound, inbound) FK list
(lifts the previously-private `read_relationships_*` pair
into the public surface, behind a `RelationshipsReply`
type alias).
- `find_rows_matching(table, column, value, limit)` →
`DataResult` for row pinpoint queries.
- **friendly module** gets:
- New `FailureContext` struct: schema-resolved facts the
runtime builds (table, column, value, parent_table,
parent_column, child_table, optional diagnostic_table).
- `TranslateContext` loses its lifetime parameter and gains
`parent_table` / `parent_column` fields. All string fields
are now `Option<String>` for ownership simplicity.
- `TranslateContext::from_facts(operation, verbosity, facts)`
helper.
- Translator's FK paths now use `ctx.parent_table` /
`ctx.parent_column` for child-side wording; FK Update gets
a dedicated `fk_child_side_update` arm.
- FK dispatch is enrichment-driven first
(`parent_table` set → child-side; `child_table` set →
parent-side), with operation as the tiebreaker.
- The translator forwards `ctx.diagnostic_table` onto the
`FriendlyError` so pinpointed rows render through the
existing ADR-0017 §7 bordered renderer.
- **Event** `DslFailed` carries `(command, error, facts)`.
The runtime populates `facts` via `enrich_dsl_failure`
before posting the event.
- **Runtime** `enrich_dsl_failure(database, command, error)`
classifies and resolves:
- UNIQUE INSERT/UPDATE: parses `T.col` from engine message,
finds the user's attempted value (with schema fallback
for natural-order multi-value INSERT — including the
serial/shortid auto-skip rule from `do_insert`), pinpoints
the existing conflicting row(s) via `find_rows_matching`
and renders as a `DiagnosticTable`.
- NOT NULL INSERT/UPDATE: parses `T.col`; no value
(definitionally null) and no pinpoint (engine doesn't
identify the row).
- FK INSERT/UPDATE: outbound relationship lookup picks the
FK column the user is touching; resolves
`parent_table`/`parent_column`/`value`. UPDATE falls back
to inbound (parent-side) when no outbound match.
- FK DELETE: inbound relationship lookup picks a child_table
that references this row.
- **App** drops its old `attempted_value_for` /
`column_from_qualified_target` helpers (their work moved to
runtime where the Database is in scope).
`build_translate_context` combines the runtime-supplied
facts with the operation derived from the Command and the
App's verbosity.
## Manual-test fixes folded in
Two issues surfaced during manual testing of the initial
implementation, both fixed:
1. Natural-order multi-value INSERT
(`insert into Orders values (4, 11.99)`) skipped FK
enrichment because `user_value_for_column` only knew the
single-value short form. The schema-aware lookup
(`user_value_for_column_with_schema`) now mirrors
`do_insert`'s position-mapping rule (auto-generated
columns skipped), so positional INSERTs onto tables with
serial/shortid PKs resolve correctly. Regression test:
`enrich_fk_insert_natural_order_multi_value_resolves_via_schema`.
2. The arity error on INSERT now lists the columns it
expected — `expected 3 value(s) for (id, Name, Email), got 2`
instead of the bare count. Surfaces what the user needs
to fix without making them go check the schema.
## Tests
`tests/friendly_enrichment.rs` (+8 integration tests):
- UNIQUE INSERT with explicit columns: facts.{table, column,
value, diagnostic_table} all resolved; pinpoint shows
conflicting row.
- UNIQUE INSERT natural-order short form: schema fallback
resolves the value.
- UNIQUE UPDATE: value pulled from assignments.
- NOT NULL INSERT: table+column resolved, value None
(correct), no pinpoint.
- FK INSERT: parent_table, parent_column, value all resolved
via outbound relationship lookup.
- FK INSERT natural-order multi-value: schema-aware lookup
with auto-skip resolves correctly (regression for the
manual-test bug).
- FK DELETE: child_table resolved via inbound relationship
lookup.
- DbError::Unsupported: enrichment returns default
FailureContext (no false positives).
App-level tests updated to populate `FailureContext` directly
(simulating runtime enrichment) for the verbosity / threading
checks.
## Tally
610 tests passing (was 603: +8 enrichment integration tests
minus 1 obsolete App-side helper test that the runtime
absorbed). Clippy clean with nursery lints. Release builds.
64 lines
2.3 KiB
Rust
64 lines
2.3 KiB
Rust
//! Friendly error layer and i18n message catalog (ADR-0019).
|
|
//!
|
|
//! Single chokepoint for user-visible message text. Engine
|
|
//! errors flow through `translate()` into a structured
|
|
//! [`FriendlyError`] payload that the renderer (in
|
|
//! `output_render`) composes into final output. Every other
|
|
//! user-visible string in the codebase migrates to this
|
|
//! catalog over time via the [`t!`] macro (ADR-0019 §9).
|
|
//!
|
|
//! ## Catalog
|
|
//!
|
|
//! The catalog lives in `strings/<locale>.yaml`, embedded at
|
|
//! compile time and parsed once on first access. Today the only
|
|
//! locale is `en-US`; runtime selection is deferred (ADR-0019
|
|
//! §8.2). Hierarchical YAML keys flatten to dot-paths
|
|
//! internally — `error.unique.insert.verbose` is the path the
|
|
//! `t!()` macro and the translator look up.
|
|
//!
|
|
//! ## The `t!()` macro
|
|
//!
|
|
//! ```ignore
|
|
//! use rdbms_playground::t;
|
|
//! let s = t!("error.unique.insert.verbose",
|
|
//! table = "Customers", column = "id");
|
|
//! ```
|
|
//!
|
|
//! Placeholder values implement `Display`. Format specifiers
|
|
//! (`{name:08.2}`, `{name:>10}`, …) are explicitly rejected at
|
|
//! substitution time — see ADR-0019 §8.4.
|
|
|
|
pub mod error;
|
|
pub mod format;
|
|
pub mod keys;
|
|
pub mod translate;
|
|
|
|
pub use error::{DiagnosticTable, FriendlyError};
|
|
pub use format::{catalog, Catalog};
|
|
pub use translate::{FailureContext, Operation, TranslateContext, Verbosity};
|
|
|
|
// `translate::translate` and `format::translate` are different
|
|
// callables — the former is the structured DbError → FriendlyError
|
|
// classifier (the H1 entry point); the latter is the lower-level
|
|
// catalog lookup the `t!()` macro expands to. Re-export both
|
|
// under non-conflicting names.
|
|
pub use format::translate;
|
|
pub use translate::translate as translate_error;
|
|
|
|
/// Look up `key` in the catalog and substitute named arguments.
|
|
///
|
|
/// Panics if the key is missing, if the template has malformed
|
|
/// placeholders, or if the args don't supply every name the
|
|
/// template references. The catalog validator unit test (Step 7,
|
|
/// ADR-0019 §8.6) catches these at build time so they should
|
|
/// never fire at runtime.
|
|
#[macro_export]
|
|
macro_rules! t {
|
|
($key:literal $(, $name:ident = $value:expr)* $(,)?) => {{
|
|
$crate::friendly::translate(
|
|
$key,
|
|
&[$( (stringify!($name), &$value as &dyn ::std::fmt::Display) ),*],
|
|
)
|
|
}};
|
|
}
|