Skip to content

Commit 01924f9

Browse files
committed
replicators: Add support for RENAME TABLE
Implements support for `RENAME TABLE` (MySQL) and somewhat improves `ALTER TABLE ... RENAME TO ...` handling (MySQL and PostgreSQL). This is achieved by adding a new `AlterTableDefinition::RenameTable` variant and, when constructing a `Changelist` for the server, converting MySQL `RenameTableStatement` into a series of `AlterTableStatement`s. The resulting `Change`s are handled in the replicator by triggering a resnapshot of each affected table. This is not very efficient, but actually "renaming" already-existing base tables would break a lot of assumptions and require coordinating with the worker/domain to rename the RocksDB directories safely. So this is the safest option, and it should be good enough for now. Release-Note-Core: Added support for `RENAME TABLE` and `ALTER TABLE ... RENAME TO ...` DDL statements. ReadySet will now detect table renames and automatically resnapshot the affected tables to maintain cache consistency. Project: rename AI-Use: level:3 Change-Id: I275c12bfbf0dfb94fd3a88f55ea3dd896a6a6964 Reviewed-on: https://gerrit.readyset.name/c/readyset/+/10887 Reviewed-by: Jason Brown <jason.b@readyset.io> Tested-by: Buildkite CI
1 parent 5564fbf commit 01924f9

File tree

9 files changed

+114
-9
lines changed

9 files changed

+114
-9
lines changed

readyset-adapter/src/backend.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3515,6 +3515,7 @@ where
35153515
| SqlQuery::DropTable(_)
35163516
| SqlQuery::DropView(_)
35173517
| SqlQuery::AlterTable(_)
3518+
| SqlQuery::RenameTable(_)
35183519
| SqlQuery::Truncate(_)
35193520
| SqlQuery::Use(_)
35203521
| SqlQuery::CreateIndex(_) => {
@@ -3524,9 +3525,6 @@ where
35243525
.await
35253526
.map(|r| QueryResult::Upstream(r, None, None))
35263527
}
3527-
SqlQuery::RenameTable(_) => {
3528-
unsupported!("{} not yet supported", query.query_type());
3529-
}
35303528
SqlQuery::Set(_)
35313529
| SqlQuery::CompoundSelect(_)
35323530
| SqlQuery::Show(_)

readyset-client/src/recipe/changelist.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ use readyset_errors::{internal, unsupported, ReadySetError, ReadySetResult};
4141
use readyset_sql::ast::{
4242
AlterTableDefinition, AlterTableStatement, CacheInner, CreateCacheStatement,
4343
CreateTableStatement, CreateViewStatement, DropTableStatement, DropViewStatement,
44-
NonReplicatedRelation, Relation, SelectStatement, SqlIdentifier, SqlQuery, TableKey,
44+
NonReplicatedRelation, Relation, RenameTableStatement, SelectStatement, SqlIdentifier,
45+
SqlQuery, TableKey,
4546
};
4647
use readyset_sql_parsing::{parse_query_with_config, ParsingConfig, ParsingPreset};
4748
use readyset_sql_passes::adapter_rewrites::{self, AdapterRewriteParams};
@@ -113,6 +114,25 @@ impl IntoChanges for AlterTableStatement {
113114
}
114115
}
115116

117+
impl IntoChanges for RenameTableStatement {
118+
/// For simplicity, we map MySQL `RENAME TABLE` statements to multiple `ALTER TABLE` statements.
119+
/// This way, Postgres (no `RENAME`) and MySQL don't diverge, and we can handle this the same
120+
/// way and in the same place as `ALTER TABLE RENAME`.
121+
fn into_changes(self) -> Vec<Change> {
122+
self.ops
123+
.into_iter()
124+
.map(|op| {
125+
let alter_statement = AlterTableStatement {
126+
table: op.from,
127+
only: false, // RENAME TABLE doesn't have an ONLY keyword
128+
definitions: Ok(vec![AlterTableDefinition::RenameTable { new_name: op.to }]),
129+
};
130+
Change::AlterTable(alter_statement)
131+
})
132+
.collect()
133+
}
134+
}
135+
116136
impl IntoChanges for Vec<Change> {
117137
fn into_changes(self) -> Vec<Change> {
118138
self
@@ -211,6 +231,9 @@ impl ChangeList {
211231
name: dcs.name,
212232
if_exists: false,
213233
}),
234+
SqlQuery::RenameTable(rts) => {
235+
changes.extend(rts.into_changes());
236+
}
214237
_ => unsupported!(
215238
"Only DDL statements supported in ChangeList (got {})",
216239
parsed.query_type()
@@ -400,6 +423,7 @@ impl Change {
400423
| AlterTableDefinition::DropColumn { .. }
401424
| AlterTableDefinition::ChangeColumn { .. }
402425
| AlterTableDefinition::RenameColumn { .. }
426+
| AlterTableDefinition::RenameTable { .. }
403427
| AlterTableDefinition::AddKey(TableKey::PrimaryKey { .. })
404428
| AlterTableDefinition::AddKey(TableKey::UniqueKey { .. })
405429
| AlterTableDefinition::DropForeignKey { .. }

readyset-server/src/controller/sql/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,7 @@ impl SqlIncorporator {
10621062
| AlterTableDefinition::DropColumn { .. }
10631063
| AlterTableDefinition::ChangeColumn { .. }
10641064
| AlterTableDefinition::RenameColumn { .. }
1065+
| AlterTableDefinition::RenameTable { .. }
10651066
| AlterTableDefinition::DropConstraint { .. }
10661067
| AlterTableDefinition::ReplicaIdentity(_)
10671068
| AlterTableDefinition::DropForeignKey { .. } => {

readyset-sql-parsing/tests/parity.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -890,8 +890,21 @@ fn where_not_in_not_between() {
890890
#[test]
891891
fn rename_table() {
892892
check_parse_both!("RENAME TABLE tb1 TO tb2");
893-
check_parse_both!("ALTER TABLE tb1 RENAME TO tb2");
894-
check_parse_both!("ALTER TABLE tb1 RENAME AS tb2");
893+
check_parse_fails!(
894+
Dialect::PostgreSQL,
895+
"ALTER TABLE tb1 RENAME TO tb2",
896+
"nom-sql AST differs"
897+
);
898+
check_parse_fails!(
899+
Dialect::MySQL,
900+
"ALTER TABLE tb1 RENAME TO tb2",
901+
"nom-sql AST differs"
902+
);
903+
check_parse_fails!(
904+
Dialect::MySQL,
905+
"ALTER TABLE tb1 RENAME AS tb2",
906+
"nom-sql AST differs"
907+
);
895908
}
896909

897910
#[test]

readyset-sql-parsing/tests/roundtrip.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,10 @@ fn create_index() {
116116
"found: SPATIAL"
117117
);
118118
}
119+
120+
#[test]
121+
fn test_alter_table_rename() {
122+
check_rt_mysql_sqlparser!("ALTER TABLE tb1 RENAME TO tb2");
123+
check_rt_postgres_sqlparser!("ALTER TABLE tb1 RENAME TO tb2");
124+
check_rt_mysql_sqlparser!("ALTER TABLE tb1 RENAME AS tb2");
125+
}

readyset-sql/src/analysis/visit.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,6 +1115,7 @@ pub fn walk_alter_table_definition<'a, V: Visitor<'a>>(
11151115
visitor.visit_sql_identifier(name)?;
11161116
visitor.visit_sql_identifier(new_name)
11171117
}
1118+
AlterTableDefinition::RenameTable { new_name } => visitor.visit_table(new_name),
11181119
AlterTableDefinition::ReplicaIdentity(_)
11191120
| AlterTableDefinition::Algorithm { .. }
11201121
| AlterTableDefinition::Lock { .. } => Ok(()),

readyset-sql/src/analysis/visit_mut.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,7 @@ pub fn walk_alter_table_definition<'a, V: VisitorMut<'a>>(
11541154
visitor.visit_sql_identifier(name)?;
11551155
visitor.visit_sql_identifier(new_name)
11561156
}
1157+
AlterTableDefinition::RenameTable { new_name } => visitor.visit_table(new_name),
11571158
AlterTableDefinition::ReplicaIdentity(_)
11581159
| AlterTableDefinition::Algorithm { .. }
11591160
| AlterTableDefinition::Lock { .. } => Ok(()),

readyset-sql/src/ast/alter.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use proptest::{
77
};
88
use readyset_util::fmt::fmt_with;
99
use serde::{Deserialize, Serialize};
10+
use sqlparser::ast::RenameTableNameKind;
1011
use test_strategy::Arbitrary;
1112

1213
use crate::{
@@ -172,6 +173,9 @@ pub enum AlterTableDefinition {
172173
name: SqlIdentifier,
173174
new_name: SqlIdentifier,
174175
},
176+
RenameTable {
177+
new_name: Relation,
178+
},
175179
DropConstraint {
176180
name: SqlIdentifier,
177181
drop_behavior: Option<DropBehavior>,
@@ -235,6 +239,12 @@ impl TryFromDialect<sqlparser::ast::AlterTableOperation> for AlterTableDefinitio
235239
name: old_column_name.into_dialect(dialect),
236240
new_name: new_column_name.into_dialect(dialect),
237241
}),
242+
sqlparser::ast::AlterTableOperation::RenameTable {
243+
table_name:
244+
RenameTableNameKind::As(object_name) | RenameTableNameKind::To(object_name),
245+
} => Ok(Self::RenameTable {
246+
new_name: object_name.try_into_dialect(dialect)?,
247+
}),
238248
ChangeColumn {
239249
old_name,
240250
new_name,
@@ -310,6 +320,9 @@ impl DialectDisplay for AlterTableDefinition {
310320
dialect.quote_identifier(new_name)
311321
)
312322
}
323+
Self::RenameTable { new_name } => {
324+
write!(f, "RENAME TO {}", new_name.display(dialect))
325+
}
313326
Self::DropConstraint {
314327
name,
315328
drop_behavior,

replicators/src/noria_adapter.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -795,9 +795,27 @@ impl<'a> NoriaAdapter<'a> {
795795
}
796796
keep
797797
}
798-
Change::AlterTable(stmt) => self
799-
.table_filter
800-
.should_be_processed(schema.as_str(), stmt.table.name.as_str()),
798+
Change::AlterTable(stmt) => {
799+
// If this alter table is a rename, we also check whether the new name is filtered
800+
// out. Only if both are ignored will this return false and this event get skipped.
801+
self.table_filter
802+
.should_be_processed(schema.as_str(), stmt.table.name.as_str())
803+
|| if let Ok(defs) = &stmt.definitions {
804+
defs.iter().any(|def| {
805+
if let readyset_sql::ast::AlterTableDefinition::RenameTable {
806+
new_name,
807+
} = def
808+
{
809+
self.table_filter
810+
.should_be_processed(schema.as_str(), new_name.name.as_str())
811+
} else {
812+
false
813+
}
814+
})
815+
} else {
816+
false
817+
}
818+
}
801819
_ => true,
802820
});
803821

@@ -811,6 +829,35 @@ impl<'a> NoriaAdapter<'a> {
811829
})
812830
}));
813831

832+
// Resolve schemas and drop renamed tables; the new names will get snapshotted when we
833+
// return `ResnapshotNeeded` in the next `if` block
834+
for change in changelist.changes_mut() {
835+
if let Change::AlterTable(stmt) = change {
836+
if stmt.table.schema.is_none() {
837+
stmt.table.schema = Some(schema.clone().into());
838+
}
839+
840+
if let Ok(definitions) = &mut stmt.definitions {
841+
for def in definitions.iter_mut() {
842+
if let readyset_sql::ast::AlterTableDefinition::RenameTable { new_name } =
843+
def
844+
{
845+
if new_name.schema.is_none() {
846+
new_name.schema = Some(schema.clone().into());
847+
}
848+
849+
info!(
850+
old_table = %stmt.table.display_unquoted(),
851+
new_table = %new_name.display_unquoted(),
852+
"Table rename detected, triggering resnapshot for table"
853+
);
854+
self.drop_table_for_resnapshot(stmt.table.clone()).await?;
855+
}
856+
}
857+
}
858+
}
859+
}
860+
814861
if self.supports_resnapshot && changelist.changes().any(Change::requires_resnapshot) {
815862
// In case we detect a DDL change that requires a full schema resnapshot exit the loop
816863
// with the proper status

0 commit comments

Comments
 (0)