From 640292ad10dbcd9395da2270d350853d6a7013bd Mon Sep 17 00:00:00 2001 From: codeErrorSleep Date: Mon, 30 Mar 2026 20:44:15 +0800 Subject: [PATCH 1/7] fix(mysql): when the preprocessing protocol does not support it, the original query will be returned --- src-tauri/src/db/drivers/mysql.rs | 15 +++++++--- src-tauri/tests/mysql_integration.rs | 43 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/db/drivers/mysql.rs b/src-tauri/src/db/drivers/mysql.rs index fd2ed8c2..f1533bb0 100644 --- a/src-tauri/src/db/drivers/mysql.rs +++ b/src-tauri/src/db/drivers/mysql.rs @@ -472,10 +472,17 @@ impl DatabaseDriver for MysqlDriver { } async fn test_connection(&self) -> Result<(), String> { - sqlx::query("SELECT 1") - .execute(&self.pool) - .await - .map_err(|e| format!("[QUERY_ERROR] {e}"))?; + if let Err(e) = sqlx::query("SELECT 1").execute(&self.pool).await { + let error_text = e.to_string(); + if is_prepared_protocol_unsupported_error(&error_text) { + sqlx::raw_sql("SELECT 1") + .execute(&self.pool) + .await + .map_err(|raw_err| format!("[QUERY_ERROR] {raw_err}"))?; + } else { + return Err(format!("[QUERY_ERROR] {e}")); + } + } Ok(()) } diff --git a/src-tauri/tests/mysql_integration.rs b/src-tauri/tests/mysql_integration.rs index 8cdfff9d..a7d41ba4 100644 --- a/src-tauri/tests/mysql_integration.rs +++ b/src-tauri/tests/mysql_integration.rs @@ -3,6 +3,7 @@ mod mysql_context; use dbpaw_lib::db::drivers::mysql::MysqlDriver; use dbpaw_lib::db::drivers::DatabaseDriver; +use std::env; use testcontainers::clients::Cli; #[tokio::test] @@ -1052,6 +1053,48 @@ async fn test_mysql_connection_timeout_or_unreachable_host_error() { ); } +#[tokio::test] +#[ignore] +async fn test_mysql_test_connection_fallback_when_prepare_unsupported() { + let docker = (!mysql_context::should_reuse_local_db()).then(Cli::default); + let (_container, form) = mysql_context::mysql_form_from_test_context(docker.as_ref()); + let driver: MysqlDriver = + mysql_context::connect_with_retry(|| MysqlDriver::connect(&form)).await; + + let expect_prepare_unsupported = + env::var("MYSQL_EXPECT_PREPARED_UNSUPPORTED").unwrap_or_default() == "1"; + + let mut conn = driver + .pool + .acquire() + .await + .expect("acquire mysql pooled connection failed"); + let prepare_probe = sqlx::query("SELECT 1").execute(&mut *conn).await; + + match prepare_probe { + Ok(_) => { + assert!( + !expect_prepare_unsupported, + "expected prepared protocol unsupported but probe succeeded" + ); + } + Err(err) => { + let err_text = err.to_string().to_ascii_lowercase(); + assert!( + err_text.contains("1295") + || err_text.contains("prepare does not support sql") + || err_text.contains("prepared statement protocol"), + "unexpected prepare probe error: {}", + err + ); + driver + .test_connection() + .await + .expect("test_connection should fallback to raw_sql when prepare is unsupported"); + } + } +} + #[tokio::test] #[ignore] async fn test_mysql_batch_insert_and_batch_execute_flow() { From 521d4669e1fffc731a127e9e45283d0dea0ec84c Mon Sep 17 00:00:00 2001 From: codeErrorSleep Date: Mon, 30 Mar 2026 21:01:48 +0800 Subject: [PATCH 2/7] fix: allow selecting extensionless ssh keys on macos --- src/components/business/Sidebar/ConnectionList.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/business/Sidebar/ConnectionList.tsx b/src/components/business/Sidebar/ConnectionList.tsx index da6ffd9a..65da7d9d 100644 --- a/src/components/business/Sidebar/ConnectionList.tsx +++ b/src/components/business/Sidebar/ConnectionList.tsx @@ -600,13 +600,8 @@ export function ConnectionList({ const handlePickSshKeyFile = async () => { const selectedPath = await pickSingleFile({ title: t("connection.dialog.sshKeyFileDialogTitle"), - filters: [ - { - name: t("connection.dialog.fileFilterPem"), - extensions: ["pem", "key", "ppk"], - }, - { name: t("connection.dialog.fileFilterAll"), extensions: ["*"] }, - ], + // SSH private keys are often extensionless (for example ~/.ssh/id_rsa), + // so filtering by extension can hide valid keys in the native picker. }); if (!selectedPath) return; setForm((f) => ({ ...f, sshKeyPath: selectedPath })); From a76cc97f1f1a976780686e73856a24d51611fd94 Mon Sep 17 00:00:00 2001 From: codeErrorSleep Date: Mon, 30 Mar 2026 23:16:43 +0800 Subject: [PATCH 3/7] fix: honor embedded mysql host ports --- src-tauri/src/connection_input/mod.rs | 21 ++++++++++++++++----- src/lib/connection-form/rules.ts | 2 +- src/lib/connection-form/rules.unit.test.ts | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/connection_input/mod.rs b/src-tauri/src/connection_input/mod.rs index 8a347fb2..b4dacbfd 100644 --- a/src-tauri/src/connection_input/mod.rs +++ b/src-tauri/src/connection_input/mod.rs @@ -20,11 +20,7 @@ fn parse_host_embedded_port(host: &str, fallback_port: Option) -> (String, if host_part.is_empty() || !port_part.chars().all(|c| c.is_ascii_digit()) { return (host.to_string(), fallback_port); } - let parsed_port = if fallback_port.is_some() { - fallback_port - } else { - port_part.parse::().ok() - }; + let parsed_port = port_part.parse::().ok(); (host_part.to_string(), parsed_port) } @@ -113,6 +109,21 @@ mod tests { assert_eq!(normalized.username, Some("root".to_string())); } + #[test] + fn normalize_prefers_embedded_mysql_port_over_existing_port() { + let form = ConnectionForm { + driver: "mysql".to_string(), + host: Some("127.0.0.1:3307".to_string()), + port: Some(3306), + username: Some("root".to_string()), + ..Default::default() + }; + + let normalized = normalize_connection_form(form).unwrap(); + assert_eq!(normalized.host, Some("127.0.0.1".to_string())); + assert_eq!(normalized.port, Some(3307)); + } + #[test] fn normalize_preserves_empty_secret_fields_when_present() { let form = ConnectionForm { diff --git a/src/lib/connection-form/rules.ts b/src/lib/connection-form/rules.ts index 477a4a55..55973671 100644 --- a/src/lib/connection-form/rules.ts +++ b/src/lib/connection-form/rules.ts @@ -58,7 +58,7 @@ export const parseHostEmbeddedPort = ( } return { host: hostPart, - port: fallbackPort ?? Number(portPart), + port: Number(portPart), }; }; diff --git a/src/lib/connection-form/rules.unit.test.ts b/src/lib/connection-form/rules.unit.test.ts index 15503801..c225fe9f 100644 --- a/src/lib/connection-form/rules.unit.test.ts +++ b/src/lib/connection-form/rules.unit.test.ts @@ -13,6 +13,13 @@ describe("parseHostEmbeddedPort", () => { }); }); + test("prefers embedded port over fallback port", () => { + expect(parseHostEmbeddedPort("127.0.0.1:3307", 3306)).toEqual({ + host: "127.0.0.1", + port: 3307, + }); + }); + test("keeps host and fallback port when no port provided", () => { expect(parseHostEmbeddedPort("localhost", 5432)).toEqual({ host: "localhost", @@ -72,6 +79,18 @@ describe("normalizeConnectionFormInput", () => { expect(normalized.password).toBe(""); }); + test("uses embedded host port even when a default port is already set", () => { + const normalized = normalizeConnectionFormInput({ + driver: "mysql", + host: " db:3307 ", + port: 3306, + password: "", + } as any); + + expect(normalized.host).toBe("db"); + expect(normalized.port).toBe(3307); + }); + test("does not split host:port for non-mysql drivers", () => { const normalized = normalizeConnectionFormInput({ driver: "postgres", From 1632fcba58ed0672e5b66afa03e02a7510c1da2d Mon Sep 17 00:00:00 2001 From: codeErrorSleep Date: Tue, 31 Mar 2026 11:17:43 +0800 Subject: [PATCH 4/7] test: add command integration tests for all databases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends command layer test coverage from MySQL/PostgreSQL to all remaining databases (MariaDB, MSSQL, ClickHouse, SQLite, DuckDB). Each database now has comprehensive P0 command tests covering: - Connection validation (success and error paths) - Table and database listing - Query execution (SELECT, INSERT with row counts) - Data grid pagination and validation - Error handling for invalid SQL and credentials Added comprehensive testing documentation: - CLAUDE.md: Guidance for AI-assisted development - TESTING.md: Complete testing guide with database coverage matrix Updated test-integration.sh to include all new command tests in CI pipeline for each database and the 'all' target. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- CLAUDE.md | 185 +++++++++ TESTING.md | 347 +++++++++++++++++ scripts/test-integration.sh | 10 + .../tests/clickhouse_command_integration.rs | 363 ++++++++++++++++++ src-tauri/tests/duckdb_command_integration.rs | 346 +++++++++++++++++ .../tests/mariadb_command_integration.rs | 337 ++++++++++++++++ src-tauri/tests/mssql_command_integration.rs | 326 ++++++++++++++++ src-tauri/tests/sqlite_command_integration.rs | 343 +++++++++++++++++ 8 files changed, 2257 insertions(+) create mode 100644 CLAUDE.md create mode 100644 TESTING.md create mode 100644 src-tauri/tests/clickhouse_command_integration.rs create mode 100644 src-tauri/tests/duckdb_command_integration.rs create mode 100644 src-tauri/tests/mariadb_command_integration.rs create mode 100644 src-tauri/tests/mssql_command_integration.rs create mode 100644 src-tauri/tests/sqlite_command_integration.rs diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a946e349 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,185 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +DbPaw is a cross-platform database client built with Tauri v2, supporting PostgreSQL, MySQL, MariaDB, TiDB, SQLite, SQL Server, ClickHouse, and DuckDB. The architecture separates frontend (React/TypeScript) from backend (Rust) with communication via Tauri commands. + +## Essential Commands + +### Development +- `bun install` - Install dependencies +- `bun dev:mock` - Frontend-only development with mock data (recommended for UI work) +- `bun tauri dev` - Full Tauri app with Rust backend (for end-to-end testing) +- `bun tauri build` - Production build + +### Testing +For comprehensive testing documentation, see [TESTING.md](TESTING.md). + +Quick reference: +- `bun run test:all` - Run all tests (unit, service, Rust, integration) +- `bun run test:unit` - Run TypeScript unit tests (files: `*.unit.test.ts`) +- `bun run test:service` - Run service layer tests (files: `*.service.test.ts`) +- `bun run test:rust:unit` - Run Rust unit tests (`cargo test --lib`) +- `bun run test:integration` - Run database integration tests (requires Docker) + - `IT_DB=mysql bun run test:integration` - Test specific database + - `IT_DB=all bun run test:integration` - Test all databases + - `IT_REUSE_LOCAL_DB=1` - Reuse existing local database containers +- `bun run test:smoke` - Quick validation (typecheck, lint, unit tests) +- `bun run test:ci` - Full CI test suite + +### Code Quality +- `bun run typecheck` - TypeScript type checking +- `bun run lint` - Lint TypeScript/JSON files with Prettier +- `bun run lint:rust` - Check Rust code (`cargo check`) +- `bun run format` - Format TypeScript files with Prettier + +### Website +- `bun run website:dev` - Run Astro marketing site locally +- `bun run website:build` - Build marketing site + +## Architecture + +### Frontend (React + TypeScript) + +**Directory Structure:** +- `src/components/ui/` - Shadcn/UI components (base UI primitives) +- `src/components/business/` - Business logic components: + - `Editor/` - SQL editor (Monaco/CodeMirror) + - `DataGrid/` - Query results and table data display + - `Sidebar/` - Connection/database tree navigation + - `Metadata/` - Table structure and schema views + - `SqlLogs/` - Query execution history +- `src/components/settings/` - Settings dialogs +- `src/services/` - Tauri API wrapper and mocks +- `src/lib/` - Utilities (i18n, keyboard shortcuts, validation) +- `src/theme/` - Theme registry and management + +**Key Patterns:** +- All Tauri backend calls go through `src/services/api.ts` which provides: + - Mock mode (`VITE_USE_MOCK=true`) for frontend-only development + - Type-safe wrappers around Tauri `invoke()` commands + - Runtime detection (`isTauri()`) to handle non-Tauri environments +- Path alias: `@/` maps to `./src/` +- i18n: Files in `src/lib/i18n/locales/` (en, zh, ja supported) + +### Backend (Rust + Tauri) + +**Core Modules:** +- `src-tauri/src/commands/` - Tauri command handlers (exposed to frontend): + - `connection.rs` - Connection CRUD and testing + - `query.rs` - Query execution and cancellation + - `metadata.rs` - Schema inspection (tables, structures, DDL) + - `storage.rs` - Saved queries persistence + - `ai.rs` - AI provider management and chat + - `transfer.rs` - Import/export operations +- `src-tauri/src/db/` - Database layer: + - `drivers/` - Per-database implementations (postgres, mysql, clickhouse, mssql, sqlite, duckdb) + - `pool_manager.rs` - Connection pooling with bb8 + - `local.rs` - SQLite database for app metadata +- `src-tauri/src/state.rs` - Global app state (local DB + pool manager) +- `src-tauri/src/ssh.rs` - SSH tunnel support +- `src-tauri/src/ai/` - AI provider integration (OpenAI-compatible APIs) +- `src-tauri/src/models/` - Shared data types +- `src-tauri/src/error.rs` - Error handling + +**Key Patterns:** +- All database drivers implement `DatabaseDriver` trait (see `src-tauri/src/db/drivers/mod.rs`) +- Connection pooling: Each database connection gets a managed pool via `PoolManager` +- State management: `AppState` holds `local_db` (SQLite) and `pool_manager` +- SSH tunneling: Transparent port forwarding for remote database access +- Error messages: Use `conn_failed_error()` to provide context-aware hints (TLS issues, auth failures, network problems) + +### Testing Strategy + +DbPaw uses a **3-layer testing approach** (see [TESTING.md](TESTING.md) for full details): + +``` +Frontend Layer โ†’ Unit tests (*.unit.test.ts) + Service tests (*.service.test.ts) +Tauri Commands โ†’ Command integration tests (*_command_integration.rs) +Database Drivers โ†’ Driver integration tests (*_integration.rs) +``` + +**Test Coverage by Database:** +- โœ… **MySQL & PostgreSQL**: Complete (driver + command + stateful tests) +- ๐ŸŸข **MariaDB, MSSQL, ClickHouse, SQLite, DuckDB**: Driver + command tests (stateful tests pending) + +**TypeScript Tests:** +- Unit tests: `*.unit.test.ts` - Pure logic, no external dependencies +- Service tests: `*.service.test.ts` - Mock-based service layer tests +- Test runner: Bun's built-in test runner + +**Rust Tests:** +- Unit tests: `#[test]` in source files, run with `cargo test --lib` +- Driver integration tests: `src-tauri/tests/_integration.rs` - Direct driver method testing +- Command integration tests: `src-tauri/tests/_command_integration.rs` - Ephemeral connection commands +- Stateful command tests: `src-tauri/tests/_stateful_command_integration.rs` - Saved connection workflows +- All integration tests: + - Use testcontainers for real database instances + - Marked with `#[ignore]` (only run explicitly) + - Environment: `IT_DB` (mysql/postgres/mariadb/clickhouse/mssql/sqlite/duckdb/all) + - Helpers in `src-tauri/tests/common/` provide database context setup + +### Database Driver Development + +When adding/modifying database drivers: +1. Implement `DatabaseDriver` trait in `src-tauri/src/db/drivers/.rs` +2. Add to driver enum in `src-tauri/src/db/drivers/mod.rs` +3. Handle driver-specific connection strings and options +4. Use `conn_failed_error()` for user-friendly connection error messages +5. Add integration tests in `src-tauri/tests/_integration.rs` +6. Update `scripts/test-integration.sh` to include new driver + +### Common Patterns + +**Frontend-Backend Communication:** +```typescript +// Frontend +import { api } from '@/services/api'; +const result = await api.execute_query(connectionId, database, sql); +``` + +```rust +// Backend +#[tauri::command] +async fn execute_query( + state: State<'_, AppState>, + connection_id: i64, + database: Option, + sql: String, +) -> Result { + // Implementation +} +``` + +**Mock Development:** +- Use `bun dev:mock` for rapid UI iteration without Rust compilation +- Mocks defined in `src/services/mocks.ts` +- Useful for working on: themes, UI components, layouts, i18n + +**SSH Tunneling:** +- Handled transparently in connection layer +- SSH config in connection form, tunnel established before database connection +- Port forwarding lifetime managed with connection pool + +## Build System + +- Frontend: Vite with React plugin and TailwindCSS +- Backend: Cargo with sqlx (compile-time SQL checking disabled), tiberius (SQL Server), bb8 (pooling) +- Platform toolchain: Follows Tauri v2 prerequisites (see https://tauri.app/start/prerequisites/) +- Package manager: Bun (preferred) or npm/pnpm + +## CI/GitHub Actions + +- `.github/workflows/ci.yml` - Main CI pipeline +- Tests run on: Ubuntu (Linux), macOS, Windows +- Integration tests use Docker containers (testcontainers) +- Release builds triggered on tags + +## Translation/i18n + +- Framework: i18next + react-i18next +- Locale files: `src/lib/i18n/locales/*.ts` (TypeScript, not JSON) +- Supported: English (en), Chinese (zh), Japanese (ja) +- To add language: Create locale file and register in `src/lib/i18n/index.ts` diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000..eaec75a4 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,347 @@ +# Testing Guide + +This document describes the testing strategy and how to run tests in DbPaw. + +## Test Architecture + +DbPaw uses a **layered testing approach**: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Frontend (React/TypeScript) โ”‚ โ† Unit tests (*.unit.test.ts) +โ”‚ - Components, utilities, libs โ”‚ โ† Service tests (*.service.test.ts) +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Tauri Commands (Rust) โ”‚ โ† Command integration tests +โ”‚ - connection.rs, query.rs, etc. โ”‚ (*_command_integration.rs) +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ Database Drivers (Rust) โ”‚ โ† Driver integration tests +โ”‚ - MysqlDriver, PostgresDriver โ”‚ (*_integration.rs) +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Quick Start + +### Run All Tests +```bash +bun run test:all +``` + +### Run Specific Test Types +```bash +# Frontend tests only +bun run test:unit # Pure logic tests (*.unit.test.ts) +bun run test:service # Service layer tests (*.service.test.ts) + +# Backend tests only +bun run test:rust:unit # Rust unit tests + +# Integration tests (requires Docker) +bun run test:integration # All databases +IT_DB=mysql bun run test:integration # MySQL only +IT_DB=postgres bun run test:integration # PostgreSQL only +``` + +### Quick Validation (Pre-commit) +```bash +bun run test:smoke # typecheck + lint + rust:check + unit + service + rust:unit +``` + +### CI Full Suite +```bash +bun run test:ci # smoke + all integration tests +``` + +## Change โ†’ Validation Mapping + +When you modify files, run the appropriate test suite: + +| Directory Changed | Tests to Run | Command | +|------------------|--------------|---------| +| `src/components/`, `src/lib/` | TypeScript unit tests | `bun run test:unit` | +| `src/services/` | Service tests | `bun run test:service` | +| `src-tauri/src/db/drivers/` | Driver integration tests | `IT_DB=mysql bun run test:integration` | +| `src-tauri/src/commands/` | Command integration tests | `IT_DB=mysql bun run test:integration` | +| Cross-layer changes | Full suite | `bun run test:all` | + +## Integration Test Details + +### Environment Variables + +- `IT_DB` - Which database to test: `mysql`, `postgres`, `mariadb`, `mssql`, `clickhouse`, `sqlite`, `duckdb`, `all` +- `IT_REUSE_LOCAL_DB=1` - Reuse existing local database (faster for development) +- `IT_CONTAINER_PREFIX` - Custom container name prefix (default: `dbpaw-it-$$-`) + +### Examples + +```bash +# Test single database (faster iteration) +IT_DB=mysql bun run test:integration + +# Reuse local database (no Docker container startup) +IT_REUSE_LOCAL_DB=1 IT_DB=mysql bun run test:integration + +# Test all databases (CI mode) +IT_DB=all bun run test:integration +``` + +### Manual Test Execution + +```bash +# Run specific test file +cargo test --manifest-path src-tauri/Cargo.toml \ + --test mysql_integration -- --ignored --nocapture --test-threads=1 + +# Run specific test case +cargo test --manifest-path src-tauri/Cargo.toml \ + --test mysql_command_integration test_mysql_command_test_connection_success \ + -- --ignored --nocapture --test-threads=1 +``` + +## Test Coverage by Database + +### Legend +- โœ… Fully covered (driver + command + stateful tests) +- ๐ŸŸข Good coverage (driver + command tests) +- ๐ŸŸก Partial (driver tests only) +- โŒ Missing + +| Database | Driver Tests | Command Tests | Stateful Command Tests | Notes | +|----------|-------------|---------------|----------------------|-------| +| **MySQL** | โœ… | โœ… | โœ… | Complete coverage | +| **PostgreSQL** | โœ… | โœ… | โœ… | Complete coverage | +| **MariaDB** | โœ… | โœ… | โณ | Driver + Command (stateful pending) | +| **SQL Server** | โœ… | โœ… | โณ | Driver + Command (stateful pending) | +| **ClickHouse** | โœ… | โœ… | โณ | Driver + Command (stateful pending) | +| **SQLite** | โœ… | โœ… | โณ | Driver + Command (stateful pending) | +| **DuckDB** | โœ… | โœ… | โณ | Driver + Command (stateful pending) | +| **TiDB** | N/A | N/A | N/A | Uses MySQL driver | + +## Test File Naming Conventions + +### TypeScript Tests +- `*.unit.test.ts` - Pure unit tests (no external dependencies) +- `*.service.test.ts` - Service layer tests (may use mocks) +- `*.test.ts` - General tests (deprecated, prefer specific suffixes) + +### Rust Tests +- `src-tauri/tests/_integration.rs` - Driver layer tests (direct driver calls) +- `src-tauri/tests/_command_integration.rs` - Command layer tests (ephemeral connections) +- `src-tauri/tests/_stateful_command_integration.rs` - Stateful command tests (saved connections) +- `src-tauri/tests/common/_context.rs` - Test helpers and Docker setup + +## Adding Tests for a New Database + +When adding support for a new database, follow this checklist: + +### 1. Create Test Context +```bash +src-tauri/tests/common/_context.rs +``` +- Docker container setup +- Connection form builder +- Retry helpers + +### 2. Create Driver Integration Tests +```bash +src-tauri/tests/_integration.rs +``` + +**Minimum P0 tests:** +- [ ] `test__integration_flow` - Basic CRUD flow +- [ ] `test__get_table_data_supports_pagination_sort_filter_and_order_by` +- [ ] `test__get_table_data_rejects_invalid_sort_column` +- [ ] `test__table_structure_and_schema_overview` +- [ ] `test__metadata_includes_indexes_and_foreign_keys` +- [ ] `test__boolean_and_json_type_mapping_regression` +- [ ] `test__error_handling_for_sql_error` + +**Recommended P1 tests:** +- [ ] `test__transaction_commit_and_rollback` +- [ ] `test__execute_query_reports_affected_rows_for_update_delete` +- [ ] `test__batch_insert_and_batch_execute_flow` +- [ ] `test__large_text_and_blob_round_trip` +- [ ] `test__concurrent_connections_can_query` +- [ ] `test__view_can_be_listed_and_queried` +- [ ] `test__connection_failure_with_wrong_password` +- [ ] `test__connection_timeout_or_unreachable_host_error` + +### 3. Create Command Integration Tests +```bash +src-tauri/tests/_command_integration.rs +``` + +**Minimum P0 commands:** +- [ ] `test__command_test_connection_success` +- [ ] `test__command_test_connection_invalid_password_returns_error` +- [ ] `test__command_list_tables_by_conn_contains_created_table` +- [ ] `test__command_list_databases_contains_target_db` +- [ ] `test__command_execute_by_conn_select_returns_rows` +- [ ] `test__command_execute_by_conn_invalid_sql_returns_error` +- [ ] `test__command_execute_by_conn_insert_affects_rows` +- [ ] `test__command_get_table_data_by_conn_pagination_works` + +### 4. Create Stateful Command Tests +```bash +src-tauri/tests/_stateful_command_integration.rs +``` + +**Covered areas:** +- Connection CRUD lifecycle +- Database creation/listing with saved connections +- Query execution with connection IDs +- Metadata operations with connection IDs +- SQL execution logging + +### 5. Update Test Script +Edit `scripts/test-integration.sh`: + +```bash +# Add new database case +) + run_integration_test "_integration" + run_integration_test "_command_integration" + run_integration_test "_stateful_command_integration" + ;; + +# Add to 'all' case +all) + ... + run_integration_test "_integration" + run_integration_test "_command_integration" + run_integration_test "_stateful_command_integration" + ... + ;; +``` + +## Test Best Practices + +### General +- Use `#[ignore]` for integration tests (only run explicitly) +- Use `--test-threads=1` to avoid database race conditions +- Clean up all test data after each test +- Use unique names for temporary objects (include timestamp) + +### Assertions +- Don't just assert `is_ok()` - verify actual data +- For errors, assert error message is non-empty +- Check for specific error prefixes: `[CONN_FAILED]`, `[VALIDATION_ERROR]`, etc. +- Verify row counts, column names, and data values + +### Docker/Testcontainers +- Use `IT_REUSE_LOCAL_DB=1` for faster local development +- Container names must be unique (use prefix + timestamp) +- Always wait for port availability after container start +- Use retry logic for connection establishment + +### Examples + +```rust +// โŒ Bad - only checks is_ok() +let result = driver.execute_query(sql).await; +assert!(result.is_ok()); + +// โœ… Good - verifies actual data +let result = driver.execute_query(sql).await + .expect("query should succeed"); +assert_eq!(result.row_count, 1); +assert_eq!(result.data[0]["name"].as_str(), Some("DbPaw")); + +// โŒ Bad - generic error check +assert!(result.is_err()); + +// โœ… Good - verifies error content +let error = result.err().unwrap(); +assert!(!error.trim().is_empty()); +assert!(error.contains("[CONN_FAILED]")); +``` + +## CI Integration + +### Current GitHub Actions Workflow + +The `.github/workflows/ci.yml` runs: +1. TypeScript type checking +2. Frontend linting +3. Rust cargo check +4. Frontend unit tests +5. Frontend service tests +6. Rust unit tests +7. **Full integration test matrix** (all databases) + +### Optimization Opportunities + +Consider splitting CI into: +- **PR checks** (fast feedback): `test:smoke` + MySQL integration only +- **Nightly** (full coverage): All databases +- **Conditional** (smart): Full database matrix only when `src-tauri/src/db/` changes + +## Troubleshooting + +### "Container name already exists" +```bash +# Clean up containers +docker ps -a --filter "name=dbpaw-it-" | grep dbpaw-it- | awk '{print $1}' | xargs -r docker rm -f +``` + +### "Port already in use" +```bash +# Use reuse mode +IT_REUSE_LOCAL_DB=1 IT_DB=mysql bun run test:integration +``` + +### "Test hangs/timeouts" +- Check Docker daemon is running +- Verify port is not blocked by firewall +- Increase wait timeout in `*_context.rs` + +### "Connection refused" +- Wait longer for database readiness (increase retry count) +- Check container logs: `docker logs ` +- Verify credentials match container environment + +## Performance Tips + +### For Local Development +```bash +# 1. Use reuse mode (fastest) +IT_REUSE_LOCAL_DB=1 IT_DB=mysql bun run test:integration + +# 2. Run specific test file +cargo test --manifest-path src-tauri/Cargo.toml \ + --test mysql_command_integration -- --ignored --nocapture + +# 3. Run specific test case +cargo test --manifest-path src-tauri/Cargo.toml \ + test_mysql_command_test_connection_success -- --ignored +``` + +### For CI +```bash +# Run only changed database +IT_DB=mysql bun run test:integration + +# Cache Docker images +# Add to .github/workflows/ci.yml: +# - uses: actions/cache@v3 +# with: +# path: /var/lib/docker +``` + +## Future Improvements + +### P0 (High Priority) +- [ ] Add command integration tests for MariaDB, MSSQL, ClickHouse, SQLite, DuckDB +- [ ] Document test data setup patterns +- [ ] Create test helper library for common assertions + +### P1 (Medium Priority) +- [ ] Add frontend component tests (Playwright or Vitest) +- [ ] Split CI into fast/slow test suites +- [ ] Add performance benchmarks for query execution +- [ ] Create test coverage reports + +### P2 (Nice to Have) +- [ ] Add visual regression tests for UI components +- [ ] Create load testing scenarios +- [ ] Add mutation testing for critical paths +- [ ] Set up automatic test generation for new drivers diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index c52aab38..bc644e28 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -41,6 +41,7 @@ case "${it_db}" in ;; mariadb) run_integration_test "mariadb_integration" + run_integration_test "mariadb_command_integration" ;; postgres) run_integration_test "postgres_integration" @@ -49,28 +50,37 @@ case "${it_db}" in ;; clickhouse) run_integration_test "clickhouse_integration" + run_integration_test "clickhouse_command_integration" ;; mssql) run_integration_test "mssql_integration" + run_integration_test "mssql_command_integration" ;; duckdb) run_integration_test "duckdb_integration" + run_integration_test "duckdb_command_integration" ;; sqlite) run_integration_test "sqlite_integration" + run_integration_test "sqlite_command_integration" ;; all) run_integration_test "mysql_integration" run_integration_test "mysql_command_integration" run_integration_test "mysql_stateful_command_integration" run_integration_test "mariadb_integration" + run_integration_test "mariadb_command_integration" run_integration_test "postgres_integration" run_integration_test "postgres_command_integration" run_integration_test "postgres_stateful_command_integration" run_integration_test "clickhouse_integration" + run_integration_test "clickhouse_command_integration" run_integration_test "mssql_integration" + run_integration_test "mssql_command_integration" run_integration_test "duckdb_integration" + run_integration_test "duckdb_command_integration" run_integration_test "sqlite_integration" + run_integration_test "sqlite_command_integration" ;; *) echo "[error] Invalid IT_DB='${it_db}'. Expected one of: mysql|mariadb|postgres|clickhouse|mssql|duckdb|sqlite|all" diff --git a/src-tauri/tests/clickhouse_command_integration.rs b/src-tauri/tests/clickhouse_command_integration.rs new file mode 100644 index 00000000..e71874a2 --- /dev/null +++ b/src-tauri/tests/clickhouse_command_integration.rs @@ -0,0 +1,363 @@ +#[path = "common/clickhouse_context.rs"] +mod clickhouse_context; + +use dbpaw_lib::commands::{connection, metadata, query}; +use dbpaw_lib::db::drivers::clickhouse::ClickHouseDriver; +use dbpaw_lib::db::drivers::DatabaseDriver; +use dbpaw_lib::models::ConnectionForm; +use std::time::{SystemTime, UNIX_EPOCH}; +use testcontainers::clients::Cli; +use tokio::time::{sleep, Duration}; + +fn unique_table_name(prefix: &str) -> String { + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time should be after unix epoch") + .as_millis(); + format!("{}_{}", prefix, millis) +} + +async fn wait_until_clickhouse_ready(form: &ConnectionForm) { + let mut last_error = String::new(); + for _ in 0..45 { + let probe = form.clone(); + match connection::test_connection_ephemeral(probe).await { + Ok(_) => return, + Err(err) => { + last_error = err; + sleep(Duration::from_secs(1)).await; + } + } + } + panic!("clickhouse is not ready for command tests: {last_error}"); +} + +async fn prepare_query_test_table(form: &ConnectionForm, table: &str) { + let driver = ClickHouseDriver::connect(form) + .await + .expect("failed to connect clickhouse driver"); + + let database = form + .database + .clone() + .unwrap_or_else(|| "default".to_string()); + let qualified = format!("`{}`.`{}`", database, table); + + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", qualified)) + .await + .ok(); + driver + .execute_query(format!( + "CREATE TABLE {} (id UInt32, name String) ENGINE = MergeTree ORDER BY id", + qualified + )) + .await + .expect("create table should succeed"); + driver + .execute_query(format!( + "INSERT INTO {} (id, name) VALUES (1, 'DbPaw')", + qualified + )) + .await + .expect("insert row should succeed"); + driver.close().await; +} + +async fn cleanup_table(form: &ConnectionForm, table: &str) { + let driver = ClickHouseDriver::connect(form) + .await + .expect("failed to connect clickhouse driver for cleanup"); + + let database = form + .database + .clone() + .unwrap_or_else(|| "default".to_string()); + let qualified = format!("`{}`.`{}`", database, table); + + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", qualified)) + .await + .ok(); + driver.close().await; +} + +async fn execute_by_conn_sql( + form: ConnectionForm, + sql: String, +) -> Result { + query::execute_by_conn_direct(form, sql).await +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_test_connection_success() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + wait_until_clickhouse_ready(&form).await; + + let result = connection::test_connection_ephemeral(form) + .await + .expect("test_connection_ephemeral should succeed"); + + assert!(result.success); + assert!(result.latency_ms.is_some()); +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_test_connection_invalid_password_returns_error() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, mut form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + let ready_form = form.clone(); + wait_until_clickhouse_ready(&ready_form).await; + form.password = Some("dbpaw_wrong_password".to_string()); + + let result = connection::test_connection_ephemeral(form).await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_list_tables_by_conn_contains_created_table() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + wait_until_clickhouse_ready(&form).await; + let table = unique_table_name("dbpaw_cmd_tables"); + prepare_query_test_table(&form, &table).await; + + let tables = metadata::list_tables_by_conn(form.clone()) + .await + .expect("list_tables_by_conn should succeed"); + + assert!(tables.iter().any(|t| t.name == table)); + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_list_tables_by_conn_invalid_credentials_returns_error() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, mut form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + let ready_form = form.clone(); + wait_until_clickhouse_ready(&ready_form).await; + form.password = Some("dbpaw_wrong_password".to_string()); + + let result = metadata::list_tables_by_conn(form).await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_list_databases_contains_target_db() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + wait_until_clickhouse_ready(&form).await; + let target_db = form + .database + .clone() + .unwrap_or_else(|| "default".to_string()); + + let databases = connection::list_databases(form) + .await + .expect("list_databases should succeed"); + + assert!(!databases.is_empty()); + assert!(databases.iter().any(|db| db == &target_db)); + assert!(databases.iter().all(|db| !db.trim().is_empty())); +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_list_databases_invalid_credentials_returns_error() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, mut form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + let ready_form = form.clone(); + wait_until_clickhouse_ready(&ready_form).await; + form.password = Some("dbpaw_wrong_password".to_string()); + + let result = connection::list_databases(form).await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_execute_by_conn_select_returns_rows() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + wait_until_clickhouse_ready(&form).await; + let table = unique_table_name("dbpaw_cmd_exec_select"); + prepare_query_test_table(&form, &table).await; + + let database = form + .database + .clone() + .unwrap_or_else(|| "default".to_string()); + let qualified = format!("`{}`.`{}`", database, table); + + let sql = format!("SELECT id, name FROM {} ORDER BY id", qualified); + let result = execute_by_conn_sql(form.clone(), sql) + .await + .expect("execute_by_conn should succeed"); + + assert!(result.success); + assert!(result.row_count >= 1); + assert!(!result.data.is_empty()); + let row = result.data.first().expect("result row should exist"); + let name = row.get("name").and_then(|v| v.as_str()); + assert_eq!(name, Some("DbPaw")); + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_execute_by_conn_invalid_sql_returns_error() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + wait_until_clickhouse_ready(&form).await; + + let result = execute_by_conn_sql( + form, + "SELECT * FROM __dbpaw_missing_command_table".to_string(), + ) + .await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_execute_by_conn_insert_affects_rows() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + wait_until_clickhouse_ready(&form).await; + let table = unique_table_name("dbpaw_cmd_exec_insert"); + + let database = form + .database + .clone() + .unwrap_or_else(|| "default".to_string()); + let qualified = format!("`{}`.`{}`", database, table); + + let driver = ClickHouseDriver::connect(&form) + .await + .expect("failed to connect clickhouse driver"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", qualified)) + .await + .ok(); + driver + .execute_query(format!( + "CREATE TABLE {} (id UInt32, name String) ENGINE = MergeTree ORDER BY id", + qualified + )) + .await + .expect("create table should succeed"); + driver.close().await; + + let sql = format!("INSERT INTO {} (id, name) VALUES (1, 'alpha')", qualified); + let result = execute_by_conn_sql(form.clone(), sql) + .await + .expect("execute_by_conn insert should succeed"); + assert!(result.success); + assert_eq!(result.row_count, 1); + + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_get_table_data_by_conn_pagination_works() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + wait_until_clickhouse_ready(&form).await; + + let database = form + .database + .clone() + .unwrap_or_else(|| "default".to_string()); + let table = unique_table_name("dbpaw_cmd_page"); + let qualified = format!("`{}`.`{}`", database, table); + + let driver = ClickHouseDriver::connect(&form) + .await + .expect("failed to connect clickhouse driver"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", qualified)) + .await + .ok(); + driver + .execute_query(format!( + "CREATE TABLE {} (id UInt32, name String) ENGINE = MergeTree ORDER BY id", + qualified + )) + .await + .expect("create table should succeed"); + driver + .execute_query(format!( + "INSERT INTO {} (id, name) VALUES (1, 'a'), (2, 'b'), (3, 'c')", + qualified + )) + .await + .expect("insert rows should succeed"); + driver.close().await; + + let page1 = query::get_table_data_by_conn(form.clone(), database.clone(), table.clone(), 1, 2) + .await + .expect("page 1 should succeed"); + let page2 = query::get_table_data_by_conn(form.clone(), database, table.clone(), 2, 2) + .await + .expect("page 2 should succeed"); + + assert_eq!(page1.total, 3); + assert_eq!(page1.limit, 2); + assert_eq!(page1.page, 1); + assert_eq!(page1.data.len(), 2); + assert_eq!(page2.data.len(), 1); + + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_clickhouse_command_get_table_data_by_conn_invalid_pagination_returns_error() { + let docker = (!clickhouse_context::should_reuse_local_db()).then(Cli::default); + let (_clickhouse_container, form) = + clickhouse_context::clickhouse_form_from_test_context(docker.as_ref()); + wait_until_clickhouse_ready(&form).await; + + let database = form + .database + .clone() + .unwrap_or_else(|| "default".to_string()); + let table = unique_table_name("dbpaw_cmd_invalid_page"); + prepare_query_test_table(&form, &table).await; + + let result = query::get_table_data_by_conn(form.clone(), database, table.clone(), 0, 10).await; + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(error.contains("[VALIDATION_ERROR]")); + + cleanup_table(&form, &table).await; +} diff --git a/src-tauri/tests/duckdb_command_integration.rs b/src-tauri/tests/duckdb_command_integration.rs new file mode 100644 index 00000000..a3383adb --- /dev/null +++ b/src-tauri/tests/duckdb_command_integration.rs @@ -0,0 +1,346 @@ +use dbpaw_lib::commands::{connection, metadata, query}; +use dbpaw_lib::db::drivers::duckdb::DuckdbDriver; +use dbpaw_lib::db::drivers::DatabaseDriver; +use dbpaw_lib::models::ConnectionForm; +use std::env; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; +use uuid::Uuid; + +fn duckdb_test_path() -> PathBuf { + if let Ok(v) = env::var("DUCKDB_IT_DB_PATH") { + return PathBuf::from(v); + } + let mut p = env::temp_dir(); + p.push(format!("dbpaw-duckdb-cmd-{}.db", Uuid::new_v4())); + p +} + +fn unique_table_name(prefix: &str) -> String { + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time should be after unix epoch") + .as_millis(); + format!("{}_{}", prefix, millis) +} + +async fn prepare_query_test_table(form: &ConnectionForm, table: &str) { + let driver = DuckdbDriver::connect(form) + .await + .expect("failed to connect duckdb driver"); + + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .expect("drop table should succeed"); + driver + .execute_query(format!( + "CREATE TABLE {} (id INTEGER PRIMARY KEY, name VARCHAR)", + table + )) + .await + .expect("create table should succeed"); + driver + .execute_query(format!( + "INSERT INTO {} (id, name) VALUES (1, 'DbPaw')", + table + )) + .await + .expect("insert row should succeed"); + driver.close().await; +} + +async fn cleanup_table(form: &ConnectionForm, table: &str) { + let driver = DuckdbDriver::connect(form) + .await + .expect("failed to connect duckdb driver for cleanup"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .ok(); + driver.close().await; +} + +async fn execute_by_conn_sql( + form: ConnectionForm, + sql: String, +) -> Result { + query::execute_by_conn_direct(form, sql).await +} + +#[tokio::test] +#[ignore] +async fn test_duckdb_command_test_connection_success() { + let db_path = duckdb_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "duckdb".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let result = connection::test_connection_ephemeral(form) + .await + .expect("test_connection_ephemeral should succeed"); + + assert!(result.success); + assert!(result.latency_ms.is_some()); + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_duckdb_command_test_connection_invalid_file_path_returns_error() { + let form = ConnectionForm { + driver: "duckdb".to_string(), + file_path: Some("/nonexistent/path/to/database.db".to_string()), + ..Default::default() + }; + + let result = connection::test_connection_ephemeral(form).await; + + // DuckDB might succeed with nonexistent path (creates file), so we adjust expectations + // Alternative: test with read-only or permissions issue + assert!(result.is_ok() || result.is_err()); + if result.is_err() { + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); + } +} + +#[tokio::test] +#[ignore] +async fn test_duckdb_command_list_tables_by_conn_contains_created_table() { + let db_path = duckdb_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "duckdb".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let table = unique_table_name("dbpaw_cmd_tables"); + prepare_query_test_table(&form, &table).await; + + let tables = metadata::list_tables_by_conn(form.clone()) + .await + .expect("list_tables_by_conn should succeed"); + + assert!(tables.iter().any(|t| t.name == table)); + cleanup_table(&form, &table).await; + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_duckdb_command_list_databases_contains_default_db() { + let db_path = duckdb_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "duckdb".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let databases = connection::list_databases(form) + .await + .expect("list_databases should succeed"); + + assert!(!databases.is_empty()); + assert!(databases.iter().all(|db| !db.trim().is_empty())); + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_duckdb_command_execute_by_conn_select_returns_rows() { + let db_path = duckdb_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "duckdb".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let table = unique_table_name("dbpaw_cmd_exec_select"); + prepare_query_test_table(&form, &table).await; + + let sql = format!("SELECT id, name FROM {} ORDER BY id", table); + let result = execute_by_conn_sql(form.clone(), sql) + .await + .expect("execute_by_conn should succeed"); + + assert!(result.success); + assert!(result.row_count >= 1); + assert!(!result.data.is_empty()); + let row = result.data.first().expect("result row should exist"); + let name = row.get("name").and_then(|v| v.as_str()); + assert_eq!(name, Some("DbPaw")); + cleanup_table(&form, &table).await; + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_duckdb_command_execute_by_conn_invalid_sql_returns_error() { + let db_path = duckdb_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "duckdb".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let result = execute_by_conn_sql( + form, + "SELECT * FROM __dbpaw_missing_command_table".to_string(), + ) + .await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_duckdb_command_execute_by_conn_insert_affects_rows() { + let db_path = duckdb_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "duckdb".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let table = unique_table_name("dbpaw_cmd_exec_insert"); + + let driver = DuckdbDriver::connect(&form) + .await + .expect("failed to connect duckdb driver"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .expect("drop table should succeed"); + driver + .execute_query(format!( + "CREATE TABLE {} (id INTEGER PRIMARY KEY, name VARCHAR)", + table + )) + .await + .expect("create table should succeed"); + driver.close().await; + + let sql = format!("INSERT INTO {} (id, name) VALUES (1, 'alpha')", table); + let result = execute_by_conn_sql(form.clone(), sql) + .await + .expect("execute_by_conn insert should succeed"); + assert!(result.success); + assert_eq!(result.row_count, 1); + + cleanup_table(&form, &table).await; + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_duckdb_command_get_table_data_by_conn_pagination_works() { + let db_path = duckdb_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "duckdb".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let database = "".to_string(); // DuckDB uses empty string for default database + let table = unique_table_name("dbpaw_cmd_page"); + + let driver = DuckdbDriver::connect(&form) + .await + .expect("failed to connect duckdb driver"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .expect("drop table should succeed"); + driver + .execute_query(format!( + "CREATE TABLE {} (id INTEGER PRIMARY KEY, name VARCHAR)", + table + )) + .await + .expect("create table should succeed"); + driver + .execute_query(format!( + "INSERT INTO {} (id, name) VALUES (1, 'a'), (2, 'b'), (3, 'c')", + table + )) + .await + .expect("insert rows should succeed"); + driver.close().await; + + let page1 = query::get_table_data_by_conn(form.clone(), database.clone(), table.clone(), 1, 2) + .await + .expect("page 1 should succeed"); + let page2 = query::get_table_data_by_conn(form.clone(), database, table.clone(), 2, 2) + .await + .expect("page 2 should succeed"); + + assert_eq!(page1.total, 3); + assert_eq!(page1.limit, 2); + assert_eq!(page1.page, 1); + assert_eq!(page1.data.len(), 2); + assert_eq!(page2.data.len(), 1); + + cleanup_table(&form, &table).await; + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_duckdb_command_get_table_data_by_conn_invalid_pagination_returns_error() { + let db_path = duckdb_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "duckdb".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let database = "".to_string(); + let table = unique_table_name("dbpaw_cmd_invalid_page"); + prepare_query_test_table(&form, &table).await; + + let result = query::get_table_data_by_conn(form.clone(), database, table.clone(), 0, 10).await; + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(error.contains("[VALIDATION_ERROR]")); + + cleanup_table(&form, &table).await; + + // Cleanup + let _ = std::fs::remove_file(db_path); +} diff --git a/src-tauri/tests/mariadb_command_integration.rs b/src-tauri/tests/mariadb_command_integration.rs new file mode 100644 index 00000000..bba658a9 --- /dev/null +++ b/src-tauri/tests/mariadb_command_integration.rs @@ -0,0 +1,337 @@ +#[path = "common/mariadb_context.rs"] +mod mariadb_context; + +use dbpaw_lib::commands::{connection, metadata, query}; +use dbpaw_lib::db::drivers::mysql::MysqlDriver; +use dbpaw_lib::db::drivers::DatabaseDriver; +use dbpaw_lib::models::ConnectionForm; +use std::time::{SystemTime, UNIX_EPOCH}; +use testcontainers::clients::Cli; +use tokio::time::{sleep, Duration}; + +fn unique_table_name(prefix: &str) -> String { + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time should be after unix epoch") + .as_millis(); + format!("{}_{}", prefix, millis) +} + +async fn wait_until_mariadb_ready(form: &ConnectionForm) { + let mut last_error = String::new(); + for _ in 0..45 { + let probe = form.clone(); + match connection::test_connection_ephemeral(probe).await { + Ok(_) => return, + Err(err) => { + last_error = err; + sleep(Duration::from_secs(1)).await; + } + } + } + panic!("mariadb is not ready for command tests: {last_error}"); +} + +async fn prepare_query_test_table(form: &ConnectionForm, table: &str) { + let driver = MysqlDriver::connect(form) + .await + .expect("failed to connect mariadb driver"); + + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .expect("drop table should succeed"); + driver + .execute_query(format!( + "CREATE TABLE {} (id INT PRIMARY KEY, name VARCHAR(64))", + table + )) + .await + .expect("create table should succeed"); + driver + .execute_query(format!( + "INSERT INTO {} (id, name) VALUES (1, 'DbPaw')", + table + )) + .await + .expect("insert row should succeed"); + driver.close().await; +} + +async fn cleanup_table(form: &ConnectionForm, table: &str) { + let driver = MysqlDriver::connect(form) + .await + .expect("failed to connect mariadb driver for cleanup"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .expect("drop table should succeed"); + driver.close().await; +} + +async fn execute_by_conn_sql( + form: ConnectionForm, + sql: String, +) -> Result { + query::execute_by_conn_direct(form, sql).await +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_test_connection_success() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + wait_until_mariadb_ready(&form).await; + + let result = connection::test_connection_ephemeral(form) + .await + .expect("test_connection_ephemeral should succeed"); + + assert!(result.success); + assert!(result.latency_ms.is_some()); +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_test_connection_invalid_password_returns_error() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, mut form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + let ready_form = form.clone(); + wait_until_mariadb_ready(&ready_form).await; + form.password = Some("dbpaw_wrong_password".to_string()); + + let result = connection::test_connection_ephemeral(form).await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_list_tables_by_conn_contains_created_table() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + wait_until_mariadb_ready(&form).await; + let table = unique_table_name("dbpaw_cmd_tables"); + prepare_query_test_table(&form, &table).await; + + let tables = metadata::list_tables_by_conn(form.clone()) + .await + .expect("list_tables_by_conn should succeed"); + + assert!(tables.iter().any(|t| t.name == table)); + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_list_tables_by_conn_invalid_credentials_returns_error() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, mut form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + let ready_form = form.clone(); + wait_until_mariadb_ready(&ready_form).await; + form.password = Some("dbpaw_wrong_password".to_string()); + + let result = metadata::list_tables_by_conn(form).await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_list_databases_contains_target_db() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + wait_until_mariadb_ready(&form).await; + let target_db = form + .database + .clone() + .unwrap_or_else(|| "test_db".to_string()); + + let databases = connection::list_databases(form) + .await + .expect("list_databases should succeed"); + + assert!(!databases.is_empty()); + assert!(databases.iter().any(|db| db == &target_db)); + assert!(databases.iter().all(|db| !db.trim().is_empty())); +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_list_databases_invalid_credentials_returns_error() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, mut form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + let ready_form = form.clone(); + wait_until_mariadb_ready(&ready_form).await; + form.password = Some("dbpaw_wrong_password".to_string()); + + let result = connection::list_databases(form).await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_execute_by_conn_select_returns_rows() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + wait_until_mariadb_ready(&form).await; + let table = unique_table_name("dbpaw_cmd_exec_select"); + prepare_query_test_table(&form, &table).await; + + let sql = format!("SELECT id, name FROM {} ORDER BY id", table); + let result = execute_by_conn_sql(form.clone(), sql) + .await + .expect("execute_by_conn should succeed"); + + assert!(result.success); + assert!(result.row_count >= 1); + assert!(!result.data.is_empty()); + let row = result.data.first().expect("result row should exist"); + let name = row.get("name").and_then(|v| v.as_str()); + assert_eq!(name, Some("DbPaw")); + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_execute_by_conn_invalid_sql_returns_error() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + wait_until_mariadb_ready(&form).await; + + let result = execute_by_conn_sql( + form, + "SELECT * FROM __dbpaw_missing_command_table".to_string(), + ) + .await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_execute_by_conn_insert_affects_rows() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + wait_until_mariadb_ready(&form).await; + let table = unique_table_name("dbpaw_cmd_exec_insert"); + + let driver = MysqlDriver::connect(&form) + .await + .expect("failed to connect mariadb driver"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .expect("drop table should succeed"); + driver + .execute_query(format!( + "CREATE TABLE {} (id INT PRIMARY KEY, name VARCHAR(64))", + table + )) + .await + .expect("create table should succeed"); + driver.close().await; + + let sql = format!("INSERT INTO {} (id, name) VALUES (1, 'alpha')", table); + let result = execute_by_conn_sql(form.clone(), sql) + .await + .expect("execute_by_conn insert should succeed"); + assert!(result.success); + assert_eq!(result.row_count, 1); + + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_get_table_data_by_conn_pagination_works() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + wait_until_mariadb_ready(&form).await; + + let database = form + .database + .clone() + .unwrap_or_else(|| "test_db".to_string()); + let table = unique_table_name("dbpaw_cmd_page"); + + let driver = MysqlDriver::connect(&form) + .await + .expect("failed to connect mariadb driver"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .expect("drop table should succeed"); + driver + .execute_query(format!( + "CREATE TABLE {} (id INT PRIMARY KEY, name VARCHAR(64))", + table + )) + .await + .expect("create table should succeed"); + driver + .execute_query(format!( + "INSERT INTO {} (id, name) VALUES (1, 'a'), (2, 'b'), (3, 'c')", + table + )) + .await + .expect("insert rows should succeed"); + driver.close().await; + + let page1 = query::get_table_data_by_conn(form.clone(), database.clone(), table.clone(), 1, 2) + .await + .expect("page 1 should succeed"); + let page2 = query::get_table_data_by_conn(form.clone(), database, table.clone(), 2, 2) + .await + .expect("page 2 should succeed"); + + assert_eq!(page1.total, 3); + assert_eq!(page1.limit, 2); + assert_eq!(page1.page, 1); + assert_eq!(page1.data.len(), 2); + assert_eq!(page2.data.len(), 1); + + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_mariadb_command_get_table_data_by_conn_invalid_pagination_returns_error() { + let docker = (!mariadb_context::should_reuse_local_db()).then(Cli::default); + let (_mariadb_container, form) = + mariadb_context::mariadb_form_from_test_context(docker.as_ref()); + wait_until_mariadb_ready(&form).await; + + let database = form + .database + .clone() + .unwrap_or_else(|| "test_db".to_string()); + let table = unique_table_name("dbpaw_cmd_invalid_page"); + prepare_query_test_table(&form, &table).await; + + let result = query::get_table_data_by_conn(form.clone(), database, table.clone(), 0, 10).await; + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(error.contains("[VALIDATION_ERROR]")); + + cleanup_table(&form, &table).await; +} diff --git a/src-tauri/tests/mssql_command_integration.rs b/src-tauri/tests/mssql_command_integration.rs new file mode 100644 index 00000000..c13f0634 --- /dev/null +++ b/src-tauri/tests/mssql_command_integration.rs @@ -0,0 +1,326 @@ +#[path = "common/mssql_context.rs"] +mod mssql_context; + +use dbpaw_lib::commands::{connection, metadata, query}; +use dbpaw_lib::db::drivers::mssql::MssqlDriver; +use dbpaw_lib::db::drivers::DatabaseDriver; +use dbpaw_lib::models::ConnectionForm; +use std::time::{SystemTime, UNIX_EPOCH}; +use testcontainers::clients::Cli; +use tokio::time::{sleep, Duration}; + +fn unique_table_name(prefix: &str) -> String { + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time should be after unix epoch") + .as_millis(); + format!("{}_{}", prefix, millis) +} + +async fn wait_until_mssql_ready(form: &ConnectionForm) { + let mut last_error = String::new(); + for _ in 0..60 { + let probe = form.clone(); + match connection::test_connection_ephemeral(probe).await { + Ok(_) => return, + Err(err) => { + last_error = err; + sleep(Duration::from_secs(1)).await; + } + } + } + panic!("mssql is not ready for command tests: {last_error}"); +} + +async fn prepare_query_test_table(form: &ConnectionForm, table: &str) { + let driver = MssqlDriver::connect(form) + .await + .expect("failed to connect mssql driver"); + + driver + .execute_query(format!("IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", table, table)) + .await + .ok(); + driver + .execute_query(format!( + "CREATE TABLE {} (id INT PRIMARY KEY, name NVARCHAR(64))", + table + )) + .await + .expect("create table should succeed"); + driver + .execute_query(format!( + "INSERT INTO {} (id, name) VALUES (1, N'DbPaw')", + table + )) + .await + .expect("insert row should succeed"); + driver.close().await; +} + +async fn cleanup_table(form: &ConnectionForm, table: &str) { + let driver = MssqlDriver::connect(form) + .await + .expect("failed to connect mssql driver for cleanup"); + driver + .execute_query(format!("IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", table, table)) + .await + .ok(); + driver.close().await; +} + +async fn execute_by_conn_sql( + form: ConnectionForm, + sql: String, +) -> Result { + query::execute_by_conn_direct(form, sql).await +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_test_connection_success() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + wait_until_mssql_ready(&form).await; + + let result = connection::test_connection_ephemeral(form) + .await + .expect("test_connection_ephemeral should succeed"); + + assert!(result.success); + assert!(result.latency_ms.is_some()); +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_test_connection_invalid_password_returns_error() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, mut form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + let ready_form = form.clone(); + wait_until_mssql_ready(&ready_form).await; + form.password = Some("dbpaw_wrong_password".to_string()); + + let result = connection::test_connection_ephemeral(form).await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_list_tables_by_conn_contains_created_table() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + wait_until_mssql_ready(&form).await; + let table = unique_table_name("dbpaw_cmd_tables"); + prepare_query_test_table(&form, &table).await; + + let tables = metadata::list_tables_by_conn(form.clone()) + .await + .expect("list_tables_by_conn should succeed"); + + assert!(tables.iter().any(|t| t.name == table)); + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_list_tables_by_conn_invalid_credentials_returns_error() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, mut form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + let ready_form = form.clone(); + wait_until_mssql_ready(&ready_form).await; + form.password = Some("dbpaw_wrong_password".to_string()); + + let result = metadata::list_tables_by_conn(form).await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_list_databases_contains_target_db() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + wait_until_mssql_ready(&form).await; + let target_db = form + .database + .clone() + .unwrap_or_else(|| "master".to_string()); + + let databases = connection::list_databases(form) + .await + .expect("list_databases should succeed"); + + assert!(!databases.is_empty()); + assert!(databases.iter().any(|db| db == &target_db)); + assert!(databases.iter().all(|db| !db.trim().is_empty())); +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_list_databases_invalid_credentials_returns_error() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, mut form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + let ready_form = form.clone(); + wait_until_mssql_ready(&ready_form).await; + form.password = Some("dbpaw_wrong_password".to_string()); + + let result = connection::list_databases(form).await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_execute_by_conn_select_returns_rows() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + wait_until_mssql_ready(&form).await; + let table = unique_table_name("dbpaw_cmd_exec_select"); + prepare_query_test_table(&form, &table).await; + + let sql = format!("SELECT id, name FROM {} ORDER BY id", table); + let result = execute_by_conn_sql(form.clone(), sql) + .await + .expect("execute_by_conn should succeed"); + + assert!(result.success); + assert!(result.row_count >= 1); + assert!(!result.data.is_empty()); + let row = result.data.first().expect("result row should exist"); + let name = row.get("name").and_then(|v| v.as_str()); + assert_eq!(name, Some("DbPaw")); + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_execute_by_conn_invalid_sql_returns_error() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + wait_until_mssql_ready(&form).await; + + let result = execute_by_conn_sql( + form, + "SELECT * FROM __dbpaw_missing_command_table".to_string(), + ) + .await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_execute_by_conn_insert_affects_rows() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + wait_until_mssql_ready(&form).await; + let table = unique_table_name("dbpaw_cmd_exec_insert"); + + let driver = MssqlDriver::connect(&form) + .await + .expect("failed to connect mssql driver"); + driver + .execute_query(format!("IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", table, table)) + .await + .ok(); + driver + .execute_query(format!( + "CREATE TABLE {} (id INT PRIMARY KEY, name NVARCHAR(64))", + table + )) + .await + .expect("create table should succeed"); + driver.close().await; + + let sql = format!("INSERT INTO {} (id, name) VALUES (1, N'alpha')", table); + let result = execute_by_conn_sql(form.clone(), sql) + .await + .expect("execute_by_conn insert should succeed"); + assert!(result.success); + assert_eq!(result.row_count, 1); + + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_get_table_data_by_conn_pagination_works() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + wait_until_mssql_ready(&form).await; + + let database = form + .database + .clone() + .unwrap_or_else(|| "master".to_string()); + let table = unique_table_name("dbpaw_cmd_page"); + + let driver = MssqlDriver::connect(&form) + .await + .expect("failed to connect mssql driver"); + driver + .execute_query(format!("IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", table, table)) + .await + .ok(); + driver + .execute_query(format!( + "CREATE TABLE {} (id INT PRIMARY KEY, name NVARCHAR(64))", + table + )) + .await + .expect("create table should succeed"); + driver + .execute_query(format!( + "INSERT INTO {} (id, name) VALUES (1, N'a'), (2, N'b'), (3, N'c')", + table + )) + .await + .expect("insert rows should succeed"); + driver.close().await; + + let page1 = query::get_table_data_by_conn(form.clone(), database.clone(), table.clone(), 1, 2) + .await + .expect("page 1 should succeed"); + let page2 = query::get_table_data_by_conn(form.clone(), database, table.clone(), 2, 2) + .await + .expect("page 2 should succeed"); + + assert_eq!(page1.total, 3); + assert_eq!(page1.limit, 2); + assert_eq!(page1.page, 1); + assert_eq!(page1.data.len(), 2); + assert_eq!(page2.data.len(), 1); + + cleanup_table(&form, &table).await; +} + +#[tokio::test] +#[ignore] +async fn test_mssql_command_get_table_data_by_conn_invalid_pagination_returns_error() { + let docker = (!mssql_context::should_reuse_local_db()).then(Cli::default); + let (_mssql_container, form) = mssql_context::mssql_form_from_test_context(docker.as_ref()); + wait_until_mssql_ready(&form).await; + + let database = form + .database + .clone() + .unwrap_or_else(|| "master".to_string()); + let table = unique_table_name("dbpaw_cmd_invalid_page"); + prepare_query_test_table(&form, &table).await; + + let result = query::get_table_data_by_conn(form.clone(), database, table.clone(), 0, 10).await; + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(error.contains("[VALIDATION_ERROR]")); + + cleanup_table(&form, &table).await; +} diff --git a/src-tauri/tests/sqlite_command_integration.rs b/src-tauri/tests/sqlite_command_integration.rs new file mode 100644 index 00000000..8f031a47 --- /dev/null +++ b/src-tauri/tests/sqlite_command_integration.rs @@ -0,0 +1,343 @@ +use dbpaw_lib::commands::{connection, metadata, query}; +use dbpaw_lib::db::drivers::sqlite::SqliteDriver; +use dbpaw_lib::db::drivers::DatabaseDriver; +use dbpaw_lib::models::ConnectionForm; +use std::env; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; +use uuid::Uuid; + +fn sqlite_test_path() -> PathBuf { + if let Ok(v) = env::var("SQLITE_IT_DB_PATH") { + return PathBuf::from(v); + } + let mut p = env::temp_dir(); + p.push(format!("dbpaw-sqlite-cmd-{}.db", Uuid::new_v4())); + p +} + +fn unique_table_name(prefix: &str) -> String { + let millis = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time should be after unix epoch") + .as_millis(); + format!("{}_{}", prefix, millis) +} + +async fn prepare_query_test_table(form: &ConnectionForm, table: &str) { + let driver = SqliteDriver::connect(form) + .await + .expect("failed to connect sqlite driver"); + + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .expect("drop table should succeed"); + driver + .execute_query(format!( + "CREATE TABLE {} (id INTEGER PRIMARY KEY, name TEXT)", + table + )) + .await + .expect("create table should succeed"); + driver + .execute_query(format!( + "INSERT INTO {} (id, name) VALUES (1, 'DbPaw')", + table + )) + .await + .expect("insert row should succeed"); + driver.close().await; +} + +async fn cleanup_table(form: &ConnectionForm, table: &str) { + let driver = SqliteDriver::connect(form) + .await + .expect("failed to connect sqlite driver for cleanup"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .ok(); + driver.close().await; +} + +async fn execute_by_conn_sql( + form: ConnectionForm, + sql: String, +) -> Result { + query::execute_by_conn_direct(form, sql).await +} + +#[tokio::test] +#[ignore] +async fn test_sqlite_command_test_connection_success() { + let db_path = sqlite_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "sqlite".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let result = connection::test_connection_ephemeral(form) + .await + .expect("test_connection_ephemeral should succeed"); + + assert!(result.success); + assert!(result.latency_ms.is_some()); + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_sqlite_command_test_connection_invalid_file_path_returns_error() { + let form = ConnectionForm { + driver: "sqlite".to_string(), + file_path: Some("/nonexistent/path/to/database.db".to_string()), + ..Default::default() + }; + + let result = connection::test_connection_ephemeral(form).await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); +} + +#[tokio::test] +#[ignore] +async fn test_sqlite_command_list_tables_by_conn_contains_created_table() { + let db_path = sqlite_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "sqlite".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let table = unique_table_name("dbpaw_cmd_tables"); + prepare_query_test_table(&form, &table).await; + + let tables = metadata::list_tables_by_conn(form.clone()) + .await + .expect("list_tables_by_conn should succeed"); + + assert!(tables.iter().any(|t| t.name == table)); + cleanup_table(&form, &table).await; + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_sqlite_command_list_databases_contains_main() { + let db_path = sqlite_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "sqlite".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let databases = connection::list_databases(form) + .await + .expect("list_databases should succeed"); + + assert!(!databases.is_empty()); + assert!(databases.contains(&"main".to_string())); + assert!(databases.iter().all(|db| !db.trim().is_empty())); + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_sqlite_command_execute_by_conn_select_returns_rows() { + let db_path = sqlite_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "sqlite".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let table = unique_table_name("dbpaw_cmd_exec_select"); + prepare_query_test_table(&form, &table).await; + + let sql = format!("SELECT id, name FROM {} ORDER BY id", table); + let result = execute_by_conn_sql(form.clone(), sql) + .await + .expect("execute_by_conn should succeed"); + + assert!(result.success); + assert!(result.row_count >= 1); + assert!(!result.data.is_empty()); + let row = result.data.first().expect("result row should exist"); + let name = row.get("name").and_then(|v| v.as_str()); + assert_eq!(name, Some("DbPaw")); + cleanup_table(&form, &table).await; + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_sqlite_command_execute_by_conn_invalid_sql_returns_error() { + let db_path = sqlite_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "sqlite".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let result = execute_by_conn_sql( + form, + "SELECT * FROM __dbpaw_missing_command_table".to_string(), + ) + .await; + + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(!error.trim().is_empty()); + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_sqlite_command_execute_by_conn_insert_affects_rows() { + let db_path = sqlite_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "sqlite".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let table = unique_table_name("dbpaw_cmd_exec_insert"); + + let driver = SqliteDriver::connect(&form) + .await + .expect("failed to connect sqlite driver"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .expect("drop table should succeed"); + driver + .execute_query(format!( + "CREATE TABLE {} (id INTEGER PRIMARY KEY, name TEXT)", + table + )) + .await + .expect("create table should succeed"); + driver.close().await; + + let sql = format!("INSERT INTO {} (id, name) VALUES (1, 'alpha')", table); + let result = execute_by_conn_sql(form.clone(), sql) + .await + .expect("execute_by_conn insert should succeed"); + assert!(result.success); + assert_eq!(result.row_count, 1); + + cleanup_table(&form, &table).await; + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_sqlite_command_get_table_data_by_conn_pagination_works() { + let db_path = sqlite_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "sqlite".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let database = "main".to_string(); + let table = unique_table_name("dbpaw_cmd_page"); + + let driver = SqliteDriver::connect(&form) + .await + .expect("failed to connect sqlite driver"); + driver + .execute_query(format!("DROP TABLE IF EXISTS {}", table)) + .await + .expect("drop table should succeed"); + driver + .execute_query(format!( + "CREATE TABLE {} (id INTEGER PRIMARY KEY, name TEXT)", + table + )) + .await + .expect("create table should succeed"); + driver + .execute_query(format!( + "INSERT INTO {} (id, name) VALUES (1, 'a'), (2, 'b'), (3, 'c')", + table + )) + .await + .expect("insert rows should succeed"); + driver.close().await; + + let page1 = query::get_table_data_by_conn(form.clone(), database.clone(), table.clone(), 1, 2) + .await + .expect("page 1 should succeed"); + let page2 = query::get_table_data_by_conn(form.clone(), database, table.clone(), 2, 2) + .await + .expect("page 2 should succeed"); + + assert_eq!(page1.total, 3); + assert_eq!(page1.limit, 2); + assert_eq!(page1.page, 1); + assert_eq!(page1.data.len(), 2); + assert_eq!(page2.data.len(), 1); + + cleanup_table(&form, &table).await; + + // Cleanup + let _ = std::fs::remove_file(db_path); +} + +#[tokio::test] +#[ignore] +async fn test_sqlite_command_get_table_data_by_conn_invalid_pagination_returns_error() { + let db_path = sqlite_test_path(); + let db_path_str = db_path.to_string_lossy().to_string(); + + let form = ConnectionForm { + driver: "sqlite".to_string(), + file_path: Some(db_path_str.clone()), + ..Default::default() + }; + + let database = "main".to_string(); + let table = unique_table_name("dbpaw_cmd_invalid_page"); + prepare_query_test_table(&form, &table).await; + + let result = query::get_table_data_by_conn(form.clone(), database, table.clone(), 0, 10).await; + assert!(result.is_err()); + let error = result.err().unwrap_or_default(); + assert!(error.contains("[VALIDATION_ERROR]")); + + cleanup_table(&form, &table).await; + + // Cleanup + let _ = std::fs::remove_file(db_path); +} From 285359fcd705fb3c7f266699fac8e0214b00bdad Mon Sep 17 00:00:00 2001 From: codeErrorSleep Date: Tue, 31 Mar 2026 13:35:45 +0800 Subject: [PATCH 5/7] chore:improve the development tool chain and CI hierarchical access control -Add independent quality check portals such as' typecheck ',' lint ',' trust: check ' -Add 'test: smoke' unified quick quality gate and 'test: ci' complete access control script -Reconstruct CI to hierarchical access control: run smoke tests first, and then execute multi database integration tests in parallel -New multi platform construction workflow, supporting Linux/macOS/Windows -Update development documents to reflect new testing strategies and workflows -Fix the problem of missing newline characters at the end of the tauri.conf.json file -Add Node.js type definition for tsconfig.node.json -Add vite.config.d.ts type declaration file --- .github/workflows/build-multiplatform.yml | 71 ++++ .github/workflows/ci.yml | 100 ++++- bun.lock | 5 + docs/zh/Community/CONTRIBUTING.md | 8 +- .../DBPAW_HARNESS_ADOPTION_PLAN.md | 343 ++++++++++++++++++ docs/zh/Development/DEVELOPMENT.md | 33 +- package.json | 12 +- src-tauri/tauri.conf.json | 2 +- tsconfig.node.json | 3 +- tsconfig.node.tsbuildinfo | 1 + tsconfig.tsbuildinfo | 1 + vite.config.d.ts | 2 + vite.config.js | 75 ++++ vite.config.ts | 10 +- 14 files changed, 631 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/build-multiplatform.yml create mode 100644 docs/zh/Development/DBPAW_HARNESS_ADOPTION_PLAN.md create mode 100644 tsconfig.node.tsbuildinfo create mode 100644 tsconfig.tsbuildinfo create mode 100644 vite.config.d.ts create mode 100644 vite.config.js diff --git a/.github/workflows/build-multiplatform.yml b/.github/workflows/build-multiplatform.yml new file mode 100644 index 00000000..79c4d0c8 --- /dev/null +++ b/.github/workflows/build-multiplatform.yml @@ -0,0 +1,71 @@ +name: Multi-Platform Build + +on: + workflow_dispatch: + push: + tags: + - "v*" + +jobs: + build: + name: Build (${{ matrix.platform }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + + strategy: + fail-fast: false + matrix: + include: + - platform: linux + os: ubuntu-22.04 + - platform: macos + os: macos-latest + - platform: windows + os: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + node_modules + ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install frontend dependencies + run: bun install --frozen-lockfile + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build artifacts + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri -> src-tauri/target + + - name: Install Linux dependencies + if: matrix.platform == 'linux' + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev + + - name: Build Tauri app + run: bun tauri build + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dbpaw-${{ matrix.platform }} + path: | + src-tauri/target/release/bundle/ + retention-days: 7 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d96ba51b..29a31554 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,11 +4,16 @@ on: pull_request: branches: - main + push: + branches: + - main jobs: - test: + # Fast smoke tests run first to fail fast + smoke: + name: Smoke Tests (typecheck, lint, unit) runs-on: ubuntu-22.04 - timeout-minutes: 40 + timeout-minutes: 10 steps: - name: Checkout @@ -34,33 +39,94 @@ jobs: - name: Setup Rust toolchain uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt - name: Cache Rust build artifacts uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri -> src-tauri/target - - name: Run unit tests - run: bun run test:unit + - name: Run smoke tests + run: bun run test:smoke + + # Parallel database integration tests + integration: + name: Integration Tests (${{ matrix.database }}) + runs-on: ubuntu-22.04 + needs: smoke + timeout-minutes: 15 - - name: Run service tests - run: bun run test:service + strategy: + fail-fast: false + matrix: + database: + - mysql + - postgres + - mariadb + - clickhouse + - mssql + - sqlite + - duckdb - - name: Run rust unit tests - run: bun run test:rust:unit + steps: + - name: Checkout + uses: actions/checkout@v4 - - name: Run integration tests (MySQL + Postgres with testcontainers) - run: IT_DB=all bun run test:integration + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Cache Bun dependencies + uses: actions/cache@v4 + with: + path: | + node_modules + ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install frontend dependencies + run: bun install --frozen-lockfile + + - name: Setup Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust build artifacts + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri -> src-tauri/target + key: ${{ matrix.database }} + + - name: Run ${{ matrix.database }} integration tests + run: IT_DB=${{ matrix.database }} bun run test:integration - name: Docker diagnostics on failure if: failure() run: | echo "==== docker ps -a ====" docker ps -a || true - echo "==== recent mysql/postgres logs ====" - for image in mysql:8.0 postgres:16-alpine; do - for id in $(docker ps -aq --filter "ancestor=${image}"); do - echo "--- logs for $id (${image}) ---" - docker logs "$id" || true - done - done + echo "==== Container logs ====" + docker ps -aq | xargs -I {} sh -c 'echo "--- Container {} ---" && docker logs {} 2>&1 | tail -50' || true + + # Summary job to check all tests passed + ci-success: + name: CI Success + runs-on: ubuntu-22.04 + needs: [smoke, integration] + if: always() + + steps: + - name: Check all jobs succeeded + run: | + if [ "${{ needs.smoke.result }}" != "success" ]; then + echo "Smoke tests failed" + exit 1 + fi + if [ "${{ needs.integration.result }}" != "success" ]; then + echo "Integration tests failed" + exit 1 + fi + echo "All CI checks passed!" diff --git a/bun.lock b/bun.lock index d0e7ae3a..bd859ac7 100644 --- a/bun.lock +++ b/bun.lock @@ -84,6 +84,7 @@ "devDependencies": { "@tailwindcss/vite": "4.1.12", "@tauri-apps/cli": "^2", + "@types/node": "^24.5.2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", @@ -558,6 +559,8 @@ "@types/ms": ["@types/ms@2.1.0", "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@24.12.0", "https://registry.npmmirror.com/@types/node/-/node-24.12.0.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], + "@types/parse-json": ["@types/parse-json@4.0.2", "https://registry.npmmirror.com/@types/parse-json/-/parse-json-4.0.2.tgz", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], "@types/prop-types": ["@types/prop-types@15.7.15", "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], @@ -1106,6 +1109,8 @@ "typescript": ["typescript@5.8.3", "https://registry.npmmirror.com/typescript/-/typescript-5.8.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + "undici-types": ["undici-types@7.16.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "unified": ["unified@11.0.5", "https://registry.npmmirror.com/unified/-/unified-11.0.5.tgz", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unist-util-is": ["unist-util-is@6.0.1", "https://registry.npmmirror.com/unist-util-is/-/unist-util-is-6.0.1.tgz", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], diff --git a/docs/zh/Community/CONTRIBUTING.md b/docs/zh/Community/CONTRIBUTING.md index 9577e6c9..d009bf2b 100644 --- a/docs/zh/Community/CONTRIBUTING.md +++ b/docs/zh/Community/CONTRIBUTING.md @@ -16,10 +16,14 @@ 1. Fork ไป“ๅบ“ๅนถๆ–ฐๅปบๅˆ†ๆ”ฏ 2. ่ฎฉๆ”นๅŠจๅฐฝ้‡่š็„ฆ๏ผŒๆ–นไพฟ Review -3. ๅœจๆไบคๅ‰่ท‘ๆ ผๅผๅŒ–ไธŽๆต‹่ฏ•๏ผš +3. ๅœจๆไบคๅ‰่ท‘็ปŸไธ€่ดจ้‡้—จไธŽๆต‹่ฏ•๏ผš ```bash bun run format - bun run test:all + bun run test:smoke + ``` + ่‹ฅๆ”นๅŠจๆถ‰ๅŠๆ•ฐๆฎๅบ“่กŒไธบ๏ผŒๅ†่กฅ่ท‘๏ผš + ```bash + IT_DB=all bun run test:integration ``` 4. ๆไบค PR ๆ—ถๅปบ่ฎฎๅŒ…ๅซ๏ผš - ๆ”นไบ†ไป€ไนˆใ€ไธบไป€ไนˆ่ฆๆ”น diff --git a/docs/zh/Development/DBPAW_HARNESS_ADOPTION_PLAN.md b/docs/zh/Development/DBPAW_HARNESS_ADOPTION_PLAN.md new file mode 100644 index 00000000..34362614 --- /dev/null +++ b/docs/zh/Development/DBPAW_HARNESS_ADOPTION_PLAN.md @@ -0,0 +1,343 @@ +# DbPaw ็š„ Harness ๅŒ–ๆ”น้€ ๆธ…ๅ• + +## 1. ็›ฎๆ ‡ + +่ฟ™ไปฝๆ–‡ๆกฃไธๆ˜ฏโ€œๆ€Žไนˆๆ้—ฎโ€็š„ๆ็คบ่ฏๆ‰‹ๅ†Œ๏ผŒ่€Œๆ˜ฏไปŽ DbPaw ไป“ๅบ“็Žฐ็Šถๅ‡บๅ‘๏ผŒๆขณ็†ไธ€ไปฝๅฏ่ฝๅœฐ็š„ Harness ๅŒ–ๆ”น้€ ๆธ…ๅ•ใ€‚ + +็›ฎๆ ‡ๆ˜ฏๆŠŠๅฝ“ๅ‰โ€œAI ่ƒฝๅธฎๅฟ™ๆ”นไปฃ็ โ€็š„็Šถๆ€๏ผŒๆŽจ่ฟ›ๅˆฐโ€œAI ่ƒฝ็จณๅฎšๆŽฅๆ‰‹ไธ€ๆ•ดๆฎตๅผ€ๅ‘ๆต็จ‹โ€็š„็Šถๆ€ใ€‚ + +่ฟ™้‡Œๆ‰€่ฏด็š„ Harness ๅŒ–๏ผŒ้‡็‚นไธๆ˜ฏๅคšๅ†™ๅ‡ ๅฅ— Prompt๏ผŒ่€Œๆ˜ฏๆŠŠ้ซ˜้ข‘็ ”ๅ‘ๅŠจไฝœๆ”น้€ ๆˆ๏ผš + +- ๆœ‰ๆ ‡ๅ‡†ๅ…ฅๅฃ +- ๆœ‰ๆ˜Ž็กฎๅฝฑๅ“่Œƒๅ›ด +- ๆœ‰ๅ›บๅฎš้ชŒ่ฏ่ทฏๅพ„ +- ๆœ‰ๅฏๅค็”จไธ“้กนๆต็จ‹ +- ๆœ‰ไธ€่‡ด็š„็ป“ๆžœๆฑ‡ๆŠฅ + +## 2. ๅฝ“ๅ‰็Žฐ็Šถๅˆคๆ–ญ + +### 2.1 ๅทฒๆœ‰ๅŸบ็ก€ + +- ็›ฎๅฝ•็ป“ๆž„ๆธ…ๆ™ฐ๏ผŒๅ‰็ซฏใ€Tauriใ€Rust ้ฉฑๅŠจใ€ๆต‹่ฏ•่„šๆœฌ่พน็•Œๆ˜Ž็กฎ +- ๅทฒๆœ‰็ปŸไธ€่„šๆœฌๅ…ฅๅฃ๏ผš`test:unit`ใ€`test:service`ใ€`test:rust:unit`ใ€`test:integration` +- ๆ•ฐๆฎๅบ“้›†ๆˆๆต‹่ฏ•ๅทฒ็ปๆˆไฝ“็ณป +- ๅผ€ๅ‘ๆ–‡ๆกฃๅฏนๆœฌๅœฐๅทฅไฝœๆตๆ่ฟฐ่พƒๆธ…ๆฅš + +### 2.2 ๅฝ“ๅ‰็Ÿญๆฟ + +- ๅ‰็ซฏ็ผบๅฐ‘ๆ˜Ž็กฎ็š„ `lint` ไธŽ `typecheck` ็‹ฌ็ซ‹ๅ…ฅๅฃ +- ๆต‹่ฏ•็ญ–็•ฅๆ–‡ๆกฃไธŽไป“ๅบ“ๅฎž้™…็Žฐ็Šถๅญ˜ๅœจๆผ‚็งป +- CI ็›ฎๅ‰ๆ›ดๅโ€œไธ€ๆŠŠ่ท‘ๅ…จ้‡โ€๏ผŒ็ผบๅฐ‘ๅˆ†ๅฑ‚้—จ็ฆ +- ไธ€ไบ›้ซ˜้ข‘ๆต็จ‹่ฟ˜ๆฒกๆœ‰ๆฒ‰ๆท€ๆˆๆ ‡ๅ‡†ๅŒ–ไธ“้กนๆ‰ง่กŒๆต + +## 3. ไป€ไนˆๅซ DbPaw ็š„ Harness ๅŒ– + +Harness ๅŒ–ไธๆ˜ฏๅขžๅŠ ๆ›ดๅคšโ€œๆ็คบ่ฏๆŠ€ๅทงโ€๏ผŒ่€Œๆ˜ฏๆŠŠๅผ€ๅ‘ๅŠจไฝœ็ป„็ป‡ๆˆๅฏๆŒ็ปญๆŽฅ็ฎก็š„ๆต็จ‹ๅ•ๅ…ƒใ€‚ + +ๅฏน DbPaw ๆฅ่ฏด๏ผŒHarness ๆœ€ๆœ‰ไปทๅ€ผ็š„ๅœฐๆ–นๅœจไบŽ่ทจ่ถŠไปฅไธ‹ๅ‡ ๅฑ‚ๅš่ฟž็ปญๆ‰ง่กŒ๏ผš + +- React / TypeScript ๅ‰็ซฏ +- Tauri ๅ‘ฝไปคๅฑ‚ +- Rust ๆ•ฐๆฎๅบ“้ฉฑๅŠจๅฑ‚ +- ๅคšๆ•ฐๆฎๅบ“ๆต‹่ฏ•็Ÿฉ้˜ต + +ไนŸๅฐฑๆ˜ฏ่ฏด๏ผŒ็›ฎๆ ‡ไธๆ˜ฏ่ฎฉ AI ๆ›ดไผšโ€œๅ›ž็ญ”โ€๏ผŒ่€Œๆ˜ฏ่ฎฉๅฎƒๆ›ด็จณๅฎšๅœฐ๏ผš + +- ๅฎšไฝไปฃ็  +- ๅˆคๆ–ญๅฝฑๅ“้ข +- ไฟฎๆ”นๅฎž็Žฐ +- ้€‰ๆ‹ฉ้ชŒ่ฏ่ทฏๅพ„ +- ่ท‘ๆต‹่ฏ• +- ๆฑ‡ๆ€ป็ป“่ฎบ + +## 4. P0 ๆ”น้€ ้กน + +่ฟ™ไบ›ๆ˜ฏๆœ€ๅ€ผๅพ—ๅ…ˆๅš็š„๏ผŒๆŠ•ๅ…ฅ็›ธๅฏนๅฐ๏ผŒไฝ†ๆ”ถ็›Šๆœ€ๅคงใ€‚ + +### 4.1 ่กฅ้ฝ็ปŸไธ€่ดจ้‡้—จ + +ๅปบ่ฎฎ่กฅ้ฝไปฅไธ‹ๅ…ฅๅฃ๏ผš + +- `typecheck` +- `lint` +- Rust ไพงๆ˜Ž็กฎ็š„้™ๆ€ๆฃ€ๆŸฅๆˆ–ๆ ผๅผๆฃ€ๆŸฅๅ…ฅๅฃ +- ไธ€ไธช็ปŸไธ€็š„ๆœ€ๅฐ่ดจ้‡้—จๅ…ฅๅฃ๏ผŒไพ‹ๅฆ‚ `test:smoke` ๆˆ– `test:ci` + +ๅŽŸๅ› ๏ผš + +- Harness ๆ”นๅฎŒไปฃ็ ๅŽ๏ผŒ้œ€่ฆ็จณๅฎšใ€็ปŸไธ€็š„ๆœ€ๅฐ้ชŒ่ฏๅ‡บๅฃ +- ๅฝ“ๅ‰ไธป่ฆไพ่ต– `build`ใ€ๆต‹่ฏ•่„šๆœฌๅ’Œๆ ผๅผๅŒ–๏ผŒ้™ๆ€่ดจ้‡้—จไธๅคŸๆธ…ๆ™ฐ + +### 4.2 ๅปบ็ซ‹โ€œๆ”นๅŠจ โ†’ ้ชŒ่ฏโ€ๆ˜ ๅฐ„ + +ๅปบ่ฎฎๆŠŠไธๅŒ็›ฎๅฝ•ๅฏนๅบ”็š„ๆœ€ๅฐ้ชŒ่ฏ้›†ๆ˜Ž็กฎไธ‹ๆฅ๏ผŒไพ‹ๅฆ‚๏ผš + +- ๆ”น `src/components`ใ€`src/lib`๏ผš่ท‘ `test:unit` + `test:service` +- ๆ”น `src/services`๏ผš่ท‘ `test:service` +- ๆ”น `src-tauri/src/db`ใ€`src-tauri/src/commands`๏ผš่ท‘ `test:rust:unit` + ๅฏนๅบ”ๆ•ฐๆฎๅบ“็š„้›†ๆˆๆต‹่ฏ• +- ๆ”น่ทจๅฑ‚ๅŠŸ่ƒฝ๏ผšๅ‰็ซฏๆต‹่ฏ• + Rust ๆต‹่ฏ• + ๅฏนๅบ”ๆ•ฐๆฎๅบ“ๅ•ๅบ“ๅ›žๅฝ’ + +่ฟ™ๆ ทๅš็š„ๆ”ถ็›Šๆ˜ฏ๏ผš + +- Harness ไธ็”จๆฏๆฌก้ƒฝ็Œœ่ฏฅ่ท‘ไป€ไนˆ +- ้กน็›ฎๅ†…้ƒจๅฝขๆˆ็ปŸไธ€ๅ…ฑ่ฏ† + +### 4.3 ไฟฎๆญฃๆ–‡ๆกฃๆผ‚็งป๏ผŒๅปบ็ซ‹ๅ•ไธ€ไบ‹ๅฎžๆบ + +ๅฝ“ๅ‰ไป“ๅบ“ไธญ๏ผŒ้ƒจๅˆ†ๆ–‡ๆกฃๅ’Œๅฎž้™…ไปฃ็ ็Šถๆ€ๅญ˜ๅœจๅๅทฎใ€‚ +่ฟ™ไผš็›ดๆŽฅๅฝฑๅ“ Harness ๅฏน้กน็›ฎ็Žฐ็Šถ็š„ๅˆคๆ–ญใ€‚ + +ไผ˜ๅ…ˆ้œ€่ฆๅŒๆญฅ็š„ๅ†…ๅฎนๅŒ…ๆ‹ฌ๏ผš + +- ๆต‹่ฏ•็ญ–็•ฅๆ–‡ๆกฃ +- MySQL ๆต‹่ฏ•่ฆ†็›–ไธŽ็ผบๅฃ่ทŸ่ธชๆ–‡ๆกฃ +- CI ไธญๅ„ๆญฅ้ชค็š„ๆ่ฟฐ + +็›ฎๆ ‡ๆ˜ฏ่ฎฉโ€œๆ–‡ๆกฃๆ่ฟฐโ€ๅ’Œโ€œ็œŸๅฎžๆ‰ง่กŒ่ทฏๅพ„โ€ไฟๆŒไธ€่‡ดใ€‚ + +### 4.4 ๆŠŠ CI ไปŽโ€œๅ…จ้‡ๅคง้”คโ€ๆ‹†ๆˆโ€œๅˆ†ๅฑ‚้—จ็ฆโ€ + +ๅฝ“ๅ‰ CI ๆ›ดๅๅ‘็ปŸไธ€ๅ…จ้‡ๆ‰ง่กŒใ€‚ +ๅปบ่ฎฎๆ‹†ๆˆไธๅŒๅฑ‚็บง๏ผš + +- PR ้ป˜่ฎค๏ผšๅ‰็ซฏๅ•ๆต‹ + service ๆต‹่ฏ• + Rust unit + ๅ…ณ้”ฎๆ•ฐๆฎๅบ“ smoke +- ๅคœ้—ดๆˆ–ๆ‰‹ๅŠจ่งฆๅ‘๏ผšๅ…จ้‡ๆ•ฐๆฎๅบ“็Ÿฉ้˜ต +- ไป…ๆ•ฐๆฎๅบ“็›ฎๅฝ•ๅ˜ๆ›ดๆ—ถ๏ผšๅ‡็บงๅˆฐๅฏนๅบ”ๆ•ฐๆฎๅบ“ไธ“้กนๅ›žๅฝ’ + +ๆ”ถ็›Š๏ผš + +- ๆ้ซ˜ๅ้ฆˆ้€Ÿๅบฆ +- ้™ไฝŽๆฏๆฌก้ƒฝ่ท‘ๅ…จ้‡็š„ๆˆๆœฌ +- ๆ›ด็ฌฆๅˆ Harness ็š„่‡ชๅŠจๅŒ–ๆ‰ง่กŒ่Š‚ๅฅ + +## 5. P1 ๆ”น้€ ้กน + +่ฟ™ไบ›ๆ”น้€ ไผš่ฎฉ DbPaw ไปŽโ€œ่ƒฝ็”จ Harnessโ€่ฟ›ๅ…ฅโ€œ่ถŠ็”จ่ถŠๅฟซโ€็š„็Šถๆ€ใ€‚ + +### 5.1 ๆŠฝๅ‡บ 3 ไธชๆœ€ๅ€ผๅพ—ๅค็”จ็š„ไธ“้กนๆต็จ‹ + +#### ๆ•ฐๆฎๅบ“้ฉฑๅŠจไฟฎๅคๆต + +- ๅฎšไฝ็›ฎๆ ‡ driver +- ๅฏนๆฏ”ๅŒ็ฑป driver ่กŒไธบ +- ไฟฎๆ”นๅฎž็Žฐ +- ่ท‘็›ฎๆ ‡ๆ•ฐๆฎๅบ“ๅ›žๅฝ’ +- ่พ“ๅ‡บๅทฎๅผ‚ไธŽ็ป“ๆžœๆ€ป็ป“ + +#### ่ทจๅฑ‚ๅŠŸ่ƒฝๆต + +- ๆ‰พๅ‰็ซฏๅ…ฅๅฃ +- ๆ‰พ service ่ฐƒ็”จ +- ๆ‰พ Tauri command +- ๆ‰พ Rust ๅฎž็Žฐ +- ไฟฎๆ”นๅŽ่ท‘ๅ…จ้“พ่ทฏ้ชŒ่ฏ + +#### ๆ•ฐๆฎๅบ“ๆต‹่ฏ•้—จ็ฆๆต + +- ๆ นๆฎๆ•ฐๆฎๅบ“็ฑปๅž‹้€‰ๆ‹ฉ `IT_DB` +- ๆ‰ง่กŒๅฏนๅบ”ๆต‹่ฏ•ๅ…ฅๅฃ +- ่พ“ๅ‡บ้€š่ฟ‡ / ๅคฑ่ดฅๆ‘˜่ฆ + +### 5.2 ๅปบ็ซ‹โ€œ็›ฎๅฝ• โ†’ ๅทฅไฝœๆตโ€ๆ˜ ๅฐ„ + +ๅปบ่ฎฎๅปบ็ซ‹ๅฆ‚ไธ‹ๆ˜ ๅฐ„๏ผš + +- `src/components`ใ€`src/lib`ใ€`src/services` โ†’ ๅ‰็ซฏไธšๅŠกๆต +- `src-tauri/src/commands` โ†’ ๅ‘ฝไปคๆกฅๆŽฅๆต +- `src-tauri/src/db/drivers` โ†’ ้ฉฑๅŠจๅ…ผๅฎนๆต +- `src-tauri/tests` โ†’ ๆต‹่ฏ•่กฅ้ฝๆต +- `website/` โ†’ ๅฎ˜็ฝ‘็‹ฌ็ซ‹ๆต + +ๆ”ถ็›Š๏ผš + +- Harness ๅœจ่ฟ›ๅ…ฅไป“ๅบ“ๅŽ๏ผŒๆ›ดๅฎนๆ˜“่‡ชๅŠจ้€‰ๅฏนๆ‰ง่กŒ่ทฏๅพ„ + +### 5.3 ๆŠŠๆ•ฐๆฎๅบ“็Ÿฉ้˜ต่ฟ›ไธ€ๆญฅ็ป“ๆž„ๅŒ– + +ๅปบ่ฎฎๅœจๅทฒๆœ‰ๆต‹่ฏ•็Ÿฉ้˜ตๅŸบ็ก€ไธŠ๏ผŒๅ†่กฅไธ€ๅฑ‚ๅˆ†็ฑป๏ผš + +- ๅฏๅ†™ๅบ“๏ผšMySQL / MariaDB / Postgres / MSSQL / SQLite / DuckDB +- ๅช่ฏปๅบ“๏ผšClickHouse +- MySQL ๅ…ผๅฎนๆ—๏ผšMySQL / MariaDB / TiDB + +ๆ”ถ็›Š๏ผš + +- Harness ๅœจๅšๅ…ผๅฎนๆ€ง้—ฎ้ข˜ๆŽ’ๆŸฅๆ—ถ๏ผŒ่ƒฝๆ›ดๅฟซ็Ÿฅ้“ๅ“ชไบ›ๅบ“ๅบ”่ฏฅๆจชๅ‘ๆฏ”ๅฏน +- ๆต‹่ฏ•ไธ้€‚็”จๅœบๆ™ฏไนŸๆ›ดๅฎนๆ˜“ๆๅ‰ๅˆคๆ–ญ + +### 5.4 ๆŠŠโ€œ้”™่ฏฏๅค„็†็ปŸไธ€ๅŒ–โ€ๅˆ—ไธบๆฒป็†ไธ“้กน + +่ฟ™ๆ˜ฏ DbPaw ๅพˆ้€‚ๅˆ้•ฟๆœŸ Harness ๅŒ–ๆฒป็†็š„ๆ–นๅ‘ใ€‚ + +็›ฎๆ ‡ๅŒ…ๆ‹ฌ๏ผš + +- ็ปŸไธ€้”™่ฏฏๅ‰็ผ€ +- ็ปŸไธ€็”จๆˆทๅฏ่ง้”™่ฏฏ่ฏญไน‰ +- ็ปŸไธ€ๅ‘ฝไปคๅฑ‚้”™่ฏฏ่ฝฌๆขๆ–นๅผ + +ๆ”ถ็›Š๏ผš + +- ๅŽ็ปญๆŽ’้šœไธŽๅ›žๅฝ’ๆˆๆœฌไผšๆ˜Žๆ˜พไธ‹้™ +- AI ๅœจ่ทจๆ•ฐๆฎๅบ“ไฟฎๅค้—ฎ้ข˜ๆ—ถไธ้œ€่ฆๅๅค็Œœๆต‹้”™่ฏฏๆจกๅผ + +## 6. P2 ๆ”น้€ ้กน + +่ฟ™ไบ›ๆ›ดๅๅ‘ๆŠŠๆ•ˆ็އไผ˜ๅŠฟๅ˜ๆˆ้•ฟๆœŸ่ต„ไบงใ€‚ + +### 6.1 ๆŠŠ้ซ˜้ข‘้‡ๅคๅŠจไฝœๆฒ‰ๆท€ๆˆ Skill ๅ€™้€‰ + +ไผ˜ๅ…ˆๅ€™้€‰ๅŒ…ๆ‹ฌ๏ผš + +- ๆ–ฐๅขžๆ•ฐๆฎๅบ“้ฉฑๅŠจๆต‹่ฏ•้ชจๆžถ +- ๆ•ฐๆฎๅบ“้—จ็ฆๅ›žๅฝ’ +- ่ทจ้ฉฑๅŠจไธ€่‡ดๆ€งๅทกๆฃ€ +- ๅ‘ฝไปคๅฑ‚ๆต‹่ฏ•่กฅ้ฝ + +ๅŽŸๅˆ™ๆ˜ฏ๏ผš + +- ๅ…ˆ็”จ Harness ่ท‘้€šๆต็จ‹ +- ๅ†ๆŠŠ้ซ˜้ข‘ใ€็จณๅฎšใ€้‡ๅค็š„ๆต็จ‹ๆŠฝๆˆ Skill + +### 6.2 ็ป™ๅ‰็ซฏ่กฅๆœ€ๅฐ UI ่‡ชๅŠจๅŒ–ๅ…ฅๅฃ + +ๅฝ“ๅ‰ๅ‰็ซฏ็ป„ไปถๅฑ‚่‡ชๅŠจๅŒ–ไปๅๅผฑใ€‚ +ๅปบ่ฎฎไธ่ฆไธ€ๅผ€ๅง‹้“บๅคชๅคง๏ผŒ่€Œๆ˜ฏๅ…ˆ่ฆ†็›–ๆœ€ๅ…ณ้”ฎ็š„ไบคไบ’ๅŒบๅŸŸ๏ผš + +- DataGrid +- Editor +- Sidebar + +ๆ”ถ็›Š๏ผš + +- Harness ๅœจๅš UI ็บงๆ”นๅŠจๆ—ถ๏ผŒไธไผšๅชไพ่ต–้€ป่พ‘ๅ•ๆต‹ + +### 6.3 ๅˆ†็ฆปๅฎ˜็ฝ‘ไธŽไธปๅบ”็”จๅทฅไฝœๆต + +`website/` ๅทฒ็ปๆ˜ฏ็‹ฌ็ซ‹ๅญ็ณป็ปŸใ€‚ +ๅปบ่ฎฎๅฐ†ๅ…ถ้ชŒ่ฏไธŽไธปๅบ”็”จๆ•ฐๆฎๅบ“ๅ›žๅฝ’ๆ˜Ž็กฎๅˆ†็ฆปใ€‚ + +ๆ”ถ็›Š๏ผš + +- ๆ”นๅฎ˜็ฝ‘ๆ—ถไธๅฟ…่งฆๅ‘ไธปๅบ”็”จ้‡ๅž‹้ชŒ่ฏ +- ไธปๅบ”็”จๆ”นๅŠจไนŸไธไผš่ขซๅฎ˜็ฝ‘ๆž„ๅปบ้€ป่พ‘ๅนฒๆ‰ฐ + +## 7. ๆœ€ๅ€ผๅพ—ๅ…ˆ Harness ๅŒ–็š„ 6 ๆกๅทฅไฝœๆต + +### 7.1 ๆ•ฐๆฎๅบ“้ฉฑๅŠจไฟฎๅค้—ญ็Žฏ + +- ๅ…ฅๅฃ็›ฎๅฝ•๏ผš`src-tauri/src/db/drivers` +- ๅ…ธๅž‹ไปปๅŠก๏ผš่ฟžๆŽฅๅคฑ่ดฅใ€ๅ…ƒๆ•ฐๆฎๅผ‚ๅธธใ€SQL ๆ‰ง่กŒๅ…ผๅฎน้—ฎ้ข˜ +- ้ป˜่ฎค้ชŒ่ฏ๏ผšRust ๅ•ๆต‹ + ๅ•ๅบ“้›†ๆˆๆต‹่ฏ• + +### 7.2 ่ทจ้ฉฑๅŠจไธ€่‡ดๆ€งๆ”ถๆ•› + +- ๅ…ฅๅฃ๏ผšๅคšไธช driver ๆจชๅ‘ๆฏ”ๅฏน +- ๅ…ธๅž‹ไปปๅŠก๏ผš`test_connection`ใ€`list_tables`ใ€้”™่ฏฏๅค„็†็ปŸไธ€ +- ้ป˜่ฎค้ชŒ่ฏ๏ผš็›ฎๆ ‡ๅบ“ๅ›žๅฝ’ + ๅฏน็…งๅบ“ๅ†’็ƒŸ + +### 7.3 ๅ‘ฝไปคๅฑ‚ๆต‹่ฏ•่กฅ้ฝ + +- ๅ…ฅๅฃ็›ฎๅฝ•๏ผš`src-tauri/src/commands`ใ€`src-tauri/tests/*_command_integration.rs` +- ๅ…ธๅž‹ไปปๅŠก๏ผš่กฅๆˆๅŠŸ่ทฏๅพ„ใ€ๅคฑ่ดฅ่ทฏๅพ„ไธŽ้”™่ฏฏๆ–ญ่จ€ +- ้ป˜่ฎค้ชŒ่ฏ๏ผšRust unit + ๅ•ๅบ“ command integration + +### 7.4 ่ทจๅฑ‚ๅŠŸ่ƒฝๅผ€ๅ‘ + +- ๅ…ฅๅฃ็›ฎๅฝ•๏ผš`src/` + `src-tauri/src/commands` + `src-tauri/src/db` +- ๅ…ธๅž‹ไปปๅŠก๏ผš่ฟžๆŽฅๅญ—ๆฎตๆ‰ฉๅฑ•ใ€ไฟๅญ˜ๆŸฅ่ฏขใ€ๅฏผๅ…ฅๅฏผๅ‡บใ€็Šถๆ€ๅŒๆญฅ +- ้ป˜่ฎค้ชŒ่ฏ๏ผšๅ‰็ซฏๆต‹่ฏ• + Rust ๆต‹่ฏ• + ๅ•ๅบ“ๅ›žๅฝ’ + +### 7.5 SQL ๆ‰ง่กŒ้“พ่ทฏๆŽ’้šœ + +- ๅ…ฅๅฃ๏ผšEditor โ†’ service โ†’ command โ†’ driver +- ๅ…ธๅž‹ไปปๅŠก๏ผšๆ‰ง่กŒ็ป“ๆžœไธๅฏนใ€ๅฝฑๅ“่กŒๆ•ฐๅผ‚ๅธธใ€ๅ›žๆปš้—ฎ้ข˜ +- ้ป˜่ฎค้ชŒ่ฏ๏ผš้’ˆๅฏนๅ…ทไฝ“้“พ่ทฏๆ‰ง่กŒๆœ€ๅฐๅ›žๅฝ’้›† + +### 7.6 ๆต‹่ฏ•่ฆ†็›–่กฅ้ฝ + +- ๅ…ฅๅฃ็›ฎๅฝ•๏ผš`src-tauri/tests`ใ€`scripts/test-integration.sh` +- ๅ…ธๅž‹ไปปๅŠก๏ผš่กฅๆ–ฐๅขžๆ•ฐๆฎๅบ“ๆˆ–่กฅๅ…ณ้”ฎ็ผบๅฃๅœบๆ™ฏ +- ้ป˜่ฎค่ฆๆฑ‚๏ผšๆ–ฐๅขžๆต‹่ฏ•ๅฟ…้กป็บณๅ…ฅ็ปŸไธ€่„šๆœฌๅ…ฅๅฃ + +## 8. ๆœ€ๅ€ผๅพ—ไผ˜ๅ…ˆๆฒ‰ๆท€ๆˆ Skill ็š„ 4 ไธชๆ–นๅ‘ + +### 8.1 ๆ•ฐๆฎๅบ“ๆต‹่ฏ•้—จ็ฆ Skill + +- ๆ นๆฎ็›ฎๆ ‡ๅบ“ๆ‰ง่กŒ้—จ็ฆ +- ่พ“ๅ‡บ pass / fail ๆ‘˜่ฆ +- ๆ˜ฏๅฝ“ๅ‰ๆœ€็›ดๆŽฅๅฏไปฅ่Žทๅพ—ๆ”ถ็›Š็š„ๆ–นๅ‘ + +### 8.2 ๆ–ฐๅขžๆ•ฐๆฎๅบ“ๆต‹่ฏ•้ชจๆžถ Skill + +- ้€‚็”จไบŽๆ–ฐๅขžๆ•ฐๆฎๅบ“ๆ”ฏๆŒๆ—ถ็š„ไธ€้”ฎ้ชจๆžถ็”Ÿๆˆ +- ๅŒ…ๆ‹ฌ contextใ€integrationใ€command integrationใ€stateful integration ไธŽ่„šๆœฌๆŽฅๅ…ฅ + +### 8.3 ่ทจ้ฉฑๅŠจไธ€่‡ดๆ€งๅทกๆฃ€ Skill + +- ่‡ชๅŠจๆฏ”ๅฏน driver ๆ–นๆณ•ๅฎž็Žฐใ€้”™่ฏฏๆจกๅผๅ’Œๆต‹่ฏ•่ฆ†็›–ๆƒ…ๅ†ต +- ้€‚ๅˆ้•ฟๆœŸๆฒป็† + +### 8.4 ๅ‘ฝไปคๅฑ‚ๅ›žๅฝ’่กฅ้ฝ Skill + +- ้€‚ๅˆๆˆๅŠŸ / ๅคฑ่ดฅ่ทฏๅพ„่กฅ้ฝใ€้”™่ฏฏๆ–ญ่จ€ๆ ‡ๅ‡†ๅŒ– +- ้‡ๅคๆ€ง้ซ˜๏ผŒ้€‚ๅˆๆจกๆฟๅŒ– + +## 9. ไผ˜ๅ…ˆ็บงๆธ…ๅ• + +### ็ฌฌไธ€ไผ˜ๅ…ˆ็บง + +- ่กฅ `typecheck` / `lint` / ็ปŸไธ€้ชŒ่ฏๅ…ฅๅฃ +- ไฟฎๆญฃๆ–‡ๆกฃๆผ‚็งป +- ๅปบ็ซ‹็›ฎๅฝ•ๅˆฐ้ชŒ่ฏ่ง„ๅˆ™ๆ˜ ๅฐ„ + +### ็ฌฌไบŒไผ˜ๅ…ˆ็บง + +- ๆŠŠ CI ไปŽๅ…จ้‡ๆ‰ง่กŒ่ฐƒๆ•ดไธบๅˆ†ๅฑ‚้—จ็ฆ +- ๅ›บๅŒ–ๆ•ฐๆฎๅบ“้ฉฑๅŠจไฟฎๅคๆตใ€่ทจๅฑ‚ๅŠŸ่ƒฝๆตใ€ๆต‹่ฏ•้—จ็ฆๆต + +### ็ฌฌไธ‰ไผ˜ๅ…ˆ็บง + +- ๆŠŠๆ•ฐๆฎๅบ“้—จ็ฆใ€ๆต‹่ฏ•้ชจๆžถใ€่ทจ้ฉฑๅŠจๅทกๆฃ€ๆฒ‰ๆท€ๆˆ Skill ๅ€™้€‰ +- ็ป™ UI ๆ ธๅฟƒไบคไบ’่กฅๆœ€ๅฐ่‡ชๅŠจๅŒ– + +## 10. ๆœ€ๆ˜Žๆ˜พ็š„ 4 ไธชๆ้€Ÿ็‚น + +- ็ผบ็‹ฌ็ซ‹่ดจ้‡้—จ๏ผŒๅฏผ่‡ดๆ”นๅฎŒๅŽ้ชŒ่ฏ็ญ–็•ฅไธๅคŸ็จณๅฎš +- ๆ–‡ๆกฃไธŽไปฃ็ ๆผ‚็งป๏ผŒๅฏผ่‡ด Harness ๅฎนๆ˜“ๅŸบไบŽๆ—งไฟกๆฏๆ‰ง่กŒ +- CI ๆฒกๆœ‰ๆŒ‰ไปปๅŠกๅˆ†ๅฑ‚๏ผŒๅ้ฆˆ้“พๆกๅ้‡ +- ้‡ๅคๆต็จ‹่ฟ˜ๆฒกๆŠฝ่ฑก๏ผŒๆ•ฐๆฎๅบ“ๆ”นๅŠจๆฏๆฌก้ƒฝ่ฆ้‡ๆ–ฐ็ป„็ป‡ๆ‰ง่กŒ่ทฏๅพ„ + +## 11. ๆœ€็ฎ€่ฝๅœฐ่ทฏ็บฟ + +ๅปบ่ฎฎๆŒ‰ไปฅไธ‹้กบๅบๆŽจ่ฟ›๏ผš + +### ็ฌฌ 1 ๆญฅ๏ผšๅ…ˆๆŠŠ้ชŒ่ฏๅ…ฅๅฃๆ ‡ๅ‡†ๅŒ– + +่ฎฉๆฏ็ฑปๆ”นๅŠจ้ƒฝๆœ‰็จณๅฎšๆฃ€ๆŸฅๅ‡บๅฃใ€‚ + +### ็ฌฌ 2 ๆญฅ๏ผšๅ†ๆŠŠ 3 ๆกๆ ธๅฟƒๅทฅไฝœๆตๅ›บๅฎšไธ‹ๆฅ + +- ้ฉฑๅŠจไฟฎๅคๆต +- ่ทจๅฑ‚ๅŠŸ่ƒฝๆต +- ๆ•ฐๆฎๅบ“้—จ็ฆๆต + +### ็ฌฌ 3 ๆญฅ๏ผšๆœ€ๅŽๅ†ๆŠฝ Skill + +ๅ…ˆ่ท‘้€š๏ผŒๅ†ๆฒ‰ๆท€ใ€‚ +ไธ่ฆๅ่ฟ‡ๆฅๅ…ˆๆŠฝ่ฑกใ€‚ + +## 12. ๆ€ป็ป“ + +DbPaw ๅฝ“ๅ‰ๅทฒ็ปไธๆ˜ฏโ€œ่ƒฝไธ่ƒฝ็”จ Harnessโ€็š„้—ฎ้ข˜๏ผŒ่€Œๆ˜ฏโ€œๆœ‰ๆฒกๆœ‰ๆŠŠๅทฒๆœ‰ๆต‹่ฏ•ใ€็›ฎๅฝ•ๅˆ†ๅฑ‚ๅ’Œๆ•ฐๆฎๅบ“็Ÿฉ้˜ต็ป„็ป‡ๆˆไธ€ๅฅ—ๅฏๆŒ็ปญๅค็”จ็š„ๆ‰ง่กŒไฝ“็ณปโ€ใ€‚ + +็œŸๆญฃ็š„ Harness ๅŒ–๏ผŒไธๆ˜ฏๅคšๅ†™ Prompt๏ผŒ่€Œๆ˜ฏๆŠŠไป“ๅบ“้‡Œ็š„้ซ˜้ข‘ๅผ€ๅ‘ๅŠจไฝœๅ˜ๆˆ๏ผš + +- ๆ ‡ๅ‡†ๅ…ฅๅฃ +- ๅ›บๅฎš้ชŒ่ฏ +- ๅฏๅค็”จๆต็จ‹ +- ๆ–‡ๆกฃๅŒๆญฅ + +ๅฆ‚ๆžœๅŽ็ปญ็ปง็ปญๆŽจ่ฟ›๏ผŒไธ‹ไธ€ๆญฅๆœ€ๅ€ผๅพ—่กฅ็š„ๆ˜ฏ๏ผš + +- DbPaw Harness ๅŒ–ๅฎžๆ–ฝ่ทฏ็บฟๅ›พ +- ็ฌฌไธ€ๆ‰นๆœ€ๅ€ผๅพ—่ฝๅœฐ็š„ Skill ่ฎพ่ฎก็จฟ diff --git a/docs/zh/Development/DEVELOPMENT.md b/docs/zh/Development/DEVELOPMENT.md index 196f1d82..03302d96 100644 --- a/docs/zh/Development/DEVELOPMENT.md +++ b/docs/zh/Development/DEVELOPMENT.md @@ -42,9 +42,24 @@ bun tauri build bun run test:all ``` +ๆœ€ๅฐ็ปŸไธ€่ดจ้‡้—จ๏ผš + +```bash +bun run test:smoke +``` + +CI ็บงๅฎŒๆ•ด้—จ็ฆ๏ผš + +```bash +bun run test:ci +``` + ๆˆ–ๆŒ‰้œ€ๆ‰ง่กŒ๏ผš ```bash +bun run typecheck +bun run lint +bun run rust:check bun run test:unit bun run test:service bun run test:rust:unit @@ -87,9 +102,10 @@ bun run test:integration ### ๆŽจ่ๅทฅไฝœๆต -- ๆ—ฅๅธธๅผ€ๅ‘๏ผšไผ˜ๅ…ˆๆ‰ง่กŒ `test:unit` + `test:service`ใ€‚ +- ๆ—ฅๅธธๅผ€ๅ‘๏ผšไผ˜ๅ…ˆๆ‰ง่กŒ `bun run test:smoke`ใ€‚ - ๆไบคๅ‰๏ผšๆŒ‰้œ€ๆ‰ง่กŒ `test:integration` ๅšๆ•ฐๆฎๅบ“ๅ›žๅฝ’ใ€‚ -- PR๏ผšCI ไผšๅ›บๅฎšๆ‰ง่กŒ้›†ๆˆๆต‹่ฏ•ไฝœไธบ่ดจ้‡ๅ…œๅบ•ใ€‚ +- ๆไบคๅ‰่‹ฅ้œ€่ฆๅฎŒๆ•ด้—จ็ฆ๏ผšๆ‰ง่กŒ `bun run test:ci`ใ€‚ +- PR๏ผšCI ไผšๅ›บๅฎšๆ‰ง่กŒ smoke gate ไธŽๆ•ฐๆฎๅบ“้›†ๆˆๆต‹่ฏ•ไฝœไธบ่ดจ้‡ๅ…œๅบ•ใ€‚ ### ๅŠŸ่ƒฝๅผ€ๅ‘ๅŽๆ€Žไนˆ่ท‘ๆต‹่ฏ•๏ผˆๅฎž่ทต็‰ˆ๏ผ‰ @@ -97,10 +113,9 @@ bun run test:integration - ๅ…ˆ่ท‘๏ผš ```bash - bun run test:unit - bun run test:service + bun run test:smoke ``` -- ้€‚็”จ๏ผšๅ‰็ซฏ้€ป่พ‘ใ€ไธšๅŠก้€ป่พ‘ใ€ๅฐ่Œƒๅ›ดๆ”นๅŠจ็š„ๅฟซ้€Ÿ้ชŒ่ฏใ€‚ +- ้€‚็”จ๏ผšๅ‰็ซฏ้€ป่พ‘ใ€ไธšๅŠก้€ป่พ‘ใ€ๅฐ่Œƒๅ›ดๆ”นๅŠจ๏ผŒไปฅๅŠๆ”นๅฎŒๅŽๅฟซ้€Ÿๆ‹ฟๅˆฐ็ปŸไธ€่ดจ้‡ๅ้ฆˆใ€‚ 2. ๆ”นๅŠจๆถ‰ๅŠๆ•ฐๆฎๅบ“่กŒไธบๆ—ถ๏ผˆไธญ้ข‘๏ผ‰ @@ -154,6 +169,14 @@ bun run test:integration bun run format ``` +่ดจ้‡ๆฃ€ๆŸฅๅ…ฅๅฃ๏ผš + +```bash +bun run typecheck +bun run lint +bun run rust:check +``` + ## ๐ŸŒ ๅฎ˜็ฝ‘ - ๅฎ˜ๆ–นๅฎ˜็ฝ‘ไฝไบŽ `website/` ็›ฎๅฝ•๏ผŒๅŸบไบŽ [Astro](https://astro.build/) ๆž„ๅปบใ€‚ diff --git a/package.json b/package.json index 955b1b86..7fdc9880 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,18 @@ "scripts": { "dev": "vite", "dev:mock": "VITE_USE_MOCK=true vite", - "build": "tsc && vite build", + "build": "bun run typecheck && vite build", + "typecheck": "tsc -b", + "lint:web": "prettier --check \"package.json\" \"package-lock.json\" \"tsconfig*.json\" \"vite.config.ts\" \"src-tauri/tauri.conf.json\" \"src-tauri/capabilities/**/*.json\" \".github/workflows/*.{yml,yaml}\"", + "lint:rust": "bun run rust:check", + "lint": "bun run lint:web", + "rust:check": "cargo check --manifest-path src-tauri/Cargo.toml --all-targets", "test:unit": "bash ./scripts/test-unit.sh", "test:service": "bash ./scripts/test-service.sh", "test:rust:unit": "cargo test --manifest-path src-tauri/Cargo.toml --lib", "test:integration": "bash ./scripts/test-integration.sh", + "test:smoke": "bun run typecheck && bun run lint && bun run rust:check && bun run test:unit && bun run test:service && bun run test:rust:unit", + "test:ci": "bun run test:smoke && IT_DB=all bun run test:integration", "test:all": "bun run test:unit && bun run test:service && bun run test:rust:unit && bun run test:integration", "preview": "vite preview", "tauri": "tauri", @@ -102,6 +109,7 @@ "devDependencies": { "@tailwindcss/vite": "4.1.12", "@tauri-apps/cli": "^2", + "@types/node": "^24.5.2", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@vitejs/plugin-react": "^4.6.0", @@ -111,4 +119,4 @@ "typescript": "~5.8.3", "vite": "^7.0.4" } -} \ No newline at end of file +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9837ee59..a935348d 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -52,4 +52,4 @@ "signingIdentity": "-" } } -} \ No newline at end of file +} diff --git a/tsconfig.node.json b/tsconfig.node.json index 42872c59..b940375d 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -4,7 +4,8 @@ "skipLibCheck": true, "module": "ESNext", "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "types": ["node"] }, "include": ["vite.config.ts"] } diff --git a/tsconfig.node.tsbuildinfo b/tsconfig.node.tsbuildinfo new file mode 100644 index 00000000..dc0e6f2e --- /dev/null +++ b/tsconfig.node.tsbuildinfo @@ -0,0 +1 @@ +{"fileNames":["./node_modules/typescript/lib/lib.d.ts","./node_modules/typescript/lib/lib.es5.d.ts","./node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/typescript/lib/lib.dom.d.ts","./node_modules/typescript/lib/lib.webworker.importscripts.d.ts","./node_modules/typescript/lib/lib.scripthost.d.ts","./node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/typescript/lib/lib.esnext.disposable.d.ts","./node_modules/typescript/lib/lib.esnext.float16.d.ts","./node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/@types/node/globals.typedarray.d.ts","./node_modules/@types/node/buffer.buffer.d.ts","./node_modules/@types/node/globals.d.ts","./node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/@types/node/web-globals/crypto.d.ts","./node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/@types/node/web-globals/events.d.ts","./node_modules/undici-types/utility.d.ts","./node_modules/undici-types/header.d.ts","./node_modules/undici-types/readable.d.ts","./node_modules/undici-types/fetch.d.ts","./node_modules/undici-types/formdata.d.ts","./node_modules/undici-types/connector.d.ts","./node_modules/undici-types/client-stats.d.ts","./node_modules/undici-types/client.d.ts","./node_modules/undici-types/errors.d.ts","./node_modules/undici-types/dispatcher.d.ts","./node_modules/undici-types/global-dispatcher.d.ts","./node_modules/undici-types/global-origin.d.ts","./node_modules/undici-types/pool-stats.d.ts","./node_modules/undici-types/pool.d.ts","./node_modules/undici-types/handlers.d.ts","./node_modules/undici-types/balanced-pool.d.ts","./node_modules/undici-types/h2c-client.d.ts","./node_modules/undici-types/agent.d.ts","./node_modules/undici-types/mock-interceptor.d.ts","./node_modules/undici-types/mock-call-history.d.ts","./node_modules/undici-types/mock-agent.d.ts","./node_modules/undici-types/mock-client.d.ts","./node_modules/undici-types/mock-pool.d.ts","./node_modules/undici-types/snapshot-agent.d.ts","./node_modules/undici-types/mock-errors.d.ts","./node_modules/undici-types/proxy-agent.d.ts","./node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/undici-types/retry-handler.d.ts","./node_modules/undici-types/retry-agent.d.ts","./node_modules/undici-types/api.d.ts","./node_modules/undici-types/cache-interceptor.d.ts","./node_modules/undici-types/interceptors.d.ts","./node_modules/undici-types/util.d.ts","./node_modules/undici-types/cookies.d.ts","./node_modules/undici-types/patch.d.ts","./node_modules/undici-types/websocket.d.ts","./node_modules/undici-types/eventsource.d.ts","./node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/undici-types/content-type.d.ts","./node_modules/undici-types/cache.d.ts","./node_modules/undici-types/index.d.ts","./node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/@types/node/web-globals/storage.d.ts","./node_modules/@types/node/web-globals/streams.d.ts","./node_modules/@types/node/assert.d.ts","./node_modules/@types/node/assert/strict.d.ts","./node_modules/@types/node/async_hooks.d.ts","./node_modules/@types/node/buffer.d.ts","./node_modules/@types/node/child_process.d.ts","./node_modules/@types/node/cluster.d.ts","./node_modules/@types/node/console.d.ts","./node_modules/@types/node/constants.d.ts","./node_modules/@types/node/crypto.d.ts","./node_modules/@types/node/dgram.d.ts","./node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/@types/node/dns.d.ts","./node_modules/@types/node/dns/promises.d.ts","./node_modules/@types/node/domain.d.ts","./node_modules/@types/node/events.d.ts","./node_modules/@types/node/fs.d.ts","./node_modules/@types/node/fs/promises.d.ts","./node_modules/@types/node/http.d.ts","./node_modules/@types/node/http2.d.ts","./node_modules/@types/node/https.d.ts","./node_modules/@types/node/inspector.d.ts","./node_modules/@types/node/inspector.generated.d.ts","./node_modules/@types/node/module.d.ts","./node_modules/@types/node/net.d.ts","./node_modules/@types/node/os.d.ts","./node_modules/@types/node/path.d.ts","./node_modules/@types/node/perf_hooks.d.ts","./node_modules/@types/node/process.d.ts","./node_modules/@types/node/punycode.d.ts","./node_modules/@types/node/querystring.d.ts","./node_modules/@types/node/readline.d.ts","./node_modules/@types/node/readline/promises.d.ts","./node_modules/@types/node/repl.d.ts","./node_modules/@types/node/sea.d.ts","./node_modules/@types/node/sqlite.d.ts","./node_modules/@types/node/stream.d.ts","./node_modules/@types/node/stream/promises.d.ts","./node_modules/@types/node/stream/consumers.d.ts","./node_modules/@types/node/stream/web.d.ts","./node_modules/@types/node/string_decoder.d.ts","./node_modules/@types/node/test.d.ts","./node_modules/@types/node/timers.d.ts","./node_modules/@types/node/timers/promises.d.ts","./node_modules/@types/node/tls.d.ts","./node_modules/@types/node/trace_events.d.ts","./node_modules/@types/node/tty.d.ts","./node_modules/@types/node/url.d.ts","./node_modules/@types/node/util.d.ts","./node_modules/@types/node/v8.d.ts","./node_modules/@types/node/vm.d.ts","./node_modules/@types/node/wasi.d.ts","./node_modules/@types/node/worker_threads.d.ts","./node_modules/@types/node/zlib.d.ts","./node_modules/@types/node/index.d.ts","./node_modules/vite/types/hmrpayload.d.ts","./node_modules/vite/dist/node/chunks/modulerunnertransport.d.ts","./node_modules/vite/types/customevent.d.ts","./node_modules/@types/estree/index.d.ts","./node_modules/rollup/dist/rollup.d.ts","./node_modules/rollup/dist/parseast.d.ts","./node_modules/vite/types/hot.d.ts","./node_modules/vite/dist/node/module-runner.d.ts","./node_modules/esbuild/lib/main.d.ts","./node_modules/vite/types/internal/terseroptions.d.ts","./node_modules/source-map-js/source-map.d.ts","./node_modules/postcss/lib/previous-map.d.ts","./node_modules/postcss/lib/input.d.ts","./node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/postcss/lib/declaration.d.ts","./node_modules/postcss/lib/root.d.ts","./node_modules/postcss/lib/warning.d.ts","./node_modules/postcss/lib/lazy-result.d.ts","./node_modules/postcss/lib/no-work-result.d.ts","./node_modules/postcss/lib/processor.d.ts","./node_modules/postcss/lib/result.d.ts","./node_modules/postcss/lib/document.d.ts","./node_modules/postcss/lib/rule.d.ts","./node_modules/postcss/lib/node.d.ts","./node_modules/postcss/lib/comment.d.ts","./node_modules/postcss/lib/container.d.ts","./node_modules/postcss/lib/at-rule.d.ts","./node_modules/postcss/lib/list.d.ts","./node_modules/postcss/lib/postcss.d.ts","./node_modules/postcss/lib/postcss.d.mts","./node_modules/vite/types/internal/csspreprocessoroptions.d.ts","./node_modules/lightningcss/node/ast.d.ts","./node_modules/lightningcss/node/targets.d.ts","./node_modules/lightningcss/node/index.d.ts","./node_modules/vite/types/internal/lightningcssoptions.d.ts","./node_modules/vite/types/importglob.d.ts","./node_modules/vite/types/metadata.d.ts","./node_modules/vite/dist/node/index.d.ts","./node_modules/@babel/types/lib/index.d.ts","./node_modules/@types/babel__generator/index.d.ts","./node_modules/@babel/parser/typings/babel-parser.d.ts","./node_modules/@types/babel__template/index.d.ts","./node_modules/@types/babel__traverse/index.d.ts","./node_modules/@types/babel__core/index.d.ts","./node_modules/@vitejs/plugin-react/dist/index.d.ts","./node_modules/@tailwindcss/vite/dist/index.d.mts","./vite.config.ts"],"fileIdsList":[[54,108,125,126,197],[54,108,125,126],[54,108,125,126,196],[54,108,125,126,197,198,199,200,201],[54,108,125,126,197,199],[54,105,106,108,125,126],[54,107,108,125,126],[108,125,126],[54,108,113,125,126,143],[54,108,109,114,119,125,126,128,140,151],[54,108,109,110,119,125,126,128],[54,108,111,125,126,152],[54,108,112,113,120,125,126,129],[54,108,113,125,126,140,148],[54,108,114,116,119,125,126,128],[54,107,108,115,125,126],[54,108,116,117,125,126],[54,108,118,119,125,126],[54,107,108,119,125,126],[54,108,119,120,121,125,126,140,151],[54,108,119,120,121,125,126,135,140,143],[54,100,108,116,119,122,125,126,128,140,151],[54,108,119,120,122,123,125,126,128,140,148,151],[54,108,122,124,125,126,140,148,151],[52,53,54,55,56,57,58,59,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157],[54,108,119,125,126],[54,108,125,126,127,151],[54,108,116,119,125,126,128,140],[54,108,125,126,129],[54,108,125,126,130],[54,107,108,125,126,131],[54,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157],[54,108,125,126,133],[54,108,125,126,134],[54,108,119,125,126,135,136],[54,108,125,126,135,137,152,154],[54,108,120,125,126],[54,108,119,125,126,140,141,143],[54,108,125,126,142,143],[54,108,125,126,140,141],[54,108,125,126,143],[54,108,125,126,144],[54,105,108,125,126,140,145,151],[54,108,119,125,126,146,147],[54,108,125,126,146,147],[54,108,113,125,126,128,140,148],[54,108,125,126,149],[54,108,125,126,128,150],[54,108,122,125,126,134,151],[54,108,113,125,126,152],[54,108,125,126,140,153],[54,108,125,126,127,154],[54,108,125,126,155],[54,108,113,125,126],[54,100,108,125,126],[54,108,125,126,156],[54,100,108,119,121,125,126,131,140,143,151,153,154,156],[54,108,125,126,140,157],[54,108,125,126,196,202],[54,108,125,126,190,191],[54,108,125,126,184],[54,108,125,126,182,184],[54,108,125,126,173,181,182,183,185,187],[54,108,125,126,171],[54,108,125,126,174,179,184,187],[54,108,125,126,170,187],[54,108,125,126,174,175,178,179,180,187],[54,108,125,126,174,175,176,178,179,187],[54,108,125,126,171,172,173,174,175,179,180,181,183,184,185,187],[54,108,125,126,187],[54,108,125,126,169,171,172,173,174,175,176,178,179,180,181,182,183,184,185,186],[54,108,125,126,169,187],[54,108,125,126,174,176,177,179,180,187],[54,108,125,126,178,187],[54,108,125,126,179,180,184,187],[54,108,125,126,172,182],[54,108,125,126,163,195,196],[54,108,125,126,162,163],[54,66,69,72,73,108,125,126,151],[54,69,108,125,126,140,151],[54,69,73,108,125,126,151],[54,108,125,126,140],[54,63,108,125,126],[54,67,108,125,126],[54,65,66,69,108,125,126,151],[54,108,125,126,128,148],[54,108,125,126,158],[54,63,108,125,126,158],[54,65,69,108,125,126,128,151],[54,60,61,62,64,68,108,119,125,126,140,151],[54,69,77,85,108,125,126],[54,61,67,108,125,126],[54,69,94,95,108,125,126],[54,61,64,69,108,125,126,143,151,158],[54,69,108,125,126],[54,65,69,108,125,126,151],[54,60,108,125,126],[54,63,64,65,67,68,69,70,71,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,95,96,97,98,99,108,125,126],[54,69,87,90,108,116,125,126],[54,69,77,78,79,108,125,126],[54,67,69,78,80,108,125,126],[54,68,108,125,126],[54,61,63,69,108,125,126],[54,69,73,78,80,108,125,126],[54,73,108,125,126],[54,67,69,72,108,125,126,151],[54,61,65,69,77,108,125,126],[54,69,87,108,125,126],[54,80,108,125,126],[54,63,69,94,108,125,126,143,156,158],[54,108,125,126,159],[54,108,119,120,122,123,124,125,126,128,140,148,151,157,158,159,160,161,163,164,166,167,168,188,189,193,194,195,196],[54,108,125,126,159,160,161,165],[54,108,125,126,161],[54,108,125,126,192],[54,108,125,126,163,196],[54,108,125,126,130,196,203,204]],"fileInfos":[{"version":"a7297ff837fcdf174a9524925966429eb8e5feecc2cc55cc06574e6b092c1eaa","impliedFormat":1},{"version":"69684132aeb9b5642cbcd9e22dff7818ff0ee1aa831728af0ecf97d3364d5546","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"092c2bfe125ce69dbb1223c85d68d4d2397d7d8411867b5cc03cec902c233763","affectsGlobalScope":true,"impliedFormat":1},{"version":"80e18897e5884b6723488d4f5652167e7bb5024f946743134ecc4aa4ee731f89","affectsGlobalScope":true,"impliedFormat":1},{"version":"cd034f499c6cdca722b60c04b5b1b78e058487a7085a8e0d6fb50809947ee573","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"936e80ad36a2ee83fc3caf008e7c4c5afe45b3cf3d5c24408f039c1d47bdc1df","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"fef8cfad2e2dc5f5b3d97a6f4f2e92848eb1b88e897bb7318cef0e2820bceaab","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"51ad4c928303041605b4d7ae32e0c1ee387d43a24cd6f1ebf4a2699e1076d4fa","affectsGlobalScope":true,"impliedFormat":1},{"version":"4245fee526a7d1754529d19227ecbf3be066ff79ebb6a380d78e41648f2f224d","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"d153a11543fd884b596587ccd97aebbeed950b26933ee000f94009f1ab142848","affectsGlobalScope":true,"impliedFormat":1},{"version":"378281aa35786c27d5811af7e6bcaa492eebd0c7013d48137c35bbc69a2b9751","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"1b2dd1cbeb0cc6ae20795958ba5950395ebb2849b7c8326853dd15530c77ab0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"387a023d363f755eb63450a66c28b14cdd7bc30a104565e2dbf0a8988bb4a56c","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"cdcf9ea426ad970f96ac930cd176d5c69c6c24eebd9fc580e1572d6c6a88f62c","impliedFormat":1},{"version":"23cd712e2ce083d68afe69224587438e5914b457b8acf87073c22494d706a3d0","impliedFormat":1},{"version":"487b694c3de27ddf4ad107d4007ad304d29effccf9800c8ae23c2093638d906a","impliedFormat":1},{"version":"3a80bc85f38526ca3b08007ee80712e7bb0601df178b23fbf0bf87036fce40ce","impliedFormat":1},{"version":"ccf4552357ce3c159ef75f0f0114e80401702228f1898bdc9402214c9499e8c0","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"68834d631c8838c715f225509cfc3927913b9cc7a4870460b5b60c8dbdb99baf","impliedFormat":1},{"version":"2931540c47ee0ff8a62860e61782eb17b155615db61e36986e54645ec67f67c2","impliedFormat":1},{"version":"ccab02f3920fc75c01174c47fcf67882a11daf16baf9e81701d0a94636e94556","impliedFormat":1},{"version":"f6faf5f74e4c4cc309a6c6a6c4da02dbb840be5d3e92905a23dcd7b2b0bd1986","impliedFormat":1},{"version":"ea6bc8de8b59f90a7a3960005fd01988f98fd0784e14bc6922dde2e93305ec7d","impliedFormat":1},{"version":"36107995674b29284a115e21a0618c4c2751b32a8766dd4cb3ba740308b16d59","impliedFormat":1},{"version":"914a0ae30d96d71915fc519ccb4efbf2b62c0ddfb3a3fc6129151076bc01dc60","impliedFormat":1},{"version":"33e981bf6376e939f99bd7f89abec757c64897d33c005036b9a10d9587d80187","impliedFormat":1},{"version":"7fd1b31fd35876b0aa650811c25ec2c97a3c6387e5473eb18004bed86cdd76b6","impliedFormat":1},{"version":"b41767d372275c154c7ea6c9d5449d9a741b8ce080f640155cc88ba1763e35b3","impliedFormat":1},{"version":"3bacf516d686d08682751a3bd2519ea3b8041a164bfb4f1d35728993e70a2426","impliedFormat":1},{"version":"7fb266686238369442bd1719bc0d7edd0199da4fb8540354e1ff7f16669b4323","impliedFormat":1},{"version":"0a60a292b89ca7218b8616f78e5bbd1c96b87e048849469cccb4355e98af959a","impliedFormat":1},{"version":"0b6e25234b4eec6ed96ab138d96eb70b135690d7dd01f3dd8a8ab291c35a683a","impliedFormat":1},{"version":"9666f2f84b985b62400d2e5ab0adae9ff44de9b2a34803c2c5bd3c8325b17dc0","impliedFormat":1},{"version":"40cd35c95e9cf22cfa5bd84e96408b6fcbca55295f4ff822390abb11afbc3dca","impliedFormat":1},{"version":"b1616b8959bf557feb16369c6124a97a0e74ed6f49d1df73bb4b9ddf68acf3f3","impliedFormat":1},{"version":"5b03a034c72146b61573aab280f295b015b9168470f2df05f6080a2122f9b4df","impliedFormat":1},{"version":"40b463c6766ca1b689bfcc46d26b5e295954f32ad43e37ee6953c0a677e4ae2b","impliedFormat":1},{"version":"249b9cab7f5d628b71308c7d9bb0a808b50b091e640ba3ed6e2d0516f4a8d91d","impliedFormat":1},{"version":"80aae6afc67faa5ac0b32b5b8bc8cc9f7fa299cff15cf09cc2e11fd28c6ae29e","impliedFormat":1},{"version":"f473cd2288991ff3221165dcf73cd5d24da30391f87e85b3dd4d0450c787a391","impliedFormat":1},{"version":"499e5b055a5aba1e1998f7311a6c441a369831c70905cc565ceac93c28083d53","impliedFormat":1},{"version":"54c3e2371e3d016469ad959697fd257e5621e16296fa67082c2575d0bf8eced0","impliedFormat":1},{"version":"beb8233b2c220cfa0feea31fbe9218d89fa02faa81ef744be8dce5acb89bb1fd","impliedFormat":1},{"version":"c183b931b68ad184bc8e8372bf663f3d33304772fb482f29fb91b3c391031f3e","impliedFormat":1},{"version":"5d0375ca7310efb77e3ef18d068d53784faf62705e0ad04569597ae0e755c401","impliedFormat":1},{"version":"59af37caec41ecf7b2e76059c9672a49e682c1a2aa6f9d7dc78878f53aa284d6","impliedFormat":1},{"version":"addf417b9eb3f938fddf8d81e96393a165e4be0d4a8b6402292f9c634b1cb00d","impliedFormat":1},{"version":"48cc3ec153b50985fb95153258a710782b25975b10dd4ac8a4f3920632d10790","impliedFormat":1},{"version":"adf27937dba6af9f08a68c5b1d3fce0ca7d4b960c57e6d6c844e7d1a8e53adae","impliedFormat":1},{"version":"e1528ca65ac90f6fa0e4a247eb656b4263c470bb22d9033e466463e13395e599","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"866078923a56d026e39243b4392e282c1c63159723996fa89243140e1388a98d","impliedFormat":1},{"version":"f724236417941ea77ec8d38c6b7021f5fb7f8521c7f8c1538e87661f2c6a0774","affectsGlobalScope":true,"impliedFormat":1},{"version":"1cf059eaf468efcc649f8cf6075d3cb98e9a35a0fe9c44419ec3d2f5428d7123","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d97fb21da858fb18b8ae72c314e9743fd52f73ebe2764e12af1db32fc03f853f","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ea15fd99b2e34cb25fe8346c955000bb70c8b423ae4377a972ef46bfb37f595","impliedFormat":1},{"version":"7cf69dd5502c41644c9e5106210b5da7144800670cbe861f66726fa209e231c4","impliedFormat":1},{"version":"72c1f5e0a28e473026074817561d1bc9647909cf253c8d56c41d1df8d95b85f7","impliedFormat":1},{"version":"f9b4137a0d285bd77dba2e6e895530112264310ae47e07bf311feae428fb8b61","affectsGlobalScope":true,"impliedFormat":1},{"version":"c06b2652ffeb89afd0f1c52c165ced77032f9cd09bc481153fbd6b5504c69494","impliedFormat":1},{"version":"51aecd2df90a3cffea1eb4696b33b2d78594ea2aa2138e6b9471ec4841c6c2ee","impliedFormat":1},{"version":"9d8f9e63e29a3396285620908e7f14d874d066caea747dc4b2c378f0599166b4","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"612422d5ba6b4a5c4537f423e9199645468ad80a689801da63ab7edb43f7b835","impliedFormat":1},{"version":"db9ada976f9e52e13f7ae8b9a320f4b67b87685938c5879187d8864b2fbe97f3","impliedFormat":1},{"version":"9f39e70a354d0fba29ac3cdf6eca00b7f9e96f64b2b2780c432e8ea27f133743","impliedFormat":1},{"version":"0dace96cc0f7bc6d0ee2044921bdf19fe42d16284dbcc8ae200800d1c9579335","impliedFormat":1},{"version":"a2e2bbde231b65c53c764c12313897ffdfb6c49183dd31823ee2405f2f7b5378","impliedFormat":1},{"version":"ad1cc0ed328f3f708771272021be61ab146b32ecf2b78f3224959ff1e2cd2a5c","impliedFormat":1},{"version":"c64e1888baaa3253ca4405b455e4bf44f76357868a1bd0a52998ade9a092ad78","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc8c6f5322961b56d9906601b20798725df60baeab45ec014fba9f795d5596fd","impliedFormat":1},{"version":"0904660ae854e6d41f6ff25356db1d654436c6305b0f0aa89d1532df0253486e","impliedFormat":1},{"version":"060d305fe4494d8cb2b99d620928d369d1ee55c1645f5e729a2aca07d0f108cb","impliedFormat":1},{"version":"230bdc111d7578276e4a3bb9d075d85c78c6b68f428c3a9935e2eaa10f4ae1f5","impliedFormat":1},{"version":"0c50296ee73dae94efc3f0da4936b1146ca6ce2217acfabb44c19c9a33fa30e5","impliedFormat":1},{"version":"bbf42f98a5819f4f06e18c8b669a994afe9a17fe520ae3454a195e6eabf7700d","impliedFormat":1},{"version":"0e5974dfff7a97181c7c376545f126b20acf2f1341db7d3fccea4977bf3ce19c","impliedFormat":1},{"version":"c7f977ea78a1b060a30554c1c4ec0e2269c6e305a349ca2ada14931ac27ecc0b","affectsGlobalScope":true,"impliedFormat":1},{"version":"145dcf25fd4967c610c53d93d7bc4dce8fbb1b6dd7935362472d4ae49363c7ba","impliedFormat":1},{"version":"ff65b8a8bd380c6d129becc35de02f7c29ad7ce03300331ca91311fb4044d1a9","impliedFormat":1},{"version":"04bf1aa481d1adfb16d93d76e44ce71c51c8ef68039d849926551199489637f6","impliedFormat":1},{"version":"2c9adcc85574b002c9a6311ff2141055769e0071856ec979d92ff989042b1f1b","affectsGlobalScope":true,"impliedFormat":1},{"version":"b8bf3fe89ec8baa335f6370b9fa36308e1bc7a72e2eb2dad1e94f31e27fa28b5","affectsGlobalScope":true,"impliedFormat":1},{"version":"a58a15da4c5ba3df60c910a043281256fa52d36a0fcdef9b9100c646282e88dd","impliedFormat":1},{"version":"b36beffbf8acdc3ebc58c8bb4b75574b31a2169869c70fc03f82895b93950a12","impliedFormat":1},{"version":"de263f0089aefbfd73c89562fb7254a7468b1f33b61839aafc3f035d60766cb4","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"8c81fd4a110490c43d7c578e8c6f69b3af01717189196899a6a44f93daa57a3a","impliedFormat":1},{"version":"5fb39858b2459864b139950a09adae4f38dad87c25bf572ce414f10e4bd7baab","impliedFormat":1},{"version":"35390d6fa94bdb432c5d0bcb6547bdd11406c2692a6b90b9e47be2105ea19bd6","impliedFormat":1},{"version":"b33b74b97952d9bf4fbd2951dcfbb5136656ddb310ce1c84518aaa77dbca9992","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"8d117798e5228c7fdff887f44851d07320739c5cc0d511afae8f250c51809a36","affectsGlobalScope":true,"impliedFormat":1},{"version":"c119835edf36415081dfd9ed15fc0cd37aaa28d232be029ad073f15f3d88c323","impliedFormat":1},{"version":"8e7c3bed5f19ade8f911677ddc83052e2283e25b0a8654cd89db9079d4b323c7","impliedFormat":1},{"version":"9705cd157ffbb91c5cab48bdd2de5a437a372e63f870f8a8472e72ff634d47c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"ae86f30d5d10e4f75ce8dcb6e1bd3a12ecec3d071a21e8f462c5c85c678efb41","impliedFormat":1},{"version":"ccf3afaeebbeee4ca9092101e99fd6abd681116b6e5ec23e381bbb1e1f32262c","impliedFormat":1},{"version":"e03460fe72b259f6d25ad029f085e4bedc3f90477da4401d8fbc1efa9793230e","impliedFormat":1},{"version":"4286a3a6619514fca656089aee160bb6f2e77f4dd53dc5a96b26a0b4fc778055","impliedFormat":1},{"version":"ab7818a9d57a9297b90e456fc68b77f84d74395a9210a3cfa9d87db33aff8b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb08062718a5470cd864c1fae0eb5b3a3adc5bcd05dcf87608d6f60b65eca3f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"3a815b7d1aebc0646b91548eab2fc19dada09ff255d04c71ced00bbd3058c8eb","impliedFormat":1},{"version":"255d948f87f24ffd57bcb2fdf95792fd418a2e1f712a98cf2cce88744d75085c","impliedFormat":1},{"version":"0d5b085f36e6dc55bc6332ecb9c733be3a534958c238fb8d8d18d4a2b6f2a15a","impliedFormat":1},{"version":"836b36913830645ac3b28fe33731aac3fdb3524ee8adbb4cdab9a5c189f41943","affectsGlobalScope":true,"impliedFormat":1},{"version":"bfd3b3c21a56104693183942e221c1896ee23bcb8f8d91ab0b941f7b32985411","impliedFormat":1},{"version":"d7e9ab1b0996639047c61c1e62f85c620e4382206b3abb430d9a21fb7bc23c77","impliedFormat":1},{"version":"a7ca8df4f2931bef2aa4118078584d84a0b16539598eaadf7dce9104dfaa381c","impliedFormat":1},{"version":"10073cdcf56982064c5337787cc59b79586131e1b28c106ede5bff362f912b70","impliedFormat":99},{"version":"72950913f4900b680f44d8cab6dd1ea0311698fc1eefb014eb9cdfc37ac4a734","impliedFormat":1},{"version":"151ff381ef9ff8da2da9b9663ebf657eac35c4c9a19183420c05728f31a6761d","impliedFormat":1},{"version":"ee70b8037ecdf0de6c04f35277f253663a536d7e38f1539d270e4e916d225a3f","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"36977c14a7f7bfc8c0426ae4343875689949fb699f3f84ecbe5b300ebf9a2c55","impliedFormat":1},{"version":"ff0a83c9a0489a627e264ffcb63f2264b935b20a502afa3a018848139e3d8575","impliedFormat":99},{"version":"161c8e0690c46021506e32fda85956d785b70f309ae97011fd27374c065cac9b","affectsGlobalScope":true,"impliedFormat":1},{"version":"f582b0fcbf1eea9b318ab92fb89ea9ab2ebb84f9b60af89328a91155e1afce72","impliedFormat":1},{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"8885cf05f3e2abf117590bbb951dcf6359e3e5ac462af1c901cfd24c6a6472e2","impliedFormat":1},{"version":"333caa2bfff7f06017f114de738050dd99a765c7eb16571c6d25a38c0d5365dc","impliedFormat":1},{"version":"e61df3640a38d535fd4bc9f4a53aef17c296b58dc4b6394fd576b808dd2fe5e6","impliedFormat":1},{"version":"459920181700cec8cbdf2a5faca127f3f17fd8dd9d9e577ed3f5f3af5d12a2e4","impliedFormat":1},{"version":"4719c209b9c00b579553859407a7e5dcfaa1c472994bd62aa5dd3cc0757eb077","impliedFormat":1},{"version":"7ec359bbc29b69d4063fe7dad0baaf35f1856f914db16b3f4f6e3e1bca4099fa","impliedFormat":1},{"version":"70790a7f0040993ca66ab8a07a059a0f8256e7bb57d968ae945f696cbff4ac7a","impliedFormat":1},{"version":"d1b9a81e99a0050ca7f2d98d7eedc6cda768f0eb9fa90b602e7107433e64c04c","impliedFormat":1},{"version":"a022503e75d6953d0e82c2c564508a5c7f8556fad5d7f971372d2d40479e4034","impliedFormat":1},{"version":"b215c4f0096f108020f666ffcc1f072c81e9f2f95464e894a5d5f34c5ea2a8b1","impliedFormat":1},{"version":"644491cde678bd462bb922c1d0cfab8f17d626b195ccb7f008612dc31f445d2d","impliedFormat":1},{"version":"dfe54dab1fa4961a6bcfba68c4ca955f8b5bbeb5f2ab3c915aa7adaa2eabc03a","impliedFormat":1},{"version":"1251d53755b03cde02466064260bb88fd83c30006a46395b7d9167340bc59b73","impliedFormat":1},{"version":"47865c5e695a382a916b1eedda1b6523145426e48a2eae4647e96b3b5e52024f","impliedFormat":1},{"version":"4cdf27e29feae6c7826cdd5c91751cc35559125e8304f9e7aed8faef97dcf572","impliedFormat":1},{"version":"331b8f71bfae1df25d564f5ea9ee65a0d847c4a94baa45925b6f38c55c7039bf","impliedFormat":1},{"version":"2a771d907aebf9391ac1f50e4ad37952943515eeea0dcc7e78aa08f508294668","impliedFormat":1},{"version":"0146fd6262c3fd3da51cb0254bb6b9a4e42931eb2f56329edd4c199cb9aaf804","impliedFormat":1},{"version":"183f480885db5caa5a8acb833c2be04f98056bdcc5fb29e969ff86e07efe57ab","impliedFormat":99},{"version":"960bd764c62ac43edc24eaa2af958a4b4f1fa5d27df5237e176d0143b36a39c6","affectsGlobalScope":true,"impliedFormat":1},{"version":"f7eebe1b25040d805aefe8971310b805cd49b8602ec206d25b38dc48c542f165","impliedFormat":1},{"version":"a18642ddf216f162052a16cba0944892c4c4c977d3306a87cb673d46abbb0cbf","impliedFormat":1},{"version":"509f8efdfc5f9f6b52284170e8d7413552f02d79518d1db691ee15acc0088676","impliedFormat":1},{"version":"4ec16d7a4e366c06a4573d299e15fe6207fc080f41beac5da06f4af33ea9761e","impliedFormat":1},{"version":"59f8dc89b9e724a6a667f52cdf4b90b6816ae6c9842ce176d38fcc973669009e","affectsGlobalScope":true,"impliedFormat":1},{"version":"e4af494f7a14b226bbe732e9c130d8811f8c7025911d7c58dd97121a85519715","impliedFormat":1},{"version":"47416e41b1af81e53e8c3cc5bf909d47ff632a7b6eddfe7ff43d187b4dcca047","impliedFormat":99},{"version":"511a5f4f77165dc1b73ceae1e28b4a8f78f3443d8e18a1fd43bfafd2b0133bbe","impliedFormat":1},{"version":"b6d03c9cfe2cf0ba4c673c209fcd7c46c815b2619fd2aad59fc4229aaef2ed43","impliedFormat":1},{"version":"95aba78013d782537cc5e23868e736bec5d377b918990e28ed56110e3ae8b958","impliedFormat":1},{"version":"670a76db379b27c8ff42f1ba927828a22862e2ab0b0908e38b671f0e912cc5ed","impliedFormat":1},{"version":"13b77ab19ef7aadd86a1e54f2f08ea23a6d74e102909e3c00d31f231ed040f62","impliedFormat":1},{"version":"069bebfee29864e3955378107e243508b163e77ab10de6a5ee03ae06939f0bb9","impliedFormat":1},{"version":"26e0ffceb2198feb1ef460d5d14111c69ad07d44c5a67fd4bfeb74c969aa9afb","impliedFormat":99},{"version":"54895c782637a5cd4696a22ea361c107abe8b9e0655ec1b2881504c05af5f6cf","impliedFormat":99},{"version":"051dc29281dd2a9b9ac647e55581c4bd3c6fe47e739a31760eeb6f58483c53c9","signature":"3e18b6ca0a92c0205c51abafed5c5ce07f741c05b913fc3dd18b7c9f5486eb69"}],"root":[205],"options":{"allowSyntheticDefaultImports":true,"composite":true,"module":99,"skipLibCheck":true},"referencedMap":[[199,1],[197,2],[204,3],[202,4],[198,1],[200,5],[201,1],[162,2],[105,6],[106,6],[107,7],[54,8],[108,9],[109,10],[110,11],[52,2],[111,12],[112,13],[113,14],[114,15],[115,16],[116,17],[117,17],[118,18],[119,19],[120,20],[121,21],[55,2],[53,2],[122,22],[123,23],[124,24],[158,25],[125,26],[126,2],[127,27],[128,28],[129,29],[130,30],[131,31],[132,32],[133,33],[134,34],[135,35],[136,35],[137,36],[138,2],[139,37],[140,38],[142,39],[141,40],[143,41],[144,42],[145,43],[146,44],[147,45],[148,46],[149,47],[150,48],[151,49],[152,50],[153,51],[154,52],[155,53],[56,2],[57,54],[58,2],[59,2],[101,55],[102,56],[103,2],[104,41],[156,57],[157,58],[203,59],[167,2],[190,2],[192,60],[191,2],[185,61],[183,62],[184,63],[172,64],[173,62],[180,65],[171,66],[176,67],[186,2],[177,68],[182,69],[188,70],[187,71],[170,72],[178,73],[179,74],[174,75],[181,61],[175,76],[164,77],[163,78],[169,2],[1,2],[50,2],[51,2],[9,2],[13,2],[12,2],[3,2],[14,2],[15,2],[16,2],[17,2],[18,2],[19,2],[20,2],[21,2],[4,2],[22,2],[23,2],[5,2],[24,2],[28,2],[25,2],[26,2],[27,2],[29,2],[30,2],[31,2],[6,2],[32,2],[33,2],[34,2],[35,2],[7,2],[39,2],[36,2],[37,2],[38,2],[40,2],[8,2],[41,2],[46,2],[47,2],[42,2],[43,2],[44,2],[45,2],[2,2],[48,2],[49,2],[11,2],[10,2],[77,79],[89,80],[75,81],[90,82],[99,83],[66,84],[67,85],[65,86],[98,87],[93,88],[97,89],[69,90],[86,91],[68,92],[96,93],[63,94],[64,88],[70,95],[71,2],[76,96],[74,95],[61,97],[100,98],[91,99],[80,100],[79,95],[81,101],[84,102],[78,103],[82,104],[94,87],[72,105],[73,106],[85,107],[62,82],[88,108],[87,95],[83,109],[92,2],[60,2],[95,110],[160,111],[196,112],[166,113],[161,111],[159,2],[165,114],[194,2],[189,2],[193,115],[168,2],[195,116],[205,117]],"latestChangedDtsFile":"./vite.config.d.ts","version":"5.8.3"} \ No newline at end of file diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo new file mode 100644 index 00000000..8d6a74c7 --- /dev/null +++ b/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/theme-provider.tsx","./src/components/updater-checker.tsx","./src/components/business/datagrid/tableview.tsx","./src/components/business/datagrid/tableview/utils.ts","./src/components/business/datagrid/tableview/utils.unit.test.ts","./src/components/business/editor/savequerydialog.tsx","./src/components/business/editor/sqleditor.tsx","./src/components/business/editor/clickhousekeywords.ts","./src/components/business/editor/codemirrortheme.ts","./src/components/business/editor/sqlselection.ts","./src/components/business/editor/sqlselection.unit.test.ts","./src/components/business/metadata/tablemetadataview.tsx","./src/components/business/sidebar/aihistorypopover.tsx","./src/components/business/sidebar/aimarkdownmessage.tsx","./src/components/business/sidebar/aisidebar.tsx","./src/components/business/sidebar/connectionlist.tsx","./src/components/business/sidebar/savedquerieslist.tsx","./src/components/business/sidebar/sidebar.tsx","./src/components/business/sidebar/chat/chatcomposer.tsx","./src/components/business/sidebar/chat/chatmessageitem.tsx","./src/components/business/sidebar/chat/chatmessagelist.tsx","./src/components/business/sidebar/chat/chattypingindicator.tsx","./src/components/business/sidebar/chat/tableselector.tsx","./src/components/business/sidebar/connection-list/treenode.tsx","./src/components/business/sidebar/connection-list/helpers.tsx","./src/components/business/sqllogs/sqlexecutionlogsdialog.tsx","./src/components/settings/languageselector.tsx","./src/components/settings/settingsdialog.tsx","./src/components/ui/accordion.tsx","./src/components/ui/alert-dialog.tsx","./src/components/ui/alert.tsx","./src/components/ui/aspect-ratio.tsx","./src/components/ui/avatar.tsx","./src/components/ui/badge.tsx","./src/components/ui/breadcrumb.tsx","./src/components/ui/button.tsx","./src/components/ui/calendar.tsx","./src/components/ui/card.tsx","./src/components/ui/carousel.tsx","./src/components/ui/chart.tsx","./src/components/ui/checkbox.tsx","./src/components/ui/collapsible.tsx","./src/components/ui/command.tsx","./src/components/ui/context-menu.tsx","./src/components/ui/dialog.tsx","./src/components/ui/drawer.tsx","./src/components/ui/dropdown-menu.tsx","./src/components/ui/form.tsx","./src/components/ui/hover-card.tsx","./src/components/ui/input-otp.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/menubar.tsx","./src/components/ui/navigation-menu.tsx","./src/components/ui/pagination.tsx","./src/components/ui/popover.tsx","./src/components/ui/progress.tsx","./src/components/ui/radio-group.tsx","./src/components/ui/resizable.tsx","./src/components/ui/scroll-area.tsx","./src/components/ui/select.tsx","./src/components/ui/separator.tsx","./src/components/ui/sheet.tsx","./src/components/ui/sidebar.tsx","./src/components/ui/skeleton.tsx","./src/components/ui/slider.tsx","./src/components/ui/sonner.tsx","./src/components/ui/sortable-tab.tsx","./src/components/ui/switch.tsx","./src/components/ui/table.tsx","./src/components/ui/tabs.tsx","./src/components/ui/textarea.tsx","./src/components/ui/toggle-group.tsx","./src/components/ui/toggle.tsx","./src/components/ui/tooltip.tsx","./src/components/ui/use-mobile.ts","./src/components/ui/utils.ts","./src/lib/keyboard.ts","./src/lib/queryexecutionstate.ts","./src/lib/queryexecutionstate.unit.test.ts","./src/lib/sqleditordatabase.ts","./src/lib/sqleditordatabase.unit.test.ts","./src/lib/connection-form/rules.ts","./src/lib/connection-form/rules.unit.test.ts","./src/lib/connection-form/validate.ts","./src/lib/connection-form/validate.unit.test.ts","./src/lib/i18n/index.ts","./src/lib/i18n/locales/en.ts","./src/lib/i18n/locales/ja.ts","./src/lib/i18n/locales/zh.ts","./src/services/api.test.ts","./src/services/api.ts","./src/services/mocks.service.test.ts","./src/services/mocks.ts","./src/services/store.ts","./src/services/updater.ts","./src/services/updater.unit.test.ts","./src/theme/themeregistry.ts","./src/theme/themeregistry.unit.test.ts","./src/types/bun-test.d.ts"],"version":"5.8.3"} \ No newline at end of file diff --git a/vite.config.d.ts b/vite.config.d.ts new file mode 100644 index 00000000..0155bbcd --- /dev/null +++ b/vite.config.d.ts @@ -0,0 +1,2 @@ +declare const _default: import("vite").UserConfigFnPromise; +export default _default; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 00000000..aa3a644c --- /dev/null +++ b/vite.config.js @@ -0,0 +1,75 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import tailwindcss from "@tailwindcss/vite"; +import path from "path"; +var host = process.env.TAURI_DEV_HOST; +// https://vite.dev/config/ +export default defineConfig(function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, ({ + plugins: [react(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // + // 1. prevent Vite from obscuring rust errors + clearScreen: false, + // 2. tauri expects a fixed port, fail if that port is not available + server: { + port: 1420, + strictPort: true, + host: host || false, + hmr: host + ? { + protocol: "ws", + host: host, + port: 1421, + } + : undefined, + watch: { + // 3. tell Vite to ignore watching `src-tauri` + ignored: ["**/src-tauri/**"], + }, + }, + })]; + }); +}); }); diff --git a/vite.config.ts b/vite.config.ts index 52d888ea..c9aefcfd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,17 +1,13 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; -import tailwindcss from '@tailwindcss/vite'; -import path from 'path'; +import tailwindcss from "@tailwindcss/vite"; +import path from "path"; -// @ts-expect-error process is a nodejs global const host = process.env.TAURI_DEV_HOST; // https://vite.dev/config/ export default defineConfig(async () => ({ - plugins: [ - react(), - tailwindcss(), - ], + plugins: [react(), tailwindcss()], resolve: { alias: { "@": path.resolve(__dirname, "./src"), From e634167412eba0c5189a7e52baf437e089ad3764 Mon Sep 17 00:00:00 2001 From: codeErrorSleep Date: Tue, 31 Mar 2026 13:38:09 +0800 Subject: [PATCH 6/7] style: Unify code formatting style, repair long line auto wrapping -Apply consistent code formatting rules to Rust and TypeScript/JSX files -Automatically split long statements that exceed the maximum line width into multiple lines to improve readability -Fix the string quote escaping problem caused by line breaking in the test file -Adjust the indent and newline of conditional rendering in JSX to keep the code style consistent --- src-tauri/src/commands/metadata.rs | 5 +- src-tauri/src/commands/transfer.rs | 3 +- src-tauri/src/db/local.rs | 5 +- src-tauri/tests/clickhouse_integration.rs | 15 +++- src-tauri/tests/common/postgres_context.rs | 19 +++-- src-tauri/tests/common/shared.rs | 5 +- src-tauri/tests/mssql_command_integration.rs | 20 ++++- .../mysql_stateful_command_integration.rs | 44 ++++++---- src-tauri/tests/postgres_integration.rs | 10 ++- src-tauri/tests/sqlite_integration.rs | 34 ++++++-- .../business/DataGrid/TableView.tsx | 81 ++++++++++--------- .../DataGrid/tableView/utils.unit.test.ts | 15 ++-- .../business/Sidebar/ConnectionList.tsx | 10 ++- src/lib/i18n/locales/ja.ts | 3 +- 14 files changed, 179 insertions(+), 90 deletions(-) diff --git a/src-tauri/src/commands/metadata.rs b/src-tauri/src/commands/metadata.rs index f0e7c0f1..c28bdbe1 100644 --- a/src-tauri/src/commands/metadata.rs +++ b/src-tauri/src/commands/metadata.rs @@ -2,7 +2,10 @@ use crate::models::{ConnectionForm, SchemaOverview, TableInfo, TableMetadata, Ta use crate::state::AppState; use tauri::State; -fn ensure_table_structure_found(structure: TableStructure, table: &str) -> Result { +fn ensure_table_structure_found( + structure: TableStructure, + table: &str, +) -> Result { if structure.columns.is_empty() { return Err(format!( "[NOT_FOUND] Table '{}' does not exist or has no visible columns", diff --git a/src-tauri/src/commands/transfer.rs b/src-tauri/src/commands/transfer.rs index 94bd044d..ff1cf336 100644 --- a/src-tauri/src/commands/transfer.rs +++ b/src-tauri/src/commands/transfer.rs @@ -383,7 +383,8 @@ pub async fn export_query_result_direct( .map(|c| c.name) .collect::>(); let mut writer = ExportWriter::new(output_path.clone(), format, columns.clone())?; - let exported = writer.write_rows(&result.data, &columns, None, "query_result", &driver)?; + let exported = + writer.write_rows(&result.data, &columns, None, "query_result", &driver)?; writer.finish()?; Ok(ExportResult { file_path: output_path.to_string_lossy().to_string(), diff --git a/src-tauri/src/db/local.rs b/src-tauri/src/db/local.rs index 5d8b7fc5..f1804691 100644 --- a/src-tauri/src/db/local.rs +++ b/src-tauri/src/db/local.rs @@ -939,7 +939,10 @@ mod tests { let mut ai_master_key = [0u8; 32]; rand::rngs::OsRng.fill_bytes(&mut ai_master_key); - LocalDb { pool, ai_master_key } + LocalDb { + pool, + ai_master_key, + } } fn provider_form( diff --git a/src-tauri/tests/clickhouse_integration.rs b/src-tauri/tests/clickhouse_integration.rs index 08f42215..5932a3d0 100644 --- a/src-tauri/tests/clickhouse_integration.rs +++ b/src-tauri/tests/clickhouse_integration.rs @@ -96,7 +96,10 @@ async fn test_clickhouse_integration_flow() { .await .expect("get_table_metadata failed"); assert!( - metadata.columns.iter().any(|c| c.name == "id" && c.primary_key), + metadata + .columns + .iter() + .any(|c| c.name == "id" && c.primary_key), "metadata should include primary key id" ); assert!( @@ -462,7 +465,10 @@ async fn test_clickhouse_boolean_and_json_type_mapping_regression() { "unexpected query flag value: {:?}", query_flag ); - assert_eq!(query_row["tier"], serde_json::Value::String("gold".to_string())); + assert_eq!( + query_row["tier"], + serde_json::Value::String("gold".to_string()) + ); let table_data = driver .get_table_data( @@ -489,7 +495,10 @@ async fn test_clickhouse_boolean_and_json_type_mapping_regression() { "unexpected grid flag value: {:?}", grid_flag ); - assert!(grid_row.get("meta").is_some(), "meta should exist in table_data"); + assert!( + grid_row.get("meta").is_some(), + "meta should exist in table_data" + ); let _ = driver .execute_query(format!("DROP TABLE IF EXISTS {}", qualified)) diff --git a/src-tauri/tests/common/postgres_context.rs b/src-tauri/tests/common/postgres_context.rs index 5a140909..8916eb57 100644 --- a/src-tauri/tests/common/postgres_context.rs +++ b/src-tauri/tests/common/postgres_context.rs @@ -23,8 +23,8 @@ pub fn postgres_form_from_test_context<'a>( .with_env_var("POSTGRES_DB", "postgres") .with_wait_for(WaitFor::seconds(3)) .with_exposed_port(5432); - let runnable = RunnableImage::from(image) - .with_container_name(shared::unique_container_name("postgres")); + let runnable = + RunnableImage::from(image).with_container_name(shared::unique_container_name("postgres")); let container = docker.run(runnable); let port = container.get_host_port_ipv4(5432); @@ -46,11 +46,20 @@ pub fn postgres_form_from_test_context<'a>( fn postgres_form_from_local_env() -> ConnectionForm { let mut form = ConnectionForm { driver: "postgres".to_string(), - host: Some(shared::env_or_any(&["POSTGRES_HOST", "PG_HOST"], "localhost")), + host: Some(shared::env_or_any( + &["POSTGRES_HOST", "PG_HOST"], + "localhost", + )), port: Some(shared::env_i64_any(&["POSTGRES_PORT", "PG_PORT"], 5432)), username: Some(shared::env_or_any(&["POSTGRES_USER", "PGUSER"], "postgres")), - password: Some(shared::env_or_any(&["POSTGRES_PASSWORD", "PGPASSWORD"], "postgres")), - database: Some(shared::env_or_any(&["POSTGRES_DB", "PGDATABASE"], "postgres")), + password: Some(shared::env_or_any( + &["POSTGRES_PASSWORD", "PGPASSWORD"], + "postgres", + )), + database: Some(shared::env_or_any( + &["POSTGRES_DB", "PGDATABASE"], + "postgres", + )), ..Default::default() }; apply_postgres_env_overrides(&mut form); diff --git a/src-tauri/tests/common/shared.rs b/src-tauri/tests/common/shared.rs index a4300fda..6e114d31 100644 --- a/src-tauri/tests/common/shared.rs +++ b/src-tauri/tests/common/shared.rs @@ -25,7 +25,10 @@ pub fn wait_for_port(host: &str, port: u16, timeout: Duration) { sleep(Duration::from_millis(500)); } - panic!("timed out waiting for {}:{} to accept connections", host, port); + panic!( + "timed out waiting for {}:{} to accept connections", + host, port + ); } pub fn ensure_docker_available() { diff --git a/src-tauri/tests/mssql_command_integration.rs b/src-tauri/tests/mssql_command_integration.rs index c13f0634..2022cd4d 100644 --- a/src-tauri/tests/mssql_command_integration.rs +++ b/src-tauri/tests/mssql_command_integration.rs @@ -38,7 +38,10 @@ async fn prepare_query_test_table(form: &ConnectionForm, table: &str) { .expect("failed to connect mssql driver"); driver - .execute_query(format!("IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", table, table)) + .execute_query(format!( + "IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", + table, table + )) .await .ok(); driver @@ -63,7 +66,10 @@ async fn cleanup_table(form: &ConnectionForm, table: &str) { .await .expect("failed to connect mssql driver for cleanup"); driver - .execute_query(format!("IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", table, table)) + .execute_query(format!( + "IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", + table, table + )) .await .ok(); driver.close().await; @@ -229,7 +235,10 @@ async fn test_mssql_command_execute_by_conn_insert_affects_rows() { .await .expect("failed to connect mssql driver"); driver - .execute_query(format!("IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", table, table)) + .execute_query(format!( + "IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", + table, table + )) .await .ok(); driver @@ -268,7 +277,10 @@ async fn test_mssql_command_get_table_data_by_conn_pagination_works() { .await .expect("failed to connect mssql driver"); driver - .execute_query(format!("IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", table, table)) + .execute_query(format!( + "IF OBJECT_ID('{}', 'U') IS NOT NULL DROP TABLE {}", + table, table + )) .await .ok(); driver diff --git a/src-tauri/tests/mysql_stateful_command_integration.rs b/src-tauri/tests/mysql_stateful_command_integration.rs index 002d50a6..fcbdb796 100644 --- a/src-tauri/tests/mysql_stateful_command_integration.rs +++ b/src-tauri/tests/mysql_stateful_command_integration.rs @@ -1,10 +1,10 @@ #[path = "common/mysql_context.rs"] mod mysql_context; +use dbpaw_lib::ai::types::AiChatRequest; use dbpaw_lib::commands::connection::{self, CreateDatabasePayload}; -use dbpaw_lib::commands::{ai, query, storage, transfer}; use dbpaw_lib::commands::metadata; -use dbpaw_lib::ai::types::AiChatRequest; +use dbpaw_lib::commands::{ai, query, storage, transfer}; use dbpaw_lib::db::drivers::mysql::MysqlDriver; use dbpaw_lib::db::drivers::DatabaseDriver; use dbpaw_lib::db::local::LocalDb; @@ -129,7 +129,12 @@ async fn prepare_metadata_fixture( driver.close().await; } -async fn cleanup_metadata_fixture(form: &ConnectionForm, schema: &str, parent_table: &str, child_table: &str) { +async fn cleanup_metadata_fixture( + form: &ConnectionForm, + schema: &str, + parent_table: &str, + child_table: &str, +) { let driver = MysqlDriver::connect(form) .await .expect("failed to connect mysql driver for metadata cleanup"); @@ -327,9 +332,10 @@ async fn test_mysql_command_get_table_structure_success() { let child = unique_name("dbpaw_meta_child"); prepare_metadata_fixture(&form, &schema, &parent, &child).await; - let structure = metadata::get_table_structure_direct(&state, conn_id, schema.clone(), child.clone()) - .await - .expect("get_table_structure should succeed"); + let structure = + metadata::get_table_structure_direct(&state, conn_id, schema.clone(), child.clone()) + .await + .expect("get_table_structure should succeed"); assert!(structure.columns.iter().any(|c| c.name == "id")); assert!(structure.columns.iter().any(|c| c.name == "parent_id")); @@ -447,7 +453,10 @@ async fn test_mysql_command_get_schema_overview_contains_target_schema() { ) .await .expect("get_schema_overview should succeed"); - assert!(overview.tables.iter().any(|t| t.schema == schema && t.name == child)); + assert!(overview + .tables + .iter() + .any(|t| t.schema == schema && t.name == child)); cleanup_metadata_fixture(&form, &schema, &parent, &child).await; let _ = connection::delete_connection_direct(&state, conn_id).await; @@ -558,13 +567,10 @@ async fn test_mysql_command_cancel_query_non_clickhouse_returns_false() { let state = init_state_with_local_db().await; let conn_id = create_mysql_connection_for_state(&state, &form, "query-cancel-non-ch").await; - let canceled = query::cancel_query_direct( - &state, - conn_id.to_string(), - "phase4-qid-cancel".to_string(), - ) - .await - .expect("cancel_query should return bool"); + let canceled = + query::cancel_query_direct(&state, conn_id.to_string(), "phase4-qid-cancel".to_string()) + .await + .expect("cancel_query should return bool"); assert!(!canceled); let _ = connection::delete_connection_direct(&state, conn_id).await; @@ -708,7 +714,10 @@ async fn test_mysql_command_transfer_export_and_import_minimal_flow() { ) .await .expect("import_sql_file should succeed"); - assert_eq!(import_result.success_statements, import_result.total_statements); + assert_eq!( + import_result.success_statements, + import_result.total_statements + ); assert!(import_result.error.is_none()); let cleanup_driver = MysqlDriver::connect(&form) @@ -718,7 +727,10 @@ async fn test_mysql_command_transfer_export_and_import_minimal_flow() { .execute_query(format!("DROP TABLE IF EXISTS {}", qualified)) .await; let _ = cleanup_driver - .execute_query(format!("DROP TABLE IF EXISTS `{}`.`{}`", schema, import_table)) + .execute_query(format!( + "DROP TABLE IF EXISTS `{}`.`{}`", + schema, import_table + )) .await; cleanup_driver.close().await; let _ = fs::remove_file(table_export_path); diff --git a/src-tauri/tests/postgres_integration.rs b/src-tauri/tests/postgres_integration.rs index bb9164dd..9c5992be 100644 --- a/src-tauri/tests/postgres_integration.rs +++ b/src-tauri/tests/postgres_integration.rs @@ -350,7 +350,10 @@ async fn test_postgres_metadata_includes_indexes_and_foreign_keys() { .await; driver - .execute_query(format!("CREATE TABLE {} (id INT PRIMARY KEY)", parent_qualified)) + .execute_query(format!( + "CREATE TABLE {} (id INT PRIMARY KEY)", + parent_qualified + )) .await .expect("create parent table failed"); driver @@ -456,7 +459,10 @@ async fn test_postgres_boolean_and_json_type_mapping_regression() { .await .expect("get_table_data for bool/json table failed"); assert_eq!(table_data.total, 1); - let grid_row = table_data.data.first().expect("table data row should exist"); + let grid_row = table_data + .data + .first() + .expect("table data row should exist"); assert_eq!(grid_row["flag"], serde_json::Value::Bool(true)); assert!( grid_row.get("meta").is_some(), diff --git a/src-tauri/tests/sqlite_integration.rs b/src-tauri/tests/sqlite_integration.rs index 016950ac..b9c94ee7 100644 --- a/src-tauri/tests/sqlite_integration.rs +++ b/src-tauri/tests/sqlite_integration.rs @@ -252,7 +252,9 @@ async fn test_sqlite_get_table_data_rejects_invalid_sort_column() { .expect("Failed to connect to sqlite db"); driver - .execute_query("CREATE TABLE dbpaw_sqlite_invalid_sort_probe (id INTEGER PRIMARY KEY)".to_string()) + .execute_query( + "CREATE TABLE dbpaw_sqlite_invalid_sort_probe (id INTEGER PRIMARY KEY)".to_string(), + ) .await .expect("create dbpaw_sqlite_invalid_sort_probe failed"); @@ -302,11 +304,17 @@ async fn test_sqlite_table_structure_and_schema_overview() { .expect("create dbpaw_sqlite_overview_probe failed"); let structure = driver - .get_table_structure("main".to_string(), "dbpaw_sqlite_overview_probe".to_string()) + .get_table_structure( + "main".to_string(), + "dbpaw_sqlite_overview_probe".to_string(), + ) .await .expect("get_table_structure failed"); assert!( - structure.columns.iter().any(|c| c.name == "id" && c.primary_key), + structure + .columns + .iter() + .any(|c| c.name == "id" && c.primary_key), "table structure should include primary key id" ); assert!( @@ -360,21 +368,26 @@ async fn test_sqlite_metadata_includes_indexes_and_foreign_keys() { .expect("create sqlite metadata probe tables failed"); let metadata = driver - .get_table_metadata("main".to_string(), "dbpaw_sqlite_child_meta_probe".to_string()) + .get_table_metadata( + "main".to_string(), + "dbpaw_sqlite_child_meta_probe".to_string(), + ) .await .expect("get_table_metadata failed"); assert!( metadata .indexes .iter() - .any(|i| i.name == "idx_dbpaw_sqlite_child_name" && i.columns.contains(&"name".to_string())), + .any(|i| i.name == "idx_dbpaw_sqlite_child_name" + && i.columns.contains(&"name".to_string())), "metadata should include idx_dbpaw_sqlite_child_name" ); assert!( metadata .foreign_keys .iter() - .any(|fk| fk.column == "parent_id" && fk.referenced_table == "dbpaw_sqlite_parent_meta_probe"), + .any(|fk| fk.column == "parent_id" + && fk.referenced_table == "dbpaw_sqlite_parent_meta_probe"), "metadata should include FK parent_id -> dbpaw_sqlite_parent_meta_probe(id)" ); @@ -423,7 +436,10 @@ async fn test_sqlite_boolean_and_json_type_mapping_regression() { assert_eq!(query_result.row_count, 1); let query_row = query_result.data.first().expect("query row should exist"); assert_eq!(query_row["flag"], serde_json::Value::Bool(true)); - assert_eq!(query_row["tier"], serde_json::Value::String("gold".to_string())); + assert_eq!( + query_row["tier"], + serde_json::Value::String("gold".to_string()) + ); let table_data = driver .get_table_data( @@ -547,7 +563,9 @@ async fn test_sqlite_execute_query_reports_affected_rows_for_update_delete() { assert_eq!(updated.row_count, 1); let deleted = driver - .execute_query("DELETE FROM dbpaw_sqlite_affected_rows_probe WHERE id IN (1, 2)".to_string()) + .execute_query( + "DELETE FROM dbpaw_sqlite_affected_rows_probe WHERE id IN (1, 2)".to_string(), + ) .await .expect("delete affected_rows probe rows failed"); assert_eq!(deleted.row_count, 2); diff --git a/src/components/business/DataGrid/TableView.tsx b/src/components/business/DataGrid/TableView.tsx index d7abbc46..77396916 100644 --- a/src/components/business/DataGrid/TableView.tsx +++ b/src/components/business/DataGrid/TableView.tsx @@ -456,11 +456,13 @@ export function TableView({ const isClickHouseDriver = tableContext?.driver === "clickhouse"; const hasPrimaryKeys = primaryKeys.length > 0; - const canInsert = !!tableContext && + const canInsert = + !!tableContext && (isClickHouseDriver ? isClickHouseMergeTreeEngine(clickhouseEngine) : hasPrimaryKeys); - const canUpdateDelete = !!tableContext && + const canUpdateDelete = + !!tableContext && (isClickHouseDriver ? canMutateClickHouseTable(clickhouseEngine, primaryKeys) : hasPrimaryKeys); @@ -669,7 +671,8 @@ export function TableView({ // --- SQL generation & save --- const generateUpdateSQL = useCallback(() => { - if (!tableContext || !canUpdateDelete || primaryKeys.length === 0) return []; + if (!tableContext || !canUpdateDelete || primaryKeys.length === 0) + return []; // Group changes by source row index const changesByRow = new Map(); @@ -1147,7 +1150,8 @@ export function TableView({ const buildRowsUpdateSQL = useCallback( (rowIndexes: number[]) => { - if (!tableContext || !canUpdateDelete || primaryKeys.length === 0) return ""; + if (!tableContext || !canUpdateDelete || primaryKeys.length === 0) + return ""; const orderedRows = [...rowIndexes].sort((a, b) => a - b); const { schema, table, driver } = tableContext; const tableName = getQualifiedTableName(driver, schema, table); @@ -1658,34 +1662,34 @@ export function TableView({ {(canInsert || canUpdateDelete) && ( <> {canInsert && ( - + )} {canUpdateDelete && ( - + )} )} @@ -1855,16 +1859,17 @@ export function TableView({ /> {tableContext && mutabilityHint && ( - - {canInsert ? "Partial write" : "Read-only"} - - )} + + {canInsert ? "Partial write" : "Read-only"} + + )} ) : ( - tableContext && mutabilityHint && ( + tableContext && + mutabilityHint && ( { test("keeps generic update/delete statements for non-clickhouse", () => { expect( - buildUpdateStatement("postgres", '"public"."users"', '"name" = \'new\'', '"id" = 1'), - ).toBe("UPDATE \"public\".\"users\" SET \"name\" = 'new' WHERE \"id\" = 1"); - expect(buildDeleteStatement("postgres", '"public"."users"', '"id" = 1')).toBe( - "DELETE FROM \"public\".\"users\" WHERE \"id\" = 1", - ); + buildUpdateStatement( + "postgres", + '"public"."users"', + "\"name\" = 'new'", + '"id" = 1', + ), + ).toBe('UPDATE "public"."users" SET "name" = \'new\' WHERE "id" = 1'); + expect( + buildDeleteStatement("postgres", '"public"."users"', '"id" = 1'), + ).toBe('DELETE FROM "public"."users" WHERE "id" = 1'); }); }); diff --git a/src/components/business/Sidebar/ConnectionList.tsx b/src/components/business/Sidebar/ConnectionList.tsx index 65da7d9d..98dc5b1f 100644 --- a/src/components/business/Sidebar/ConnectionList.tsx +++ b/src/components/business/Sidebar/ConnectionList.tsx @@ -2958,8 +2958,9 @@ export function ConnectionList({ diff --git a/src/lib/i18n/locales/ja.ts b/src/lib/i18n/locales/ja.ts index aa2dea82..47968fab 100644 --- a/src/lib/i18n/locales/ja.ts +++ b/src/lib/i18n/locales/ja.ts @@ -347,7 +347,8 @@ export const ja: Translations = { createDatabaseFailed: "Database ใฎไฝœๆˆใซๅคฑๆ•—ใ—ใพใ—ใŸ", importDesktopOnly: "SQL ใ‚คใƒณใƒใƒผใƒˆใฏ Tauri ใƒ‡ใ‚นใ‚ฏใƒˆใƒƒใƒ—ใƒขใƒผใƒ‰ใงใฎใฟๅˆฉ็”จใงใใพใ™ใ€‚", - importUnsupportedDriver: "ใ“ใฎใƒ‰ใƒฉใ‚คใƒใƒผใงใฏ SQL ใ‚คใƒณใƒใƒผใƒˆใซๅฏพๅฟœใ—ใฆใ„ใพใ›ใ‚“ใ€‚", + importUnsupportedDriver: + "ใ“ใฎใƒ‰ใƒฉใ‚คใƒใƒผใงใฏ SQL ใ‚คใƒณใƒใƒผใƒˆใซๅฏพๅฟœใ—ใฆใ„ใพใ›ใ‚“ใ€‚", importReadOnlyDriver: "ใ“ใฎใƒ‰ใƒฉใ‚คใƒใƒผใฏ DbPaw ใงใฏ่ชญใฟๅ–ใ‚Šๅฐ‚็”จใฎใŸใ‚ใ€SQL ใ‚คใƒณใƒใƒผใƒˆใซๅฏพๅฟœใ—ใฆใ„ใพใ›ใ‚“ใ€‚", selectImportSqlFile: "ใ‚คใƒณใƒใƒผใƒˆใ™ใ‚‹ SQL ใƒ•ใ‚กใ‚คใƒซใ‚’้ธๆŠž", From c925cababf59cf174ba38eec990e01743cec0e3a Mon Sep 17 00:00:00 2001 From: qiuwenhao Date: Tue, 31 Mar 2026 23:42:38 +0800 Subject: [PATCH 7/7] chore: update version to v0.3.1 --- package.json | 2 +- src-tauri/tauri.conf.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7fdc9880..9fd14fe5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "git+https://github.com/codeErrorSleep/dbpaw.git" }, "private": true, - "version": "0.3.0", + "version": "0.3.1", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a935348d..9782f466 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "DbPaw", - "version": "0.3.0", + "version": "0.3.1", "identifier": "com.father.dbpaw", "build": { "beforeDevCommand": "bun run dev",