f1e9484af3
End-to-end `seed <table> [count]` path, both modes: - Command::Seed AST + grammar node (show-data table slot + optional positional count) + REGISTRY registration + build_seed. - Runtime dispatch -> Database::seed -> Request::Seed worker arm -> do_seed. - do_seed (Phase-1 skeleton): generates whole rows for non-FK, non-autogen columns via the seed library and inserts them one at a time through do_insert (reusing validation / autogen autofill / FK-error / persistence). One undo step (snapshot_then wraps it) and one history.log line (only the first row carries the source); default count 20. - help (`help seed`) + parse-usage catalog entries. - Reuses CommandOutcome::Insert for the auto-show; a dedicated SeedResult (capped preview + advisory) replaces it in P1.3. 5 Tier-3 integration tests (parse, populate+persist, default-20, reproducible --seed, one history line). 2327 pass / 0 fail / 0 skip, clippy all-targets clean. Deferred to P1.3: FK sampling, identifier/constraint uniqueness, CHECK derivation, block guard, capped preview, advisory, multi-row path. Deferred to P1.4: completion/highlight/hint/validity wiring + --seed flag.
754 lines
29 KiB
Rust
754 lines
29 KiB
Rust
//! Per-category catalog key schemas (ADR-0019 §8.3).
|
|
//!
|
|
//! Every catalog key the friendly-error layer references at
|
|
//! runtime is enumerated here together with its expected
|
|
//! placeholder set. The validator
|
|
//! (`tests::keys_validate_against_catalog`) walks this list and
|
|
//! asserts:
|
|
//!
|
|
//! - the key exists in the catalog;
|
|
//! - every placeholder declared here appears at least once in
|
|
//! the template;
|
|
//! - no placeholder appears in the template that isn't declared
|
|
//! here (catches typos in either direction);
|
|
//! - every catalog key (outside the `_test.*` sanity group) is
|
|
//! declared here (catches dead YAML entries).
|
|
//!
|
|
//! Adding a new translation site is a two-step change: add the
|
|
//! key + placeholders here, add the YAML entry. Either alone
|
|
//! fails the validator.
|
|
//!
|
|
//! ## Convention
|
|
//!
|
|
//! Each error entry in the catalog has:
|
|
//!
|
|
//! - a `.headline` template — used in both short and verbose
|
|
//! modes;
|
|
//! - optionally a `.hint` template — surfaced only in verbose
|
|
//! mode.
|
|
//!
|
|
//! Single-line errors (object-not-found, already-exists,
|
|
//! invalid-value) have no hint; the headline carries the whole
|
|
//! message.
|
|
//!
|
|
//! Other categories (`help.*`, `ok.*`, `client_side.*`,
|
|
//! `replay.*`, `parse.*`, modal labels, …) get added to this
|
|
//! list as the migration sweep (ADR-0019 §9) lands them.
|
|
|
|
/// `(key, expected_placeholders)`. Sorted by key for grep-ability.
|
|
pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
|
|
// ---- Pre-submit diagnostics (ADR-0027) ----
|
|
("diagnostic.ambiguous_column", &["column", "qualifiers"]),
|
|
("diagnostic.auto_column_overridden", &["column", "type"]),
|
|
("diagnostic.compound_arity_mismatch", &["op", "left_n", "right_n"]),
|
|
("diagnostic.cte_arity_mismatch", &["cte", "declared", "actual"]),
|
|
("diagnostic.duplicate_cte", &["name"]),
|
|
("diagnostic.eq_null", &[]),
|
|
("diagnostic.insert_arity_mismatch", &["expected", "actual"]),
|
|
// ADR-0033 §8.1 / Amendment 5: Form B (no column list) variant.
|
|
// Cited from issue #1 — every column must be supplied positionally.
|
|
(
|
|
"diagnostic.insert_arity_mismatch_form_b",
|
|
&["expected", "actual"],
|
|
),
|
|
// ADR-0036 Amendment 1 / issue #17: simple-mode Form B variants.
|
|
(
|
|
"diagnostic.insert_arity_mismatch_form_b_simple",
|
|
&["expected", "columns", "skipped", "actual"],
|
|
),
|
|
(
|
|
"diagnostic.insert_arity_mismatch_all_auto",
|
|
&["table", "actual"],
|
|
),
|
|
("diagnostic.not_null_missing", &["column"]),
|
|
("diagnostic.like_numeric", &["column", "type"]),
|
|
("diagnostic.projection_alias_misplaced", &["alias", "clause"]),
|
|
("diagnostic.type_mismatch", &["column", "type"]),
|
|
("diagnostic.unknown_column", &["name", "table"]),
|
|
("diagnostic.unknown_qualifier", &["qualifier"]),
|
|
("diagnostic.unknown_table", &["name"]),
|
|
// ---- Friendly-error translations of engine messages
|
|
// ---- (ADR-0019; ADR-0032 §11.5).
|
|
("engine.aggregate_misuse", &["name"]),
|
|
("engine.ambiguous_column", &["column"]),
|
|
("engine.compound_arity_mismatch", &["op"]),
|
|
("engine.group_by_required", &[]),
|
|
("engine.no_such_column", &["name"]),
|
|
("engine.no_such_table", &["name"]),
|
|
("engine.recursive_cte_malformed", &[]),
|
|
("engine.scalar_subquery_too_many_rows", &[]),
|
|
// ---- Already-exists collisions (anchor: "already exists") ----
|
|
("error.already_exists.column.headline", &["table", "column"]),
|
|
("error.already_exists.relationship.headline", &["name"]),
|
|
("error.already_exists.table.headline", &["name"]),
|
|
// ---- CHECK violations ----
|
|
("error.check.insert.headline", &["table", "column"]),
|
|
("error.check.insert.hint", &["column"]),
|
|
(
|
|
"error.check.insert.hint_with_rule",
|
|
&["value", "rule", "column"],
|
|
),
|
|
("error.check.update.headline", &["table", "column"]),
|
|
("error.check.update.hint", &["column"]),
|
|
(
|
|
"error.check.update.hint_with_rule",
|
|
&["value", "rule", "column"],
|
|
),
|
|
// ---- FK violations (anchor: "referenced by") ----
|
|
(
|
|
"error.foreign_key.child_side.insert.headline",
|
|
&["parent_table", "parent_column", "value"],
|
|
),
|
|
(
|
|
"error.foreign_key.child_side.insert.hint",
|
|
&["parent_table", "parent_column"],
|
|
),
|
|
(
|
|
"error.foreign_key.child_side.update.headline",
|
|
&["parent_table", "parent_column", "value"],
|
|
),
|
|
(
|
|
"error.foreign_key.child_side.update.hint",
|
|
&["parent_table", "parent_column"],
|
|
),
|
|
(
|
|
"error.foreign_key.parent_side.delete.headline",
|
|
&["table", "child_table"],
|
|
),
|
|
("error.foreign_key.parent_side.delete.hint", &[]),
|
|
(
|
|
"error.foreign_key.parent_side.update.headline",
|
|
&["table", "child_table"],
|
|
),
|
|
("error.foreign_key.parent_side.update.hint", &[]),
|
|
// ---- Generic engine refusal ----
|
|
("error.generic.headline", &["operation"]),
|
|
("error.generic.hint", &["table"]),
|
|
("error.generic.hint_no_table", &[]),
|
|
// ---- Invalid-value errors (pre-engine, single-line) ----
|
|
(
|
|
"error.invalid_value.arity.headline",
|
|
&["expected", "actual"],
|
|
),
|
|
("error.invalid_value.empty_insert.headline", &[]),
|
|
("error.invalid_value.empty_update.headline", &[]),
|
|
// ---- Not-found errors (anchor: "no such ...") ----
|
|
("error.not_found.column.headline", &["table", "column"]),
|
|
("error.not_found.column_unqualified.headline", &["column"]),
|
|
("error.not_found.relationship.headline", &["name"]),
|
|
("error.not_found.table.headline", &["name"]),
|
|
// ---- NOT NULL violations ----
|
|
("error.not_null.insert.headline", &["table", "column"]),
|
|
("error.not_null.insert.hint", &["column"]),
|
|
("error.not_null.update.headline", &["table", "column"]),
|
|
("error.not_null.update.hint", &["column"]),
|
|
// ---- Type mismatch ----
|
|
(
|
|
"error.type_mismatch.change_column.headline",
|
|
&["table", "column", "src_type", "target_type"],
|
|
),
|
|
(
|
|
"error.type_mismatch.change_column.hint",
|
|
&["target_type"],
|
|
),
|
|
(
|
|
"error.type_mismatch.insert.headline",
|
|
&["value", "expected_type"],
|
|
),
|
|
(
|
|
"error.type_mismatch.insert.hint",
|
|
&["table", "column", "expected_type"],
|
|
),
|
|
(
|
|
"error.type_mismatch.update.headline",
|
|
&["value", "expected_type"],
|
|
),
|
|
(
|
|
"error.type_mismatch.update.hint",
|
|
&["table", "column", "expected_type"],
|
|
),
|
|
// ---- Help text ----
|
|
("help.cli_banner", &[]),
|
|
// In-app `help` — framing + per-command entries keyed by
|
|
// each CommandNode's `help_id` (ADR-0024 §help_id).
|
|
("help.intro", &[]),
|
|
("help.dsl_section", &[]),
|
|
("help.types_reference", &[]),
|
|
("help.detail_hint", &[]),
|
|
("help.unknown_topic", &["topic"]),
|
|
("help.app.quit", &[]),
|
|
("help.app.help", &[]),
|
|
("help.app.rebuild", &[]),
|
|
("help.app.save", &[]),
|
|
("help.app.new", &[]),
|
|
("help.app.load", &[]),
|
|
("help.app.export", &[]),
|
|
("help.app.import", &[]),
|
|
("help.app.mode", &[]),
|
|
("help.app.messages", &[]),
|
|
("help.app.undo", &[]),
|
|
("help.app.redo", &[]),
|
|
("help.app.copy", &[]),
|
|
("help.ddl.create", &[]),
|
|
("help.ddl.create_m2n", &[]),
|
|
("help.ddl.sql_create_table", &[]),
|
|
("help.ddl.sql_drop_table", &[]),
|
|
("help.ddl.sql_create_index", &[]),
|
|
("help.ddl.sql_drop_index", &[]),
|
|
("help.ddl.sql_alter_table", &[]),
|
|
// Advanced-mode SQL CREATE TABLE / DROP TABLE no-op notes (ADR-0035 §4).
|
|
("ddl.create_skipped_exists", &["name"]),
|
|
("ddl.drop_skipped_absent", &["name"]),
|
|
// Advanced-mode SQL CREATE INDEX / DROP INDEX no-op notes (ADR-0035 §4d).
|
|
("ddl.create_index_skipped_exists", &["name"]),
|
|
("ddl.drop_index_skipped_absent", &["name"]),
|
|
("help.ddl.drop", &[]),
|
|
("help.ddl.add", &[]),
|
|
("help.ddl.rename", &[]),
|
|
("help.ddl.change", &[]),
|
|
("help.data.show", &[]),
|
|
("help.data.seed", &[]),
|
|
("help.data.insert", &[]),
|
|
("help.data.update", &[]),
|
|
("help.data.delete", &[]),
|
|
("help.data.replay", &[]),
|
|
("help.data.explain", &[]),
|
|
// ---- Hint panel ambient typing assistance (ADR-0022 §6) ----
|
|
("hint.ambient_complete", &[]),
|
|
(
|
|
"hint.ambient_error_with_usage",
|
|
&["message", "usage"],
|
|
),
|
|
("hint.ambient_expected", &["expected"]),
|
|
(
|
|
"hint.ambient_invalid_ident",
|
|
&["kind", "found"],
|
|
),
|
|
("hint.ambient_typing_name", &[]),
|
|
// Issue #4: introduce the advanced-mode CREATE TABLE element
|
|
// slot (`create table T (`) so the otherwise-invisible
|
|
// column-name role reads as the dominant first move.
|
|
("hint.create_table_element", &[]),
|
|
("hint.value_literal_slot", &[]),
|
|
(
|
|
"hint.ambient_typing_name_then",
|
|
&["next"],
|
|
),
|
|
// Per-column-type value-slot hints (ADR-0024 §Phase D).
|
|
("hint.value_slot_blob", &[]),
|
|
("hint.value_slot_bool", &[]),
|
|
("hint.value_slot_date", &[]),
|
|
("hint.value_slot_datetime", &[]),
|
|
("hint.value_slot_decimal", &[]),
|
|
("hint.value_slot_int", &[]),
|
|
("hint.value_slot_real", &[]),
|
|
("hint.value_slot_serial", &[]),
|
|
("hint.value_slot_shortid", &[]),
|
|
("hint.value_slot_text", &[]),
|
|
("hint.value_slot_for_column", &["column", "detail"]),
|
|
("hint.value_slot_autogen_skipped", &["columns"]),
|
|
// ---- Parse error rendering ----
|
|
("parse.available_commands", &["commands"]),
|
|
("parse.caret", &["padding"]),
|
|
// Custom (try_map / source-slice) error messages raised
|
|
// by the DSL parser. See `parse.custom.*` in the catalog.
|
|
("parse.custom.alter_add_primary_key", &[]),
|
|
("parse.custom.alter_named_unique", &[]),
|
|
("parse.custom.bind_type_mismatch", &["found", "expected"]),
|
|
("parse.custom.change_column_flags_exclusive", &[]),
|
|
("parse.custom.constraint_redundant_on_pk", &["column", "constraint"]),
|
|
("parse.custom.create_table_needs_pk", &[]),
|
|
("parse.custom.expression_too_deep", &[]),
|
|
("parse.custom.insert_form_a_missing_values", &["columns"]),
|
|
("parse.custom.on_action_specified_twice", &["target"]),
|
|
("parse.custom.replay_path_expected", &[]),
|
|
("parse.custom.unknown_action", &["found", "expected"]),
|
|
("parse.custom.unknown_type", &["found", "expected"]),
|
|
("parse.empty", &[]),
|
|
("parse.error", &["detail"]),
|
|
("parse.error_wrapper", &["detail"]),
|
|
// Per-command usage templates (ADR-0021 §1). One key per
|
|
// command. Multi-entry families (`add`, `drop`, `show`)
|
|
// each have multiple keys. Templates are pure prose with
|
|
// no placeholders — the renderer prepends "usage: " in
|
|
// code, not the catalog, because spacing is alignment-
|
|
// sensitive in the multi-entry case.
|
|
("parse.usage.add_column", &[]),
|
|
("parse.usage.add_constraint", &[]),
|
|
("parse.usage.add_index", &[]),
|
|
("parse.usage.add_relationship", &[]),
|
|
("parse.usage.change_column", &[]),
|
|
("parse.usage.create_table", &[]),
|
|
("parse.usage.create_m2n", &[]),
|
|
("parse.usage.sql_create_table", &[]),
|
|
("parse.usage.sql_drop_table", &[]),
|
|
("parse.usage.sql_create_index", &[]),
|
|
("parse.usage.sql_drop_index", &[]),
|
|
("parse.usage.sql_alter_table", &[]),
|
|
("parse.usage.delete", &[]),
|
|
("parse.usage.drop_column", &[]),
|
|
("parse.usage.drop_constraint", &[]),
|
|
("parse.usage.drop_index", &[]),
|
|
("parse.usage.drop_relationship", &[]),
|
|
("parse.usage.drop_table", &[]),
|
|
("parse.usage.explain", &[]),
|
|
("parse.usage.insert", &[]),
|
|
("parse.usage.rename_column", &[]),
|
|
("parse.usage.export", &[]),
|
|
("parse.usage.help", &[]),
|
|
("parse.usage.import", &[]),
|
|
("parse.usage.copy", &[]),
|
|
("parse.usage.load", &[]),
|
|
("parse.usage.messages", &[]),
|
|
("parse.usage.mode", &[]),
|
|
("parse.usage.new", &[]),
|
|
("parse.usage.quit", &[]),
|
|
("parse.usage.rebuild", &[]),
|
|
("parse.usage.redo", &[]),
|
|
("parse.usage.replay", &[]),
|
|
("parse.usage.undo", &[]),
|
|
("parse.usage.save", &[]),
|
|
("parse.usage.select", &[]),
|
|
("parse.usage.seed", &[]),
|
|
("parse.usage.show_data", &[]),
|
|
("parse.usage.show_table", &[]),
|
|
("parse.usage.show_tables", &[]),
|
|
("parse.usage.show_relationships", &[]),
|
|
("parse.usage.show_indexes", &[]),
|
|
("parse.usage.show_relationship", &[]),
|
|
("parse.usage.show_index", &[]),
|
|
("parse.usage.update", &[]),
|
|
("parse.usage.with", &[]),
|
|
("parse.expect.select_projection", &[]),
|
|
("parse.cross_join_no_on", &[]),
|
|
// ---- Project lifecycle event notes ----
|
|
("project.export_failed", &["error"]),
|
|
("project.export_ok", &["path"]),
|
|
("project.export_usage", &[]),
|
|
("project.import_empty_target", &[]),
|
|
("project.import_usage", &[]),
|
|
("project.import_zip_missing", &["path"]),
|
|
("persistence.csv.empty", &[]),
|
|
("persistence.csv.invalid_utf8", &[]),
|
|
("persistence.csv.unterminated_quote", &[]),
|
|
("persistence.encode", &["kind", "path", "message"]),
|
|
("persistence.io", &["operation", "path", "source"]),
|
|
("persistence.migrate.bad_output", &["detail"]),
|
|
("persistence.migrate.io", &["path", "source"]),
|
|
(
|
|
"persistence.migrate.newer_than_supported",
|
|
&["file", "latest"],
|
|
),
|
|
("persistence.migrate.no_migrator", &["version"]),
|
|
("persistence.migrate.step_failed", &["from", "to", "source"]),
|
|
("persistence.migrate.version_parse", &["detail"]),
|
|
("persistence.yaml.syntax", &["detail"]),
|
|
("persistence.yaml.unknown_action", &["raw"]),
|
|
("persistence.yaml.unknown_type", &["table", "column", "raw"]),
|
|
("persistence.yaml.unsupported_version", &["version"]),
|
|
("project.already_exists", &["path"]),
|
|
("project.data_root_unavailable", &[]),
|
|
("project.io", &["path", "source"]),
|
|
("project.load_path_missing", &["path"]),
|
|
("project.lock.already_held", &["pid", "hostname", "path"]),
|
|
("project.lock.read", &["path", "source"]),
|
|
("project.lock.write", &["path", "source"]),
|
|
("project.naming.too_many_collisions", &["attempts"]),
|
|
("project.naming.wordlist_too_small", &["count"]),
|
|
("project.not_a_project", &["path"]),
|
|
("project.path_not_found", &["path"]),
|
|
("project.safe_delete.io", &["path", "source"]),
|
|
("project.safe_delete.refused", &["path", "reason"]),
|
|
("project.user_name.empty", &[]),
|
|
("project.user_name.invalid_char", &["ch"]),
|
|
("project.user_name.leading_dot", &[]),
|
|
("project.resume_no_previous", &["data_root"]),
|
|
("project.resume_recorded_missing", &["path"]),
|
|
("project.saveas_target_exists", &["path"]),
|
|
("project.rebuild_failed", &["error"]),
|
|
("project.rebuild_ok", &["summary"]),
|
|
("project.switch_failed", &["error"]),
|
|
("project.switched_ok", &["display_name"]),
|
|
// ---- Advanced-mode placeholder ----
|
|
("advanced_mode.not_implemented", &["input"]),
|
|
// ---- Advanced-mode SQL surface (ADR-0030) ----
|
|
("advanced_mode.sql_in_simple", &["command"]),
|
|
("advanced_mode.also_valid_sql", &[]),
|
|
// Education note appended to a simple-mode INSERT Form B
|
|
// parse error when the user supplied more values than the
|
|
// non-auto-generated columns expect (issue #1 sub-task 2).
|
|
(
|
|
"insert.form_b_extra_values_note",
|
|
&["table", "expected_phrase", "auto_phrase", "all_cols"],
|
|
),
|
|
// Pre-flight teaching note for advanced-mode positional
|
|
// `INSERT INTO T VALUES (…)` (no column list) when the value
|
|
// count doesn't match the column count (issue #1 sub-task 3).
|
|
(
|
|
"insert.form_b_positional_count_mismatch_note",
|
|
&["table", "col_count", "col_list", "supplied", "non_auto_csv"],
|
|
),
|
|
("select.internal_table", &["table"]),
|
|
(
|
|
"cli.invalid_value",
|
|
&["flag", "value", "expected"],
|
|
),
|
|
("cli.missing_value", &["flag"]),
|
|
("cli.multiple_paths", &["first", "second"]),
|
|
("cli.resume_with_path", &[]),
|
|
("cli.unknown_argument", &["arg"]),
|
|
(
|
|
"archive.export_sequence_exhausted",
|
|
&["project", "target_dir", "limit"],
|
|
),
|
|
("archive.import_collision_exhausted", &["path", "limit"]),
|
|
("archive.invalid_zip", &["detail"]),
|
|
("archive.io", &["path", "source"]),
|
|
("archive.multiple_top_folders", &[]),
|
|
("archive.not_a_project_archive", &[]),
|
|
("archive.unsafe_entry", &["entry"]),
|
|
("archive.zip", &["path", "message"]),
|
|
// ---- DSL failure wrapper / running echo ----
|
|
("dsl.failed", &["rendered"]),
|
|
("dsl.running", &["input"]),
|
|
// ---- Persistence-fatal banner ----
|
|
("fatal.persistence", &["operation", "path", "message"]),
|
|
// ---- Modal labels ----
|
|
("modal.generic_cancelled", &["title"]),
|
|
("modal.load_cancelled", &[]),
|
|
("modal.load_picker_empty", &[]),
|
|
("modal.load_picker_nothing", &[]),
|
|
("modal.load_picker_path_prompt", &[]),
|
|
("modal.load_picker_title", &[]),
|
|
("modal.path_entry_empty_name", &[]),
|
|
("modal.path_entry_empty_path", &[]),
|
|
("modal.rebuild_cancelled", &[]),
|
|
("modal.rebuild_confirm_prompt", &[]),
|
|
("modal.rebuild_confirm_title", &[]),
|
|
("modal.redo_cancelled", &[]),
|
|
("modal.redo_confirm_command", &[]),
|
|
("modal.redo_confirm_title", &[]),
|
|
("modal.undo_cancelled", &[]),
|
|
("modal.undo_confirm_command", &[]),
|
|
("modal.undo_confirm_prompt", &[]),
|
|
("modal.undo_confirm_title", &[]),
|
|
("modal.undo_confirm_when", &["timestamp"]),
|
|
// ---- Undo / redo command surfaces (ADR-0006 Amendment 1) ----
|
|
("undo.disabled", &[]),
|
|
("undo.nothing_to_undo", &[]),
|
|
("undo.nothing_to_redo", &[]),
|
|
("undo.undone_ok", &["command"]),
|
|
("undo.redone_ok", &["command"]),
|
|
("undo.undo_failed", &["error"]),
|
|
("undo.redo_failed", &["error"]),
|
|
// ---- Status bar + panels ----
|
|
("panel.hint_empty", &[]),
|
|
("panel.hint_title", &[]),
|
|
("panel.output_title", &[]),
|
|
("panel.relationships_empty", &[]),
|
|
("panel.relationships_title", &[]),
|
|
("panel.tables_empty", &[]),
|
|
("panel.tables_title", &[]),
|
|
("status.no_project", &[]),
|
|
("status.project_label", &[]),
|
|
("value.format", &["column", "message"]),
|
|
("value.type_mismatch", &["column", "expected_human", "got"]),
|
|
// ---- Save / save-as surfaces ----
|
|
("save.already_saved", &[]),
|
|
("save.path_prompt", &[]),
|
|
("save.title_as", &[]),
|
|
("save.title_save", &[]),
|
|
// ---- Shortcut hint labels ----
|
|
("shortcut.advanced_once", &[]),
|
|
("shortcut.back_to_list", &[]),
|
|
("shortcut.browse_path", &[]),
|
|
("shortcut.cancel", &[]),
|
|
("shortcut.cancel_one_shot", &[]),
|
|
("shortcut.confirm", &[]),
|
|
("shortcut.load", &[]),
|
|
("shortcut.no", &[]),
|
|
("shortcut.quit", &[]),
|
|
("shortcut.select", &[]),
|
|
("shortcut.submit", &[]),
|
|
("shortcut.switch", &[]),
|
|
("shortcut.yes", &[]),
|
|
// ---- mode / messages banners ----
|
|
("messages.set_short", &[]),
|
|
("messages.set_verbose", &[]),
|
|
("messages.show", &["current"]),
|
|
("messages.unknown", &["value"]),
|
|
("mode.label_advanced", &[]),
|
|
("mode.label_advanced_one_shot", &[]),
|
|
("mode.label_simple", &[]),
|
|
("mode.set_advanced", &[]),
|
|
("mode.set_simple", &[]),
|
|
("mode.show_advanced", &[]),
|
|
("mode.show_simple", &[]),
|
|
("mode.unknown", &["value"]),
|
|
("mode.usage", &[]),
|
|
// ---- copy (ADR-0041) ----
|
|
("copy.done", &["count"]),
|
|
("copy.nothing", &[]),
|
|
("copy.unknown", &["value"]),
|
|
// ---- DbError Display fallback ----
|
|
("db.error.invalid_value", &["detail"]),
|
|
("db.error.io", &["detail"]),
|
|
(
|
|
"db.error.persistence_fatal",
|
|
&["operation", "path", "message"],
|
|
),
|
|
(
|
|
"db.error.rebuild_row_failed",
|
|
&["row_number", "csv_path", "table", "detail"],
|
|
),
|
|
("db.error.sqlite", &["message"]),
|
|
("db.error.unsupported", &["detail"]),
|
|
("db.error.worker_gone", &[]),
|
|
// ---- Cascade-effect summaries (per ADR-0014) ----
|
|
("db.cascade.action_blocked", &[]),
|
|
("db.cascade.action_deleted", &[]),
|
|
("db.cascade.action_set_null", &[]),
|
|
(
|
|
"db.cascade.summary",
|
|
&["count", "action", "child_table", "rel", "on_delete"],
|
|
),
|
|
// ---- change-column dry-run diagnostics (per ADR-0017) ----
|
|
// ---- add-constraint dry-run diagnostics (per ADR-0029 §5) ----
|
|
(
|
|
"db.diagnostic.add_check_summary",
|
|
&["table", "column", "total", "rule"],
|
|
),
|
|
(
|
|
"db.diagnostic.add_not_null_summary",
|
|
&["table", "column", "total"],
|
|
),
|
|
(
|
|
"db.diagnostic.add_unique_summary",
|
|
&["table", "column", "total"],
|
|
),
|
|
("db.diagnostic.force_conversion_hint", &[]),
|
|
("db.diagnostic.header_becomes", &[]),
|
|
("db.diagnostic.header_from", &[]),
|
|
("db.diagnostic.header_reason", &[]),
|
|
("db.diagnostic.header_source_rows", &["pk_label"]),
|
|
("db.diagnostic.header_source_values", &[]),
|
|
("db.diagnostic.header_to", &[]),
|
|
("db.diagnostic.header_value", &[]),
|
|
(
|
|
"db.diagnostic.incompatible_summary",
|
|
&["table", "column", "src_ty", "target_ty", "total"],
|
|
),
|
|
(
|
|
"db.diagnostic.lossy_summary",
|
|
&["table", "column", "src_ty", "target_ty", "total"],
|
|
),
|
|
(
|
|
"db.diagnostic.uniqueness_summary",
|
|
&["table", "column", "src_ty", "target_ty", "total"],
|
|
),
|
|
// ---- DSL command success summaries (ADR-0019 §9 sweep) ----
|
|
("ok.index_dropped_with_column", &["index"]),
|
|
("ok.rows_deleted", &["count"]),
|
|
("ok.rows_inserted", &["count"]),
|
|
("ok.rows_updated", &["count"]),
|
|
// ---- Client-side success notes (ADR-0017 §6, ADR-0018 §9) ----
|
|
("client_side.auto_fill_add_serial", &["count"]),
|
|
("client_side.auto_fill_add_shortid", &["count"]),
|
|
("client_side.auto_fill_transition", &["count", "kind"]),
|
|
("client_side.dont_convert_caveat", &[]),
|
|
("client_side.transformed", &["count"]),
|
|
("client_side.transformed_lossy", &["count", "lossy"]),
|
|
// ---- Replay command surfaces (ADR-0019 §9 sweep) ----
|
|
("replay.command_echo", &["command"]),
|
|
("replay.completed", &["path", "count"]),
|
|
("replay.error_could_not_open", &["path", "detail"]),
|
|
("replay.error_parse", &["detail"]),
|
|
("replay.failed_at_line", &["path", "line_number", "error"]),
|
|
("replay.failed_open", &["path", "error"]),
|
|
("replay.skipped_import", &["line", "command"]),
|
|
("replay.skipped_replay", &["line", "command"]),
|
|
// ---- UNIQUE violations (anchor: "already has the value") ----
|
|
(
|
|
"error.unique.insert.headline",
|
|
&["table", "column", "value"],
|
|
),
|
|
("error.unique.insert.hint", &["table", "column"]),
|
|
("error.unique.pk.insert.headline", &["table", "value"]),
|
|
("error.unique.pk.insert.hint", &[]),
|
|
("error.unique.pk.update.headline", &["table", "value"]),
|
|
("error.unique.pk.update.hint", &[]),
|
|
(
|
|
"error.unique.update.headline",
|
|
&["table", "column", "value"],
|
|
),
|
|
("error.unique.update.hint", &["table", "column"]),
|
|
];
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::KEYS_AND_PLACEHOLDERS;
|
|
use crate::friendly::format::catalog;
|
|
use std::collections::HashSet;
|
|
|
|
// The pre-Phase-F `keyword_and_punct_have_complete_token_vocabulary`
|
|
// test cross-checked the `Keyword` / `Punct` enums against
|
|
// `parse.token.keyword.*` / `parse.token.punct.*` catalog
|
|
// keys. With those enums deleted (ADR-0024 §migration Phase F)
|
|
// and the walker rendering keyword wording via
|
|
// `format!("`{word}`")`, the catalog entries survive only as
|
|
// historic vocabulary; the `keys_validate_against_catalog`
|
|
// test below still asserts every key in `KEYS_AND_PLACEHOLDERS`
|
|
// resolves and vice versa, which keeps the catalog itself
|
|
// honest. The dead entries collapse in ADR-0024 §cleanup-pass.
|
|
|
|
/// Walks `KEYS_AND_PLACEHOLDERS` and verifies every entry
|
|
/// matches the catalog. ADR-0019 §8.6.
|
|
///
|
|
/// Checks:
|
|
/// 1. every declared key exists in the catalog;
|
|
/// 2. every declared placeholder appears in the template;
|
|
/// 3. every placeholder used is declared (catches typos);
|
|
/// 4. every catalog key (outside `_test.*`) is declared
|
|
/// (catches dead YAML entries);
|
|
/// 5. no template contains a format specifier
|
|
/// (`{name:...}`); ADR-0019 §8.4 forbids these;
|
|
/// 6. no template contains forbidden engine vocabulary
|
|
/// (ADR-0002 user-facing posture; same forbidden list
|
|
/// as `tests/engine_vocabulary_audit.rs`).
|
|
#[test]
|
|
fn keys_validate_against_catalog() {
|
|
let cat = catalog();
|
|
let mut errors: Vec<String> = Vec::new();
|
|
|
|
for (key, expected) in KEYS_AND_PLACEHOLDERS {
|
|
let Some(template) = cat.get(key) else {
|
|
errors.push(format!("catalog missing key `{key}`"));
|
|
continue;
|
|
};
|
|
|
|
// Placeholder set check (declared ↔ used).
|
|
let actual = collect_placeholders(template);
|
|
let expected_set: HashSet<&str> = expected.iter().copied().collect();
|
|
for name in &expected_set {
|
|
if !actual.contains(*name) {
|
|
errors.push(format!(
|
|
"key `{key}`: declared placeholder `{{{name}}}` is not used in template:\n{template}"
|
|
));
|
|
}
|
|
}
|
|
for name in &actual {
|
|
if !expected_set.contains(name.as_str()) {
|
|
errors.push(format!(
|
|
"key `{key}`: template uses `{{{name}}}` but it isn't declared in keys.rs:\n{template}"
|
|
));
|
|
}
|
|
}
|
|
|
|
// Format-specifier check (ADR-0019 §8.4). Look for
|
|
// `{name:...}` shapes — the substitute helper would
|
|
// panic at runtime, but catching it at test time
|
|
// means we never ship a binary that can hit that
|
|
// panic.
|
|
if has_format_specifier(template) {
|
|
errors.push(format!(
|
|
"key `{key}`: template contains a `{{name:...}}` format specifier:\n{template}"
|
|
));
|
|
}
|
|
|
|
// Engine-vocabulary check (ADR-0002 user-facing
|
|
// posture, regression-tested in
|
|
// tests/engine_vocabulary_audit.rs).
|
|
for needle in FORBIDDEN_ENGINE_VOCABULARY {
|
|
if template.contains(needle) {
|
|
errors.push(format!(
|
|
"key `{key}`: template contains forbidden token `{needle}`:\n{template}"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
let declared: HashSet<&str> =
|
|
KEYS_AND_PLACEHOLDERS.iter().map(|(k, _)| *k).collect();
|
|
for key in cat.keys() {
|
|
if key.starts_with("_test.") {
|
|
continue;
|
|
}
|
|
if !declared.contains(key) {
|
|
errors.push(format!(
|
|
"catalog has key `{key}` but it isn't declared in keys::KEYS_AND_PLACEHOLDERS"
|
|
));
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
errors.is_empty(),
|
|
"catalog validation failed:\n {}",
|
|
errors.join("\n ")
|
|
);
|
|
}
|
|
|
|
/// Mirror of `tests/engine_vocabulary_audit.rs::FORBIDDEN`,
|
|
/// duplicated here so the catalog validator is self-contained
|
|
/// (no dependency on the integration-test binary).
|
|
const FORBIDDEN_ENGINE_VOCABULARY: &[&str] = &[
|
|
"SQLite", "sqlite", "rusqlite", "STRICT", "PRAGMA",
|
|
];
|
|
|
|
/// Detect a `{name:...}` format-specifier placeholder.
|
|
/// Doubled braces `{{` / `}}` are escapes — must skip them.
|
|
fn has_format_specifier(template: &str) -> bool {
|
|
let mut chars = template.chars().peekable();
|
|
while let Some(c) = chars.next() {
|
|
if c == '{' {
|
|
if chars.peek() == Some(&'{') {
|
|
chars.next();
|
|
continue;
|
|
}
|
|
while let Some(&nc) = chars.peek() {
|
|
if nc == '}' {
|
|
break;
|
|
}
|
|
if nc == ':' {
|
|
return true;
|
|
}
|
|
chars.next();
|
|
}
|
|
} else if c == '}' && chars.peek() == Some(&'}') {
|
|
chars.next();
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Walk `template` and pull out every `{name}` placeholder.
|
|
/// Mirrors the substitution helper's parse — if the helper
|
|
/// accepts a placeholder, this collects it.
|
|
fn collect_placeholders(template: &str) -> HashSet<String> {
|
|
let mut out = HashSet::new();
|
|
let mut chars = template.chars().peekable();
|
|
while let Some(c) = chars.next() {
|
|
if c == '{' {
|
|
if chars.peek() == Some(&'{') {
|
|
chars.next();
|
|
continue;
|
|
}
|
|
let mut name = String::new();
|
|
while let Some(&nc) = chars.peek() {
|
|
if nc == '}' {
|
|
chars.next();
|
|
break;
|
|
}
|
|
chars.next();
|
|
name.push(nc);
|
|
}
|
|
if !name.is_empty() && !name.contains(':') {
|
|
out.insert(name);
|
|
}
|
|
} else if c == '}' && chars.peek() == Some(&'}') {
|
|
chars.next();
|
|
}
|
|
}
|
|
out
|
|
}
|
|
}
|