mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(opentenbase): add PostgreSQL-compatible database support
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<!-- OpenTenBase symbol adapted from the official docs main logo. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 194 129" fill="none">
|
||||
<path d="M36.9849 64.0449L73.9615 128.09H0.00836182L36.9849 64.0449Z" fill="#006EFF"/>
|
||||
<path d="M73.9532 0L110.93 64.0453H36.9766L73.9532 0Z" fill="#006EFF"/>
|
||||
<path d="M156.123 0L193.1 64.0453H119.147L156.123 0Z" fill="#1EECFF"/>
|
||||
<path d="M73.9523 128.079L110.929 64.0335H36.9758L73.9523 128.079Z" fill="#00A4FF"/>
|
||||
<path d="M36.9766 64.0453L73.9532 0H0.0000610352L36.9766 64.0453Z" fill="#00A4FF"/>
|
||||
<path d="M119.147 64.0453L156.123 0H82.1703L119.147 64.0453Z" fill="#00C8DC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 642 B |
@@ -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<string, string> = {
|
||||
mysql: "mysql",
|
||||
postgres: "postgres",
|
||||
cloudberry: "cloudberry",
|
||||
opentenbase: "opentenbase",
|
||||
sqlite: "sqlite",
|
||||
rqlite: "rqlite",
|
||||
turso: "turso",
|
||||
@@ -2813,6 +2822,7 @@ const iconTypeMap: Record<string, string> = {
|
||||
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",
|
||||
|
||||
@@ -14,6 +14,7 @@ const assetIcons: Record<string, string> = {
|
||||
postgres: "postgres",
|
||||
postgresql: "postgres",
|
||||
cloudberry: "cloudberry",
|
||||
opentenbase: "opentenbase",
|
||||
sqlite: "sqlite",
|
||||
rqlite: "rqlite.png",
|
||||
turso: "turso.png",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,6 +34,7 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ const profileMap: Record<string, ConnectionProfile> = {
|
||||
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(" ");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String>),
|
||||
Modulo(Vec<String>),
|
||||
Shard(Vec<String>),
|
||||
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<Option<DistributionPolicy>, String> {
|
||||
let client = db::postgres::checkout_postgres_client(pool, None, super::connection_timeout()).await?;
|
||||
let Some(row) =
|
||||
client.query_opt(TABLE_DISTRIBUTION_SQL, &[&schema, &table]).await.map_err(|error| error.to_string())?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let locator_type = row.try_get::<_, String>(0).map_err(|error| error.to_string())?;
|
||||
let columns = row
|
||||
.try_get::<_, String>(1)
|
||||
.map_err(|error| error.to_string())?
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|column| !column.is_empty())
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>();
|
||||
parse_distribution_policy(&locator_type, columns)
|
||||
}
|
||||
|
||||
pub fn append_distribution_clause(ddl: &str, distribution: &DistributionPolicy) -> Result<String, String> {
|
||||
let clause = render_distribution_clause(distribution)?;
|
||||
let insertion = ddl
|
||||
.find(";\n")
|
||||
.or_else(|| ddl.find(';'))
|
||||
.ok_or_else(|| "OpenTenBase DDL has no CREATE TABLE terminator".to_string())?;
|
||||
let mut output = String::with_capacity(ddl.len() + clause.len() + 2);
|
||||
output.push_str(&ddl[..insertion]);
|
||||
output.push('\n');
|
||||
output.push_str(&clause);
|
||||
output.push_str(&ddl[insertion..]);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn parse_distribution_policy(locator_type: &str, columns: Vec<String>) -> Result<Option<DistributionPolicy>, String> {
|
||||
let column_distribution = |kind| {
|
||||
if columns.is_empty() {
|
||||
Err(format!("OpenTenBase {kind} distribution has no distribution columns"))
|
||||
} else {
|
||||
Ok(columns)
|
||||
}
|
||||
};
|
||||
match locator_type {
|
||||
"H" => column_distribution("HASH").map(|columns| Some(DistributionPolicy::Hash(columns))),
|
||||
"M" => column_distribution("MODULO").map(|columns| Some(DistributionPolicy::Modulo(columns))),
|
||||
"S" => column_distribution("SHARD").map(|columns| Some(DistributionPolicy::Shard(columns))),
|
||||
"R" => Ok(Some(DistributionPolicy::Replication)),
|
||||
"N" => Ok(Some(DistributionPolicy::RoundRobin)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_distribution_clause(distribution: &DistributionPolicy) -> Result<String, String> {
|
||||
let render_columns = |kind: &str, columns: &[String]| {
|
||||
if columns.is_empty() {
|
||||
return Err(format!("OpenTenBase {kind} distribution has no distribution columns"));
|
||||
}
|
||||
Ok(format!(
|
||||
"DISTRIBUTE BY {kind} ({})",
|
||||
columns.iter().map(|column| db::postgres::pg_quote_ident(column)).collect::<Vec<_>>().join(", ")
|
||||
))
|
||||
};
|
||||
match distribution {
|
||||
DistributionPolicy::Hash(columns) => render_columns("HASH", columns),
|
||||
DistributionPolicy::Modulo(columns) => render_columns("MODULO", columns),
|
||||
DistributionPolicy::Shard(columns) => render_columns("SHARD", columns),
|
||||
DistributionPolicy::Replication => Ok("DISTRIBUTE BY REPLICATION".to_string()),
|
||||
DistributionPolicy::RoundRobin => Ok("DISTRIBUTE BY ROUNDROBIN".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn renders_hash_distribution_before_create_table_terminator() {
|
||||
let ddl = "CREATE TABLE \"public\".\"orders\" (\n \"tenant_id\" integer\n);\n";
|
||||
let rendered = append_distribution_clause(
|
||||
ddl,
|
||||
&DistributionPolicy::Hash(vec!["tenant_id".to_string(), "order id".to_string()]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
"CREATE TABLE \"public\".\"orders\" (\n \"tenant_id\" integer\n)\nDISTRIBUTE BY HASH (\"tenant_id\", \"order id\");\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_replication_and_round_robin_distribution() {
|
||||
assert_eq!(render_distribution_clause(&DistributionPolicy::Replication).unwrap(), "DISTRIBUTE BY REPLICATION");
|
||||
assert_eq!(render_distribution_clause(&DistributionPolicy::RoundRobin).unwrap(), "DISTRIBUTE BY ROUNDROBIN");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_current_opentenbase_locator_types() {
|
||||
assert_eq!(
|
||||
parse_distribution_policy("S", vec!["tenant_id".to_string()]).unwrap(),
|
||||
Some(DistributionPolicy::Shard(vec!["tenant_id".to_string()]))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_distribution_policy("M", vec!["tenant_id".to_string()]).unwrap(),
|
||||
Some(DistributionPolicy::Modulo(vec!["tenant_id".to_string()]))
|
||||
);
|
||||
assert_eq!(parse_distribution_policy("D", Vec::new()).unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distribution_query_matches_opentenbase_catalog_contract() {
|
||||
assert!(TABLE_DISTRIBUTION_SQL.contains("pg_catalog.pgxc_class"));
|
||||
assert!(TABLE_DISTRIBUTION_SQL.contains("c.discolnums::smallint[]"));
|
||||
assert!(TABLE_DISTRIBUTION_SQL.contains("WITH ORDINALITY"));
|
||||
assert!(TABLE_DISTRIBUTION_SQL.contains("NOT a.attisdropped"));
|
||||
}
|
||||
}
|
||||
@@ -2554,14 +2554,29 @@ pub async fn completion_assistant_search(
|
||||
}
|
||||
|
||||
if candidates.len() < limit && kinds.iter().any(CompletionAssistantObjectKind::is_routine_like) {
|
||||
let prokinds = postgres_completion_prokinds(&kinds);
|
||||
let rows = postgres_query_cached(
|
||||
&client,
|
||||
postgres_completion_routines_sql(),
|
||||
&[&routine_schema, &pattern, &prokinds, &((limit - candidates.len()) as i64)],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let has_proc_prokind = postgres_proc_has_prokind(&client).await?;
|
||||
let rows = if has_proc_prokind {
|
||||
let prokinds = postgres_completion_prokinds(&kinds);
|
||||
postgres_query_cached(
|
||||
&client,
|
||||
postgres_completion_routines_sql(true),
|
||||
&[&routine_schema, &pattern, &prokinds, &((limit - candidates.len()) as i64)],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
} else if kinds.iter().any(|kind| {
|
||||
matches!(kind, CompletionAssistantObjectKind::Function | CompletionAssistantObjectKind::Routine)
|
||||
}) {
|
||||
postgres_query_cached(
|
||||
&client,
|
||||
postgres_completion_routines_sql(false),
|
||||
&[&routine_schema, &pattern, &((limit - candidates.len()) as i64)],
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
for row in rows {
|
||||
let routine_type: String = pg_row_try_string(&row, 2);
|
||||
candidates.push(CompletionAssistantCandidate {
|
||||
@@ -2665,15 +2680,26 @@ fn postgres_completion_tables_sql() -> &'static str {
|
||||
ORDER BY c.relname LIMIT $4"
|
||||
}
|
||||
|
||||
fn postgres_completion_routines_sql() -> &'static str {
|
||||
"SELECT p.proname, n.nspname, CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END, \
|
||||
fn postgres_completion_routines_sql(has_proc_prokind: bool) -> &'static str {
|
||||
if has_proc_prokind {
|
||||
return "SELECT p.proname, n.nspname, CASE p.prokind WHEN 'p' THEN 'PROCEDURE' ELSE 'FUNCTION' END, \
|
||||
obj_description(p.oid) AS routine_comment, COALESCE(pg_get_function_result(p.oid), '') AS data_type, \
|
||||
pg_get_function_identity_arguments(p.oid) AS signature \
|
||||
FROM pg_catalog.pg_proc p \
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace \
|
||||
WHERE n.nspname = $1 AND p.prokind::text = ANY($3::text[]) \
|
||||
AND ($2 = '%%' OR p.proname ILIKE $2 ESCAPE '~') \
|
||||
ORDER BY p.proname LIMIT $4"
|
||||
ORDER BY p.proname LIMIT $4";
|
||||
}
|
||||
|
||||
"SELECT p.proname, n.nspname, 'FUNCTION'::text, \
|
||||
obj_description(p.oid) AS routine_comment, COALESCE(pg_get_function_result(p.oid), '') AS data_type, \
|
||||
pg_get_function_identity_arguments(p.oid) AS signature \
|
||||
FROM pg_catalog.pg_proc p \
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace \
|
||||
WHERE n.nspname = $1 \
|
||||
AND ($2 = '%%' OR p.proname ILIKE $2 ESCAPE '~') \
|
||||
ORDER BY p.proname LIMIT $3"
|
||||
}
|
||||
|
||||
fn postgres_completion_sequences_sql() -> &'static str {
|
||||
@@ -9456,10 +9482,15 @@ mod tests {
|
||||
assert!(postgres_completion_tables_sql().contains("pg_catalog.pg_table_is_visible(c.oid)"));
|
||||
assert!(postgres_completion_tables_sql().contains("c.relkind::text = ANY($3::text[])"));
|
||||
assert!(postgres_completion_tables_sql().contains("ORDER BY c.relname LIMIT $4"));
|
||||
assert!(postgres_completion_routines_sql().contains("p.proname ILIKE $2 ESCAPE '~'"));
|
||||
assert!(postgres_completion_routines_sql().contains("p.prokind::text = ANY($3::text[])"));
|
||||
assert!(postgres_completion_routines_sql().contains("pg_get_function_identity_arguments(p.oid) AS signature"));
|
||||
assert!(postgres_completion_routines_sql().contains("ORDER BY p.proname LIMIT $4"));
|
||||
assert!(postgres_completion_routines_sql(true).contains("p.proname ILIKE $2 ESCAPE '~'"));
|
||||
assert!(postgres_completion_routines_sql(true).contains("p.prokind::text = ANY($3::text[])"));
|
||||
assert!(
|
||||
postgres_completion_routines_sql(true).contains("pg_get_function_identity_arguments(p.oid) AS signature")
|
||||
);
|
||||
assert!(postgres_completion_routines_sql(true).contains("ORDER BY p.proname LIMIT $4"));
|
||||
assert!(!postgres_completion_routines_sql(false).contains("p.prokind"));
|
||||
assert!(postgres_completion_routines_sql(false).contains("'FUNCTION'::text"));
|
||||
assert!(postgres_completion_routines_sql(false).contains("ORDER BY p.proname LIMIT $3"));
|
||||
assert!(postgres_completion_sequences_sql().contains("c.relkind = 'S'"));
|
||||
assert!(postgres_completion_sequences_sql().contains("has_schema_privilege(n.oid, 'USAGE')"));
|
||||
assert!(postgres_completion_sequences_sql().contains("pg_catalog.pg_table_is_visible(c.oid)"));
|
||||
|
||||
@@ -6616,6 +6616,9 @@ async fn get_table_ddl_once(
|
||||
PoolKind::Postgres(p) if db_config.as_ref().is_some_and(is_cloudberry_config) => {
|
||||
cloudberry_ddl(p, schema, table).await
|
||||
}
|
||||
PoolKind::Postgres(p) if db_config.as_ref().is_some_and(db::opentenbase::is_config) => {
|
||||
opentenbase_ddl(p, schema, table).await
|
||||
}
|
||||
PoolKind::Postgres(p)
|
||||
if include_postgres_access && db_config.as_ref().is_some_and(is_native_postgres_config) =>
|
||||
{
|
||||
@@ -9451,6 +9454,34 @@ pub async fn cloudberry_ddl(pool: &deadpool_postgres::Pool, schema: &str, table:
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn opentenbase_ddl(pool: &deadpool_postgres::Pool, schema: &str, table: &str) -> Result<String, String> {
|
||||
let ddl = pg_ddl(pool, schema, table).await?;
|
||||
match db::opentenbase::table_distribution(pool, schema, table).await {
|
||||
Ok(Some(distribution)) => match db::opentenbase::append_distribution_clause(&ddl, &distribution) {
|
||||
Ok(ddl) => Ok(ddl),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"[schema][opentenbase:table-ddl-distribution-render-fallback] schema={} table={} error={}",
|
||||
schema,
|
||||
table,
|
||||
error
|
||||
);
|
||||
Ok(ddl)
|
||||
}
|
||||
},
|
||||
Ok(None) => Ok(ddl),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"[schema][opentenbase:table-ddl-distribution-query-fallback] schema={} table={} error={}",
|
||||
schema,
|
||||
table,
|
||||
error
|
||||
);
|
||||
Ok(ddl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_postgres_table_ddl(
|
||||
schema: &str,
|
||||
table: &str,
|
||||
|
||||
@@ -260,6 +260,9 @@ pub(in crate::schema) async fn table_ddl(
|
||||
PoolKind::Postgres(p) if config.is_some_and(is_cloudberry_config) => {
|
||||
super::super::cloudberry_ddl(p, schema, table).await
|
||||
}
|
||||
PoolKind::Postgres(p) if config.is_some_and(db::opentenbase::is_config) => {
|
||||
super::super::opentenbase_ddl(p, schema, table).await
|
||||
}
|
||||
PoolKind::Postgres(p) => super::super::pg_ddl(p, schema, table).await,
|
||||
PoolKind::Sqlite(p) => super::super::sqlite_ddl(p, schema, table).await,
|
||||
PoolKind::Rqlite(client) => db::rqlite_driver::table_ddl(client, table).await,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use dbx_core::{db, schema};
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires DBX_TEST_OPENTENBASE_URL pointing at a writable OpenTenBase Coordinator"]
|
||||
async fn opentenbase_metadata_and_ddl_round_trip() {
|
||||
let url = std::env::var("DBX_TEST_OPENTENBASE_URL").expect("DBX_TEST_OPENTENBASE_URL");
|
||||
let pool = db::postgres::connect(&url, Duration::from_secs(10)).await.expect("connect OpenTenBase Coordinator");
|
||||
let suffix = uuid::Uuid::new_v4().simple().to_string();
|
||||
let source_schema = format!("dbx_otb_source_{suffix}");
|
||||
let target_schema = format!("dbx_otb_target_{suffix}");
|
||||
let source_ident = quote_ident(&source_schema);
|
||||
let target_ident = quote_ident(&target_schema);
|
||||
|
||||
db::postgres::execute_batch(
|
||||
&pool,
|
||||
&[
|
||||
format!("CREATE SCHEMA {source_ident}"),
|
||||
format!("CREATE SCHEMA {target_ident}"),
|
||||
format!(
|
||||
"CREATE TABLE {source_ident}.shard_events \
|
||||
(tenant_id integer, event_id bigint, payload text) \
|
||||
DISTRIBUTE BY SHARD (tenant_id)"
|
||||
),
|
||||
format!(
|
||||
"CREATE TABLE {source_ident}.replicated_dimensions \
|
||||
(id integer, name text) DISTRIBUTE BY REPLICATION"
|
||||
),
|
||||
],
|
||||
)
|
||||
.await
|
||||
.expect("create OpenTenBase fixtures");
|
||||
|
||||
let exercise = async {
|
||||
let tables = db::postgres::list_tables(&pool, &source_schema).await?;
|
||||
if !tables.iter().any(|table| table.name == "shard_events") {
|
||||
return Err("shard_events was not listed".to_string());
|
||||
}
|
||||
if !tables.iter().any(|table| table.name == "replicated_dimensions") {
|
||||
return Err("replicated_dimensions was not listed".to_string());
|
||||
}
|
||||
|
||||
let cases = [
|
||||
("shard_events", "DISTRIBUTE BY SHARD (\"tenant_id\")"),
|
||||
("replicated_dimensions", "DISTRIBUTE BY REPLICATION"),
|
||||
];
|
||||
for (table, expected) in cases {
|
||||
let ddl = schema::opentenbase_ddl(&pool, &source_schema, table).await?;
|
||||
if !ddl.contains(expected) {
|
||||
return Err(format!("{table} DDL did not contain {expected}: {ddl}"));
|
||||
}
|
||||
let target_ddl = ddl.replace(&source_ident, &target_ident);
|
||||
db::postgres::execute_query(&pool, &target_ddl).await?;
|
||||
}
|
||||
Ok::<_, String>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
db::postgres::execute_batch(
|
||||
&pool,
|
||||
&[format!("DROP SCHEMA {target_ident} CASCADE"), format!("DROP SCHEMA {source_ident} CASCADE")],
|
||||
)
|
||||
.await
|
||||
.expect("drop OpenTenBase fixtures");
|
||||
exercise.expect("validate OpenTenBase metadata and DDL");
|
||||
}
|
||||
|
||||
fn quote_ident(value: &str) -> String {
|
||||
format!("\"{}\"", value.replace('"', "\"\""))
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const databaseSupport = [
|
||||
{ name: "MySQL", icon: "/icons/database/mysql.svg", tone: "#4479a1" },
|
||||
{ name: "PostgreSQL", icon: "/icons/database/postgres.svg", tone: "#4169e1" },
|
||||
{ name: "Cloudberry", icon: "/icons/database/cloudberry.svg", tone: "#ff5900" },
|
||||
{ name: "OpenTenBase", icon: "/icons/database/opentenbase.svg", tone: "#006eff" },
|
||||
{ name: "SQLite", icon: "/icons/database/sqlite.svg", tone: "#5aa6d6" },
|
||||
{ name: "Redis", icon: "/icons/database/redis.svg", tone: "#ff4438" },
|
||||
{ name: "DuckDB", icon: "/icons/database/duckdb.svg", tone: "#fff000" },
|
||||
|
||||
@@ -50,6 +50,7 @@ const databaseSupport = [
|
||||
{ name: "MySQL", icon: "/icons/database/mysql.svg", tone: "#4479a1" },
|
||||
{ name: "PostgreSQL", icon: "/icons/database/postgres.svg", tone: "#4169e1" },
|
||||
{ name: "Cloudberry", icon: "/icons/database/cloudberry.svg", tone: "#ff5900" },
|
||||
{ name: "OpenTenBase", icon: "/icons/database/opentenbase.svg", tone: "#006eff" },
|
||||
{ name: "SQLite", icon: "/icons/database/sqlite.svg", tone: "#5aa6d6" },
|
||||
{ name: "Redis", icon: "/icons/database/redis.svg", tone: "#ff4438" },
|
||||
{ name: "DuckDB", icon: "/icons/database/duckdb.svg", tone: "#fff000" },
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- OpenTenBase symbol adapted from the official docs main logo. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 194 129" fill="none">
|
||||
<path d="M36.9849 64.0449L73.9615 128.09H0.00836182L36.9849 64.0449Z" fill="#006EFF"/>
|
||||
<path d="M73.9532 0L110.93 64.0453H36.9766L73.9532 0Z" fill="#006EFF"/>
|
||||
<path d="M156.123 0L193.1 64.0453H119.147L156.123 0Z" fill="#1EECFF"/>
|
||||
<path d="M73.9523 128.079L110.929 64.0335H36.9758L73.9523 128.079Z" fill="#00A4FF"/>
|
||||
<path d="M36.9766 64.0453L73.9532 0H0.0000610352L36.9766 64.0453Z" fill="#00A4FF"/>
|
||||
<path d="M119.147 64.0453L156.123 0H82.1703L119.147 64.0453Z" fill="#00C8DC"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 642 B |
Reference in New Issue
Block a user