feat(manticore): add Manticore Search support
Add Manticore Search table data editing, structure editing, metadata handling, and SQL completion support.
This commit is contained in:
1 parent
93d2e7dc18
commit
f385386284
34 files changed
+1250
-130
No files matched your search
@@ -252,7 +252,10 @@ pub fn build_data_grid_copy_update_statements(options: DataGridCopyUpdateStateme
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" AND ");
|
||||
statements.push(format!("UPDATE {table} SET {sets} WHERE {where_clause};"));
|
||||
statements.push(data_grid_statement(
|
||||
options.database_type,
|
||||
format!("UPDATE {table} SET {sets} WHERE {where_clause}"),
|
||||
));
|
||||
}
|
||||
statements
|
||||
}
|
||||
@@ -528,7 +531,10 @@ fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Ve
|
||||
row,
|
||||
column_info,
|
||||
);
|
||||
statements.push(format!("UPDATE {table} SET {sets} WHERE {where_clause};"));
|
||||
statements.push(data_grid_statement(
|
||||
options.database_type,
|
||||
format!("UPDATE {table} SET {sets} WHERE {where_clause}"),
|
||||
));
|
||||
}
|
||||
|
||||
for row_index in &options.deleted_rows {
|
||||
@@ -542,7 +548,8 @@ fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Ve
|
||||
row,
|
||||
column_info,
|
||||
);
|
||||
statements.push(format!("DELETE FROM {table} WHERE {where_clause};"));
|
||||
statements
|
||||
.push(data_grid_statement(options.database_type, format!("DELETE FROM {table} WHERE {where_clause}")));
|
||||
}
|
||||
|
||||
for row in &options.new_rows {
|
||||
@@ -568,7 +575,10 @@ fn build_data_grid_save_statements(options: &DataGridSaveStatementOptions) -> Ve
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
statements.push(format!("INSERT INTO {table} ({columns}) VALUES ({values});"));
|
||||
statements.push(data_grid_statement(
|
||||
options.database_type,
|
||||
format!("INSERT INTO {table} ({columns}) VALUES ({values})"),
|
||||
));
|
||||
}
|
||||
|
||||
statements
|
||||
@@ -591,7 +601,8 @@ fn build_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) -
|
||||
for row in &options.new_rows {
|
||||
let where_clause = build_row_where(options.database_type, &save_columns, row, column_info);
|
||||
if !where_clause.is_empty() {
|
||||
statements.push(format!("DELETE FROM {table} WHERE {where_clause};"));
|
||||
statements
|
||||
.push(data_grid_statement(options.database_type, format!("DELETE FROM {table} WHERE {where_clause}")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,7 +628,10 @@ fn build_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) -
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
statements.push(format!("INSERT INTO {table} ({columns}) VALUES ({values});"));
|
||||
statements.push(data_grid_statement(
|
||||
options.database_type,
|
||||
format!("INSERT INTO {table} ({columns}) VALUES ({values})"),
|
||||
));
|
||||
}
|
||||
|
||||
for (row_index, changes) in &options.dirty_rows {
|
||||
@@ -668,9 +682,12 @@ fn build_data_grid_rollback_statements(options: &DataGridSaveStatementOptions) -
|
||||
predicates.extend(writable_changes.iter().map(|((_, value), column)| {
|
||||
build_column_predicate(options.database_type, column, value, column_info_for(column_info, column))
|
||||
}));
|
||||
statements.push(format!(
|
||||
"UPDATE {table} SET {sets} WHERE {};",
|
||||
predicates.into_iter().filter(|part| !part.is_empty()).collect::<Vec<_>>().join(" AND ")
|
||||
statements.push(data_grid_statement(
|
||||
options.database_type,
|
||||
format!(
|
||||
"UPDATE {table} SET {sets} WHERE {}",
|
||||
predicates.into_iter().filter(|part| !part.is_empty()).collect::<Vec<_>>().join(" AND ")
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -749,6 +766,11 @@ pub fn format_grid_sql_literal(
|
||||
return format_pg_array_sql_literal(arr);
|
||||
}
|
||||
let text = value.as_str().map_or_else(|| value.to_string(), ToString::to_string);
|
||||
if database_type == Some(DatabaseType::ManticoreSearch) {
|
||||
if let Some(typed_value) = manticore_typed_attribute_value(&text, column_info) {
|
||||
return format_grid_sql_literal(&typed_value, database_type, column_info);
|
||||
}
|
||||
}
|
||||
if text.is_empty() {
|
||||
return if database_type == Some(DatabaseType::SqlServer) { "N''" } else { "''" }.to_string();
|
||||
}
|
||||
@@ -779,6 +801,20 @@ fn is_bit_column_type(data_type: &str) -> bool {
|
||||
lower.split(|ch: char| !ch.is_ascii_alphanumeric()).any(|token| token == "bit")
|
||||
}
|
||||
|
||||
fn manticore_typed_attribute_value(text: &str, column_info: Option<&DataGridColumnInfo>) -> Option<Value> {
|
||||
let data_type = column_info?.data_type.to_ascii_lowercase();
|
||||
if is_boolean_type(&data_type) && text.eq_ignore_ascii_case("true") {
|
||||
return Some(Value::Bool(true));
|
||||
}
|
||||
if is_boolean_type(&data_type) && text.eq_ignore_ascii_case("false") {
|
||||
return Some(Value::Bool(false));
|
||||
}
|
||||
if is_numeric_type(&data_type) && is_numeric_literal(text) {
|
||||
return text.parse::<serde_json::Number>().ok().map(Value::Number);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn format_mysql_bit_literal_text(text: &str) -> Option<String> {
|
||||
let trimmed = text.trim();
|
||||
if trimmed.eq_ignore_ascii_case("true") {
|
||||
@@ -1032,6 +1068,14 @@ fn build_column_predicate(
|
||||
}
|
||||
}
|
||||
|
||||
fn data_grid_statement(database_type: Option<DatabaseType>, sql: String) -> String {
|
||||
if database_type == Some(DatabaseType::ManticoreSearch) {
|
||||
sql
|
||||
} else {
|
||||
format!("{sql};")
|
||||
}
|
||||
}
|
||||
|
||||
fn uses_mysql_binary_text_predicate(
|
||||
database_type: Option<DatabaseType>,
|
||||
value: &Value,
|
||||
@@ -1250,6 +1294,7 @@ fn uses_keyless_row_predicate(database_type: Option<DatabaseType>) -> bool {
|
||||
database_type,
|
||||
Some(
|
||||
DatabaseType::Mysql
|
||||
| DatabaseType::ManticoreSearch
|
||||
| DatabaseType::Postgres
|
||||
| DatabaseType::Sqlite
|
||||
| DatabaseType::DuckDb
|
||||
@@ -1568,6 +1613,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepares_manticore_save_statements_without_trailing_semicolons() {
|
||||
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
table_meta: DataGridTableMeta {
|
||||
schema: None,
|
||||
table_name: "rt_products".to_string(),
|
||||
primary_keys: vec![],
|
||||
columns: Some(vec![column("id", "bigint", false, None), column("title", "text", true, None)]),
|
||||
},
|
||||
columns: vec!["id".to_string(), "title".to_string()],
|
||||
source_columns: None,
|
||||
rows: vec![vec![json!("1"), json!("old")], vec![json!("2"), json!("deleted")]],
|
||||
dirty_rows: vec![(0, vec![(1, json!("new"))])],
|
||||
deleted_rows: vec![1],
|
||||
new_rows: vec![vec![json!("3"), json!("inserted")]],
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"UPDATE `rt_products` SET `title` = 'new' WHERE `id` = 1 AND `title` = 'old'",
|
||||
"DELETE FROM `rt_products` WHERE `id` = 2 AND `title` = 'deleted'",
|
||||
"INSERT INTO `rt_products` (`id`, `title`) VALUES (3, 'inserted')",
|
||||
]
|
||||
);
|
||||
assert!(result.rollback_statements.iter().all(|statement| !statement.ends_with(';')));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_duplicate_inserted_primary_keys() {
|
||||
let result = prepare_data_grid_save(DataGridSaveStatementOptions {
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
use mysql_async::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::types::{ColumnInfo, IndexInfo, ObjectInfo};
|
||||
|
||||
use super::mysql::{self, MySqlPool};
|
||||
|
||||
fn quote_identifier(value: &str) -> String {
|
||||
format!("`{}`", value.replace('`', "``"))
|
||||
}
|
||||
|
||||
fn row_get<T, I>(row: &mysql_async::Row, index: I) -> Option<T>
|
||||
where
|
||||
T: mysql_async::prelude::FromValue,
|
||||
I: mysql_async::prelude::ColumnIndex,
|
||||
{
|
||||
row.get_opt::<T, I>(index).and_then(|result| result.ok())
|
||||
}
|
||||
|
||||
fn get_str_by_name(row: &mysql_async::Row, name: &str) -> String {
|
||||
row_get::<String, _>(row, name)
|
||||
.or_else(|| row_get::<Vec<u8>, _>(row, name).map(|bytes| String::from_utf8_lossy(&bytes).to_string()))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_opt_str(row: &mysql_async::Row, name: &str) -> Option<String> {
|
||||
row_get::<String, _>(row, name)
|
||||
.or_else(|| row_get::<Vec<u8>, _>(row, name).map(|bytes| String::from_utf8_lossy(&bytes).to_string()))
|
||||
}
|
||||
|
||||
fn preferred_metadata(primary: Option<String>, fallback: Option<String>) -> Option<String> {
|
||||
primary.filter(|value| !value.trim().is_empty()).or_else(|| fallback.filter(|value| !value.trim().is_empty()))
|
||||
}
|
||||
|
||||
pub async fn list_objects(pool: &MySqlPool, database: &str) -> Result<Vec<ObjectInfo>, String> {
|
||||
let (tables, functions) = tokio::join!(mysql::list_tables_show(pool, database), list_udf_objects(pool, database));
|
||||
let mut objects: Vec<ObjectInfo> = tables?
|
||||
.into_iter()
|
||||
.map(|table| ObjectInfo {
|
||||
name: table.name,
|
||||
object_type: "TABLE".to_string(),
|
||||
schema: Some(database.to_string()),
|
||||
comment: table.comment,
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
parent_schema: table.parent_schema,
|
||||
parent_name: table.parent_name,
|
||||
})
|
||||
.collect();
|
||||
|
||||
match functions {
|
||||
Ok(functions) => objects.extend(functions),
|
||||
Err(err) => log::warn!("Skipping UDFs for Manticore Search database `{}` in object browser: {}", database, err),
|
||||
}
|
||||
|
||||
Ok(objects)
|
||||
}
|
||||
|
||||
async fn list_udf_objects(pool: &MySqlPool, database: &str) -> Result<Vec<ObjectInfo>, String> {
|
||||
let mut conn = pool.get_conn().await.map_err(|err| err.to_string())?;
|
||||
let result = conn.query_iter("SHOW PLUGINS").await.map_err(|err| err.to_string())?;
|
||||
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|err| err.to_string())?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
.filter_map(|row| {
|
||||
plugin_object(
|
||||
&get_str_by_name(row, "Type"),
|
||||
&get_str_by_name(row, "Name"),
|
||||
get_opt_str(row, "Library").as_deref(),
|
||||
get_opt_str(row, "Extra").as_deref(),
|
||||
database,
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn plugin_object(
|
||||
plugin_type: &str,
|
||||
name: &str,
|
||||
library: Option<&str>,
|
||||
extra: Option<&str>,
|
||||
database: &str,
|
||||
) -> Option<ObjectInfo> {
|
||||
if !plugin_type.eq_ignore_ascii_case("udf") {
|
||||
return None;
|
||||
}
|
||||
let name = name.trim();
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut comment_parts = Vec::new();
|
||||
if let Some(library) = library.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
comment_parts.push(format!("SONAME {library}"));
|
||||
}
|
||||
if let Some(return_type) = extra.map(str::trim).filter(|value| !value.is_empty()) {
|
||||
comment_parts.push(format!("RETURNS {return_type}"));
|
||||
}
|
||||
Some(ObjectInfo {
|
||||
name: name.to_string(),
|
||||
object_type: "FUNCTION".to_string(),
|
||||
schema: Some(database.to_string()),
|
||||
comment: if comment_parts.is_empty() { None } else { Some(comment_parts.join(", ")) },
|
||||
created_at: None,
|
||||
updated_at: None,
|
||||
parent_schema: None,
|
||||
parent_name: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_columns(pool: &MySqlPool, database: &str, table: &str) -> Result<Vec<ColumnInfo>, String> {
|
||||
let mut columns = mysql::get_columns_show(pool, database, table).await?;
|
||||
let properties = column_properties(pool, database, table).await.unwrap_or_default();
|
||||
for column in &mut columns {
|
||||
if let Some(property) = properties.get(&column.name.to_lowercase()) {
|
||||
column.extra = preferred_metadata(column.extra.take(), Some(property.clone()));
|
||||
}
|
||||
}
|
||||
Ok(columns)
|
||||
}
|
||||
|
||||
async fn column_properties(pool: &MySqlPool, database: &str, table: &str) -> Result<HashMap<String, String>, String> {
|
||||
let sql = show_columns_sql(database, table, true);
|
||||
let mut conn = pool.get_conn().await.map_err(|err| err.to_string())?;
|
||||
let rows: Vec<mysql_async::Row> = match conn.query_iter(&sql).await {
|
||||
Ok(result) => result.collect_and_drop().await.map_err(|err| err.to_string())?,
|
||||
Err(_) => {
|
||||
let sql = show_columns_sql(database, table, false);
|
||||
let result = conn.query_iter(&sql).await.map_err(|err| err.to_string())?;
|
||||
result.collect_and_drop().await.map_err(|err| err.to_string())?
|
||||
}
|
||||
};
|
||||
Ok(rows
|
||||
.iter()
|
||||
.filter_map(|row| {
|
||||
let name = get_str_by_name(row, "Field").trim().to_string();
|
||||
if name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
get_opt_str(row, "Properties")
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(|properties| (name.to_lowercase(), properties))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn show_columns_sql(database: &str, table: &str, full: bool) -> String {
|
||||
let prefix = if full { "SHOW FULL COLUMNS FROM" } else { "SHOW COLUMNS FROM" };
|
||||
if database.trim().is_empty() {
|
||||
format!("{prefix} {}", quote_identifier(table))
|
||||
} else {
|
||||
format!("{prefix} {}.{}", quote_identifier(database), quote_identifier(table))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_indexes(pool: &MySqlPool, table: &str) -> Result<Vec<IndexInfo>, String> {
|
||||
let sql = list_indexes_sql(table);
|
||||
let mut conn = pool.get_conn().await.map_err(|err| err.to_string())?;
|
||||
let result = conn.query_iter(&sql).await.map_err(|err| err.to_string())?;
|
||||
let rows: Vec<mysql_async::Row> = result.collect_and_drop().await.map_err(|err| err.to_string())?;
|
||||
|
||||
Ok(rows.iter().map(index_info_from_row).collect())
|
||||
}
|
||||
|
||||
fn list_indexes_sql(table: &str) -> String {
|
||||
format!("SHOW TABLE INDEXES FROM {}", quote_identifier(table))
|
||||
}
|
||||
|
||||
fn index_info_from_row(row: &mysql_async::Row) -> IndexInfo {
|
||||
let name = get_str_by_name(row, "Name");
|
||||
let enabled = get_str_by_name(row, "Enabled");
|
||||
let percent = get_str_by_name(row, "Percent");
|
||||
let comment_parts: Vec<String> = [
|
||||
(!enabled.is_empty()).then(|| format!("Enabled: {enabled}")),
|
||||
(!percent.is_empty()).then(|| format!("Percent: {percent}")),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
IndexInfo {
|
||||
name: name.clone(),
|
||||
columns: vec![name],
|
||||
is_unique: false,
|
||||
is_primary: false,
|
||||
filter: None,
|
||||
index_type: Some(get_str_by_name(row, "Type")),
|
||||
included_columns: None,
|
||||
comment: (!comment_parts.is_empty()).then(|| comment_parts.join(", ")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn show_metadata_uses_unqualified_table_names_when_database_is_empty() {
|
||||
assert_eq!(show_columns_sql("", "idx", true), "SHOW FULL COLUMNS FROM `idx`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_extra_falls_back_to_properties() {
|
||||
assert_eq!(
|
||||
preferred_metadata(Some("auto_increment".to_string()), Some("indexed attribute".to_string())),
|
||||
Some("auto_increment".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
preferred_metadata(Some(" ".to_string()), Some("indexed attribute".to_string())),
|
||||
Some("indexed attribute".to_string())
|
||||
);
|
||||
assert_eq!(preferred_metadata(None, Some(" ".to_string())), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexes_sql_uses_show_table_indexes() {
|
||||
assert_eq!(list_indexes_sql("materials"), "SHOW TABLE INDEXES FROM `materials`");
|
||||
assert_eq!(list_indexes_sql("odd`name"), "SHOW TABLE INDEXES FROM `odd``name`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn udf_plugin_maps_to_function_object() {
|
||||
let object = plugin_object("udf", "sequence", Some("udfexample.dll"), Some("INT"), "app").expect("udf plugin");
|
||||
|
||||
assert_eq!(object.name, "sequence");
|
||||
assert_eq!(object.object_type, "FUNCTION");
|
||||
assert_eq!(object.schema.as_deref(), Some("app"));
|
||||
assert_eq!(object.comment.as_deref(), Some("SONAME udfexample.dll, RETURNS INT"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_udf_plugins_are_not_sidebar_functions() {
|
||||
assert!(plugin_object("ranker", "bm25", Some("ranker.so"), None, "app").is_none());
|
||||
assert!(plugin_object("udf", " ", Some("udf.so"), Some("INT"), "app").is_none());
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod elasticsearch_driver;
|
||||
pub mod elasticsearch_sql;
|
||||
pub mod file_validator;
|
||||
pub mod influxdb_driver;
|
||||
pub mod manticoresearch;
|
||||
pub mod mongo_driver;
|
||||
pub mod mysql;
|
||||
pub mod ob_oracle;
|
||||
|
||||
@@ -698,6 +698,20 @@ mod tests {
|
||||
assert_eq!(filtered.into_iter().map(|database| database.name).collect::<Vec<_>>(), vec!["Manticore"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_show_metadata_uses_unqualified_table_names() {
|
||||
let config = test_connection_config(DatabaseType::ManticoreSearch);
|
||||
|
||||
assert_eq!(super::mysql_show_metadata_database_for_config(Some(&config), "Manticore"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doris_show_metadata_keeps_database_qualifier() {
|
||||
let config = test_connection_config(DatabaseType::Doris);
|
||||
|
||||
assert_eq!(super::mysql_show_metadata_database_for_config(Some(&config), "analytics"), "analytics");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn doris_database_list_keeps_system_databases() {
|
||||
let databases = vec![test_database_info("information_schema"), test_database_info("analytics")];
|
||||
@@ -922,6 +936,8 @@ async fn list_objects_once(
|
||||
// Note: mysql and ob_oracle take different second args (database vs schema)
|
||||
if *mode == MysqlMode::OceanBaseOracle {
|
||||
db::ob_oracle::list_objects(p, schema).await
|
||||
} else if db_config.as_ref().is_some_and(is_manticoresearch_config) {
|
||||
db::manticoresearch::list_objects(p, database).await
|
||||
} else if db_config.as_ref().is_some_and(is_doris_family_config) {
|
||||
db::mysql::list_table_objects_show(p, database).await
|
||||
} else {
|
||||
@@ -1215,8 +1231,13 @@ pub async fn get_columns_core(
|
||||
let pool = connections.get(&pool_key).ok_or("Pool not found")?;
|
||||
|
||||
match pool {
|
||||
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(is_manticoresearch_config) => {
|
||||
let metadata_database = mysql_show_metadata_database_for_config(db_config.as_ref(), database);
|
||||
db::manticoresearch::get_columns(p, metadata_database, table).await.map(deduplicate_column_infos)
|
||||
}
|
||||
PoolKind::Mysql(p, _) if db_config.as_ref().is_some_and(is_doris_family_config) => {
|
||||
db::mysql::get_columns_show(p, database, table).await.map(deduplicate_column_infos)
|
||||
let metadata_database = mysql_show_metadata_database_for_config(db_config.as_ref(), database);
|
||||
db::mysql::get_columns_show(p, metadata_database, table).await.map(deduplicate_column_infos)
|
||||
}
|
||||
PoolKind::Mysql(p, mode) => {
|
||||
dispatch_mysql!(p, mode, db::mysql::get_columns, db::ob_oracle::get_columns, database, table)
|
||||
@@ -1282,6 +1303,7 @@ pub async fn list_indexes_core(
|
||||
table: &str,
|
||||
) -> Result<Vec<db::IndexInfo>, String> {
|
||||
let pool_key = state.get_or_create_pool(connection_id, Some(database)).await?;
|
||||
let db_config = connection_config(state, connection_id).await;
|
||||
|
||||
{
|
||||
let connections = state.connections.read().await;
|
||||
@@ -1294,6 +1316,9 @@ pub async fn list_indexes_core(
|
||||
|
||||
match pool {
|
||||
PoolKind::Mysql(p, mode) => {
|
||||
if db_config.as_ref().is_some_and(is_manticoresearch_config) {
|
||||
return db::manticoresearch::list_indexes(p, table).await;
|
||||
}
|
||||
dispatch_mysql!(p, mode, db::mysql::list_indexes, db::ob_oracle::list_indexes, schema, table)
|
||||
}
|
||||
PoolKind::Postgres(p) => db::postgres::list_indexes(p, schema, table).await,
|
||||
@@ -1514,6 +1539,14 @@ fn is_manticoresearch_config(config: &ConnectionConfig) -> bool {
|
||||
|| matches!(config.driver_profile.as_deref(), Some("manticoresearch"))
|
||||
}
|
||||
|
||||
fn mysql_show_metadata_database_for_config<'a>(config: Option<&ConnectionConfig>, database: &'a str) -> &'a str {
|
||||
if config.is_some_and(is_manticoresearch_config) {
|
||||
""
|
||||
} else {
|
||||
database
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_mysql_system_databases_for_config(
|
||||
databases: Vec<db::DatabaseInfo>,
|
||||
config: Option<&ConnectionConfig>,
|
||||
|
||||
@@ -69,6 +69,9 @@ pub(in crate::schema) async fn list_objects(
|
||||
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => {
|
||||
db::ob_oracle::list_objects(p, schema).await.map(Some)
|
||||
}
|
||||
PoolKind::Mysql(p, _) if config.is_some_and(is_manticoresearch_config) => {
|
||||
db::manticoresearch::list_objects(p, database).await.map(Some)
|
||||
}
|
||||
PoolKind::Mysql(p, _) if config.is_some_and(is_doris_family_config) => {
|
||||
db::mysql::list_table_objects_show(p, database).await.map(Some)
|
||||
}
|
||||
@@ -103,8 +106,13 @@ pub(in crate::schema) async fn get_columns(
|
||||
table: &str,
|
||||
) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
match pool {
|
||||
PoolKind::Mysql(p, _) if config.is_some_and(is_manticoresearch_config) => {
|
||||
let metadata_database = mysql_show_metadata_database_for_config(config, database);
|
||||
db::manticoresearch::get_columns(p, metadata_database, table).await
|
||||
}
|
||||
PoolKind::Mysql(p, _) if config.is_some_and(is_doris_family_config) => {
|
||||
db::mysql::get_columns_show(p, database, table).await
|
||||
let metadata_database = mysql_show_metadata_database_for_config(config, database);
|
||||
db::mysql::get_columns_show(p, metadata_database, table).await
|
||||
}
|
||||
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => {
|
||||
db::ob_oracle::get_columns(p, database, table).await
|
||||
@@ -273,6 +281,17 @@ fn is_manticoresearch_config(config: &ConnectionConfig) -> bool {
|
||||
|| matches!(config.driver_profile.as_deref(), Some("manticoresearch"))
|
||||
}
|
||||
|
||||
fn mysql_show_metadata_database_for_config<'a>(
|
||||
config: Option<&ConnectionConfig>,
|
||||
database: &'a str,
|
||||
) -> &'a str {
|
||||
if config.is_some_and(is_manticoresearch_config) {
|
||||
""
|
||||
} else {
|
||||
database
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_mysql_system_databases_for_config(
|
||||
databases: Vec<db::DatabaseInfo>,
|
||||
config: Option<&ConnectionConfig>,
|
||||
|
||||
@@ -4,8 +4,8 @@ use super::comments::build_sqlserver_column_comment_sql;
|
||||
use super::dialect::{capabilities_for, database_label, StructureDialect};
|
||||
use super::types::{EditableStructureColumn, SingleColumnAlterSqlOptions, TableStructureSqlResult};
|
||||
use super::util::{
|
||||
clean, format_default_for_sql, normalize_default, original_comment, original_default, qualified_table, quote_ident,
|
||||
quote_string,
|
||||
clean, format_default_for_sql, is_protected_manticore_id_column, normalize_default, original_comment,
|
||||
original_default, qualified_table, quote_ident, quote_string,
|
||||
};
|
||||
use crate::table_structure_sql::ColumnExtra;
|
||||
|
||||
@@ -30,6 +30,10 @@ pub fn build_single_column_alter_sql(options: SingleColumnAlterSqlOptions) -> Ta
|
||||
warnings.push(format!("Primary key column \"{}\" cannot be dropped from this editor.", original.name));
|
||||
return TableStructureSqlResult { statements, warnings };
|
||||
}
|
||||
if is_protected_manticore_id_column(dialect, &original.name) {
|
||||
warnings.push("Manticore Search id column cannot be dropped from this editor.".to_string());
|
||||
return TableStructureSqlResult { statements, warnings };
|
||||
}
|
||||
statements.push(build_drop_column_sql(dialect, &table, &original.name));
|
||||
return TableStructureSqlResult { statements, warnings };
|
||||
}
|
||||
@@ -95,6 +99,22 @@ fn is_column_extra_empty(extra: &ColumnExtra) -> bool {
|
||||
!extra.auto_increment.unwrap_or(false)
|
||||
&& !extra.on_update_current_timestamp.unwrap_or(false)
|
||||
&& extra.identity.is_none()
|
||||
&& !extra.manticore_indexed.unwrap_or(false)
|
||||
&& !extra.manticore_stored.unwrap_or(false)
|
||||
&& !extra.manticore_attribute.unwrap_or(false)
|
||||
&& !extra.manticore_secondary_index.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn original_manticore_extra_flags(extra: &str) -> (bool, bool, bool, bool) {
|
||||
let lower = extra.to_lowercase();
|
||||
(
|
||||
lower.split_whitespace().any(|token| token == "indexed"),
|
||||
lower.split_whitespace().any(|token| token == "stored"),
|
||||
lower.split_whitespace().any(|token| token == "attribute"),
|
||||
lower.contains("secondary_index='1'")
|
||||
|| lower.contains("secondary_index=\"1\"")
|
||||
|| lower.contains("secondary_index=1"),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn has_column_extra_change(column: &EditableStructureColumn) -> bool {
|
||||
@@ -103,8 +123,18 @@ pub(super) fn has_column_extra_change(column: &EditableStructureColumn) -> bool
|
||||
match (current_extra, original.extra.as_deref()) {
|
||||
// Neither has extra → no change
|
||||
(None, None | Some("")) => false,
|
||||
// Current extra is empty (all None) → no effective extra
|
||||
(Some(curr), _) if is_column_extra_empty(curr) => false,
|
||||
// Current extra is empty (all None) → changed only if the original had effective extra
|
||||
(Some(curr), None | Some("")) if is_column_extra_empty(curr) => false,
|
||||
(Some(curr), Some(orig)) if is_column_extra_empty(curr) => {
|
||||
let (indexed, stored, attribute, secondary_index) = original_manticore_extra_flags(orig);
|
||||
let orig_lower = orig.to_lowercase();
|
||||
orig_lower.contains("auto_increment")
|
||||
|| orig_lower.contains("on update")
|
||||
|| indexed
|
||||
|| stored
|
||||
|| attribute
|
||||
|| secondary_index
|
||||
}
|
||||
// Extra added or removed
|
||||
(Some(_), None | Some("")) => true,
|
||||
(None, Some(_)) => true,
|
||||
@@ -116,8 +146,18 @@ pub(super) fn has_column_extra_change(column: &EditableStructureColumn) -> bool
|
||||
let curr_has_on_update = curr.on_update_current_timestamp.unwrap_or(false);
|
||||
let orig_has_on_update = orig_lower.contains("on update");
|
||||
let curr_has_identity = curr.identity.is_some();
|
||||
let curr_manticore = (
|
||||
curr.manticore_indexed.unwrap_or(false),
|
||||
curr.manticore_stored.unwrap_or(false),
|
||||
curr.manticore_attribute.unwrap_or(false),
|
||||
curr.manticore_secondary_index.unwrap_or(false),
|
||||
);
|
||||
let orig_manticore = original_manticore_extra_flags(orig);
|
||||
// identity is harder to detect in free-form original.extra, so treat it as changed if present
|
||||
curr_has_ai != orig_has_ai || curr_has_on_update != orig_has_on_update || curr_has_identity
|
||||
curr_has_ai != orig_has_ai
|
||||
|| curr_has_on_update != orig_has_on_update
|
||||
|| curr_has_identity
|
||||
|| curr_manticore != orig_manticore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,9 +71,44 @@ pub(super) fn column_data_type(dialect: StructureDialect, column: &EditableStruc
|
||||
if dialect == StructureDialect::ClickHouse {
|
||||
return clickhouse_column_type(column);
|
||||
}
|
||||
if dialect == StructureDialect::ManticoreSearch {
|
||||
return manticore_column_type(column);
|
||||
}
|
||||
normalize_column_data_type(dialect, &column.data_type)
|
||||
}
|
||||
|
||||
fn manticore_column_type(column: &EditableStructureColumn) -> String {
|
||||
let data_type = normalize_column_data_type(StructureDialect::ManticoreSearch, &column.data_type);
|
||||
let normalized = data_type.trim().to_ascii_lowercase();
|
||||
if normalized == "json" {
|
||||
let Some(extra) = column.extra.as_ref() else {
|
||||
return data_type;
|
||||
};
|
||||
if extra.manticore_secondary_index.unwrap_or(false) {
|
||||
return format!("{data_type} secondary_index='1'");
|
||||
}
|
||||
return data_type;
|
||||
}
|
||||
if !matches!(normalized.as_str(), "text" | "string") {
|
||||
return data_type;
|
||||
}
|
||||
|
||||
let Some(extra) = column.extra.as_ref() else {
|
||||
return data_type;
|
||||
};
|
||||
let mut parts = vec![data_type];
|
||||
if extra.manticore_stored.unwrap_or(false) {
|
||||
parts.push("stored".to_string());
|
||||
}
|
||||
if extra.manticore_attribute.unwrap_or(false) {
|
||||
parts.push("attribute".to_string());
|
||||
}
|
||||
if extra.manticore_indexed.unwrap_or(false) {
|
||||
parts.push("indexed".to_string());
|
||||
}
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
pub(super) fn normalize_column_data_type(dialect: StructureDialect, data_type: &str) -> String {
|
||||
let trimmed = data_type.trim();
|
||||
let Some(open_index) = trimmed.find('(') else {
|
||||
|
||||
@@ -9,7 +9,8 @@ use super::comments::build_sqlserver_column_comment_sql;
|
||||
use super::dialect::{capabilities_for, database_label, StructureDialect};
|
||||
use super::types::{EditableStructureColumn, TableStructureSqlOptions};
|
||||
use super::util::{
|
||||
clean, normalize_default, original_comment, original_default, qualified_table, quote_ident, quote_string,
|
||||
clean, is_protected_manticore_id_column, normalize_default, original_comment, original_default, qualified_table,
|
||||
quote_ident, quote_string,
|
||||
};
|
||||
|
||||
pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mut Vec<String>) -> Vec<String> {
|
||||
@@ -34,6 +35,10 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu
|
||||
warnings.push(format!("Primary key column \"{}\" cannot be dropped from this editor.", original.name));
|
||||
continue;
|
||||
}
|
||||
if is_protected_manticore_id_column(dialect, &original.name) {
|
||||
warnings.push("Manticore Search id column cannot be dropped from this editor.".to_string());
|
||||
continue;
|
||||
}
|
||||
statements.push(build_drop_column_sql(dialect, &table, &original.name));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -31,14 +31,17 @@ pub fn build_create_table_sql(options: TableStructureSqlOptions) -> TableStructu
|
||||
for column in &active_columns {
|
||||
let data_type = column_data_type(dialect, column);
|
||||
let mut parts = vec![quote_ident(dialect, &column.name), data_type];
|
||||
if !column.is_nullable && !column.is_primary_key && dialect != StructureDialect::ClickHouse {
|
||||
if !column.is_nullable
|
||||
&& !column.is_primary_key
|
||||
&& !matches!(dialect, StructureDialect::ClickHouse | StructureDialect::ManticoreSearch)
|
||||
{
|
||||
parts.push("NOT NULL".to_string());
|
||||
}
|
||||
if let Some(extra_clause) = column_extra_clause(dialect, column) {
|
||||
parts.push(extra_clause);
|
||||
}
|
||||
let default_value = normalize_default(Some(&column.default_value));
|
||||
if !default_value.is_empty() {
|
||||
if !default_value.is_empty() && dialect != StructureDialect::ManticoreSearch {
|
||||
parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
|
||||
}
|
||||
if let Some(on_update) = column.extra.as_ref().and_then(|e| e.on_update_current_timestamp).filter(|v| *v) {
|
||||
@@ -52,7 +55,10 @@ pub fn build_create_table_sql(options: TableStructureSqlOptions) -> TableStructu
|
||||
column_definitions.push(parts.join(" "));
|
||||
}
|
||||
|
||||
let pk_columns: Vec<_> = active_columns.iter().filter(|column| column.is_primary_key).collect();
|
||||
let pk_columns: Vec<_> = active_columns
|
||||
.iter()
|
||||
.filter(|column| column.is_primary_key && dialect != StructureDialect::ManticoreSearch)
|
||||
.collect();
|
||||
if !pk_columns.is_empty() {
|
||||
let pk_list = pk_columns.iter().map(|column| quote_ident(dialect, &column.name)).collect::<Vec<_>>().join(", ");
|
||||
column_definitions.push(format!("PRIMARY KEY ({pk_list})"));
|
||||
|
||||
@@ -11,6 +11,7 @@ pub(super) enum StructureDialect {
|
||||
Oracle,
|
||||
H2,
|
||||
ClickHouse,
|
||||
ManticoreSearch,
|
||||
Informix,
|
||||
Unsupported,
|
||||
}
|
||||
@@ -204,6 +205,12 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
|
||||
comment: true,
|
||||
..base
|
||||
},
|
||||
Some(DatabaseType::ManticoreSearch) => TableStructureCapabilities {
|
||||
dialect: StructureDialect::ManticoreSearch,
|
||||
add_column: true,
|
||||
drop_column: true,
|
||||
..base
|
||||
},
|
||||
Some(DatabaseType::Informix) => TableStructureCapabilities {
|
||||
dialect: StructureDialect::Informix,
|
||||
add_column: true,
|
||||
@@ -245,6 +252,7 @@ pub(super) fn dialect_label(dialect: StructureDialect) -> String {
|
||||
StructureDialect::Oracle => "oracle",
|
||||
StructureDialect::H2 => "h2",
|
||||
StructureDialect::ClickHouse => "clickhouse",
|
||||
StructureDialect::ManticoreSearch => "manticoresearch",
|
||||
StructureDialect::Informix => "informix",
|
||||
StructureDialect::Unsupported => "this database",
|
||||
}
|
||||
@@ -262,6 +270,7 @@ pub(super) fn database_type_for_dialect(dialect: StructureDialect) -> Option<Dat
|
||||
StructureDialect::Oracle => Some(DatabaseType::Oracle),
|
||||
StructureDialect::H2 => Some(DatabaseType::H2),
|
||||
StructureDialect::ClickHouse => Some(DatabaseType::ClickHouse),
|
||||
StructureDialect::ManticoreSearch => Some(DatabaseType::ManticoreSearch),
|
||||
StructureDialect::Informix => Some(DatabaseType::Informix),
|
||||
StructureDialect::Unsupported => None,
|
||||
}
|
||||
|
||||
@@ -215,6 +215,76 @@ fn mysql_create_index_with_comment() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_builds_create_table_sql_only() {
|
||||
let mut title = column("title");
|
||||
title.data_type = "text".to_string();
|
||||
title.is_nullable = false;
|
||||
let mut views = column("views");
|
||||
views.data_type = "int".to_string();
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![title, views],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(result.statements, vec!["CREATE TABLE `materials` (\n `title` text,\n `views` int\n);"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_builds_add_and_drop_column_sql() {
|
||||
let mut old_code = column("code");
|
||||
old_code.data_type = "string".to_string();
|
||||
old_code.marked_for_drop = true;
|
||||
old_code.original = Some(ColumnInfo {
|
||||
name: "code".to_string(),
|
||||
data_type: "string".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let mut name = column("name");
|
||||
name.data_type = "string".to_string();
|
||||
name.extra =
|
||||
Some(ColumnExtra { manticore_attribute: Some(true), manticore_indexed: Some(true), ..Default::default() });
|
||||
let mut resource = column("resource");
|
||||
resource.data_type = "json".to_string();
|
||||
resource.extra = Some(ColumnExtra { manticore_secondary_index: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![old_code, name, resource],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"ALTER TABLE `materials` DROP COLUMN `code`;",
|
||||
"ALTER TABLE `materials` ADD COLUMN `name` string attribute indexed;",
|
||||
"ALTER TABLE `materials` ADD COLUMN `resource` json secondary_index='1';",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gbase8a_uses_limited_mysql_ddl() {
|
||||
let mut renamed = column("display_email");
|
||||
@@ -278,6 +348,193 @@ fn gbase8a_uses_limited_mysql_ddl() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_does_not_drop_id_column() {
|
||||
let mut id = column("id");
|
||||
id.data_type = "bigint".to_string();
|
||||
id.marked_for_drop = true;
|
||||
id.original = Some(ColumnInfo {
|
||||
name: "id".to_string(),
|
||||
data_type: "bigint".to_string(),
|
||||
is_nullable: false,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![id],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.statements, Vec::<String>::new());
|
||||
assert_eq!(result.warnings, vec!["Manticore Search id column cannot be dropped from this editor."]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_warns_when_existing_column_properties_change() {
|
||||
let mut name = column("name");
|
||||
name.data_type = "string".to_string();
|
||||
name.extra = Some(ColumnExtra {
|
||||
manticore_indexed: Some(true),
|
||||
manticore_stored: Some(true),
|
||||
manticore_attribute: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
name.original = Some(ColumnInfo {
|
||||
name: "name".to_string(),
|
||||
data_type: "string".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let mut resource = column("resource");
|
||||
resource.data_type = "json".to_string();
|
||||
resource.extra = Some(ColumnExtra { manticore_secondary_index: Some(true), ..Default::default() });
|
||||
resource.original = Some(ColumnInfo {
|
||||
name: "resource".to_string(),
|
||||
data_type: "json".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let mut old_resource = column("old_resource");
|
||||
old_resource.data_type = "json".to_string();
|
||||
old_resource.extra = Some(ColumnExtra::default());
|
||||
old_resource.original = Some(ColumnInfo {
|
||||
name: "old_resource".to_string(),
|
||||
data_type: "json".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: Some("secondary_index='1'".to_string()),
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![name, resource, old_resource],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.statements, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.warnings,
|
||||
vec![
|
||||
"Editing existing columns is not supported for manticoresearch yet.",
|
||||
"Editing existing columns is not supported for manticoresearch yet.",
|
||||
"Editing existing columns is not supported for manticoresearch yet.",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_ignores_mysql_column_options() {
|
||||
let mut title = column("title");
|
||||
title.data_type = "text".to_string();
|
||||
title.is_nullable = false;
|
||||
title.is_primary_key = true;
|
||||
title.default_value = "'untitled'".to_string();
|
||||
title.comment = "Title text".to_string();
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![title],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(result.statements, vec!["CREATE TABLE `materials` (\n `title` text\n);"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_builds_text_column_properties() {
|
||||
let mut title = column("title");
|
||||
title.data_type = "text".to_string();
|
||||
title.extra =
|
||||
Some(ColumnExtra { manticore_indexed: Some(true), manticore_stored: Some(true), ..Default::default() });
|
||||
let mut sku = column("sku");
|
||||
sku.data_type = "string".to_string();
|
||||
sku.extra =
|
||||
Some(ColumnExtra { manticore_indexed: Some(true), manticore_attribute: Some(true), ..Default::default() });
|
||||
let mut name = column("name");
|
||||
name.data_type = "string".to_string();
|
||||
name.extra = Some(ColumnExtra {
|
||||
manticore_indexed: Some(true),
|
||||
manticore_stored: Some(true),
|
||||
manticore_attribute: Some(true),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![title, sku, name],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"CREATE TABLE `materials` (\n `title` text stored indexed,\n `sku` string attribute indexed,\n `name` string stored attribute indexed\n);"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manticoresearch_builds_json_secondary_index_property() {
|
||||
let mut metadata = column("metadata");
|
||||
metadata.data_type = "json".to_string();
|
||||
metadata.extra = Some(ColumnExtra { manticore_secondary_index: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::ManticoreSearch),
|
||||
schema: None,
|
||||
table_name: "materials".to_string(),
|
||||
columns: vec![metadata],
|
||||
indexes: Vec::new(),
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(result.warnings, Vec::<String>::new());
|
||||
assert_eq!(result.statements, vec!["CREATE TABLE `materials` (\n `metadata` json secondary_index='1'\n);"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_create_unique_index_with_comment_and_btree() {
|
||||
let mut idx = index("uniq_users_email", &["email"]);
|
||||
@@ -854,7 +1111,7 @@ fn mysql_create_table_with_auto_increment() {
|
||||
col.data_type = "int".to_string();
|
||||
col.is_nullable = false;
|
||||
col.is_primary_key = true;
|
||||
col.extra = Some(ColumnExtra { auto_increment: Some(true), on_update_current_timestamp: None, identity: None });
|
||||
col.extra = Some(ColumnExtra { auto_increment: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
@@ -879,7 +1136,7 @@ fn mysql_create_table_with_on_update_current_timestamp() {
|
||||
col.data_type = "timestamp".to_string();
|
||||
col.is_nullable = false;
|
||||
col.default_value = "CURRENT_TIMESTAMP".to_string();
|
||||
col.extra = Some(ColumnExtra { auto_increment: None, on_update_current_timestamp: Some(true), identity: None });
|
||||
col.extra = Some(ColumnExtra { on_update_current_timestamp: Some(true), ..Default::default() });
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Mysql),
|
||||
@@ -903,9 +1160,8 @@ fn postgres_create_table_with_identity() {
|
||||
col.data_type = "integer".to_string();
|
||||
col.is_nullable = false;
|
||||
col.extra = Some(ColumnExtra {
|
||||
auto_increment: None,
|
||||
on_update_current_timestamp: None,
|
||||
identity: Some(ColumnIdentity { generation: Some("BY DEFAULT".to_string()), seed: None, increment: None }),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
@@ -931,8 +1187,8 @@ fn sqlserver_create_table_with_identity() {
|
||||
col.is_nullable = false;
|
||||
col.extra = Some(ColumnExtra {
|
||||
auto_increment: Some(true),
|
||||
on_update_current_timestamp: None,
|
||||
identity: Some(ColumnIdentity { generation: None, seed: Some(100), increment: Some(5) }),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let result = build_create_table_sql(TableStructureSqlOptions {
|
||||
|
||||
@@ -25,7 +25,7 @@ pub struct EditableStructureColumn {
|
||||
pub marked_for_drop: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ColumnExtra {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -34,6 +34,14 @@ pub struct ColumnExtra {
|
||||
pub on_update_current_timestamp: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub identity: Option<ColumnIdentity>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manticore_indexed: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manticore_stored: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manticore_attribute: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub manticore_secondary_index: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -18,7 +18,7 @@ pub(super) fn qualified_table(dialect: StructureDialect, schema: Option<&str>, t
|
||||
|
||||
pub(super) fn quote_ident(dialect: StructureDialect, name: &str) -> String {
|
||||
match dialect {
|
||||
StructureDialect::Mysql => format!("`{}`", name.replace('`', "``")),
|
||||
StructureDialect::Mysql | StructureDialect::ManticoreSearch => format!("`{}`", name.replace('`', "``")),
|
||||
StructureDialect::SqlServer => format!("[{}]", name.replace(']', "]]")),
|
||||
StructureDialect::Informix if is_simple_informix_identifier(name) => name.to_string(),
|
||||
_ => format!("\"{}\"", name.replace('"', "\"\"")),
|
||||
@@ -42,6 +42,10 @@ pub(super) fn clean(value: &str) -> String {
|
||||
value.trim().to_string()
|
||||
}
|
||||
|
||||
pub(super) fn is_protected_manticore_id_column(dialect: StructureDialect, column_name: &str) -> bool {
|
||||
dialect == StructureDialect::ManticoreSearch && column_name.trim().eq_ignore_ascii_case("id")
|
||||
}
|
||||
|
||||
pub(super) fn is_temporal_type_for_default(dialect: StructureDialect, base_type: &str) -> bool {
|
||||
let normalized = base_type.split_whitespace().collect::<Vec<_>>().join(" ").to_ascii_lowercase();
|
||||
match dialect {
|
||||
|
||||
Reference in new issue
Block a user