diff --git a/.github/scripts/bump-agent-versions.mjs b/.github/scripts/bump-agent-versions.mjs index 7fb5fef8f9..1779488551 100644 --- a/.github/scripts/bump-agent-versions.mjs +++ b/.github/scripts/bump-agent-versions.mjs @@ -62,7 +62,10 @@ const nativeDriverDirectories = { zookeeper: "zookeeper", tdengine: "tdengine", }; -const nativeDriverModules = new Set(["cassandra", "duckdb", "hive", "oracle", "xugu", "kingbase", "iotdb", "neo4j", "vastbase", "rabbitmq", "rocketmq", "zookeeper", "tdengine"]); +const crateNativeDriverDirectories = { + "sqlite-worker": "crates/dbx-sqlite-worker", +}; +const nativeDriverModules = new Set(["cassandra", "duckdb", "hive", "oracle", "xugu", "kingbase", "iotdb", "neo4j", "vastbase", "rabbitmq", "rocketmq", "zookeeper", "tdengine", "sqlite-worker"]); const nativeDriverSharedPaths = { hive: [ "agents/go-common/go-gssapi", @@ -76,6 +79,18 @@ const nativeDriverSharedPaths = { }; function resolveAgentModule(moduleName, { legacyStandaloneModules, moduleExists, readModuleFile }) { + const cratePath = crateNativeDriverDirectories[moduleName]; + if (cratePath && moduleExists(cratePath)) { + return { + checkDir: cratePath, + modulePath: cratePath, + sharedPaths: [], + javaBuild: false, + nativeBuild: true, + commonDependent: false, + }; + } + let checkDir = null; const nativeDriverDirectory = nativeDriverDirectories[moduleName]; if (nativeDriverDirectory && moduleExists(`agents/drivers/${nativeDriverDirectory}`)) { diff --git a/.github/scripts/bump-agent-versions.test.mjs b/.github/scripts/bump-agent-versions.test.mjs index ea6e6e06cc..212dd67c11 100644 --- a/.github/scripts/bump-agent-versions.test.mjs +++ b/.github/scripts/bump-agent-versions.test.mjs @@ -34,6 +34,20 @@ test("bumps DuckDB after its initial release", () => { assert.equal(result.versions.duckdb, "0.1.1"); }); +test("bumps the SQLite SSH worker from its crate path", () => { + const result = evaluateAgentVersionBump({ + versions: { "sqlite-worker": "0.1.0" }, + changedFiles: ["crates/dbx-sqlite-worker/src/runtime.rs"], + moduleExists: (path) => path === "crates/dbx-sqlite-worker", + readModuleFile: () => "", + }); + + assert.equal(result.versions["sqlite-worker"], "0.1.1"); + assert.deepEqual(result.changedModules, ["sqlite-worker"]); + assert.deepEqual(result.javaModules, []); + assert.deepEqual(result.nativeModules, ["sqlite-worker"]); +}); + test("classifies TDengine Rust changes as native-only", () => { const result = evaluateAgentVersionBump({ versions: { tdengine: "0.1.39" }, diff --git a/.github/workflows/agents-release.yml b/.github/workflows/agents-release.yml index 84fde5197d..dfe2736d3c 100644 --- a/.github/workflows/agents-release.yml +++ b/.github/workflows/agents-release.yml @@ -746,6 +746,57 @@ jobs: name: duckdb-native-${{ matrix.platform }} path: "release-native/dbx-agent-duckdb-*" + build-sqlite-worker-native: + needs: [bump-versions] + if: ${{ contains(fromJSON(needs.bump-versions.outputs.native_modules), 'sqlite-worker') }} + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-22.04 + target: x86_64-unknown-linux-musl + platform: linux-x64 + rustflags: "-C target-feature=+crt-static" + - runner: ubuntu-22.04-arm + target: aarch64-unknown-linux-musl + platform: linux-aarch64 + rustflags: "-C target-feature=+crt-static -C link-arg=-z -C link-arg=max-page-size=65536" + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v5 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@1.97.1 + with: + targets: ${{ matrix.target }} + + - name: Install cargo-zigbuild + run: pip3 install cargo-zigbuild + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: "./ -> target" + shared-key: sqlite-worker-${{ matrix.target }} + cache-on-failure: true + + - name: Build musl SQLite SSH worker + env: + RUSTFLAGS: ${{ matrix.rustflags }} + run: cargo zigbuild --release -p dbx-sqlite-worker --target ${{ matrix.target }} + + - name: Stage SQLite SSH worker + run: | + mkdir -p release-native + cp \ + "target/${{ matrix.target }}/release/dbx-sqlite-worker" \ + "release-native/dbx-agent-sqlite-worker-${{ matrix.platform }}" + + - uses: actions/upload-artifact@v4 + with: + name: sqlite-worker-native-${{ matrix.platform }} + path: "release-native/dbx-agent-sqlite-worker-*" + build-tdengine-native: needs: [bump-versions] if: ${{ contains(fromJSON(needs.bump-versions.outputs.native_modules), 'tdengine') }} @@ -966,7 +1017,7 @@ jobs: retention-days: 1 release: - needs: [bump-versions, commit-versions, build-agents, build-oracle-native, build-xugu-native, build-rabbitmq-native, build-rocketmq-native, build-zookeeper-native, build-cassandra-native, build-hive-native, build-kingbase-native, build-vastbase-native, build-neo4j-native, build-iotdb-native, build-duckdb-native, build-tdengine-native, build-jre, reuse-previous-assets] + needs: [bump-versions, commit-versions, build-agents, build-oracle-native, build-xugu-native, build-rabbitmq-native, build-rocketmq-native, build-zookeeper-native, build-cassandra-native, build-hive-native, build-kingbase-native, build-vastbase-native, build-neo4j-native, build-iotdb-native, build-duckdb-native, build-sqlite-worker-native, build-tdengine-native, build-jre, reuse-previous-assets] if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} runs-on: ubuntu-latest steps: @@ -1091,6 +1142,7 @@ jobs: kingbase) echo "金仓KingbaseES" ;; vastbase) echo "Vastbase" ;; duckdb) echo "DuckDB" ;; + sqlite-worker) echo "SQLite SSH Worker" ;; xugu) echo "虚谷 XuguDB" ;; rabbitmq) echo "RabbitMQ" ;; rocketmq) echo "Apache RocketMQ" ;; @@ -1163,7 +1215,7 @@ jobs: [ -n "$DRIVERS" ] && DRIVERS="${DRIVERS},"$'\n' DRIVERS="${DRIVERS}$(generate_jar_entry "$name" "$label" "$f" "$jre_key" "$version" "$external_driver" "$native_json")" done - for name in oracle xugu kingbase vastbase neo4j iotdb duckdb rabbitmq rocketmq zookeeper cassandra hive tdengine; do + for name in oracle xugu kingbase vastbase neo4j iotdb duckdb sqlite-worker rabbitmq rocketmq zookeeper cassandra hive tdengine; do version=$(get_module_version "$name") [ -f "release/dbx-agent-${name}-${version}.jar" ] && continue native_json=$(generate_native_platforms "$name" "$version") @@ -1243,6 +1295,7 @@ jobs: kingbase) echo "金仓KingbaseES" ;; vastbase) echo "Vastbase" ;; duckdb) echo "DuckDB" ;; + sqlite-worker) echo "SQLite SSH Worker" ;; oracle) echo "Oracle" ;; xugu) echo "虚谷 XuguDB" ;; rabbitmq) echo "RabbitMQ" ;; @@ -1285,6 +1338,8 @@ jobs: LOG_PATH="agents/drivers/neo4j-go/" elif [ "$name" = "iotdb" ]; then LOG_PATH="agents/drivers/iotdb/" + elif [ "$name" = "sqlite-worker" ]; then + LOG_PATH="crates/dbx-sqlite-worker/" elif [ -d "agents/drivers/$name" ]; then LOG_PATH="agents/drivers/$name/" else diff --git a/Cargo.lock b/Cargo.lock index 55b7bac5ce..af93e3b723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2087,6 +2087,7 @@ dependencies = [ "cbc 0.1.2", "chrono", "csv", + "dbx-sqlite-worker", "deadpool-postgres", "encoding_rs", "flate2", @@ -2172,6 +2173,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "dbx-sqlite-worker" +version = "0.1.1" +dependencies = [ + "rusqlite", + "serde", + "serde_json", + "sqlparser", +] + [[package]] name = "dbx-web" version = "0.5.98" diff --git a/Cargo.toml b/Cargo.toml index d2ae9675e2..4a079ee0b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["src-tauri", "crates/dbx-core", "crates/dbx-web", "crates/dbx-mcp", "crates/dbx-cli"] +members = ["src-tauri", "crates/dbx-core", "crates/dbx-web", "crates/dbx-mcp", "crates/dbx-cli", "crates/dbx-sqlite-worker"] exclude = ["vendor/ctor", "vendor/dirs-sys", "vendor/pageant", "vendor/rumqttc", "vendor/tiberius", "vendor/wry"] [patch.crates-io] diff --git a/agents/drivers/duckdb/Cargo.lock b/agents/drivers/duckdb/Cargo.lock index 8a541f141c..58f9fd4d62 100644 --- a/agents/drivers/duckdb/Cargo.lock +++ b/agents/drivers/duckdb/Cargo.lock @@ -1243,6 +1243,7 @@ dependencies = [ "cbc 0.1.2", "chrono", "csv", + "dbx-sqlite-worker", "deadpool-postgres", "encoding_rs", "flate2", @@ -1317,6 +1318,14 @@ dependencies = [ "uuid", ] +[[package]] +name = "dbx-sqlite-worker" +version = "0.1.1" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "deadpool" version = "0.12.3" diff --git a/agents/scripts/driver_release_packages_test.py b/agents/scripts/driver_release_packages_test.py index d4f54c632b..3bf76e3c1c 100644 --- a/agents/scripts/driver_release_packages_test.py +++ b/agents/scripts/driver_release_packages_test.py @@ -47,6 +47,7 @@ class DriverReleasePackagesTest(unittest.TestCase): "cassandra": "0.1.37", "hive": "0.1.43", "tdengine": "0.1.0", + "sqlite-worker": "0.1.0", } renamed = version_agent_artifacts(release_dir, versions) @@ -315,6 +316,25 @@ class DriverReleasePackagesTest(unittest.TestCase): self.assertFalse(source.exists()) self.assertEqual(versioned.read_bytes(), b"\x7fELFtest-zookeeper-agent") + def test_versions_sqlite_worker_native_artifacts(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + release_dir = Path(temp_dir) + x64 = release_dir / "dbx-agent-sqlite-worker-linux-x64" + arm = release_dir / "dbx-agent-sqlite-worker-linux-aarch64" + x64.write_bytes(b"\x7fELFtest-sqlite-worker-x64") + arm.write_bytes(b"\x7fELFtest-sqlite-worker-arm") + versions = {driver: "0.1.0" for driver in NATIVE_DRIVERS} + + renamed = version_agent_artifacts(release_dir, versions) + versioned_x64 = release_dir / "dbx-agent-sqlite-worker-0.1.0-linux-x64" + versioned_arm = release_dir / "dbx-agent-sqlite-worker-0.1.0-linux-aarch64" + + self.assertEqual(renamed, [versioned_arm, versioned_x64]) + self.assertFalse(x64.exists()) + self.assertFalse(arm.exists()) + self.assertEqual(versioned_x64.read_bytes(), b"\x7fELFtest-sqlite-worker-x64") + self.assertEqual(versioned_arm.read_bytes(), b"\x7fELFtest-sqlite-worker-arm") + def test_full_offline_bundle_includes_supported_windows_artifacts(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: release_dir = Path(temp_dir) diff --git a/agents/scripts/validate_agents.py b/agents/scripts/validate_agents.py index 604e470333..81ebe6685e 100644 --- a/agents/scripts/validate_agents.py +++ b/agents/scripts/validate_agents.py @@ -27,6 +27,9 @@ NATIVE_ONLY_AGENT_MODULES = { "rocketmq": "drivers/rocketmq", "zookeeper": "drivers/zookeeper", } +CRATE_NATIVE_AGENT_MODULES = { + "sqlite-worker": Path("..") / "crates" / "dbx-sqlite-worker", +} AUTO_VERSIONED_NATIVE_MODULES = {"duckdb"} JDBC_ARCHITECTURE_ALLOWLIST = { "h2-legacy": "reuses the H2 agent implementation with an isolated legacy driver version", @@ -73,14 +76,24 @@ def included_agent_modules(root: Path) -> set[str]: return included - INFRA_MODULES +def crate_native_modules(root: Path) -> set[str]: + return {name for name, path in CRATE_NATIVE_AGENT_MODULES.items() if (root / path).exists()} + + +def skipped_native_modules() -> set[str]: + return set(NATIVE_ONLY_AGENT_MODULES) | set(CRATE_NATIVE_AGENT_MODULES) + + def agent_modules(root: Path) -> set[str]: native = {name for name, path in NATIVE_ONLY_AGENT_MODULES.items() if (root / path).exists()} - return included_agent_modules(root) | native + return included_agent_modules(root) | native | crate_native_modules(root) def module_dir(root: Path, module: str) -> Path: if module in NATIVE_ONLY_AGENT_MODULES: return root / NATIVE_ONLY_AGENT_MODULES[module] + if module in CRATE_NATIVE_AGENT_MODULES: + return root / CRATE_NATIVE_AGENT_MODULES[module] nested = root / "drivers" / module if nested.exists(): return nested @@ -137,7 +150,7 @@ def validate_manifest_fields(root: Path, modules: set[str]) -> list[str]: root_build_text, ) for module in sorted(modules): - if module in NATIVE_ONLY_AGENT_MODULES: + if module in skipped_native_modules(): continue build_file = module_relative_path(root, module, "build.gradle") relative = build_file.relative_to(root) @@ -172,7 +185,7 @@ def validate_manifest_fields(root: Path, modules: set[str]) -> list[str]: def validate_jdbc_architecture(root: Path, modules: set[str]) -> list[str]: problems: list[str] = [] - for module in sorted(modules - NON_JDBC_AGENT_MODULES - set(NATIVE_ONLY_AGENT_MODULES)): + for module in sorted(modules - NON_JDBC_AGENT_MODULES - skipped_native_modules()): source = main_class_source(root, module) if source is None or not source.exists(): continue @@ -204,7 +217,7 @@ def validate_jdbc_pool_coverage(root: Path, modules: set[str]) -> list[str]: if match is None: return ["build.gradle: missing pooledJdbcProjects set"] pooled = set(re.findall(r"['\"]([^'\"]+)['\"]", match.group(1))) - expected = modules - NON_JDBC_AGENT_MODULES - set(NATIVE_ONLY_AGENT_MODULES) + expected = modules - NON_JDBC_AGENT_MODULES - skipped_native_modules() return ( [f"JDBC module missing pooled runtime dependency: {module}" for module in sorted(expected - pooled)] + [f"non-JDBC module listed as pooled JDBC runtime: {module}" for module in sorted(pooled - expected)] diff --git a/agents/scripts/validate_agents_test.py b/agents/scripts/validate_agents_test.py index 509992b80e..1604ee287f 100644 --- a/agents/scripts/validate_agents_test.py +++ b/agents/scripts/validate_agents_test.py @@ -48,6 +48,24 @@ class ValidateAgentsTest(unittest.TestCase): self.assertEqual([], validate_agents.validate_versions(root)) + def test_versions_accept_crate_native_sqlite_worker(self): + with tempfile.TemporaryDirectory() as tmp: + repo = Path(tmp) + root = repo / "agents" + crate = repo / "crates" / "dbx-sqlite-worker" + root.mkdir() + crate.mkdir(parents=True) + (root / "settings.gradle").write_text( + "include 'common', 'test-support', 'h2'\n", + encoding="utf-8", + ) + (root / "versions.json").write_text( + json.dumps({"h2": "0.1.0", "sqlite-worker": "0.1.0"}), + encoding="utf-8", + ) + + self.assertEqual([], validate_agents.validate_versions(root)) + def test_source_scan_rejects_old_execute_query_patterns(self): with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) diff --git a/agents/scripts/version_agent_artifacts.py b/agents/scripts/version_agent_artifacts.py index 3c5a3a3e6c..2732142298 100644 --- a/agents/scripts/version_agent_artifacts.py +++ b/agents/scripts/version_agent_artifacts.py @@ -4,7 +4,7 @@ import json from pathlib import Path -NATIVE_DRIVERS = ("cassandra", "hive", "oracle", "xugu", "kingbase", "iotdb", "neo4j", "vastbase", "duckdb", "rabbitmq", "rocketmq", "zookeeper", "tdengine") +NATIVE_DRIVERS = ("cassandra", "hive", "oracle", "xugu", "kingbase", "iotdb", "neo4j", "vastbase", "duckdb", "rabbitmq", "rocketmq", "zookeeper", "tdengine", "sqlite-worker") PLATFORMS = ( "macos-aarch64", "macos-x64", diff --git a/agents/versions.json b/agents/versions.json index 216d20d150..c385adb91e 100644 --- a/agents/versions.json +++ b/agents/versions.json @@ -25,6 +25,7 @@ "oceanbase-oracle": "0.1.47", "oracle": "0.1.56", "snowflake": "0.1.50", + "sqlite-worker": "0.1.1", "spanner": "0.1.3", "saphana": "0.1.46", "sundb": "0.1.50", diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue index accfd494db..22272f1bd6 100644 --- a/apps/desktop/src/components/connection/ConnectionDialog.vue +++ b/apps/desktop/src/components/connection/ConnectionDialog.vue @@ -55,7 +55,7 @@ import { doltSystemTablesVisible, isDoltDriverProfile, setDoltSystemTablesVisibl import { DamengJvmSystemPropertyError, damengJvmSystemPropertiesText, parseDamengJvmSystemProperties } from "@/lib/database/damengJvmOptions"; import { copyToClipboard } from "@/lib/common/clipboard"; import { configuredDatabaseProductName, connectionConfigFingerprint, databaseInfoCopyText, databaseInfoRows, normalizeDatabaseConnectionInfo, type DatabaseInfoField } from "@/lib/connection/connectionDatabaseInfo"; -import { agentDriverInstallKey, appendAgentDriverUpdateHint, hasAgentDriverUpdate, showAgentDriverInstallHint, type AgentDriverInstallState, type DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint"; +import { agentDriverInstallKey, appendAgentDriverUpdateHint, connectionUsesSsh, hasAgentDriverUpdate, showAgentDriverInstallHint, type AgentDriverInstallState, type DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint"; import { prestoSqlBuiltinDriverPaths } from "@/lib/database/prestoSqlBuiltinDriver"; import { JDBCX_DEFAULT_URL, JDBCX_DRIVER_PROFILE, JDBCX_JDBC_DRIVER_CLASS, ensureJdbcxRuntimeDrivers, isJdbcxRuntimeBundle, isJdbcxRuntimePath, jdbcxHighPrivilegeExtensionsEnabled, setJdbcxHighPrivilegeExtensionsEnabled } from "@/lib/database/jdbcxBuiltinDriver"; import { SQLITE_DATABASE_FILE_EXTENSIONS } from "@/lib/database/databaseFileDetection"; @@ -1726,7 +1726,7 @@ function errorMessage(error: unknown): string { function connectionErrorWithDriverUpdateHint(config: ConnectionConfig, message: string): string { message = appendConnectionErrorHints(config, message, t); - if (!hasAgentDriverUpdate(config.db_type, agentDrivers.value, config.driver_profile)) return message; + if (!hasAgentDriverUpdate(config.db_type, agentDrivers.value, config.driver_profile, { ssh: connectionUsesSsh(config) })) return message; return appendAgentDriverUpdateHint(message, t("connection.agentDriverUpdateConnectionHint")); } @@ -1838,11 +1838,11 @@ async function ensureRequiredAgentDriverInstalled(config: ConnectionConfig): Pro await installSqlServerLegacyCompatibilityComponentIfNeeded(); } - const driverKey = agentDriverInstallKey(config.db_type, config.driver_profile); + const driverKey = agentDriverInstallKey(config.db_type, config.driver_profile, { ssh: connectionUsesSsh(config) }); if (!driverKey) return; let drivers = agentDrivers.value.length ? agentDrivers.value : await refreshLocalAgentDrivers(); - if (!showAgentDriverInstallHint(config.db_type, drivers, config.driver_profile)) return; + if (!showAgentDriverInstallHint(config.db_type, drivers, config.driver_profile, { ssh: connectionUsesSsh(config) })) return; if (installedAgentDriver(drivers, driverKey)?.installed === true) return; drivers = await refreshLocalAgentDrivers(); @@ -2659,7 +2659,11 @@ const selectedSshLayer = computed(() => (selectedTransportLayer.value?.type === const selectedProxyLayer = computed(() => (selectedTransportLayer.value?.type === "proxy" ? selectedTransportLayer.value : null)); const selectedHttpTunnelLayer = computed(() => (selectedTransportLayer.value?.type === "http_tunnel" ? selectedTransportLayer.value : null)); -const tunnelProfiles = computed(() => tunnelProfileStore.profiles); +const tunnelProfiles = computed(() => { + const profiles = tunnelProfileStore.profiles; + if (!sqliteSshOnlyTransport.value) return profiles; + return profiles.filter((profile) => profile.type === "ssh"); +}); const selectedLayerProfileId = computed(() => selectedTransportLayer.value?.profile_id || ""); const selectedLayerProfile = computed(() => tunnelProfileStore.profileById(selectedLayerProfileId.value)); @@ -3102,10 +3106,63 @@ const zookeeperAuthScheme = computed({ resetTestState(); }, }); -const canUseTransportLayers = computed(() => form.value.db_type !== "sqlite" && form.value.db_type !== "access" && !isCloudflareD1Connection(form.value) && !isH2FileMode.value && !(form.value.db_type === "oracle" && form.value.oracle_connection_type === "tns")); -const shouldShowAgentDriverInstallHint = computed(() => showAgentDriverInstallHint(form.value.db_type, agentDrivers.value, form.value.driver_profile)); +const canUseTransportLayers = computed(() => { + if (form.value.db_type === "access" || isCloudflareD1Connection(form.value) || isH2FileMode.value || (form.value.db_type === "oracle" && form.value.oracle_connection_type === "tns")) { + return false; + } + if (form.value.db_type === "sqlite") { + return isDesktop; + } + return true; +}); +const sqliteSshOnlyTransport = computed(() => form.value.db_type === "sqlite"); +const sqliteUsesSsh = computed(() => form.value.db_type === "sqlite" && connectionUsesSsh(form.value)); +const sqliteWorkerPlacement = computed({ + get: () => getUrlParam(form.value.url_params, "dbx_sqlite_worker") || "session", + set: (value: string) => { + const next = value === "session" ? "" : value; + form.value.url_params = setUrlParam(form.value.url_params, "dbx_sqlite_worker", next); + if (value !== "preplaced" && !getUrlParam(form.value.url_params, "dbx_sqlite_worker_path")) { + return; + } + if (value === "session") { + form.value.url_params = setUrlParam(form.value.url_params, "dbx_sqlite_worker_path", ""); + } + }, +}); +const sqliteWorkerPath = computed({ + get: () => getUrlParam(form.value.url_params, "dbx_sqlite_worker_path"), + set: (value: string) => { + form.value.url_params = setUrlParam(form.value.url_params, "dbx_sqlite_worker_path", value); + }, +}); +const sqliteWorkerPlacementOptions = [ + { + value: "session", + labelKey: "connection.sqliteWorkerPlacementSession", + hintKey: "connection.sqliteWorkerPlacementSessionHint", + recommended: true, + }, + { + value: "persist", + labelKey: "connection.sqliteWorkerPlacementPersist", + hintKey: "connection.sqliteWorkerPlacementPersistHint", + recommended: false, + }, + { + value: "preplaced", + labelKey: "connection.sqliteWorkerPlacementPreplaced", + hintKey: "connection.sqliteWorkerPlacementPreplacedHint", + recommended: false, + }, +] as const; +const shouldShowSqliteSshWorkerInstallHint = computed(() => form.value.db_type === "sqlite" && sqliteUsesSsh.value && showAgentDriverInstallHint("sqlite", agentDrivers.value, form.value.driver_profile, { ssh: true })); +const shouldShowAgentDriverInstallHint = computed(() => { + if (form.value.db_type === "sqlite" && sqliteUsesSsh.value) return false; + return showAgentDriverInstallHint(form.value.db_type, agentDrivers.value, form.value.driver_profile, { ssh: sqliteUsesSsh.value }); +}); +const agentDriverFocus = computed(() => ({ target: "driver", driver: agentDriverInstallKey(form.value.db_type, form.value.driver_profile, { ssh: sqliteUsesSsh.value }) })); const h2DriverMissing = computed(() => form.value.db_type === "h2" && isH2FileMode.value && agentDrivers.value.find((d) => d.db_type === "h2")?.installed !== true); -const agentDriverFocus = computed(() => ({ target: "driver", driver: agentDriverInstallKey(form.value.db_type, form.value.driver_profile) })); const canChooseVisibleNacosNamespaces = computed(() => form.value.db_type === "nacos"); const isNacosV3AdminPlane = computed(() => nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "admin"); const isNacosV3ConsolePlane = computed(() => nacosImplementation.value === "nacos" && nacosVersionMode.value === "v3" && nacosApiPlane.value === "console"); @@ -5056,6 +5113,15 @@ watch(canUseTransportLayers, (value) => { } }); +watch(sqliteSshOnlyTransport, (sshOnly) => { + if (!sshOnly) return; + const layers = form.value.transport_layers || []; + const next = layers.filter((layer) => layer.type === "ssh"); + if (next.length === layers.length) return; + form.value.transport_layers = next; + selectedTransportLayerId.value = next[0]?.id || null; +}); + watch(supportsTlsToggle, (value) => { if (!value && configTab.value === "tls") { configTab.value = "connection"; @@ -5077,6 +5143,7 @@ function addSshTunnel() { } function addProxyTunnel() { + if (sqliteSshOnlyTransport.value) return; const next: TransportLayerConfig = { type: "proxy", ...defaultProxyTunnel() }; next.name = `Proxy ${transportLayers.value.length + 1}`; form.value.transport_layers = [...transportLayers.value, next]; @@ -5085,6 +5152,7 @@ function addProxyTunnel() { } function addHttpTunnel() { + if (sqliteSshOnlyTransport.value) return; const next: TransportLayerConfig = { type: "http_tunnel", ...defaultHttpTunnel() }; next.name = t("connection.httpTunnelDefaultName", { index: 1 }); form.value.transport_layers = [next, ...transportLayers.value]; @@ -5093,6 +5161,7 @@ function addHttpTunnel() { } function duplicateTransportLayer(layer: TransportLayerConfig) { + if (sqliteSshOnlyTransport.value && layer.type !== "ssh") return; const next = normalizeTransportLayer({ ...layer, id: uuid(), name: layer.name ? `${layer.name} copy` : "" }); form.value.transport_layers = [...transportLayers.value, next]; selectedTransportLayerId.value = next.id; @@ -5132,6 +5201,7 @@ function dropTransportLayer(targetId: string) { function changeSelectedTransportLayerType(type: "ssh" | "proxy" | "http_tunnel") { const selected = selectedTransportLayer.value; if (!selected || selected.type === type) return; + if (sqliteSshOnlyTransport.value && type !== "ssh") return; const replacement: TransportLayerConfig = type === "proxy" ? { type: "proxy", ...defaultProxyTunnel(), id: selected.id, name: selected.name } : type === "http_tunnel" ? { type: "http_tunnel", ...defaultHttpTunnel(), id: selected.id, name: selected.name } : { type: "ssh", ...defaultSshTunnel(), id: selected.id, name: selected.name }; form.value.transport_layers = transportLayers.value.map((layer) => (layer.id === selected.id ? replacement : layer)); @@ -5154,6 +5224,9 @@ function updateSelectedSshAuthMethod(value: unknown) { function validateTransportLayers(config: LegacyConnectionConfig) { const layers = config.transport_layers || []; + if (config.db_type === "sqlite" && layers.some((layer) => layer.enabled !== false && layer.type !== "ssh")) { + throw new Error(t("connection.sqliteTransportSshOnly")); + } layers.forEach((layer, index) => { if (layer.enabled === false) return; // Profile-referencing layers are stubs: the shared profile supplies the @@ -6119,10 +6192,10 @@ function openExternalUrl(url: string) {
- + - @@ -6138,23 +6211,54 @@ function openExternalUrl(url: string) { - {{ t("connection.createSqliteFile") }}
-

+

+ {{ t("connection.sqliteRemotePathHint") }} +

+

{{ t("connection.memoryDatabasePathHint") }}

-
+
+ +
+
+ + + + + +
+ {{ t(option.labelKey) }} + {{ t("connection.sqliteWorkerPlacementDefault") }} +
+

{{ t(option.hintKey) }}

+
+
+
+ +

+ {{ t("connection.driverInstallHintPrefix") }}{{ t("toolbar.driverManager") }}{{ t("connection.driverInstallHintSuffix") }} +

+
+
+
-
+
@@ -8419,11 +8523,11 @@ function openExternalUrl(url: string) { {{ t("connection.sshHopAdd") }} - - @@ -8472,7 +8576,7 @@ function openExternalUrl(url: string) { {{ t("connection.tunnelProfileMissing") }}
-
+
- {{ t("connection.sshHostKeyVerifyRemember") }} + {{ current.kind === "WorkerUploadConsent" ? t("connection.sshWorkerUploadConsentRemember") : t("connection.sshHostKeyVerifyRemember") }}
@@ -289,12 +290,12 @@ function submitSecret() {
- +