Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 184 additions & 17 deletions docs/design/data_branch_privilege.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ with only one side of the required permission create or modify branch objects.
This document defines a complete privilege model for all user-facing
`DATA BRANCH` statements and a concrete implementation plan.

The database-identity extension in section 12 is a proposed amendment for
issue #26068. Its implementation remains gated on independent approval of the
persisted-state and rollout contract recorded there.

The central rule is:

> A data branch statement must require the same privileges as the ordinary SQL
Expand Down Expand Up @@ -392,11 +396,13 @@ Execution safety:
- If the database contains user data tables, every contained user data table
must have an active `mo_branch_metadata` child row.
- Fail before DDL if any contained user data table is not a branch child.
- If validation finds no active branch child tables, fail before DDL. Current
branch metadata is table-level only and cannot distinguish an empty
branch-created database from an ordinary empty database, so `DATA BRANCH
DELETE DATABASE` must fail closed for empty, view-only, or sequence-only
databases until a database-level branch identity exists.
- Before database-identity protocol v59 is active, if validation finds no active
branch child tables, fail before DDL. Current table-level metadata cannot
distinguish an empty branch-created database from an ordinary empty database.
- At protocol v59 or later, `mo_database.dat_type = 'data-branch'` is the
database-level identity. A marked database may contain no ordinary tables,
but every ordinary table that is present must still have an active
`mo_branch_metadata` child row.
- Views are not recorded in `mo_branch_metadata` today. Do not require branch
metadata for views during database-delete validation; they are covered by the
required `DROP DATABASE` privilege.
Expand Down Expand Up @@ -774,9 +780,10 @@ Required unit tests:
- database owner fallback -> same;
- database with non-branch user table -> fail before drop.
- failed validation leaves the database and all contained objects untouched.
- empty database with normal `DROP DATABASE` privilege -> fail before drop.
- database containing only views/sequences and no active branch child tables
-> fail before drop.
- pre-v59 or unmarked empty database with normal `DROP DATABASE` privilege ->
fail before drop.
- v59 marked database containing only views/sequences and no active branch
child tables -> pass.
- database containing only branch tables plus restored views -> pass.

### 9.2 BVT regression cases
Expand Down Expand Up @@ -849,11 +856,10 @@ Implement these decisions in this change:
destination database; do not allow a destination snapshot or overwrite an
existing table.
2. Empty database created by `DATA BRANCH CREATE DATABASE`
- Reject `DATA BRANCH DELETE DATABASE` if validation finds no active branch
child tables. Current metadata is table-level only and cannot distinguish
an empty branch-created database from an ordinary empty database, so the
secure behavior is to fail closed until database-level branch identity is
implemented.
- Before the section 12 protocol is approved and active, reject `DATA BRANCH
DELETE DATABASE` if validation finds no active branch child tables. At v59
or later, accept an empty database only when its catalog row carries the
approved database-level branch identity.
3. View handling in `CREATE DATABASE`
- Require `SELECT` on each source view returned by `showFullTables`.
- Preserve existing view security behavior through synthetic SELECT planning
Expand All @@ -862,10 +868,10 @@ Implement these decisions in this change:
- Do not require `mo_branch_metadata` rows for restored views during
`DATA BRANCH DELETE DATABASE`; current metadata is table-level only.
4. Database-level branch identity
- Keep this fix table-metadata based. Adding database-level branch metadata
is the clean long-term model for empty, view-only, or sequence-only branch
databases, but it requires a metadata schema/upgrade change and should be
done as a separate design and implementation step.
- The privilege fix remains table-metadata based below v59. The proposed
issue #26068 extension is specified separately in section 12 and must not
emit or consume its persistent semantics until the cluster-wide capability
gate is active.
5. Metadata validation batching
- Validate branch child metadata in bounded batches using the shared
`dataBranchMetadataIDBatchSize` constant. This keeps validation and update
Expand All @@ -876,3 +882,164 @@ Implement these decisions in this change:
table names, and `TO ACCOUNT` account names, must use the shared
identifier quoting helper. Do not combine user/catalog identifiers with
raw backticks or bare `%s` formatting.

## 12. Proposed Database-level Branch Identity Amendment

- Status: proposed; implementation activation blocked pending independent
design approval
- Tracking issue: [matrixorigin/matrixone#26068](https://github.com/matrixorigin/matrixone/issues/26068)
- Implementation PR: [matrixorigin/matrixone#28272](https://github.com/matrixorigin/matrixone/pull/28272)
- Design revision: `data-branch-database-identity-2026-09-09-r11`
- Required rollout capability: `MORPCVersion59`

Revision r11 moves the proposed capability to v59 because v48 through v58 are
now owned by the generation-aware CDC watermark catalog contract, vector-level
grouping-set projection expansion, the two ordered ODKU protocol contracts,
MySQL binary JSON subtype tags, ordered-stream distributed Top-N merge, and
catalog-authenticated proxy prepared-cache reuse, followed by session-owned
temporary DDL with transactional data and session-scoped AUTO_INCREMENT
increment/offset and provenance, and the Arrow LOAD external-scan pipeline
payload, followed by binary-string function semantics and runtime-domain
metadata.

### 12.1 Problem and invariant

`DATA BRANCH CREATE DATABASE` commits the destination database even when no
ordinary table receipt is produced, but the pre-v59 delete validator recognizes
database branches only through active table receipts. The result is a database
that the matching delete statement cannot recognize after an empty,
view-only, or sequence-only clone, or after every cloned ordinary table is
dropped.

The new invariant is:

> Every database successfully created by a v59-capable `DATA BRANCH CREATE
> DATABASE` has durable database-level identity until that database is dropped,
> independent of the number and lifetime of its ordinary tables.

Its negation remains fail-closed: an unmarked empty database is not inferred to
be a branch, and a marked database containing any ordinary table without an
active child receipt is not deletable through the data-branch path.

The database catalog row is the first owner of database identity. Table lineage,
parent snapshots, and their cleanup remain owned by `mo_branch_metadata` and
`mo_snapshots`; the database marker does not replace or synthesize table
receipts.

### 12.2 Persistent representation and transaction boundary

Use the existing categorical `mo_database.dat_type` column with the value
`data-branch`. This adds a value, not a column or table, and therefore requires
no catalog schema migration. The internal clone path attaches the value to the
same `CREATE DATABASE` engine call that creates the catalog row. Database
creation, restored objects, table receipts, and branch-protection snapshots
remain in the existing background-executor transaction, so any error rolls all
of them back together.

The delete transaction reads and locks the target database through the existing
path, validates the marker and every current ordinary table receipt, then drops
the database and performs lineage cleanup. Missing, subscription, unknown-type,
unmarked-empty, and mixed branch/non-branch targets fail before DDL.

`data-branch` remains a user database category for publication purposes only
when v59 semantics are active. Subscription and unknown non-empty types remain
non-user databases.

### 12.3 Capability, upgrade, and mixed-version behavior

`MORPCVersion59` is the deployment capability for every producer and consumer
of the marker. `MOProtocolVersion` is maintained at the oldest live service
version, so all three frontend decisions use the same gate:

| Common protocol | CREATE DATABASE branch | DELETE DATABASE branch | CREATE/ALTER PUBLICATION |
| --- | --- | --- | --- |
| `< v59` | new CN rejects before DDL; it does not emit a marker | use legacy table-receipt identity; empty/all-dropped targets fail closed | treat `data-branch` as non-user, matching old CNs |
| `>= v59` | persist `dat_type = 'data-branch'` atomically | accept a marked zero-table target; still validate every present ordinary table | treat `data-branch` as a user database |

Rejecting create below v59 is deliberate. Merely omitting the marker would let a
new CN create another database that later becomes undeletable, while writing it
would expose unknown persistent semantics to an old CN. Existing pre-v59
table-backed branches remain usable and deletable through their receipts during
rollout. Existing pre-v59 empty/all-dropped branches remain indistinguishable
from ordinary databases and cannot be migrated automatically.

No session-local flag or per-call probe may override the common protocol. The
gate must be checked before privilege resolution, target locks, or DDL so a
rejected create has no catalog, table, receipt, or snapshot side effects.

### 12.4 Downgrade and rollback

Deployment must lower the common protocol below v59 before removing v59-capable
CNs. New CNs then immediately stop creating branch databases and use legacy
delete/publication behavior, matching old CNs. Existing marker rows remain
valid opaque catalog data:

- ordinary SQL access and normal `DROP DATABASE` remain unchanged;
- table-backed branches with active receipts remain deletable through `DATA
BRANCH DELETE DATABASE`;
- empty or all-dropped marked branches fail closed until v59 is restored;
- publication create/alter rejects a marked database until v59 is restored.

Rollback does not erase or rewrite markers because doing so would permanently
destroy trustworthy identity. Re-enabling v59 restores the new behavior without
migration. A permanent downgrade must first remove marked branch databases
while v59 is active; silently converting them into ordinary databases is not an
allowed fallback.

### 12.5 Backup, snapshot, PITR, and restore

Physical backup/checkpoint/replay already persists `mo_database.dat_type`; the
new categorical value requires no new serialization. Restore into a deployment
whose common protocol is below v59 is unsupported for snapshots containing a
marked database and must fail before an existing target database is dropped.

Logical snapshot, account/cluster restore, and PITR recreate database catalog
rows rather than replaying them directly. The restore reader must therefore
read `dat_type` with `dat_createsql` and attach `data-branch` to the internal
database create at v59. It must not infer identity from table receipts, names,
views, sequences, or SQL text. Restoring an unmarked pre-v59 database leaves it
unmarked. Subscription restoration retains its existing create-SQL path.

After database recreation, existing object and system-catalog restoration owns
table receipts and protection snapshots as before. A failed type read, capability
check, database create, object restore, or catalog restore returns through the
restore transaction and does not publish a partially restored identity.

### 12.6 Alternatives

- Keep table receipts only: preserves the bug for zero-table and all-dropped
databases and violates the new invariant.
- Reserve a synthetic `mo_branch_metadata.table_id`: overloads a table primary
key and would enter table-lineage DAG, reclamation, and snapshot cleanup as a
fake table. It also changes more persistent invariants than a database-owned
category.
- Add a database metadata table or new column: gives explicit schema shape but
requires catalog bootstrap/upgrade and another owner for a single categorical
fact already represented by `dat_type`.
- Infer from names, object contents, create SQL, or historical snapshots: none
is authoritative and each can misclassify an ordinary database, so all are
rejected.

### 12.7 Deterministic validation

Focused tests must cover both sides of the capability boundary:

- v58 create rejects before any DDL; v59 create attaches the marker;
- old-created unmarked empty/all-dropped databases reject at both versions;
- old-created unmarked table-backed databases with valid receipts delete at
both versions;
- new-created marked empty/all-dropped databases reject at v58 and delete at
v59;
- new-created marked table-backed databases retain legacy receipt validation at
v58 and require the same receipts at v59;
- publication treats the marker as non-user at v58 and user at v59;
- snapshot/PITR restore rejects marked input before destructive work at v58 and
preserves the marker at v59;
- missing/subscription/unknown identity and locally added ordinary tables remain
negative controls;
- BVT at the active protocol proves empty, view-only, sequence-only,
all-dropped, cross-account, publication, and failure-atomicity behavior.

No new goroutine, cache, retry, background worker, external I/O, or unbounded
state is introduced. The additional database-type catalog read replaces the
former existence read and does not change asymptotic work.
1 change: 1 addition & 0 deletions pkg/catalog/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ const (
SystemColNoConstraint = "n"

SystemDBTypeSubscription = "subscription"
SystemDBTypeDataBranch = "data-branch"

MOPartitionMetadata = "mo_partition_metadata"
MOPartitionTables = "mo_partition_tables"
Expand Down
3 changes: 3 additions & 0 deletions pkg/frontend/data_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,9 @@ func dataBranchCreateDatabase(
execCtx.reqCtx, tree.CloneLevelCtxKey{}, tree.NormalCloneLevelDatabase,
)
execCtx.reqCtx = context.WithValue(execCtx.reqCtx, dataBranchCloneLockCtxKey{}, true)
execCtx.reqCtx = context.WithValue(
execCtx.reqCtx, defines.DatTypKey{}, catalog.SystemDBTypeDataBranch,
)

if !skipDataBranchPrivilegeCheck(ses) {
if authStats, err = authenticateDataBranchCreateDatabase(execCtx.reqCtx, ses, stmt); err != nil {
Expand Down
52 changes: 35 additions & 17 deletions pkg/frontend/data_branch_privilege.go
Original file line number Diff line number Diff line change
Expand Up @@ -659,9 +659,13 @@ func validateDataBranchDeleteDatabaseTarget(
if err != nil {
return nil, err
}
if err = validateBranchDatabaseExists(ctx, ses, bh, accId, dbName); err != nil {
databaseType, err := loadBranchDatabaseType(ctx, ses, bh, accId, dbName)
if err != nil {
return nil, err
}
if databaseType != "" && databaseType != catalog.SystemDBTypeDataBranch {
return nil, inactiveBranchDatabaseError(ctx, dbName)
}

sql := branchDeleteDatabaseTableIDsSQL(accId, dbName)
sqlRet, err := runSql(ctx, ses, bh, sql, nil, nil)
Expand All @@ -686,8 +690,8 @@ func validateDataBranchDeleteDatabaseTarget(
}
return true
})
if len(tableNames) == 0 {
return nil, moerr.NewInternalErrorf(ctx, "DATA BRANCH DELETE target %s is not an active branch database", dbName)
if len(tableNames) == 0 && databaseType != catalog.SystemDBTypeDataBranch {
return nil, inactiveBranchDatabaseError(ctx, dbName)
}
if err = validateActiveBranchChildTableIDs(ctx, ses, bh, tableNames); err != nil {
return nil, err
Expand All @@ -713,35 +717,49 @@ func branchDeleteDatabaseTableIDsSQL(accId uint32, dbName string) string {
)
}

func validateBranchDatabaseExists(
func loadBranchDatabaseType(
ctx context.Context,
ses *Session,
bh BackgroundExec,
accId uint32,
dbName string,
) error {
sql := fmt.Sprintf(
"select dat_id from %s.%s where account_id = %d and datname = %s",
catalog.MO_CATALOG,
catalog.MO_DATABASE,
accId,
quoteSQLStringLiteral(dbName),
)
sqlRet, err := runSql(ctx, ses, bh, sql, nil, nil)
) (string, error) {
sqlRet, err := runSql(ctx, ses, bh, branchDatabaseTypeSQL(accId, dbName), nil, nil)
if err != nil {
return err
return "", err
}
defer sqlRet.Close()

var databaseType string
found := false
sqlRet.ReadRows(func(rows int, cols []*vector.Vector) bool {
found = rows > 0
if rows > 0 {
databaseType = executor.GetStringRows(cols[0])[0]
found = true
}
return false
})
if !found {
return moerr.NewBadDB(ctx, dbName)
return "", moerr.NewBadDB(ctx, dbName)
}
return nil
return databaseType, nil
}

func branchDatabaseTypeSQL(accId uint32, dbName string) string {
return fmt.Sprintf(
"select coalesce(%s, '') from %s.%s where %s = %d and %s = %s",
catalog.SystemDBAttr_Type,
catalog.MO_CATALOG,
catalog.MO_DATABASE,
catalog.SystemDBAttr_AccID,
accId,
catalog.SystemDBAttr_Name,
quoteSQLStringLiteral(dbName),
)
}

func inactiveBranchDatabaseError(ctx context.Context, dbName string) error {
return moerr.NewInternalErrorf(ctx, "DATA BRANCH DELETE target %s is not an active branch database", dbName)
}

func validateActiveBranchChildTableIDs(
Expand Down
Loading
Loading