refactor(workspace): split SQL and database driver crates
This commit is contained in:
180 files changed
+1422
-538
No files matched your search
@@ -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"
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+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());
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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 }
|
||||
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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::*;
|
||||
File renamed without changes.
@@ -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::*;
|
||||
File renamed without changes.
@@ -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);
|
||||
}
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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"
|
||||
File renamed without changes.
@@ -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;
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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 }
|
||||
Loaded 100 of 180 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user