feat(opentenbase): add PostgreSQL-compatible database support

This commit is contained in:
t8y2
2026-08-14 23:40:50 +08:00
parent 722b6e7d19
commit 2d526db91b
16 changed files with 412 additions and 19 deletions
@@ -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(" ");
+1
View File
@@ -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;
+151
View File
@@ -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"));
}
}
+46 -15
View File
@@ -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)"));
+31
View File
@@ -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,
+71
View File
@@ -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('"', "\"\""))
}
+1
View File
@@ -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" },
+1
View File
@@ -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