Skip to content

Commit 517d2f3

Browse files
readyset-server: add support for ALTER TABLE ADD KEY
This commit adds support for the ALTER TABLE ADD KEY statement to not require a re-snapshot of the table. When we restart readyset we essentially re-run the snapshot for all tables. Then we check if the body of the table we have stored in the registry is the same as the body from the DDL we are trying to apply. If it is, we don't need to re-snapshot the table. The change on this commit ensures that we update the in-memory copy of the DDL we have stored and also persists a new copy of the table body with the new key in the registry, to avoid re-snapshotting the table when we restart readyset. Fixes: REA-5753 Fixes: #1521 Release-Note-Core: Avoid re-snapshotting tables when user executes an ALTER TABLE ADD KEY statement. Change-Id: If42bc9b9ba78179fe22e7f6cb8bb030f90e1e713 Reviewed-on: https://gerrit.readyset.name/c/readyset/+/9601 Reviewed-by: Michael Zink <michael.z@readyset.io> Tested-by: Buildkite CI
1 parent 10571f9 commit 517d2f3

File tree

6 files changed

+482
-29
lines changed

6 files changed

+482
-29
lines changed

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

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ use readyset_errors::{
1616
ReadySetResult,
1717
};
1818
use readyset_sql::ast::{
19-
self, CompoundSelectStatement, CreateTableBody, CreateTableOption, FieldDefinitionExpr,
20-
NonReplicatedRelation, NotReplicatedReason, Relation, SelectSpecification, SelectStatement,
21-
SqlIdentifier, SqlType, TableExpr,
19+
self, AlterTableDefinition, CompoundSelectStatement, CreateTableBody, CreateTableOption,
20+
FieldDefinitionExpr, NonReplicatedRelation, NotReplicatedReason, Relation, SelectSpecification,
21+
SelectStatement, SqlIdentifier, SqlType, TableExpr, TableKey,
2222
};
2323
use readyset_sql::DialectDisplay;
2424
use readyset_sql_passes::alias_removal::TableAliasRewrite;
@@ -292,7 +292,7 @@ impl SqlIncorporator {
292292
// Table already exists, so check if it has been changed.
293293
if current_body != &body {
294294
// Table has changed. Drop and recreate.
295-
trace!(
295+
info!(
296296
table = %cts.table.display_unquoted(),
297297
"table exists and has changed. Dropping and recreating..."
298298
);
@@ -408,9 +408,13 @@ impl SqlIncorporator {
408408
Change::CreateCache(cc) => {
409409
self.add_query(cc.name, *cc.statement, cc.always, &schema_search_path, mig)?;
410410
}
411-
Change::AlterTable(_) => {
412-
// The only ALTER TABLE changes that can end up here (currently) are ones that
413-
// aren't relevant to ReadySet, so we can just ignore them.
411+
ref change @ Change::AlterTable(ref alter_table_body) => {
412+
// for PG we don't replicate create index. Also Foreign key is not retrieved
413+
// as part of SHOW CREATE TABLE so we don't need to modify the base schema
414+
if dialect == Dialect::DEFAULT_MYSQL {
415+
let mut table = alter_table_body.table.clone();
416+
self.modify_base_schema(&mut table, &schema_search_path, change)?;
417+
}
414418
}
415419
Change::CreateType { mut name, ty } => {
416420
if let Some(first_schema) = schema_search_path.first() {
@@ -969,6 +973,100 @@ impl SqlIncorporator {
969973
}
970974
}
971975

976+
pub(super) fn propagate_default_search_path(
977+
&self,
978+
name: &mut Relation,
979+
schema_search_path: &[SqlIdentifier],
980+
) -> ReadySetResult<()> {
981+
if name.schema.is_none() {
982+
let Some(first_schema) = schema_search_path.first() else {
983+
return Err(invalid_query_err!("No schema search path provided"));
984+
};
985+
name.schema = Some(first_schema.clone());
986+
}
987+
Ok(())
988+
}
989+
990+
/// Modify the base schema of a table. This needs to be aligned with [`Change::requires_resnapshot`]
991+
/// This will update the base_schemas and the registry.
992+
pub(super) fn modify_base_schema(
993+
&mut self,
994+
name: &mut Relation,
995+
schema_search_path: &[SqlIdentifier],
996+
change: &Change,
997+
) -> ReadySetResult<()> {
998+
self.propagate_default_search_path(name, schema_search_path)?;
999+
if change.requires_resnapshot() {
1000+
return Err(invalid_query_err!("Unsupported alter table type"));
1001+
}
1002+
let base_schema = self
1003+
.base_schemas
1004+
.get_mut(name)
1005+
.ok_or_else(|| invalid_query_err!("Table not found"))?;
1006+
1007+
if let Change::AlterTable(alter_table_statement) = change {
1008+
alter_table_statement
1009+
.definitions
1010+
.iter()
1011+
.try_for_each(|definition| {
1012+
definition.iter().try_for_each(|alter_table_body| {
1013+
match *alter_table_body {
1014+
// Extensive list of operations to adjust the base schema.
1015+
AlterTableDefinition::AddKey(ref key @ TableKey::Key { .. })
1016+
| AlterTableDefinition::AddKey(ref key @ TableKey::ForeignKey { .. })
1017+
| AlterTableDefinition::AddKey(
1018+
ref key @ TableKey::CheckConstraint { .. },
1019+
)
1020+
| AlterTableDefinition::AddKey(
1021+
ref key @ TableKey::FulltextKey { .. },
1022+
) => {
1023+
let mut key = key.clone();
1024+
// propagate default search path to the new key. Otherwise we might get different table body on restarts.
1025+
key.get_columns_mut()
1026+
.iter_mut()
1027+
.filter(|col| col.table.is_none())
1028+
.for_each(|col| col.table = Some(name.clone()));
1029+
base_schema
1030+
.statement
1031+
.add_key_with_name_mysql(key.clone(), name.clone());
1032+
1033+
if let Some(RecipeExpr::Table { ref body, .. }) =
1034+
self.registry.get(name)
1035+
{
1036+
let mut new_body = body.clone();
1037+
new_body.add_key_with_name_mysql(key.clone(), name.clone());
1038+
let _ = self.registry.update_query(
1039+
name,
1040+
RecipeExpr::Table {
1041+
name: name.clone(),
1042+
body: new_body,
1043+
pg_meta: base_schema.pg_meta.clone(),
1044+
},
1045+
);
1046+
}
1047+
Ok(())
1048+
}
1049+
// These operations will change the underlying table, and should have been caught earlier
1050+
AlterTableDefinition::AddColumn(_)
1051+
| AlterTableDefinition::AddKey(TableKey::PrimaryKey { .. })
1052+
| AlterTableDefinition::AddKey(TableKey::UniqueKey { .. })
1053+
| AlterTableDefinition::AlterColumn { .. }
1054+
| AlterTableDefinition::DropColumn { .. }
1055+
| AlterTableDefinition::ChangeColumn { .. }
1056+
| AlterTableDefinition::RenameColumn { .. }
1057+
| AlterTableDefinition::DropConstraint { .. }
1058+
| AlterTableDefinition::ReplicaIdentity(_)
1059+
| AlterTableDefinition::DropForeignKey { .. } => {
1060+
Err(invalid_query_err!("Unsupported ALTER TABLE type"))
1061+
}
1062+
}
1063+
})
1064+
})?;
1065+
}
1066+
1067+
Ok(())
1068+
}
1069+
9721070
fn add_base_via_mir(
9731071
&mut self,
9741072
name: Relation,

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,22 @@ impl ExprRegistry {
423423
Ok(true)
424424
}
425425

426+
/// Update the [`RecipeExpr`] associated with the given name (or alias).
427+
pub(super) fn update_query(
428+
&mut self,
429+
name: &Relation,
430+
expression: RecipeExpr,
431+
) -> ReadySetResult<()> {
432+
let query_id = self.aliases.get(name).ok_or_else(|| {
433+
ReadySetError::RecipeInvariantViolated(format!(
434+
"Query {} does not exist",
435+
name.display_unquoted(),
436+
))
437+
})?;
438+
self.expressions.insert(*query_id, expression);
439+
Ok(())
440+
}
441+
426442
/// Retrieves the [`RecipeExpr`] associated with the given name or alias.
427443
/// If no query is found, returns `None`.
428444
pub(super) fn get(&self, alias: &Relation) -> Option<&RecipeExpr> {

readyset-sql/src/ast/common.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,16 @@ impl fmt::Display for NullsDistinct {
158158
}
159159
}
160160

161+
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Arbitrary)]
162+
pub enum TableKeyType {
163+
PrimaryKey,
164+
UniqueKey,
165+
FulltextKey,
166+
Key,
167+
ForeignKey,
168+
CheckConstraint,
169+
}
170+
161171
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Arbitrary)]
162172
pub enum TableKey {
163173
PrimaryKey {
@@ -357,6 +367,7 @@ impl TableKey {
357367
_ => None,
358368
}
359369
}
370+
360371
/// Returns a mutable reference to the target columns of the foreign key
361372
pub fn target_columns_mut(&mut self) -> Option<&mut Vec<Column>> {
362373
match self {
@@ -396,6 +407,11 @@ impl TableKey {
396407
matches!(self, TableKey::ForeignKey { .. })
397408
}
398409

410+
/// Check if the key is of type Key
411+
pub fn is_key(&self) -> bool {
412+
matches!(self, TableKey::Key { .. })
413+
}
414+
399415
/// Get the columns that the key is defined on
400416
pub fn get_columns(&self) -> &[Column] {
401417
match self {
@@ -419,6 +435,18 @@ impl TableKey {
419435
TableKey::CheckConstraint { .. } => &mut [],
420436
}
421437
}
438+
439+
/// Order the column keys following the rules of MySQL
440+
pub fn priority(&self) -> u8 {
441+
match self {
442+
TableKey::PrimaryKey { .. } => 0,
443+
TableKey::UniqueKey { .. } => 1,
444+
TableKey::Key { .. } => 2,
445+
TableKey::FulltextKey { .. } => 3,
446+
TableKey::ForeignKey { .. } => 4,
447+
TableKey::CheckConstraint { .. } => 5,
448+
}
449+
}
422450
}
423451

424452
impl DialectDisplay for TableKey {
@@ -573,6 +601,19 @@ impl DialectDisplay for TableKey {
573601
}
574602
}
575603

604+
impl PartialOrd for TableKey {
605+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
606+
Some(self.cmp(other))
607+
}
608+
}
609+
610+
impl Ord for TableKey {
611+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
612+
// Compare by priority
613+
self.priority().cmp(&other.priority())
614+
}
615+
}
616+
576617
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord, Serialize, Deserialize, Arbitrary)]
577618
#[allow(clippy::large_enum_variant)] // NOTE(aspen): do we actually care about this?
578619
#[derive(Default)]

readyset-sql/src/ast/create.rs

Lines changed: 52 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ impl CreateTableBody {
145145
pub fn add_key(&mut self, key: TableKey) {
146146
if let Some(keys) = &mut self.keys {
147147
keys.push(key);
148+
keys.sort();
148149
} else {
149150
self.keys = Some(vec![key]);
150151
}
@@ -153,15 +154,16 @@ impl CreateTableBody {
153154
/// Add a new key and generate a name for it if it doesn't have one
154155
/// The column name uses the first column as index name, if that name is already in use,
155156
/// we append a number to the name.
156-
pub fn add_key_with_name(&mut self, key: TableKey, table: Relation) {
157+
pub fn add_key_with_name_mysql(&mut self, key: TableKey, table: Relation) {
157158
let mut new_key = key;
158159

159160
// check index name for non-foreign keys
160161
if !new_key.is_foreign_key() && new_key.index_name().is_none() {
161162
let first_column = new_key.get_columns().first().unwrap();
162163
let base_index_name = first_column.name.as_str().to_string();
163164
let mut index_name = base_index_name.clone();
164-
let mut i = 0;
165+
// [make_unique_key_name](https://github.com/mysql/mysql-server/blob/mysql-8.0.42/sql/sql_table.cc#L10386)
166+
let mut i = 2;
165167
while self.is_index_name_in_use(&index_name) {
166168
index_name = format!("{base_index_name}_{i}");
167169
i += 1;
@@ -171,31 +173,47 @@ impl CreateTableBody {
171173

172174
// special case for foreign keys
173175
if new_key.is_foreign_key() {
174-
if new_key.constraint_name().is_none() {
175-
let table_name = table.name.clone();
176-
let base_constraint_name = format!("{table_name}_ibfk");
177-
let mut i = 1;
178-
let mut constraint_name = format!("{base_constraint_name}_{i}");
179-
while self.is_constraint_name_in_use(&constraint_name) {
180-
i += 1;
181-
constraint_name = format!("{base_constraint_name}_{i}");
182-
}
183-
new_key.set_constraint_name(constraint_name.into());
184-
}
176+
self.handle_add_foreign_key_mysql(&mut new_key, table);
177+
}
178+
self.add_key(new_key);
179+
}
185180

186-
// target_table
187-
if let Some(target_table) = new_key.target_table_mut() {
188-
target_table.set_schema(table.schema.clone());
181+
/// MySQL logic to add a new foreign key to the table
182+
fn handle_add_foreign_key_mysql(&mut self, new_key: &mut TableKey, table: Relation) {
183+
if new_key.constraint_name().is_none() {
184+
let table_name = table.name.clone();
185+
let base_constraint_name = format!("{table_name}_ibfk");
186+
let mut i = 1;
187+
let mut constraint_name = format!("{base_constraint_name}_{i}");
188+
while self.is_constraint_name_in_use(&constraint_name) {
189+
i += 1;
190+
constraint_name = format!("{base_constraint_name}_{i}");
189191
}
192+
new_key.set_constraint_name(constraint_name.into());
193+
}
190194

191-
// target_columns
192-
if let Some(target_columns) = new_key.target_columns_mut() {
193-
for column in target_columns {
194-
column.table = Some(table.clone());
195-
}
195+
// target_table
196+
if let Some(target_table) = new_key.target_table_mut() {
197+
target_table.set_schema(table.schema.clone());
198+
}
199+
200+
// target_columns
201+
if let Some(target_columns) = new_key.target_columns_mut() {
202+
for column in target_columns {
203+
column.table = Some(table.clone());
196204
}
197205
}
198-
self.add_key(new_key);
206+
let columns = new_key.get_columns();
207+
if !self.has_key_on_columns(columns) {
208+
// add a new key of type Key on target columns
209+
let target_col_new_key = TableKey::Key {
210+
index_name: new_key.constraint_name().clone(),
211+
columns: columns.to_vec(),
212+
index_type: None,
213+
constraint_name: None,
214+
};
215+
self.add_key(target_col_new_key);
216+
}
199217
}
200218

201219
/// Returns the primary key of the table, if one exists.
@@ -213,6 +231,18 @@ impl CreateTableBody {
213231
.iter()
214232
.find(|key| matches!(key, TableKey::UniqueKey { .. }))
215233
}
234+
235+
/// Check if there is an index of type Key covering a list of columns
236+
pub fn has_key_on_columns(&self, columns: &[Column]) -> bool {
237+
self.keys.as_ref().unwrap_or(&Vec::new()).iter().any(|key| {
238+
key.is_key()
239+
&& key.get_columns().iter().all(|column| {
240+
columns
241+
.iter()
242+
.any(|c| c.name.as_str() == column.name.as_str())
243+
})
244+
})
245+
}
216246
}
217247

218248
impl DialectDisplay for CreateTableBody {

0 commit comments

Comments
 (0)