use std::collections::HashMap;
const MERGE_ENGINE_OPTION: &str = "merge-engine";
const PARTIAL_UPDATE_ENGINE: &str = "partial-update";
const IGNORE_DELETE_OPTION: &str = "ignore-delete";
const IGNORE_DELETE_SUFFIX: &str = ".ignore-delete";
const PARTIAL_UPDATE_REMOVE_RECORD_ON_DELETE_OPTION: &str =
"partial-update.remove-record-on-delete";
const PARTIAL_UPDATE_REMOVE_RECORD_ON_SEQUENCE_GROUP_OPTION: &str =
"partial-update.remove-record-on-sequence-group";
const FIELDS_DEFAULT_AGG_FUNCTION_OPTION: &str = "fields.default-aggregate-function";
const FIELDS_PREFIX: &str = "fields.";
const SEQUENCE_GROUP_SUFFIX: &str = ".sequence-group";
const AGGREGATION_FUNCTION_SUFFIX: &str = ".aggregate-function";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PartialUpdateMode {
Basic,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct PartialUpdateConfig<'a> {
options: &'a HashMap<String, String>,
}
impl<'a> PartialUpdateConfig<'a> {
pub(crate) fn new(options: &'a HashMap<String, String>) -> Self {
Self { options }
}
pub(crate) fn is_enabled(&self) -> bool {
self.options
.get(MERGE_ENGINE_OPTION)
.is_some_and(|value| value.eq_ignore_ascii_case(PARTIAL_UPDATE_ENGINE))
}
pub(crate) fn validate_create_mode(
&self,
has_primary_keys: bool,
) -> crate::Result<Option<PartialUpdateMode>> {
match self.validated_mode(has_primary_keys) {
Ok(mode) => Ok(mode),
Err(unsupported_options) => Err(crate::Error::ConfigInvalid {
message: format!(
"merge-engine=partial-update only supports the basic mode in this build; unsupported options: {}",
unsupported_options.join(", ")
),
}),
}
}
pub(crate) fn validate_runtime_mode(
&self,
has_primary_keys: bool,
table_name: &str,
) -> crate::Result<Option<PartialUpdateMode>> {
match self.validated_mode(has_primary_keys) {
Ok(mode) => Ok(mode),
Err(unsupported_options) => Err(crate::Error::Unsupported {
message: format!(
"Table '{table_name}' uses merge-engine=partial-update options not supported by this build: {}",
unsupported_options.join(", ")
),
}),
}
}
fn validated_mode(
&self,
has_primary_keys: bool,
) -> std::result::Result<Option<PartialUpdateMode>, Vec<String>> {
if !has_primary_keys || !self.is_enabled() {
return Ok(None);
}
let unsupported_options = self.unsupported_option_keys();
if !unsupported_options.is_empty() {
return Err(unsupported_options);
}
Ok(Some(PartialUpdateMode::Basic))
}
fn unsupported_option_keys(&self) -> Vec<String> {
let mut keys: Vec<String> = self
.options
.keys()
.filter(|key| is_unsupported_partial_update_option(key))
.cloned()
.collect();
keys.sort();
keys
}
}
fn is_unsupported_partial_update_option(key: &str) -> bool {
key == IGNORE_DELETE_OPTION
|| key.ends_with(IGNORE_DELETE_SUFFIX)
|| key == PARTIAL_UPDATE_REMOVE_RECORD_ON_DELETE_OPTION
|| key == PARTIAL_UPDATE_REMOVE_RECORD_ON_SEQUENCE_GROUP_OPTION
|| key == FIELDS_DEFAULT_AGG_FUNCTION_OPTION
|| is_fields_option_with_suffix(key, SEQUENCE_GROUP_SUFFIX)
|| is_fields_option_with_suffix(key, AGGREGATION_FUNCTION_SUFFIX)
}
fn is_fields_option_with_suffix(key: &str, suffix: &str) -> bool {
key.starts_with(FIELDS_PREFIX) && key.ends_with(suffix)
}
#[cfg(test)]
mod tests {
use super::*;
fn partial_update_options(extra: &[(&str, &str)]) -> HashMap<String, String> {
let mut options = HashMap::from([(
MERGE_ENGINE_OPTION.to_string(),
PARTIAL_UPDATE_ENGINE.to_string(),
)]);
options.extend(
extra
.iter()
.map(|(key, value)| ((*key).to_string(), (*value).to_string())),
);
options
}
#[test]
fn test_validate_create_mode_accepts_basic_pk_partial_update() {
let options = partial_update_options(&[]);
let config = PartialUpdateConfig::new(&options);
assert_eq!(
config.validate_create_mode(true).unwrap(),
Some(PartialUpdateMode::Basic)
);
}
#[test]
fn test_validate_create_mode_ignores_non_pk_tables() {
let options = partial_update_options(&[(IGNORE_DELETE_OPTION, "true")]);
let config = PartialUpdateConfig::new(&options);
assert_eq!(config.validate_create_mode(false).unwrap(), None);
}
#[test]
fn test_validate_create_mode_rejects_unsupported_partial_update_options() {
for key in [
IGNORE_DELETE_OPTION,
"partial-update.ignore-delete",
PARTIAL_UPDATE_REMOVE_RECORD_ON_DELETE_OPTION,
PARTIAL_UPDATE_REMOVE_RECORD_ON_SEQUENCE_GROUP_OPTION,
"fields.price.sequence-group",
"fields.price.aggregate-function",
FIELDS_DEFAULT_AGG_FUNCTION_OPTION,
] {
let options = partial_update_options(&[(key, "value")]);
let config = PartialUpdateConfig::new(&options);
let err = config.validate_create_mode(true).unwrap_err();
assert!(
matches!(err, crate::Error::ConfigInvalid { ref message } if message.contains(key)),
"expected create-time rejection to mention '{key}', got {err:?}"
);
}
}
#[test]
fn test_validate_runtime_mode_rejects_unsupported_partial_update_options() {
let options =
partial_update_options(&[("fields.price.aggregate-function", "last_non_null")]);
let config = PartialUpdateConfig::new(&options);
let err = config.validate_runtime_mode(true, "default.t").unwrap_err();
assert!(
matches!(err, crate::Error::Unsupported { ref message } if message.contains("fields.price.aggregate-function")),
"expected runtime rejection to mention the unsupported option, got {err:?}"
);
}
}