From d7b6461ed78237d5585704e527d1947160c75df1 Mon Sep 17 00:00:00 2001 From: SuLe Date: Sat, 2 May 2026 09:32:41 +0800 Subject: [PATCH] remove superpowers docs from sql file pr --- .../plans/2026-05-02-sql-file-execution.md | 1322 ----------------- .../2026-05-02-sql-file-execution-design.md | 134 -- 2 files changed, 1456 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-02-sql-file-execution.md delete mode 100644 docs/superpowers/specs/2026-05-02-sql-file-execution-design.md diff --git a/docs/superpowers/plans/2026-05-02-sql-file-execution.md b/docs/superpowers/plans/2026-05-02-sql-file-execution.md deleted file mode 100644 index f4bcaa2f72..0000000000 --- a/docs/superpowers/plans/2026-05-02-sql-file-execution.md +++ /dev/null @@ -1,1322 +0,0 @@ -# SQL File Execution Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build a safe SQL file execution/import workflow with toolbar and tree context-menu entry points, backend streaming execution, progress events, stop-on-error defaults, and cancellation. - -**Architecture:** Add a focused Rust command module for SQL file preview, splitting, execution, progress events, and cancellation. Reuse the existing query execution path by extracting a small shared helper from `commands/query.rs`, then add a Vue dialog component that owns file selection and progress UI. - -**Tech Stack:** Tauri 2 commands/events, Rust async file IO, existing `tokio_util::sync::CancellationToken`, Vue 3 + Pinia + shadcn-vue/reka UI, existing i18n files, existing Tauri dialog/fs patterns. - ---- - -## File Structure - -- Create `src-tauri/src/commands/sql_file.rs` - - SQL splitter. - - File preview command. - - SQL file execution request/progress types. - - File execution command and cancel command. - - Rust unit tests for splitting and execution control flow. -- Modify `src-tauri/src/commands/query.rs` - - Extract a reusable `execute_sql_statement` helper that accepts an optional cancellation token. - - Keep existing `execute_query` behavior unchanged. -- Modify `src-tauri/src/commands/mod.rs` - - Register `sql_file` module. -- Modify `src-tauri/src/lib.rs` - - Register `preview_sql_file`, `execute_sql_file`, and `cancel_sql_file_execution`. -- Modify `src/lib/tauri.ts` - - Add request/progress/preview types. - - Add wrappers and event subscription helper. -- Modify `src/stores/connectionStore.ts` - - Add `sqlFileSource` prefill state mirroring transfer/schema diff patterns. -- Create `src/components/sql-file/SqlFileExecutionDialog.vue` - - File picker, connection/database selectors, preview metadata, progress UI, cancel. -- Modify `src/App.vue` - - Toolbar entry. - - Dialog state, prefill watcher, and component mount. -- Modify `src/components/sidebar/TreeItem.vue` - - Context-menu entry on connection/database/schema nodes. -- Modify `src/i18n/locales/en.ts` and `src/i18n/locales/zh-CN.ts` - - Add `sqlFile` strings and context-menu labels. - ---- - -### Task 1: Backend SQL Splitter - -**Files:** -- Create: `src-tauri/src/commands/sql_file.rs` -- Modify: `src-tauri/src/commands/mod.rs` - -- [ ] **Step 1: Add failing splitter tests** - -Add `mod sql_file;` to `src-tauri/src/commands/mod.rs`. - -Create `src-tauri/src/commands/sql_file.rs` with tests first: - -```rust -#[cfg(test)] -mod tests { - use super::split_sql_script; - - #[test] - fn splits_semicolon_delimited_statements() { - assert_eq!( - split_sql_script("CREATE TABLE a(id int); INSERT INTO a VALUES (1);").unwrap(), - vec!["CREATE TABLE a(id int)", "INSERT INTO a VALUES (1)"] - ); - } - - #[test] - fn ignores_semicolons_inside_quotes_and_comments() { - let sql = "\ - INSERT INTO logs VALUES ('a;b', \"c;d\", `weird;name`);\n\ - -- comment ; ignored\n\ - /* block ; ignored */\n\ - SELECT 1;"; - assert_eq!( - split_sql_script(sql).unwrap(), - vec![ - "INSERT INTO logs VALUES ('a;b', \"c;d\", `weird;name`)", - "-- comment ; ignored\n/* block ; ignored */\nSELECT 1", - ] - ); - } - - #[test] - fn emits_trailing_statement_without_semicolon() { - assert_eq!( - split_sql_script("CREATE TABLE a(id int);\nINSERT INTO a VALUES (1)").unwrap(), - vec!["CREATE TABLE a(id int)", "INSERT INTO a VALUES (1)"] - ); - } -} -``` - -- [ ] **Step 2: Run splitter tests and verify RED** - -Run: - -```bash -cd src-tauri -cargo test sql_file::tests::splits_semicolon_delimited_statements --lib -``` - -Expected: fail because `split_sql_script` does not exist. - -- [ ] **Step 3: Implement minimal splitter** - -In `src-tauri/src/commands/sql_file.rs`, add: - -```rust -use serde::{Deserialize, Serialize}; - -#[derive(Default)] -struct SqlStatementSplitter { - buffer: String, - in_single_quote: bool, - in_double_quote: bool, - in_backtick: bool, - in_line_comment: bool, - in_block_comment: bool, - previous: Option, -} - -impl SqlStatementSplitter { - fn push_chunk(&mut self, chunk: &str) -> Vec { - let mut statements = Vec::new(); - let mut chars = chunk.chars().peekable(); - - while let Some(ch) = chars.next() { - let next = chars.peek().copied(); - - if self.in_line_comment { - self.buffer.push(ch); - if ch == '\n' { - self.in_line_comment = false; - } - self.previous = Some(ch); - continue; - } - - if self.in_block_comment { - self.buffer.push(ch); - if self.previous == Some('*') && ch == '/' { - self.in_block_comment = false; - } - self.previous = Some(ch); - continue; - } - - if !self.in_single_quote && !self.in_double_quote && !self.in_backtick { - if ch == '-' && next == Some('-') { - self.in_line_comment = true; - self.buffer.push(ch); - self.previous = Some(ch); - continue; - } - if ch == '/' && next == Some('*') { - self.in_block_comment = true; - self.buffer.push(ch); - self.previous = Some(ch); - continue; - } - } - - match ch { - '\'' if !self.in_double_quote && !self.in_backtick && self.previous != Some('\\') => { - self.in_single_quote = !self.in_single_quote; - self.buffer.push(ch); - } - '"' if !self.in_single_quote && !self.in_backtick && self.previous != Some('\\') => { - self.in_double_quote = !self.in_double_quote; - self.buffer.push(ch); - } - '`' if !self.in_single_quote && !self.in_double_quote => { - self.in_backtick = !self.in_backtick; - self.buffer.push(ch); - } - ';' if !self.in_single_quote && !self.in_double_quote && !self.in_backtick => { - self.push_current_statement(&mut statements); - } - _ => self.buffer.push(ch), - } - - self.previous = Some(ch); - } - - statements - } - - fn finish(mut self) -> Vec { - let mut statements = Vec::new(); - self.push_current_statement(&mut statements); - statements - } - - fn push_current_statement(&mut self, statements: &mut Vec) { - let statement = self.buffer.trim(); - if !statement.is_empty() { - statements.push(statement.to_string()); - } - self.buffer.clear(); - self.previous = None; - } -} - -#[cfg(test)] -fn split_sql_script(sql: &str) -> Result, String> { - let mut splitter = SqlStatementSplitter::default(); - let mut statements = splitter.push_chunk(sql); - statements.extend(splitter.finish()); - Ok(statements) -} -``` - -- [ ] **Step 4: Run splitter tests and verify GREEN** - -Run: - -```bash -cd src-tauri -cargo test sql_file::tests --lib -``` - -Expected: all splitter tests pass. - -- [ ] **Step 5: Commit** - -```bash -git add src-tauri/src/commands/mod.rs src-tauri/src/commands/sql_file.rs -git commit -m "add sql file statement splitter" -``` - ---- - -### Task 2: Shared Query Execution Helper - -**Files:** -- Modify: `src-tauri/src/commands/query.rs` -- Test: `src-tauri/src/commands/query.rs` - -- [ ] **Step 1: Add helper-level cancellation regression test** - -Add this test to `src-tauri/src/commands/query.rs` tests module: - -```rust -#[tokio::test] -async fn wait_for_query_without_token_still_times_out() { - let result = wait_for_query(None, async { - tokio::time::sleep(Duration::from_secs(31)).await; - Ok(db::QueryResult { - columns: vec![], - rows: vec![], - affected_rows: 0, - execution_time_ms: 0, - truncated: false, - }) - }) - .await; - - assert_eq!(result.unwrap_err(), timeout_error()); -} -``` - -- [ ] **Step 2: Run query tests and verify they pass before refactor** - -Run: - -```bash -cd src-tauri -cargo test commands::query::tests --lib -``` - -Expected: query tests pass before refactor. - -- [ ] **Step 3: Extract reusable helper** - -In `src-tauri/src/commands/query.rs`, add a public-in-commands helper below `do_execute`: - -```rust -pub(super) async fn execute_sql_statement( - state: &AppState, - connection_id: &str, - database: &str, - sql: &str, - cancel_token: Option, -) -> Result { - let pool_key = if database.is_empty() { - connection_id.to_string() - } else { - state.get_or_create_pool(connection_id, Some(database)).await? - }; - - if is_canceled(&cancel_token) { - return Err(canceled_error()); - } - - let result = do_execute(state, &pool_key, sql, cancel_token.clone()).await; - - match &result { - Err(e) if is_connection_error(e) && !is_canceled(&cancel_token) => { - let db_opt = if database.is_empty() { None } else { Some(database) }; - let new_key = state.reconnect_pool(connection_id, db_opt).await?; - do_execute(state, &new_key, sql, cancel_token).await - } - _ => result, - } -} -``` - -Then simplify `execute_query` to call it: - -```rust -let result = execute_sql_statement( - &state, - &connection_id, - &database, - &sql, - cancel_token, -) -.await; - -result -``` - -- [ ] **Step 4: Run query tests** - -Run: - -```bash -cd src-tauri -cargo test commands::query::tests --lib -``` - -Expected: all query tests pass. - -- [ ] **Step 5: Commit** - -```bash -git add src-tauri/src/commands/query.rs -git commit -m "share query execution helper" -``` - ---- - -### Task 3: Backend SQL File Preview, Execution, Progress, And Cancel - -**Files:** -- Modify: `src-tauri/src/commands/sql_file.rs` -- Modify: `src-tauri/src/lib.rs` - -- [ ] **Step 1: Add backend control-flow tests** - -Add test-only fake execution helpers in `src-tauri/src/commands/sql_file.rs`: - -```rust -#[cfg(test)] -mod execution_tests { - use super::*; - use tokio_util::sync::CancellationToken; - - async fn run_fake_script( - statements: Vec, - continue_on_error: bool, - cancel_after_successes: Option, - ) -> SqlFileSummary { - let token = CancellationToken::new(); - run_statements_for_test(statements, continue_on_error, token, cancel_after_successes).await - } - - #[tokio::test] - async fn stops_on_first_failure_by_default() { - let summary = run_fake_script( - vec!["ok 1".into(), "fail 2".into(), "ok 3".into()], - false, - None, - ).await; - - assert_eq!(summary.success_count, 1); - assert_eq!(summary.failure_count, 1); - assert_eq!(summary.status, SqlFileStatus::Error); - assert_eq!(summary.failed_statement_index, Some(2)); - } - - #[tokio::test] - async fn continues_after_failure_when_enabled() { - let summary = run_fake_script( - vec!["ok 1".into(), "fail 2".into(), "ok 3".into()], - true, - None, - ).await; - - assert_eq!(summary.success_count, 2); - assert_eq!(summary.failure_count, 1); - assert_eq!(summary.status, SqlFileStatus::Done); - } - - #[tokio::test] - async fn cancellation_stops_before_next_statement() { - let summary = run_fake_script( - vec!["ok 1".into(), "ok 2".into(), "ok 3".into()], - true, - Some(1), - ).await; - - assert_eq!(summary.success_count, 1); - assert_eq!(summary.status, SqlFileStatus::Cancelled); - } -} -``` - -- [ ] **Step 2: Run backend control-flow tests and verify RED** - -Run: - -```bash -cd src-tauri -cargo test sql_file::execution_tests --lib -``` - -Expected: fail because `SqlFileSummary`, `SqlFileStatus`, and `run_statements_for_test` do not exist. - -- [ ] **Step 3: Add command types and execution implementation** - -Add these types and command functions to `src-tauri/src/commands/sql_file.rs`: - -```rust -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::Instant; - -use serde::{Deserialize, Serialize}; -use tauri::{AppHandle, Emitter, State}; -use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::sync::RwLock; -use tokio_util::sync::CancellationToken; - -use crate::commands::connection::AppState; -use crate::commands::query::execute_sql_statement; - -static SQL_FILE_EXECUTIONS: std::sync::LazyLock>> = - std::sync::LazyLock::new(|| RwLock::new(HashMap::new())); - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SqlFileRequest { - pub execution_id: String, - pub connection_id: String, - pub database: String, - pub file_path: String, - pub continue_on_error: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SqlFilePreview { - pub file_name: String, - pub file_path: String, - pub size_bytes: u64, - pub preview: String, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum SqlFileStatus { - Started, - Running, - StatementDone, - StatementFailed, - Done, - Error, - Cancelled, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SqlFileProgress { - pub execution_id: String, - pub status: SqlFileStatus, - pub statement_index: usize, - pub success_count: usize, - pub failure_count: usize, - pub affected_rows: u64, - pub elapsed_ms: u128, - pub statement_summary: String, - pub error: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct SqlFileSummary { - status: SqlFileStatus, - success_count: usize, - failure_count: usize, - failed_statement_index: Option, -} -``` - -Add preview command: - -```rust -#[tauri::command] -pub async fn preview_sql_file(file_path: String) -> Result { - let path = PathBuf::from(&file_path); - let metadata = tokio::fs::metadata(&path).await.map_err(|e| e.to_string())?; - let mut file = tokio::fs::File::open(&path).await.map_err(|e| e.to_string())?; - let mut buffer = vec![0; 4096]; - let bytes_read = tokio::io::AsyncReadExt::read(&mut file, &mut buffer) - .await - .map_err(|e| e.to_string())?; - buffer.truncate(bytes_read); - let preview = String::from_utf8_lossy(&buffer).to_string(); - - Ok(SqlFilePreview { - file_name: path - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or("script.sql") - .to_string(), - file_path, - size_bytes: metadata.len(), - preview, - }) -} -``` - -Add execution command: - -```rust -#[tauri::command] -pub async fn execute_sql_file( - app: AppHandle, - state: State<'_, Arc>, - request: SqlFileRequest, -) -> Result<(), String> { - let token = CancellationToken::new(); - SQL_FILE_EXECUTIONS - .write() - .await - .insert(request.execution_id.clone(), token.clone()); - - let started_at = Instant::now(); - emit_progress(&app, &request.execution_id, SqlFileStatus::Started, 0, 0, 0, 0, started_at, "", None); - - let result = execute_sql_file_inner(&app, &state, &request, token, started_at).await; - SQL_FILE_EXECUTIONS.write().await.remove(&request.execution_id); - result -} - -#[tauri::command] -pub async fn cancel_sql_file_execution(execution_id: String) -> Result { - let executions = SQL_FILE_EXECUTIONS.read().await; - if let Some(token) = executions.get(&execution_id) { - token.cancel(); - Ok(true) - } else { - Ok(false) - } -} -``` - -Add the execution helpers: - -```rust -async fn execute_sql_file_inner( - app: &AppHandle, - state: &AppState, - request: &SqlFileRequest, - token: CancellationToken, - started_at: Instant, -) -> Result<(), String> { - let file = tokio::fs::File::open(&request.file_path).await.map_err(|e| e.to_string())?; - let mut reader = BufReader::new(file); - let mut splitter = SqlStatementSplitter::default(); - let mut line = String::new(); - let mut statement_index = 0; - let mut success_count = 0; - let mut failure_count = 0; - let mut affected_rows = 0; - - loop { - line.clear(); - let read = reader.read_line(&mut line).await.map_err(|e| e.to_string())?; - if read == 0 { - break; - } - for statement in splitter.push_chunk(&line) { - statement_index += 1; - let outcome = execute_statement_with_progress( - app, - state, - request, - &token, - statement_index, - &statement, - &mut success_count, - &mut failure_count, - &mut affected_rows, - started_at, - ) - .await; - if outcome? { - return Ok(()); - } - } - } - - for statement in splitter.finish() { - statement_index += 1; - let outcome = execute_statement_with_progress( - app, - state, - request, - &token, - statement_index, - &statement, - &mut success_count, - &mut failure_count, - &mut affected_rows, - started_at, - ) - .await; - if outcome? { - return Ok(()); - } - } - - emit_progress(app, &request.execution_id, SqlFileStatus::Done, statement_index, success_count, failure_count, affected_rows, started_at, "", None); - Ok(()) -} - -async fn execute_statement_with_progress( - app: &AppHandle, - state: &AppState, - request: &SqlFileRequest, - token: &CancellationToken, - statement_index: usize, - statement: &str, - success_count: &mut usize, - failure_count: &mut usize, - affected_rows: &mut u64, - started_at: Instant, -) -> Result { - if token.is_cancelled() { - emit_progress(app, &request.execution_id, SqlFileStatus::Cancelled, statement_index, *success_count, *failure_count, *affected_rows, started_at, "", None); - return Ok(true); - } - - emit_progress(app, &request.execution_id, SqlFileStatus::Running, statement_index, *success_count, *failure_count, *affected_rows, started_at, statement, None); - - match execute_sql_statement( - state, - &request.connection_id, - &request.database, - statement, - Some(token.clone()), - ).await { - Ok(result) => { - *success_count += 1; - *affected_rows += result.affected_rows; - emit_progress(app, &request.execution_id, SqlFileStatus::StatementDone, statement_index, *success_count, *failure_count, *affected_rows, started_at, statement, None); - Ok(false) - } - Err(err) => { - *failure_count += 1; - let status = if token.is_cancelled() { SqlFileStatus::Cancelled } else { SqlFileStatus::StatementFailed }; - emit_progress(app, &request.execution_id, status, statement_index, *success_count, *failure_count, *affected_rows, started_at, statement, Some(err.clone())); - if token.is_cancelled() { - return Ok(true); - } - if request.continue_on_error { - Ok(false) - } else { - emit_progress(app, &request.execution_id, SqlFileStatus::Error, statement_index, *success_count, *failure_count, *affected_rows, started_at, statement, Some(format!("Statement {statement_index} failed: {err}. Previous {} statement(s) may have been committed.", statement_index.saturating_sub(1)))); - Ok(true) - } - } - } -} - -fn emit_progress( - app: &AppHandle, - execution_id: &str, - status: SqlFileStatus, - statement_index: usize, - success_count: usize, - failure_count: usize, - affected_rows: u64, - started_at: Instant, - statement: &str, - error: Option, -) { - let _ = app.emit("sql-file-progress", SqlFileProgress { - execution_id: execution_id.to_string(), - status, - statement_index, - success_count, - failure_count, - affected_rows, - elapsed_ms: started_at.elapsed().as_millis(), - statement_summary: statement_summary(statement), - error, - }); -} - -fn statement_summary(statement: &str) -> String { - let collapsed = statement.split_whitespace().collect::>().join(" "); - if collapsed.chars().count() <= 120 { - collapsed - } else { - let mut summary = collapsed.chars().take(117).collect::(); - summary.push_str("..."); - summary - } -} -``` - -Use this outcome convention throughout Task 3: `Ok(true)` means stop because cancelled or stop-on-error already emitted a terminal event. - -Add the test-only fake runner required by the tests: - -```rust -#[cfg(test)] -async fn run_statements_for_test( - statements: Vec, - continue_on_error: bool, - token: CancellationToken, - cancel_after_successes: Option, -) -> SqlFileSummary { - let mut success_count = 0; - let mut failure_count = 0; - let mut failed_statement_index = None; - - for (idx, statement) in statements.iter().enumerate() { - if token.is_cancelled() { - return SqlFileSummary { - status: SqlFileStatus::Cancelled, - success_count, - failure_count, - failed_statement_index, - }; - } - - if statement.starts_with("fail") { - failure_count += 1; - failed_statement_index = Some(idx + 1); - if !continue_on_error { - return SqlFileSummary { - status: SqlFileStatus::Error, - success_count, - failure_count, - failed_statement_index, - }; - } - } else { - success_count += 1; - if cancel_after_successes == Some(success_count) { - token.cancel(); - } - } - } - - SqlFileSummary { - status: if token.is_cancelled() { SqlFileStatus::Cancelled } else { SqlFileStatus::Done }, - success_count, - failure_count, - failed_statement_index, - } -} -``` - -- [ ] **Step 4: Register commands** - -In `src-tauri/src/lib.rs`, add to `generate_handler!`: - -```rust -commands::sql_file::preview_sql_file, -commands::sql_file::execute_sql_file, -commands::sql_file::cancel_sql_file_execution, -``` - -- [ ] **Step 5: Run backend tests** - -Run: - -```bash -cd src-tauri -cargo test sql_file --lib -cargo test commands::query::tests --lib -``` - -Expected: tests pass. - -- [ ] **Step 6: Commit** - -```bash -git add src-tauri/src/commands/sql_file.rs src-tauri/src/lib.rs -git commit -m "execute sql files with progress" -``` - ---- - -### Task 4: Tauri API Wrappers And Store Prefill - -**Files:** -- Modify: `src/lib/tauri.ts` -- Modify: `src/stores/connectionStore.ts` - -- [ ] **Step 1: Add TypeScript API types** - -In `src/lib/tauri.ts`, add: - -```ts -export type SqlFileStatus = - | "started" - | "running" - | "statementDone" - | "statementFailed" - | "done" - | "error" - | "cancelled"; - -export interface SqlFileRequest { - executionId: string; - connectionId: string; - database: string; - filePath: string; - continueOnError: boolean; -} - -export interface SqlFilePreview { - fileName: string; - filePath: string; - sizeBytes: number; - preview: string; -} - -export interface SqlFileProgress { - executionId: string; - status: SqlFileStatus; - statementIndex: number; - successCount: number; - failureCount: number; - affectedRows: number; - elapsedMs: number; - statementSummary: string; - error?: string | null; -} -``` - -- [ ] **Step 2: Add wrappers** - -In `src/lib/tauri.ts`, add: - -```ts -export async function previewSqlFile(filePath: string): Promise { - return invoke("preview_sql_file", { filePath }); -} - -export async function executeSqlFile(request: SqlFileRequest): Promise { - return invoke("execute_sql_file", { request }); -} - -export async function cancelSqlFileExecution(executionId: string): Promise { - return invoke("cancel_sql_file_execution", { executionId }); -} - -export async function listenSqlFileProgress( - handler: (progress: SqlFileProgress) => void, -): Promise { - return listen("sql-file-progress", (event) => handler(event.payload)); -} -``` - -- [ ] **Step 3: Add connection store prefill** - -In `src/stores/connectionStore.ts`, add beside transfer/schema diff refs: - -```ts -const sqlFileSource = ref<{ connectionId: string; database: string } | null>(null); -``` - -Return it from the store: - -```ts -sqlFileSource, -``` - -- [ ] **Step 4: Run frontend typecheck** - -Run: - -```bash -pnpm build -``` - -Expected: build passes or fails only because the dialog is not created yet. If it fails because the new wrappers are unused, continue; TypeScript should not fail on unused exports. - -- [ ] **Step 5: Commit** - -```bash -git add src/lib/tauri.ts src/stores/connectionStore.ts -git commit -m "add sql file frontend api" -``` - ---- - -### Task 5: SQL File Execution Dialog - -**Files:** -- Create: `src/components/sql-file/SqlFileExecutionDialog.vue` - -- [ ] **Step 1: Create dialog component** - -Create `src/components/sql-file/SqlFileExecutionDialog.vue`: - -```vue - - - -``` - -- [ ] **Step 2: Run frontend build** - -Run: - -```bash -pnpm build -``` - -Expected: build passes. If TypeScript reports that `Checkbox` requires `boolean | "indeterminate"`, change the binding to `:checked="continueOnError" @update:checked="(value) => { continueOnError = value === true }"` and rerun `pnpm build`. - -- [ ] **Step 3: Commit** - -```bash -git add src/components/sql-file/SqlFileExecutionDialog.vue -git commit -m "add sql file execution dialog" -``` - ---- - -### Task 6: Toolbar And Tree Entry Points - -**Files:** -- Modify: `src/App.vue` -- Modify: `src/components/sidebar/TreeItem.vue` -- Modify: `src/i18n/locales/en.ts` -- Modify: `src/i18n/locales/zh-CN.ts` - -- [ ] **Step 1: Add App state and dialog mount** - -In `src/App.vue`, import the dialog and icon: - -```ts -import SqlFileExecutionDialog from "@/components/sql-file/SqlFileExecutionDialog.vue"; -``` - -Add state beside transfer/schema diff state: - -```ts -const showSqlFileDialog = ref(false); -const sqlFilePrefillConnectionId = ref(""); -const sqlFilePrefillDatabase = ref(""); -``` - -Add watcher: - -```ts -watch(() => connectionStore.sqlFileSource, (v) => { - if (v) { - sqlFilePrefillConnectionId.value = v.connectionId; - sqlFilePrefillDatabase.value = v.database; - showSqlFileDialog.value = true; - connectionStore.sqlFileSource = null; - } -}); -``` - -Add toolbar button after Data Transfer: - -```vue - -``` - -Mount dialog near existing dialogs: - -```vue - -``` - -- [ ] **Step 2: Add tree context action** - -In `src/components/sidebar/TreeItem.vue`, add: - -```ts -function openSqlFileExecution() { - if (props.node.connectionId) { - connectionStore.sqlFileSource = { - connectionId: props.node.connectionId, - database: props.node.database ?? "", - }; - } -} -``` - -For connection menu, add after New Query: - -```vue - - {{ t('sqlFile.title') }} - -``` - -For database/schema menu, add after New Query: - -```vue - - {{ t('sqlFile.title') }} - -``` - -- [ ] **Step 3: Add i18n strings** - -In `src/i18n/locales/en.ts`, add: - -```ts -sqlFile: { - title: "Execute SQL File", - file: "SQL File", - chooseFile: "Choose File", - execute: "Execute File", - cancel: "Cancel", - continueOnError: "Continue after failed statements", - progress: "Statement {current} · {success} succeeded · {failed} failed", -}, -``` - -In `src/i18n/locales/zh-CN.ts`, add: - -```ts -sqlFile: { - title: "执行 SQL 文件", - file: "SQL 文件", - chooseFile: "选择文件", - execute: "执行文件", - cancel: "取消", - continueOnError: "失败后继续执行", - progress: "第 {current} 条 · 成功 {success} · 失败 {failed}", -}, -``` - -- [ ] **Step 4: Run frontend build** - -Run: - -```bash -pnpm build -``` - -Expected: build passes. - -- [ ] **Step 5: Commit** - -```bash -git add src/App.vue src/components/sidebar/TreeItem.vue src/i18n/locales/en.ts src/i18n/locales/zh-CN.ts -git commit -m "wire sql file execution entry points" -``` - ---- - -### Task 7: Final Verification - -**Files:** -- All changed files. - -- [ ] **Step 1: Run Rust tests** - -Run: - -```bash -cd src-tauri -cargo test --lib -``` - -Expected: all tests pass. - -- [ ] **Step 2: Run frontend build** - -Run: - -```bash -pnpm build -``` - -Expected: build passes. Existing chunk-size warnings are acceptable. - -- [ ] **Step 3: Run diff hygiene** - -Run: - -```bash -git diff --check -git status -sb -``` - -Expected: no whitespace errors. Status should show only intended tracked changes plus any pre-existing untracked docs that are not part of this PR. - -- [ ] **Step 4: Manual smoke test** - -Create a small file outside the repo: - -```sql -CREATE TABLE IF NOT EXISTS codex_sql_file_test (id INTEGER); -INSERT INTO codex_sql_file_test VALUES (1); -SELECT * FROM codex_sql_file_test; -``` - -Open DBX with `pnpm tauri dev`, choose a local SQLite or DuckDB connection, execute the file, and verify progress reaches done with at least two successful statements. - -- [ ] **Step 5: Manual failure smoke test** - -Create a small file outside the repo: - -```sql -CREATE TABLE IF NOT EXISTS codex_sql_file_test_failure (id INTEGER); -BROKEN STATEMENT; -INSERT INTO codex_sql_file_test_failure VALUES (1); -``` - -Run with default stop-on-error. Verify the dialog stops at statement 2 and shows the error without executing statement 3. - -- [ ] **Step 6: Final commit after verification fixes** - -If verification uncovers a concrete issue, stage the exact files changed by that fix: - -```bash -git add src-tauri/src/commands/sql_file.rs src/App.vue src/components/sql-file/SqlFileExecutionDialog.vue src/lib/tauri.ts -git commit -m "polish sql file execution" -``` - -If those exact files were not all touched by the fix, remove the untouched paths from the `git add` command before running it. If no verification fixes were needed, leave the task commits as the implementation history. diff --git a/docs/superpowers/specs/2026-05-02-sql-file-execution-design.md b/docs/superpowers/specs/2026-05-02-sql-file-execution-design.md deleted file mode 100644 index df4897e419..0000000000 --- a/docs/superpowers/specs/2026-05-02-sql-file-execution-design.md +++ /dev/null @@ -1,134 +0,0 @@ -# SQL File Execution Design - -## Goal - -Add a first-class workflow for executing `.sql` files against a selected connection and database. The feature targets common migration, initialization, and dump-restore tasks where users need to run a script file, see progress, stop on errors by default, and cancel long-running execution. - -## Entry Points - -- Add a toolbar action labeled "Execute SQL File". -- Add a context-menu action labeled "Execute SQL File" on connection and database tree nodes. -- The context-menu entry opens the same dialog as the toolbar action, with the current connection and database preselected when available. - -## Dialog Behavior - -The dialog lets the user choose: - -- Connection. -- Database, when the selected connection supports databases. -- SQL file path. -- Failure policy. - -The default failure policy is to stop on the first failed statement. A secondary option allows continuing after statement failures. - -Before execution, the dialog shows the file name, size, and a small preview from the beginning of the file. The preview is informational only; execution uses the file path directly so large files do not need to be loaded into frontend memory. - -During execution, the dialog shows: - -- Current statement index. -- Successful statement count. -- Failed statement count. -- Elapsed time. -- Current statement summary. -- Last error, if any. - -The dialog provides a Cancel button while execution is active. - -## Backend Architecture - -Add a new SQL file execution command family: - -- `execute_sql_file(connection_id, database, file_path, execution_id, continue_on_error)`. -- `cancel_sql_file_execution(execution_id)`. - -The backend reads the file incrementally from disk and splits it into statements as it reads. It executes statements one at a time using the existing query execution path so database-specific connection handling, reconnect behavior, timeout behavior, and result handling remain consistent with the query editor. - -Execution sends progress events through Tauri: - -- `started`: file execution has begun. -- `statementDone`: one statement completed successfully. -- `statementFailed`: one statement failed. -- `cancelled`: execution stopped because the user cancelled. -- `done`: execution completed. - -Each event includes the `execution_id` so the frontend can ignore events from stale runs. - -## SQL Splitting - -The first implementation supports standard semicolon-delimited SQL files. The splitter must ignore semicolons inside: - -- Single-quoted strings. -- Double-quoted strings. -- Backtick-quoted identifiers. -- Line comments. -- Block comments. - -The splitter also emits a final trailing statement when the file does not end with a semicolon. - -MySQL `DELIMITER` syntax for stored procedures is intentionally out of scope for this first PR. If a file depends on custom delimiters, execution may fail at the relevant statement and report the failing index. A later PR can add delimiter directives without blocking the common migration and initialization cases. - -## Failure And Cancellation - -Default behavior is stop-on-first-error. When a statement fails: - -- Emit `statementFailed`. -- Stop immediately unless `continue_on_error` is true. -- Report the failing statement index and the count of statements that may already have committed. - -When `continue_on_error` is true, execution continues after failures and emits a final summary including successful and failed counts. - -Cancellation uses a cancellation token keyed by `execution_id`. Cancelling stops before the next statement when possible and also passes the token into the active statement execution path so supported drivers can stop promptly. The UI should show cancellation as a stopped state rather than a generic SQL error. - -## Frontend Architecture - -Add a dedicated SQL file execution dialog component. The component owns: - -- File selection via Tauri dialog. -- Connection and database selection. -- Preview metadata. -- Execution progress state. -- Cancel action. - -Add lightweight API wrappers in `src/lib/tauri.ts` for the two backend commands and the progress event payload type. - -Do not store SQL file execution as a normal query tab. A file execution can contain many statements and progress events, so a modal task workflow is clearer than overloading the query result grid. - -## Testing - -Rust tests: - -- Split multiple statements by semicolon. -- Ignore semicolons in single-quoted strings. -- Ignore semicolons in double-quoted strings. -- Ignore semicolons in backtick identifiers. -- Ignore semicolons in line comments and block comments. -- Emit trailing statement without a final semicolon. -- Stop on first failure when `continue_on_error` is false. -- Continue after failure when `continue_on_error` is true. -- Stop execution after cancellation. - -Frontend verification: - -- Typecheck and production build. -- Manual smoke test with a small SQL file showing successful progress. -- Manual smoke test with a failing second statement showing stop-on-error and the failing index. - -## Scope Boundaries - -In scope: - -- Local `.sql` file selection. -- Streaming backend execution. -- Progress events. -- Stop-on-error default. -- Continue-on-error option. -- Cancellation. -- Toolbar and tree context-menu entry points. - -Out of scope for this PR: - -- MySQL custom `DELIMITER` directives. -- Transaction wrapping of the whole file. -- Remote file URLs. -- Saving import history. -- Editing the SQL file inside dbx before execution.