Skip to content

Keep the data through a table rebuild, and leave a table that cannot be rebuilt as it was [all-db] - #516

Merged
jeff377 merged 4 commits into
mainfrom
claude/rebuild-keeps-data
Nov 15, 2026
Merged

jeff377 merged 4 commits into
mainfrom
claude/rebuild-keeps-data

Conversation

@jeff377

@jeff377 jeff377 commented Nov 15, 2026

Copy link
Copy Markdown
Contributor

Closes #513

Principles

This pull request makes the rebuild follow the table upgrade principles. They are now stated in the schema upgrade guide and in .claude/rules/database.md:

  1. Keep the existing data.
  2. The current TableSchema is the target.
  3. One upgrade is enough.
  4. When the existing data blocks the upgrade, stop and report, and leave the decision to a person.

What changes

The rebuild keeps AutoIncrement numbers on every database

SQL Server, PostgreSQL, MySQL and Oracle left AutoIncrement columns out of the copy, so the rebuilt table numbered its rows again. Every dialect now copies the values. The table below shows how each one continues the sequence afterwards:

Database Copy Next number
SQLite direct sqlite_sequence carried over (from #514)
SQL Server SET IDENTITY_INSERT reseed past the old table's sys.identity_columns.last_value through dynamic DBCC CHECKIDENT
PostgreSQL direct (GENERATED BY DEFAULT) setval past the old sequence's last value
Oracle direct (GENERATED BY DEFAULT) identity restarted after the old sequence's LAST_NUMBER; a few cached numbers may be skipped
MySQL direct counter continues after the highest copied value. A reseed would need a user variable, which the connection does not allow, so a number freed at the end of the table can come back.

A plain column that becomes AutoIncrement is not copied. Its rows are numbered afresh, as before, since old values (often all 0) cannot serve as identity values.

A rebuild that would lose part of the table is refused before anything runs

Two cases are refused at plan time:

  • An index only the database has. The providers read back its columns but not its filter, included columns, prefix lengths or kind. Recreating it could change what a unique index enforces, or fail after the old table is gone (MySQL and Oracle commit DDL as they go).
  • A column of a type the framework does not map. The rebuild cannot recreate it.

In both cases the error names the index or column and the table is left as it was. A person then declares them in the definition or drops them, and upgrades again (RebuildSchemaFactory.ThrowIfRebuildWouldLoseParts).

Documentation

database-schema-upgrade.md (en, zh-TW) gains two sections:

  • Upgrade principles.
  • When the existing data blocks an upgrade. It covers the state the table is left in (including a tmp_ table on MySQL and Oracle), where the error is recorded (db upgrade's per-table line and exit code, TableUpgradeReport, the exception of a single-table upgrade), and the steps to resolve it.

Tests

RebuildPreservationDbTests runs on every database. It checks:

  • A rebuild keeps the rows and their numbers, and the next row does not take a number a delete freed (MySQL: it continues after the highest).
  • A rebuild of a table with a database-only index is refused, and the table, its index and its rows are unchanged.
  • A rebuild whose copy fails on the existing data leaves the table and rows unchanged, and the next attempt fails the same way.
  • On SQLite, a column of an unmapped type is refused.

New unit tests cover the plain-to-AutoIncrement copy and the index refusal.

Review

An adversarial review of the first commit found the problems with recreating database-only indexes:

  • filtered or INCLUDE unique indexes changing meaning;
  • prefix or FULLTEXT indexes failing after the drop on MySQL and Oracle;
  • SQLite sqlite_autoindex_* names.

It also found the copy of a plain column into a new AutoIncrement. The third commit replaces the recreation with the refusal and fixes the copy.

Verification

Run locally:

  • dotnet build Polhem.slnx -c Release is clean.
  • Polhem.Db.UnitTests passes on SQL Server, PostgreSQL, MySQL, Oracle and SQLite (1677 tests), after the rebase onto Gate the table upgrade on idempotency and fix the loops it finds [all-db] #515's merge.
  • The full ./test.sh before the rebase failed only:
    • Generate_iOS_Builds, because the local Xcode does not match the iOS workload;
    • one Blazor browser test, which passed when run on its own.

…be rebuilt as it was [all-db]

A rebuild is meant to keep what the table holds. Three things contradicted
that:

- SQL Server, PostgreSQL, MySQL and Oracle left AutoIncrement columns out
  of the copy, so the rebuilt table numbered its rows again. Every dialect
  now copies them: SQL Server with IDENTITY_INSERT and a reseed past the old
  last value, PostgreSQL with setval past the old sequence, Oracle by
  restarting the identity after it. MySQL keeps the values and its counter
  carries on after the highest one, since a reseed would need a user
  variable the connection does not allow.
- Indexes only the database had were dropped with the old table. They are
  now created again on the rebuilt table once it has its final name
  (RebuildSchemaFactory.GetDatabaseOnlyIndexes).
- A column of a type the framework does not map made every rebuild of its
  table throw part way. The plan now refuses such a rebuild before any
  statement runs, naming the column, and the table stays as it was.

RebuildPreservationDbTests checks on every database that a rebuild keeps
the rows, their numbers and a database-only index, and that the next row
does not take a number a delete freed; that a rebuild whose copy fails on
the existing data leaves the table and its rows unchanged, twice; and, on
SQLite, the refusal for an unmapped column.

.claude/rules/database.md now states the table upgrade principles: keep
the data, upgrade to the current TableSchema, never need a second upgrade,
and stop and report when the data blocks the upgrade.

Closes #513
…ks an upgrade

The schema upgrade guide said what an upgrade does and how a failing ALTER
stage rolls back, but not what an upgrade promises about the data, nor
where a failure is recorded. It now has:

- a section on the upgrade principles: keep the existing data, upgrade to
  the current TableSchema, need no second upgrade, and stop and report when
  the data blocks the upgrade;
- a section on a failed upgrade: the table stays as it was (including the
  temporary table MySQL and Oracle may leave), a column the rebuild cannot
  recreate is refused up front, and where the error is recorded (db
  upgrade's per-table line and exit code, TableUpgradeReport, or the
  exception of a single-table upgrade), with the steps to resolve it.

The rebuild section points to it instead of repeating it. The zh-TW
translation follows.
…new AutoIncrement afresh

Review of the previous commit:

- Recreating an index only the database has was not faithful. The
  providers read back its columns but not its filter, included columns,
  prefix lengths or kind, so a unique index could come back enforcing more
  or less than before, and a recreate that failed ran after the old table
  was dropped (MySQL, Oracle), leaving a half-done upgrade. A SQLite UNIQUE
  column constraint read back as a reserved sqlite_autoindex name that could
  not be created at all. The plan now refuses such a rebuild before anything
  runs, naming the indexes, so a person declares them in the definition or
  drops them (RebuildSchemaFactory.ThrowIfRebuildWouldLoseParts).
- A plain column that becomes AutoIncrement copied its old values, often
  all 0, into an identity column. Such a column is now left out of the copy
  and numbered afresh, as before (RebuildSchemaFactory.GetUncopiedFieldNames).

RebuildPreservationDbTests checks the refusal on every database, with the
table, its index and its rows left as they were. The guide and the rule
describe both.
@jeff377
jeff377 enabled auto-merge November 15, 2026 05:10
SonarCloud S3267 on the pull request: the loops in
DdlFragments.BuildInsertSelectStatement and
RebuildSchemaFactory.GetUncopiedFieldNames only filter and map, so they
read as Where/Select. No behaviour changes.
@sonarqubecloud

Copy link
Copy Markdown

@jeff377
jeff377 added this pull request to the merge queue Nov 15, 2026
Merged via the queue into main with commit 3a8df1a Nov 15, 2026
5 checks passed
@jeff377
jeff377 deleted the claude/rebuild-keeps-data branch November 15, 2026 05:48
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.

Keep AutoIncrement values, database-only indexes and unknown column types through a table rebuild

1 participant