1use std::collections::BTreeSet;
11
12use mz_proto::{ProtoType, RustType, TryFromProtoError};
13use mz_repr::SqlColumnType;
14#[cfg(any(test, feature = "proptest"))]
15use proptest::prelude::any;
16#[cfg(any(test, feature = "proptest"))]
17use proptest_derive::Arbitrary;
18use serde::{Deserialize, Serialize};
19
20use self::proto_my_sql_column_desc::Meta;
21use crate::schema_change::{KeyRef, SchemaChange, SchemaChangeError};
22
23include!(concat!(env!("OUT_DIR"), "/mz_mysql_util.rs"));
24
25#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
26#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
27pub struct MySqlTableDesc {
28 pub schema_name: String,
30 pub name: String,
32 #[cfg_attr(
38 any(test, feature = "proptest"),
39 proptest(strategy = "proptest::collection::vec(any::<MySqlColumnDesc>(), 0..4)")
40 )]
41 pub columns: Vec<MySqlColumnDesc>,
42 #[cfg_attr(
45 any(test, feature = "proptest"),
46 proptest(strategy = "proptest::collection::btree_set(any::<MySqlKeyDesc>(), 0..4)")
47 )]
48 pub keys: BTreeSet<MySqlKeyDesc>,
49}
50
51impl RustType<ProtoMySqlTableDesc> for MySqlTableDesc {
52 fn into_proto(&self) -> ProtoMySqlTableDesc {
53 ProtoMySqlTableDesc {
54 schema_name: self.schema_name.clone(),
55 name: self.name.clone(),
56 columns: self.columns.iter().map(|c| c.into_proto()).collect(),
57 keys: self.keys.iter().map(|c| c.into_proto()).collect(),
58 }
59 }
60
61 fn from_proto(proto: ProtoMySqlTableDesc) -> Result<Self, TryFromProtoError> {
62 Ok(Self {
63 schema_name: proto.schema_name,
64 name: proto.name,
65 columns: proto
66 .columns
67 .into_iter()
68 .map(MySqlColumnDesc::from_proto)
69 .collect::<Result<_, _>>()?,
70 keys: proto
71 .keys
72 .into_iter()
73 .map(MySqlKeyDesc::from_proto)
74 .collect::<Result<_, _>>()?,
75 })
76 }
77}
78
79impl MySqlTableDesc {
80 pub fn determine_compatibility(
93 &self,
94 other: &MySqlTableDesc,
95 binlog_full_metadata: bool,
96 ) -> Result<(), SchemaChangeError> {
97 if self == other {
98 return Ok(());
99 }
100
101 if self.schema_name != other.schema_name || self.name != other.name {
102 return Err(self.build_schema_change_error(SchemaChange::TableRenamed));
103 }
104
105 for (i, self_column) in self.columns.iter().enumerate() {
112 if self_column.column_type.is_none() {
113 continue;
115 }
116 let wire_idx = if !binlog_full_metadata {
117 (i < other.columns.len()).then_some(i)
119 } else {
120 other
123 .columns
124 .iter()
125 .position(|oc| oc.name.as_str() == self_column.name.as_str())
126 };
127
128 let dropped_column_error = || {
129 self.build_schema_change_error(SchemaChange::ColumnDropped {
130 column: self_column.name.clone(),
131 })
132 };
133 let wire_idx = match wire_idx {
134 Some(idx) => idx,
135 None => return Err(dropped_column_error()),
136 };
137 let other_column = other
138 .columns
139 .get(wire_idx)
140 .ok_or_else(dropped_column_error)?;
141 if let Some(change) = self_column.get_incompatible_schema_change(other_column) {
142 return Err(self.build_schema_change_error(change));
143 }
144 }
145 if let Some(key) = self.keys.difference(&other.keys).next() {
154 let still_exists = other.keys.iter().any(|k| k.name == key.name);
155 let key = KeyRef {
156 name: key.name.clone(),
157 is_primary: key.is_primary,
158 columns: key.columns.clone(),
159 };
160 let change = if still_exists {
161 SchemaChange::KeyAltered { key }
162 } else {
163 SchemaChange::KeyDropped { key }
164 };
165 return Err(self.build_schema_change_error(change));
166 }
167
168 Ok(())
169 }
170
171 pub fn build_schema_change_error(&self, change: SchemaChange) -> SchemaChangeError {
172 SchemaChangeError {
173 schema_name: self.schema_name.clone(),
174 name: self.name.clone(),
175 change,
176 }
177 }
178}
179
180#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
181#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
182pub struct MySqlColumnMetaEnum {
183 #[cfg_attr(
184 any(test, feature = "proptest"),
185 proptest(strategy = "proptest::collection::vec(any::<String>(), 0..3)")
186 )]
187 pub values: Vec<String>,
188}
189
190impl RustType<ProtoMySqlColumnMetaEnum> for MySqlColumnMetaEnum {
191 fn into_proto(&self) -> ProtoMySqlColumnMetaEnum {
192 ProtoMySqlColumnMetaEnum {
193 values: self.values.clone(),
194 }
195 }
196
197 fn from_proto(proto: ProtoMySqlColumnMetaEnum) -> Result<Self, TryFromProtoError> {
198 Ok(Self {
199 values: proto.values,
200 })
201 }
202}
203
204trait IsCompatible {
205 fn is_compatible(&self, other: &Self) -> bool;
206}
207
208#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
209#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
210pub enum MySqlColumnMeta {
211 Enum(MySqlColumnMetaEnum),
213 Json,
215 Year,
217 Date,
219 Timestamp(u32),
221 Bit(u32),
223}
224
225impl IsCompatible for Option<MySqlColumnMeta> {
226 fn is_compatible(&self, other: &Option<MySqlColumnMeta>) -> bool {
227 match (self, other) {
228 (None, None) => true,
229 (Some(_), None) => false,
230 (None, Some(_)) => false,
231 (Some(MySqlColumnMeta::Enum(self_enum)), Some(MySqlColumnMeta::Enum(other_enum))) => {
232 match other_enum.values.get(0..self_enum.values.len()) {
235 Some(prefix) => self_enum.values == prefix,
236 None => false,
237 }
238 }
239 (Some(MySqlColumnMeta::Json), Some(MySqlColumnMeta::Json)) => true,
240 (Some(MySqlColumnMeta::Year), Some(MySqlColumnMeta::Year)) => true,
241 (Some(MySqlColumnMeta::Date), Some(MySqlColumnMeta::Date)) => true,
242 (
244 Some(MySqlColumnMeta::Timestamp(precision)),
245 Some(MySqlColumnMeta::Timestamp(other_precision)),
246 ) => precision <= other_precision,
247 (Some(MySqlColumnMeta::Bit(_)), Some(MySqlColumnMeta::Bit(_))) => true,
250 _ => false,
251 }
252 }
253}
254
255#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
256#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
257pub struct MySqlColumnDesc {
258 pub name: String,
260 pub column_type: Option<SqlColumnType>,
263 pub meta: Option<MySqlColumnMeta>,
265}
266
267impl RustType<ProtoMySqlColumnDesc> for MySqlColumnDesc {
268 fn into_proto(&self) -> ProtoMySqlColumnDesc {
269 ProtoMySqlColumnDesc {
270 name: self.name.clone(),
271 column_type: self.column_type.into_proto(),
272 meta: self.meta.as_ref().and_then(|meta| match meta {
273 MySqlColumnMeta::Enum(e) => Some(Meta::Enum(e.into_proto())),
274 MySqlColumnMeta::Json => Some(Meta::Json(ProtoMySqlColumnMetaJson {})),
275 MySqlColumnMeta::Year => Some(Meta::Year(ProtoMySqlColumnMetaYear {})),
276 MySqlColumnMeta::Date => Some(Meta::Date(ProtoMySqlColumnMetaDate {})),
277 MySqlColumnMeta::Timestamp(precision) => {
278 Some(Meta::Timestamp(ProtoMySqlColumnMetaTimestamp {
279 precision: *precision,
280 }))
281 }
282 MySqlColumnMeta::Bit(precision) => Some(Meta::Bit(ProtoMySqlColumnMetaBit {
283 precision: *precision,
284 })),
285 }),
286 }
287 }
288
289 fn from_proto(proto: ProtoMySqlColumnDesc) -> Result<Self, TryFromProtoError> {
290 Ok(Self {
291 name: proto.name,
292 column_type: proto.column_type.into_rust()?,
293 meta: proto
294 .meta
295 .and_then(|meta| match meta {
296 Meta::Enum(e) => Some(
297 MySqlColumnMetaEnum::from_proto(e)
298 .and_then(|e| Ok(MySqlColumnMeta::Enum(e))),
299 ),
300 Meta::Json(_) => Some(Ok(MySqlColumnMeta::Json)),
301 Meta::Year(_) => Some(Ok(MySqlColumnMeta::Year)),
302 Meta::Date(_) => Some(Ok(MySqlColumnMeta::Date)),
303 Meta::Timestamp(e) => Some(Ok(MySqlColumnMeta::Timestamp(e.precision))),
304 Meta::Bit(e) => Some(Ok(MySqlColumnMeta::Bit(e.precision))),
305 })
306 .transpose()?,
307 })
308 }
309}
310
311impl MySqlColumnDesc {
312 fn get_incompatible_schema_change(&self, other: &MySqlColumnDesc) -> Option<SchemaChange> {
315 let column = self.name.clone();
316 if self.name != other.name {
317 return Some(SchemaChange::ColumnDropped { column });
318 }
319 if !self.meta.is_compatible(&other.meta) {
320 return Some(SchemaChange::ColumnTypeChanged { column });
321 }
322 match (&self.column_type, &other.column_type) {
323 (None, None) => None,
324 (Some(self_type), Some(other_type)) => {
325 if self_type.scalar_type != other_type.scalar_type {
326 return Some(SchemaChange::ColumnTypeChanged { column });
327 }
328 if !self_type.nullable && other_type.nullable {
331 return Some(SchemaChange::NotNullDropped { column });
332 }
333 None
334 }
335 (Some(_), None) | (None, Some(_)) => Some(SchemaChange::ColumnTypeChanged { column }),
336 }
337 }
338}
339
340#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Ord, PartialOrd)]
341#[cfg_attr(any(test, feature = "proptest"), derive(Arbitrary))]
342pub struct MySqlKeyDesc {
343 pub name: String,
345 pub is_primary: bool,
347 #[cfg_attr(
349 any(test, feature = "proptest"),
350 proptest(strategy = "proptest::collection::vec(any::<String>(), 0..4)")
351 )]
352 pub columns: Vec<String>,
353}
354
355impl RustType<ProtoMySqlKeyDesc> for MySqlKeyDesc {
356 fn into_proto(&self) -> ProtoMySqlKeyDesc {
357 ProtoMySqlKeyDesc {
358 name: self.name.clone(),
359 is_primary: self.is_primary.clone(),
360 columns: self.columns.clone(),
361 }
362 }
363
364 fn from_proto(proto: ProtoMySqlKeyDesc) -> Result<Self, TryFromProtoError> {
365 Ok(Self {
366 name: proto.name,
367 is_primary: proto.is_primary,
368 columns: proto.columns,
369 })
370 }
371}