Files
rdbms-playground/docs/adr/0009-dsl-command-syntax-conventions.md
claude@clouddev1 a95c8074f3 fix: resolve table names case-insensitively across all executors
SQL identifiers are case-insensitive, so the engine resolves a table
named in any capitalization — but our metadata tables (keyed by
table_name / parent_table / child_table) and data/<table>.csv files use
case-sensitive TEXT '=', so an operation naming a table in a different
case than stored drifted: schema ops orphaned metadata rows, and a
wrong-case insert/update/delete silently skipped the CSV write, losing
the change on the next reload/rebuild. This contradicted ADR-0009's
stated rule (case-insensitive resolution, case-preserving display).

Add a canonical_table_name helper (resolve to the stored case via
COLLATE NOCASE, excluding sqlite_* and __rdbms_* tables) and apply it at
the entry of every table-naming executor — drop table, add/drop/rename
column, change column type, add/drop constraint, add relationship, add
index, rename table, insert/update/delete, and the advanced SQL DML —
so the live schema, the metadata, and the CSV stay in step regardless of
how the user capitalized the name. This also folds the internal-table
guard into the same lookup (executors that previously lacked it now
refuse __rdbms_*/sqlite_* as "no such table"). do_rename_table now
accepts a case-variant source too.

Column names remain matched case-sensitively (a wrong case is refused as
"no such column" — strict, but never drifting), per the scope agreed
with the user.

Tests: tests/case_insensitive_names.rs — wrong-case rename-column,
insert (survives a fresh rebuild — no data loss), add-column, drop-table,
rename-table, and add-relationship, all with fresh-rebuild round-trips.
Full suite 1909 passing / 0 failing / 1 ignored; clippy clean.
2026-05-26 10:04:27 +00:00

3.7 KiB

ADR-0009: DSL command syntax conventions

Status

Accepted

Context

As the DSL grows, its commands need consistent surface conventions. Without an explicit rule, every command would invent its own way of expressing optional vs. required parts, and the surface would drift toward an unreadable soup.

The decision is informed by experience from this iteration: when we initially proposed create table X --pk the most common form (a basic table with a primary key) required a -- flag, which is cosmetically wrong — -- reads as "extra option," and the most-used form should not look like one.

Decision

The DSL surface follows three rules.

1. Required clauses use keyword grammar

Required parts of a command are written in plain words and read like English. Examples:

  • create table <Name> with pk <name>(<type>)
  • add column to table <Name>: <Name> (<Type>)
  • drop table <Name>

The with clause format is the canonical pattern for attaching required structural information to an entity-creating command, and is reusable: future iterations may add with index, with check, etc. Multiple with clauses on the same command are allowed in principle.

2. Optional flags use --prefix

Flags signal "I am asking for an extra capability or non-default behaviour." Examples planned for later iterations:

  • add 1:n relationship on Customers.Id=Orders.CustId --create-fk (auto-creates the FK column instead of requiring it to exist)
  • (future) --rename-on-clash, --no-strict, etc.

A user reading "with pk id:serial" sees only what's needed; a user reading "...with pk id:serial --some-flag" sees that they have asked for something beyond default. The visual distinction is intentional.

3. One sigil only — : for the simple-mode advanced escape

Per ADR-0003, prefixing a single line with : in simple mode treats that one submission as if it were entered in advanced mode. This is the only sigil in the system. App-level commands, DSL commands, and SQL all use plain words.

Lexical rules

  • Keywords are case-insensitive. CREATE TABLE Customers WITH PK email:TEXT is equivalent to create table Customers with pk email:text.
  • Identifiers are case-preserving. Customers and customers are different identifiers if a backend would treat them as such (we follow SQLite's case-insensitive identifier rules at the schema level but preserve the user's written casing in display). Concretely, a command may name a table in any capitalization (the engine resolves it case-insensitively); every executor canonicalizes a user-supplied table name to its stored case before touching metadata or CSV (canonical_table_name in db.rs), so the live schema, the metadata tables, and the data/<table>.csv files stay in step regardless of how the user capitalized the name. (Column names are matched case-sensitively and a wrong case is refused as "no such column" — strict, but never drifting.)
  • Whitespace is liberal. Any amount of horizontal whitespace between tokens is accepted, including around punctuation (,, :, (, )).

Consequences

  • The basic, most-common form of any command remains readable and free of cosmetic punctuation. New users see only words.
  • Optional adornments are visually distinct, encouraging discoverability of advanced features without forcing them on beginners.
  • New commands inherit a uniform shape: keyword-based clauses for required parts, -- flags for opt-ins. Drift is bounded by this rule.
  • The grammar implementation (chumsky) maps cleanly onto this structure: a with_clause rule can be reused across commands, and flag parsing has a single representation when it lands.