diff --git a/README.md b/README.md
index 9a6271adb..0e1ecd3a9 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@
+
@@ -87,7 +88,7 @@
### 50+ Databases, One Tool
-MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, and more. Agent/JDBC-oriented profiles extend DBX to H2, Snowflake, Trino, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, and custom JDBC connections. New native and agent-driven drivers also cover Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase, Databend, RQLite, Turso, InfluxDB, IoTDB, etcd, IRIS, and more. All in a single ~15 MB app. No bundled Chromium.
+MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, and more. Agent/JDBC-oriented profiles extend DBX to H2, Snowflake, Trino, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, and custom JDBC connections. New native and agent-driven drivers also cover Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase, Databend, RQLite, Turso, InfluxDB, QuestDB, IoTDB, etcd, IRIS, and more. All in a single ~15 MB app. No bundled Chromium.
### Query Editor
@@ -324,7 +325,7 @@ DBX is 15 MB with no runtime dependencies (no Java, no Python). It includes AI a
What databases are supported?
-MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, and more. Agent/JDBC-oriented profiles extend support to H2, Snowflake, Trino, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase, Databend, RQLite, Turso, InfluxDB, IoTDB, etcd, IRIS, and custom JDBC connections.
+MySQL, PostgreSQL, SQLite, Redis, MongoDB, DuckDB, ClickHouse, SQL Server, Oracle, Elasticsearch, MariaDB, TiDB, OceanBase, openGauss, GaussDB, KWDB, KingBase, Vastbase, GoldenDB, Doris, SelectDB, StarRocks, Manticore Search, Redshift, DM, TDengine, XuguDB, CockroachDB, Access, HighGo, and more. Agent/JDBC-oriented profiles extend support to H2, Snowflake, Trino, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, Databricks, SAP HANA, Teradata, Vertica, Firebird, Exasol, YashanDB, GBase, Databend, RQLite, Turso, InfluxDB, QuestDB, IoTDB, etcd, IRIS, and custom JDBC connections.
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 3610cae54..eb7b1d13f 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -37,6 +37,7 @@
+
@@ -87,7 +88,7 @@
### 50+ 种数据库,一个工具搞定
-MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo 等数据库都能直接连接。Agent/JDBC 方向的配置还可扩展到 H2、Snowflake、Trino、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB 和自定义 JDBC。新增的原生与 Agent 驱动还覆盖了 Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase、Databend、RQLite、Turso、InfluxDB、IoTDB、etcd、IRIS 等。全部装进约 15 MB 的应用里,不内嵌 Chromium。
+MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo 等数据库都能直接连接。Agent/JDBC 方向的配置还可扩展到 H2、Snowflake、Trino、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB 和自定义 JDBC。新增的原生与 Agent 驱动还覆盖了 Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase、Databend、RQLite、Turso、InfluxDB、QuestDB、IoTDB、etcd、IRIS 等。全部装进约 15 MB 的应用里,不内嵌 Chromium。
### 查询编辑器
@@ -320,7 +321,7 @@ DBX 仅 15 MB,无需运行时依赖(无需 Java、无需 Python)。AI 和
支持哪些数据库?
-MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo 等。JDBC 方向配置可扩展到 H2、Snowflake、Trino、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase、Databend、RQLite、Turso、InfluxDB、IoTDB、etcd、IRIS 及自定义 JDBC 连接。
+MySQL、PostgreSQL、SQLite、Redis、MongoDB、DuckDB、ClickHouse、SQL Server、Oracle、Elasticsearch、MariaDB、TiDB、OceanBase、openGauss、GaussDB、KWDB、KingBase、Vastbase、GoldenDB、Doris、SelectDB、StarRocks、Manticore Search、Redshift、DM、TDengine、虚谷 XuguDB、CockroachDB、Access、HighGo 等。JDBC 方向配置可扩展到 H2、Snowflake、Trino、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、Databricks、SAP HANA、Teradata、Vertica、Firebird、Exasol、崖山 YashanDB、GBase、Databend、RQLite、Turso、InfluxDB、QuestDB、IoTDB、etcd、IRIS 及自定义 JDBC 连接。
diff --git a/apps/desktop/public/icons/database/questdb.svg b/apps/desktop/public/icons/database/questdb.svg
new file mode 100644
index 000000000..48bff1499
--- /dev/null
+++ b/apps/desktop/public/icons/database/questdb.svg
@@ -0,0 +1,25 @@
+
diff --git a/apps/desktop/src/components/connection/ConnectionDialog.vue b/apps/desktop/src/components/connection/ConnectionDialog.vue
index a18720150..615505054 100644
--- a/apps/desktop/src/components/connection/ConnectionDialog.vue
+++ b/apps/desktop/src/components/connection/ConnectionDialog.vue
@@ -401,6 +401,7 @@ const driverProfiles: Record<
},
gaussdb: { type: "gaussdb", port: 5432, user: "gaussdb", label: "GaussDB", icon: "gaussdb" },
kwdb: { type: "kwdb", port: 26257, user: "root", label: "KWDB", icon: "kwdb" },
+ questdb: { type: "questdb", port: 8812, user: "questdb", label: "QuestDB", icon: "questdb" },
kingbase: { type: "kingbase", port: 54321, user: "system", label: "KingBase", icon: "kingbase" },
highgo: { type: "highgo", port: 5866, user: "highgo", label: "瀚高 HighGo", icon: "highgo" },
yashandb: { type: "yashandb", port: 1688, user: "sys", label: "崖山 YashanDB", icon: "yashandb" },
@@ -737,6 +738,7 @@ const iconTypeMap: Record = {
opengauss: "opengauss",
gaussdb: "gaussdb",
kwdb: "kwdb",
+ questdb: "questdb",
kingbase: "kingbase",
highgo: "highgo",
yashandb: "yashandb",
@@ -789,6 +791,7 @@ const dbOptions: DbOption[] = [
{ value: "clickhouse", label: "ClickHouse" },
{ value: "gaussdb", label: "GaussDB" },
{ value: "kwdb", label: "KWDB" },
+ { value: "questdb", label: "QuestDB" },
{ value: "tidb", label: "TiDB" },
{ value: "oceanbase", label: "OceanBase" },
{ value: "goldendb", label: "GoldenDB" },
@@ -878,7 +881,7 @@ const sqliteExtensionPaths = computed({
form.value.url_params = setSqliteExtensionPaths(form.value.url_params, value);
},
});
-const tlsCapableDatabaseTypes = new Set(["mysql", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "redis", "etcd", "clickhouse", "elasticsearch", "influxdb"]);
+const tlsCapableDatabaseTypes = new Set(["mysql", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb", "redis", "etcd", "clickhouse", "elasticsearch", "influxdb"]);
const supportsTlsToggle = computed(() => tlsCapableDatabaseTypes.has(form.value.db_type));
const supportsCaCertificatePath = computed(() => form.value.db_type === "clickhouse");
const supportsGenericUrlParams = computed(() => form.value.db_type !== "manticoresearch");
diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue
index 469593e6c..d81ba5d6f 100644
--- a/apps/desktop/src/components/icons/DatabaseIcon.vue
+++ b/apps/desktop/src/components/icons/DatabaseIcon.vue
@@ -30,6 +30,7 @@ const assetIcons: Record = {
oceanbase: "oceanbase",
opengauss: "opengauss",
gaussdb: "gaussdb",
+ questdb: "questdb",
kwdb: "kwdb",
kingbase: "kingbase",
highgo: "highgo.png",
diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue
index 9a713e666..921aedb2d 100644
--- a/apps/desktop/src/components/objects/ObjectBrowser.vue
+++ b/apps/desktop/src/components/objects/ObjectBrowser.vue
@@ -167,6 +167,7 @@ const sourceFormatDialect = computed(() => {
case "gaussdb":
case "kwdb":
case "opengauss":
+ case "questdb":
return "postgres";
default:
return "generic";
diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue
index ada084f44..ca1b84311 100644
--- a/apps/desktop/src/components/structure/TableStructureEditor.vue
+++ b/apps/desktop/src/components/structure/TableStructureEditor.vue
@@ -37,6 +37,7 @@ import {
getColumnEditorControls,
getDataTypeOptions,
getDefaultLengthForType,
+ isDataTypeLengthDisabled,
isProtectedManticoreIdColumn,
splitDataType,
toColumnNames,
@@ -509,10 +510,11 @@ function isColumnTypeDisabled(column: EditableStructureColumn): boolean {
}
function isColumnLengthDisabled(column: EditableStructureColumn): boolean {
- if (isColumnTypeDisabled(column)) return true;
- if (databaseType.value !== "manticoresearch") return false;
+ if (isColumnTypeDisabled(column)) {
+ return true;
+ }
const baseType = splitDataType(column.dataType).baseType.trim().toLowerCase();
- return baseType !== "bit" && baseType !== "float_vector";
+ return isDataTypeLengthDisabled(databaseType.value, baseType);
}
function isColumnNullableDisabled(column: EditableStructureColumn): boolean {
diff --git a/apps/desktop/src/lib/connectionPresentation.ts b/apps/desktop/src/lib/connectionPresentation.ts
index 1912e6c8e..c876878e6 100644
--- a/apps/desktop/src/lib/connectionPresentation.ts
+++ b/apps/desktop/src/lib/connectionPresentation.ts
@@ -90,6 +90,7 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string {
case "kwdb":
case "yashandb":
case "redshift":
+ case "questdb":
return "postgresql://user:password@host:port/database";
case "redis":
diff --git a/apps/desktop/src/lib/connectionUrl.ts b/apps/desktop/src/lib/connectionUrl.ts
index 1ab19491e..7c4454317 100644
--- a/apps/desktop/src/lib/connectionUrl.ts
+++ b/apps/desktop/src/lib/connectionUrl.ts
@@ -48,6 +48,7 @@ const SCHEME_PROFILES: Record = {
"informix-sqli": { type: "informix", profile: "informix", label: "Informix", defaultPort: 9088 },
yashandb: { type: "yashandb", profile: "yashandb", label: "YashanDB", defaultPort: 1688 },
opengauss: { type: "gaussdb", profile: "opengauss", label: "openGauss", defaultPort: 5432 },
+ questdb: { type: "questdb", profile: "questdb", label: "QuestDB", defaultPort: 8812 },
tdengine: { type: "tdengine", profile: "tdengine", label: "TDengine", defaultPort: 6041 },
"taos-ws": { type: "tdengine", profile: "tdengine", label: "TDengine", defaultPort: 6041 },
xugu: { type: "xugu", profile: "xugu", label: "XuguDB", defaultPort: 5138 },
diff --git a/apps/desktop/src/lib/databaseCapabilitySets.ts b/apps/desktop/src/lib/databaseCapabilitySets.ts
index 65b82042e..34ce27f2c 100644
--- a/apps/desktop/src/lib/databaseCapabilitySets.ts
+++ b/apps/desktop/src/lib/databaseCapabilitySets.ts
@@ -35,7 +35,7 @@ export const SCHEMA_AWARE_TYPES = new Set([
"duckdb",
]);
-export const SINGLE_DATABASE_TYPES = new Set(["oracle", "dameng", "firebird", "oceanbase-oracle", "access"]);
+export const SINGLE_DATABASE_TYPES = new Set(["oracle", "dameng", "firebird", "oceanbase-oracle", "access", "questdb"]);
export const FETCH_FIRST_TYPES = new Set(["oracle", "dameng"]);
@@ -71,6 +71,6 @@ export const TREE_SCHEMA_TYPES = new Set([
export const DATABASE_OBJECT_TREE_TYPES = new Set(["jdbc"]);
-export const PG_LIKE_STRUCTURE_TYPES = new Set(["postgres", "redshift", "gaussdb", "kwdb", "opengauss"]);
+export const PG_LIKE_STRUCTURE_TYPES = new Set(["postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb"]);
-export const DIAGRAM_SQL_TYPES = new Set(["mysql", "postgres", "sqlite", "rqlite", "turso", "sqlserver", "oracle", "redshift", "dameng", "gaussdb", "kwdb", "opengauss", "oceanbase-oracle"]);
+export const DIAGRAM_SQL_TYPES = new Set(["mysql", "postgres", "sqlite", "rqlite", "turso", "sqlserver", "oracle", "redshift", "dameng", "gaussdb", "kwdb", "opengauss", "questdb", "oceanbase-oracle"]);
diff --git a/apps/desktop/src/lib/databaseObjectCapabilities.ts b/apps/desktop/src/lib/databaseObjectCapabilities.ts
index d25cd5dd7..1b2edd8e0 100644
--- a/apps/desktop/src/lib/databaseObjectCapabilities.ts
+++ b/apps/desktop/src/lib/databaseObjectCapabilities.ts
@@ -8,14 +8,16 @@ export interface DatabaseObjectCapabilities {
executable: SidebarObjectKind[];
}
+const TABLE_OBJECTS: SidebarObjectKind[] = ["TABLE"];
const TABLE_VIEW_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW"];
const TABLE_FUNCTION_OBJECTS: SidebarObjectKind[] = ["TABLE", "FUNCTION"];
const ROUTINE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION"];
const POSTGRES_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "SEQUENCE"];
const ORACLE_OBJECTS: SidebarObjectKind[] = ["TABLE", "VIEW", "PROCEDURE", "FUNCTION", "PACKAGE", "PACKAGE_BODY"];
+const TABLE_ONLY_TYPES = new Set(["influxdb"]);
const TABLE_FUNCTION_TYPES = new Set(["manticoresearch"]);
-const TABLE_VIEW_ONLY_TYPES = new Set(["sqlite", "rqlite", "turso", "duckdb", "clickhouse", "doris", "starrocks", "databend", "hive", "trino", "cassandra", "bigquery", "kylin", "tdengine", "iotdb", "neo4j"]);
+const TABLE_VIEW_ONLY_TYPES = new Set(["sqlite", "rqlite", "turso", "duckdb", "clickhouse", "doris", "starrocks", "databend", "hive", "trino", "cassandra", "bigquery", "kylin", "tdengine", "iotdb", "neo4j", "questdb"]);
const ORACLE_PACKAGE_TYPES = new Set(["oracle", "oceanbase-oracle"]);
const POSTGRES_SEQUENCE_TYPES = new Set(["postgres", "gaussdb", "kwdb", "opengauss"]);
@@ -32,6 +34,7 @@ export function databaseObjectCapabilities(dbType?: DatabaseType): DatabaseObjec
export function sidebarObjectKindsForDatabase(dbType?: DatabaseType): SidebarObjectKind[] {
if (!dbType) return [...TABLE_VIEW_OBJECTS];
if (ORACLE_PACKAGE_TYPES.has(dbType)) return [...ORACLE_OBJECTS];
+ if (TABLE_ONLY_TYPES.has(dbType)) return [...TABLE_OBJECTS];
if (TABLE_FUNCTION_TYPES.has(dbType)) return [...TABLE_FUNCTION_OBJECTS];
if (TABLE_VIEW_ONLY_TYPES.has(dbType)) return [...TABLE_VIEW_OBJECTS];
if (POSTGRES_SEQUENCE_TYPES.has(dbType)) return [...POSTGRES_OBJECTS];
diff --git a/apps/desktop/src/lib/databaseTableDataCapabilities.ts b/apps/desktop/src/lib/databaseTableDataCapabilities.ts
index 72b1e42ba..ca51f80f5 100644
--- a/apps/desktop/src/lib/databaseTableDataCapabilities.ts
+++ b/apps/desktop/src/lib/databaseTableDataCapabilities.ts
@@ -73,6 +73,7 @@ const NAVICAT_STYLE_TABLE_DATA_TYPES = new Set([
"firebird",
"exasol",
"opengauss",
+ "questdb",
"oceanbase-oracle",
"gbase",
"access",
diff --git a/apps/desktop/src/lib/databaseUserAdmin.ts b/apps/desktop/src/lib/databaseUserAdmin.ts
index 8d9cf6271..3bf119d1d 100644
--- a/apps/desktop/src/lib/databaseUserAdmin.ts
+++ b/apps/desktop/src/lib/databaseUserAdmin.ts
@@ -46,7 +46,7 @@ export interface DatabaseUserAdminProvider {
}
export const MYSQL_USER_ADMIN_TYPES = new Set(["mysql", "goldendb"]);
-export const POSTGRES_USER_ADMIN_TYPES = new Set(["postgres", "gaussdb", "highgo", "kingbase", "kwdb", "opengauss", "vastbase"]);
+export const POSTGRES_USER_ADMIN_TYPES = new Set(["postgres", "gaussdb", "highgo", "kingbase", "kwdb", "opengauss", "questdb", "vastbase"]);
export const MYSQL_COMMON_PRIVILEGES = ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "INDEX", "REFERENCES", "EXECUTE", "SHOW VIEW", "TRIGGER", "EVENT", "CREATE TEMPORARY TABLES"] as const;
diff --git a/apps/desktop/src/lib/dbeaverImport.ts b/apps/desktop/src/lib/dbeaverImport.ts
index 279d61e39..818cd9b67 100644
--- a/apps/desktop/src/lib/dbeaverImport.ts
+++ b/apps/desktop/src/lib/dbeaverImport.ts
@@ -50,6 +50,7 @@ const profileMap: Record = {
gaussdb: { dbType: "gaussdb", profile: "gaussdb", label: "GaussDB", port: 5432, user: "gaussdb" },
kwdb: { dbType: "kwdb", profile: "kwdb", label: "KWDB", port: 26257, user: "root" },
opengauss: { dbType: "gaussdb", profile: "opengauss", label: "openGauss", port: 5432, user: "gaussdb" },
+ questdb: { dbType: "questdb", profile: "questdb", label: "QuestDB", port: 8812, user: "questdb" },
influxdb: { dbType: "influxdb", profile: "influxdb", label: "InfluxDB", port: 8086, user: "" },
};
diff --git a/apps/desktop/src/lib/explainPlan.ts b/apps/desktop/src/lib/explainPlan.ts
index cdc5030aa..ee96060a5 100644
--- a/apps/desktop/src/lib/explainPlan.ts
+++ b/apps/desktop/src/lib/explainPlan.ts
@@ -16,15 +16,15 @@ export interface ExplainPlanNode {
}
export interface ParsedExplainPlan {
- databaseType: "mysql" | "postgres" | "dameng";
+ databaseType: "mysql" | "postgres" | "dameng" | "questdb";
raw: unknown;
nodes: ExplainPlanNode[];
}
export type BuildExplainSqlResult = { ok: true; sql: string } | { ok: false; reason: "unsupported" | "empty" | "unsafe" };
-const SUPPORTED_EXPLAIN_TYPES = new Set(["mysql", "postgres", "dameng"]);
-export function supportsExplainPlan(databaseType?: DatabaseType): databaseType is "mysql" | "postgres" | "dameng" {
+const SUPPORTED_EXPLAIN_TYPES = new Set(["mysql", "postgres", "dameng", "questdb"]);
+export function supportsExplainPlan(databaseType?: DatabaseType): databaseType is "mysql" | "postgres" | "dameng" | "questdb" {
return !!databaseType && supportsDatabaseFeature(databaseType, "sqlExplain") && SUPPORTED_EXPLAIN_TYPES.has(databaseType);
}
@@ -32,9 +32,11 @@ export function buildExplainSql(databaseType: DatabaseType | undefined, sql: str
return api.buildExplainSql({ databaseType, sql }) as Promise;
}
-export function parseExplainResult(databaseType: "mysql" | "postgres" | "dameng", result: QueryResult): ParsedExplainPlan {
+export function parseExplainResult(databaseType: "mysql" | "postgres" | "dameng" | "questdb", result: QueryResult): ParsedExplainPlan {
if (databaseType === "dameng") {
return parseDamengExplain(result);
+ } else if (databaseType === "questdb") {
+ return parseQuestdbExplain(result);
}
const raw = parseExplainCell(result.rows[0]?.[0]);
const nodes = databaseType === "postgres" ? parsePostgresExplain(raw) : parseMysqlExplain(raw);
@@ -511,6 +513,26 @@ function parseMysqlTable(table: Record, id: string): ExplainPla
};
}
+// ── QuestDB explain parser ─────────────────────────────────────────
+function parseQuestdbExplain(result: QueryResult): ParsedExplainPlan {
+ let nodes: ExplainPlanNode[] = result.rows.map((row: unknown[], index: number) => {
+ const text = String(row[0] ?? "");
+ const node: ExplainPlanNode = {
+ id: `plan_${index}`,
+ title: text,
+ nodeType: "Plan",
+ relation: "",
+ index: String(index),
+ cost: undefined,
+ rows: undefined,
+ details: [text],
+ children: [],
+ };
+ return node;
+ });
+ return { databaseType: "questdb", raw: result, nodes };
+}
+
// ── Helpers ───────────────────────────────────────────────────────────
function objectValue(value: unknown): Record | null {
diff --git a/apps/desktop/src/lib/tableMetadataCapabilities.ts b/apps/desktop/src/lib/tableMetadataCapabilities.ts
index b2843f451..be51a3803 100644
--- a/apps/desktop/src/lib/tableMetadataCapabilities.ts
+++ b/apps/desktop/src/lib/tableMetadataCapabilities.ts
@@ -36,6 +36,11 @@ const capabilityByType: Partial> = {
mysql: mysqlCapabilities,
doris: mysqlCapabilities,
@@ -256,6 +277,7 @@ const capabilityByType: Partial
gaussdb: postgresCapabilities,
kwdb: postgresCapabilities,
opengauss: postgresCapabilities,
+ questdb: questdbCapabilities,
redshift: redshiftCapabilities,
vertica: redshiftCapabilities,
highgo: postgresCapabilities,
diff --git a/apps/desktop/src/lib/tableStructureEditorState.ts b/apps/desktop/src/lib/tableStructureEditorState.ts
index 42819eda6..3477bfd15 100644
--- a/apps/desktop/src/lib/tableStructureEditorState.ts
+++ b/apps/desktop/src/lib/tableStructureEditorState.ts
@@ -267,6 +267,7 @@ export const DATA_TYPE_OPTIONS: Record = {
"datetime year to fraction",
"interval day to second",
],
+ questdb: ["boolean", "ipv4", "byte", "short", "char", "int", "float", "symbol", "varchar", "string", "long", "date", "timestamp", "timestamp_ns", "double", "uuid", "binary", "long256", "geohash", "array", "interval", "decimal"],
};
const DATA_TYPE_OPTION_ALIASES: Partial> = {
@@ -278,6 +279,7 @@ const DATA_TYPE_OPTION_ALIASES: Partial> = {
gaussdb: "postgres",
kwdb: "postgres",
opengauss: "postgres",
+ questdb: "questdb",
redshift: "postgres",
highgo: "postgres",
vastbase: "postgres",
@@ -365,6 +367,13 @@ export const DEFAULT_TYPE_LENGTHS: Record = {
year: "4",
};
+export const QUESTDB_TYPE_LENGTHS: Record = {
+ geohash: "8c",
+ decimal: "10,2",
+};
+
+export const DEFAULT_TYPE_LENGTH_DISABLES: string[] = [];
+
export function parseExtraToColumnExtra(extra: string | null | undefined, databaseType?: DatabaseType): ColumnExtra {
const result: ColumnExtra = {};
if (!extra) return result;
@@ -378,7 +387,7 @@ export function parseExtraToColumnExtra(extra: string | null | undefined, databa
if (lower.includes("on update current_timestamp")) {
result.onUpdateCurrentTimestamp = true;
}
- } else if (databaseType === "postgres" || databaseType === "gaussdb" || databaseType === "kwdb" || databaseType === "opengauss" || databaseType === "highgo" || databaseType === "vastbase" || databaseType === "kingbase") {
+ } else if (databaseType === "postgres" || databaseType === "gaussdb" || databaseType === "kwdb" || databaseType === "opengauss" || databaseType === "questdb" || databaseType === "highgo" || databaseType === "vastbase" || databaseType === "kingbase") {
const identityMatch = lower.match(/generated\s+(by\s+default|always)\s+as\s+identity/i);
if (identityMatch) {
const sequenceMatch = lower.match(/start\s+with\s*(-?\d+)\s+increment\s+by\s*(-?\d+)/i);
@@ -635,6 +644,8 @@ function isTemporalPrecisionType(dbType: DatabaseType | undefined, baseType: str
case "dameng":
case "oceanbase-oracle":
return ["timestamp", "timestamp with time zone", "timestamp with local time zone"].includes(normalized);
+ case "questdb":
+ return ["timestamp"].includes(normalized);
default:
return false;
}
@@ -649,7 +660,22 @@ function isValidTemporalPrecision(dbType: DatabaseType | undefined, params: stri
export function getDefaultLengthForType(_dbType: DatabaseType | undefined, baseType: string): string {
const key = baseType.trim().toLowerCase();
- return DEFAULT_TYPE_LENGTHS[key] ?? "";
+ if (_dbType === "questdb") {
+ return QUESTDB_TYPE_LENGTHS[key] ?? "";
+ } else {
+ return DEFAULT_TYPE_LENGTHS[key] ?? "";
+ }
+}
+
+export function isDataTypeLengthDisabled(_dbType: DatabaseType | undefined, baseType: string): boolean {
+ const key = baseType.trim().toLowerCase();
+ if (_dbType === "questdb") {
+ return key !== "geohash" && key !== "decimal";
+ } else if (_dbType === "manticoresearch") {
+ return key !== "bit" && key !== "float_vector";
+ } else {
+ return DEFAULT_TYPE_LENGTH_DISABLES.includes(key);
+ }
}
export function buildStructureTargetLabel(connectionName: string | undefined, database: string | undefined, schema: string | undefined, tableName: string | undefined): string {
diff --git a/apps/desktop/src/lib/visibleDatabases.ts b/apps/desktop/src/lib/visibleDatabases.ts
index 930009a51..72c0b337d 100644
--- a/apps/desktop/src/lib/visibleDatabases.ts
+++ b/apps/desktop/src/lib/visibleDatabases.ts
@@ -11,6 +11,7 @@ const SYSTEM_DATABASE_RULES: Partial>>
gaussdb: new Set(["template0", "template1"]),
kwdb: new Set(["template0", "template1"]),
opengauss: new Set(["template0", "template1"]),
+ questdb: new Set(["template0", "template1"]),
kingbase: new Set(["template0", "template1"]),
highgo: new Set(["template0", "template1"]),
vastbase: new Set(["template0", "template1"]),
diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts
index d66e93a35..bfb788856 100644
--- a/apps/desktop/src/stores/connectionStore.ts
+++ b/apps/desktop/src/stores/connectionStore.ts
@@ -251,6 +251,7 @@ export const useConnectionStore = defineStore("connection", () => {
redshift: "Redshift",
dameng: "DM (Dameng)",
gaussdb: "GaussDB",
+ questdb: "QuestDB",
kwdb: "KWDB",
kingbase: "KingBase",
highgo: "瀚高 HighGo",
@@ -278,6 +279,8 @@ export const useConnectionStore = defineStore("connection", () => {
dbType = "gaussdb" as ConnectionConfig["db_type"];
} else if (profile === "kwdb" && dbType === "postgres") {
dbType = "kwdb" as ConnectionConfig["db_type"];
+ } else if (profile === "questdb" && dbType === "postgres") {
+ dbType = "questdb" as ConnectionConfig["db_type"];
} else if (profile === "redshift" && dbType === "postgres") {
dbType = "redshift" as ConnectionConfig["db_type"];
} else if (profile === "kingbase" && dbType === "postgres") {
diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts
index 6e627117d..6c5bf1178 100644
--- a/apps/desktop/src/types/database.ts
+++ b/apps/desktop/src/types/database.ts
@@ -32,6 +32,7 @@ export type DatabaseType =
| "exasol"
| "opengauss"
| "oceanbase-oracle"
+ | "questdb"
| "gbase"
| "access"
| "h2"
diff --git a/crates/dbx-core/assets/database-drivers.manifest.json b/crates/dbx-core/assets/database-drivers.manifest.json
index ce2ede2c4..0dc4302fd 100644
--- a/crates/dbx-core/assets/database-drivers.manifest.json
+++ b/crates/dbx-core/assets/database-drivers.manifest.json
@@ -913,6 +913,35 @@
"driverManagement": true
}
},
+ {
+ "dbType": "questdb",
+ "label": "QuestDB",
+ "runtimeMode": "native",
+ "mcpMode": "direct",
+ "singleConnectionPool": false,
+ "metadataConnectionScoped": false,
+ "skipTcpProbe": false,
+ "defaultPort": 8812,
+ "supportLevel": "operate",
+ "capabilities": {
+ "queryExecution": true,
+ "metadataBrowse": true,
+ "objectBrowser": true,
+ "objectSource": true,
+ "schemaSearch": true,
+ "diagram": false,
+ "tableDataEdit": true,
+ "tableStructureEdit": true,
+ "tableImport": true,
+ "dataTransfer": true,
+ "sqlFileExecution": true,
+ "databaseCreate": false,
+ "fieldLineage": false,
+ "sqlExplain": true,
+ "userAdmin": false,
+ "driverManagement": true
+ }
+ },
{
"dbType": "oceanbase-oracle",
"label": "OceanBase Oracle Mode",
diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs
index c17c6aad7..9ff5d16b6 100644
--- a/crates/dbx-core/src/connection.rs
+++ b/crates/dbx-core/src/connection.rs
@@ -371,6 +371,7 @@ impl AppState {
| DatabaseType::Redshift
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
+ | DatabaseType::Questdb
| DatabaseType::OpenGauss => PoolKind::Postgres(db::postgres::connect(&url, connect_timeout).await?),
DatabaseType::Sqlite => {
let extensions = db::sqlite::sqlite_extension_specs_from_url_params(db_config.url_params.as_deref())
@@ -1154,7 +1155,7 @@ fn external_driver_connect_timeout(config: &ConnectionConfig) -> std::time::Dura
fn native_postgres_url_config(config: &ConnectionConfig) -> Option {
match config.db_type {
- DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::OpenGauss => {
+ DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::OpenGauss | DatabaseType::Questdb => {
let mut normalized = config.clone();
normalized.database = normalized.effective_database().map(str::to_string);
if matches!(config.db_type, DatabaseType::Gaussdb | DatabaseType::Kwdb) {
diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs
index c97fb6d68..c8e29ddb1 100644
--- a/crates/dbx-core/src/data_grid_sql.rs
+++ b/crates/dbx-core/src/data_grid_sql.rs
@@ -1318,6 +1318,7 @@ fn uses_keyless_row_predicate(database_type: Option) -> bool {
| DatabaseType::Firebird
| DatabaseType::Exasol
| DatabaseType::OpenGauss
+ | DatabaseType::Questdb
| DatabaseType::OceanbaseOracle
| DatabaseType::Gbase
| DatabaseType::Access
diff --git a/crates/dbx-core/src/db/mod.rs b/crates/dbx-core/src/db/mod.rs
index e87636f69..4f73ce85c 100644
--- a/crates/dbx-core/src/db/mod.rs
+++ b/crates/dbx-core/src/db/mod.rs
@@ -11,6 +11,7 @@ pub mod mysql;
pub mod ob_oracle;
pub mod postgres;
pub mod proxy_tunnel;
+pub mod questdb;
pub mod redis_driver;
pub mod rqlite_driver;
pub mod sqlite;
diff --git a/crates/dbx-core/src/db/questdb.rs b/crates/dbx-core/src/db/questdb.rs
new file mode 100644
index 000000000..11d974f72
--- /dev/null
+++ b/crates/dbx-core/src/db/questdb.rs
@@ -0,0 +1,147 @@
+use crate::db;
+use crate::db::{ColumnInfo, IndexInfo, ObjectInfo, QueryResult, TableInfo};
+use crate::models::connection::DatabaseType;
+use crate::sql_dialect::quote_table_identifier;
+use deadpool_postgres::Pool;
+
+pub async fn list_objects(pool: &Pool, schema: &str) -> Result, String> {
+ Ok(list_tables(pool, schema)
+ .await?
+ .iter()
+ .map(|t| ObjectInfo {
+ name: t.name.clone(),
+ object_type: t.table_type.clone(),
+ schema: None,
+ comment: t.comment.clone(),
+ created_at: None,
+ updated_at: None,
+ parent_schema: None,
+ parent_name: None,
+ })
+ .collect())
+}
+
+pub async fn list_tables(pool: &Pool, _schema: &str) -> Result, String> {
+ let client = pool.get().await.map_err(|e| e.to_string())?;
+
+ let stmt = client.prepare_cached(questdb_tables_sql()).await.map_err(|e| e.to_string())?;
+ let rows = client.query(&stmt, &[]).await.map_err(|e| e.to_string())?;
+
+ Ok(rows
+ .iter()
+ .map(|row| {
+ let table_type_col = row.get::<_, String>(1);
+ let table_type = if table_type_col.eq_ignore_ascii_case("T") { "TABLE" } else { "VIEW" };
+ let comment =
+ if table_type_col.eq_ignore_ascii_case("M") { Some("Materialized".to_string()) } else { None };
+ TableInfo {
+ name: row.get::<_, String>(0),
+ table_type: table_type.to_string(),
+ comment,
+ parent_schema: None,
+ parent_name: None,
+ }
+ })
+ .collect())
+}
+
+fn questdb_tables_sql() -> &'static str {
+ "SELECT table_name, table_type FROM tables"
+}
+
+pub async fn get_columns(pool: &Pool, _schema: &str, table: &str) -> Result, String> {
+ let client = pool.get().await.map_err(|e| e.to_string())?;
+ let sql = format!("SHOW COLUMNS FROM {}", quote_table_identifier(Some(DatabaseType::Questdb), table));
+ let stmt = client.prepare_cached(&sql).await.map_err(|e| e.to_string())?;
+ let rows = client.query(&stmt, &[]).await.map_err(|e| e.to_string())?;
+
+ let not_null_types: [&str; 3] = ["boolean", "byte", "short"];
+
+ Ok(rows
+ .iter()
+ .map(|row| {
+ let column_type = row.get::<_, String>(1);
+ ColumnInfo {
+ name: row.get::<_, String>(0),
+ data_type: column_type.clone().to_lowercase(),
+ is_nullable: !not_null_types.contains(&column_type.as_str()),
+ column_default: None,
+ is_primary_key: column_type.eq_ignore_ascii_case("timestamp")
+ || column_type.eq_ignore_ascii_case("symbol"),
+ extra: None,
+ comment: None,
+ numeric_precision: None,
+ numeric_scale: None,
+ character_maximum_length: None,
+ }
+ })
+ .collect())
+}
+
+pub async fn list_indexes(pool: &Pool, _schema: &str, table: &str) -> Result, String> {
+ let client = pool.get().await.map_err(|e| e.to_string())?;
+ let sql = format!("SHOW COLUMNS FROM {}", quote_table_identifier(Some(DatabaseType::Questdb), table));
+ let stmt = client.prepare_cached(&sql).await.map_err(|e| e.to_string())?;
+ let rows = client.query(&stmt, &[]).await.map_err(|e| e.to_string())?;
+
+ Ok(rows
+ .iter()
+ .filter(|r| r.get::<_, bool>(2))
+ .map(|row| {
+ let column: String = row.get::<_, String>(0);
+ let key_cols = vec![column];
+ IndexInfo {
+ name: row.get::<_, String>(0),
+ columns: key_cols,
+ is_unique: false,
+ is_primary: true,
+ filter: None,
+ index_type: None,
+ included_columns: None,
+ comment: None,
+ }
+ })
+ .collect())
+}
+
+pub async fn questdb_object_source(pool: &Pool, name: &str) -> Result {
+ questdb_view_ddl(pool, name).await.map_err(|e| e.to_string())
+}
+
+pub async fn questdb_table_or_view_ddl(pool: &Pool, table_or_view: &str) -> Result {
+ match questdb_table_ddl(pool, table_or_view).await {
+ Ok(ddl) => Ok(ddl),
+ Err(_) => questdb_view_ddl(pool, table_or_view).await,
+ }
+}
+
+async fn questdb_table_ddl(pool: &Pool, table: &str) -> Result {
+ let sql = format!("SHOW CREATE TABLE {}", quote_table_identifier(Some(DatabaseType::Questdb), table));
+ first_string_cell(db::postgres::execute_query(pool, &sql).await?)
+}
+
+async fn questdb_view_ddl(pool: &Pool, view: &str) -> Result {
+ match questdb_mat_view_ddl(pool, view).await {
+ Ok(ddl) => Ok(ddl),
+ Err(_) => questdb_normal_view_ddl(pool, view).await,
+ }
+}
+
+async fn questdb_mat_view_ddl(pool: &Pool, view: &str) -> Result {
+ let sql = format!("SHOW CREATE MATERIALIZED VIEW {}", quote_table_identifier(Some(DatabaseType::Questdb), view));
+ first_string_cell(db::postgres::execute_query(pool, &sql).await?)
+}
+
+async fn questdb_normal_view_ddl(pool: &Pool, view: &str) -> Result {
+ let sql = format!("SHOW CREATE VIEW {}", quote_table_identifier(Some(DatabaseType::Questdb), view));
+ first_string_cell(db::postgres::execute_query(pool, &sql).await?)
+}
+
+fn first_string_cell(result: QueryResult) -> Result {
+ result
+ .rows
+ .first()
+ .and_then(|row| row.iter().find_map(|value| value.as_str().map(str::to_string)))
+ .filter(|value| !value.trim().is_empty())
+ .ok_or_else(|| "Object source not found".to_string())
+}
diff --git a/crates/dbx-core/src/db_admin_sql.rs b/crates/dbx-core/src/db_admin_sql.rs
index fe14dfe7c..80d14cc39 100644
--- a/crates/dbx-core/src/db_admin_sql.rs
+++ b/crates/dbx-core/src/db_admin_sql.rs
@@ -193,6 +193,7 @@ pub fn build_drop_table_child_object_sql(options: DropTableChildObjectSqlOptions
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::OpenGauss
+ | DatabaseType::Questdb
| DatabaseType::Highgo
| DatabaseType::Vastbase
| DatabaseType::Kingbase
@@ -223,6 +224,7 @@ pub fn build_drop_table_child_object_sql(options: DropTableChildObjectSqlOptions
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::OpenGauss
+ | DatabaseType::Questdb
| DatabaseType::Highgo
| DatabaseType::Vastbase
| DatabaseType::Kingbase
@@ -249,7 +251,9 @@ pub fn build_empty_table_sql(options: TableAdminSqlOptions) -> String {
match options.database_type {
Some(DatabaseType::ClickHouse) => format!("ALTER TABLE {table} DELETE WHERE 1 = 1;"),
Some(DatabaseType::Bigquery) => format!("DELETE FROM {table} WHERE TRUE;"),
- Some(DatabaseType::Cassandra | DatabaseType::Hive | DatabaseType::Kylin) => format!("TRUNCATE TABLE {table};"),
+ Some(DatabaseType::Cassandra | DatabaseType::Hive | DatabaseType::Kylin | DatabaseType::Questdb) => {
+ format!("TRUNCATE TABLE {table};")
+ }
Some(DatabaseType::Iotdb) => format!("DELETE FROM {};", iotdb_timeseries_pattern(&table)),
_ => format!("DELETE FROM {table};"),
}
@@ -289,6 +293,9 @@ pub fn build_duplicate_table_structure_sql(options: DuplicateTableStructureSqlOp
if options.database_type == Some(DatabaseType::Mysql) {
return format!("CREATE TABLE {target} LIKE {source};");
}
+ if options.database_type == Some(DatabaseType::Questdb) {
+ return format!("CREATE TABLE {target} (LIKE {source});");
+ }
if options.database_type.is_some_and(is_postgres_like_structure_copy) {
return format!("CREATE TABLE {target} (LIKE {source} INCLUDING ALL);");
}
@@ -400,6 +407,7 @@ fn is_postgres_like_structure_copy(database_type: DatabaseType) -> bool {
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::OpenGauss
+ | DatabaseType::Questdb
)
}
@@ -607,6 +615,23 @@ mod tests {
}),
"DELETE FROM root.test.DCU_101.*;"
);
+
+ assert_eq!(
+ build_empty_table_sql(TableAdminSqlOptions {
+ database_type: Some(DatabaseType::Questdb),
+ schema: None,
+ table_name: "table_sample".to_string(),
+ }),
+ "TRUNCATE TABLE `table_sample`;"
+ );
+ assert_eq!(
+ build_truncate_table_sql(TableAdminSqlOptions {
+ database_type: Some(DatabaseType::Questdb),
+ schema: None,
+ table_name: "table_sample".to_string(),
+ }),
+ "TRUNCATE TABLE `table_sample`;"
+ );
}
#[test]
@@ -711,6 +736,18 @@ mod tests {
.unwrap(),
"DROP TRIGGER \"orders_audit\" ON \"public\".\"orders\";"
);
+
+ assert_eq!(
+ build_drop_table_child_object_sql(DropTableChildObjectSqlOptions {
+ database_type: Some(DatabaseType::Questdb),
+ object_type: TableChildObjectType::Column,
+ schema: Some("public".to_string()),
+ table_name: "orders".to_string(),
+ name: "status".to_string(),
+ })
+ .unwrap(),
+ "ALTER TABLE `orders` DROP COLUMN `status`;"
+ );
}
#[test]
@@ -751,6 +788,15 @@ mod tests {
}),
"CREATE TABLE \"HR\".\"USERS_COPY\" AS SELECT * FROM \"HR\".\"USERS\" WHERE 1=0"
);
+ assert_eq!(
+ build_duplicate_table_structure_sql(DuplicateTableStructureSqlOptions {
+ database_type: Some(DatabaseType::Questdb),
+ schema: None,
+ source_name: "users".to_string(),
+ target_name: "users_copy".to_string(),
+ }),
+ "CREATE TABLE `users_copy` (LIKE `users`);"
+ );
}
#[test]
diff --git a/crates/dbx-core/src/models/connection.rs b/crates/dbx-core/src/models/connection.rs
index 7a3037c8c..723bdf837 100644
--- a/crates/dbx-core/src/models/connection.rs
+++ b/crates/dbx-core/src/models/connection.rs
@@ -292,6 +292,8 @@ pub enum DatabaseType {
Turso,
#[serde(rename = "influxdb")]
InfluxDb,
+ #[serde(rename = "questdb")]
+ Questdb,
Jdbc,
}
@@ -725,6 +727,7 @@ impl ConnectionConfig {
format!("{base}?{params}")
}
}
+ DatabaseType::Questdb => format!("questdb://{host}:{port}{db_part}"),
DatabaseType::Gbase => format!("gbase://{host}:{port}{db_part}"),
DatabaseType::H2 => format!("h2://{host}:{port}{db_part}"),
DatabaseType::Snowflake => format!("snowflake://{host}/{db_part}"),
@@ -886,6 +889,9 @@ impl ConnectionConfig {
format!("{base}?{params}")
}
}
+ DatabaseType::Questdb => {
+ format!("questdb://{}:{}@{host}:{port}{db_part}", username, password)
+ }
DatabaseType::Gbase => {
format!("gbase://{}:{}@{host}:{port}{db_part}", username, password)
}
diff --git a/crates/dbx-core/src/object_source_sql.rs b/crates/dbx-core/src/object_source_sql.rs
index cf997cad6..48de13060 100644
--- a/crates/dbx-core/src/object_source_sql.rs
+++ b/crates/dbx-core/src/object_source_sql.rs
@@ -127,7 +127,11 @@ pub fn build_executable_object_source_statements(input: EditableObjectSourceSqlI
if matches!(
input.database_type,
- DatabaseType::Postgres | DatabaseType::Gaussdb | DatabaseType::Kwdb | DatabaseType::OpenGauss
+ DatabaseType::Postgres
+ | DatabaseType::Gaussdb
+ | DatabaseType::Kwdb
+ | DatabaseType::OpenGauss
+ | DatabaseType::Questdb
) && input.object_type == ObjectSourceKind::View
{
return Ok(vec![format!(
@@ -159,9 +163,11 @@ pub fn build_view_ddl_sql(input: BuildViewDdlInput) -> String {
};
if input.database_type.is_none()
- || input
- .database_type
- .is_some_and(|database_type| is_postgres_like(database_type) || database_type == DatabaseType::OpenGauss)
+ || input.database_type.is_some_and(|database_type| {
+ is_postgres_like(database_type)
+ || database_type == DatabaseType::OpenGauss
+ || database_type == DatabaseType::Questdb
+ })
{
return format!("CREATE OR REPLACE VIEW {qualified_name} AS\n{}", ensure_semicolon(source));
}
@@ -215,6 +221,7 @@ fn is_postgres_like(database_type: DatabaseType) -> bool {
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::OpenGauss
+ | DatabaseType::Questdb
| DatabaseType::Kingbase
| DatabaseType::Highgo
| DatabaseType::Vastbase
diff --git a/crates/dbx-core/src/query_execution_sql.rs b/crates/dbx-core/src/query_execution_sql.rs
index f1f307ff6..e5bc71ff9 100644
--- a/crates/dbx-core/src/query_execution_sql.rs
+++ b/crates/dbx-core/src/query_execution_sql.rs
@@ -41,12 +41,14 @@ pub fn build_explain_sql(options: ExplainSqlOptions) -> ExplainSqlBuildResult {
return explain_err("unsafe");
}
- let sql = if options.database_type == Some(DatabaseType::Postgres) {
- format!("EXPLAIN (FORMAT JSON) {source}")
- } else if options.database_type == Some(DatabaseType::Dameng) {
- format!("EXPLAIN {source}")
- } else {
- format!("EXPLAIN FORMAT=JSON {source}")
+ let sql = match options.database_type {
+ Some(DatabaseType::Postgres | DatabaseType::MongoDb) => {
+ format!("EXPLAIN (FORMAT JSON) {source}")
+ }
+ Some(DatabaseType::Dameng | DatabaseType::Questdb) => {
+ format!("EXPLAIN {source}")
+ }
+ _ => format!("EXPLAIN FORMAT=JSON {source}"),
};
ExplainSqlBuildResult { ok: true, sql: Some(sql), reason: None }
}
@@ -71,7 +73,10 @@ pub fn build_dropped_file_preview_sql(options: DroppedFilePreviewSqlOptions) ->
}
pub fn supports_explain_plan(database_type: Option) -> bool {
- matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Postgres | DatabaseType::Dameng))
+ matches!(
+ database_type,
+ Some(DatabaseType::Mysql | DatabaseType::Postgres | DatabaseType::Questdb | DatabaseType::Dameng)
+ )
}
pub fn is_safe_dameng_autotrace_sql(sql: &str) -> bool {
diff --git a/crates/dbx-core/src/query_result_sql.rs b/crates/dbx-core/src/query_result_sql.rs
index d4b7a5567..de190bd1a 100644
--- a/crates/dbx-core/src/query_result_sql.rs
+++ b/crates/dbx-core/src/query_result_sql.rs
@@ -7,7 +7,7 @@ use crate::sql::find_statement_at_cursor;
use crate::sql_dialect::{quote_table_identifier, uses_fetch_first};
static LIMIT_OFFSET_STRIP_RE: LazyLock = LazyLock::new(|| {
- Regex::new(r"(?i)(\s+LIMIT\s+\d+(\s+OFFSET\s+\d+)?|\s+OFFSET\s+\d+(\s+LIMIT\s+\d+)?|\s+OFFSET\s+\d+\s+ROWS?\s+FETCH\s+(?:FIRST|NEXT)\s+\d+\s+ROWS?\s+ONLY|\s+FETCH\s+(?:FIRST|NEXT)\s+\d+\s+ROWS?\s+ONLY)\s*$").unwrap()
+ Regex::new(r"(?i)(\s+LIMIT\s+\d+(\s+OFFSET\s+\d+)?|\s+LIMIT\s+\d+(\s*,\s*\d+)?|\s+OFFSET\s+\d+(\s+LIMIT\s+\d+)?|\s+OFFSET\s+\d+\s+ROWS?\s+FETCH\s+(?:FIRST|NEXT)\s+\d+\s+ROWS?\s+ONLY|\s+FETCH\s+(?:FIRST|NEXT)\s+\d+\s+ROWS?\s+ONLY)\s*$").unwrap()
});
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -178,6 +178,10 @@ pub fn build_paginated_query_sql(options: PaginatedQuerySqlOptions) -> QuerySqlB
return ok(add_mysql_limit(&statement, safe_limit, safe_offset));
}
+ if options.database_type == Some(DatabaseType::Questdb) {
+ return ok(add_questdb_limit(&statement, safe_limit, safe_offset));
+ }
+
if options.database_type == Some(DatabaseType::Elasticsearch) {
// If the user wrote their own LIMIT, leave the SQL alone — they
// explicitly bounded the result set and the front-end will paginate
@@ -431,6 +435,18 @@ fn add_informix_first_limit(statement: &str, limit: usize, offset: usize) -> Str
format!("SELECT {row_limit} * FROM ({statement}) dbx_page;")
}
+fn add_questdb_limit(statement: &str, limit: usize, offset: usize) -> String {
+ if has_top_level_limit(statement) {
+ return format!("{statement};");
+ }
+ if offset > 0 {
+ let upper_bound = offset + limit;
+ format!("{statement} LIMIT {offset}, {upper_bound};")
+ } else {
+ format!("{statement} LIMIT {limit};")
+ }
+}
+
fn has_top_level_limit(sql: &str) -> bool {
top_level_sql_tokens(sql).iter().any(|token| token.text == "LIMIT")
}
diff --git a/crates/dbx-core/src/schema.rs b/crates/dbx-core/src/schema.rs
index 859e1a7a7..82c7d5dca 100644
--- a/crates/dbx-core/src/schema.rs
+++ b/crates/dbx-core/src/schema.rs
@@ -549,6 +549,9 @@ async fn list_tables_once(
dispatch_mysql!(p, mode, db::mysql::list_tables, db::ob_oracle::list_tables, schema)
.map(|tables| filter_table_infos(tables, filter, limit, offset))
}
+ PoolKind::Postgres(p) if db_config.as_ref().is_some_and(is_questdb_config) => {
+ db::questdb::list_tables(p, schema).await.map(|tables| filter_table_infos(tables, filter, limit, offset))
+ }
PoolKind::Postgres(p) => db::postgres::list_tables_filtered(p, schema, filter, limit, offset).await,
PoolKind::Sqlite(p) => {
db::sqlite::list_tables(p, schema).await.map(|tables| filter_table_infos(tables, filter, limit, offset))
@@ -944,6 +947,9 @@ async fn list_objects_once(
db::mysql::list_objects(p, database).await
}
}
+ PoolKind::Postgres(p) if db_config.as_ref().is_some_and(is_questdb_config) => {
+ db::questdb::list_objects(p, schema).await
+ }
PoolKind::Postgres(p) => db::postgres::list_objects(p, schema).await,
_ => {
drop(connections);
@@ -1049,6 +1055,9 @@ async fn list_completion_objects_once(
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => {
db::ob_oracle::list_objects(p, schema).await.map(filter_completion_objects)
}
+ PoolKind::Postgres(p) if db_config.as_ref().is_some_and(is_questdb_config) => {
+ db::questdb::list_objects(p, schema).await.map(filter_completion_objects)
+ }
PoolKind::Postgres(p) => db::postgres::list_objects(p, schema).await.map(filter_completion_objects),
PoolKind::SqlServer(_) => {
drop(connections);
@@ -1243,6 +1252,9 @@ pub async fn get_columns_core(
dispatch_mysql!(p, mode, db::mysql::get_columns, db::ob_oracle::get_columns, database, table)
.map(deduplicate_column_infos)
}
+ PoolKind::Postgres(p) if db_config.as_ref().is_some_and(is_questdb_config) => {
+ db::questdb::get_columns(p, schema, table).await.map(deduplicate_column_infos)
+ }
PoolKind::Postgres(p) => db::postgres::get_columns(p, schema, table).await.map(deduplicate_column_infos),
PoolKind::Sqlite(p) => db::sqlite::get_columns(p, schema, table).await.map(deduplicate_column_infos),
PoolKind::Rqlite(client) => {
@@ -1321,6 +1333,9 @@ pub async fn list_indexes_core(
}
dispatch_mysql!(p, mode, db::mysql::list_indexes, db::ob_oracle::list_indexes, schema, table)
}
+ PoolKind::Postgres(p) if db_config.as_ref().is_some_and(is_questdb_config) => {
+ db::questdb::list_indexes(p, schema, table).await
+ }
PoolKind::Postgres(p) => db::postgres::list_indexes(p, schema, table).await,
PoolKind::Sqlite(p) => db::sqlite::list_indexes(p, schema, table).await,
PoolKind::Rqlite(client) => db::rqlite_driver::list_indexes(client, schema, table).await,
@@ -1513,6 +1528,12 @@ pub async fn get_table_ddl_core(
Err(_) => pg_ddl(p, schema, table).await,
}
}
+ PoolKind::Postgres(p) if db_config.as_ref().is_some_and(is_questdb_config) => {
+ match db::questdb::questdb_table_or_view_ddl(p, table).await {
+ Ok(ddl) => Ok(ddl),
+ Err(_) => pg_ddl(p, schema, table).await,
+ }
+ }
PoolKind::Postgres(p) => pg_ddl(p, schema, table).await,
PoolKind::Sqlite(p) => sqlite_ddl(p, table).await,
PoolKind::Rqlite(client) => db::rqlite_driver::table_ddl(client, table).await,
@@ -1562,6 +1583,10 @@ fn is_mysql_system_database(name: &str) -> bool {
matches!(name.to_ascii_lowercase().as_str(), "information_schema" | "mysql" | "performance_schema" | "sys")
}
+fn is_questdb_config(config: &ConnectionConfig) -> bool {
+ matches!(config.db_type, DatabaseType::Questdb) || matches!(config.driver_profile.as_deref(), Some("questdb"))
+}
+
fn sql_string(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
@@ -1793,6 +1818,10 @@ pub async fn get_object_source_core(
} else {
match connections.get(&pool_key).ok_or("Pool not found")? {
PoolKind::Mysql(pool, _) => mysql_object_source(pool, name, &object_type).await?,
+ PoolKind::Postgres(pool) if db_config.as_ref().is_some_and(is_questdb_config) => {
+ // only view
+ db::questdb::questdb_object_source(pool, name).await?
+ }
PoolKind::Postgres(pool) => postgres_object_source(pool, schema, name, &object_type).await?,
PoolKind::Sqlite(pool) => first_string_cell(
db::sqlite::execute_query(pool, &sqlite_object_source_sql(name, &object_type)).await?,
diff --git a/crates/dbx-core/src/schema/providers/native.rs b/crates/dbx-core/src/schema/providers/native.rs
index b0e3e1634..13923bd03 100644
--- a/crates/dbx-core/src/schema/providers/native.rs
+++ b/crates/dbx-core/src/schema/providers/native.rs
@@ -45,6 +45,9 @@ pub(in crate::schema) async fn list_tables(
let db = if schema.is_empty() { database } else { schema };
db::mysql::list_tables(p, db).await
}
+ PoolKind::Postgres(p) if config.is_some_and(is_questdb_config) => {
+ db::questdb::list_tables(p, schema).await
+ }
PoolKind::Postgres(p) => db::postgres::list_tables(p, schema).await,
PoolKind::Sqlite(p) => db::sqlite::list_tables(p, schema).await,
PoolKind::Rqlite(client) => db::rqlite_driver::list_tables(client, schema).await,
@@ -76,6 +79,9 @@ pub(in crate::schema) async fn list_objects(
db::mysql::list_table_objects_show(p, database).await.map(Some)
}
PoolKind::Mysql(p, _) => db::mysql::list_objects(p, database).await.map(Some),
+ PoolKind::Postgres(p) if config.is_some_and(is_questdb_config) => {
+ db::questdb::list_objects(p, schema).await.map(Some)
+ }
PoolKind::Postgres(p) => db::postgres::list_objects(p, schema).await.map(Some),
_ => Ok(None),
}
@@ -83,6 +89,7 @@ pub(in crate::schema) async fn list_objects(
pub(in crate::schema) async fn list_completion_objects(
pool: &PoolKind,
+ config: Option<&ConnectionConfig>,
database: &str,
schema: &str,
) -> Result