Repository navigation
Conversation
… database UpsertDatabase overwrites project, environment and metadata on an (instance, name) conflict, and the create-database task is its only caller. On a workspace-level instance nothing stopped a create-database plan that named a database already registered in another project from rebinding it to the plan's project and dropping its labels. The rebind committed before the engine create ran, so the caller's own project roles then covered the database's data; on PostgreSQL, CockroachDB, Redshift and MongoDB the task completed and the move was silent, on the other engines it failed at the create with the move already done. A caller needed only plan and rollout rights in their own project, and no access to the database's project. The refusal lives in the upsert: ON CONFLICT ... DO UPDATE ... WHERE db.project = EXCLUDED.project, refusing when no row is written. Because it is part of the write, two concurrent first creates of one name from different projects cannot both win. A soft-deleted row is held to its project too, not exempted: the deleted flag also marks a live database the sync allowlist excluded, which the store cannot tell from a freed name. The narrow cost is that reusing a genuinely-dropped name from another project is refused; an engine-existence check that restores it is a follow-up. A project instance already forces its own project. A real move goes through DatabaseService/UpdateDatabase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
ecmadao
approved these changes
Oct 8, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A create-database change can no longer take over a database that is already live in another project. Before this fix, a create-database plan that named such a database rebound it to the plan's project, changed its environment, and dropped its labels before the engine create ran, so the caller's own project roles (SQL select, DDL and DML) then applied to its data. The rebind committed in its own transaction, so it stayed whatever happened next: on PostgreSQL, CockroachDB, Redshift and MongoDB the engine create was skipped or wrote into the moved database and the task completed, so the move was silent; on the other engines the task failed at the create, but the database had already moved. The caller needed only the rights to drive a plan and a rollout in their own project (running the task through task-run permission, or an automatic rollout policy), and no access to the project the database belonged to. The path is the same for the console, the public API and MCP.
The fix
Store.UpsertDatabaseoverwrites project, environment and metadata on an(instance, name)conflict, and the create-database task is its only caller. The refusal now lives in that write:and the call refuses when the statement returns no row. Because the guard is part of the write, not a prior read, two concurrent first-creates of one name from different projects cannot both win: one inserts, the other meets the conflict and its
WHEREfails. A project-scoped instance forces its own project inprojectForUpsert, so the clause always holds there and its behavior is unchanged. A genuine move between projects still goes throughDatabaseService/UpdateDatabase, which checks both projects.The guard does not exempt a soft-deleted row. A
deletedrow is either a genuinely dropped database or a live one the instance's sync allowlist excluded, and the store cannot tell them apart (backend/runner/schemasync/syncer.go), so exempting it would re-open the takeover for the excluded-but-live case. The cost is narrow and stated below.UpsertDatabasehas one caller, so the guard sits safely in the statement with no new parameter and no split of a shared-looking primitive; a short comment on the clause says so.Who was affected
UpsertDatabase's overwritingON CONFLICTclause is there in each (git show <tag>:backend/store/database.go). The exact first release is older still and not pinned here.The narrow cost
Reusing a genuinely-dropped database's name from another project is now refused, which
origin/mainallowed. The owning project can still reuse its own dropped name (theWHEREmatches), and a create-database task that re-runs against its own database is unaffected. There is no API to move a soft-deleted row to a new project, so a cross-project reuse of a dropped name has no path until the follow-up's engine-existence check restores it. This is a rare, cross-project operation; refusing it is the price of a store-only guard that cannot tell a dropped name from an allowlist-excluded live database. The refusal also keeps the old row's changelog, revisions and schema (keyed on instance and name, with no project column) from moving to the new project, which the pre-fix reuse did move, so it is as much a fix as a cost.Not covered here
ON CONFLICTstatement is the refusal atomic with the write; a plan-time API check would leave a window, since nothing re-checks a create before the task runs.Breaking Changes
UpdateDatabase. Reusing a genuinely-dropped name from another project is also refused now (see The narrow cost); the owning project can still reuse its own dropped name.Release note
MCP sessions and API callers can no longer take over a database that is live in another project through a create-database plan: a plan that names such a database now fails before the database is moved, instead of rebinding it to the plan's project and dropping its labels. A database's project and environment decide which approval and SQL review rules apply to it, and the project's roles decide who can read and change its data, so this closes a cross-project escalation. Reusing a genuinely-dropped database name from another project is also refused; the owning project can still reuse its own.
Docs to update (bytebase.com)
Tests
backend/store/database_ownership_test.go:TestDatabaseWritersRespectProjectInstanceOwnershipgains a workspace-instance case — a cross-project upsert is refused withcommon.Invalidand the row is unchanged; the same-project upsert still succeeds.TestWorkspaceInstanceUpsertDroppedNameStaysWithItsProject— a dropped name cannot be taken by another project; the owning project can reuse it.TestWorkspaceInstanceUpsertRaceKeepsOneOwner— two concurrent first-creates of one name from different projects: exactly one wins, the other is refused.backend/tests/create_database_takeover_test.go:TestCreateDatabasePlanCannotTakeOverAnotherProjectsDatabasedrives the full path as a Project Owner of the plan's project only; the task fails naming the refusal, and the existing database keeps its project, environment and labels.origin/main's store and pass with the fix.Receipts, read at 80524fc and branch HEAD:
WHEREin that clausebackend/store/database.goUpsertDatabasegrep -rn 'UpsertDatabase(' backend --include='*.go'(non-test): onlybackend/runner/taskrun/database_create_executor.goprojectForUpsert,backend/store/database.gobackend/runner/taskrun/database_create_executor.goupserts, then runs the create; PostgreSQL/CockroachDB/Redshift skip it for an existing database, MongoDB'screateCollectionwrites into the moved database, so the task completes silently there; the other engines run a plainCREATE DATABASEand fail after the movegit show v3.4.0:backend/store/database.gohasON CONFLICT (instance, name) DO UPDATE SET project = EXCLUDED.project, ...inUpsertDatabase(tag isv3.4.0, no3.4.0tag)backend/store/predefined_roles.go(Project Owner holdspermission.SQLSelect,SQLDdl,SQLDml)backend/api/v1/plan_service.govalidateSpecscreate branchbackend/runner/taskrun/running_scheduler.govalidateTaskFreshness(Task_DATABASE_CREATE)backend/store/predefined_roles.go🤖 Generated with Claude Code