diff --git a/pkg/frontend/databranchutils/branch_dag_test.go b/pkg/frontend/databranchutils/branch_dag_test.go index 5e6fa347dd876..0c7d277404069 100644 --- a/pkg/frontend/databranchutils/branch_dag_test.go +++ b/pkg/frontend/databranchutils/branch_dag_test.go @@ -272,6 +272,71 @@ func TestComputeAlterLineageCompactionPlan(t *testing.T) { ) } +func TestComponentHasLiveLogicalBranch(t *testing.T) { + for _, tc := range []struct { + name string + start uint64 + rows []DataBranchMetadata + want bool + }{ + { + name: "historical ALTER generations only", + start: 3, + rows: []DataBranchMetadata{ + {TableID: 2, PTableID: 1, Level: AlterLineageLevel}, + {TableID: 3, PTableID: 2, Level: AlterLineageLevel}, + }, + }, + { + name: "live logical branch", + start: 2, + rows: []DataBranchMetadata{ + {TableID: 2, PTableID: 1, Level: "table"}, + }, + want: true, + }, + { + name: "logical ownership inherited through ALTER", + start: 3, + rows: []DataBranchMetadata{ + {TableID: 2, PTableID: 1, Level: "table", TableDeleted: true}, + {TableID: 3, PTableID: 2, Level: "alter:table"}, + }, + want: true, + }, + { + name: "live logical sibling across ancestor", + start: 3, + rows: []DataBranchMetadata{ + {TableID: 2, PTableID: 1, Level: "table"}, + {TableID: 3, PTableID: 1, Level: AlterLineageLevel}, + }, + want: true, + }, + { + name: "deleted logical sibling", + start: 3, + rows: []DataBranchMetadata{ + {TableID: 2, PTableID: 1, Level: "table", TableDeleted: true}, + {TableID: 3, PTableID: 1, Level: AlterLineageLevel}, + }, + }, + { + name: "historical ALTER cycle", + start: 1, + rows: []DataBranchMetadata{ + {TableID: 1, PTableID: 2, Level: AlterLineageLevel}, + {TableID: 2, PTableID: 1, Level: AlterLineageLevel}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + dag := NewBranchReclaimDag(tc.rows) + require.Equal(t, tc.want, dag.ComponentHasLiveLogicalBranch(tc.start)) + }) + } +} + func TestComputeAlterLineageCompactionPlanReclaimsDeletedAlterGenerations(t *testing.T) { rows := []DataBranchMetadata{ {TableID: 2, PTableID: 1, CloneTS: 100, Level: "alter", TableDeleted: true}, diff --git a/pkg/frontend/databranchutils/branch_protect_snapshot.go b/pkg/frontend/databranchutils/branch_protect_snapshot.go index 103933c2d76a4..b24702d79112e 100644 --- a/pkg/frontend/databranchutils/branch_protect_snapshot.go +++ b/pkg/frontend/databranchutils/branch_protect_snapshot.go @@ -232,32 +232,7 @@ func ComputeAlterLineageCompactionPlan( if _, ok := visited[start]; ok { continue } - component := make([]uint64, 0, 4) - stack := []uint64{start} - logicalOwner := false - for len(stack) > 0 { - last := len(stack) - 1 - tableID := stack[last] - stack = stack[:last] - if tableID == 0 { - continue - } - if _, ok := visited[tableID]; ok { - continue - } - visited[tableID] = struct{}{} - component = append(component, tableID) - - if meta, ok := dag.Info[tableID]; ok { - if !meta.Deleted && isLogicalBranchOwnerLevel(meta.Level) { - logicalOwner = true - } - if meta.ParentTableID != 0 { - stack = append(stack, meta.ParentTableID) - } - } - stack = append(stack, dag.Children[tableID]...) - } + component, logicalOwner := dag.connectedComponent(start, visited) if logicalOwner { continue @@ -315,6 +290,48 @@ func ComputeAlterLineageCompactionPlan( return plan } +func (d BranchReclaimDag) connectedComponent( + start uint64, + visited map[uint64]struct{}, +) (component []uint64, hasLiveLogicalBranch bool) { + stack := []uint64{start} + for len(stack) > 0 { + last := len(stack) - 1 + tableID := stack[last] + stack = stack[:last] + if tableID == 0 { + continue + } + if _, ok := visited[tableID]; ok { + continue + } + visited[tableID] = struct{}{} + component = append(component, tableID) + + if meta, ok := d.Info[tableID]; ok { + if !meta.Deleted && isLogicalBranchOwnerLevel(meta.Level) { + hasLiveLogicalBranch = true + } + if meta.ParentTableID != 0 { + stack = append(stack, meta.ParentTableID) + } + } + stack = append(stack, d.Children[tableID]...) + } + return component, hasLiveLogicalBranch +} + +// ComponentHasLiveLogicalBranch reports whether the lineage component that +// contains start has a live logical branch owner. Plain "alter" rows only pin +// historical physical generations and do not represent logical branches. +func (d BranchReclaimDag) ComponentHasLiveLogicalBranch(start uint64) bool { + _, hasLiveLogicalBranch := d.connectedComponent( + start, + make(map[uint64]struct{}, len(d.Info)), + ) + return hasLiveLogicalBranch +} + // BuildAlterLineageSnapshotDeleteSQL deletes only branch-managed snapshots. func BuildAlterLineageSnapshotDeleteSQL(snames []string) string { return BuildBranchSnapshotDeleteSQL(snames) diff --git a/pkg/sql/compile/alter.go b/pkg/sql/compile/alter.go index da56d5caee024..82b920596f799 100644 --- a/pkg/sql/compile/alter.go +++ b/pkg/sql/compile/alter.go @@ -95,6 +95,14 @@ func alterCopySQLAtLineageSnapshot(sql string, plan alterDataBranchLineagePlan) return sql + fmt.Sprintf(" {MO_TS = %d}", plan.cloneTS) } +func isExplicitAlterTxn(byBegin, autocommit bool) bool { + return byBegin || !autocommit +} + +func shouldUseFixedAlterCopySnapshot(snapshotAdvanced, txnHasWorkspaceHistory bool) bool { + return snapshotAdvanced && !txnHasWorkspaceHistory +} + func alterDataBranchParticipationSQL(oldTableID uint64) string { return fmt.Sprintf( "select 1 from %s.%s where table_id = %d or p_table_id = %d limit 1", @@ -281,12 +289,22 @@ func (c *Compile) prepareAlterDataBranchLineage( } hasLiveLineage := false if participates { - op := c.proc.GetTxnOperator() - opts := op.TxnOptions() - if err = validateAlterDataBranchLineageTxn( - opts.GetByBegin(), opts.GetAutocommit(), op.Txn().IsPessimistic(), - ); err != nil { - return alterDataBranchLineagePlan{}, err + // ALTER-only rows preserve physical history for a snapshot or PITR but + // are not logical data branches. Inspect the complete connected + // component so an ALTER generation neither triggers a false transaction + // restriction nor hides a live logical sibling behind an ancestor. + ownershipDAG, dagErr := c.loadAlterDataBranchDAG(false) + if dagErr != nil { + return alterDataBranchLineagePlan{}, dagErr + } + if ownershipDAG.ComponentHasLiveLogicalBranch(oldTableID) { + op := c.proc.GetTxnOperator() + opts := op.TxnOptions() + if err = validateAlterDataBranchLineageTxn( + opts.GetByBegin(), opts.GetAutocommit(), op.Txn().IsPessimistic(), + ); err != nil { + return alterDataBranchLineagePlan{}, err + } } if err = c.compactExpiredAlterDataBranchLineage(time.Time{}); err != nil { return alterDataBranchLineagePlan{}, err @@ -316,7 +334,7 @@ func (c *Compile) prepareAlterDataBranchLineage( } func validateAlterDataBranchLineageTxn(byBegin, autocommit, _ bool) error { - if byBegin || !autocommit { + if isExplicitAlterTxn(byBegin, autocommit) { return moerr.NewNotSupportedNoCtx( "ALTER on a data-branch lineage is not supported inside an explicit transaction", ) @@ -1165,7 +1183,19 @@ func (s *Scope) AlterTableCopy(c *Compile) (err error) { if lineagePlan.enabled { if lineageSnapshotAdvanced { lineagePlan.cloneTS = lineageCloneTS - lineagePlan.fixedCopyTS = true + // A snapshot hint cannot see this transaction's workspace: it would + // lose earlier DML and cannot resolve a generation created by earlier + // DDL. The current operator already has the lock-held advanced snapshot + // and overlays that workspace, so explicit transactions copy from it. + lineageTxnOpts := lineageTxnOp.TxnOptions() + txnHasWorkspaceHistory := isExplicitAlterTxn( + lineageTxnOpts.GetByBegin(), + lineageTxnOpts.GetAutocommit(), + ) || c.getHaveDDL() + lineagePlan.fixedCopyTS = shouldUseFixedAlterCopySnapshot( + lineageSnapshotAdvanced, + txnHasWorkspaceHistory, + ) } else { // Optimistic mode has no row-lock snapshot barrier. Its statement // snapshot is nevertheless the exact source view copied by ALTER, so diff --git a/pkg/sql/compile/alter_test.go b/pkg/sql/compile/alter_test.go index 31ebfe726c7e8..3b22046f881a5 100644 --- a/pkg/sql/compile/alter_test.go +++ b/pkg/sql/compile/alter_test.go @@ -47,6 +47,7 @@ import ( "github.com/matrixorigin/matrixone/pkg/pb/lock" plan2 "github.com/matrixorigin/matrixone/pkg/pb/plan" "github.com/matrixorigin/matrixone/pkg/pb/timestamp" + "github.com/matrixorigin/matrixone/pkg/pb/txn" "github.com/matrixorigin/matrixone/pkg/sql/plan" "github.com/matrixorigin/matrixone/pkg/testutil" "github.com/matrixorigin/matrixone/pkg/txn/client" @@ -61,6 +62,31 @@ func TestShouldEnableAlterCopyPipelineFlush(t *testing.T) { assert.True(t, shouldEnableAlterCopyPipelineFlush(&plan2.AlterCopyOpt{SkipPkDedup: true})) } +func TestShouldUseFixedAlterCopySnapshot(t *testing.T) { + require.True(t, isExplicitAlterTxn(true, true)) + require.True(t, isExplicitAlterTxn(false, false)) + require.False(t, isExplicitAlterTxn(false, true)) + + require.True(t, shouldUseFixedAlterCopySnapshot(true, false)) + require.False(t, shouldUseFixedAlterCopySnapshot(true, true)) + require.False(t, shouldUseFixedAlterCopySnapshot(false, false)) + require.False(t, shouldUseFixedAlterCopySnapshot(false, true)) +} + +func TestAlterCopySQLAtLineageSnapshot(t *testing.T) { + const sql = "insert into copy select * from source" + require.Equal(t, sql, alterCopySQLAtLineageSnapshot(sql, alterDataBranchLineagePlan{})) + require.Equal(t, sql, alterCopySQLAtLineageSnapshot(sql, alterDataBranchLineagePlan{ + enabled: true, + cloneTS: 123, + })) + require.Equal(t, sql+" {MO_TS = 123}", alterCopySQLAtLineageSnapshot(sql, alterDataBranchLineagePlan{ + enabled: true, + fixedCopyTS: true, + cloneTS: 123, + })) +} + func TestAlterCopySameStatementColumnReplacement(t *testing.T) { tableDef := &plan2.TableDef{Cols: []*plan2.ColDef{ {Name: "a", ColId: 1, Seqnum: 0}, @@ -285,6 +311,70 @@ func TestPrepareAlterDataBranchLineageAllowsHistoricalSourceTxn(t *testing.T) { } } +func TestPrepareAlterDataBranchLineageAllowsHistoricalOnlyGenerationInExplicitTxn(t *testing.T) { + const ( + oldTableID = uint64(42) + parentTableID = uint64(41) + database = "test" + table = "dept" + cloneTS = int64(100) + ) + ctrl := gomock.NewController(t) + spyExec := &alterCopyInsertSpyExecutor{ + results: make(map[string]executor.Result), + resultSequences: make(map[string][]executor.Result), + } + c := newAlterCopyPrecheckCompile(t, ctrl, spyExec) + txnOp := mock_frontend.NewMockTxnOperator(ctrl) + txnOp.EXPECT().TxnOptions().Return(txn.TxnOptions{ByBegin: true, Autocommit: true}).AnyTimes() + txnOp.EXPECT().Txn().Return(txn.TxnMeta{}).AnyTimes() + txnOp.EXPECT().SnapshotTS().Return(timestamp.Timestamp{PhysicalTime: cloneTS + 1}).AnyTimes() + c.proc.Base.TxnOperator = txnOp + + participationSQL := alterDataBranchParticipationSQL(oldTableID) + metadataSQL := "select table_id, p_table_id, clone_ts, creator, level, table_deleted from mo_catalog.mo_branch_metadata" + lockedMetadataSQL := metadataSQL + " for update" + edgeSQL := alterDataBranchLineageEdgeSQL() + snapshotSourceSQL := alterDataBranchSnapshotSourceSQL() + pitrSourceSQL := alterDataBranchPitrSourceSQL() + spyExec.results[participationSQL] = newAlterCopyFixedResult( + t, c.proc.Mp(), types.T_int32.ToType(), []int32{1}, + ) + newMetadataResult := func() executor.Result { + return newAlterLineageMetadataResult( + t, c.proc.Mp(), []uint64{oldTableID}, []uint64{parentTableID}, []int64{cloneTS}, + []uint64{uint64(catalog.System_Account)}, []string{databranchutils.AlterLineageLevel}, []bool{false}, + ) + } + spyExec.resultSequences[metadataSQL] = []executor.Result{newMetadataResult(), newMetadataResult()} + spyExec.results[lockedMetadataSQL] = newMetadataResult() + spyExec.results[edgeSQL] = newAlterLineageEdgeResult( + t, c.proc.Mp(), []string{databranchutils.BranchSnapshotName(oldTableID)}, []int64{cloneTS}, + []string{""}, []string{database}, []string{table}, []uint64{parentTableID}, + ) + spyExec.results[snapshotSourceSQL] = newAlterLineageSnapshotSourceResult( + t, c.proc.Mp(), []int64{cloneTS - 1}, []string{"table"}, []string{""}, + []string{database}, []string{table}, []uint64{parentTableID}, + ) + spyExec.results[pitrSourceSQL] = newAlterLineagePitrSourceResult( + t, c.proc.Mp(), nil, nil, nil, nil, nil, nil, nil, + ) + + lineagePlan, err := c.prepareAlterDataBranchLineage(oldTableID, database, table) + require.NoError(t, err) + require.True(t, lineagePlan.enabled) + require.False(t, lineagePlan.preserveHistoricalSource) + require.Equal(t, []string{ + participationSQL, + metadataSQL, + lockedMetadataSQL, + edgeSQL, + snapshotSourceSQL, + pitrSourceSQL, + metadataSQL, + }, spyExec.executedSQLs) +} + func TestShouldAdvanceAlterDataBranchLineageSnapshot(t *testing.T) { require.True(t, shouldAdvanceAlterDataBranchLineageSnapshot(true, true)) require.False(t, shouldAdvanceAlterDataBranchLineageSnapshot(true, false)) @@ -467,13 +557,14 @@ func TestAlterCopyAutoIncrementCleanupDiscardsTrackedReset(t *testing.T) { } type alterCopyInsertSpyExecutor struct { - insertSQL string - insertErr error - insertCtx context.Context - insertOption executor.StatementOption - results map[string]executor.Result - errs map[string]error - executedSQLs []string + insertSQL string + insertErr error + insertCtx context.Context + insertOption executor.StatementOption + results map[string]executor.Result + resultSequences map[string][]executor.Result + errs map[string]error + executedSQLs []string } func TestReconcileAlterCopyAutoIncrementUsesStableIdentityAndSafeBounds(t *testing.T) { @@ -643,6 +734,10 @@ func (e *alterCopyInsertSpyExecutor) Exec( return executor.Result{}, err } } + if results := e.resultSequences[sql]; len(results) > 0 { + e.resultSequences[sql] = results[1:] + return results[0], nil + } if e.results != nil { if res, ok := e.results[sql]; ok { return res, nil diff --git a/test/distributed/cases/pessimistic_transaction/alter_table_historical_lineage.result b/test/distributed/cases/pessimistic_transaction/alter_table_historical_lineage.result new file mode 100644 index 0000000000000..3356a497c56c8 --- /dev/null +++ b/test/distributed/cases/pessimistic_transaction/alter_table_historical_lineage.result @@ -0,0 +1,125 @@ +drop snapshot if exists issue26549_table_snapshot; +drop snapshot if exists issue26549_rollback_snapshot; +drop snapshot if exists issue26549_database_snapshot; +drop pitr if exists issue26549_table_pitr; +drop database if exists issue26549_alter_txn; +create database issue26549_alter_txn; +use issue26549_alter_txn; +create table snapshot_alter ( +id varchar(36) primary key, +status tinyint not null default 1, +index idx_status (status) +); +insert into snapshot_alter values ('r-001', 1), ('r-002', 1), ('r-003', 1); +create snapshot issue26549_table_snapshot for table issue26549_alter_txn snapshot_alter; +begin; +insert into snapshot_alter values ('r-txn', 1); +alter table snapshot_alter add column a int not null default 0; +alter table snapshot_alter add column b int not null default 0; +commit; +insert into snapshot_alter (id, status) values ('r-new', 1); +select count(*) as indexed_count from snapshot_alter where status = 1; +➤ indexed_count[-5,64,0] 𝄀 +5 +select count(*) as base_scan_count from snapshot_alter where status + 0 = 1; +➤ base_scan_count[-5,64,0] 𝄀 +5 +select id, status, a, b from snapshot_alter order by id; +➤ id[12,-1,0] ¦ status[-6,8,0] ¦ a[4,32,0] ¦ b[4,32,0] 𝄀 +r-001 ¦ 1 ¦ 0 ¦ 0 𝄀 +r-002 ¦ 1 ¦ 0 ¦ 0 𝄀 +r-003 ¦ 1 ¦ 0 ¦ 0 𝄀 +r-new ¦ 1 ¦ 0 ¦ 0 𝄀 +r-txn ¦ 1 ¦ 0 ¦ 0 +select id, status from snapshot_alter {snapshot = 'issue26549_table_snapshot'} order by id; +➤ id[12,-1,0] ¦ status[-6,8,0] 𝄀 +r-001 ¦ 1 𝄀 +r-002 ¦ 1 𝄀 +r-003 ¦ 1 +drop snapshot issue26549_table_snapshot; +create table pitr_alter ( +id int primary key, +payload int, +index idx_payload (payload) +); +insert into pitr_alter values (1, 10), (2, 20); +create pitr issue26549_table_pitr for table issue26549_alter_txn pitr_alter range 1 'h'; +set autocommit = 0; +update pitr_alter set payload = 11 where id = 1; +alter table pitr_alter add column extra int not null default 5; +alter table pitr_alter modify column extra bigint not null default 5; +alter table pitr_alter rename column payload to score; +commit; +set autocommit = 1; +insert into pitr_alter values (3, 30, 7); +select id, score, extra from pitr_alter order by id; +➤ id[4,32,0] ¦ score[4,32,0] ¦ extra[-5,64,0] 𝄀 +1 ¦ 11 ¦ 5 𝄀 +2 ¦ 20 ¦ 5 𝄀 +3 ¦ 30 ¦ 7 +select count(*) as renamed_index_count from pitr_alter where score = 20; +➤ renamed_index_count[-5,64,0] 𝄀 +1 +select count(*) as renamed_base_count from pitr_alter where score + 0 = 20; +➤ renamed_base_count[-5,64,0] 𝄀 +1 +drop pitr issue26549_table_pitr; +create table rollback_alter (id int primary key, payload int); +insert into rollback_alter values (1, 10); +create snapshot issue26549_rollback_snapshot for table issue26549_alter_txn rollback_alter; +begin; +alter table rollback_alter add column rolled_back_a int default 1; +alter table rollback_alter add column rolled_back_b int default 2; +rollback; +select column_name +from information_schema.columns +where table_schema = 'issue26549_alter_txn' and table_name = 'rollback_alter' +order by ordinal_position; +➤ column_name[12,-1,0] 𝄀 +id 𝄀 +payload +select * from rollback_alter order by id; +➤ id[4,32,0] ¦ payload[4,32,0] 𝄀 +1 ¦ 10 +drop snapshot issue26549_rollback_snapshot; +create table multi_alter_a (id int primary key); +create table multi_alter_b (id int primary key); +insert into multi_alter_a values (1); +insert into multi_alter_b values (2); +create snapshot issue26549_database_snapshot for database issue26549_alter_txn; +begin; +alter table multi_alter_a add column a1 int default 11; +alter table multi_alter_b add column b1 int default 21; +alter table multi_alter_a add column a2 int default 12; +alter table multi_alter_b add column b2 int default 22; +commit; +select * from multi_alter_a order by id; +➤ id[4,32,0] ¦ a1[4,32,0] ¦ a2[4,32,0] 𝄀 +1 ¦ 11 ¦ 12 +select * from multi_alter_b order by id; +➤ id[4,32,0] ¦ b1[4,32,0] ¦ b2[4,32,0] 𝄀 +2 ¦ 21 ¦ 22 +drop snapshot issue26549_database_snapshot; +create table live_base (id int primary key, payload int); +insert into live_base values (1, 10); +data branch create table live_child from live_base; +alter table live_child add column child_generation int default 1; +begin; +alter table live_child add column rejected_child_generation int default 2; +-- @regex("ALTER on a data-branch lineage is not supported inside an explicit transaction", true) +not supported: ALTER on a data-branch lineage is not supported inside an explicit transaction +rollback; +alter table live_base add column base_generation int default 3; +begin; +alter table live_base add column rejected_base_generation int default 4; +-- @regex("ALTER on a data-branch lineage is not supported inside an explicit transaction", true) +not supported: ALTER on a data-branch lineage is not supported inside an explicit transaction +rollback; +select id, payload, child_generation from live_child order by id; +➤ id[4,32,0] ¦ payload[4,32,0] ¦ child_generation[4,32,0] 𝄀 +1 ¦ 10 ¦ 1 +select id, payload, base_generation from live_base order by id; +➤ id[4,32,0] ¦ payload[4,32,0] ¦ base_generation[4,32,0] 𝄀 +1 ¦ 10 ¦ 3 +data branch delete table live_child; +drop database issue26549_alter_txn; diff --git a/test/distributed/cases/pessimistic_transaction/alter_table_historical_lineage.sql b/test/distributed/cases/pessimistic_transaction/alter_table_historical_lineage.sql new file mode 100644 index 0000000000000..b4ec797ab439a --- /dev/null +++ b/test/distributed/cases/pessimistic_transaction/alter_table_historical_lineage.sql @@ -0,0 +1,110 @@ +-- @suite +-- @case +-- @desc: issue #26549 - historical ALTER lineage must not impersonate a live data branch +-- @label:bvt + +drop snapshot if exists issue26549_table_snapshot; +drop snapshot if exists issue26549_rollback_snapshot; +drop snapshot if exists issue26549_database_snapshot; +drop pitr if exists issue26549_table_pitr; +drop database if exists issue26549_alter_txn; +create database issue26549_alter_txn; +use issue26549_alter_txn; + +-- A table snapshot makes the first COPY ALTER preserve physical history. The +-- second ALTER in the same BEGIN transaction must treat that generated +-- level='alter' row as historical lineage, not as a logical data branch. +create table snapshot_alter ( + id varchar(36) primary key, + status tinyint not null default 1, + index idx_status (status) +); +insert into snapshot_alter values ('r-001', 1), ('r-002', 1), ('r-003', 1); +create snapshot issue26549_table_snapshot for table issue26549_alter_txn snapshot_alter; +begin; +insert into snapshot_alter values ('r-txn', 1); +alter table snapshot_alter add column a int not null default 0; +alter table snapshot_alter add column b int not null default 0; +commit; +insert into snapshot_alter (id, status) values ('r-new', 1); +select count(*) as indexed_count from snapshot_alter where status = 1; +select count(*) as base_scan_count from snapshot_alter where status + 0 = 1; +select id, status, a, b from snapshot_alter order by id; +select id, status from snapshot_alter {snapshot = 'issue26549_table_snapshot'} order by id; +drop snapshot issue26549_table_snapshot; + +-- Exercise the other explicit-transaction form and different COPY ALTER +-- actions while PITR is the only historical owner. +create table pitr_alter ( + id int primary key, + payload int, + index idx_payload (payload) +); +insert into pitr_alter values (1, 10), (2, 20); +create pitr issue26549_table_pitr for table issue26549_alter_txn pitr_alter range 1 'h'; +set autocommit = 0; +update pitr_alter set payload = 11 where id = 1; +alter table pitr_alter add column extra int not null default 5; +alter table pitr_alter modify column extra bigint not null default 5; +alter table pitr_alter rename column payload to score; +commit; +set autocommit = 1; +insert into pitr_alter values (3, 30, 7); +select id, score, extra from pitr_alter order by id; +select count(*) as renamed_index_count from pitr_alter where score = 20; +select count(*) as renamed_base_count from pitr_alter where score + 0 = 20; +drop pitr issue26549_table_pitr; + +-- Repeated ALTER remains fully transactional: rollback must remove both new +-- physical generations and their schema changes. +create table rollback_alter (id int primary key, payload int); +insert into rollback_alter values (1, 10); +create snapshot issue26549_rollback_snapshot for table issue26549_alter_txn rollback_alter; +begin; +alter table rollback_alter add column rolled_back_a int default 1; +alter table rollback_alter add column rolled_back_b int default 2; +rollback; +select column_name + from information_schema.columns + where table_schema = 'issue26549_alter_txn' and table_name = 'rollback_alter' + order by ordinal_position; +select * from rollback_alter order by id; +drop snapshot issue26549_rollback_snapshot; + +-- A database-level snapshot covers multiple ordinary tables. Interleaving the +-- first and second ALTERs proves lineage classification stays table-local. +create table multi_alter_a (id int primary key); +create table multi_alter_b (id int primary key); +insert into multi_alter_a values (1); +insert into multi_alter_b values (2); +create snapshot issue26549_database_snapshot for database issue26549_alter_txn; +begin; +alter table multi_alter_a add column a1 int default 11; +alter table multi_alter_b add column b1 int default 21; +alter table multi_alter_a add column a2 int default 12; +alter table multi_alter_b add column b2 int default 22; +commit; +select * from multi_alter_a order by id; +select * from multi_alter_b order by id; +drop snapshot issue26549_database_snapshot; + +-- Nearest controls: logical branch ownership remains restricted after ALTER +-- moves either the branch itself or its base to a new physical generation. +create table live_base (id int primary key, payload int); +insert into live_base values (1, 10); +data branch create table live_child from live_base; +alter table live_child add column child_generation int default 1; +begin; +-- @regex("ALTER on a data-branch lineage is not supported inside an explicit transaction", true) +alter table live_child add column rejected_child_generation int default 2; +rollback; +alter table live_base add column base_generation int default 3; +begin; +-- @regex("ALTER on a data-branch lineage is not supported inside an explicit transaction", true) +alter table live_base add column rejected_base_generation int default 4; +rollback; +select id, payload, child_generation from live_child order by id; +select id, payload, base_generation from live_base order by id; +data branch delete table live_child; + +drop database issue26549_alter_txn;