Migration-tool decision: Goose vs ydb-platform/golang-migrate
Short conclusion: for the MVP, keep upstream Goose, but use it through our Go migration binary rather than as an uncontrolled CLI invocation in multiple replicas. ydb-platform/golang-migrate should remain a fallback/spike option, but should not become the primary production dependency now.
Correction to the previous research
The statement “Goose has no YDB driver” was incorrect. YDB officially documents Goose support, and upstream Goose explicitly includes the ydb driver:
- official YDB Goose integration;
- upstream Goose: YDB in the list of drivers;
- support has been present since Goose 3.16; the foundation pins 3.27.1.
Comparison
| Criterion | Upstream Goose | ydb-platform/golang-migrate |
|---|---|---|
| YDB support | Officially documented by YDB; uses the YDB database/sql driver and scripting mode |
Its own YDB driver inside the fork |
| Release/pinning | Versioned upstream releases | The fork has no published releases; we would have to pin a commit |
| Dependency shape | Regular Go dependency / ready-made CLI | The fork retains the module path github.com/golang-migrate/migrate/v4, so it would require replacing the entire upstream module or maintaining our own fork build |
| Migration history | Stores the history of applied versions | Stores one current version plus a dirty record |
| Concurrency | No YDB migration lock by default; requires our own SessionLocker/single-flight deployment |
Includes schema_lock for YDB and a serializable lock transaction |
| Failure model | YDB documentation explicitly warns that a fake transaction in scripting mode may leave a migration step in an intermediate state | The dirty flag provides better visibility into an interrupted migration, but DDL still does not become transactional |
| Authentication | We can open the YDB SDK connection inside our Go binary using environment/metadata credentials | The DSN path supports anonymous/static/access-token credentials; metadata/service-account authentication is convenient only through WithInstance and our wrapper |
| Maintenance | Active upstream | YDB fork: head dated 2026-01-13, while upstream has already moved forward; we would have to maintain the YDB-specific code together with the entire fork |
Sources for the fork: YDB driver, module declaration, releases — none published.
Important limitation of YDB itself
The tool choice does not change a fundamental YDB property: schema transactions are not supported. DDL executes outside a transaction; in multi-statement DDL, earlier statements are not rolled back when a later statement fails. This is documented in YDB query execution and YDB transactions.
Therefore, safety must come from our protocol, not from the name of the migration tool.
Decision for #5
- Add
cmd/schema-migratein Go with embeddedmigrations/ydb/*.sqland the upstream Goose library. - Open the YDB connection through SDK/
database/sqlwith environment/metadata credentials; do not pass an IAM token through argv/DSN. - Add a YDB-backed single-flight lock through Goose
SessionLockeror a separate lease table. Two migration runners must produce exactly one winner. - Use one idempotent schema operation per recoverable migration step; large data backfills must be separate resumable jobs.
- Use expand → migrate/backfill → contract; prohibit destructive down/reset operations in production.
- Prohibit changes to already-applied files. Add checksum/drift validation because the standard version history is insufficient to detect rewritten SQL.
- Test crash recovery: failure after DDL but before recording the version; repeated
up; manual repair/runbook. - Run the same suite against pinned YDB Local and cloud-dev serverless YDB.
When to revisit ydb-platform/golang-migrate
Run a short comparative spike within #5 if Goose fails the concurrent-runner/crash-recovery acceptance criteria. Compare both tools using the same fixtures:
- two parallel migrators;
- crash between DDL and version update;
- restart after partial DDL;
- metadata authentication in a Serverless Container;
- drift in modified applied SQL.
For now, the fork wins on its built-in lock/dirty state, but loses on release discipline, full-history/auditability, and the ownership cost of an entire fork. For the MVP, it is not a cost-effective default.


Implementation update — YDB state store
Implementation is published in MR !4, branch ai/mvp-03-ydb-state-store, commit 992478d.
Implemented
- 22 ordered YDB row-table migrations with tenant-first primary keys, explicit TTL columns, and dedicated time-keyed ready/expiry tables for reconciler access without JSON or full-table scans.
- A repository-owned
schema-migratebinary using embedded Goose migrations, a fenced YDB lease, pre-execution SHA-256 checksums, migration status output, and forward-only crash recovery. - The MVP-02
StateStoreport over YDB serializable transactions with official SDK retry classification and tenant mismatch rejection. - Atomic Telegram update deduplication plus run, initial attempt, and dispatch outbox creation.
- Exactly-one fenced lease claim, renewal, stale-fence rejection for checkpoints/results, and tenant/time lease recovery reads.
- Atomic quota transitions, usage observations, artifact manifests, Telegram delivery outbox state, and dispatch acknowledgement.
- Official environment/metadata authentication; credentials remain outside DSNs, command lines, images, and repository files.
- A pinned YDB Local Compose service and a mandatory GitHub Actions job that applies the schema twice and runs concurrency/isolation tests.
- Public documentation for key/access patterns, TTL behavior, expand/migrate/contract deployment, and migration incident repair.
- The remaining
worker-codexskeleton name was changed to harness-neutralworker-runtime.
Verified locally
make ci— formatting,go vet, race tests, five binaries, and foundation integration tests passed.git diff --checkpassed.- Shell and YAML syntax validation passed.
- The
ydbintegrationpackage compiles with its build tag.
Docker is not installed on the development workstation, so no local YDB execution is claimed. Issue #5 remains open until the GitCode branch reaches the GitHub mirror and the new YDB integration job proves repeatable migrations, duplicate-ingress rollback, exactly one concurrent lease winner, tenant isolation, and runtime image builds.


Post-merge CI follow-up
The first post-merge mirror run reached the new YDB Local job but failed before opening a YDB connection: GitHub Actions run 30444678066, job YDB schema and state store.
Root cause: the Makefile exports GOTMPDIR=.build/tmp, but make migrate-local did not depend on prepare in the fresh isolated job. Go therefore failed with stat .../.build/tmp: no such file or directory; this is a CI setup defect, not a schema or YDB failure.
The one-line fix is published in MR !5, commit 3a4a886: migrate-local: prepare.
Local make ci, make -n migrate-local, and git diff --check pass. Issue #5 remains open until !5 is merged, mirrored, and the YDB job reaches and passes the real migration/concurrency suite.


Completion report — MVP-03
MVP-03 is complete and verified on the merged source-of-truth branch.
Delivered
- The authoritative YDB schema for tenants, frontend identities and conversations, context epochs, runs and attempts, leases, subscription connections, quota reservations and observations, usage events, artifact manifests, dispatch and delivery outboxes, and audit events.
- Twenty-two ordered, forward-only YDB migrations with tenant-first primary keys, explicit TTL columns, and dedicated ready/expiry access paths.
- A repository-owned
schema-migratebinary using embedded upstream Goose migrations, a fenced YDB migration lease, pre-execution SHA-256 drift checks, status output, and documented crash-recovery procedures. - A transactional YDB implementation of the harness-neutral
StateStorecontract, including atomic ingress deduplication, run/outbox creation, fenced lease operations, checkpoint/result commits, quota transitions, usage observations, artifact manifests, delivery state, and dispatch acknowledgements. - Official environment/metadata authentication with no credentials in DSNs, command lines, images, or repository files.
- Pinned YDB Local integration infrastructure and mandatory CI coverage for repeatable migrations, concurrent lease claims, duplicate ingress, retry idempotency, tenant isolation, and runtime image builds.
- Harness-neutral runtime naming: the former
worker-codexskeleton is nowworker-runtime.
Merge and verification evidence
- Main implementation: MR !4, merged as
bc4575a. - CI preparation fix: MR !5, merged as
83eade6. - The merged
maintree for !5 matches the reviewed head tree. - The GitHub mirror reached the same full commit SHA,
83eade65a00dd7d71c76ec880d7fdd0c69a08e00. - Post-merge GitHub Actions run #6 completed successfully:
- Go verification: success.
- YDB schema and state store: success.
- Runtime images: success.
The YDB job applied the schema repeatedly and completed the integration suite after the merged migrate-local: prepare fix. This closes the only post-merge CI failure observed after !4.
All acceptance criteria in this issue are now satisfied.


Parent and architecture
Outcome
Provide the authoritative YDB persistence layer for Telegram ingestion, runs, leases, quota accounting, context manifests, audit and outboxes.
Estimate
Scope
Design requirements
Verification
Acceptance criteria