Skip to content

feat(migration): add the migration runner and schema differ - #949

Open
abnegate wants to merge 1 commit into
feat-query-libfrom
feat-migration-runner
Open

feat(migration): add the migration runner and schema differ#949
abnegate wants to merge 1 commit into
feat-query-libfrom
feat-migration-runner

Conversation

@abnegate

@abnegate abnegate commented Aug 27, 2026

Copy link
Copy Markdown
Member

Split out of #823, which had carried these along with the query-lib migration.

Migration gives versioned up/down migrations with a Runner, an online schema change path, and a Generator that scaffolds a migration file. Schema gives the introspector and the Diff that reports what changed between a declared collection and the one the engine holds.

The two are a closed loop: Schema exists to serve Migration\Generator, and bin/tasks/migrate.php is the only entry point, registered from bin/cli.php.

Why separately

The migration is a forced move; a migration runner is a capability we chose, and it arrived on the same branch.

Why it targets feat-query-lib

Written against the Attribute, Collection and Index value objects #823 introduces. Retarget to main once that lands.

One fix worth noting

loadMigrations() discovers migration classes by what the file declares rather than by its filename. migrate:generate writes the class under a namespace (App\Migration by default), so a filename lookup found nothing — silently, leaving the run reporting success having skipped the migration. Two tests in CLITasksTest cover it, and they have to run after the test that includes bin/tasks/migrate.php, since a second include redeclares the function.

Introspector

This PR owns src/Database/Schema/Introspector.php, in the form it has after the ORM split: introspectCollection() and introspectDatabase(), which read schema and need nothing from the entity mapper. generateEntityClass() and the four helpers only it used went with the mapper to #947 — and are currently in neither branch, since #947 is based on #823 where this file does not exist. Whichever of the two lands second can put the codegen back.

Chain

Landing order, bottom up:

  1. utopia-php/database#823 — the query-lib migration itself
  2. utopia-php/abuse#124, utopia-php/audit#133, utopia-php/migration#222 — the schema call sites in the libraries
  3. appwrite/appwrite#11649
  4. appwrite-labs/cloud#5410

Stacked on #823 but not part of it, and not required by anything above: #947 (ORM), #948 (repositories and seeding), #949 (migration runner and schema differ).

Every dev-feat-query-lib pin in this train is re-pinned to its branch head whenever one of them moves, so each PR's CI runs against what the others actually contain.

Not verified

Nothing outside this library consumes it. Neither appwrite nor cloud references Utopia\Database\Migration or Utopia\Database\Schema.

CI is not being driven to green on this PR. It is parked behind #823 and will be rebased and fixed once that lands.

Splits out of the query-lib migration, which had carried these along with it.

Migration gives versioned up/down migrations with a Runner, an online schema
change path and a Generator that scaffolds a migration file. Schema gives the
introspector and the Diff that reports what changed between a declared
collection and the one the engine holds. The two are a closed loop: Schema
serves Migration\Generator, and bin/tasks/migrate.php is the only entry
point, registered from bin/cli.php.

Stacked on feat-query-lib rather than main: both are written against the
Attribute, Collection and Index value objects that migration introduces.

loadMigrations() discovers migration classes by what the file declares rather
than by its filename, because migrate:generate writes the class under a
namespace and a filename lookup silently found nothing -- the run reported
success having skipped the migration. Two tests in CLITasksTest cover that,
and they must run after the test that includes bin/tasks/migrate.php, since a
second include redeclares the function.

Nothing outside this library consumes it. Neither appwrite nor cloud
references Utopia\Database\Migration or Utopia\Database\Schema.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • main
  • 0.69.x

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b09c4ca7-0d3a-4516-a255-50d71fd67845

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a versioned migration framework, CLI migration commands, online schema-change helpers, schema introspection and diffing, plus tests for the new flows.

  • Registers migrate, rollback, status, fresh, and generation commands
  • Adds migration execution and tracking with transactional up/down operations
  • Adds schema comparison and migration source generation
  • Adds expand-contract and alter-lock strategies

Confidence Score: 2/5

The PR should not merge until fresh migrations reliably remove every collection and surface deletion failures, and generated migrations preserve attribute modifications.

The fresh path can leave collections or partial schema behind while reporting completion, and the generator turns a supported attribute modification into a successful no-op migration.

Files Needing Attention: src/Database/Migration/Runner.php, src/Database/Migration/Generator.php

Important Files Changed

Filename Overview
src/Database/Migration/Runner.php Implements migration execution, rollback, status, and fresh resets; fresh is incomplete for paginated collection lists and suppresses deletion failures.
src/Database/Migration/Generator.php Generates migration PHP from schema changes but silently omits supported ModifyAttribute changes.
bin/tasks/migrate.php Registers migration commands and discovers namespaced migration classes; command success reporting inherits Runner::fresh's incomplete-reset behavior.
src/Database/Migration/Tracker.php Creates and queries the migration metadata collection and records version/batch state.
src/Database/Schema/Diff.php Detects attribute and index additions, removals, and modifications between collection definitions.
src/Database/Schema/DiffResult.php Applies supported diff operations directly and categorizes additions, removals, and modifications.
src/Database/Migration/Strategy/ExpandContract.php Adds batched cursor-based data transformation and attribute expansion/contract operations.
src/Database/Migration/Strategy/OnlineSchemaChange.php Temporarily disables alter locks and reliably restores the adapter's previous lock state.

Fix all with Greploop Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
src/Database/Migration/Runner.php:112
**Fresh truncates collection list**

If the database contains more than 25 collections, `listCollections()` returns only its default-sized first page, so `fresh()` leaves later collections and their data intact while the command reports completion.

### Issue 2
src/Database/Migration/Runner.php:117-120
**Fresh suppresses deletion failures**

When `deleteCollection()` throws or returns false, `fresh()` ignores the failure and proceeds to reset migration tracking and rerun migrations, leaving retained or partially deleted schema while the CLI can still report successful completion.

### Issue 3
src/Database/Migration/Generator.php:108-122
**Generator drops attribute modifications**

When a diff contains `ModifyAttribute`, both statement generators fall through to `null`, producing a no-op migration that can be marked applied while the stored attribute remains unchanged.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(migration): add the migration runne..." | Re-trigger Greptile

*/
public function fresh(array $migrations): int
{
$collections = $this->db->listCollections();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fresh truncates collection list

If the database contains more than 25 collections, listCollections() returns only its default-sized first page, so fresh() leaves later collections and their data intact while the command reports completion.

Knowledge Base Used: Collection schema management

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Database/Migration/Runner.php
Line: 112

Comment:
**Fresh truncates collection list**

If the database contains more than 25 collections, `listCollections()` returns only its default-sized first page, so `fresh()` leaves later collections and their data intact while the command reports completion.

**Knowledge Base Used:** [Collection schema management](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/utopia-php/database/-/docs/collection-schema-management.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Comment on lines +117 to +120
try {
$this->db->deleteCollection($id);
} catch (\Throwable) {
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Fresh suppresses deletion failures

When deleteCollection() throws or returns false, fresh() ignores the failure and proceeds to reset migration tracking and rerun migrations, leaving retained or partially deleted schema while the CLI can still report successful completion.

Knowledge Base Used: Collection schema management

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Database/Migration/Runner.php
Line: 117-120

Comment:
**Fresh suppresses deletion failures**

When `deleteCollection()` throws or returns false, `fresh()` ignores the failure and proceeds to reset migration tracking and rerun migrations, leaving retained or partially deleted schema while the CLI can still report successful completion.

**Knowledge Base Used:** [Collection schema management](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/utopia-php/database/-/docs/collection-schema-management.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Comment on lines +108 to +122
return match ($change->type) {
ChangeType::AddAttribute => $change->attribute !== null
? "\$db->createAttribute('{$collectionId}', new \\Utopia\\Database\\Attribute(key: '{$change->attribute->key}', type: \\Utopia\\Query\\Schema\\ColumnType::" . \ucfirst($change->attribute->type->value) . ", size: {$change->attribute->size}));"
: null,
ChangeType::DropAttribute => $change->attribute !== null
? "\$db->deleteAttribute('{$collectionId}', '{$change->attribute->key}');"
: null,
ChangeType::AddIndex => $change->index !== null
? "\$db->createIndex('{$collectionId}', new \\Utopia\\Database\\Index(key: '{$change->index->key}', type: \\Utopia\\Query\\Schema\\IndexType::" . \ucfirst($change->index->type->value) . ", attributes: " . \var_export($change->index->attributes, true) . '));'
: null,
ChangeType::DropIndex => $change->index !== null
? "\$db->deleteIndex('{$collectionId}', '{$change->index->key}');"
: null,
default => null,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Generator drops attribute modifications

When a diff contains ModifyAttribute, both statement generators fall through to null, producing a no-op migration that can be marked applied while the stored attribute remains unchanged.

Knowledge Base Used: Collection schema management

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Database/Migration/Generator.php
Line: 108-122

Comment:
**Generator drops attribute modifications**

When a diff contains `ModifyAttribute`, both statement generators fall through to `null`, producing a no-op migration that can be marked applied while the stored attribute remains unchanged.

**Knowledge Base Used:** [Collection schema management](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/utopia-php/database/-/docs/collection-schema-management.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

@abnegate abnegate mentioned this pull request Aug 27, 2026
abnegate added a commit that referenced this pull request Aug 27, 2026
Re-adds the entity mapper that came in with the query-lib migration and was
split back out of it, rebased onto the migration's current head.

Introspector::generateEntityClass() does not come back with it. That method
emits the mapping attributes as text, so it belongs to the mapper, but it
lives in Schema/Introspector.php, which moved to the migration-runner change
(#949). This branch is based on the query-lib migration, where that file does
not exist. Whoever lands both can put the codegen back on top; nothing in the
mapper calls it, and no caller in this library or downstream references it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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