fix(gbase): support 8a and 8s profiles
This commit is contained in:
1 parent
213162aad3
commit
cbc7f9b0db
41 files changed
+596
-50
No files matched your search
@@ -945,10 +945,10 @@
|
||||
},
|
||||
{
|
||||
"dbType": "gbase",
|
||||
"label": "GBase",
|
||||
"label": "GBase 8a",
|
||||
"runtimeMode": "agent",
|
||||
"mcpMode": "bridge",
|
||||
"agentKey": "gbase",
|
||||
"agentKey": "gbase8a",
|
||||
"singleConnectionPool": false,
|
||||
"metadataConnectionScoped": false,
|
||||
"skipTcpProbe": true,
|
||||
@@ -958,6 +958,11 @@
|
||||
"profile": "gbase8s",
|
||||
"label": "GBase 8s",
|
||||
"agentKey": "gbase8s"
|
||||
},
|
||||
{
|
||||
"profile": "gbase8a",
|
||||
"label": "GBase 8a",
|
||||
"agentKey": "gbase8a"
|
||||
}
|
||||
],
|
||||
"supportLevel": "operate",
|
||||
@@ -1178,7 +1183,7 @@
|
||||
"schemaSearch": true,
|
||||
"diagram": false,
|
||||
"tableDataEdit": true,
|
||||
"tableStructureEdit": false,
|
||||
"tableStructureEdit": true,
|
||||
"tableImport": false,
|
||||
"dataTransfer": false,
|
||||
"sqlFileExecution": true,
|
||||
|
||||
@@ -22,8 +22,10 @@ const ORACLE_PROFILES: &[AgentDriverProfile] = &[
|
||||
AgentDriverProfile { profile: "oracle-10g", key: "oracle", label: "Oracle", store_visible: false },
|
||||
];
|
||||
|
||||
const GBASE_PROFILES: &[AgentDriverProfile] =
|
||||
&[AgentDriverProfile { profile: "gbase8s", key: "gbase8s", label: "GBase 8s", store_visible: true }];
|
||||
const GBASE_PROFILES: &[AgentDriverProfile] = &[
|
||||
AgentDriverProfile { profile: "gbase8s", key: "gbase8s", label: "GBase 8s", store_visible: true },
|
||||
AgentDriverProfile { profile: "gbase8a", key: "gbase8a", label: "GBase 8a", store_visible: true },
|
||||
];
|
||||
|
||||
const AGENT_CATALOG: &[AgentCatalogEntry] = &[
|
||||
AgentCatalogEntry {
|
||||
@@ -119,8 +121,8 @@ const AGENT_CATALOG: &[AgentCatalogEntry] = &[
|
||||
},
|
||||
AgentCatalogEntry {
|
||||
db_type: DatabaseType::Gbase,
|
||||
key: "gbase",
|
||||
label: "GBase",
|
||||
key: "gbase8a",
|
||||
label: "GBase 8a",
|
||||
store_visible: true,
|
||||
profiles: GBASE_PROFILES,
|
||||
},
|
||||
|
||||
@@ -48,6 +48,7 @@ pub fn agent_connect_params(config: &ConnectionConfig, host: &str, port: u16, da
|
||||
"client_cert_path": config.client_cert_path,
|
||||
"client_key_path": config.client_key_path,
|
||||
"etcd_endpoints": etcd_endpoints,
|
||||
"gbase_server": config.gbase_server,
|
||||
"jdbc_driver_class": config.jdbc_driver_class.as_deref().unwrap_or(""),
|
||||
"jdbc_driver_paths": &config.jdbc_driver_paths,
|
||||
})
|
||||
@@ -537,6 +538,7 @@ mod tests {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
@@ -596,6 +596,7 @@ mod tests {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
@@ -1338,6 +1338,7 @@ mod tests {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
@@ -508,6 +508,7 @@ mod tests {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: crate::models::connection::default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
@@ -1990,6 +1990,7 @@ mod tests {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: crate::models::connection::default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
@@ -64,6 +64,8 @@ pub struct ConnectionConfig {
|
||||
pub redis_key_separator: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub etcd_endpoints: String,
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub gbase_server: String,
|
||||
/// Typed configuration for external tabular sources.
|
||||
#[serde(default)]
|
||||
pub external_config: Option<serde_json::Value>,
|
||||
@@ -356,6 +358,8 @@ struct ConnectionConfigData {
|
||||
#[serde(default)]
|
||||
pub etcd_endpoints: String,
|
||||
#[serde(default)]
|
||||
pub gbase_server: String,
|
||||
#[serde(default)]
|
||||
pub external_config: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
pub jdbc_driver_class: Option<String>,
|
||||
@@ -404,6 +408,7 @@ impl From<ConnectionConfigData> for ConnectionConfig {
|
||||
redis_cluster_nodes: data.redis_cluster_nodes,
|
||||
redis_key_separator: data.redis_key_separator,
|
||||
etcd_endpoints: data.etcd_endpoints,
|
||||
gbase_server: data.gbase_server,
|
||||
external_config: data.external_config,
|
||||
jdbc_driver_class: data.jdbc_driver_class,
|
||||
jdbc_driver_paths: data.jdbc_driver_paths,
|
||||
@@ -1412,6 +1417,7 @@ mod tests {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
@@ -1983,6 +1983,7 @@ mod tests {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
@@ -195,6 +195,10 @@ pub fn build_paginated_query_sql(options: PaginatedQuerySqlOptions) -> QuerySqlB
|
||||
return ok(format!("{statement};"));
|
||||
}
|
||||
|
||||
if options.database_type == Some(DatabaseType::Informix) {
|
||||
return ok(add_informix_first_limit(&statement, safe_limit, safe_offset));
|
||||
}
|
||||
|
||||
if options.database_type.is_some_and(uses_fetch_first) {
|
||||
return ok(add_fetch_first_limit(&statement, safe_limit, safe_offset));
|
||||
}
|
||||
@@ -406,10 +410,48 @@ fn add_mysql_limit(statement: &str, limit: usize, offset: usize) -> String {
|
||||
format!("{statement} LIMIT {limit}{offset_sql};")
|
||||
}
|
||||
|
||||
fn add_informix_first_limit(statement: &str, limit: usize, offset: usize) -> String {
|
||||
if has_top_level_informix_row_limit(statement) {
|
||||
return format!("{statement};");
|
||||
}
|
||||
let row_limit = if offset > 0 { format!("SKIP {offset} FIRST {limit}") } else { format!("FIRST {limit}") };
|
||||
if statement.len() >= 6 && statement[..6].eq_ignore_ascii_case("SELECT") {
|
||||
let rest = &statement[6..];
|
||||
if let Some((leading, after_modifier)) = strip_sql_server_select_modifier(rest, "DISTINCT") {
|
||||
return format!("SELECT{leading}DISTINCT {row_limit}{after_modifier};");
|
||||
}
|
||||
if let Some((leading, after_modifier)) = strip_sql_server_select_modifier(rest, "UNIQUE") {
|
||||
return format!("SELECT{leading}UNIQUE {row_limit}{after_modifier};");
|
||||
}
|
||||
if let Some((leading, after_modifier)) = strip_sql_server_select_modifier(rest, "ALL") {
|
||||
return format!("SELECT{leading}ALL {row_limit}{after_modifier};");
|
||||
}
|
||||
return format!("SELECT {row_limit}{rest};");
|
||||
}
|
||||
format!("SELECT {row_limit} * FROM ({statement}) dbx_page;")
|
||||
}
|
||||
|
||||
fn has_top_level_limit(sql: &str) -> bool {
|
||||
top_level_sql_tokens(sql).iter().any(|token| token.text == "LIMIT")
|
||||
}
|
||||
|
||||
fn has_top_level_informix_row_limit(sql: &str) -> bool {
|
||||
if has_top_level_limit(sql) {
|
||||
return true;
|
||||
}
|
||||
let tokens = top_level_sql_tokens(sql);
|
||||
let Some(select_index) = tokens.iter().position(|token| token.text == "SELECT") else {
|
||||
return false;
|
||||
};
|
||||
let from_index = tokens
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(index, token)| *index > select_index && token.text == "FROM")
|
||||
.map(|(index, _)| index)
|
||||
.unwrap_or(tokens.len());
|
||||
tokens[select_index + 1..from_index].iter().any(|token| token.text == "FIRST" || token.text == "SKIP")
|
||||
}
|
||||
|
||||
fn has_top_level_fetch_first(sql: &str) -> bool {
|
||||
let tokens = top_level_sql_tokens(sql);
|
||||
tokens.windows(2).any(|w| w[0].text == "FETCH" && w[1].text == "FIRST")
|
||||
@@ -831,6 +873,30 @@ mod tests {
|
||||
assert_eq!(result.sql.unwrap(), "SELECT id FROM users FETCH FIRST 100 ROWS ONLY;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uses_skip_first_pagination_for_informix() {
|
||||
let result = build_paginated_query_sql(PaginatedQuerySqlOptions {
|
||||
original_sql: "SELECT id FROM users WHERE active = 1".to_string(),
|
||||
database_type: Some(DatabaseType::Informix),
|
||||
limit: 50,
|
||||
offset: 100,
|
||||
});
|
||||
|
||||
assert_eq!(result.sql.unwrap(), "SELECT SKIP 100 FIRST 50 id FROM users WHERE active = 1;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn informix_pagination_keeps_existing_first() {
|
||||
let result = build_paginated_query_sql(PaginatedQuerySqlOptions {
|
||||
original_sql: "SELECT FIRST 20 id FROM users".to_string(),
|
||||
database_type: Some(DatabaseType::Informix),
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
});
|
||||
|
||||
assert_eq!(result.sql.unwrap(), "SELECT FIRST 20 id FROM users;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uses_mysql_style_alias_for_pagination() {
|
||||
let result = build_paginated_query_sql(PaginatedQuerySqlOptions {
|
||||
|
||||
@@ -659,6 +659,7 @@ mod tests {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: crate::models::connection::default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
@@ -179,6 +179,7 @@ mod tests {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
@@ -68,6 +68,11 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
|
||||
return format!("SELECT TOP {limit} {select_columns} FROM {table_alias}{where_clause}{order}");
|
||||
}
|
||||
|
||||
if database_type == Some(DatabaseType::Informix) {
|
||||
let row_limit = informix_row_limit_clause(limit, options.offset.unwrap_or(0));
|
||||
return format!("SELECT {row_limit} {select_columns} FROM {table_alias}{where_clause}{order}");
|
||||
}
|
||||
|
||||
if database_type == Some(DatabaseType::Db2) && options.offset.is_some_and(|offset| offset > 0) {
|
||||
return build_db2_table_select_page_sql(
|
||||
&table_alias,
|
||||
@@ -142,6 +147,10 @@ pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String {
|
||||
return format!("SELECT TOP {limit} {select_columns} FROM {table}{order_by}");
|
||||
}
|
||||
|
||||
if database_type == Some(DatabaseType::Informix) {
|
||||
return format!("SELECT FIRST {limit} {select_columns} FROM {table}{order_by}");
|
||||
}
|
||||
|
||||
if database_type.is_some_and(uses_fetch_first) {
|
||||
return format!("SELECT {select_columns} FROM {table}{order_by} FETCH FIRST {limit} ROWS ONLY");
|
||||
}
|
||||
@@ -153,6 +162,14 @@ pub fn build_table_select_sql(options: TableSelectSqlOptions<'_>) -> String {
|
||||
format!("SELECT {select_columns} FROM {table}{order_by} LIMIT {limit};")
|
||||
}
|
||||
|
||||
fn informix_row_limit_clause(limit: usize, offset: usize) -> String {
|
||||
if offset > 0 {
|
||||
format!("SKIP {offset} FIRST {limit}")
|
||||
} else {
|
||||
format!("FIRST {limit}")
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_oracle_row_id(database_type: Option<DatabaseType>, name: &str) -> bool {
|
||||
database_type == Some(DatabaseType::Oracle) && name.eq_ignore_ascii_case(DBX_ROWID_COLUMN)
|
||||
}
|
||||
|
||||
@@ -278,6 +278,38 @@ fn builds_table_data_where_and_schema_queries() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_informix_table_data_with_skip_first_pagination() {
|
||||
assert_eq!(
|
||||
build_table_data_select_sql(TableDataSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Informix),
|
||||
schema: Some("ignored".to_string()),
|
||||
table_name: "users".to_string(),
|
||||
primary_keys: vec!["id".to_string()],
|
||||
columns: vec!["id".to_string(), "name".to_string()],
|
||||
fallback_order_columns: Vec::new(),
|
||||
order_by: None,
|
||||
limit: Some(50),
|
||||
offset: Some(100),
|
||||
where_input: Some("WHERE active = 1".to_string()),
|
||||
include_row_id: false,
|
||||
}),
|
||||
"SELECT SKIP 100 FIRST 50 * FROM users WHERE (active = 1) ORDER BY id ASC"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
build_table_select_sql(TableSelectSqlOptions {
|
||||
database_type: Some(DatabaseType::Informix),
|
||||
schema: None,
|
||||
table_name: "systables",
|
||||
columns: &["tabname".to_string()],
|
||||
order_columns: &[],
|
||||
limit: 1,
|
||||
}),
|
||||
"SELECT FIRST 1 tabname FROM systables"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_table_data_order_overrides_default_key_order() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::column_format::{clickhouse_column_type, column_data_type, column_definition};
|
||||
use super::columns::build_drop_column_sql;
|
||||
use super::comments::build_sqlserver_column_comment_sql;
|
||||
use super::dialect::{capabilities_for, database_label, StructureDialect};
|
||||
use super::types::{EditableStructureColumn, SingleColumnAlterSqlOptions, TableStructureSqlResult};
|
||||
@@ -29,7 +30,7 @@ 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 };
|
||||
}
|
||||
statements.push(format!("ALTER TABLE {table} DROP COLUMN {};", quote_ident(dialect, &original.name)));
|
||||
statements.push(build_drop_column_sql(dialect, &table, &original.name));
|
||||
return TableStructureSqlResult { statements, warnings };
|
||||
}
|
||||
|
||||
@@ -74,6 +75,7 @@ pub fn build_single_column_alter_sql(options: SingleColumnAlterSqlOptions) -> Ta
|
||||
StructureDialect::ClickHouse => {
|
||||
statements.extend(build_clickhouse_existing_column_sql(&table, &options.column, ""))
|
||||
}
|
||||
StructureDialect::Informix => statements.extend(build_informix_existing_column_sql(&table, &options.column)),
|
||||
StructureDialect::SqlServer => statements.extend(build_sqlserver_existing_column_sql(
|
||||
&table,
|
||||
&options.column,
|
||||
@@ -191,6 +193,42 @@ pub(super) fn build_postgres_existing_column_sql(table: &str, column: &EditableS
|
||||
statements
|
||||
}
|
||||
|
||||
pub(super) fn build_informix_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec<String> {
|
||||
let Some(original) = &column.original else {
|
||||
return Vec::new();
|
||||
};
|
||||
let dialect = StructureDialect::Informix;
|
||||
let mut statements = Vec::new();
|
||||
let mut current_name = original.name.clone();
|
||||
if column.name != original.name {
|
||||
statements.push(format!(
|
||||
"RENAME COLUMN {table}.{} TO {};",
|
||||
quote_ident(dialect, &original.name),
|
||||
quote_ident(dialect, &column.name)
|
||||
));
|
||||
current_name = column.name.clone();
|
||||
}
|
||||
let type_changed = column.data_type.trim() != original.data_type.trim();
|
||||
let nullable_changed = column.is_nullable != original.is_nullable;
|
||||
let default_changed = normalize_default(Some(&column.default_value)) != original_default(column);
|
||||
if type_changed || nullable_changed || default_changed {
|
||||
let mut parts = vec![quote_ident(dialect, ¤t_name), column_data_type(dialect, column)];
|
||||
if column.is_nullable {
|
||||
parts.push("NULL".to_string());
|
||||
} else {
|
||||
parts.push("NOT NULL".to_string());
|
||||
}
|
||||
let default_value = normalize_default(Some(&column.default_value));
|
||||
if !default_value.is_empty() {
|
||||
parts.push(format!("DEFAULT {}", format_default_for_sql(dialect, &column.data_type, &default_value)));
|
||||
} else if default_changed {
|
||||
parts.push("DEFAULT NULL".to_string());
|
||||
}
|
||||
statements.push(format!("ALTER TABLE {table} MODIFY ({});", parts.join(" ")));
|
||||
}
|
||||
statements
|
||||
}
|
||||
|
||||
pub(super) fn build_oracle_like_existing_column_sql(
|
||||
dialect: StructureDialect,
|
||||
table: &str,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::column_alter::{
|
||||
build_clickhouse_existing_column_sql, build_h2_existing_column_sql, build_mysql_existing_column_sql,
|
||||
build_oracle_like_existing_column_sql, build_postgres_existing_column_sql, build_sqlite_existing_column_sql,
|
||||
build_sqlserver_existing_column_sql, has_column_extra_change, has_existing_column_attribute_change,
|
||||
build_clickhouse_existing_column_sql, build_h2_existing_column_sql, build_informix_existing_column_sql,
|
||||
build_mysql_existing_column_sql, build_oracle_like_existing_column_sql, build_postgres_existing_column_sql,
|
||||
build_sqlite_existing_column_sql, build_sqlserver_existing_column_sql, has_column_extra_change,
|
||||
has_existing_column_attribute_change,
|
||||
};
|
||||
use super::column_format::column_definition;
|
||||
use super::comments::build_sqlserver_column_comment_sql;
|
||||
@@ -33,7 +34,7 @@ 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;
|
||||
}
|
||||
statements.push(format!("ALTER TABLE {table} DROP COLUMN {};", quote_ident(dialect, &original.name)));
|
||||
statements.push(build_drop_column_sql(dialect, &table, &original.name));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -106,6 +107,7 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu
|
||||
column,
|
||||
if has_position_change { &position_clause } else { "" },
|
||||
)),
|
||||
StructureDialect::Informix => statements.extend(build_informix_existing_column_sql(&table, column)),
|
||||
StructureDialect::SqlServer => statements.extend(build_sqlserver_existing_column_sql(
|
||||
&table,
|
||||
column,
|
||||
@@ -185,11 +187,13 @@ pub(super) fn build_add_column_sql(
|
||||
schema: Option<&str>,
|
||||
table_name: &str,
|
||||
) -> Vec<String> {
|
||||
let add_keyword = if dialect == StructureDialect::SqlServer { "ADD" } else { "ADD COLUMN" };
|
||||
let definition = column_definition(dialect, column);
|
||||
let mut statements = if dialect == StructureDialect::Oracle {
|
||||
vec![format!("ALTER TABLE {table} ADD ({definition});")]
|
||||
} else if dialect == StructureDialect::Informix {
|
||||
vec![format!("ALTER TABLE {table} ADD ({definition});")]
|
||||
} else {
|
||||
let add_keyword = if dialect == StructureDialect::SqlServer { "ADD" } else { "ADD COLUMN" };
|
||||
vec![format!("ALTER TABLE {table} {add_keyword} {definition}{position_clause};")]
|
||||
};
|
||||
if matches!(dialect, StructureDialect::Postgres | StructureDialect::Oracle) && !clean(&column.comment).is_empty() {
|
||||
@@ -212,6 +216,13 @@ pub(super) fn build_add_column_sql(
|
||||
statements
|
||||
}
|
||||
|
||||
pub(super) fn build_drop_column_sql(dialect: StructureDialect, table: &str, column_name: &str) -> String {
|
||||
if dialect == StructureDialect::Informix {
|
||||
return format!("ALTER TABLE {table} DROP ({});", quote_ident(dialect, column_name));
|
||||
}
|
||||
format!("ALTER TABLE {table} DROP COLUMN {};", quote_ident(dialect, column_name))
|
||||
}
|
||||
|
||||
pub(super) fn column_position_clause(
|
||||
dialect: StructureDialect,
|
||||
columns: &[&EditableStructureColumn],
|
||||
|
||||
@@ -11,6 +11,7 @@ pub(super) enum StructureDialect {
|
||||
Oracle,
|
||||
H2,
|
||||
ClickHouse,
|
||||
Informix,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
@@ -64,8 +65,7 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
|
||||
| DatabaseType::StarRocks
|
||||
| DatabaseType::Goldendb
|
||||
| DatabaseType::Sundb
|
||||
| DatabaseType::Databend
|
||||
| DatabaseType::Gbase,
|
||||
| DatabaseType::Databend,
|
||||
) => TableStructureCapabilities {
|
||||
dialect: StructureDialect::Mysql,
|
||||
add_column: true,
|
||||
@@ -82,6 +82,13 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
|
||||
alter_primary_key: true,
|
||||
..base
|
||||
},
|
||||
Some(DatabaseType::Gbase) => TableStructureCapabilities {
|
||||
dialect: StructureDialect::Mysql,
|
||||
add_column: true,
|
||||
drop_column: true,
|
||||
rename_column: true,
|
||||
..base
|
||||
},
|
||||
Some(
|
||||
DatabaseType::Postgres
|
||||
| DatabaseType::Gaussdb
|
||||
@@ -197,6 +204,17 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
|
||||
comment: true,
|
||||
..base
|
||||
},
|
||||
Some(DatabaseType::Informix) => TableStructureCapabilities {
|
||||
dialect: StructureDialect::Informix,
|
||||
add_column: true,
|
||||
drop_column: true,
|
||||
rename_column: true,
|
||||
alter_existing_column: true,
|
||||
create_index: true,
|
||||
drop_index: true,
|
||||
rebuild_index: true,
|
||||
..base
|
||||
},
|
||||
_ => base,
|
||||
}
|
||||
}
|
||||
@@ -227,6 +245,7 @@ pub(super) fn dialect_label(dialect: StructureDialect) -> String {
|
||||
StructureDialect::Oracle => "oracle",
|
||||
StructureDialect::H2 => "h2",
|
||||
StructureDialect::ClickHouse => "clickhouse",
|
||||
StructureDialect::Informix => "informix",
|
||||
StructureDialect::Unsupported => "this database",
|
||||
}
|
||||
.to_string()
|
||||
@@ -243,6 +262,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::Informix => Some(DatabaseType::Informix),
|
||||
StructureDialect::Unsupported => None,
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ pub(super) fn build_drop_index_sql(
|
||||
if matches!(dialect, StructureDialect::Mysql | StructureDialect::SqlServer) {
|
||||
return format!("DROP INDEX {} ON {table};", quote_ident(dialect, index_name));
|
||||
}
|
||||
if matches!(dialect, StructureDialect::Postgres | StructureDialect::Oracle)
|
||||
if matches!(dialect, StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::Informix)
|
||||
&& schema.is_some_and(|schema| !schema.trim().is_empty())
|
||||
{
|
||||
return format!("DROP INDEX {}.{};", quote_ident(dialect, schema.unwrap()), quote_ident(dialect, index_name));
|
||||
|
||||
@@ -117,6 +117,75 @@ fn builds_mysql_column_and_index_changes() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_informix_column_and_index_changes() {
|
||||
let mut renamed = column("display_name");
|
||||
renamed.data_type = "varchar(120)".to_string();
|
||||
renamed.is_nullable = false;
|
||||
renamed.default_value = "'guest'".to_string();
|
||||
renamed.original = Some(ColumnInfo {
|
||||
name: "name".to_string(),
|
||||
data_type: "varchar(80)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: Some(String::new()),
|
||||
});
|
||||
let mut email = column("email");
|
||||
email.is_nullable = false;
|
||||
let mut old_col = column("old_col");
|
||||
old_col.marked_for_drop = true;
|
||||
old_col.original = Some(ColumnInfo {
|
||||
name: "old_col".to_string(),
|
||||
data_type: "varchar(20)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
let mut old_index = index("idx_old", &["name"]);
|
||||
old_index.marked_for_drop = true;
|
||||
old_index.original = Some(IndexInfo {
|
||||
name: "idx_old".to_string(),
|
||||
columns: vec!["name".to_string()],
|
||||
is_unique: false,
|
||||
is_primary: false,
|
||||
filter: None,
|
||||
index_type: None,
|
||||
included_columns: None,
|
||||
comment: None,
|
||||
});
|
||||
let mut email_index = index("uniq_users_email", &["email"]);
|
||||
email_index.is_unique = true;
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Informix),
|
||||
schema: Some("gbasedbt".to_string()),
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![renamed, email, old_col],
|
||||
indexes: vec![old_index, email_index],
|
||||
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![
|
||||
"RENAME COLUMN gbasedbt.users.name TO display_name;",
|
||||
"ALTER TABLE gbasedbt.users MODIFY (display_name varchar(120) NOT NULL DEFAULT 'guest');",
|
||||
"ALTER TABLE gbasedbt.users ADD (email varchar(255) NOT NULL);",
|
||||
"ALTER TABLE gbasedbt.users DROP (old_col);",
|
||||
"DROP INDEX gbasedbt.idx_old;",
|
||||
"CREATE UNIQUE INDEX uniq_users_email ON gbasedbt.users (email);",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_create_index_with_comment() {
|
||||
let mut col = column("name");
|
||||
@@ -146,6 +215,69 @@ fn mysql_create_index_with_comment() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gbase8a_uses_limited_mysql_ddl() {
|
||||
let mut renamed = column("display_email");
|
||||
renamed.data_type = "varchar(255)".to_string();
|
||||
renamed.original = Some(ColumnInfo {
|
||||
name: "email".to_string(),
|
||||
data_type: "varchar(255)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
let new_col = column("nickname");
|
||||
let mut old_col = column("old_col");
|
||||
old_col.marked_for_drop = true;
|
||||
old_col.original = Some(ColumnInfo {
|
||||
name: "old_col".to_string(),
|
||||
data_type: "varchar(20)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
let mut index = index("idx_users_email", &["display_email"]);
|
||||
index.original = Some(IndexInfo {
|
||||
name: "idx_users_email".to_string(),
|
||||
columns: vec!["email".to_string()],
|
||||
is_unique: false,
|
||||
is_primary: false,
|
||||
filter: None,
|
||||
index_type: None,
|
||||
included_columns: None,
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Gbase),
|
||||
schema: None,
|
||||
table_name: "users".to_string(),
|
||||
columns: vec![renamed, new_col, old_col],
|
||||
indexes: vec![index],
|
||||
foreign_keys: Vec::new(),
|
||||
triggers: Vec::new(),
|
||||
table_comment: None,
|
||||
original_table_comment: None,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
result.statements,
|
||||
vec![
|
||||
"ALTER TABLE `users` CHANGE COLUMN `email` `display_email` varchar(255);",
|
||||
"ALTER TABLE `users` ADD COLUMN `nickname` varchar(255);",
|
||||
"ALTER TABLE `users` DROP COLUMN `old_col`;",
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
result.warnings,
|
||||
vec!["Editing existing indexes is not supported for gbase from this editor.".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_create_unique_index_with_comment_and_btree() {
|
||||
let mut idx = index("uniq_users_email", &["email"]);
|
||||
|
||||
@@ -4,7 +4,11 @@ use super::types::EditableStructureColumn;
|
||||
pub(super) fn qualified_table(dialect: StructureDialect, schema: Option<&str>, table_name: &str) -> String {
|
||||
if matches!(
|
||||
dialect,
|
||||
StructureDialect::Postgres | StructureDialect::Oracle | StructureDialect::SqlServer | StructureDialect::H2
|
||||
StructureDialect::Postgres
|
||||
| StructureDialect::Oracle
|
||||
| StructureDialect::SqlServer
|
||||
| StructureDialect::H2
|
||||
| StructureDialect::Informix
|
||||
) && schema.is_some_and(|schema| !schema.trim().is_empty())
|
||||
{
|
||||
return format!("{}.{}", quote_ident(dialect, schema.unwrap()), quote_ident(dialect, table_name));
|
||||
@@ -16,10 +20,20 @@ pub(super) fn quote_ident(dialect: StructureDialect, name: &str) -> String {
|
||||
match dialect {
|
||||
StructureDialect::Mysql => format!("`{}`", name.replace('`', "``")),
|
||||
StructureDialect::SqlServer => format!("[{}]", name.replace(']', "]]")),
|
||||
StructureDialect::Informix if is_simple_informix_identifier(name) => name.to_string(),
|
||||
_ => format!("\"{}\"", name.replace('"', "\"\"")),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_simple_informix_identifier(name: &str) -> bool {
|
||||
let mut chars = name.chars();
|
||||
let Some(first) = chars.next() else {
|
||||
return false;
|
||||
};
|
||||
(first.is_ascii_alphabetic() || first == '_')
|
||||
&& chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$')
|
||||
}
|
||||
|
||||
pub(super) fn quote_string(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "''"))
|
||||
}
|
||||
@@ -78,6 +92,7 @@ pub(super) fn is_temporal_type_for_default(dialect: StructureDialect, base_type:
|
||||
StructureDialect::Sqlite => {
|
||||
matches!(normalized.as_str(), "date" | "datetime" | "timestamp" | "time")
|
||||
}
|
||||
StructureDialect::Informix => matches!(normalized.as_str(), "date" | "datetime"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3330,6 +3330,7 @@ mod tests {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
@@ -44,6 +44,7 @@ fn postgres_test_config(id: &str, database: &str) -> ConnectionConfig {
|
||||
redis_cluster_nodes: String::new(),
|
||||
redis_key_separator: dbx_core::models::connection::default_redis_key_separator(),
|
||||
etcd_endpoints: String::new(),
|
||||
gbase_server: String::new(),
|
||||
external_config: None,
|
||||
jdbc_driver_class: None,
|
||||
jdbc_driver_paths: Vec::new(),
|
||||
|
||||
Reference in new issue
Block a user