fix(ddl): replace refreshSnapshotAfterLock with orphan-table cleanup in DropDatabase#23791
fix(ddl): replace refreshSnapshotAfterLock with orphan-table cleanup in DropDatabase#23791LeftHandCold wants to merge 8 commits intomatrixorigin:3.0-devfrom
Conversation
Review Summary by QodoFix DROP DATABASE duplicate-key errors by using snapshot read instead of advancing transaction snapshot
WalkthroughsDescription• Replace refreshSnapshotAfterLock with listRelationsAtLatestSnapshot to fix duplicate-key errors during DROP DATABASE in restore cluster scenarios • Use separate read-only transaction at latest snapshot to list relations, avoiding workspace tombstone stale rowIDs • Remove unused imports (moruntime, encoding/hex) and simplify table filtering logic • Fix variable reference from database.GetDatabaseId to db.GetDatabaseId • Update tests to verify snapshot is NOT advanced during relation listing Diagramflowchart LR
A["DropDatabase acquires<br/>exclusive lock"] --> B["listRelationsAtLatestSnapshot<br/>reads mo_tables in<br/>separate txn"]
B --> C["Returns deleteTables<br/>and ignoreTables"]
C --> D["Current txn snapshot<br/>unchanged"]
D --> E["Workspace tombstone<br/>transfer works correctly"]
E --> F["No duplicate-key<br/>errors on commit"]
File Changes1. pkg/sql/compile/ddl.go
|
Code Review by Qodo
1. Unescaped dbName literal
|
| sql := fmt.Sprintf( | ||
| "select %s from `%s`.`%s` where %s = %d and %s = '%s'", | ||
| catalog.SystemRelAttr_Name, catalog.MO_CATALOG, catalog.MO_TABLES, | ||
| catalog.SystemRelAttr_AccID, accountID, | ||
| catalog.SystemRelAttr_DBName, dbName, | ||
| ) |
There was a problem hiding this comment.
1. Unescaped dbname literal 🐞 Bug ⛨ Security
listRelationsAtLatestSnapshot interpolates dbName into a single-quoted SQL literal using '%s' without escaping, which can break the query or be abused if a database name contains quotes/backslashes. The repo already provides a standard mo_tables-in-DB query format that uses %q for safe quoting/escaping of the database-name literal.
Agent Prompt
### Issue description
`listRelationsAtLatestSnapshot` builds an internal SQL query by interpolating `dbName` into a single-quoted SQL literal (`... reldatabase = '%s'`). This does not escape quotes/backslashes and can lead to malformed SQL or unintended query behavior.
### Issue Context
The codebase already defines `catalog.MoTablesInDBQueryFormat`, which performs the same lookup and uses `%q` for the database-name literal.
### Fix Focus Areas
- pkg/sql/compile/ddl.go[3848-3863]
- pkg/catalog/types.go[622-635]
### Suggested change
- Replace the ad-hoc `fmt.Sprintf("... reldatabase = '%s'", ..., dbName)` with `fmt.Sprintf(catalog.MoTablesInDBQueryFormat, accountID, dbName)` (or equivalent safe quoting/escaping for the literal).
- (Optional) Add/adjust a unit test to cover db names containing `'` / `\\` so this doesn’t regress.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…in DropDatabase The previous fix (matrixorigin#23767) called refreshSnapshotAfterLock after acquiring the exclusive lock on mo_database. This advanced the transaction's SnapshotTS, which bypassed the workspace tombstone transfer mechanism and caused duplicate-key errors in restore-cluster scenarios where multiple DDLs run inside a single transaction. Root cause: A concurrent 'data branch create table' can commit between the DropDatabase transaction's snapshot and its exclusive lock acquisition. The new table is invisible to the current txn, so DROP TABLE IF EXISTS is a no-op for it, leaving orphan records in mo_tables/mo_columns. New approach (two-step, no snapshot advancement): 1. listRelationsAtLatestSnapshot: independent read-only txn (no WithTxn), queries mo_tables at the latest committed snapshot to discover all tables including those committed by concurrent transactions. 2. listVisibleRelations: uses the current txn (WithTxn) to get tables visible to the current snapshot. Captured BEFORE Engine.Delete modifies the workspace (tombstones would otherwise make tables invisible). 3. diffStringSlice: orphanTables = allTables - visibleTables. 4. deleteOrphanTableRecords: uses exec.ExecTxn (independent txn) to call engine.Database.Delete() for each orphan table. Uses the engine API (not raw SQL DELETE) because TN expects the full GenDropTableTuple batch format. Cannot use DROP TABLE IF EXISTS (calls lockMoDatabase(Shared), deadlocks with parent's Exclusive lock). Atomicity note: The orphan-cleanup txn and the parent DropDatabase txn commit separately. If cleanup succeeds but parent fails: orphan records are deleted but the DB still exists — harmless, orphans should not exist anyway. If cleanup fails: DropDatabase returns the error and the parent does not continue. Also removes lockMoTableSentinel and all 7 call sites (no longer needed).
a652cd3 to
3552991
Compare
What type of PR is this?
Which issue(s) this PR fixes:
issue #23766
What this PR does / why we need it:
The previous fix (#23767) called refreshSnapshotAfterLock after acquiring the
exclusive lock on mo_database. This advanced the transaction's SnapshotTS,
which bypassed the workspace tombstone transfer mechanism and caused
duplicate-key errors in restore-cluster scenarios where multiple DDLs run
inside a single transaction.
Root cause:
A concurrent 'data branch create table' can commit between the DropDatabase
transaction's snapshot and its exclusive lock acquisition. The new table is
invisible to the current txn, so DROP TABLE IF EXISTS is a no-op for it,
leaving orphan records in mo_tables/mo_columns.
New approach (two-step, no snapshot advancement):
queries mo_tables at the latest committed snapshot to discover all tables
including those committed by concurrent transactions.
to the current snapshot. Captured BEFORE Engine.Delete modifies the
workspace (tombstones would otherwise make tables invisible).
engine.Database.Delete() for each orphan table. Uses the engine API
(not raw SQL DELETE) because TN expects the full GenDropTableTuple batch
format. Cannot use DROP TABLE IF EXISTS (calls lockMoDatabase(Shared),
deadlocks with parent's Exclusive lock).
Atomicity note:
The orphan-cleanup txn and the parent DropDatabase txn commit separately.
If cleanup succeeds but parent fails: orphan records are deleted but the DB
still exists — harmless, orphans should not exist anyway.
If cleanup fails: DropDatabase returns the error and the parent does not
continue.
Also removes lockMoTableSentinel and all 7 call sites (no longer needed)