Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions apps/chat2db-web/src/legacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,12 @@ pub struct LegacyDriverQuery {
pub db_type: String,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LegacyTableDdlExampleQuery {
pub db_type: String,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LegacyMetadataQuery {
Expand Down Expand Up @@ -1958,6 +1964,36 @@ pub(crate) async fn list_keys(
list_indexes(application, query).await
}

/// Returns the native `SHOW CREATE TABLE` result used by Community's export action.
pub(crate) async fn export_table_ddl(
application: &Application,
query: &LegacyTableDetailQuery,
) -> LegacyResult<String> {
let datasource_id = query.data_source_id.as_string();
resolve_mysql_database_type(application, &datasource_id, &query.database_type).await?;
Ok(application
.table_ddl(
&datasource_id,
&query.database_name,
&query.schema_name,
&query.table_name,
)
.await?)
}

/// Preserves Community `MySQL`'s null create/alter example configuration.
pub(crate) fn mysql_table_ddl_example(
query: &LegacyTableDdlExampleQuery,
) -> LegacyResult<Option<String>> {
if normalize_database_type(&query.db_type) != "MYSQL" {
return Err(LegacyFailure {
code: "unsupported_database_type".to_owned(),
message: "This Community compatibility route currently supports MySQL only".to_owned(),
});
}
Ok(None)
}

/// Lists views in the same page wrapper used by the retained tree.
pub(crate) async fn list_views(
application: &Application,
Expand Down Expand Up @@ -5734,6 +5770,22 @@ async fn dispatch_inner(
Err(error) => Err(error),
}
}
("get", "/api/rdb/ddl/export" | "/api/rdb/table/export") => {
match decode::<LegacyTableDetailQuery>(request.message) {
Ok(query) => serialized(export_table_ddl(application, &query).await),
Err(error) => Err(error),
}
}
(
"get",
"/api/rdb/ddl/create/example"
| "/api/rdb/ddl/update/example"
| "/api/rdb/table/create/example"
| "/api/rdb/table/update/example",
) => match decode::<LegacyTableDdlExampleQuery>(request.message) {
Ok(query) => serialized(mysql_table_ddl_example(&query)),
Err(error) => Err(error),
},
("get", "/api/rdb/table/list") => match decode::<LegacyTableListQuery>(request.message) {
Ok(query) => serialized(list_tables(application, &query).await),
Err(error) => Err(error),
Expand Down Expand Up @@ -5983,6 +6035,9 @@ const LEGACY_PATHS: &[&str] = &[
"/api/rdb/table/list",
"/api/rdb/table/table_meta",
"/api/rdb/table/query",
"/api/rdb/table/export",
"/api/rdb/table/create/example",
"/api/rdb/table/update/example",
"/api/rdb/table/modify/sql",
"/api/rdb/table/truncate",
"/api/rdb/table/copy",
Expand All @@ -5993,6 +6048,9 @@ const LEGACY_PATHS: &[&str] = &[
"/api/rdb/ddl/column_list",
"/api/rdb/ddl/index_list",
"/api/rdb/ddl/key_list",
"/api/rdb/ddl/export",
"/api/rdb/ddl/create/example",
"/api/rdb/ddl/update/example",
"/api/rdb/ddl/delete",
"/api/rdb/delete/database/prepare",
"/api/rdb/delete/database/execute",
Expand Down Expand Up @@ -6148,6 +6206,15 @@ pub(crate) fn routes() -> Router<Application> {
.route("/api/rdb/table/list", get(table_list_handler))
.route("/api/rdb/table/table_meta", get(table_meta_handler))
.route("/api/rdb/table/query", get(table_query_handler))
.route("/api/rdb/table/export", get(table_ddl_export_handler))
.route(
"/api/rdb/table/create/example",
get(table_ddl_example_handler),
)
.route(
"/api/rdb/table/update/example",
get(table_ddl_example_handler),
)
.route("/api/rdb/table/modify/sql", post(table_modify_sql_handler))
.route("/api/rdb/table/truncate", post(table_truncate_handler))
.route("/api/rdb/table/copy", post(table_copy_handler))
Expand All @@ -6158,6 +6225,15 @@ pub(crate) fn routes() -> Router<Application> {
.route("/api/rdb/ddl/column_list", get(ddl_column_list_handler))
.route("/api/rdb/ddl/index_list", get(ddl_index_list_handler))
.route("/api/rdb/ddl/key_list", get(ddl_key_list_handler))
.route("/api/rdb/ddl/export", get(table_ddl_export_handler))
.route(
"/api/rdb/ddl/create/example",
get(table_ddl_example_handler),
)
.route(
"/api/rdb/ddl/update/example",
get(table_ddl_example_handler),
)
.route("/api/rdb/ddl/delete", post(table_drop_handler))
.route(
"/api/rdb/delete/database/prepare",
Expand Down Expand Up @@ -6432,6 +6508,19 @@ async fn table_query_handler(
envelope(Box::pin(get_editable_table(&application, &query)).await)
}

async fn table_ddl_export_handler(
State(application): State<Application>,
Query(query): Query<LegacyTableDetailQuery>,
) -> Json<LegacyEnvelope<String>> {
envelope(export_table_ddl(&application, &query).await)
}

async fn table_ddl_example_handler(
Query(query): Query<LegacyTableDdlExampleQuery>,
) -> Json<LegacyEnvelope<Option<String>>> {
envelope(mysql_table_ddl_example(&query))
}

async fn table_modify_sql_handler(
State(application): State<Application>,
Json(request): Json<LegacyTableModifyRequest>,
Expand Down Expand Up @@ -6760,6 +6849,12 @@ mod tests {
const REQUIRED_EDITABLE_PATHS: &[(&str, &str)] = &[
("get", "/api/rdb/table/table_meta"),
("get", "/api/rdb/table/query"),
("get", "/api/rdb/table/export"),
("get", "/api/rdb/table/create/example"),
("get", "/api/rdb/table/update/example"),
("get", "/api/rdb/ddl/export"),
("get", "/api/rdb/ddl/create/example"),
("get", "/api/rdb/ddl/update/example"),
("post", "/api/rdb/table/modify/sql"),
("post", "/api/rdb/ddl/delete"),
("post", "/api/rdb/table/truncate"),
Expand Down Expand Up @@ -7444,6 +7539,52 @@ mod tests {
}
}

#[tokio::test]
async fn mysql_ddl_example_aliases_preserve_community_null_data() {
let application = Application::new();
let router = routes().with_state(application.clone());
for path in [
"/api/rdb/ddl/create/example",
"/api/rdb/ddl/update/example",
"/api/rdb/table/create/example",
"/api/rdb/table/update/example",
] {
let response = router
.clone()
.oneshot(
Request::get(format!("{path}?dbType=MYSQL"))
.body(Body::empty())
.expect("request must build"),
)
.await
.expect("router must respond");
assert_eq!(response.status(), StatusCode::OK);
let body = response
.into_body()
.collect()
.await
.expect("response body must collect")
.to_bytes();
let http: serde_json::Value =
serde_json::from_slice(&body).expect("response must be JSON");
assert_eq!(http["success"], true, "HTTP alias failed: {path}");
assert!(http["data"].is_null(), "HTTP alias returned SQL: {path}");
assert!(http["errorCode"].is_null());
assert!(http["errorMessage"].is_null());

let desktop = dispatch(
&application,
LegacyDispatchRequest {
request_url: path.to_owned(),
method: "get".to_owned(),
message: serde_json::json!({"dbType": "MYSQL"}),
},
)
.await;
assert_eq!(desktop, http, "desktop alias diverged: {path}");
}
}

#[test]
fn metadata_projections_use_the_historical_frontend_field_names() {
let column = column_response(CommunityTableColumn {
Expand Down
40 changes: 40 additions & 0 deletions apps/chat2db-web/tests/native_mysql_editable_ddl_docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,46 @@ async fn verify_product_vertical(config: &MysqlTestConfig, database_name: &str)
)
.await;

let export_message = json!({
"dataSourceId": datasource,
"databaseType": "MYSQL",
"databaseName": database_name,
"schemaName": "",
"tableName": "items"
});
let mut exported_ddl = None;
for path in ["/api/rdb/ddl/export", "/api/rdb/table/export"] {
let http_ddl = get(
&router,
&format!(
"{path}?dataSourceId={datasource}&databaseType=MYSQL&databaseName={database_name}&schemaName=&tableName=items"
),
)
.await;
let http_ddl = http_ddl.as_str().expect("exported DDL must be a string");
assert!(http_ddl.starts_with("CREATE TABLE `items`"));
assert!(http_ddl.contains("`label` varchar(128) NOT NULL COMMENT 'editable label'"));
assert!(http_ddl.ends_with(';'));
if let Some(expected) = exported_ddl.as_deref() {
assert_eq!(http_ddl, expected, "HTTP export aliases diverged");
} else {
exported_ddl = Some(http_ddl.to_owned());
}

let desktop = chat2db_web::legacy::dispatch(
&application,
chat2db_web::legacy::LegacyDispatchRequest {
request_url: path.to_owned(),
method: "get".to_owned(),
message: export_message.clone(),
},
)
.await;
assert_eq!(desktop["success"], true, "desktop export failed: {desktop}");
assert_eq!(desktop["data"], http_ddl, "desktop export diverged: {path}");
}
assert_java_dormant(&application);

let old_table = get(
&router,
&format!(
Expand Down
15 changes: 15 additions & 0 deletions crates/chat2db-core/src/community.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,21 @@ impl Application {
.await
}

/// Reads a `MySQL` table definition through the native driver without starting Java.
///
/// # Errors
///
/// Returns datasource, metadata, connection, or identifier validation errors.
pub async fn table_ddl(
&self,
data_source_id: &str,
database_name: &str,
schema_name: &str,
table_name: &str,
) -> Result<String, AppError> {
native_mysql::table_ddl(self, data_source_id, database_name, schema_name, table_name).await
}

/// Lists indexes through Community metadata using a forced read-only session.
///
/// # Errors
Expand Down
24 changes: 24 additions & 0 deletions crates/chat2db-core/src/native_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,30 @@ pub(crate) async fn get_view(
finish_connection(conn, result).await
}

pub(crate) async fn table_ddl(
application: &Application,
datasource_id: &str,
database_name: &str,
_schema_name: &str,
table_name: &str,
) -> Result<String, AppError> {
validate_metadata_identifier(database_name, "databaseName")?;
validate_metadata_identifier(table_name, "tableName")?;
let qualified_name =
qualified_identifier(database_name, "databaseName", table_name, "tableName")?;
let resolved = resolve_native_connection(application, datasource_id).await?;
let mut conn = open_connection(&resolved.connection).await?;
let result =
metadata_query(conn.query_first::<Row, _>(format!("SHOW CREATE TABLE {qualified_name}")))
.await
.and_then(|row| {
let row =
row.ok_or_else(|| metadata_not_found("table", database_name, table_name))?;
row_string_at(&row, 1).map(|ddl| format!("{ddl};"))
});
finish_connection(conn, result).await
}

pub(crate) async fn list_imported_keys(
application: &Application,
datasource_id: &str,
Expand Down
16 changes: 16 additions & 0 deletions crates/chat2db-core/tests/native_mysql_product.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,22 @@ async fn verify_native_metadata(
assert_eq!(table.table_type, "TABLE");
assert_eq!(table.engine, "InnoDB");
assert_java_dormant(application);

let ddl = application
.table_ddl(datasource_id, database_name, "", "items")
.await
.expect("native MySQL table DDL must load");
assert!(ddl.starts_with("CREATE TABLE `items`"));
assert!(ddl.contains("CONSTRAINT `fk_items_category`"));
assert!(ddl.ends_with(';'));
assert_java_dormant(application);

let invalid = application
.table_ddl(datasource_id, database_name, "", "")
.await
.expect_err("an empty MySQL table identifier must be rejected");
assert_eq!(invalid.api_error().code, "invalid_mysql_metadata_request");
assert_java_dormant(application);
}

#[allow(clippy::too_many_lines)]
Expand Down
19 changes: 14 additions & 5 deletions docs/mysql-community-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
## Status

- Community baseline: `OtterMind/Chat2DB` `main@3cb8af54cad5bd5caa20bb25f10d9b0e4f01931c`.
- Rust baseline: `OtterMind/Chat2DB-Rust` `main@e6e0c9ff07b4d2c8d07d8ce8a0ea12849e596db4`.
- Rust baseline: `OtterMind/Chat2DB-Rust` `main@fb042aa056a7dbc18969f1bc191d2b771fea787d`.
- Current Issue `#10` branch: commits `1e52a69` and `21947cb` add native
table-DDL retrieval plus the original Web and Tauri route aliases.
- Product target: the original Community React frontend running against the Rust Web or Tauri host.
- Runtime-tested now: datasource CRUD/test; database, schema, table, column,
index, foreign-key, primary-key, view, function, procedure, trigger, and
Expand Down Expand Up @@ -33,7 +35,11 @@
invisible, `ZEROFILL`, or otherwise unmodeled column is rejected after a live
metadata check instead of emitting a lossy `MODIFY COLUMN`. MySQL `view_meta`
returns the original six form configurations and creation template without
requiring an existing view.
requiring an existing view. Native `SHOW CREATE TABLE` now backs both
`/api/rdb/ddl/export` and `/api/rdb/table/export`; the four Community
create/update example aliases preserve MySQL's successful `data: null`
contract. HTTP and desktop dispatch return identical envelopes, and the real
MySQL 8.4 vertical proves Java remains dormant.
- Complete parity: not implemented.

This file is the acceptance contract for MySQL work. Community frontend routes
Expand Down Expand Up @@ -66,7 +72,7 @@ route reaches it and a real MySQL product test covers the behavior.
| Database and schema mutation | database create/modify/delete and `/api/rdb/delete/{database,schema}/{prepare,execute}` | Historical create-SQL routes and two-phase confirmed database/schema deletion are implemented; database create/delete is real-MySQL tested | Add unsupported database alteration fields and close remaining exact projection differences. |
| Table inventory and detail | `/api/rdb/table/list`, `/table_list`, `/table_meta`, `/column_list`, `/index_list`, `/key_list`, `/query` | List, compact list, table metadata/query, column, index, and key routes are implemented with native MySQL metadata; nullable defaults, type-suffix-aware `UNSIGNED`/`ZEROFILL`, empty and quoted ENUM/SET values, composite primary-key order, and legacy envelopes match the retained editor and are real-MySQL tested | Close remaining field-level differences as original editor scenarios expose them. |
| Table data operations | `/api/rdb/dml/execute_table`, `/execute_update`, `/get_update_sql`, `/copy_update_sql`, `/copy_in_values_sql`, `/count` | Editable previews, PK-first optimistic insert/update/delete SQL, bounded native execution, copy-as-INSERT/UPDATE/WHERE, frontend `IN_VALUES`, and protected count queries are implemented and real-MySQL tested | Close remaining clipboard and uncommon result-type differences. |
| Table DDL | `/api/rdb/ddl/*`, `/api/rdb/table/modify/sql`, `/delete`, `/truncate`, `/copy`, create/update examples, DDL export | Create/alter/drop/truncate/copy previews and execution are implemented for columns, indexes, engine, charset, collation, comments, auto-increment, and MySQL editor types; explicit-null editor rows and drag-only `FIRST`/`AFTER` reordering are real-MySQL tested, while live metadata rejects generated, invisible, `ZEROFILL`, and other unmodeled columns before a lossy reorder | Add foreign-key mutation, examples/export, and remaining table options. |
| Table DDL | `/api/rdb/ddl/*`, `/api/rdb/table/modify/sql`, `/delete`, `/truncate`, `/copy`, create/update examples, DDL export | Create/alter/drop/truncate/copy previews and execution are implemented for columns, indexes, engine, charset, collation, comments, auto-increment, and MySQL editor types; explicit-null editor rows and drag-only `FIRST`/`AFTER` reordering are real-MySQL tested, while live metadata rejects generated, invisible, `ZEROFILL`, and other unmodeled columns before a lossy reorder. Native `SHOW CREATE TABLE` backs both export aliases with the Community trailing semicolon; all four MySQL example aliases preserve Community's null response. Foreign keys are implemented as read-only metadata; pinned Community `MysqlSqlBuilder` and `MysqlIndexTypeEnum` do not generate or modify `foreignKeyList`, and the Community MySQL editor exposes no foreign-key mutation contract. | Add remaining table options and close field-level edge cases; foreign-key mutation is not a current Community parity requirement, while foreign-key metadata remains available to read-only metadata and future ER flows. |
| Views | `/api/rdb/view/list`, `/column_list`, `/detail`, `/query`, `/view_meta`, `/modify/sql`, `/delete`, `/drop` | Native list/detail plus historical query, the six-option Community `view_meta` creation template, create-or-replace preview/execution, and drop are implemented and real-MySQL tested | Add any remaining delete alias and uncommon definer/security projection differences. |
| Functions, procedures, and triggers | `/api/rdb/{function,procedure,trigger}/{list,detail}`, `/api/rdb/routine/{preview_invocation,preview_migration,execute_migration}` | Native list/detail and routine-parameter projections are implemented; every original list/detail route is mapped | Add invocation and migration preview/execution flows. |
| Console SELECT | `/api/rdb/dml/execute`, desktop `sql-execute`/`sql-cancel` | Native unparameterized MySQL reads, CTEs, normal/all-row paging, preserved-single dispatch, `EXPLAIN`, limits, multiple result sets, affected-row counts, datasource read-only enforcement, and cancellation implemented | Add JDBC-style bind parameters and close remaining warning/error/result-shape differences. |
Expand All @@ -90,8 +96,11 @@ route reaches it and a real MySQL product test covers the behavior.
large-cell retrieval. Bind parameters and remaining exact Community edge-case
conformance are still required for complete parity.
3. Implemented slice: table data editing plus database/schema/table/view DDL
preview and execution. Foreign-key mutation, DDL export/examples, and
remaining exact Community edge cases are still required for complete parity.
preview and execution, native table DDL retrieval/export, and the Community
create/update example route aliases.
Foreign-key mutation is not a current Community MySQL editor requirement;
foreign keys remain read-only metadata for metadata and future ER flows.
Remaining exact Community edge cases are still required for complete parity.
4. Import/export/tasks, datasource lifecycle/SSH/import, account administration,
routines, structure comparison, pins, and ER metadata.
5. Original AI mapping and MySQL conformance for Agent, CLI, and MCP.
Expand Down
Loading