diff --git a/src-tauri/src/commands/schema.rs b/src-tauri/src/commands/schema.rs index 17d31e98f1..fd100404ec 100644 --- a/src-tauri/src/commands/schema.rs +++ b/src-tauri/src/commands/schema.rs @@ -297,3 +297,135 @@ pub async fn list_triggers( _ => Ok(vec![]), } } + +#[tauri::command] +pub async fn get_table_ddl( + state: State<'_, Arc>, + connection_id: String, + database: String, + schema: String, + table: String, +) -> Result { + let pool_key = state.get_or_create_pool(&connection_id, Some(&database)).await?; + + { + let connections = state.connections.lock().await; + if let Some(con) = extract_duckdb(&connections, &pool_key) { + drop(connections); + let tbl = table.replace('\'', "''"); + let con = con.lock().map_err(|e| e.to_string())?; + let mut stmt = con.prepare(&format!("SELECT sql FROM duckdb_tables() WHERE table_name = '{tbl}'")) + .map_err(|e| e.to_string())?; + let mut rows = stmt.query([]).map_err(|e| e.to_string())?; + if let Some(row) = rows.next().map_err(|e| e.to_string())? { + return row.get::<_, String>(0).map_err(|e| e.to_string()); + } + return Err("Table not found".to_string()); + } + if let Some(client) = extract_clickhouse(&connections, &pool_key) { + drop(connections); + let result = db::clickhouse_driver::execute_query(&client, &database, &format!("SHOW CREATE TABLE `{table}`")).await?; + return result.rows.first() + .and_then(|r| r.first()) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .ok_or_else(|| "Table not found".to_string()); + } + if let Some(client) = extract_sqlserver(&connections, &pool_key) { + drop(connections); + let mut client = client.lock().await; + return build_sqlserver_ddl(&mut client, &schema, &table).await; + } + } + + let connections = state.connections.lock().await; + let pool = connections.get(&pool_key).ok_or("Pool not found")?; + + match pool { + PoolKind::Mysql(p) => mysql_ddl(p, &table).await, + PoolKind::Postgres(p) => pg_ddl(p, &schema, &table).await, + PoolKind::Sqlite(p) => sqlite_ddl(p, &table).await, + _ => Err("DDL not supported for this database type".to_string()), + } +} + +async fn mysql_ddl(pool: &sqlx::mysql::MySqlPool, table: &str) -> Result { + use sqlx::Row; + let row: sqlx::mysql::MySqlRow = sqlx::query(&format!("SHOW CREATE TABLE `{}`", table.replace('`', "``"))) + .fetch_one(pool).await.map_err(|e| e.to_string())?; + row.try_get::(1) + .or_else(|_| row.try_get::, _>(1).map(|b| String::from_utf8_lossy(&b).to_string())) + .map_err(|e| e.to_string()) +} + +async fn sqlite_ddl(pool: &sqlx::sqlite::SqlitePool, table: &str) -> Result { + use sqlx::Row; + let row: sqlx::sqlite::SqliteRow = sqlx::query("SELECT sql FROM sqlite_master WHERE type='table' AND name=?") + .bind(table) + .fetch_one(pool).await.map_err(|e| e.to_string())?; + row.try_get::(0).map_err(|e| e.to_string()) +} + +async fn pg_ddl(pool: &sqlx::postgres::PgPool, schema: &str, table: &str) -> Result { + let columns = db::postgres::get_columns(pool, schema, table).await?; + let indexes = db::postgres::list_indexes(pool, schema, table).await?; + let fkeys = db::postgres::list_foreign_keys(pool, schema, table).await?; + + let mut ddl = format!("CREATE TABLE \"{schema}\".\"{table}\" (\n"); + let col_lines: Vec = columns.iter().map(|c| { + let mut line = format!(" \"{}\" {}", c.name, c.data_type); + if !c.is_nullable { line.push_str(" NOT NULL"); } + if let Some(ref def) = c.column_default { line.push_str(&format!(" DEFAULT {def}")); } + line + }).collect(); + ddl.push_str(&col_lines.join(",\n")); + + let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect(); + if !pks.is_empty() { + ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.iter().map(|k| format!("\"{k}\"")).collect::>().join(", "))); + } + for fk in &fkeys { + ddl.push_str(&format!(",\n CONSTRAINT \"{}\" FOREIGN KEY (\"{}\") REFERENCES \"{}\"(\"{}\")", fk.name, fk.column, fk.ref_table, fk.ref_column)); + } + ddl.push_str("\n);\n"); + + for idx in &indexes { + if idx.is_primary { continue; } + let unique = if idx.is_unique { "UNIQUE " } else { "" }; + let cols = idx.columns.iter().map(|c| format!("\"{c}\"")).collect::>().join(", "); + ddl.push_str(&format!("\nCREATE {unique}INDEX \"{}\" ON \"{schema}\".\"{table}\" ({cols});", idx.name)); + } + Ok(ddl) +} + +async fn build_sqlserver_ddl(client: &mut db::sqlserver::SqlServerClient, schema: &str, table: &str) -> Result { + let columns = db::sqlserver::get_columns(client, schema, table).await?; + let indexes = db::sqlserver::list_indexes(client, schema, table).await?; + let fkeys = db::sqlserver::list_foreign_keys(client, schema, table).await?; + + let mut ddl = format!("CREATE TABLE [{schema}].[{table}] (\n"); + let col_lines: Vec = columns.iter().map(|c| { + let mut line = format!(" [{}] {}", c.name, c.data_type); + if !c.is_nullable { line.push_str(" NOT NULL"); } + if let Some(ref def) = c.column_default { line.push_str(&format!(" DEFAULT {def}")); } + line + }).collect(); + ddl.push_str(&col_lines.join(",\n")); + + let pks: Vec<&str> = columns.iter().filter(|c| c.is_primary_key).map(|c| c.name.as_str()).collect(); + if !pks.is_empty() { + ddl.push_str(&format!(",\n PRIMARY KEY ({})", pks.iter().map(|k| format!("[{k}]")).collect::>().join(", "))); + } + for fk in &fkeys { + ddl.push_str(&format!(",\n CONSTRAINT [{}] FOREIGN KEY ([{}]) REFERENCES [{}]([{}])", fk.name, fk.column, fk.ref_table, fk.ref_column)); + } + ddl.push_str("\n);\n"); + + for idx in &indexes { + if idx.is_primary { continue; } + let unique = if idx.is_unique { "UNIQUE " } else { "" }; + let cols = idx.columns.iter().map(|c| format!("[{c}]")).collect::>().join(", "); + ddl.push_str(&format!("\nCREATE {unique}INDEX [{}] ON [{schema}].[{table}] ({cols});", idx.name)); + } + Ok(ddl) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4b409fa3e2..f02b4becf1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -47,6 +47,7 @@ pub fn run() { commands::schema::list_indexes, commands::schema::list_foreign_keys, commands::schema::list_triggers, + commands::schema::get_table_ddl, commands::query::execute_query, commands::redis_cmd::redis_list_databases, commands::redis_cmd::redis_scan_keys, diff --git a/src/App.vue b/src/App.vue index c7102435b5..d1f287f4ec 100644 --- a/src/App.vue +++ b/src/App.vue @@ -477,7 +477,7 @@ async function setupFileDrop() {
- +
@@ -627,6 +627,8 @@ async function setupFileDrop() { :result="activeTab.result" :sql="activeTab.sql" :editable="!!activeTab.tableMeta?.primaryKeys?.length" + :connection-id="activeTab.connectionId" + :database="activeTab.database" :table-meta="activeTab.tableMeta" :on-execute-sql="onExecuteSql" @reload="onReloadData" diff --git a/src/components/grid/DataGrid.vue b/src/components/grid/DataGrid.vue index 317675e277..22880b0d14 100644 --- a/src/components/grid/DataGrid.vue +++ b/src/components/grid/DataGrid.vue @@ -1,8 +1,13 @@ + + +
+ +
+
+ + {{ tableMeta?.tableName }} DDL + + +
+
+ +
+

+            
+
@@ -648,6 +746,9 @@ function copySql() { + @@ -708,4 +809,15 @@ function copySql() { min-width: var(--total-w); overflow: visible; } + +.ddl-code :deep(.ddl-kw) { + color: oklch(0.6 0.15 250); + font-weight: 600; +} +.ddl-code :deep(.ddl-ident) { + color: oklch(0.65 0.15 150); +} +.ddl-code :deep(.ddl-str) { + color: oklch(0.65 0.15 50); +} diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts index e354fc14cf..7b12b0a48e 100644 --- a/src/lib/tauri.ts +++ b/src/lib/tauri.ts @@ -85,6 +85,10 @@ export async function listTriggers(connectionId: string, database: string, schem return invoke("list_triggers", { connectionId, database, schema, table }); } +export async function getTableDdl(connectionId: string, database: string, schema: string, table: string): Promise { + return invoke("get_table_ddl", { connectionId, database, schema, table }); +} + export async function saveConnections(configs: ConnectionConfig[]): Promise { return invoke("save_connections", { configs }); }