Skip to content

dasSQLITE chunk 14b: typed ALTER macros + module rename#2592

Merged
borisbat merged 2 commits intomasterfrom
dassqlite-chunk-14b
May 6, 2026
Merged

dasSQLITE chunk 14b: typed ALTER macros + module rename#2592
borisbat merged 2 commits intomasterfrom
dassqlite-chunk-14b

Conversation

@borisbat
Copy link
Copy Markdown
Collaborator

@borisbat borisbat commented May 6, 2026

Two commits, in order:

1. dasSQLITE: rename daslib/sql_migrate -> daslib/sqlite_migrate

Migrations as currently designed are SQLite-specific (the __schema_version audit table, BEGIN IMMEDIATE, unixepoch(), busy-timeout-based concurrent-runner contract). Sitting under the daslib/sql_* namespace would lie about provider neutrality. When the dasSQL abstraction lands, an abstract migration runner naturally takes the daslib/sql_migrate name; this provider-specific piece moves to daslib/sqlite_migrate. Doing the rename now keeps the public surface honest and avoids a churn migration when the abstraction lands.

Mechanical search-replace across 40 sites: file move + module sql_migrate -> module sqlite_migrate, internal qmacro sql_migrate::_add_migration_entry -> sqlite_migrate::, internal init thunk register'sql'migrations -> register'sqlite'migrations, all 23 test-side require sqlite/sql_migrate -> require sqlite/sqlite_migrate, mockup daslib/sql_migrate -> daslib/sqlite_migrate, prose references in API_REWORK / API_MIGRATION / API_MISSING / TUTORIALS / tutorials/sql/{39,41,42}-*.das + matching RST files, modules/dasSQLITE/.das_module sqlite_paths array, tests/aot/CMakeLists.txt AOT module list.

The annotation [sql_migration] is unchanged (SQL-level conceptual noun; an abstract migration layer landing later could reuse it).

2. dasSQLITE chunk 14b: typed ALTER macros

Adds the typed-ALTER ergonomics layer designed in API_MIGRATION.md Scenario 2 on top of the 14a migrations spine. New macros under daslib/sqlite_boost (transitively visible via daslib/sqlite_migrate's existing public re-export):

db |> add_column(type<T>, "Field" [, defaultLit])
db |> create_index(type<T>, "Field" | ("A","B") [, "name"])
db |> create_unique_index(type<T>, ... same shape ...)
db |> drop_index_if_exists("name")

What the macros buy:

  • compile-time field-name + struct-shape checks (no more "user's DB panics on startup" surprises),
  • type-derived NOT NULL: macro refuses to ADD a non-nullable column without a literal default; Option<T> wrapping = nullable,
  • @sql_column rename + @sql_json / @sql_blob storage are honored, same conventions [sql_table] / [sql_index] already use,
  • compile-time rejection for shapes that need a table rebuild (PK / UNIQUE inline / generated columns / @sql_default_fn).

Field selectors are string literals, not the .Field syntax used inside _sql {...} blocks — gen2's parser only synthesizes _ as a placeholder inside that block scope. Plain call sites pass explicit string field names, same convention [sql_index(fields="A")] already uses.

create_unique_index is a separate macro rather than a unique=true named arg because gen2's named-arg form [name=val] produces an ExprNamedCall that [call_macro] doesn't intercept; positional separation is cleaner.

Implementation notes:

  • AddColumnMacro emits qmacro(_::exec($e(db), build_string $(w) { ... })) writing prefix + sql_storage_type_for(type<T>) + suffix — runtime SQL-type derivation via the existing _::sql_bind adapter rail.
  • CreateIndex(Unique)Macro: pure macro-time DDL build via derive_index_name (extracted from SqlIndexMacro for reuse).
  • drop_index_if_exists: 5-line runtime function, IF EXISTS native.
  • is_option_field_type relaxed: now matches both the typeMacro pre-instantiation form (what [sql_table] sees during structure_macro apply()) AND the post-template-instantiation tStructure form (what call_macros see at infer time). The latter has _module=<callee> (downstream of the template instantiation site), so module-name matching was unsound — switched to "Option<...>" name-shape match.

Tests

16 new under tests/dasSQLITE/:

  • 8 positives (migrate_14_* through migrate_21_*): Option<T> -> nullable; int + default=0 -> NOT NULL DEFAULT + backfill; embedded single-quote escaping in DEFAULT; @sql_column rename propagation; @sql_json -> TEXT; @sql_blob struct -> BLOB; auto-named single-col index; UNIQUE composite + named index + drop_index_if_exists idempotence.
  • 1 runtime-error positive (migrate_22_add_column_dup_runtime): re-adding the same column at runtime panics inside the migration; α-shape transaction rolls back the whole call (audit table empty, table doesn't exist).
  • 7 failed_* compile-error negatives: unknown field, no [sql_table], PK rejected, UNIQUE rejected, non-literal default, unknown index field, empty fields list.

tests/dasSQLITE: 748/748 pass interpreted and AOT, no GC leaks, lint clean, Sphinx clean (no warnings or errors).

Tutorial

tutorials/sql/43-migrations.das + doc/source/reference/tutorials/sql_43_migrations.rst refreshed:

  • migrations 002 / 003 rewritten to use the typed forms (raw-SQL equivalent kept as a sidebar comment),
  • new "Typed ALTER: when daslang has enough info to validate" section between Inspection and Adoption,
  • migrations 004 / 005 added showing create_index + idempotent rebuild via drop_index_if_exists.

API_MIGRATION.md, API_REWORK.md, TUTORIALS.md updated to mark 14b as shipped. Chunk 14c (struct rebuild) remains pending.

🤖 Generated with Claude Code

borisbat and others added 2 commits May 6, 2026 10:02
Migrations as designed are SQLite-specific (the __schema_version audit
table, BEGIN IMMEDIATE, unixepoch(), busy-timeout-based concurrency).
Sitting under the daslib/sql_* namespace would lie about provider
neutrality. When the eventual dasSQL abstraction lands, an abstract
migration runner would naturally take the daslib/sql_migrate name; this
provider-specific piece moves to daslib/sqlite_migrate. Doing the
rename now keeps the public surface honest and avoids a churn migration
when the abstraction lands.

Mechanical search-replace across 40 sites:
- file move + 'module sql_migrate' -> 'module sqlite_migrate'
- internal qmacro 'sql_migrate::_add_migration_entry' -> 'sqlite_migrate::'
- internal init thunk register'sql'migrations -> register'sqlite'migrations
- 'require sqlite/sql_migrate' (23 tests + tutorial 43 + skills/sql.md
  + sql_43_migrations.rst) -> 'require sqlite/sqlite_migrate'
- 'require daslib/sql_migrate' (mockups + design docs) -> 'daslib/sqlite_migrate'
- prose references in API_REWORK / API_MIGRATION / API_MISSING /
  TUTORIALS / tutorials/sql/{39,41,42}-*.das + matching RST files
- modules/dasSQLITE/.das_module sqlite_paths array
- tests/aot/CMakeLists.txt AOT module list

Annotation [sql_migration] is unchanged: SQL-level conceptual noun, an
abstract migration layer landing later could reuse it.

dastest tests/dasSQLITE: 728/728 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the typed-ALTER ergonomics layer designed in API_MIGRATION.md
Scenario 2 on top of the 14a migrations spine. New macros under
daslib/sqlite_boost (transitively visible via daslib/sqlite_migrate's
existing public re-export):

  db |> add_column(type<T>, "Field" [, defaultLit])
  db |> create_index(type<T>, "Field" or ("A","B") [, "name"])
  db |> create_unique_index(type<T>, ...same shape...)
  db |> drop_index_if_exists("name")

What the macros buy:
- compile-time field-name + struct-shape checks (no more "user's DB
  panics on startup" surprises),
- type-derived NOT NULL: macro refuses to ADD a non-nullable column
  without a literal default; Option<T> wrapping = nullable,
- @sql_column rename + @sql_json / @sql_blob storage are honored,
  same conventions sql_table / sql_index already use,
- compile-time rejection for shapes that need a table rebuild
  (PK / UNIQUE inline / generated / sql_default_fn).

Field selectors are string literals, not the .Field syntax used
inside _sql blocks: gen2's parser only synthesizes _ as a placeholder
inside that block scope. Plain call sites pass explicit string field
names -- same convention sql_index(fields="A") already uses.

create_unique_index is a separate macro rather than a unique=true
named arg because gen2's named-arg form [name=val] produces an
ExprNamedCall that call_macro does not intercept; positional
separation is cleaner and fully discoverable.

Implementation notes:
- AddColumnMacro emits qmacro(_::exec(db, build_string)) with a body
  that writes prefix + sql_storage_type_for(type<T>) + suffix --
  runtime SQL-type derivation via the existing _::sql_bind adapter
  rail.
- CreateIndex(Unique)Macro: pure macro-time DDL build via
  derive_index_name (extracted from SqlIndexMacro for reuse).
- drop_index_if_exists: 5-line runtime function, IF EXISTS native.
- is_option_field_type relaxed: now matches both the typeMacro
  pre-instantiation form (what sql_table sees during structure_macro
  apply) AND the post-template-instantiation tStructure form (what
  call_macros see at infer time). The latter has _module pointing to
  the callee (downstream of the template instantiation site), so
  module-name matching was unsound -- switch to Option<...> name
  shape match.

Tests (16 new, AOT-clean): 8 positives + 1 runtime-error (dup column
runtime panic rolls back via alpha-shape) + 7 failed_* compile-error
negatives. tests/dasSQLITE: 748/748 pass, no GC leaks.

Tutorial 43 + RST refreshed:
- migrations 002 / 003 rewritten to use the typed forms (raw-SQL
  equivalent kept as a sidebar comment),
- new "Typed ALTER" section between Inspection and Adoption,
- migrations 004 / 005 added showing index + idempotent rebuild.

API_MIGRATION.md, API_REWORK.md, TUTORIALS.md updated to mark 14b as
shipped. 14c (struct rebuild) remains pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 6, 2026 17:10
@borisbat borisbat merged commit 7461d29 into master May 6, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant