mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 08:13:34 +00:00
fix(app): 修复 Oracle 表结构已有字段识别
Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
isSqlServerIdentityCompatibleDataType,
|
||||
isProtectedManticoreIdColumn,
|
||||
parseExtraToColumnExtra,
|
||||
rehydrateColumnDraftsFromMetadata,
|
||||
splitDataType,
|
||||
toColumnNames,
|
||||
applyManticoreDdlColumnExtras,
|
||||
@@ -618,6 +619,7 @@ let skipNextRefreshVersion = false;
|
||||
let restoringDraft = false;
|
||||
let syncingDraft = false;
|
||||
let draftHydrated = false;
|
||||
let hydratingRestoredDraft = false;
|
||||
|
||||
function cloneDraftValue<T>(value: T): T {
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
@@ -647,6 +649,7 @@ function syncDraftToParent() {
|
||||
|
||||
function restoreDraft(draft: TableStructureEditorDraft) {
|
||||
restoringDraft = true;
|
||||
draftHydrated = false;
|
||||
activeTab.value = draft.activeTab || "columns";
|
||||
newTableName.value = draft.newTableName || "";
|
||||
tableComment.value = draft.tableComment || "";
|
||||
@@ -656,7 +659,45 @@ function restoreDraft(draft: TableStructureEditorDraft) {
|
||||
foreignKeys.value = cloneDraftValue(draft.foreignKeys || []);
|
||||
triggers.value = cloneDraftValue(draft.triggers || []);
|
||||
restoringDraft = false;
|
||||
draftHydrated = true;
|
||||
draftHydrated = !needsColumnDraftMetadataHydration();
|
||||
}
|
||||
|
||||
function needsColumnDraftMetadataHydration() {
|
||||
return !isCreateMode.value && columns.value.some((column) => !column.original && !column.id.startsWith("new:") && !!column.name.trim());
|
||||
}
|
||||
|
||||
async function hydrateRestoredDraftFromDatabase() {
|
||||
if (!needsColumnDraftMetadataHydration() || hydratingRestoredDraft) return;
|
||||
const connectionId = props.connectionId;
|
||||
const database = props.database;
|
||||
const schema = metadataSchema.value;
|
||||
const tableName = props.tableName;
|
||||
if (!connectionId || !database || !tableName) return;
|
||||
|
||||
hydratingRestoredDraft = true;
|
||||
let shouldRefreshPreview = false;
|
||||
try {
|
||||
await store.ensureConnected(connectionId);
|
||||
let nextColumns = await api.getColumns(connectionId, database, schema, tableName);
|
||||
if (databaseType.value === "manticoresearch" && tableMetadataCapabilities.value.ddl) {
|
||||
try {
|
||||
const ddl = await api.getTableDdl(connectionId, database, schema, tableName);
|
||||
ddlContent.value = await formatSqlForDisplay(ddl, sqlFormatDialectForDbType(databaseType.value), settingsStore.editorSettings.sqlFormatter);
|
||||
ddlFetched.value = true;
|
||||
nextColumns = applyManticoreDdlColumnExtras(nextColumns, ddl);
|
||||
} catch {
|
||||
/* ignore — Manticore column properties can still come from SHOW COLUMNS when available */
|
||||
}
|
||||
}
|
||||
columns.value = rehydrateColumnDraftsFromMetadata(columns.value, nextColumns, databaseType.value);
|
||||
markDraftHydratedAndSync();
|
||||
shouldRefreshPreview = true;
|
||||
} catch (e: any) {
|
||||
console.warn("[DBX][structure-editor:draft-hydration-failed]", e);
|
||||
} finally {
|
||||
hydratingRestoredDraft = false;
|
||||
if (shouldRefreshPreview) scheduleSqlPreviewRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
function markDraftHydratedAndSync() {
|
||||
@@ -736,6 +777,18 @@ async function loadDynamicDataTypeOptions() {
|
||||
}
|
||||
|
||||
function scheduleSqlPreviewRefresh() {
|
||||
if (hydratingRestoredDraft || needsColumnDraftMetadataHydration()) {
|
||||
if (sqlPreviewDebounceTimer) {
|
||||
clearTimeout(sqlPreviewDebounceTimer);
|
||||
sqlPreviewDebounceTimer = undefined;
|
||||
}
|
||||
sqlPreviewRequestId++;
|
||||
deferredSqlPreviewRefresh = false;
|
||||
pendingStatements.value = [];
|
||||
warnings.value = [];
|
||||
sqlPreviewLoading.value = true;
|
||||
return;
|
||||
}
|
||||
if (!hasPendingStructureChanges()) {
|
||||
clearSqlPreviewState();
|
||||
return;
|
||||
@@ -1589,6 +1642,7 @@ onMounted(() => {
|
||||
if (props.draft?.initialized) {
|
||||
restoreDraft(props.draft);
|
||||
applyInitialStructureTab();
|
||||
void hydrateRestoredDraftFromDatabase();
|
||||
} else if (isCreateMode.value) {
|
||||
markDraftHydratedAndSync();
|
||||
} else {
|
||||
@@ -1601,6 +1655,7 @@ onActivated(() => {
|
||||
void loadDynamicDataTypeOptions();
|
||||
if (props.draft?.initialized && !draftHydrated) {
|
||||
restoreDraft(props.draft);
|
||||
void hydrateRestoredDraftFromDatabase();
|
||||
}
|
||||
});
|
||||
onDeactivated(unregisterStructureEditorShortcuts);
|
||||
|
||||
@@ -606,6 +606,74 @@ export function createColumnDrafts(columns: ColumnInfo[], databaseType?: Databas
|
||||
});
|
||||
}
|
||||
|
||||
function existingColumnIdName(id: string): string | undefined {
|
||||
const prefix = "existing:";
|
||||
return id.startsWith(prefix) ? id.slice(prefix.length) : undefined;
|
||||
}
|
||||
|
||||
function isNewColumnDraftId(id: string): boolean {
|
||||
return id.startsWith("new:");
|
||||
}
|
||||
|
||||
function uniqueNames(names: Array<string | undefined>): string[] {
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const name of names) {
|
||||
if (!name) continue;
|
||||
if (seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
result.push(name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function findColumnDraftByName(columns: EditableStructureColumn[], names: string[], usedIndexes: Set<number>): number | undefined {
|
||||
for (const name of names) {
|
||||
const exactIndex = columns.findIndex((column, index) => !usedIndexes.has(index) && column.name === name);
|
||||
if (exactIndex >= 0) return exactIndex;
|
||||
}
|
||||
|
||||
for (const name of names) {
|
||||
const lowerName = name.toLowerCase();
|
||||
const matches = columns.map((column, index) => ({ column, index })).filter(({ column, index }) => !usedIndexes.has(index) && column.name.toLowerCase() === lowerName);
|
||||
if (matches.length === 1) return matches[0]!.index;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function rehydrateColumnDraftsFromMetadata(draftColumns: EditableStructureColumn[], columns: ColumnInfo[], databaseType?: DatabaseType): EditableStructureColumn[] {
|
||||
const metadataDrafts = createColumnDrafts(columns, databaseType);
|
||||
if (!metadataDrafts.length) return draftColumns;
|
||||
if (!draftColumns.length) return metadataDrafts;
|
||||
|
||||
const usedMetadataIndexes = new Set<number>();
|
||||
const nextColumns = draftColumns.map((column) => {
|
||||
if (!column.original && isNewColumnDraftId(column.id)) return column;
|
||||
|
||||
const needsHydration = !column.original || column.originalPosition === undefined;
|
||||
const candidates = uniqueNames([column.original?.name, existingColumnIdName(column.id), column.name]);
|
||||
const metadataIndex = findColumnDraftByName(metadataDrafts, candidates, usedMetadataIndexes);
|
||||
if (metadataIndex === undefined) return column;
|
||||
usedMetadataIndexes.add(metadataIndex);
|
||||
if (!needsHydration) return column;
|
||||
|
||||
const metadataDraft = metadataDrafts[metadataIndex]!;
|
||||
return {
|
||||
...column,
|
||||
original: column.original ?? metadataDraft.original,
|
||||
originalPosition: column.originalPosition ?? metadataDraft.originalPosition,
|
||||
};
|
||||
});
|
||||
|
||||
if (usedMetadataIndexes.size === 0) {
|
||||
return [...metadataDrafts, ...draftColumns];
|
||||
}
|
||||
|
||||
const missingMetadataDrafts = metadataDrafts.filter((_, index) => !usedMetadataIndexes.has(index));
|
||||
return [...nextColumns, ...missingMetadataDrafts];
|
||||
}
|
||||
|
||||
export function createIndexDrafts(indexes: IndexInfo[]): EditableStructureIndex[] {
|
||||
return indexes.map((index) => ({
|
||||
id: `existing:${index.name}`,
|
||||
|
||||
@@ -954,6 +954,122 @@ fn oracle_table_comments_from_query_result(result: db::QueryResult) -> HashMap<S
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn oracle_columns_sql(schema: &str, table: &str) -> String {
|
||||
format!(
|
||||
"SELECT c.COLUMN_NAME, c.DATA_TYPE, c.NULLABLE, c.DATA_DEFAULT, \
|
||||
c.DATA_LENGTH, c.DATA_PRECISION, c.DATA_SCALE, c.COLUMN_ID, \
|
||||
CASE WHEN cc.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IS_PK, \
|
||||
cm.COMMENTS \
|
||||
FROM ALL_TAB_COLUMNS c \
|
||||
LEFT JOIN ( \
|
||||
SELECT cols.OWNER, cols.TABLE_NAME, cols.COLUMN_NAME \
|
||||
FROM ALL_CONS_COLUMNS cols \
|
||||
JOIN ALL_CONSTRAINTS con \
|
||||
ON con.CONSTRAINT_NAME = cols.CONSTRAINT_NAME \
|
||||
AND con.OWNER = cols.OWNER \
|
||||
AND con.CONSTRAINT_TYPE = 'P' \
|
||||
) cc ON cc.OWNER = c.OWNER AND cc.TABLE_NAME = c.TABLE_NAME AND cc.COLUMN_NAME = c.COLUMN_NAME \
|
||||
LEFT JOIN ALL_COL_COMMENTS cm \
|
||||
ON cm.OWNER = c.OWNER AND cm.TABLE_NAME = c.TABLE_NAME AND cm.COLUMN_NAME = c.COLUMN_NAME \
|
||||
WHERE c.OWNER = {} AND c.TABLE_NAME = {} \
|
||||
ORDER BY c.COLUMN_ID",
|
||||
oracle_owner_filter(schema),
|
||||
sql_string(table),
|
||||
)
|
||||
}
|
||||
|
||||
fn oracle_column_type(data_type: &str, precision: Option<i32>, scale: Option<i32>, length: Option<i32>) -> String {
|
||||
match data_type.to_ascii_uppercase().as_str() {
|
||||
"NUMBER" => match (precision, scale) {
|
||||
(Some(precision), Some(scale)) if scale > 0 => format!("NUMBER({precision},{scale})"),
|
||||
(Some(precision), _) => format!("NUMBER({precision})"),
|
||||
_ => "NUMBER".to_string(),
|
||||
},
|
||||
"VARCHAR2" | "NVARCHAR2" | "CHAR" | "NCHAR" | "RAW" => match length {
|
||||
Some(length) => format!("{data_type}({length})"),
|
||||
None => data_type.to_string(),
|
||||
},
|
||||
_ => data_type.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn oracle_columns_from_query_result(result: db::QueryResult) -> Vec<db::ColumnInfo> {
|
||||
result
|
||||
.rows
|
||||
.into_iter()
|
||||
.filter_map(|row| {
|
||||
let name = query_result_cell_string(&row, 0)?;
|
||||
let data_type = query_result_cell_string(&row, 1).unwrap_or_default();
|
||||
let nullable = query_result_cell_string(&row, 2).unwrap_or_default();
|
||||
let default_value = query_result_cell_string(&row, 3)
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
let length = query_result_cell_i64(&row, 4).and_then(|value| i32::try_from(value).ok());
|
||||
let precision = query_result_cell_i64(&row, 5).and_then(|value| i32::try_from(value).ok());
|
||||
let scale = query_result_cell_i64(&row, 6).and_then(|value| i32::try_from(value).ok());
|
||||
let is_primary_key = query_result_cell_i64(&row, 8).unwrap_or(0) == 1;
|
||||
let comment = query_result_cell_string(&row, 9).filter(|value| !value.trim().is_empty());
|
||||
Some(db::ColumnInfo {
|
||||
name,
|
||||
data_type: oracle_column_type(&data_type, precision, scale, length),
|
||||
is_nullable: nullable == "Y",
|
||||
column_default: default_value,
|
||||
is_primary_key,
|
||||
extra: None,
|
||||
comment,
|
||||
numeric_precision: precision,
|
||||
numeric_scale: scale,
|
||||
character_maximum_length: length,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn oracle_columns_via_sql(
|
||||
database: &str,
|
||||
schema: &str,
|
||||
table: &str,
|
||||
client: &mut db::agent_driver::AgentDriverClient,
|
||||
timeout_duration: Option<Duration>,
|
||||
) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
let sql = oracle_columns_sql(schema, table);
|
||||
let result = client
|
||||
.execute_query_with_timeout::<db::QueryResult>(
|
||||
agent_execute_query_params(
|
||||
&sql,
|
||||
if database.is_empty() { None } else { Some(database) },
|
||||
if schema.is_empty() { None } else { Some(schema) },
|
||||
QueryExecutionOptions { max_rows: Some(10_000), ..Default::default() },
|
||||
),
|
||||
timeout_duration,
|
||||
)
|
||||
.await?;
|
||||
Ok(deduplicate_column_infos(oracle_columns_from_query_result(result)))
|
||||
}
|
||||
|
||||
async fn external_driver_oracle_columns_via_sql(
|
||||
session: Arc<crate::plugins::PluginDriverSession>,
|
||||
config: &ConnectionConfig,
|
||||
database: &str,
|
||||
schema: &str,
|
||||
table: &str,
|
||||
) -> Result<Vec<db::ColumnInfo>, String> {
|
||||
let result: db::QueryResult = session
|
||||
.invoke_with_timeout(
|
||||
"executeQuery",
|
||||
serde_json::json!({
|
||||
"connection": config,
|
||||
"database": database,
|
||||
"schema": schema,
|
||||
"sql": oracle_columns_sql(schema, table),
|
||||
"maxRows": 10_000
|
||||
}),
|
||||
agent_metadata_timeout(Some(config)),
|
||||
)
|
||||
.await?;
|
||||
Ok(deduplicate_column_infos(oracle_columns_from_query_result(result)))
|
||||
}
|
||||
|
||||
fn oracle_object_statistics_sql(schema: &str) -> String {
|
||||
oracle_object_statistics_owner_segments_sql(schema, "ALL_SEGMENTS")
|
||||
}
|
||||
@@ -1676,11 +1792,12 @@ mod tests {
|
||||
clickhouse_metadata_database, deduplicate_column_infos, filter_mysql_system_databases_for_config,
|
||||
filter_table_infos, filter_visible_schema_names, is_agent_postgres_metadata_fallback_config,
|
||||
is_retryable_metadata_error, mysql_table_metadata_catalog, normalize_information_schema_table_type,
|
||||
oracle_object_statistics_dba_segments_sql, oracle_object_statistics_from_query_result,
|
||||
oracle_object_statistics_rows_only_sql, oracle_object_statistics_sql,
|
||||
oracle_object_statistics_user_segments_sql, oracle_table_comment_from_query_result, oracle_table_comment_sql,
|
||||
oracle_table_comments_from_query_result, oracle_table_comments_sql, presto_like_information_schema_tables_sql,
|
||||
presto_like_tables_from_query_result, visible_schema_filter,
|
||||
oracle_columns_from_query_result, oracle_columns_sql, oracle_object_statistics_dba_segments_sql,
|
||||
oracle_object_statistics_from_query_result, oracle_object_statistics_rows_only_sql,
|
||||
oracle_object_statistics_sql, oracle_object_statistics_user_segments_sql,
|
||||
oracle_table_comment_from_query_result, oracle_table_comment_sql, oracle_table_comments_from_query_result,
|
||||
oracle_table_comments_sql, presto_like_information_schema_tables_sql, presto_like_tables_from_query_result,
|
||||
visible_schema_filter,
|
||||
};
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
use super::{
|
||||
@@ -2186,6 +2303,79 @@ mod tests {
|
||||
assert!(!comments.contains_key("PRODUCTS"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_columns_sql_uses_exact_table_name_for_quoted_lowercase_tables() {
|
||||
let sql = oracle_columns_sql("DBX_TEST", "test");
|
||||
|
||||
assert!(sql.contains("ALL_TAB_COLUMNS"));
|
||||
assert!(sql.contains("ALL_COL_COMMENTS"));
|
||||
assert!(sql.contains("c.OWNER = 'DBX_TEST'"));
|
||||
assert!(sql.contains("c.TABLE_NAME = 'test'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_columns_from_query_result_maps_types_comments_and_primary_key() {
|
||||
let result = db::QueryResult {
|
||||
columns: vec![
|
||||
"COLUMN_NAME".to_string(),
|
||||
"DATA_TYPE".to_string(),
|
||||
"NULLABLE".to_string(),
|
||||
"DATA_DEFAULT".to_string(),
|
||||
"DATA_LENGTH".to_string(),
|
||||
"DATA_PRECISION".to_string(),
|
||||
"DATA_SCALE".to_string(),
|
||||
"COLUMN_ID".to_string(),
|
||||
"IS_PK".to_string(),
|
||||
"COMMENTS".to_string(),
|
||||
],
|
||||
column_types: Vec::new(),
|
||||
column_sortables: Vec::new(),
|
||||
rows: vec![
|
||||
vec![
|
||||
serde_json::json!("id"),
|
||||
serde_json::json!("VARCHAR2"),
|
||||
serde_json::json!("N"),
|
||||
serde_json::Value::Null,
|
||||
serde_json::json!("255"),
|
||||
serde_json::Value::Null,
|
||||
serde_json::Value::Null,
|
||||
serde_json::json!("1"),
|
||||
serde_json::json!("1"),
|
||||
serde_json::json!("identifier"),
|
||||
],
|
||||
vec![
|
||||
serde_json::json!("data"),
|
||||
serde_json::json!("TIMESTAMP"),
|
||||
serde_json::json!("Y"),
|
||||
serde_json::Value::Null,
|
||||
serde_json::Value::Null,
|
||||
serde_json::Value::Null,
|
||||
serde_json::Value::Null,
|
||||
serde_json::json!("2"),
|
||||
serde_json::json!("0"),
|
||||
serde_json::Value::Null,
|
||||
],
|
||||
],
|
||||
affected_rows: 0,
|
||||
execution_time_ms: 0,
|
||||
truncated: false,
|
||||
session_id: None,
|
||||
has_more: false,
|
||||
};
|
||||
|
||||
let columns = oracle_columns_from_query_result(result);
|
||||
|
||||
assert_eq!(columns.len(), 2);
|
||||
assert_eq!(columns[0].name, "id");
|
||||
assert_eq!(columns[0].data_type, "VARCHAR2(255)");
|
||||
assert!(!columns[0].is_nullable);
|
||||
assert!(columns[0].is_primary_key);
|
||||
assert_eq!(columns[0].comment.as_deref(), Some("identifier"));
|
||||
assert_eq!(columns[1].name, "data");
|
||||
assert_eq!(columns[1].data_type, "TIMESTAMP");
|
||||
assert!(columns[1].is_nullable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_object_statistics_sql_reads_rows_and_segment_bytes() {
|
||||
let sql = oracle_object_statistics_sql("app's");
|
||||
@@ -2989,6 +3179,30 @@ pub async fn get_columns_core(
|
||||
agent_metadata_timeout(Some(config.as_ref())),
|
||||
)
|
||||
.await?;
|
||||
if columns.is_empty() && config.db_type == DatabaseType::Oracle {
|
||||
match external_driver_oracle_columns_via_sql(
|
||||
session.clone(),
|
||||
config.as_ref(),
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(fallback_columns) if !fallback_columns.is_empty() => return Ok(fallback_columns),
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"[schema][external-driver:get_columns:oracle-fallback-failed] connection_id={} database={} schema={} table={} error={}",
|
||||
connection_id,
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(deduplicate_column_infos(columns));
|
||||
}
|
||||
#[cfg(feature = "duckdb-bundled")]
|
||||
@@ -3045,6 +3259,30 @@ pub async fn get_columns_core(
|
||||
Ok(columns) if !columns.is_empty() => return Ok(deduplicate_column_infos(columns)),
|
||||
Ok(columns) => {
|
||||
if let Some(config) = fallback_config.as_ref() {
|
||||
if config.db_type == DatabaseType::Oracle {
|
||||
match oracle_columns_via_sql(
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
&mut client,
|
||||
agent_metadata_timeout(Some(config)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(fallback_columns) if !fallback_columns.is_empty() => return Ok(fallback_columns),
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"[schema][agent:get_columns:oracle-fallback-failed] connection_id={} database={} schema={} table={} error={}",
|
||||
connection_id,
|
||||
database,
|
||||
schema,
|
||||
table,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
match native_postgres_metadata_pool(state, connection_id, database, config).await {
|
||||
Ok(Some(pool)) => {
|
||||
return db::postgres::get_columns(&pool, schema, table)
|
||||
|
||||
@@ -20,6 +20,13 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu
|
||||
let table = qualified_table(dialect, options.schema.as_deref(), &options.table_name);
|
||||
let database_label = database_label(options.database_type);
|
||||
let active_columns: Vec<_> = options.columns.iter().filter(|column| !column.marked_for_drop).collect();
|
||||
if dialect == StructureDialect::Oracle
|
||||
&& active_columns.is_empty()
|
||||
&& options.columns.iter().any(|column| column.marked_for_drop)
|
||||
{
|
||||
warnings.push("Oracle does not allow dropping all columns from a table. Keep at least one column or drop the table instead.".to_string());
|
||||
return Vec::new();
|
||||
}
|
||||
let has_original_column_positions = active_columns.iter().any(|column| column.original_position.is_some());
|
||||
let mut simulated_column_order =
|
||||
if has_original_column_positions { original_active_column_order(&active_columns) } else { Vec::new() };
|
||||
|
||||
@@ -246,6 +246,52 @@ fn builds_informix_column_and_index_changes() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oracle_does_not_generate_drop_sql_for_all_columns() {
|
||||
let mut id = column("id");
|
||||
id.marked_for_drop = true;
|
||||
id.original = Some(ColumnInfo {
|
||||
name: "id".to_string(),
|
||||
data_type: "varchar2(255)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
let mut name = column("name");
|
||||
name.marked_for_drop = true;
|
||||
name.original = Some(ColumnInfo {
|
||||
name: "name".to_string(),
|
||||
data_type: "varchar2(255)".to_string(),
|
||||
is_nullable: true,
|
||||
column_default: None,
|
||||
is_primary_key: false,
|
||||
extra: None,
|
||||
comment: None,
|
||||
});
|
||||
|
||||
let result = build_table_structure_change_sql(TableStructureSqlOptions {
|
||||
database_type: Some(DatabaseType::Oracle),
|
||||
schema: Some("DBX_TEST".to_string()),
|
||||
table_name: "test".to_string(),
|
||||
columns: vec![id, name],
|
||||
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![
|
||||
"Oracle does not allow dropping all columns from a table. Keep at least one column or drop the table instead."
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iris_drop_index_includes_table_name() {
|
||||
let mut old_index = index("index_id", &["ID"]);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
isSqlServerIdentityCompatibleDataType,
|
||||
normalizeDataTypeParams,
|
||||
parseExtraToColumnExtra,
|
||||
rehydrateColumnDraftsFromMetadata,
|
||||
toColumnNames,
|
||||
} from "../../apps/desktop/src/lib/tableStructureEditorState.ts";
|
||||
import type { ColumnInfo, IndexInfo } from "../../apps/desktop/src/types/database.ts";
|
||||
@@ -93,6 +94,73 @@ test("creates editable column drafts from column metadata", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("rehydrates restored existing column drafts from live metadata", () => {
|
||||
const drafts = rehydrateColumnDraftsFromMetadata(
|
||||
[
|
||||
{
|
||||
id: "existing:id",
|
||||
name: "id",
|
||||
dataType: "varchar(10)",
|
||||
isNullable: true,
|
||||
defaultValue: "",
|
||||
comment: "",
|
||||
isPrimaryKey: false,
|
||||
extra: {},
|
||||
markedForDrop: false,
|
||||
},
|
||||
{
|
||||
id: "existing:data",
|
||||
name: "data",
|
||||
dataType: "timestamp",
|
||||
isNullable: true,
|
||||
defaultValue: "",
|
||||
comment: "",
|
||||
isPrimaryKey: false,
|
||||
extra: {},
|
||||
markedForDrop: false,
|
||||
},
|
||||
{
|
||||
id: "new:note",
|
||||
name: "note",
|
||||
dataType: "varchar2(100)",
|
||||
isNullable: true,
|
||||
defaultValue: "",
|
||||
comment: "",
|
||||
isPrimaryKey: false,
|
||||
extra: {},
|
||||
markedForDrop: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: "id",
|
||||
data_type: "varchar(10)",
|
||||
is_nullable: true,
|
||||
column_default: null,
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
name: "data",
|
||||
data_type: "timestamp",
|
||||
is_nullable: true,
|
||||
column_default: null,
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: null,
|
||||
},
|
||||
],
|
||||
"oracle",
|
||||
);
|
||||
|
||||
assert.equal(drafts[0].original?.name, "id");
|
||||
assert.equal(drafts[0].originalPosition, 0);
|
||||
assert.equal(drafts[1].original?.name, "data");
|
||||
assert.equal(drafts[1].originalPosition, 1);
|
||||
assert.equal(drafts[2].original, undefined);
|
||||
});
|
||||
|
||||
test("normalizes PostgreSQL string default casts in editable column drafts", () => {
|
||||
const drafts = createColumnDrafts(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user