refactor(workspace): split SQL and database driver crates

This commit is contained in:
t8y2
2026-09-25 15:51:58 +08:00
parent b99c60aff6
commit e0da2d09d3
180 changed files with 1422 additions and 538 deletions
+9 -2
View File
@@ -1,6 +1,13 @@
export const rustGroups = {
foundation: ["dbx-types", "dbx-platform", "dbx-sql", "dbx-formats", "dbx-ai-provider", "dbx-plugin-runtime"],
drivers: ["dbx-drivers", "dbx-sqlite-worker"],
foundation: [
"dbx-types", "dbx-platform", "dbx-sql-core", "dbx-sql-dialect", "dbx-sql-data", "dbx-sql-schema", "dbx-sql",
"dbx-formats", "dbx-ai-provider", "dbx-plugin-runtime",
],
drivers: [
"dbx-driver-support", "dbx-driver-agent", "dbx-driver-elasticsearch", "dbx-driver-mongodb",
"dbx-driver-mysql", "dbx-driver-postgres", "dbx-driver-redis", "dbx-driver-sqlserver", "dbx-drivers",
"dbx-sqlite-worker",
],
application: ["dbx", "dbx-core", "dbx-web", "dbx-cli", "dbx-mcp"],
};
+2 -2
View File
@@ -42,7 +42,7 @@ export function planCi({ files, metadata, root, eventName = "pull_request", rust
if (owner) affected.add(owner[0]);
else if (/^(?:crates|src-tauri)\//.test(file)) unknownRust = true;
if (file.startsWith("plugins/connection-types/")) affected.add("dbx-types");
if (file.startsWith("plugins/dialects/")) affected.add("dbx-sql");
if (file.startsWith("plugins/dialects/")) affected.add("dbx-sql-dialect");
}
const knownGroups = new Set(Object.values(rustGroups).flat());
const unknownMember = packages.some((pkg) => !knownGroups.has(pkg.name));
@@ -72,7 +72,7 @@ export function planCi({ files, metadata, root, eventName = "pull_request", rust
&& !nativeDrivers.some((driver) => file.startsWith(`agents/drivers/${driver}/`))
&& !jdbcDrivers.some((driver) => file.startsWith(`agents/drivers/${driver}/`)))
|| files.some((file) => file.startsWith(".github/scripts/bump-agent-versions.") || file === ".github/workflows/agents-release.yml"
|| file === "crates/dbx-drivers/assets/agent-protocol-v2.json");
|| file === "crates/dbx-driver-agent/assets/agent-protocol-v2.json");
const allAgents = sharedAgents || (agentsChanged && nativeChanges.size === 0 && !javaDriverChanges);
if (allAgents) {
for (const driver of nativeDrivers) nativeChanges.add(driver);
+17 -4
View File
@@ -12,9 +12,22 @@ import { assertCoverage, parseCoverage } from "./ci-rust-coverage.mjs";
const root = "/fixture";
const graph = {
"dbx-types": [], "dbx-platform": [], "dbx-formats": [], "dbx-sql": ["dbx-types"],
"dbx-types": [], "dbx-platform": [], "dbx-formats": [], "dbx-sql-core": ["dbx-types"],
"dbx-sql-dialect": ["dbx-types"], "dbx-sql-data": ["dbx-sql-core", "dbx-sql-dialect", "dbx-types"],
"dbx-sql-schema": ["dbx-sql-core", "dbx-sql-dialect", "dbx-types"],
"dbx-sql": ["dbx-sql-core", "dbx-sql-data", "dbx-sql-dialect", "dbx-sql-schema", "dbx-types"],
"dbx-ai-provider": ["dbx-platform"], "dbx-plugin-runtime": ["dbx-platform", "dbx-types"],
"dbx-sqlite-worker": [], "dbx-drivers": ["dbx-platform", "dbx-sql", "dbx-types", "dbx-sqlite-worker"],
"dbx-driver-support": ["dbx-platform", "dbx-types"],
"dbx-driver-agent": ["dbx-platform", "dbx-sql-core", "dbx-types"],
"dbx-driver-elasticsearch": ["dbx-driver-support", "dbx-types"],
"dbx-driver-mongodb": ["dbx-driver-support", "dbx-types"],
"dbx-driver-mysql": ["dbx-driver-support", "dbx-sql-core", "dbx-sql-dialect", "dbx-types"],
"dbx-driver-postgres": ["dbx-driver-support", "dbx-sql-core", "dbx-types"],
"dbx-driver-redis": ["dbx-driver-support", "dbx-types"],
"dbx-driver-sqlserver": ["dbx-driver-support", "dbx-sql-core", "dbx-sql-data", "dbx-types"],
"dbx-sqlite-worker": [], "dbx-drivers": ["dbx-driver-agent", "dbx-driver-elasticsearch", "dbx-driver-mongodb",
"dbx-driver-mysql", "dbx-driver-postgres", "dbx-driver-redis", "dbx-driver-sqlserver", "dbx-driver-support",
"dbx-platform", "dbx-sql-core", "dbx-sql-data", "dbx-sql-dialect", "dbx-types", "dbx-sqlite-worker"],
"dbx-core": ["dbx-drivers", "dbx-sql", "dbx-types", "dbx-platform", "dbx-ai-provider", "dbx-plugin-runtime", "dbx-formats"],
"dbx-mcp": ["dbx-core"], "dbx-cli": ["dbx-core", "dbx-mcp"], "dbx-web": ["dbx-core", "dbx-mcp"], "dbx": ["dbx-core", "dbx-mcp"],
};
@@ -51,7 +64,7 @@ test("core and consumer changes do not pull unrelated foundation test groups", (
const cli = plan(["crates/dbx-cli/src/main.rs"]);
assert.deepEqual(groups(cli), ["application"]);
assert.equal(cli.agents, false);
assert.deepEqual(groups(plan(["crates/dbx-drivers/src/db/postgres.rs"])), ["drivers", "application"]);
assert.deepEqual(groups(plan(["crates/dbx-driver-postgres/src/postgres.rs"])), ["drivers", "application"]);
});
test("code generation and test-only dependencies participate in impact analysis", () => {
@@ -123,7 +136,7 @@ test("known JDBC driver changes only select Java agent tests", () => {
test("shared Agent inputs and unknown native modules never silently lose coverage", () => {
for (const file of ["agents/common/src/main/java/Protocol.java", "agents/scripts/validate_agents.py", "agents/build.gradle",
"agents/drivers/new-driver/main.go", "crates/dbx-drivers/assets/agent-protocol-v2.json", ".github/workflows/agents-release.yml"]) {
"agents/drivers/new-driver/main.go", "crates/dbx-driver-agent/assets/agent-protocol-v2.json", ".github/workflows/agents-release.yml"]) {
const result = plan([file]);
assert.equal(result.agent_go.include.length, 10, file);
assert.equal(result.agent_integration.include.length, 16, file);
+3
View File
@@ -12,9 +12,12 @@ export function rustCommand(action, group, mode) {
if (mode === "full") capabilityFeatures.push("system-fonts");
const appFeatures = ["dbx", "dbx-core", "dbx-web"].flatMap((name) => capabilityFeatures.map((feature) => `${name}/${feature}`));
const foundationFeatures = ["dbx-types/mq-admin", "dbx-types/openapi", "dbx-sql/duckdb-sidecar", "dbx-sql/openapi",
"dbx-sql-data/openapi", "dbx-sql-schema/duckdb-sidecar",
"dbx-platform/downloads", "dbx-platform/host-prompts", "dbx-platform/test-support", "dbx-plugin-runtime/default", "dbx-plugin-runtime/test-support"];
const features = group === "foundation" ? foundationFeatures : group === "drivers" ? [
...["duckdb-sidecar", "dynamodb", "mq-admin", "sqlite-bundled", "sqlite-sqlcipher", "test-support"].map((feature) => `dbx-drivers/${feature}`),
"dbx-driver-agent/test-support", "dbx-driver-mysql/test-support", "dbx-driver-redis/test-support",
"dbx-driver-support/test-support",
"dbx-sqlite-worker/runtime", "dbx-types/openapi", "dbx-sql/openapi",
] : appFeatures;
const packages = group === "workspace" ? ["--workspace"] : rustGroups[group].flatMap((name) => ["--package", name]);
+1 -1
View File
@@ -30,7 +30,7 @@ export const STATIC_LABEL_SPECS = {
"tests-only": { color: "bfd4f2", description: "Changes only tests, fixtures, or snapshots" },
};
const CORE_CRATE_PATH = /^crates\/dbx-(?:core|drivers|sql|formats|types|platform|ai-provider|plugin-runtime)\//;
const CORE_CRATE_PATH = /^crates\/dbx-(?:core|driver(?:s|-[^/]+)|sql(?:-[^/]+)?|formats|types|platform|ai-provider|plugin-runtime)\//;
const TYPE_LABELS = new Set(["bug", "enhancement", "documentation", "maintenance"]);
const LABEL_PALETTE = [
+3 -3
View File
@@ -108,8 +108,8 @@ test("recognizes extracted core crates and their database sources", () => {
const result = evaluatePullRequestLabels({
title: "fix(sql): preserve PostgreSQL query contracts",
changedFiles: [
"crates/dbx-drivers/src/db/postgres.rs",
"crates/dbx-sql/src/sql.rs",
"crates/dbx-driver-postgres/src/postgres.rs",
"crates/dbx-sql-core/src/sql.rs",
"crates/dbx-types/src/types.rs",
"crates/dbx-platform/src/process.rs",
"crates/dbx-ai-provider/src/ai.rs",
@@ -129,7 +129,7 @@ test("collapses broad area and database changes", () => {
changedFiles: [
"agents/drivers/mysql/build.gradle",
"apps/desktop/src/components/grid/GeometryViewer.vue",
"crates/dbx-drivers/src/db/postgres.rs",
"crates/dbx-driver-postgres/src/postgres.rs",
"crates/dbx-mcp/src/main.rs",
"crates/dbx-web/src/main.rs",
"docs/content/docs/geometry.mdx",
+4 -2
View File
@@ -960,7 +960,8 @@ jobs:
- 'scripts/run-check.mjs'
- 'crates/dbx-core/src/**'
- 'crates/dbx-drivers/src/**'
- 'crates/dbx-sql/src/**'
- 'crates/dbx-driver-*/src/**'
- 'crates/dbx-sql*/src/**'
- 'crates/dbx-formats/src/**'
- 'crates/dbx-types/src/**'
- 'crates/dbx-plugin-runtime/src/**'
@@ -1002,7 +1003,7 @@ jobs:
- '.github/workflows/ci.yml'
agents:
- 'agents/**'
- 'crates/dbx-drivers/assets/agent-protocol-v2.json'
- 'crates/dbx-driver-agent/assets/agent-protocol-v2.json'
- 'crates/dbx-core/Cargo.toml'
- 'Cargo.toml'
- 'Cargo.lock'
@@ -1045,6 +1046,7 @@ jobs:
- 'src-tauri/src/commands/update.rs'
- 'crates/dbx-core/Cargo.toml'
- 'crates/dbx-drivers/**'
- 'crates/dbx-driver-*/**'
- 'crates/dbx-platform/**'
- 'crates/dbx-core/src/host/update.rs'
- 'Cargo.toml'
Generated
+232 -9
View File
@@ -2212,6 +2212,172 @@ dependencies = [
"zstd",
]
[[package]]
name = "dbx-driver-agent"
version = "0.1.0"
dependencies = [
"chrono",
"crc",
"dbx-platform",
"dbx-sql-core",
"dbx-types",
"flate2",
"futures",
"log",
"percent-encoding",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2 0.10.9",
"tar",
"tempfile",
"tokio",
"tokio-util",
"uuid",
"zip",
"zstd",
]
[[package]]
name = "dbx-driver-elasticsearch"
version = "0.1.0"
dependencies = [
"dbx-driver-support",
"dbx-types",
"percent-encoding",
"regex",
"reqwest 0.12.28",
"serde",
"serde_json",
"tokio",
"uuid",
]
[[package]]
name = "dbx-driver-mongodb"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"chrono",
"dbx-driver-support",
"dbx-types",
"futures",
"json5",
"log",
"mongodb",
"percent-encoding",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2 0.10.9",
"tokio",
"uuid",
]
[[package]]
name = "dbx-driver-mysql"
version = "0.1.0"
dependencies = [
"chrono",
"dbx-driver-support",
"dbx-sql-core",
"dbx-sql-dialect",
"dbx-types",
"futures",
"log",
"mysql_async",
"mysql_common",
"percent-encoding",
"rust_decimal",
"serde_json",
"sqlparser",
"tokio",
"tokio-util",
]
[[package]]
name = "dbx-driver-postgres"
version = "0.1.0"
dependencies = [
"bytes",
"chrono",
"dbx-driver-support",
"dbx-sql-core",
"dbx-types",
"deadpool-postgres",
"futures",
"log",
"openssl",
"percent-encoding",
"portpicker",
"postgres-openssl",
"rustls 0.23.43",
"rustls-pemfile 2.2.0",
"serde_json",
"sqlparser",
"tempfile",
"tokio",
"tokio-openssl",
"tokio-postgres",
"tokio-postgres-rustls",
"tokio-util",
"uuid",
"webpki-roots 0.26.11",
]
[[package]]
name = "dbx-driver-redis"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"dbx-driver-support",
"dbx-types",
"redis",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "dbx-driver-sqlserver"
version = "0.1.0"
dependencies = [
"chrono",
"dbx-driver-support",
"dbx-sql-core",
"dbx-sql-data",
"dbx-types",
"futures",
"log",
"serde",
"serde_json",
"sqlparser",
"tiberius",
"tokio",
"tokio-util",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "dbx-driver-support"
version = "0.1.0"
dependencies = [
"bytes",
"dbx-platform",
"dbx-types",
"portpicker",
"reqwest 0.12.28",
"russh",
"serde",
"serde_json",
"sqlparser",
"tokio",
"tokio-util",
"uuid",
]
[[package]]
name = "dbx-drivers"
version = "0.1.0"
@@ -2222,8 +2388,18 @@ dependencies = [
"chrono",
"crc",
"csv",
"dbx-driver-agent",
"dbx-driver-elasticsearch",
"dbx-driver-mongodb",
"dbx-driver-mysql",
"dbx-driver-postgres",
"dbx-driver-redis",
"dbx-driver-sqlserver",
"dbx-driver-support",
"dbx-platform",
"dbx-sql",
"dbx-sql-core",
"dbx-sql-data",
"dbx-sql-dialect",
"dbx-sqlite-worker",
"dbx-types",
"deadpool-postgres",
@@ -2232,7 +2408,6 @@ dependencies = [
"json5",
"libsqlite3-hotbundle",
"log",
"mongodb",
"mysql_async",
"mysql_common",
"nix 0.31.3",
@@ -2241,7 +2416,6 @@ dependencies = [
"percent-encoding",
"portpicker",
"postgres-openssl",
"redis",
"regex",
"reqwest 0.12.28",
"rusqlite",
@@ -2350,14 +2524,69 @@ dependencies = [
[[package]]
name = "dbx-sql"
version = "0.1.0"
dependencies = [
"dbx-sql-core",
"dbx-sql-data",
"dbx-sql-dialect",
"dbx-sql-schema",
"dbx-types",
]
[[package]]
name = "dbx-sql-core"
version = "0.1.0"
dependencies = [
"chrono",
"dbx-types",
"encoding_rs",
"log",
"regex",
"serde",
"serde_json",
"sqlparser",
"unicode-ident",
"winnow 1.0.4",
]
[[package]]
name = "dbx-sql-data"
version = "0.1.0"
dependencies = [
"chrono",
"dbx-sql-core",
"dbx-sql-dialect",
"dbx-types",
"serde",
"serde_json",
"sqlparser",
"unicode-width",
"utoipa",
]
[[package]]
name = "dbx-sql-dialect"
version = "0.1.0"
dependencies = [
"chrono",
"dbx-types",
"insta",
"log",
"notify",
"percent-encoding",
"serde",
"serde_yaml",
"tempfile",
"tokio",
]
[[package]]
name = "dbx-sql-schema"
version = "0.1.0"
dependencies = [
"dbx-sql-core",
"dbx-sql-dialect",
"dbx-types",
"log",
"rayon",
"regex",
"rusqlite",
@@ -2365,12 +2594,6 @@ dependencies = [
"serde_json",
"serde_yaml",
"sqlparser",
"tempfile",
"tokio",
"unicode-ident",
"unicode-width",
"utoipa",
"winnow 1.0.4",
]
[[package]]
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["src-tauri", "crates/dbx-core", "crates/dbx-drivers", "crates/dbx-sql", "crates/dbx-plugin-runtime", "crates/dbx-formats", "crates/dbx-types", "crates/dbx-platform", "crates/dbx-ai-provider", "crates/dbx-web", "crates/dbx-mcp", "crates/dbx-cli", "crates/dbx-sqlite-worker"]
members = ["src-tauri", "crates/dbx-core", "crates/dbx-driver-agent", "crates/dbx-driver-support", "crates/dbx-driver-elasticsearch", "crates/dbx-driver-mongodb", "crates/dbx-driver-mysql", "crates/dbx-driver-postgres", "crates/dbx-driver-redis", "crates/dbx-driver-sqlserver", "crates/dbx-drivers", "crates/dbx-sql", "crates/dbx-sql-core", "crates/dbx-sql-data", "crates/dbx-sql-dialect", "crates/dbx-sql-schema", "crates/dbx-plugin-runtime", "crates/dbx-formats", "crates/dbx-types", "crates/dbx-platform", "crates/dbx-ai-provider", "crates/dbx-web", "crates/dbx-mcp", "crates/dbx-cli", "crates/dbx-sqlite-worker"]
exclude = ["vendor/tauri-plugin-updater", "vendor/ctor", "vendor/dirs-sys", "vendor/pageant", "vendor/rumqttc", "vendor/tiberius", "vendor/webview2-com-sys", "vendor/wry"]
[patch.crates-io]
@@ -7,7 +7,7 @@ import java.util.List;
/**
* Decode PostGIS EWKB / WKB byte streams (and PGgeometry / hex strings) into
* OGC Well-Known Text. Behaves identically to the Rust implementation in
* {@code crates/dbx-drivers/src/db/postgres.rs::ewkb_to_wkt}, including the
* {@code crates/dbx-driver-support/src/wkb.rs::wkb_to_wkt}, including the
* {@code Z} / {@code M} / {@code ZM} dimension suffixes and {@code EMPTY}
* geometries. Used by the JDBC agent layer so that PostgresLike databases
* such as HighGo / KingBase / Vastbase / openGauss / GaussDB return WKT for
@@ -79,7 +79,7 @@ public abstract class PostgresLikeAgent extends AbstractJdbcAgent {
/**
* Wrap {@link AbstractJdbcAgent#resultValue} so PostGIS-style {@code geometry}
* and {@code geography} columns are decoded into WKT (matching the native
* tokio_postgres path in {@code crates/dbx-drivers/src/db/postgres.rs}).
* tokio_postgres path in {@code crates/dbx-driver-postgres/src/postgres.rs}).
*/
private JdbcExecutor.ColumnAwareResultValueReader geometryAwareResolver() {
return (rs, index, sqlType, columnTypeName) -> {
@@ -22,7 +22,7 @@ class EwkbWktDecoderTest {
// ──────────────────────────────────────────────────────────────────────
// Cross-checked against the Rust ewkb_to_wkt tests in
// crates/dbx-drivers/src/db/postgres.rs (same hex inputs, same expected WKT).
// crates/dbx-driver-support/src/wkb.rs (same hex inputs, same expected WKT).
// ──────────────────────────────────────────────────────────────────────
@Test
+225 -7
View File
@@ -1316,6 +1316,167 @@ dependencies = [
"zip 4.6.1",
]
[[package]]
name = "dbx-driver-agent"
version = "0.1.0"
dependencies = [
"chrono",
"crc",
"dbx-platform",
"dbx-sql-core",
"dbx-types",
"flate2",
"futures",
"log",
"percent-encoding",
"reqwest",
"serde",
"serde_json",
"sha2 0.10.9",
"tar",
"tempfile",
"tokio",
"tokio-util",
"uuid",
"zip 4.6.1",
"zstd",
]
[[package]]
name = "dbx-driver-elasticsearch"
version = "0.1.0"
dependencies = [
"dbx-driver-support",
"dbx-types",
"percent-encoding",
"regex",
"reqwest",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "dbx-driver-mongodb"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"chrono",
"dbx-driver-support",
"dbx-types",
"futures",
"json5",
"log",
"mongodb",
"percent-encoding",
"reqwest",
"serde",
"serde_json",
"sha2 0.10.9",
"tokio",
"uuid",
]
[[package]]
name = "dbx-driver-mysql"
version = "0.1.0"
dependencies = [
"chrono",
"dbx-driver-support",
"dbx-sql-core",
"dbx-sql-dialect",
"dbx-types",
"futures",
"log",
"mysql_async",
"percent-encoding",
"rust_decimal",
"serde_json",
"sqlparser",
"tokio",
"tokio-util",
]
[[package]]
name = "dbx-driver-postgres"
version = "0.1.0"
dependencies = [
"bytes",
"chrono",
"dbx-driver-support",
"dbx-sql-core",
"dbx-types",
"deadpool-postgres",
"futures",
"log",
"openssl",
"percent-encoding",
"postgres-openssl",
"rustls 0.23.42",
"rustls-pemfile 2.2.0",
"serde_json",
"sqlparser",
"tokio",
"tokio-postgres",
"tokio-postgres-rustls",
"tokio-util",
"uuid",
"webpki-roots 0.26.11",
]
[[package]]
name = "dbx-driver-redis"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"dbx-driver-support",
"dbx-types",
"redis",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "dbx-driver-sqlserver"
version = "0.1.0"
dependencies = [
"chrono",
"dbx-driver-support",
"dbx-sql-core",
"dbx-sql-data",
"dbx-types",
"futures",
"log",
"serde",
"serde_json",
"sqlparser",
"tiberius",
"tokio",
"tokio-util",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]]
name = "dbx-driver-support"
version = "0.1.0"
dependencies = [
"bytes",
"dbx-platform",
"dbx-types",
"portpicker",
"reqwest",
"russh",
"serde",
"serde_json",
"sqlparser",
"tokio",
"tokio-util",
"uuid",
]
[[package]]
name = "dbx-drivers"
version = "0.1.0"
@@ -1325,8 +1486,18 @@ dependencies = [
"chrono",
"crc",
"csv",
"dbx-driver-agent",
"dbx-driver-elasticsearch",
"dbx-driver-mongodb",
"dbx-driver-mysql",
"dbx-driver-postgres",
"dbx-driver-redis",
"dbx-driver-sqlserver",
"dbx-driver-support",
"dbx-platform",
"dbx-sql",
"dbx-sql-core",
"dbx-sql-data",
"dbx-sql-dialect",
"dbx-sqlite-worker",
"dbx-types",
"deadpool-postgres",
@@ -1334,14 +1505,12 @@ dependencies = [
"futures",
"json5",
"log",
"mongodb",
"mysql_async",
"openssl",
"pageant",
"percent-encoding",
"portpicker",
"postgres-openssl",
"redis",
"regex",
"reqwest",
"rusqlite",
@@ -1434,23 +1603,72 @@ dependencies = [
[[package]]
name = "dbx-sql"
version = "0.1.0"
dependencies = [
"dbx-sql-core",
"dbx-sql-data",
"dbx-sql-dialect",
"dbx-sql-schema",
"dbx-types",
]
[[package]]
name = "dbx-sql-core"
version = "0.1.0"
dependencies = [
"chrono",
"dbx-types",
"encoding_rs",
"log",
"regex",
"serde",
"serde_json",
"sqlparser",
"unicode-ident",
"winnow",
]
[[package]]
name = "dbx-sql-data"
version = "0.1.0"
dependencies = [
"chrono",
"dbx-sql-core",
"dbx-sql-dialect",
"dbx-types",
"serde",
"serde_json",
"sqlparser",
"unicode-width",
]
[[package]]
name = "dbx-sql-dialect"
version = "0.1.0"
dependencies = [
"chrono",
"dbx-types",
"log",
"notify",
"percent-encoding",
"serde",
"serde_yaml",
"tokio",
]
[[package]]
name = "dbx-sql-schema"
version = "0.1.0"
dependencies = [
"dbx-sql-core",
"dbx-sql-dialect",
"dbx-types",
"log",
"rayon",
"regex",
"serde",
"serde_json",
"serde_yaml",
"sqlparser",
"tokio",
"unicode-ident",
"unicode-width",
"winnow",
]
[[package]]
@@ -1003,7 +1003,7 @@ describe("RedisKeyBrowser continuation ownership (issue #7779)", () => {
// `iterations` (aka `max_iterations`) is the 6th positional arg the frontend
// sends to `redisScanKeysBatch` — the same unit the backend spends as real
// Redis SCAN calls (see `crates/dbx-drivers/src/db/redis_driver.rs`).
// Redis SCAN calls (see `crates/dbx-driver-redis/src/lib.rs`).
function totalIterationsRequested(): number {
return mocks.redisScanKeysBatch.mock.calls.reduce((sum: number, call: unknown[]) => sum + (call[5] as number), 0);
}
@@ -220,7 +220,7 @@ describe("useSidebarTableMutationRuntime SAP HANA schema-scoped actions", () =>
it("classifies a JDBC-agent-routed timeout (SAP HANA/Oracle/DB2/SQL Server) as timed-out, not failed, even though its message has no timeout wording", async () => {
const { feature } = runtime("");
// AgentCallError::Timeout carries no `detail` (crates/dbx-drivers/src/backend_error.rs),
// AgentCallError::Timeout carries no `detail` (crates/dbx-driver-agent/src/backend_error.rs),
// so BackendErrorException.message degrades to a generic fallback with no
// "timed out" text — only the structured backendError distinguishes it.
const agentTimeoutError = {
@@ -2,7 +2,7 @@
* Issue #6189 — SQL Server "focus jumps to the message result".
*
* Background:
* - `crates/dbx-drivers/src/db/sqlserver.rs:523-542` synthesizes a pseudo result with a
* - `crates/dbx-driver-sqlserver/src/sqlserver.rs` synthesizes a pseudo result with a
* single "Message" column and `server_message: true` for any batch segment that
* produced only server messages (PRINT / "DBCC execution completed" / ...).
* - `stores/queryStore.ts:4727` then picks the first result that HAS COLUMNS as the
+4 -4
View File
@@ -48,7 +48,7 @@ const STRUCTURED_BACKEND_ERROR_KEYS = [
"backendErrors.unknown",
] as const;
// Reproduces the exact string crates/dbx-drivers/src/agent_service.rs builds on
// Reproduces the exact string crates/dbx-driver-agent/src/agent_service.rs builds on
// Windows: `\` line continuations strip the newline plus the following indent.
const WINDOWS_JRE_REMOVE_ERROR = [
"Failed to remove the old JRE directory: C:\\dbx\\jre21",
@@ -644,9 +644,9 @@ describe("backend error wording is pinned to the Rust sources", () => {
["crates/dbx-core/src/data/mongodb_import_export.rs", "MongoDB Legacy Agent does not support insertMany; upgrade or reinstall the MongoDB Legacy driver"],
["crates/dbx-core/src/data/mongodb_import_export.rs", "MongoDB Legacy Agent returned an invalid find cursor"],
["crates/dbx-core/src/query/mongo_ops.rs", "MongoDB Legacy Agent rejected "],
["crates/dbx-drivers/src/agent_service.rs", "Failed to remove the old JRE directory: "],
["crates/dbx-drivers/src/agent_service.rs", "is in use by drivers: "],
["crates/dbx-drivers/src/agent_service.rs", "agent-registry.json not found in the ZIP; not a valid offline driver package."],
["crates/dbx-driver-agent/src/agent_service.rs", "Failed to remove the old JRE directory: "],
["crates/dbx-driver-agent/src/agent_service.rs", "is in use by drivers: "],
["crates/dbx-driver-agent/src/agent_service.rs", "agent-registry.json not found in the ZIP; not a valid offline driver package."],
["crates/dbx-core/src/admin/mq/adapters/kafka.rs", "Kafka does not support unloading topics"],
["crates/dbx-plugin-runtime/src/plugins/installer.rs", "Plugin update source change requires confirmation:"],
["crates/dbx-plugin-runtime/src/plugins/installer.rs", "Plugin downgrade to version "],
+1 -1
View File
@@ -149,7 +149,7 @@ const patterns: [RegExp, string][] = [
// Legacy bundled DuckDB error kept for compatibility with older backends.
[/^The previous DuckDB query is still stopping\. Please try again shortly\.$/, "editor.duckdbDraining"],
// Driver / JRE management (crates/dbx-drivers/src/agent_service.rs, routes/agents.rs)
// Driver / JRE management (crates/dbx-driver-agent/src/agent_service.rs, routes/agents.rs)
// The Windows variant is multi-line, so it must be tried before the single-line one.
[/^Failed to remove the old JRE directory: (.+)\nPossible causes:[\s\S]*\(original error: ([\s\S]+)\)$/, "driverStore.jreDirRemoveFailedWindows"],
[/^Failed to remove the old JRE directory: (.+) \(original error: ([\s\S]+)\)$/, "driverStore.jreDirRemoveFailed"],
+1 -1
View File
@@ -7,7 +7,7 @@
- `app`, `tabs`, `sidebar`, `connection`: shell, navigation, and connection UI state helpers.
- `database`, `metadata`, `schema`, `table`: relational database metadata, capabilities, DDL, and table-object helpers.
- `sql`, `sql/semantic`, `editor`, `query`, `history`, `savedSql`: SQL editing, execution, diagnostics, history, and saved SQL behavior.
Keep fixed SQL completion statements and their database-specific variants in `sql/sqlSnippetTemplates.ts`; cross-check row-limiting families against `crates/dbx-sql/src/sql_dialect/capabilities.rs` and authoritative dialect documentation, and never replace a user-customized built-in body with a generated default.
Keep fixed SQL completion statements and their database-specific variants in `sql/sqlSnippetTemplates.ts`; cross-check row-limiting families against `crates/dbx-sql-dialect/src/sql_dialect/capabilities.rs` and authoritative dialect documentation, and never replace a user-customized built-in body with a generated default.
- `dataGrid`: result/grid rendering, editing, previews, pagination, and export helpers tied to the grid.
- `ai`, `mcp`: AI assistant and MCP configuration helpers.
- `redis`, `mongo`, `elasticsearch`, `etcd`, `kv`, `mq`, `nacos`, `zookeeper`, `webdav`: non-relational or service-specific helpers.
@@ -69,7 +69,7 @@ describe("databaseObjectCapabilities", () => {
it("exposes materialized views for StarRocks only", () => {
// StarRocks has a dedicated MV listing/classification path in
// crates/dbx-drivers/src/db/mysql.rs (`list_starrocks_tables` +
// crates/dbx-driver-mysql/src/mysql.rs (`list_starrocks_tables` +
// `classify_starrocks_materialized_views`).
expect(sidebarObjectKindsForDatabase("starrocks")).toContain("MATERIALIZED_VIEW");
@@ -2,7 +2,7 @@
* Read/write classification for Elasticsearch-compatible REST requests.
*
* Mirrors `classify_search_engine_query_risk` in
* `crates/dbx-sql/src/query_execution_sql.rs` so the desktop guards (read-only
* `crates/dbx-sql-core/src/query_execution_sql.rs` so the desktop guards (read-only
* unlock, production safety) agree with the backend read-only gate instead of
* treating every `GET`/`POST` request as an unrecognized — and therefore
* unsafe — statement.
@@ -1,4 +1,4 @@
// Mirrors dbx-core `redis_database_index()` (crates/dbx-drivers/src/db/redis_driver.rs):
// Mirrors dbx-core `redis_database_index()` (crates/dbx-driver-redis/src/lib.rs):
// a Redis database is a numeric index. Anything else (e.g. redis-cli flags like
// "0 --tls --insecure" pasted into the field or imported from a dirty URL) fails
// to parse on the backend and silently falls back to db 0, so every frontend
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Read/write classification for Apache Solr REST requests.
*
* Mirrors `classify_solr_query_risk` in `crates/dbx-sql/src/query_execution_sql.rs`
* Mirrors `classify_solr_query_risk` in `crates/dbx-sql-core/src/query_execution_sql.rs`
* so the desktop guards (read-only unlock, production safety) agree with the
* backend read-only gate instead of treating every request as unrecognized.
*
+1 -1
View File
@@ -151,7 +151,7 @@ export function resolveNewQueryTable(input: ResolveNewQueryTableInput): NewQuery
*
* Time-series engines get the same rolling-window scoping the sidebar
* quick-open uses (see `default_time_series_predicate` in
* `crates/dbx-sql/src/sql_dialect/table_select.rs`). Without a `time`
* `crates/dbx-sql-dialect/src/sql_dialect/table_select.rs`). Without a `time`
* predicate InfluxDB scans every shard (v1/v2) or every Parquet file
* (v3), which turns "let me draft a query against this table" into a
* full-history scan the moment the user hits Run. VictoriaMetrics
+1 -1
View File
@@ -212,7 +212,7 @@ fn exports_schema_wide_objects(request: &DatabaseExportRequest) -> bool {
}
/// MySQL lists triggers and events as schema-wide objects (see
/// `crates/dbx-drivers/src/db/mysql.rs`), which is what the export writes out. Other
/// `crates/dbx-driver-mysql/src/mysql.rs`), which is what the export writes out. Other
/// engines either report triggers per table (PostgreSQL) or not at all, so their export
/// stays unchanged.
fn exports_mysql_trigger_objects(db_type: DatabaseType) -> bool {
+1 -1
View File
@@ -2249,7 +2249,7 @@ fn postgres_index_column_sql(
) -> String {
// The base key text: a real column is quoted as an identifier; an expression/functional
// key part arrives as raw expression text (the per-column `pg_get_indexdef` omits the
// opclass — see `crates/dbx-drivers/src/db/postgres.rs`), so quoting the whole thing as
// opclass — see `crates/dbx-driver-postgres/src/postgres.rs`), so quoting the whole thing as
// an identifier would turn it into a nonexistent column reference (#6295).
let base = if is_expression { column.to_string() } else { quote_identifier(column, &DatabaseType::Postgres) };
// The opclass is read separately from `pg_index.indclass` for every key position
+1 -1
View File
@@ -14524,7 +14524,7 @@ fn render_postgres_table_ddl_with_constraints_and_partition_info(
.map(|(i, c)| {
// A real column is quoted via `pg_ident`; an expression/functional key part
// arrives as raw expression text (the per-column `pg_get_indexdef` omits the
// opclass — see `crates/dbx-drivers/src/db/postgres.rs`), so quoting the whole
// opclass — see `crates/dbx-driver-postgres/src/postgres.rs`), so quoting the whole
// thing as an identifier would turn it into a nonexistent column reference
// (#6295).
let is_expr = idx.key_is_expression.get(i).copied().unwrap_or(false);
+32
View File
@@ -0,0 +1,32 @@
[package]
name = "dbx-driver-agent"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
publish = false
[features]
default = []
test-support = []
[dependencies]
dbx-platform = { path = "../dbx-platform", features = ["host-prompts", "downloads"] }
dbx-sql-core = { path = "../dbx-sql-core" }
dbx-types = { path = "../dbx-types", default-features = false }
chrono = { version = "0.4", features = ["serde"] }
crc = "3"
flate2 = "1"
futures = "0.3"
log = "0.4"
percent-encoding = "2"
reqwest = { version = "0.12", default-features = false, features = ["cookies", "json", "stream", "rustls-tls", "socks"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
sha2 = "0.10"
tar = "0.4"
tempfile = "3"
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["compat"] }
uuid = { version = "1", features = ["v4", "serde"] }
zip = { version = "4", default-features = false, features = ["deflate"] }
zstd = "0.13"
@@ -5448,7 +5448,7 @@ for line in sys.stdin:
#[test]
fn agent_protocol_matches_contract_file() {
let contract: serde_json::Value =
serde_json::from_str(include_str!("../../assets/agent-protocol-v2.json")).unwrap();
serde_json::from_str(include_str!("../assets/agent-protocol-v2.json")).unwrap();
assert_eq!(contract["protocolVersion"], AGENT_PROTOCOL_VERSION);
assert_eq!(contract["handshakeMethod"], AgentMethod::Handshake.as_str());
+21
View File
@@ -0,0 +1,21 @@
#![recursion_limit = "256"]
pub use dbx_platform::download::DownloadSource;
pub use dbx_platform::{path_utils, process};
pub use dbx_sql_core::sql_error_position;
pub use dbx_types::{database_manifest, models, types};
pub mod agent_catalog;
pub mod agent_connection;
pub mod agent_driver;
pub mod agent_manager;
pub mod agent_offline_export;
pub mod agent_recovery;
pub mod agent_runtime;
pub mod agent_service;
pub mod backend_error;
pub mod database_capabilities;
pub mod db {
pub use crate::agent_driver;
}
@@ -0,0 +1,19 @@
[package]
name = "dbx-driver-elasticsearch"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
publish = false
[dependencies]
dbx-driver-support = { path = "../dbx-driver-support" }
dbx-types = { path = "../dbx-types", default-features = false }
percent-encoding = "2"
regex = "1"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
tokio = { version = "1", features = ["full"] }
[dev-dependencies]
uuid = { version = "1", features = ["v4"] }
@@ -10,9 +10,16 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use super::{apply_tls_certificates, http_client_builder, with_connection_timeout};
use crate::db::document_result::DocumentQueryResult;
use crate::types::QueryResult;
use dbx_driver_support::db::{apply_tls_certificates, http_client_builder, with_connection_timeout};
pub mod db {
pub use dbx_driver_support::db::*;
pub use dbx_driver_support::{document_result, elasticsearch_sql};
}
pub use dbx_types::types;
const ELASTICSEARCH_PATH_SEGMENT_ENCODE_SET: &AsciiSet = &CONTROLS
.add(b' ')
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "dbx-driver-mongodb"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
publish = false
[dependencies]
dbx-driver-support = { path = "../dbx-driver-support" }
dbx-types = { path = "../dbx-types", default-features = false }
base64 = "0.22"
chrono = { version = "0.4", features = ["serde"] }
futures = "0.3"
json5 = "0.4"
log = "0.4"
mongodb = { version = "3.2.5", features = ["socks5-proxy"] }
percent-encoding = "2"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
sha2 = "0.10"
tokio = { version = "1", features = ["full"] }
uuid = { version = "1", features = ["v4"] }
+9
View File
@@ -0,0 +1,9 @@
#![recursion_limit = "256"]
pub use dbx_driver_support::db::{safe_i64_to_json, with_connection_timeout, JS_MAX_SAFE_INTEGER};
pub use dbx_driver_support::document_result;
pub use dbx_types::{models, types};
pub mod mongo_driver;
pub mod mongo_oidc;
pub mod mongo_shell;
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "dbx-driver-mysql"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
publish = false
[features]
default = []
test-support = []
[dependencies]
dbx-driver-support = { path = "../dbx-driver-support" }
dbx-sql-core = { path = "../dbx-sql-core" }
dbx-sql-dialect = { path = "../dbx-sql-dialect" }
dbx-types = { path = "../dbx-types", default-features = false }
chrono = { version = "0.4", features = ["serde"] }
futures = "0.3"
log = "0.4"
mysql_async = { version = "0.37", default-features = false, features = ["default-rustls", "client_ed25519", "chrono", "rust_decimal"] }
percent-encoding = "2"
rust_decimal = { version = "1", features = ["serde"] }
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
sqlparser = { version = "0.62.0", features = ["visitor"] }
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["compat"] }
[dev-dependencies]
mysql_common = { git = "https://github.com/t8y2/rust_mysql_common.git", rev = "47740d85a8277167a5717afac785d37b46a36296", default-features = false }
+17
View File
@@ -0,0 +1,17 @@
#![recursion_limit = "256"]
pub use dbx_driver_support::db::*;
pub use dbx_driver_support::{db, execution, file_validator, wkb};
pub use dbx_sql_core::{mysql_event_sql, sql};
pub use dbx_sql_dialect::sql_dialect;
pub use dbx_types::{models, types};
pub mod dolt;
pub mod doris;
pub mod manticoresearch;
pub mod mysql;
pub mod mysql_compatible;
pub mod ob_oracle;
pub mod oceanbase_mysql;
pub mod starrocks;
pub mod tidb;
+34
View File
@@ -0,0 +1,34 @@
[package]
name = "dbx-driver-postgres"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
publish = false
[dependencies]
dbx-driver-support = { path = "../dbx-driver-support" }
dbx-sql-core = { path = "../dbx-sql-core" }
dbx-types = { path = "../dbx-types", default-features = false }
chrono = { version = "0.4", features = ["serde"] }
bytes = "1"
deadpool-postgres = { version = "0.14", features = ["rt_tokio_1"] }
futures = "0.3"
log = "0.4"
openssl = { version = "0.10", features = ["vendored"] }
percent-encoding = "2"
postgres-openssl = "0.5"
rustls = { version = "0.23", features = ["aws-lc-rs"] }
rustls-pemfile = "2.2"
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
sqlparser = { version = "0.62.0", features = ["visitor"] }
tokio = { version = "1", features = ["full"] }
tokio-postgres = { version = "0.7", features = ["with-chrono-0_4", "with-uuid-1", "with-serde_json-1"] }
tokio-postgres-rustls = "0.13"
tokio-util = { version = "0.7", features = ["compat"] }
uuid = { version = "1", features = ["v4", "serde"] }
webpki-roots = "0.26"
[dev-dependencies]
portpicker = "0.1.1"
tempfile = "3"
tokio-openssl = "0.6"
+10
View File
@@ -0,0 +1,10 @@
#![recursion_limit = "256"]
pub use dbx_driver_support::db::*;
pub use dbx_driver_support::{db, execution, file_validator, wkb};
pub use dbx_sql_core::{sql, sql_error_position};
pub use dbx_types::{models, types};
mod postgres;
pub use postgres::*;
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "dbx-driver-redis"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
publish = false
[features]
default = []
test-support = []
[dependencies]
dbx-driver-support = { path = "../dbx-driver-support" }
dbx-types = { path = "../dbx-types", default-features = false }
base64 = "0.22"
redis = { version = "0.32.2", features = ["tokio-comp", "tls-rustls", "tls-rustls-insecure", "tokio-rustls-comp", "sentinel", "cluster-async"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
tokio = { version = "1", features = ["full"] }
@@ -1,5 +1,6 @@
use crate::models::connection::{ConnectionConfig, DatabaseConnectionInfo};
use base64::Engine;
use dbx_driver_support::db::{connection_timeout, json_value_for_js, safe_i64_to_json, CONNECTION_TIMEOUT_SECS};
use dbx_types::models::connection::{ConnectionConfig, DatabaseConnectionInfo};
use redis::{
aio::ConnectionLike,
cluster::ClusterClient,
@@ -12,8 +13,6 @@ use serde::{Deserialize, Serialize};
use std::{collections::HashMap, future::Future, sync::OnceLock, time::Duration, time::Instant};
use tokio::sync::{Mutex, MutexGuard};
use super::json_value_for_js;
const STREAM_ENTRY_PAGE_SIZE: usize = 50;
const STREAM_PENDING_PAGE_SIZE: usize = 100;
const COLLECTION_PAGE_SIZE: usize = 200;
@@ -711,11 +710,11 @@ pub async fn connect_sentinel(config: &ConnectionConfig) -> Result<redis::aio::M
redis_connection_info: Some(redis_connection_info(&config.username, &config.password, 0)),
};
let client = tokio::time::timeout(
super::connection_timeout(),
connection_timeout(),
sentinel.async_master_for(service_name, Some(&node_connection_info)),
)
.await
.map_err(|_| format!("Redis Sentinel lookup timed out ({}s)", super::CONNECTION_TIMEOUT_SECS))?
.map_err(|_| format!("Redis Sentinel lookup timed out ({}s)", CONNECTION_TIMEOUT_SECS))?
.map_err(|e| format!("Redis Sentinel master lookup failed: {e}"))?;
connect_client(client).await
@@ -741,13 +740,13 @@ pub async fn discover_sentinel_master(
0,
))
.map_err(|e| format!("Redis Sentinel connect failed: {e}"))?;
let mut con = connect_client_with_timeout(client, super::connection_timeout(), "Redis Sentinel").await?;
let mut con = connect_client_with_timeout(client, connection_timeout(), "Redis Sentinel").await?;
let raw = tokio::time::timeout(
super::connection_timeout(),
connection_timeout(),
redis::cmd("SENTINEL").arg("get-master-addr-by-name").arg(service_name).query_async::<RedisRawValue>(&mut con),
)
.await
.map_err(|_| format!("Redis Sentinel master lookup timed out ({}s)", super::CONNECTION_TIMEOUT_SECS))?
.map_err(|_| format!("Redis Sentinel master lookup timed out ({}s)", CONNECTION_TIMEOUT_SECS))?
.map_err(|e| format!("Redis Sentinel master lookup failed: {e}"))?;
redis_sentinel_master_endpoint(raw)
@@ -772,9 +771,9 @@ pub async fn connect_cluster(config: &ConnectionConfig) -> Result<RedisClusterPo
})
.collect();
let client = ClusterClient::new(cluster_nodes).map_err(|e| format!("Redis cluster connection failed: {e}"))?;
let mut con = match tokio::time::timeout(super::connection_timeout(), client.get_async_connection())
let mut con = match tokio::time::timeout(connection_timeout(), client.get_async_connection())
.await
.map_err(|_| format!("Redis cluster connection timed out ({}s)", super::CONNECTION_TIMEOUT_SECS))?
.map_err(|_| format!("Redis cluster connection timed out ({}s)", CONNECTION_TIMEOUT_SECS))?
.map_err(|e| format!("Redis cluster connection failed: {e}"))
{
Ok(con) => con,
@@ -789,16 +788,18 @@ pub async fn connect_cluster(config: &ConnectionConfig) -> Result<RedisClusterPo
Err(err) => return Err(err),
};
let cluster_ping =
match tokio::time::timeout(super::connection_timeout(), redis::cmd("PING").query_async::<String>(&mut con))
.await
{
Ok(Ok(_)) => Ok(()),
// Reachable and authenticated, only the PING privilege is missing (issue #9394).
Ok(Err(error)) if is_redis_command_denied_error(&error.to_string()) => Ok(()),
Ok(Err(error)) => Err(format!("Redis cluster ping failed: {error}")),
Err(_) => Err(format!("Redis cluster ping timed out ({}s)", super::CONNECTION_TIMEOUT_SECS)),
};
let cluster_ping = match tokio::time::timeout(
connection_timeout(),
redis::cmd("PING").query_async::<String>(&mut con),
)
.await
{
Ok(Ok(_)) => Ok(()),
// Reachable and authenticated, only the PING privilege is missing (issue #9394).
Ok(Err(error)) if is_redis_command_denied_error(&error.to_string()) => Ok(()),
Ok(Err(error)) => Err(format!("Redis cluster ping failed: {error}")),
Err(_) => Err(format!("Redis cluster ping timed out ({}s)", CONNECTION_TIMEOUT_SECS)),
};
match cluster_ping {
Ok(_) => {
let seed_routes = identity_routes(&seed_nodes);
@@ -897,7 +898,7 @@ pub async fn connect_routed_cluster(
}
{
let mut con = cluster_any_connection(&pool).await?;
redis_ping(&mut con, "Redis cluster", super::connection_timeout()).await?;
redis_ping(&mut con, "Redis cluster", connection_timeout()).await?;
}
Ok(pool)
}
@@ -906,11 +907,11 @@ pub async fn test_connection(connection: &RedisConnection) -> Result<(), String>
match connection {
RedisConnection::Direct(con) => {
let mut con = con.lock().await;
redis_ping(&mut *con, "Redis", super::connection_timeout()).await
redis_ping(&mut *con, "Redis", connection_timeout()).await
}
RedisConnection::Cluster(cluster) => {
let mut con = cluster_any_connection(cluster).await?;
redis_ping(&mut con, "Redis cluster", super::connection_timeout()).await
redis_ping(&mut con, "Redis cluster", connection_timeout()).await
}
}
}
@@ -934,22 +935,20 @@ async fn redis_database_connection_info<C>(con: &mut C) -> Result<DatabaseConnec
where
C: ConnectionLike + Send + Sync + Unpin,
{
let info = match tokio::time::timeout(
super::connection_timeout(),
redis::cmd("INFO").arg("server").query_async::<String>(con),
)
.await
{
Ok(Ok(info)) => info,
// Managed Redis accounts and proxies frequently allow connecting but not `INFO`. The
// connection is still usable, so report the identity we do know instead of failing all
// server metadata (issue #9394).
Ok(Err(error)) if is_redis_command_denied_error(&error.to_string()) => String::new(),
Ok(Err(error)) => return Err(format!("Redis INFO command failed: {error}")),
Err(_) => {
return Err(format!("Redis INFO command timed out ({}s)", super::CONNECTION_TIMEOUT_SECS));
}
};
let info =
match tokio::time::timeout(connection_timeout(), redis::cmd("INFO").arg("server").query_async::<String>(con))
.await
{
Ok(Ok(info)) => info,
// Managed Redis accounts and proxies frequently allow connecting but not `INFO`. The
// connection is still usable, so report the identity we do know instead of failing all
// server metadata (issue #9394).
Ok(Err(error)) if is_redis_command_denied_error(&error.to_string()) => String::new(),
Ok(Err(error)) => return Err(format!("Redis INFO command failed: {error}")),
Err(_) => {
return Err(format!("Redis INFO command timed out ({}s)", CONNECTION_TIMEOUT_SECS));
}
};
let product_version = info.lines().find_map(|line| {
let (key, value) = line.trim().split_once(':')?;
(key.eq_ignore_ascii_case("redis_version") && !value.trim().is_empty()).then(|| value.trim().to_string())
@@ -1169,12 +1168,12 @@ fn parse_redis_port(port: &str) -> Result<u16, String> {
}
async fn connect_client(client: redis::Client) -> Result<redis::aio::MultiplexedConnection, String> {
let mut con = tokio::time::timeout(super::connection_timeout(), client.get_multiplexed_async_connection())
let mut con = tokio::time::timeout(connection_timeout(), client.get_multiplexed_async_connection())
.await
.map_err(|_| format!("Redis connection timed out ({}s)", super::CONNECTION_TIMEOUT_SECS))?
.map_err(|_| format!("Redis connection timed out ({}s)", CONNECTION_TIMEOUT_SECS))?
.map_err(|e| format!("Redis connection failed: {e}"))?;
redis_ping(&mut con, "Redis", super::connection_timeout()).await?;
redis_ping(&mut con, "Redis", connection_timeout()).await?;
Ok(con)
}
@@ -2139,7 +2138,7 @@ pub fn redis_command_raw_to_json(value: RedisRawValue) -> serde_json::Value {
}
RedisRawValue::SimpleString(value) => serde_json::Value::String(value),
RedisRawValue::Okay => serde_json::Value::String("OK".to_string()),
RedisRawValue::Int(value) => super::safe_i64_to_json(value),
RedisRawValue::Int(value) => safe_i64_to_json(value),
RedisRawValue::Double(value) => {
serde_json::Number::from_f64(value).map_or(serde_json::Value::Null, serde_json::Value::Number)
}
@@ -4509,7 +4508,7 @@ mod tests {
RedisRawValue, RedisSetItem, RedisStreamConsumer, RedisStreamEntry, RedisStreamField, RedisStreamGroup,
RedisStreamPendingEntry, RedisValue, RedisValueData, RedisZsetItem,
};
use crate::models::connection::ConnectionConfig;
use dbx_types::models::connection::ConnectionConfig;
use redis::{aio::ConnectionLike, Cmd, ConnectionAddr, Pipeline, RedisFuture};
struct FakeRedisConnection {
@@ -7372,7 +7371,7 @@ mod tests {
id: "redis".to_string(),
name: "Redis".to_string(),
note: String::new(),
db_type: crate::models::connection::DatabaseType::Redis,
db_type: dbx_types::models::connection::DatabaseType::Redis,
driver_profile: None,
driver_label: None,
url_params: None,
@@ -7391,10 +7390,10 @@ mod tests {
init_script: None,
color: None,
transport_layers: Vec::new(),
connect_timeout_secs: crate::models::connection::default_connect_timeout_secs(),
query_timeout_secs: crate::models::connection::default_query_timeout_secs(),
idle_timeout_secs: crate::models::connection::default_idle_timeout_secs(),
keepalive_interval_secs: crate::models::connection::default_keepalive_interval_secs(),
connect_timeout_secs: dbx_types::models::connection::default_connect_timeout_secs(),
query_timeout_secs: dbx_types::models::connection::default_query_timeout_secs(),
idle_timeout_secs: dbx_types::models::connection::default_idle_timeout_secs(),
keepalive_interval_secs: dbx_types::models::connection::default_keepalive_interval_secs(),
ssl: false,
ca_cert_path: String::new(),
client_cert_path: String::new(),
@@ -7409,7 +7408,7 @@ mod tests {
redis_sentinel_password: String::new(),
redis_sentinel_tls: false,
redis_cluster_nodes: String::new(),
redis_key_separator: crate::models::connection::default_redis_key_separator(),
redis_key_separator: dbx_types::models::connection::default_redis_key_separator(),
redis_scan_page_size: None,
redis_database_aliases: Default::default(),
redis_key_templates: Vec::new(),
+24
View File
@@ -0,0 +1,24 @@
[package]
name = "dbx-driver-sqlserver"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
publish = false
[dependencies]
dbx-driver-support = { path = "../dbx-driver-support" }
dbx-sql-core = { path = "../dbx-sql-core" }
dbx-sql-data = { path = "../dbx-sql-data" }
dbx-types = { path = "../dbx-types", default-features = false }
chrono = { version = "0.4", features = ["serde"] }
futures = "0.3"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
sqlparser = { version = "0.62.0", features = ["visitor"] }
tiberius = { version = "0.12.3", default-features = false, features = ["tds73", "chrono", "rust_decimal", "rustls", "sql-browser-tokio"] }
tokio = { version = "1", features = ["full"] }
tokio-util = { version = "0.7", features = ["compat"] }
tracing = "0.1"
tracing-subscriber = "0.3"
uuid = { version = "1", features = ["v4", "serde"] }
+11
View File
@@ -0,0 +1,11 @@
#![recursion_limit = "256"]
pub use dbx_driver_support::db::*;
pub use dbx_driver_support::{db, execution};
pub use dbx_sql_core::{sql, sqlserver_temporal};
pub use dbx_sql_data::query_result_sql;
pub use dbx_types::types;
mod sqlserver;
pub use sqlserver::*;
+27
View File
@@ -0,0 +1,27 @@
[package]
name = "dbx-driver-support"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
publish = false
[features]
default = []
test-support = ["dbx-platform/test-support"]
[dependencies]
dbx-platform = { path = "../dbx-platform" }
dbx-types = { path = "../dbx-types", default-features = false }
bytes = "1"
portpicker = "0.1.1"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
russh = "0.60"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
sqlparser = { version = "0.62.0", features = ["visitor"] }
tokio = { version = "1", features = ["io-util", "net", "rt", "sync", "time"] }
tokio-util = { version = "0.7", features = ["rt"] }
uuid = { version = "1", features = ["v4"] }
[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread", "test-util"] }
+318
View File
@@ -0,0 +1,318 @@
use crate::path_utils::expand_tilde;
use reqwest::ClientBuilder;
use reqwest::{Certificate, Identity};
use std::fmt;
use std::future::Future;
use std::time::Duration;
// Re-export types so that `db::QueryResult` etc. work within dbx-core
pub use crate::file_validator::validate_file_path;
pub use crate::types::*;
pub const CONNECTION_TIMEOUT_SECS: u64 = 5;
pub const TCP_PROBE_TIMEOUT_SECS: u64 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoolCheckoutStage {
Wait,
Create,
Recycle,
Unknown,
}
impl PoolCheckoutStage {
pub fn as_str(self) -> &'static str {
match self {
Self::Wait => "wait",
Self::Create => "create",
Self::Recycle => "recycle",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PoolCheckoutError {
Timeout { database: &'static str, stage: PoolCheckoutStage, timeout: Duration },
Failed { database: &'static str, stage: PoolCheckoutStage, detail: String },
Canceled,
}
impl PoolCheckoutError {
pub fn stage(&self) -> Option<PoolCheckoutStage> {
match self {
Self::Timeout { stage, .. } | Self::Failed { stage, .. } => Some(*stage),
Self::Canceled => None,
}
}
pub fn is_pool_saturation(&self) -> bool {
matches!(self, Self::Timeout { stage: PoolCheckoutStage::Wait, .. })
}
}
impl fmt::Display for PoolCheckoutError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Timeout { database, stage, timeout } => write!(
formatter,
"{database} connection pool checkout timed out [stage={}, timeout_ms={}]",
stage.as_str(),
timeout.as_millis()
),
Self::Failed { database, stage, detail } => {
write!(formatter, "{database} connection pool checkout failed [stage={}]: {detail}", stage.as_str())
}
Self::Canceled => formatter.write_str(crate::execution::QUERY_CANCELED),
}
}
}
pub fn connection_timeout() -> Duration {
Duration::from_secs(CONNECTION_TIMEOUT_SECS)
}
pub fn http_client_builder(timeout: Duration) -> ClientBuilder {
reqwest::Client::builder().connect_timeout(timeout).no_proxy()
}
/// Augments an HTTP client builder with custom TLS material for drivers that
/// need to trust a non-system CA or present a client certificate (mTLS).
///
/// * `ca_cert_path` adds an extra root certificate (PEM bundle) alongside the
/// system roots, so self-signed server certificates are trusted.
/// * `client_cert_path` + `client_key_path` (required together) are concatenated
/// into a single PEM identity, as expected by reqwest's rustls backend.
///
/// Returns the augmented builder, or a descriptive error if a configured
/// certificate/key file cannot be read or parsed. Callers surface that error
/// instead of silently falling back to the system trust store, so a typo in a
/// cert path fails the connection loudly rather than being ignored.
pub fn apply_tls_certificates(
mut builder: ClientBuilder,
ca_cert_path: Option<&str>,
client_cert_path: Option<&str>,
client_key_path: Option<&str>,
) -> Result<ClientBuilder, String> {
// The desktop build unifies reqwest with both `default-tls` and `rustls-tls`,
// and `TlsBackend` defaults to native-tls there, which rejects the PEM identity
// below with `incompatible TLS identity type`. Pin rustls like the Consul and
// etcd-metrics clients so custom CA roots and PEM client identities work in
// every target.
builder = builder.use_rustls_tls();
if let Some(ca) = ca_cert_path.filter(|path| !path.is_empty()) {
let ca = expand_tilde(ca);
let contents = std::fs::read(&ca)
.map_err(|error| format!("Failed to read Elasticsearch CA certificate at {ca}: {error}"))?;
let certificates = Certificate::from_pem_bundle(&contents)
.map_err(|error| format!("Failed to parse Elasticsearch CA certificate at {ca}: {error}"))?;
for certificate in certificates {
builder = builder.add_root_certificate(certificate);
}
}
if let (Some(cert_path), Some(key_path)) =
(client_cert_path.filter(|path| !path.is_empty()), client_key_path.filter(|path| !path.is_empty()))
{
let cert_path = expand_tilde(cert_path);
let key_path = expand_tilde(key_path);
let mut pem = std::fs::read(&cert_path)
.map_err(|error| format!("Failed to read Elasticsearch client certificate at {cert_path}: {error}"))?;
// reqwest's rustls backend concatenates the cert and key as two PEM
// blocks; ensure they are newline-separated so parsing succeeds even
// when the cert file does not end with a trailing newline.
if !pem.ends_with(b"\n") {
pem.push(b'\n');
}
pem.extend(
std::fs::read(&key_path)
.map_err(|error| format!("Failed to read Elasticsearch client key at {key_path}: {error}"))?,
);
let identity = Identity::from_pem(&pem)
.map_err(|error| format!("Failed to parse Elasticsearch client identity: {error}"))?;
builder = builder.identity(identity);
}
Ok(builder)
}
pub use dbx_types::json::JS_MAX_SAFE_INTEGER;
pub use dbx_types::json::{json_value_for_js, safe_i64_to_json, safe_u64_to_json};
pub fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
pub fn binary_value_to_json(bytes: &[u8]) -> serde_json::Value {
serde_json::Value::String(format!("0x{}", hex_encode(bytes)))
}
pub fn tcp_probe_timeout() -> Duration {
Duration::from_secs(TCP_PROBE_TIMEOUT_SECS)
}
pub fn parse_connect_timeout(url: &str) -> Duration {
parse_connect_timeout_with_fallback(url, connection_timeout())
}
pub fn parse_connect_timeout_with_fallback(url: &str, fallback: Duration) -> Duration {
let Some(query) = url.split('?').nth(1) else {
return fallback;
};
for param in query.split('&') {
let trimmed = param.trim();
if trimmed.is_empty() {
continue;
}
let (key, value) = match trimmed.split_once('=') {
Some(pair) => pair,
None => continue,
};
if key.eq_ignore_ascii_case("connect_timeout")
|| key.eq_ignore_ascii_case("connectTimeout")
|| key.eq_ignore_ascii_case("connection_timeout")
|| key.eq_ignore_ascii_case("connectionTimeout")
{
if let Ok(v) = value.parse::<u64>() {
if (1..=300).contains(&v) {
return Duration::from_secs(v);
}
}
}
}
fallback
}
pub async fn with_connection_timeout<T, F>(label: &str, timeout: Duration, future: F) -> Result<T, String>
where
F: Future<Output = Result<T, String>>,
{
tokio::time::timeout(timeout, future)
.await
.map_err(|_| format!("{label} connection timed out ({}s)", timeout.as_secs()))?
}
pub async fn probe_tcp_endpoint(label: &str, host: &str, port: u16, timeout: Duration) -> Result<(), String> {
tokio::time::timeout(timeout, tokio::net::TcpStream::connect((host, port)))
.await
.map_err(|_| format!("{label} TCP connection timed out ({}s)", timeout.as_secs()))?
.map(|_| ())
.map_err(|e| format!("{label} TCP connection failed: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binary_values_are_displayed_as_prefixed_hex() {
assert_eq!(binary_value_to_json(&[0x00, 0x01, 0xab, 0xff]), serde_json::json!("0x0001abff"));
}
}
#[cfg(test)]
mod tls_tests {
use super::{apply_tls_certificates, http_client_builder};
use std::path::Path;
use std::process::Command;
use std::time::Duration;
/// Generates a throwaway CA + client certificate/key (PEM) via openssl and
/// returns their paths. If openssl is unavailable the files stay empty, which
/// makes `apply_tls_certificates` surface a read error instead of degrading.
fn gen_certs(dir: &Path) -> (std::path::PathBuf, std::path::PathBuf, std::path::PathBuf) {
let ca_key = dir.join("ca.key");
let ca_pem = dir.join("ca.pem");
let csr = dir.join("client.csr");
let client_key = dir.join("client.key");
let client_crt = dir.join("client.crt");
// Extensions force an X.509 v3 client cert; some toolchains (e.g.
// OpenSSL 3.0 on CI) emit v1 without one, which rustls rejects.
let client_ext = dir.join("client.ext");
let _ = std::fs::write(&client_ext, "basicConstraints=critical,CA:FALSE\n");
let _ = Command::new("openssl")
.args([
"req",
"-x509",
"-newkey",
"rsa:2048",
"-nodes",
"-keyout",
ca_key.to_str().unwrap(),
"-out",
ca_pem.to_str().unwrap(),
"-days",
"1",
"-subj",
"/CN=dbx-test-ca",
])
.output();
let _ = Command::new("openssl")
.args([
"req",
"-newkey",
"rsa:2048",
"-nodes",
"-keyout",
client_key.to_str().unwrap(),
"-out",
csr.to_str().unwrap(),
"-subj",
"/CN=dbx-test-client",
])
.output();
let _ = Command::new("openssl")
.args([
"x509",
"-req",
"-in",
csr.to_str().unwrap(),
"-CA",
ca_pem.to_str().unwrap(),
"-CAkey",
ca_key.to_str().unwrap(),
"-CAcreateserial",
"-out",
client_crt.to_str().unwrap(),
"-days",
"1",
"-extfile",
client_ext.to_str().unwrap(),
])
.output();
(ca_pem, client_crt, client_key)
}
#[test]
fn apply_tls_certificates_builds_with_ca_and_client_cert() {
let dir = std::env::temp_dir().join(format!("dbx-tls-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (ca, client_crt, client_key) = gen_certs(&dir);
let built = apply_tls_certificates(
http_client_builder(Duration::from_secs(5)),
ca.to_str(),
client_crt.to_str(),
client_key.to_str(),
)
.expect("client should build with CA + client cert")
.build();
assert!(built.is_ok(), "built client should be usable with CA + client cert: {:?}", built.err());
// A misconfigured CA path must now surface a descriptive error instead of
// silently degrading to the system trust store.
let missing = apply_tls_certificates(
http_client_builder(Duration::from_secs(5)),
Some("/nonexistent/ca.pem"),
None,
None,
);
assert!(missing.is_err(), "missing CA path must surface a read error");
let _ = std::fs::remove_dir_all(&dir);
}
}
+16
View File
@@ -0,0 +1,16 @@
#![recursion_limit = "256"]
pub use dbx_platform::path_utils;
pub use dbx_types::{models, types};
pub mod db;
pub mod ddl_scan;
pub mod document_result;
pub mod elasticsearch_sql;
pub mod execution;
pub mod file_validator;
pub mod http_tunnel;
pub mod runtime_config;
pub mod ssh_config;
pub mod ssh_host_key;
pub mod wkb;
+14 -5
View File
@@ -7,17 +7,27 @@ publish = false
[features]
default = []
duckdb-sidecar = ["dbx-sql/duckdb-sidecar"]
duckdb-sidecar = []
dynamodb = ["dep:aws-sdk-dynamodb"]
mq-admin = ["dbx-types/mq-admin"]
sqlite-bundled = ["rusqlite/bundled"]
sqlite-sqlcipher = ["rusqlite/bundled-sqlcipher-vendored-openssl"]
sqlite-multiple-ciphers = ["dep:libsqlite3-hotbundle"]
test-support = ["dbx-platform/test-support"]
test-support = ["dbx-driver-agent/test-support", "dbx-driver-mysql/test-support", "dbx-driver-redis/test-support", "dbx-driver-support/test-support", "dbx-platform/test-support"]
[dependencies]
dbx-driver-agent = { path = "../dbx-driver-agent" }
dbx-driver-support = { path = "../dbx-driver-support" }
dbx-driver-elasticsearch = { path = "../dbx-driver-elasticsearch" }
dbx-driver-mongodb = { path = "../dbx-driver-mongodb" }
dbx-driver-mysql = { path = "../dbx-driver-mysql" }
dbx-driver-postgres = { path = "../dbx-driver-postgres" }
dbx-driver-redis = { path = "../dbx-driver-redis" }
dbx-driver-sqlserver = { path = "../dbx-driver-sqlserver" }
dbx-types = { path = "../dbx-types", default-features = false }
dbx-sql = { path = "../dbx-sql", default-features = false }
dbx-sql-core = { path = "../dbx-sql-core" }
dbx-sql-data = { path = "../dbx-sql-data" }
dbx-sql-dialect = { path = "../dbx-sql-dialect" }
dbx-platform = { path = "../dbx-platform", features = ["host-prompts", "downloads"] }
dbx-sqlite-worker = { path = "../dbx-sqlite-worker", default-features = false }
serde = { version = "1.0", features = ["derive"] }
@@ -43,13 +53,11 @@ rusqlite = { version = "0.32", default-features = false, features = ["load_exten
libsqlite3-hotbundle = { git = "https://github.com/zipg/libsqlite3-hotbundle.git", rev = "7f12f203b4f4694a273410bd9b02d77f82b6114e", features = ["cipher-sqlcipher"], optional = true }
mysql_async = { version = "0.37", default-features = false, features = ["default-rustls", "client_ed25519", "chrono", "rust_decimal"] }
sqlparser = { version = "0.62.0", features = ["visitor"] }
redis = { version = "0.32.2", features = ["tokio-comp", "tls-rustls", "tls-rustls-insecure", "tokio-rustls-comp", "sentinel", "cluster-async"] }
rustls = { version = "0.23", features = ["aws-lc-rs"] }
rustls-pemfile = "2.2"
tiberius = { version = "0.12.3", default-features = false, features = ["tds73", "chrono", "rust_decimal", "rustls", "sql-browser-tokio"] }
reqwest = { version = "0.12", default-features = false, features = ["cookies", "json", "stream", "rustls-tls", "socks"] }
futures = "0.3"
mongodb = { version = "3.2.5", features = ["socks5-proxy"] }
aws-sdk-dynamodb = { version = "=1.104.0", optional = true }
russh = "0.60"
pageant = "0.2"
@@ -69,6 +77,7 @@ tempfile = "3"
windows-sys = { version = "0.61", features = ["Win32_Storage_FileSystem", "Win32_Foundation", "Win32_Security"] }
[dev-dependencies]
dbx-driver-support = { path = "../dbx-driver-support", features = ["test-support"] }
dbx-platform = { path = "../dbx-platform", features = ["test-support"] }
mysql_common = { git = "https://github.com/t8y2/rust_mysql_common.git", rev = "47740d85a8277167a5717afac785d37b46a36296", default-features = false }
tokio = { version = "1", features = ["test-util"] }
+18 -341
View File
@@ -1,12 +1,10 @@
pub mod agent_driver;
pub use dbx_driver_agent::agent_driver;
pub mod clickhouse_driver;
pub mod cloudberry;
pub mod cloudflare_d1;
pub use cloudflare_d1 as cloudflare_d1_driver;
pub mod ddl_scan;
pub mod document_result;
pub mod dolt;
pub mod doris;
pub use dbx_driver_mysql::{dolt, doris};
pub use dbx_driver_support::{ddl_scan, document_result};
pub mod duckdb_sql;
#[cfg(feature = "duckdb-sidecar")]
pub mod duckdb_worker_process;
@@ -19,359 +17,38 @@ pub mod dynamodb_driver;
#[path = "dynamodb_driver_disabled.rs"]
pub mod dynamodb_driver;
pub mod easysearch_driver;
pub mod elasticsearch_driver;
pub mod elasticsearch_sql;
pub mod file_validator;
pub use dbx_driver_elasticsearch as elasticsearch_driver;
pub use dbx_driver_support::{elasticsearch_sql, file_validator};
pub mod hbase_driver;
pub mod http_tunnel;
pub use dbx_driver_support::http_tunnel;
pub mod influxdb3_driver;
pub mod influxdb_driver;
pub mod manticoresearch;
pub use dbx_driver_mysql::manticoresearch;
pub mod meilisearch_driver;
pub mod mongo_driver;
pub mod mysql;
pub mod mysql_compatible;
pub mod ob_oracle;
pub mod oceanbase_mysql;
pub use dbx_driver_mongodb::mongo_driver;
pub use dbx_driver_mysql::{mysql, mysql_compatible, ob_oracle, oceanbase_mysql};
pub mod opentenbase;
pub mod postgres;
pub use dbx_driver_postgres as postgres;
pub mod proxy_tunnel;
pub mod questdb;
pub mod redis_driver;
pub use dbx_driver_redis as redis_driver;
pub mod rqlite_driver;
pub mod salesforce_driver;
pub mod solr_driver;
pub mod sqlite;
pub mod sqlite_worker;
pub mod sqlserver;
pub mod ssh_host_key;
pub use dbx_driver_sqlserver as sqlserver;
pub use dbx_driver_support::ssh_host_key;
pub use dbx_platform::ssh_prompt;
pub mod ssh_tunnel;
pub mod starrocks;
pub use dbx_sql::tdsql_mysql;
pub mod tidb;
pub use dbx_driver_mysql::starrocks;
pub use dbx_driver_mysql::tidb;
pub use dbx_sql_core::tdsql_mysql;
pub mod transport_layer_tunnel;
pub mod turso_driver;
pub mod vector_driver;
pub mod victoriametrics_driver;
pub mod wkb;
pub use dbx_driver_support::wkb;
use crate::path_utils::expand_tilde;
use reqwest::ClientBuilder;
use reqwest::{Certificate, Identity};
use std::fmt;
use std::future::Future;
use std::time::Duration;
// Re-export types so that `db::QueryResult` etc. work within dbx-core
pub use crate::mysql_event_sql::MysqlEventInfo;
pub use crate::types::*;
pub use file_validator::validate_file_path;
pub const CONNECTION_TIMEOUT_SECS: u64 = 5;
pub const TCP_PROBE_TIMEOUT_SECS: u64 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PoolCheckoutStage {
Wait,
Create,
Recycle,
Unknown,
}
impl PoolCheckoutStage {
pub fn as_str(self) -> &'static str {
match self {
Self::Wait => "wait",
Self::Create => "create",
Self::Recycle => "recycle",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PoolCheckoutError {
Timeout { database: &'static str, stage: PoolCheckoutStage, timeout: Duration },
Failed { database: &'static str, stage: PoolCheckoutStage, detail: String },
Canceled,
}
impl PoolCheckoutError {
pub fn stage(&self) -> Option<PoolCheckoutStage> {
match self {
Self::Timeout { stage, .. } | Self::Failed { stage, .. } => Some(*stage),
Self::Canceled => None,
}
}
pub fn is_pool_saturation(&self) -> bool {
matches!(self, Self::Timeout { stage: PoolCheckoutStage::Wait, .. })
}
}
impl fmt::Display for PoolCheckoutError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Timeout { database, stage, timeout } => write!(
formatter,
"{database} connection pool checkout timed out [stage={}, timeout_ms={}]",
stage.as_str(),
timeout.as_millis()
),
Self::Failed { database, stage, detail } => {
write!(formatter, "{database} connection pool checkout failed [stage={}]: {detail}", stage.as_str())
}
Self::Canceled => formatter.write_str(crate::execution::QUERY_CANCELED),
}
}
}
pub fn connection_timeout() -> Duration {
Duration::from_secs(CONNECTION_TIMEOUT_SECS)
}
pub fn http_client_builder(timeout: Duration) -> ClientBuilder {
reqwest::Client::builder().connect_timeout(timeout).no_proxy()
}
/// Augments an HTTP client builder with custom TLS material for drivers that
/// need to trust a non-system CA or present a client certificate (mTLS).
///
/// * `ca_cert_path` adds an extra root certificate (PEM bundle) alongside the
/// system roots, so self-signed server certificates are trusted.
/// * `client_cert_path` + `client_key_path` (required together) are concatenated
/// into a single PEM identity, as expected by reqwest's rustls backend.
///
/// Returns the augmented builder, or a descriptive error if a configured
/// certificate/key file cannot be read or parsed. Callers surface that error
/// instead of silently falling back to the system trust store, so a typo in a
/// cert path fails the connection loudly rather than being ignored.
pub fn apply_tls_certificates(
mut builder: ClientBuilder,
ca_cert_path: Option<&str>,
client_cert_path: Option<&str>,
client_key_path: Option<&str>,
) -> Result<ClientBuilder, String> {
// The desktop build unifies reqwest with both `default-tls` and `rustls-tls`,
// and `TlsBackend` defaults to native-tls there, which rejects the PEM identity
// below with `incompatible TLS identity type`. Pin rustls like the Consul and
// etcd-metrics clients so custom CA roots and PEM client identities work in
// every target.
builder = builder.use_rustls_tls();
if let Some(ca) = ca_cert_path.filter(|path| !path.is_empty()) {
let ca = expand_tilde(ca);
let contents = std::fs::read(&ca)
.map_err(|error| format!("Failed to read Elasticsearch CA certificate at {ca}: {error}"))?;
let certificates = Certificate::from_pem_bundle(&contents)
.map_err(|error| format!("Failed to parse Elasticsearch CA certificate at {ca}: {error}"))?;
for certificate in certificates {
builder = builder.add_root_certificate(certificate);
}
}
if let (Some(cert_path), Some(key_path)) =
(client_cert_path.filter(|path| !path.is_empty()), client_key_path.filter(|path| !path.is_empty()))
{
let cert_path = expand_tilde(cert_path);
let key_path = expand_tilde(key_path);
let mut pem = std::fs::read(&cert_path)
.map_err(|error| format!("Failed to read Elasticsearch client certificate at {cert_path}: {error}"))?;
// reqwest's rustls backend concatenates the cert and key as two PEM
// blocks; ensure they are newline-separated so parsing succeeds even
// when the cert file does not end with a trailing newline.
if !pem.ends_with(b"\n") {
pem.push(b'\n');
}
pem.extend(
std::fs::read(&key_path)
.map_err(|error| format!("Failed to read Elasticsearch client key at {key_path}: {error}"))?,
);
let identity = Identity::from_pem(&pem)
.map_err(|error| format!("Failed to parse Elasticsearch client identity: {error}"))?;
builder = builder.identity(identity);
}
Ok(builder)
}
pub use dbx_types::json::JS_MAX_SAFE_INTEGER;
pub use dbx_types::json::{json_value_for_js, safe_i64_to_json, safe_u64_to_json};
pub fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
pub fn binary_value_to_json(bytes: &[u8]) -> serde_json::Value {
serde_json::Value::String(format!("0x{}", hex_encode(bytes)))
}
pub fn tcp_probe_timeout() -> Duration {
Duration::from_secs(TCP_PROBE_TIMEOUT_SECS)
}
pub fn parse_connect_timeout(url: &str) -> Duration {
parse_connect_timeout_with_fallback(url, connection_timeout())
}
pub fn parse_connect_timeout_with_fallback(url: &str, fallback: Duration) -> Duration {
let Some(query) = url.split('?').nth(1) else {
return fallback;
};
for param in query.split('&') {
let trimmed = param.trim();
if trimmed.is_empty() {
continue;
}
let (key, value) = match trimmed.split_once('=') {
Some(pair) => pair,
None => continue,
};
if key.eq_ignore_ascii_case("connect_timeout")
|| key.eq_ignore_ascii_case("connectTimeout")
|| key.eq_ignore_ascii_case("connection_timeout")
|| key.eq_ignore_ascii_case("connectionTimeout")
{
if let Ok(v) = value.parse::<u64>() {
if (1..=300).contains(&v) {
return Duration::from_secs(v);
}
}
}
}
fallback
}
pub async fn with_connection_timeout<T, F>(label: &str, timeout: Duration, future: F) -> Result<T, String>
where
F: Future<Output = Result<T, String>>,
{
tokio::time::timeout(timeout, future)
.await
.map_err(|_| format!("{label} connection timed out ({}s)", timeout.as_secs()))?
}
pub async fn probe_tcp_endpoint(label: &str, host: &str, port: u16, timeout: Duration) -> Result<(), String> {
tokio::time::timeout(timeout, tokio::net::TcpStream::connect((host, port)))
.await
.map_err(|_| format!("{label} TCP connection timed out ({}s)", timeout.as_secs()))?
.map(|_| ())
.map_err(|e| format!("{label} TCP connection failed: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binary_values_are_displayed_as_prefixed_hex() {
assert_eq!(binary_value_to_json(&[0x00, 0x01, 0xab, 0xff]), serde_json::json!("0x0001abff"));
}
}
#[cfg(test)]
mod tls_tests {
use super::{apply_tls_certificates, http_client_builder};
use std::path::Path;
use std::process::Command;
use std::time::Duration;
/// Generates a throwaway CA + client certificate/key (PEM) via openssl and
/// returns their paths. If openssl is unavailable the files stay empty, which
/// makes `apply_tls_certificates` surface a read error instead of degrading.
fn gen_certs(dir: &Path) -> (std::path::PathBuf, std::path::PathBuf, std::path::PathBuf) {
let ca_key = dir.join("ca.key");
let ca_pem = dir.join("ca.pem");
let csr = dir.join("client.csr");
let client_key = dir.join("client.key");
let client_crt = dir.join("client.crt");
// Extensions force an X.509 v3 client cert; some toolchains (e.g.
// OpenSSL 3.0 on CI) emit v1 without one, which rustls rejects.
let client_ext = dir.join("client.ext");
let _ = std::fs::write(&client_ext, "basicConstraints=critical,CA:FALSE\n");
let _ = Command::new("openssl")
.args([
"req",
"-x509",
"-newkey",
"rsa:2048",
"-nodes",
"-keyout",
ca_key.to_str().unwrap(),
"-out",
ca_pem.to_str().unwrap(),
"-days",
"1",
"-subj",
"/CN=dbx-test-ca",
])
.output();
let _ = Command::new("openssl")
.args([
"req",
"-newkey",
"rsa:2048",
"-nodes",
"-keyout",
client_key.to_str().unwrap(),
"-out",
csr.to_str().unwrap(),
"-subj",
"/CN=dbx-test-client",
])
.output();
let _ = Command::new("openssl")
.args([
"x509",
"-req",
"-in",
csr.to_str().unwrap(),
"-CA",
ca_pem.to_str().unwrap(),
"-CAkey",
ca_key.to_str().unwrap(),
"-CAcreateserial",
"-out",
client_crt.to_str().unwrap(),
"-days",
"1",
"-extfile",
client_ext.to_str().unwrap(),
])
.output();
(ca_pem, client_crt, client_key)
}
#[test]
fn apply_tls_certificates_builds_with_ca_and_client_cert() {
let dir = std::env::temp_dir().join(format!("dbx-tls-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let (ca, client_crt, client_key) = gen_certs(&dir);
let built = apply_tls_certificates(
http_client_builder(Duration::from_secs(5)),
ca.to_str(),
client_crt.to_str(),
client_key.to_str(),
)
.expect("client should build with CA + client cert")
.build();
assert!(built.is_ok(), "built client should be usable with CA + client cert: {:?}", built.err());
// A misconfigured CA path must now surface a descriptive error instead of
// silently degrading to the system trust store.
let missing = apply_tls_certificates(
http_client_builder(Duration::from_secs(5)),
Some("/nonexistent/ca.pem"),
None,
None,
);
assert!(missing.is_err(), "missing CA path must surface a read error");
let _ = std::fs::remove_dir_all(&dir);
}
}
pub use dbx_driver_support::db::*;
+10 -16
View File
@@ -1,26 +1,20 @@
#![recursion_limit = "256"]
pub use dbx_sql::query_result_sql;
pub use dbx_sql_data::query_result_sql;
pub use dbx_driver_support::{runtime_config, ssh_config};
pub use dbx_platform::download::DownloadSource;
pub use dbx_platform::{path_utils, process};
pub use dbx_sql::{mysql_ddl_normalize, mysql_event_sql, sql, sql_dialect, sql_error_position, sqlserver_temporal};
pub use dbx_sql_core::{mysql_ddl_normalize, mysql_event_sql, sql, sql_error_position, sqlserver_temporal};
pub use dbx_sql_dialect::sql_dialect;
pub use dbx_types::{database_manifest, models, types};
pub mod agent_catalog;
pub mod agent_connection;
pub mod agent_manager;
pub mod agent_offline_export;
pub mod agent_recovery;
pub mod agent_runtime;
pub mod agent_service;
pub mod backend_error;
pub mod database_capabilities;
pub use dbx_driver_agent::{
agent_catalog, agent_connection, agent_manager, agent_offline_export, agent_recovery, agent_runtime, agent_service,
backend_error, database_capabilities,
};
pub mod db;
pub mod execution;
pub use dbx_driver_support::execution;
pub mod metadata;
pub mod mongo_oidc;
pub mod mongo_shell;
pub mod runtime_config;
pub use dbx_driver_mongodb::{mongo_oidc, mongo_shell};
pub mod salesforce_oauth;
pub mod ssh_config;
@@ -1,6 +1,6 @@
use dbx_drivers::db::sqlserver::{self, SqlServerStreamItem};
use dbx_drivers::models::connection::DatabaseType;
use dbx_sql::data_grid_sql::format_grid_sql_literal;
use dbx_sql_data::data_grid_sql::format_grid_sql_literal;
use serde_json::Value;
use std::time::Duration;
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "dbx-sql-core"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
publish = false
[dependencies]
dbx-types = { path = "../dbx-types", default-features = false }
chrono = { version = "0.4", features = ["serde"] }
encoding_rs = "0.8"
log = "0.4"
regex = "1"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
sqlparser = { version = "0.62.0", features = ["visitor"] }
unicode-ident = "1"
winnow = "1.0"
+18
View File
@@ -0,0 +1,18 @@
#![recursion_limit = "256"]
pub use dbx_types::{database_manifest, models, types};
pub mod dml_preview_sql;
pub mod mysql_ddl_normalize;
pub mod mysql_event_sql;
pub mod object_source_sql;
pub mod query_execution_sql;
pub mod sql;
pub mod sql_analysis;
pub mod sql_diagnostics;
pub mod sql_editability;
pub mod sql_error_position;
pub mod sql_risk;
pub mod sqlserver_temporal;
pub mod tdsql_mysql;
pub mod value_literals;
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "dbx-sql-data"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0"
publish = false
[features]
default = []
openapi = ["dep:utoipa", "dbx-types/openapi"]
[dependencies]
dbx-sql-core = { path = "../dbx-sql-core" }
dbx-sql-dialect = { path = "../dbx-sql-dialect" }
dbx-types = { path = "../dbx-types", default-features = false }
chrono = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["arbitrary_precision", "preserve_order"] }
sqlparser = { version = "0.62.0", features = ["visitor"] }
unicode-width = "0.2"
utoipa = { version = "5.5", optional = true }

Some files were not shown because too many files have changed in this diff Show More