mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
refactor(workspace): split SQL and database driver crates
This commit is contained in:
@@ -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"],
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Generated
+225
-7
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
@@ -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 "],
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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,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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
+1
-1
@@ -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());
|
||||
@@ -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"] }
|
||||
+8
-1
@@ -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' ')
|
||||
@@ -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"] }
|
||||
@@ -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;
|
||||
@@ -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 }
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
@@ -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::*;
|
||||
@@ -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(),
|
||||
@@ -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"] }
|
||||
@@ -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::*;
|
||||
@@ -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"] }
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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
Reference in New Issue
Block a user