diff --git a/apps/desktop/public/icons/database/opentenbase.svg b/apps/desktop/public/icons/database/opentenbase.svg
new file mode 100644
index 0000000000..e733bfec2c
--- /dev/null
+++ b/apps/desktop/public/icons/database/opentenbase.svg
@@ -0,0 +1,9 @@
+
+
diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue
index 1ef1aecdf2..076fd62f9b 100644
--- a/apps/desktop/src/components/connection/ConnectionDialog.vue
+++ b/apps/desktop/src/components/connection/ConnectionDialog.vue
@@ -1031,6 +1031,14 @@ const driverProfiles: Record<
icon: "cloudberry",
urlParams: "",
},
+ opentenbase: {
+ type: "postgres",
+ port: 11000,
+ user: "opentenbase",
+ label: "OpenTenBase",
+ icon: "opentenbase",
+ urlParams: "",
+ },
redis: { type: "redis", port: 6379, user: "", label: "Redis", icon: "redis" },
sqlite: { type: "sqlite", port: 0, user: "", label: "SQLite", icon: "sqlite" },
rqlite: { type: "rqlite", port: 4001, user: "", label: "RQLite", icon: "rqlite" },
@@ -2720,6 +2728,7 @@ const iconTypeMap: Record = {
mysql: "mysql",
postgres: "postgres",
cloudberry: "cloudberry",
+ opentenbase: "opentenbase",
sqlite: "sqlite",
rqlite: "rqlite",
turso: "turso",
@@ -2813,6 +2822,7 @@ const iconTypeMap: Record = {
const dbOptions: DbOption[] = [
{ value: "postgres", label: "PostgreSQL" },
{ value: "cloudberry", label: "Apache Cloudberry" },
+ { value: "opentenbase", label: "OpenTenBase" },
{ value: "mysql", label: "MySQL" },
{ value: "mongodb", label: "MongoDB" },
{ value: "redis", label: "Redis" },
@@ -2920,7 +2930,7 @@ const dbCategoryDefinitions: Array<{
{
key: "domestic",
titleKey: "connection.databaseCategoryDomestic",
- optionValues: ["dm", "opengauss", "gaussdb", "kwdb", "tidb", "oceanbase", "goldendb", "tdsql", "polardb", "greatsql", "gbase", "kingbase", "highgo", "uxdb", "yashandb", "vastbase", "sundb", "oscar", "xugu"],
+ optionValues: ["dm", "opengauss", "opentenbase", "gaussdb", "kwdb", "tidb", "oceanbase", "goldendb", "tdsql", "polardb", "greatsql", "gbase", "kingbase", "highgo", "uxdb", "yashandb", "vastbase", "sundb", "oscar", "xugu"],
},
{
key: "lightweight",
diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue
index 150b45a266..4da59dd00a 100644
--- a/apps/desktop/src/components/icons/DatabaseIcon.vue
+++ b/apps/desktop/src/components/icons/DatabaseIcon.vue
@@ -14,6 +14,7 @@ const assetIcons: Record = {
postgres: "postgres",
postgresql: "postgres",
cloudberry: "cloudberry",
+ opentenbase: "opentenbase",
sqlite: "sqlite",
rqlite: "rqlite.png",
turso: "turso.png",
diff --git a/apps/desktop/src/lib/__tests__/connection/connectionUrl.opentenbase.spec.ts b/apps/desktop/src/lib/__tests__/connection/connectionUrl.opentenbase.spec.ts
new file mode 100644
index 0000000000..59c5778e27
--- /dev/null
+++ b/apps/desktop/src/lib/__tests__/connection/connectionUrl.opentenbase.spec.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from "vitest";
+import { connectionProfileForScheme, parseConnectionUrl } from "@/lib/connection/connectionUrl";
+
+describe("OpenTenBase connection URLs", () => {
+ it("parses the OpenTenBase alias as a PostgreSQL-compatible profile", () => {
+ const parsed = parseConnectionUrl("opentenbase://opentenbase:secret@cn.example.com/postgres");
+
+ expect(parsed).toMatchObject({
+ dbType: "postgres",
+ driverProfile: "opentenbase",
+ driverLabel: "OpenTenBase",
+ host: "cn.example.com",
+ port: 11000,
+ username: "opentenbase",
+ password: "secret",
+ database: "postgres",
+ });
+ });
+
+ it("keeps the OpenTenBase profile for standard PostgreSQL URLs", () => {
+ const parsed = parseConnectionUrl("postgresql://cn.example.com:15432/postgres", "opentenbase");
+
+ expect(parsed.dbType).toBe("postgres");
+ expect(parsed.driverProfile).toBe("opentenbase");
+ expect(parsed.driverLabel).toBe("OpenTenBase");
+ expect(parsed.port).toBe(15432);
+ });
+
+ it("exposes OpenTenBase to connection deep links", () => {
+ expect(connectionProfileForScheme("opentenbase")).toEqual({
+ type: "postgres",
+ profile: "opentenbase",
+ label: "OpenTenBase",
+ defaultPort: 11000,
+ });
+ });
+});
diff --git a/apps/desktop/src/lib/__tests__/imports/dbeaverImport.spec.ts b/apps/desktop/src/lib/__tests__/imports/dbeaverImport.spec.ts
index 256783dac2..f7ba726477 100644
--- a/apps/desktop/src/lib/__tests__/imports/dbeaverImport.spec.ts
+++ b/apps/desktop/src/lib/__tests__/imports/dbeaverImport.spec.ts
@@ -140,3 +140,36 @@ describe("DBeaver Cloudberry import", () => {
});
});
});
+
+describe("DBeaver OpenTenBase import", () => {
+ it("preserves OpenTenBase while reusing the PostgreSQL backend", async () => {
+ const connections = await parseDbeaverConnections(
+ payload({
+ connections: {
+ opentenbase: {
+ id: "opentenbase",
+ name: "distributed-postgres",
+ provider: "opentenbase",
+ driver: "opentenbase-postgresql",
+ configuration: {
+ host: "cn.example.com",
+ port: 11000,
+ database: "postgres",
+ user: "opentenbase",
+ },
+ },
+ },
+ }),
+ );
+
+ expect(connections[0]).toMatchObject({
+ db_type: "postgres",
+ driver_profile: "opentenbase",
+ driver_label: "OpenTenBase",
+ host: "cn.example.com",
+ port: 11000,
+ database: "postgres",
+ username: "opentenbase",
+ });
+ });
+});
diff --git a/apps/desktop/src/lib/connection/connectionUrl.ts b/apps/desktop/src/lib/connection/connectionUrl.ts
index dd08222ff4..208516905f 100644
--- a/apps/desktop/src/lib/connection/connectionUrl.ts
+++ b/apps/desktop/src/lib/connection/connectionUrl.ts
@@ -34,6 +34,7 @@ const SCHEME_PROFILES: Record = {
postgres: { type: "postgres", profile: "postgres", label: "PostgreSQL", defaultPort: 5432 },
postgresql: { type: "postgres", profile: "postgres", label: "PostgreSQL", defaultPort: 5432 },
cloudberry: { type: "postgres", profile: "cloudberry", label: "Apache Cloudberry", defaultPort: 5432 },
+ opentenbase: { type: "postgres", profile: "opentenbase", label: "OpenTenBase", defaultPort: 11000 },
redshift: { type: "redshift", profile: "redshift", label: "Redshift", defaultPort: 5439 },
redis: { type: "redis", profile: "redis", label: "Redis", defaultPort: 6379 },
rediss: { type: "redis", profile: "redis", label: "Redis", defaultPort: 6379 },
@@ -325,9 +326,10 @@ export function connectionProfileForScheme(scheme: string, preferredProfile?: st
if ((normalizedScheme === "http" || normalizedScheme === "https") && normalizedPreferredProfile) {
return HTTP_SELECTED_PROFILES[normalizedPreferredProfile];
}
- // Cloudberry uses PostgreSQL URLs, so keep the selected product profile when parsing a pasted URL.
- if ((normalizedScheme === "postgres" || normalizedScheme === "postgresql") && normalizedPreferredProfile === "cloudberry") {
- return SCHEME_PROFILES.cloudberry;
+ // PostgreSQL-compatible products use standard PostgreSQL URLs, so keep the
+ // selected product profile when parsing a pasted URL.
+ if ((normalizedScheme === "postgres" || normalizedScheme === "postgresql") && (normalizedPreferredProfile === "cloudberry" || normalizedPreferredProfile === "opentenbase")) {
+ return SCHEME_PROFILES[normalizedPreferredProfile];
}
return SCHEME_PROFILES[normalizedScheme];
}
diff --git a/apps/desktop/src/lib/imports/dbeaverImport.ts b/apps/desktop/src/lib/imports/dbeaverImport.ts
index a5ca39ce7d..1bb5023e29 100644
--- a/apps/desktop/src/lib/imports/dbeaverImport.ts
+++ b/apps/desktop/src/lib/imports/dbeaverImport.ts
@@ -42,6 +42,7 @@ const profileMap: Record = {
postgresql: { dbType: "postgres", profile: "postgres", label: "PostgreSQL", port: 5432, user: "postgres" },
postgres: { dbType: "postgres", profile: "postgres", label: "PostgreSQL", port: 5432, user: "postgres" },
cloudberry: { dbType: "postgres", profile: "cloudberry", label: "Apache Cloudberry", port: 5432, user: "postgres" },
+ opentenbase: { dbType: "postgres", profile: "opentenbase", label: "OpenTenBase", port: 11000, user: "opentenbase" },
sqlite: { dbType: "sqlite", profile: "sqlite", label: "SQLite", port: 0, user: "" },
sqlserver: { dbType: "sqlserver", profile: "sqlserver", label: "SQL Server", port: 1433, user: "sa" },
mssql: { dbType: "sqlserver", profile: "sqlserver", label: "SQL Server", port: 1433, user: "sa" },
@@ -82,6 +83,7 @@ function getNumber(value: unknown) {
function inferProfile(entry: DbeaverConnectionEntry): ConnectionProfile {
if (/^jdbcx:/i.test(getString(entry.configuration?.url))) return profileMap.jdbcx;
+ if (normalizeKey(entry.provider) === "opentenbase") return profileMap.opentenbase;
const driverProfile = profileMap[normalizeKey(entry.driver)];
if (driverProfile) return driverProfile;
const candidates = [entry.provider, entry.driver, entry.configuration?.url, entry.name].map(normalizeKey).join(" ");
diff --git a/crates/dbx-core/src/db/mod.rs b/crates/dbx-core/src/db/mod.rs
index a500b55ad4..020e6eb751 100644
--- a/crates/dbx-core/src/db/mod.rs
+++ b/crates/dbx-core/src/db/mod.rs
@@ -25,6 +25,7 @@ pub mod mysql;
pub mod mysql_compatible;
pub mod ob_oracle;
pub mod oceanbase_mysql;
+pub mod opentenbase;
pub mod postgres;
pub mod proxy_tunnel;
pub mod questdb;
diff --git a/crates/dbx-core/src/db/opentenbase.rs b/crates/dbx-core/src/db/opentenbase.rs
new file mode 100644
index 0000000000..573cfeb87e
--- /dev/null
+++ b/crates/dbx-core/src/db/opentenbase.rs
@@ -0,0 +1,151 @@
+use deadpool_postgres::Pool;
+
+use crate::db;
+use crate::models::connection::ConnectionConfig;
+
+pub const DRIVER_PROFILE: &str = "opentenbase";
+
+const TABLE_DISTRIBUTION_SQL: &str = "SELECT c.pclocatortype::text, \
+ COALESCE(( \
+ SELECT string_agg(a.attname, E'\\n' ORDER BY distributed_column.ordinality) \
+ FROM unnest(c.discolnums::smallint[]) WITH ORDINALITY \
+ AS distributed_column(attnum, ordinality) \
+ JOIN pg_catalog.pg_attribute a \
+ ON a.attrelid = c.pcrelid AND a.attnum = distributed_column.attnum \
+ WHERE NOT a.attisdropped \
+ ), '')::text \
+ FROM pg_catalog.pgxc_class c \
+ JOIN pg_catalog.pg_class relation ON relation.oid = c.pcrelid \
+ JOIN pg_catalog.pg_namespace namespace ON namespace.oid = relation.relnamespace \
+ WHERE namespace.nspname = $1 AND relation.relname = $2 \
+ LIMIT 1";
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum DistributionPolicy {
+ Hash(Vec),
+ Modulo(Vec),
+ Shard(Vec),
+ Replication,
+ RoundRobin,
+}
+
+pub fn is_config(config: &ConnectionConfig) -> bool {
+ matches!(config.driver_profile.as_deref(), Some(DRIVER_PROFILE))
+}
+
+pub async fn table_distribution(pool: &Pool, schema: &str, table: &str) -> Result