Skip to content

feat: gate schema changes behind an explicit migrate - #646

Merged
kartikeya-27 merged 10 commits into
masterfrom
feat/explicit-migrate
Aug 10, 2026
Merged

feat: gate schema changes behind an explicit migrate#646
kartikeya-27 merged 10 commits into
masterfrom
feat/explicit-migrate

Conversation

@pratyush618

@pratyush618 pratyush618 commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Opening a queue was the only way to migrate it, and opening always migrated. A deployment whose application credentials do not permit DDL — or where a DBA owns every schema change — had nowhere to stand: the first process to start applied the migrations whether or not anyone had approved them.

This splits the two. The default is unchanged.

Opening without applying

SqliteStorage::unmigrated and PostgresStorage::unmigrated connect and stop there; the migrating constructors now route through the same builder with the flag set. Each shell exposes it as an open option:

SDK option
Python Queue(auto_migrate=False)
Node new Queue({ autoMigrate: false })
Java Taskito.builder().autoMigrate(false)

taskito-server reads TASKITO_AUTO_MIGRATE.

The flag is threaded into the workflow store too. That store is built lazily on first workflow use and migrates in its constructor, so without this the first workflow call would quietly apply the DDL the operator withheld. It gained the same unmigrated / migrate pair, and WorkflowStorageBackend::migrate dispatches across backends.

Applying explicitly

StorageBackend::migrate() applies pending changes and returns a MigrationReport: the core versions applied, the workflow versions applied, the rows the one-time backlog sweep archived, and schemaless for a backend that
stores no schema and never will. To make that report possible, run_sqlite / run_postgres now return the versions they applied instead of unit.

The backlog sweep moved with it. archive_old_jobs(i64::MAX) used to run alongside the migrations, outside the ledger; it now lives inside migrate(), so a gated deployment cannot skip it, and the report counts what it moved.

Exposed as migrate() in all three SDKs, plus a migrate CLI command that opens unmigrated — the command is then the only thing in the system applying DDL:

taskito migrate

Idempotent: a current database applies nothing and says so. Redis reports "nothing to migrate" rather than failing.

Interaction with the contract floor

Reading the floor needs the settings table, which a gated open has not created. So a gated open skips the check and migrate() performs it once the schema exists. The server keeps its check unconditional — it is meant to run against an already-migrated database — but a missing schema now fails naming TASKITO_AUTO_MIGRATE instead of leaking a bare "no such table".

Verification

  • Rust: cargo test --workspace and --features workflows green; clippy clean including --all-targets --all-features; Postgres and Redis both compile.
  • Python 1425 passed, ruff + mypy clean. Node 702 passed, biome clean. Java ./gradlew build green. Docs lint + typecheck clean.
  • New tests: 2 core (an unmigrated open answers no queries then migrates and is idempotent; the sweep is reported), 3 Python, 2 Node, 2 Java, 1 server (a gated open against an empty database names the variable to fix).

Beyond the plan on purpose: workflow tables and the server. A migrate that leaves half the schema unapplied does not deliver the gate.

Summary by CodeRabbit

  • New Features

    • Added explicit schema migration APIs across Node.js, Python, and Java SDKs.
    • Added automatic or deferred migration controls during queue startup.
    • Added migration reports with applied schemas, archived jobs, and schemaless status.
    • Added migrate commands to the Node.js, Python, and Java CLIs.
    • Added deferred workflow migrations and migration-state checks for supported databases.
  • Documentation

    • Added deployment guidance for controlling and running schema migrations explicitly.
  • Bug Fixes

    • Improved handling and validation of queues opened before required schemas are available.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0364c5f2-c145-4277-acf0-ef001d387d17

📥 Commits

Reviewing files that changed from the base of the PR and between 70c6ac0 and 25d5bf2.

📒 Files selected for processing (9)
  • crates/taskito-node/src/queue/admin.rs
  • crates/taskito-python/src/py_queue/mod.rs
  • sdks/node/src/cli/commands/migrate.ts
  • sdks/node/src/queue.ts
  • sdks/node/test/core/migrate.test.ts
  • sdks/python/taskito/_taskito.pyi
  • sdks/python/taskito/app.py
  • sdks/python/taskito/detached.py
  • sdks/python/tests/core/test_migrate.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • ByteVeda/taskito (manual)
🚧 Files skipped from review as they are similar to previous changes (5)
  • sdks/python/taskito/app.py
  • sdks/python/taskito/_taskito.pyi
  • sdks/node/src/cli/commands/migrate.ts
  • sdks/node/test/core/migrate.test.ts
  • sdks/node/src/queue.ts

📝 Walkthrough

Walkthrough

The change adds automatic or deferred schema migration across core storage, workflow storage, the server, and the Java, Node, and Python SDKs. Explicit migrate() APIs and CLI commands return migration and archival results.

Changes

Migration flow

Layer / File(s) Summary
Migration contracts and storage runners
crates/taskito-core/..., crates/taskito-workflows/...
Migration runners return applied versions. SQLite, PostgreSQL, and workflow stores support unmigrated construction and explicit migration. Redis returns a schemaless report. Contract validation runs after migration.
Java migration integration
crates/taskito-java/..., sdks/java/...
Java storage opening supports autoMigrate. JNI, the Java API, and the CLI expose migration reports and explicit migration commands.
Node migration integration
crates/taskito-node/..., sdks/node/...
Node queues support autoMigrate and Queue.migrate(). The CLI adds a migrate command and tests gated, automatic, and idempotent migration.
Python migration integration
crates/taskito-python/..., sdks/python/taskito/..., sdks/python/tests/...
Python queues support deferred migration, delayed webhook loading, and migrate(). The CLI reports migration results and tests post-migration operation.
Server configuration and deployment operations
crates/taskito-server/..., docs/content/docs/shared/guides/operations/deployment.mdx
The server reads TASKITO_AUTO_MIGRATE, selects migrated or unmigrated backends, validates gated schemas, and documents explicit migration operations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant QueueSDK
  participant NativeBinding
  participant StorageBackend
  CLI->>QueueSDK: open with automatic migration disabled
  QueueSDK->>NativeBinding: migrate()
  NativeBinding->>StorageBackend: apply core and workflow migrations
  StorageBackend-->>NativeBinding: MigrationReport
  NativeBinding-->>CLI: migration report as JSON
Loading

Possibly related PRs

  • ByteVeda/taskito#430: The migration APIs extend the migration engine and storage APIs introduced by this PR.
  • ByteVeda/taskito#645: The migration flow extends contract-floor enforcement with explicit migration validation.
  • ByteVeda/taskito#319: The migration runners directly overlap with the migration DDL and legacy-table cleanup behavior changed here.

Suggested labels: workflows

Suggested reviewers: kartikeya-27

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: requiring explicit migration to apply schema changes while preserving the broader migration-gating objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/taskito-core/src/storage/mod.rs`:
- Around line 1303-1314: Update StorageBackend::migrate in
crates/taskito-core/src/storage/mod.rs:1303-1314 to call
ensure_contract_supported(self) after the selected backend migration succeeds
and before returning the MigrationReport. Update
crates/taskito-core/BINDING_CONTRACT.md:402-404 to document that
StorageBackend::migrate performs this deferred contract check and that gated
shells must validate an already-migrated schema before processing work.

In `@crates/taskito-java/src/queue/admin.rs`:
- Around line 479-480: Update the migration flow containing
queue.storage.migrate() and migrate_workflow_storage(queue) to acquire a
database-scoped migration lock before either operation and release it only after
both complete, covering the entire core and workflow migration sequence. Use the
database’s cross-process locking mechanism rather than the existing
workflow_init lock or an in-process Java mutex, while preserving error
propagation and cleanup.

In `@crates/taskito-node/src/queue/mod.rs`:
- Around line 49-57: Update the contract-floor enforcement around
ensure_contract_supported so it runs for existing schemas even when auto_migrate
is false, while skipping only when the migration ledger is absent. Preserve the
auto_migrate behavior and use a non-DDL readiness or ledger-presence check to
distinguish unmigrated storage from existing storage before validating the
contract floor.

In `@crates/taskito-server/src/config/backend.rs`:
- Around line 51-60: Update the backend startup validation around
ensure_contract_supported to also perform a non-DDL workflow schema readiness
check when auto_migrate is false, so gated startup rejects databases missing
workflow tables. Add coverage in crates/taskito-server/src/config/backend.rs
lines 173-198 for migrated core tables with absent workflow tables and assert
the gated open fails. In
docs/content/docs/shared/guides/operations/deployment.mdx lines 1748-1752,
retain the startup guarantee only with the workflow check in place, or narrow it
to core storage.

In `@sdks/python/taskito/cli.py`:
- Around line 457-460: Update run_migrate and the _load_queue construction path
so the application opens Queue with auto_migrate=False before calling
queue.migrate(). Use a CLI-safe application factory or pre-construction
override, ensuring importing the application cannot apply migrations implicitly
and the command’s report reflects the full explicit migration.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 76c84405-ae13-484b-b15d-559a4cab116a

📥 Commits

Reviewing files that changed from the base of the PR and between f5a0c0b and c14cca6.

📒 Files selected for processing (47)
  • crates/taskito-core/BINDING_CONTRACT.md
  • crates/taskito-core/src/storage/migrate.rs
  • crates/taskito-core/src/storage/mod.rs
  • crates/taskito-core/src/storage/postgres/mod.rs
  • crates/taskito-core/src/storage/redis_backend/mod.rs
  • crates/taskito-core/src/storage/sqlite/mod.rs
  • crates/taskito-core/src/storage/sqlite/tests.rs
  • crates/taskito-java/src/backend.rs
  • crates/taskito-java/src/convert.rs
  • crates/taskito-java/src/queue/admin.rs
  • crates/taskito-node/src/backend.rs
  • crates/taskito-node/src/config.rs
  • crates/taskito-node/src/queue/admin.rs
  • crates/taskito-node/src/queue/mod.rs
  • crates/taskito-node/src/queue/workflows.rs
  • crates/taskito-python/src/py_queue/mod.rs
  • crates/taskito-python/src/py_queue/workflow_ops/mod.rs
  • crates/taskito-python/src/py_queue/workflow_ops/test_helpers.rs
  • crates/taskito-server/src/config/backend.rs
  • crates/taskito-server/src/config/mod.rs
  • crates/taskito-server/src/runtime/mod.rs
  • crates/taskito-server/tests/backends.rs
  • crates/taskito-workflows/src/lib.rs
  • crates/taskito-workflows/src/postgres_store.rs
  • crates/taskito-workflows/src/sqlite_store.rs
  • docs/content/docs/shared/guides/operations/deployment.mdx
  • sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java
  • sdks/java/src/main/java/org/byteveda/taskito/Taskito.java
  • sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/JniQueueBackend.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/NativeQueue.java
  • sdks/java/src/main/java/org/byteveda/taskito/model/MigrationReport.java
  • sdks/java/src/main/java/org/byteveda/taskito/spi/QueueBackend.java
  • sdks/java/src/test/java/org/byteveda/taskito/MigrateTest.java
  • sdks/node/src/cli/commands/index.ts
  • sdks/node/src/cli/commands/migrate.ts
  • sdks/node/src/cli/connect.ts
  • sdks/node/src/cli/index.ts
  • sdks/node/src/native.ts
  • sdks/node/src/queue.ts
  • sdks/node/test/core/migrate.test.ts
  • sdks/python/taskito/_taskito.pyi
  • sdks/python/taskito/app.py
  • sdks/python/taskito/cli.py
  • sdks/python/taskito/mixins/settings.py
  • sdks/python/taskito/webhooks.py
  • sdks/python/tests/core/test_migrate.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • ByteVeda/taskito (manual)

Comment thread crates/taskito-core/src/storage/mod.rs
Comment thread crates/taskito-java/src/queue/admin.rs
Comment thread crates/taskito-node/src/queue/mod.rs
Comment thread crates/taskito-server/src/config/backend.rs Outdated
Comment thread sdks/python/taskito/cli.py
@pratyush618
pratyush618 force-pushed the feat/explicit-migrate branch from c14cca6 to 70c6ac0 Compare August 10, 2026 17:07

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/taskito-node/src/queue/admin.rs`:
- Around line 273-295: Convert the Node-facing QueueAdmin::migrate method and
its migration flow to asynchronous execution, running storage migration and
migrate_workflow_storage inside spawn_blocking so database and archive work
leave the event loop; preserve both workflow and non-workflows configurations,
including a compatible helper signature. Update the queue TypeScript API,
migrate CLI command, and migration tests to await the returned Promise.

In `@crates/taskito-python/src/py_queue/mod.rs`:
- Around line 705-724: Update migrate to call
ensure_contract_supported(&self.storage) immediately after the core
self.storage.migrate() succeeds and before
workflow_ops::migrate_workflow_storage, propagating any validation error through
PyResult. Add a regression test covering storage that skipped construction-time
validation and then explicitly migrates, verifying the contract floor is
enforced.

In `@sdks/python/taskito/app.py`:
- Around line 366-369: Update PyQueue initialization and WebhookManager setup at
sdks/python/taskito/app.py:366-369 to use actual schema readiness, preloading
webhooks for storage migrated by a prior process and deferring only
never-migrated schema-backed storage; do not use auto_migrate as the readiness
signal. At sdks/python/taskito/app.py:242-247, restrict unavailable-table
warnings to never-migrated SQLite/PostgreSQL storage and document that
schemaless Redis remains usable. Add coverage for a fresh gated runtime opened
after a separate process migrates the database.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9fc5a08e-e2a2-488f-8322-3c179e9e96b8

📥 Commits

Reviewing files that changed from the base of the PR and between c14cca6 and 70c6ac0.

📒 Files selected for processing (18)
  • crates/taskito-core/BINDING_CONTRACT.md
  • crates/taskito-core/src/contract.rs
  • crates/taskito-core/src/storage/mod.rs
  • crates/taskito-core/src/storage/postgres/mod.rs
  • crates/taskito-core/src/storage/redis_backend/mod.rs
  • crates/taskito-core/src/storage/sqlite/mod.rs
  • crates/taskito-core/src/storage/sqlite/tests.rs
  • crates/taskito-java/src/backend.rs
  • crates/taskito-java/src/queue/admin.rs
  • crates/taskito-node/src/queue/admin.rs
  • crates/taskito-node/src/queue/mod.rs
  • crates/taskito-python/src/py_queue/mod.rs
  • crates/taskito-server/src/config/backend.rs
  • docs/content/docs/shared/guides/operations/deployment.mdx
  • sdks/python/taskito/app.py
  • sdks/python/taskito/cli.py
  • sdks/python/taskito/migration_gate.py
  • sdks/python/tests/core/test_contract_floor.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • ByteVeda/taskito (manual)
🚧 Files skipped from review as they are similar to previous changes (9)
  • crates/taskito-core/BINDING_CONTRACT.md
  • crates/taskito-core/src/storage/redis_backend/mod.rs
  • docs/content/docs/shared/guides/operations/deployment.mdx
  • crates/taskito-node/src/queue/mod.rs
  • crates/taskito-java/src/queue/admin.rs
  • sdks/python/taskito/cli.py
  • crates/taskito-server/src/config/backend.rs
  • crates/taskito-core/src/storage/sqlite/tests.rs
  • crates/taskito-java/src/backend.rs

Comment thread crates/taskito-node/src/queue/admin.rs Outdated
Comment thread crates/taskito-python/src/py_queue/mod.rs
Comment thread sdks/python/taskito/app.py Outdated
An explicit migrate has to say what it did; the runners returned unit, so the caller could only report success.
Opening was the only way to migrate, so a deployment whose credentials forbid runtime DDL had nowhere to stand. The unmigrated constructors and migrate() split the two, sweep included.
Each shell gains an auto-migrate open option, threaded into the workflow store too — otherwise the first workflow call applies the DDL the operator withheld. A gated open skips the contract-floor read, which needs the schema.
The gated open needs a way to apply the schema: a migrate() reporting what ran, and a CLI command that opens unmigrated so it is the only thing applying DDL.
Skipping it whenever migrations were gated let a build too old for the deployment join it — the schema is usually already there, so the floor is readable. Only a never-migrated storage is exempt, and migrate() covers that case.
The Python command reaches its queue by importing the application, which built a migrating queue — so the import applied the schema and the command reported work it had not done.
A fresh database means the whole schema plus the backlog sweep, which is unbounded work for a synchronous binding to hold the loop for.
Gating migrations does not mean the schema is missing: the normal flow migrates in one process and starts the application in another, still gated, against a fully migrated database — whose webhook snapshot then never loaded.
@pratyush618
pratyush618 force-pushed the feat/explicit-migrate branch from 70c6ac0 to 25d5bf2 Compare August 10, 2026 17:33
@kartikeya-27
kartikeya-27 merged commit 705ff60 into master Aug 10, 2026
38 checks passed
@pratyush618
pratyush618 deleted the feat/explicit-migrate branch August 10, 2026 18:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants