feat: add QuestDB support

Thanks @ptma!
This commit is contained in:
John Jin 2026-06-14 22:58:05 +08:00 committed by GitHub
parent 0ae720d950
commit 1afc271c18
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 633 additions and 46 deletions

View File

@ -37,6 +37,7 @@
<img src="https://img.shields.io/badge/KingBase-003B8E?logoColor=white" />
<img src="https://img.shields.io/badge/TDengine-2F6FFF?logoColor=white" />
<img src="https://img.shields.io/badge/CockroachDB-6933FF?logoColor=white" />
<img src="https://img.shields.io/badge/InfluxDB-d30971?logo=influxdb&logoColor=white" />
<img src="https://img.shields.io/badge/JDBC-4B5563?logoColor=white" />
<img src="https://img.shields.io/badge/and%20more...-555555?logoColor=white" />
</p>
@ -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
<details>
<summary><strong>What databases are supported?</strong></summary>
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.
</details>
<details>

View File

@ -37,6 +37,7 @@
<img src="https://img.shields.io/badge/KingBase-003B8E?logoColor=white" />
<img src="https://img.shields.io/badge/TDengine-2F6FFF?logoColor=white" />
<img src="https://img.shields.io/badge/CockroachDB-6933FF?logoColor=white" />
<img src="https://img.shields.io/badge/InfluxDB-d30971?logo=influxdb&logoColor=white" />
<img src="https://img.shields.io/badge/JDBC-4B5563?logoColor=white" />
<img src="https://img.shields.io/badge/and%20more...-555555?logoColor=white" />
</p>
@ -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 和
<details>
<summary><strong>支持哪些数据库?</strong></summary>
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 连接。
</details>
<details>

View File

@ -0,0 +1,25 @@
<svg width="812" height="701" viewBox="0 0 812 701" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M133.387 350.187C133.387 228.785 233.243 130.989 357.202 130.989C401.965 130.989 443.284 143.354 477.717 165.836L574.13 72.5358C514.446 27.572 438.693 0.59375 357.202 0.59375C159.786 0.59375 0.246094 156.843 0.246094 350.187C0.246094 543.531 159.786 699.781 357.202 699.781V569.386C233.243 569.386 133.387 471.589 133.387 350.187Z" fill="url(#paint0_linear)"/>
<path d="M581.015 350.192H714.156C714.156 258.016 677.428 173.709 617.744 110.76L523.627 202.935C559.208 242.279 581.015 293.987 581.015 350.192Z" fill="url(#paint1_linear)"/>
<path d="M466.239 361.43H269.971L616.596 700.906L811.717 699.782L466.239 361.43Z" fill="url(#paint2_linear)"/>
<path opacity="0.6" d="M47.3016 175.957C-38.7809 421.01 218.319 523.302 218.319 523.302C190.772 503.069 156.339 455.857 146.01 422.134C124.202 352.44 131.089 297.359 166.669 236.658" fill="url(#paint3_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="803.686" y1="702.01" x2="178.788" y2="54.7941" gradientUnits="userSpaceOnUse">
<stop stop-color="#892C6C"/>
<stop offset="1" stop-color="#D14671"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="143.714" y1="42.0442" x2="790.93" y2="692.448" gradientUnits="userSpaceOnUse">
<stop stop-color="#C34170"/>
<stop offset="1" stop-color="#8B2C6C"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="774.991" y1="686.071" x2="115.023" y2="42.0437" gradientUnits="userSpaceOnUse">
<stop stop-color="#882B6B"/>
<stop offset="1" stop-color="#CF4671"/>
</linearGradient>
<linearGradient id="paint3_linear" x1="85.9224" y1="476.222" x2="175.953" y2="220.969" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.38"/>
<stop offset="0.8598" stop-color="white" stop-opacity="0.01"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -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<string, string> = {
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<DatabaseType>(["mysql", "postgres", "redshift", "gaussdb", "kwdb", "opengauss", "redis", "etcd", "clickhouse", "elasticsearch", "influxdb"]);
const tlsCapableDatabaseTypes = new Set<DatabaseType>(["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");

View File

@ -30,6 +30,7 @@ const assetIcons: Record<string, string> = {
oceanbase: "oceanbase",
opengauss: "opengauss",
gaussdb: "gaussdb",
questdb: "questdb",
kwdb: "kwdb",
kingbase: "kingbase",
highgo: "highgo.png",

View File

@ -167,6 +167,7 @@ const sourceFormatDialect = computed<SqlFormatDialect>(() => {
case "gaussdb":
case "kwdb":
case "opengauss":
case "questdb":
return "postgres";
default:
return "generic";

View File

@ -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 {

View File

@ -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":

View File

@ -48,6 +48,7 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
"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 },

View File

@ -35,7 +35,7 @@ export const SCHEMA_AWARE_TYPES = new Set<DatabaseType>([
"duckdb",
]);
export const SINGLE_DATABASE_TYPES = new Set<DatabaseType>(["oracle", "dameng", "firebird", "oceanbase-oracle", "access"]);
export const SINGLE_DATABASE_TYPES = new Set<DatabaseType>(["oracle", "dameng", "firebird", "oceanbase-oracle", "access", "questdb"]);
export const FETCH_FIRST_TYPES = new Set<DatabaseType>(["oracle", "dameng"]);
@ -71,6 +71,6 @@ export const TREE_SCHEMA_TYPES = new Set<DatabaseType>([
export const DATABASE_OBJECT_TREE_TYPES = new Set<DatabaseType>(["jdbc"]);
export const PG_LIKE_STRUCTURE_TYPES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb", "kwdb", "opengauss"]);
export const PG_LIKE_STRUCTURE_TYPES = new Set<DatabaseType>(["postgres", "redshift", "gaussdb", "kwdb", "opengauss", "questdb"]);
export const DIAGRAM_SQL_TYPES = new Set<DatabaseType>(["mysql", "postgres", "sqlite", "rqlite", "turso", "sqlserver", "oracle", "redshift", "dameng", "gaussdb", "kwdb", "opengauss", "oceanbase-oracle"]);
export const DIAGRAM_SQL_TYPES = new Set<DatabaseType>(["mysql", "postgres", "sqlite", "rqlite", "turso", "sqlserver", "oracle", "redshift", "dameng", "gaussdb", "kwdb", "opengauss", "questdb", "oceanbase-oracle"]);

View File

@ -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<DatabaseType>(["influxdb"]);
const TABLE_FUNCTION_TYPES = new Set<DatabaseType>(["manticoresearch"]);
const TABLE_VIEW_ONLY_TYPES = new Set<DatabaseType>(["sqlite", "rqlite", "turso", "duckdb", "clickhouse", "doris", "starrocks", "databend", "hive", "trino", "cassandra", "bigquery", "kylin", "tdengine", "iotdb", "neo4j"]);
const TABLE_VIEW_ONLY_TYPES = new Set<DatabaseType>(["sqlite", "rqlite", "turso", "duckdb", "clickhouse", "doris", "starrocks", "databend", "hive", "trino", "cassandra", "bigquery", "kylin", "tdengine", "iotdb", "neo4j", "questdb"]);
const ORACLE_PACKAGE_TYPES = new Set<DatabaseType>(["oracle", "oceanbase-oracle"]);
const POSTGRES_SEQUENCE_TYPES = new Set<DatabaseType>(["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];

View File

@ -73,6 +73,7 @@ const NAVICAT_STYLE_TABLE_DATA_TYPES = new Set<DatabaseType>([
"firebird",
"exasol",
"opengauss",
"questdb",
"oceanbase-oracle",
"gbase",
"access",

View File

@ -46,7 +46,7 @@ export interface DatabaseUserAdminProvider {
}
export const MYSQL_USER_ADMIN_TYPES = new Set<DatabaseType>(["mysql", "goldendb"]);
export const POSTGRES_USER_ADMIN_TYPES = new Set<DatabaseType>(["postgres", "gaussdb", "highgo", "kingbase", "kwdb", "opengauss", "vastbase"]);
export const POSTGRES_USER_ADMIN_TYPES = new Set<DatabaseType>(["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;

View File

@ -50,6 +50,7 @@ const profileMap: Record<string, ConnectionProfile> = {
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: "" },
};

View File

@ -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<DatabaseType>(["mysql", "postgres", "dameng"]);
export function supportsExplainPlan(databaseType?: DatabaseType): databaseType is "mysql" | "postgres" | "dameng" {
const SUPPORTED_EXPLAIN_TYPES = new Set<DatabaseType>(["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<BuildExplainSqlResult>;
}
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<string, unknown>, 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<string, unknown> | null {

View File

@ -36,6 +36,11 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
foreignKeys: false,
triggers: false,
},
questdb: {
indexes: true,
foreignKeys: false,
triggers: false,
},
};
export function getTableMetadataCapabilities(dbType?: DatabaseType): TableMetadataCapabilities {

View File

@ -244,6 +244,27 @@ const manticoreSearchCapabilities = capabilities({
dropColumn: true,
});
const questdbCapabilities = capabilities({
dialect: "postgres",
createTable: true,
addColumn: true,
dropColumn: true,
renameColumn: true,
alterExistingColumn: true,
alterType: true,
alterNullability: false,
alterDefault: false,
comment: false,
createIndex: false,
dropIndex: false,
rebuildIndex: false,
indexType: false,
indexInclude: false,
indexFilter: false,
indexComment: false,
alterPrimaryKey: false,
});
const capabilityByType: Partial<Record<DatabaseType, TableStructureCapabilities>> = {
mysql: mysqlCapabilities,
doris: mysqlCapabilities,
@ -256,6 +277,7 @@ const capabilityByType: Partial<Record<DatabaseType, TableStructureCapabilities>
gaussdb: postgresCapabilities,
kwdb: postgresCapabilities,
opengauss: postgresCapabilities,
questdb: questdbCapabilities,
redshift: redshiftCapabilities,
vertica: redshiftCapabilities,
highgo: postgresCapabilities,

View File

@ -267,6 +267,7 @@ export const DATA_TYPE_OPTIONS: Record<string, string[]> = {
"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<Record<DatabaseType, string>> = {
@ -278,6 +279,7 @@ const DATA_TYPE_OPTION_ALIASES: Partial<Record<DatabaseType, string>> = {
gaussdb: "postgres",
kwdb: "postgres",
opengauss: "postgres",
questdb: "questdb",
redshift: "postgres",
highgo: "postgres",
vastbase: "postgres",
@ -365,6 +367,13 @@ export const DEFAULT_TYPE_LENGTHS: Record<string, string> = {
year: "4",
};
export const QUESTDB_TYPE_LENGTHS: Record<string, string> = {
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 {

View File

@ -11,6 +11,7 @@ const SYSTEM_DATABASE_RULES: Partial<Record<DatabaseType, ReadonlySet<string>>>
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"]),

View File

@ -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") {

View File

@ -32,6 +32,7 @@ export type DatabaseType =
| "exasol"
| "opengauss"
| "oceanbase-oracle"
| "questdb"
| "gbase"
| "access"
| "h2"

View File

@ -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",

View File

@ -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<ConnectionConfig> {
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) {

View File

@ -1318,6 +1318,7 @@ fn uses_keyless_row_predicate(database_type: Option<DatabaseType>) -> bool {
| DatabaseType::Firebird
| DatabaseType::Exasol
| DatabaseType::OpenGauss
| DatabaseType::Questdb
| DatabaseType::OceanbaseOracle
| DatabaseType::Gbase
| DatabaseType::Access

View File

@ -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;

View File

@ -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<Vec<ObjectInfo>, 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<Vec<TableInfo>, 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<Vec<ColumnInfo>, 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<Vec<IndexInfo>, 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<String, String> {
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<String, String> {
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<String, String> {
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<String, String> {
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<String, String> {
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<String, String> {
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<String, String> {
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())
}

View File

@ -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]

View File

@ -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)
}

View File

@ -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

View File

@ -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<DatabaseType>) -> 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 {

View File

@ -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<Regex> = 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")
}

View File

@ -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?,

View File

@ -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<Option<Vec<db::ObjectInfo>>, String> {
@ -93,6 +100,9 @@ pub(in crate::schema) async fn list_completion_objects(
PoolKind::Mysql(p, mode) if *mode == MysqlMode::OceanBaseOracle => {
db::ob_oracle::list_objects(p, schema).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),
}
@ -118,6 +128,9 @@ pub(in crate::schema) async fn get_columns(
db::ob_oracle::get_columns(p, database, table).await
}
PoolKind::Mysql(p, _) => db::mysql::get_columns(p, database, table).await,
PoolKind::Postgres(p) if config.is_some_and(is_questdb_config) => {
db::questdb::get_columns(p, schema, table).await
}
PoolKind::Postgres(p) => db::postgres::get_columns(p, schema, table).await,
PoolKind::Sqlite(p) => db::sqlite::get_columns(p, schema, table).await,
PoolKind::Rqlite(client) => db::rqlite_driver::get_columns(client, schema, table).await,
@ -129,6 +142,7 @@ pub(in crate::schema) async fn get_columns(
pub(in crate::schema) async fn list_indexes(
pool: &PoolKind,
config: Option<&ConnectionConfig>,
database: &str,
schema: &str,
table: &str,
@ -138,6 +152,9 @@ pub(in crate::schema) async fn list_indexes(
db::ob_oracle::list_indexes(p, schema, table).await
}
PoolKind::Mysql(p, _) => db::mysql::list_indexes(p, schema, table).await,
PoolKind::Postgres(p) if config.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,
@ -200,6 +217,12 @@ pub(in crate::schema) async fn table_ddl(
Err(_) => super::super::pg_ddl(p, schema, table).await,
}
}
PoolKind::Postgres(p) if config.is_some_and(is_questdb_config) => {
match db::questdb::questdb_table_or_view_ddl(p, table).await {
Ok(ddl) => Ok(ddl),
Err(_) => super::super::pg_ddl(p, schema, table).await,
}
}
PoolKind::Postgres(p) => super::super::pg_ddl(p, schema, table).await,
PoolKind::Sqlite(p) => super::super::sqlite_ddl(p, table).await,
PoolKind::Rqlite(client) => db::rqlite_driver::table_ddl(client, table).await,
@ -306,3 +329,8 @@ fn filter_mysql_system_databases_for_config(
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"))
}

View File

@ -90,7 +90,9 @@ fn starts_with_duckdb_parser_gap_sql(sql: &str) -> bool {
fn normalize_dialect(dialect: Option<&str>) -> String {
match dialect.unwrap_or("generic").to_ascii_lowercase().as_str() {
"postgres" | "postgresql" | "redshift" | "opengauss" | "gaussdb" | "highgo" => "postgres".to_string(),
"postgres" | "postgresql" | "redshift" | "opengauss" | "gaussdb" | "highgo" | "questdb" => {
"postgres".to_string()
}
"mysql" | "mariadb" | "doris" | "starrocks" | "manticoresearch" | "oceanbase" => "mysql".to_string(),
"sqlite" => "sqlite".to_string(),
"sqlserver" | "mssql" => "sqlserver".to_string(),

View File

@ -41,7 +41,8 @@ pub fn quote_table_identifier(database_type: Option<DatabaseType>, name: &str) -
| DatabaseType::Databend
| DatabaseType::Tdengine
| DatabaseType::Access
| DatabaseType::Bigquery,
| DatabaseType::Bigquery
| DatabaseType::Questdb,
) => {
format!("`{}`", name.replace('`', "``"))
}
@ -69,7 +70,8 @@ pub(crate) fn quote_transfer_identifier(name: &str, database_type: &DatabaseType
| DatabaseType::ClickHouse
| DatabaseType::Doris
| DatabaseType::StarRocks
| DatabaseType::Hive => format!("`{}`", name.replace('`', "``")),
| DatabaseType::Hive
| DatabaseType::Questdb => format!("`{}`", name.replace('`', "``")),
DatabaseType::SqlServer => format!("[{}]", name.replace(']', "]]")),
_ => format!("\"{}\"", name.replace('\"', "\"\"")),
}
@ -77,7 +79,8 @@ pub(crate) fn quote_transfer_identifier(name: &str, database_type: &DatabaseType
pub(crate) fn qualified_transfer_table(table_name: &str, schema: &str, database_type: &DatabaseType) -> String {
let table = quote_transfer_identifier(table_name, database_type);
if schema.is_empty() || matches!(database_type, DatabaseType::Mysql | DatabaseType::MongoDb) {
if schema.is_empty() || matches!(database_type, DatabaseType::Mysql | DatabaseType::MongoDb | DatabaseType::Questdb)
{
table
} else {
format!("{}.{}", quote_transfer_identifier(schema, database_type), table)

View File

@ -110,6 +110,17 @@ pub fn build_table_data_select_sql(options: TableDataSelectSqlOptions) -> String
);
}
if database_type == Some(DatabaseType::Questdb) {
return build_questdb_table_select_sql(
&table_alias,
&where_clause,
&order,
&options.columns,
limit,
options.offset.unwrap_or(0),
);
}
let offset =
options.offset.filter(|offset| *offset > 0).map(|offset| format!(" OFFSET {offset}")).unwrap_or_default();
format!("SELECT {select_columns} FROM {table_alias}{where_clause}{order} LIMIT {limit}{offset};")
@ -332,3 +343,27 @@ pub(super) fn build_neo4j_table_select_sql(options: &TableDataSelectSqlOptions,
let skip = options.offset.filter(|offset| *offset > 0).map(|offset| format!(" SKIP {offset}")).unwrap_or_default();
format!("MATCH (n:{label}){where_clause} RETURN {returns}{order}{skip} LIMIT {limit};")
}
pub(super) fn build_questdb_table_select_sql(
table: &str,
where_clause: &str,
order_by: &str,
columns: &[String],
limit: usize,
offset: usize,
) -> String {
let columns_sql = if columns.is_empty() {
"*".to_string()
} else {
columns
.iter()
.map(|column| quote_table_identifier(Some(DatabaseType::Questdb), column))
.collect::<Vec<_>>()
.join(", ")
};
if offset == 0 {
return format!("SELECT {columns_sql} FROM {table}{where_clause}{order_by} LIMIT {limit}");
}
let upper_bound = offset + limit;
return format!("SELECT {columns_sql} FROM {table}{where_clause}{order_by} LIMIT {offset}, {upper_bound}");
}

View File

@ -551,6 +551,29 @@ pub(super) fn build_sqlite_existing_column_sql(
statements
}
pub(super) fn build_questdb_existing_column_sql(table: &str, column: &EditableStructureColumn) -> Vec<String> {
let Some(original) = &column.original else {
return Vec::new();
};
let mut statements = Vec::new();
let current_name = &column.name;
if column.name != original.name {
statements.push(format!(
"ALTER TABLE {table} RENAME COLUMN {} TO {};",
quote_ident(StructureDialect::Questdb, &original.name),
quote_ident(StructureDialect::Questdb, &column.name)
));
}
if column.data_type.trim() != original.data_type.trim() {
statements.push(format!(
"ALTER TABLE {table} ALTER COLUMN {} TYPE {};",
quote_ident(StructureDialect::Questdb, current_name),
column_data_type(StructureDialect::Questdb, column)
));
}
statements
}
pub(super) fn has_existing_column_attribute_change(column: &EditableStructureColumn) -> bool {
let Some(original) = &column.original else {
return false;

View File

@ -5,6 +5,11 @@ use super::util::{clean, format_default_for_sql, normalize_default, quote_ident,
pub(super) fn column_definition(dialect: StructureDialect, column: &EditableStructureColumn) -> String {
let data_type = column_data_type(dialect, column);
let mut parts = vec![quote_ident(dialect, &column.name), data_type];
// QuestDB SQL Syntax: ```ALTER TABLE tableName ADD COLUMN [IF NOT EXISTS] columnName typeDef```
if dialect == StructureDialect::Questdb {
parts.insert(0, "IF NOT EXISTS".to_string());
return parts.join(" ");
}
if !column.is_nullable && !is_oracle_like(dialect) && dialect != StructureDialect::ClickHouse {
parts.push("NOT NULL".to_string());
}
@ -74,6 +79,9 @@ pub(super) fn column_data_type(dialect: StructureDialect, column: &EditableStruc
if dialect == StructureDialect::ManticoreSearch {
return manticore_column_type(column);
}
if dialect == StructureDialect::Questdb {
return questdb_column_type(column);
}
normalize_column_data_type(dialect, &column.data_type)
}
@ -186,3 +194,22 @@ pub(super) fn unwrap_clickhouse_nullable_type(data_type: &str) -> String {
trimmed.to_string()
}
}
/// QuestDB 类型处理
/// geohash, decimal 这2种类型才能带()
pub(super) fn questdb_column_type(column: &EditableStructureColumn) -> String {
let data_type = column.data_type.trim().to_ascii_lowercase();
let length_types: [&str; 2] = ["geohash", "decimal"];
match data_type.find('(') {
Some(pos) => {
let base_type = &data_type[..pos];
let params = &data_type[pos..];
if length_types.contains(&base_type) {
format!("{}{}", base_type, params)
} else {
base_type.to_string()
}
}
None => data_type,
}
}

View File

@ -1,8 +1,8 @@
use super::column_alter::{
build_clickhouse_existing_column_sql, build_h2_existing_column_sql, build_informix_existing_column_sql,
build_mysql_existing_column_sql, build_oracle_like_existing_column_sql, build_postgres_existing_column_sql,
build_sqlite_existing_column_sql, build_sqlserver_existing_column_sql, has_column_extra_change,
has_existing_column_attribute_change,
build_questdb_existing_column_sql, build_sqlite_existing_column_sql, build_sqlserver_existing_column_sql,
has_column_extra_change, has_existing_column_attribute_change,
};
use super::column_format::column_definition;
use super::comments::build_sqlserver_column_comment_sql;
@ -120,6 +120,7 @@ pub(super) fn build_column_sql(options: &TableStructureSqlOptions, warnings: &mu
&options.table_name,
)),
StructureDialect::Sqlite => statements.extend(build_sqlite_existing_column_sql(&table, column, warnings)),
StructureDialect::Questdb => statements.extend(build_questdb_existing_column_sql(&table, column)),
_ => warnings.push(format!("Editing existing columns is not supported for {database_label} yet.")),
}
}

View File

@ -13,6 +13,7 @@ pub(super) enum StructureDialect {
ClickHouse,
ManticoreSearch,
Informix,
Questdb,
Unsupported,
}
@ -116,6 +117,23 @@ pub(super) fn capabilities_for(database_type: Option<DatabaseType>) -> TableStru
alter_primary_key: true,
..base
},
Some(DatabaseType::Questdb) => TableStructureCapabilities {
dialect: StructureDialect::Questdb,
add_column: true,
drop_column: true,
rename_column: true,
alter_existing_column: true,
reorder_column: false,
comment: false,
create_index: false,
drop_index: false,
rebuild_index: false,
index_type: false,
index_include: false,
index_filter: false,
index_comment: false,
alter_primary_key: false,
},
Some(DatabaseType::Redshift | DatabaseType::Vertica) => TableStructureCapabilities {
dialect: StructureDialect::Postgres,
add_column: true,
@ -254,6 +272,7 @@ pub(super) fn dialect_label(dialect: StructureDialect) -> String {
StructureDialect::ClickHouse => "clickhouse",
StructureDialect::ManticoreSearch => "manticoresearch",
StructureDialect::Informix => "informix",
StructureDialect::Questdb => "questdb",
StructureDialect::Unsupported => "this database",
}
.to_string()
@ -272,6 +291,7 @@ pub(super) fn database_type_for_dialect(dialect: StructureDialect) -> Option<Dat
StructureDialect::ClickHouse => Some(DatabaseType::ClickHouse),
StructureDialect::ManticoreSearch => Some(DatabaseType::ManticoreSearch),
StructureDialect::Informix => Some(DatabaseType::Informix),
StructureDialect::Questdb => Some(DatabaseType::Questdb),
StructureDialect::Unsupported => None,
}
}

View File

@ -18,7 +18,9 @@ pub(super) fn qualified_table(dialect: StructureDialect, schema: Option<&str>, t
pub(super) fn quote_ident(dialect: StructureDialect, name: &str) -> String {
match dialect {
StructureDialect::Mysql | StructureDialect::ManticoreSearch => format!("`{}`", name.replace('`', "``")),
StructureDialect::Mysql | StructureDialect::ManticoreSearch | StructureDialect::Questdb => {
format!("`{}`", name.replace('`', "``"))
}
StructureDialect::SqlServer => format!("[{}]", name.replace(']', "]]")),
StructureDialect::Informix if is_simple_informix_identifier(name) => name.to_string(),
_ => format!("\"{}\"", name.replace('"', "\"\"")),

View File

@ -1471,6 +1471,10 @@ pub fn pagination_sql(
"SELECT {col_list} FROM {full_table} ORDER BY (SELECT NULL) OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY"
)
}
DatabaseType::Questdb => {
let upper_bound = offset + limit as u64;
format!("SELECT {col_list} FROM {full_table} LIMIT {offset}, {upper_bound}")
}
_ => {
format!("SELECT {col_list} FROM {full_table} LIMIT {limit} OFFSET {offset}")
}
@ -1497,6 +1501,11 @@ pub fn pagination_sql_with_order(
"SELECT {col_list} FROM {full_table} ORDER BY {order_by} OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY"
)
}
DatabaseType::Questdb => {
let upper_bound = offset + limit as u64;
let order_by = order_expression.map(|value| format!(" ORDER BY {value}")).unwrap_or_default();
format!("SELECT {col_list} FROM {full_table}{order_by} LIMIT {offset}, {upper_bound}")
}
_ => {
let order_by = order_expression.map(|value| format!(" ORDER BY {value}")).unwrap_or_default();
format!("SELECT {col_list} FROM {full_table}{order_by} LIMIT {limit} OFFSET {offset}")
@ -1533,6 +1542,11 @@ pub fn pagination_sql_with_filter_order(
"SELECT {col_list} FROM {full_table}{where_clause} ORDER BY {order_by} OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY"
)
}
DatabaseType::Questdb => {
let upper_bound = offset + limit as u64;
let order_by = order_expression.map(|value| format!(" ORDER BY {value}")).unwrap_or_default();
format!("SELECT {col_list} FROM {full_table}{where_clause}{order_by} LIMIT {offset}, {upper_bound}")
}
_ => {
let order_by = order_expression.map(|value| format!(" ORDER BY {value}")).unwrap_or_default();
format!("SELECT {col_list} FROM {full_table}{where_clause}{order_by} LIMIT {limit} OFFSET {offset}")
@ -3601,6 +3615,21 @@ mod tests {
assert_eq!(sql, "SELECT \"id\", \"name\" FROM \"public\".\"users\" ORDER BY \"id\" LIMIT 100 OFFSET 200");
}
#[test]
fn questdb_pagination_uses_stable_primary_key_order() {
let sql = pagination_sql_with_order(
&[String::from("id"), String::from("name")],
"users",
"public",
&DatabaseType::Questdb,
200,
100,
&[String::from("id")],
);
assert_eq!(sql, "SELECT `id`, `name` FROM `users` ORDER BY `id` LIMIT 200, 300");
}
#[test]
fn filtered_pagination_preserves_where_and_order() {
let sql = pagination_sql_with_filter_order(

View File

@ -141,6 +141,7 @@ fn classifies_agent_database_types() {
assert!(!is_agent_type(&DatabaseType::Gaussdb));
assert!(!is_agent_type(&DatabaseType::Kwdb));
assert!(!is_agent_type(&DatabaseType::OpenGauss));
assert!(!is_agent_type(&DatabaseType::Questdb));
}
#[test]
@ -192,6 +193,7 @@ fn skips_tcp_probe_for_local_file_plugin_and_agent_types() {
assert!(!skips_tcp_probe(&DatabaseType::Gaussdb));
assert!(!skips_tcp_probe(&DatabaseType::Kwdb));
assert!(!skips_tcp_probe(&DatabaseType::OpenGauss));
assert!(!skips_tcp_probe(&DatabaseType::Questdb));
}
#[test]

View File

@ -50,6 +50,7 @@ const databaseSupport = [
{ name: "Cassandra", icon: "/icons/database/cassandra.svg", tone: "#1287b1" },
{ name: "Kylin", icon: "/icons/database/apache_kylin.svg", tone: "#fb8c00" },
{ name: "InfluxDB", icon: "/icons/database/influxdb.svg", tone: "#22adf6" },
{ name: "QuestDB", icon: "/icons/database/questdb.svg", tone: "#dc2626" },
{ name: "IoTDB", icon: "/icons/database/iotdb.svg", tone: "#3cb371" },
{ name: "KWDB", icon: "/icons/database/kwdb.svg", tone: "#6366f1" },
{ name: "Vastbase", icon: "/icons/database/vastbase.png", tone: "#2563eb" },

View File

@ -102,6 +102,7 @@ const databaseSupport = [
{ name: "Cassandra", icon: "/icons/database/cassandra.svg", tone: "#1287b1" },
{ name: "Kylin", icon: "/icons/database/apache_kylin.svg", tone: "#fb8c00" },
{ name: "InfluxDB", icon: "/icons/database/influxdb.svg", tone: "#22adf6" },
{ name: "QuestDB", icon: "/icons/database/questdb.svg", tone: "#dc2626" },
{ name: "IoTDB", icon: "/icons/database/iotdb.svg", tone: "#3cb371" },
{ name: "KWDB", icon: "/icons/database/kwdb.svg", tone: "#6366f1" },
{ name: "Vastbase", icon: "/icons/database/vastbase.png", tone: "#2563eb" },

View File

@ -16,7 +16,7 @@ description: 了解 DBX 可以连接哪些数据库,以及每类高级功能
| MySQL 兼容类型 | MariaDB、TiDB、OceanBase、Doris、SelectDB、StarRocks、Manticore Search、GoldenDB、TDSQL、PolarDB、GreatSQL、自定义 MySQL | 复用 MySQL 风格连接能力 |
| PostgreSQL 兼容类型 | openGauss、KingBase、HighGo、Vastbase、CockroachDB、自定义 PostgreSQL | 复用 PostgreSQL 风格连接能力 |
| 文件型数据库 | SQLite、DuckDB、Microsoft Access、RQLite、Turso | 选择本地数据库文件或 HTTP 端点,不填写主机和端口 |
| 时序与边缘数据库 | InfluxDB、IoTDB、TDengine、Databend | 面向时序、IoT 或边缘场景优化 |
| 时序与边缘数据库 | InfluxDB、IoTDB、TDengine、Databend、QuestDB | 面向时序、IoT 或边缘场景优化 |
| Agent/JDBC 扩展类型 | H2、Snowflake、Trino、Hive、DB2、Informix、Neo4j、Cassandra、BigQuery、Kylin、SunDB、XuguDB、Databricks、IRIS、JDBC | 功能覆盖取决于对应驱动路径 |
## 默认端口
@ -67,6 +67,7 @@ description: 了解 DBX 可以连接哪些数据库,以及每类高级功能
| SunDB | 22000 |
| TDengine | 6041 |
| XuguDB | 5138 |
| QuestDB | 8812 |
<Callout type="info">SQLite、DuckDB、Access 和 JDBC 在不需要网络端口时,会在连接模型中保存为端口 `0`。</Callout>

View File

@ -16,7 +16,7 @@ The connection dialog exposes ready-to-use profiles with default ports and drive
| MySQL-compatible profiles | MariaDB, TiDB, OceanBase, Doris, SelectDB, StarRocks, Manticore Search, GoldenDB, TDSQL, PolarDB, GreatSQL, custom MySQL | Reuse MySQL-style connection handling where the engine speaks a compatible protocol |
| PostgreSQL-compatible profiles | openGauss, KingBase, HighGo, Vastbase, CockroachDB, custom PostgreSQL | Reuse PostgreSQL-style connection handling where the engine speaks a compatible protocol |
| File-based engines | SQLite, DuckDB, Microsoft Access, RQLite, Turso | Choose a local database file or HTTP endpoint instead of host and port |
| Time-series and edge | InfluxDB, IoTDB, TDengine, Databend | Optimized for time-series, IoT, or edge workloads |
| Time-series and edge | InfluxDB, IoTDB, TDengine, Databend, QuestDB | Optimized for time-series, IoT, or edge workloads |
| Agent/JDBC-oriented engines | H2, Snowflake, Trino, Hive, DB2, Informix, Neo4j, Cassandra, BigQuery, Kylin, SunDB, XuguDB, Databricks, IRIS, JDBC | Feature coverage depends on the driver path used by that engine |
## Default Ports
@ -67,6 +67,7 @@ The connection dialog exposes ready-to-use profiles with default ports and drive
| SunDB | 22000 |
| TDengine | 6041 |
| XuguDB | 5138 |
| QuestDB | 8812 |
<Callout type="info">SQLite, DuckDB, Access, and JDBC profiles use port `0` in the saved connection model when a network port is not part of the connection.</Callout>
@ -93,7 +94,7 @@ DBX intentionally enables advanced workflows only where the app has enough metad
Most network databases support host, port, username, password, default database, optional URL parameters, SSL, connection color, SSH tunnel, and proxy settings. File-based engines replace host and port with a file picker.
DBX can also parse common connection URLs for engines such as MySQL, PostgreSQL, Redis, MongoDB, ClickHouse, SQL Server, Oracle, Elasticsearch, DM, GaussDB, KWDB, openGauss, TDengine, XuguDB, Access, Teradata, Vertica, Firebird, Exasol, GBase, YashanDB, SAP HANA, InfluxDB, IoTDB, etcd, RQLite, and Databricks.
DBX can also parse common connection URLs for engines such as MySQL, PostgreSQL, Redis, MongoDB, ClickHouse, SQL Server, Oracle, Elasticsearch, DM, GaussDB, KWDB, openGauss, TDengine, XuguDB, Access, Teradata, Vertica, Firebird, Exasol, GBase, YashanDB, SAP HANA, InfluxDB, QuestDB, IoTDB, etcd, RQLite, and Databricks.
### ClickHouse TLS

View File

@ -64,7 +64,7 @@ test("treats Access as a local single-database agent driver", () => {
});
test("exposes the extended JDBC agent ecosystem through driver management", () => {
for (const dbType of ["databricks", "saphana", "teradata", "vertica", "firebird", "exasol", "opengauss", "oceanbase-oracle", "gbase"] as const) {
for (const dbType of ["databricks", "saphana", "teradata", "vertica", "firebird", "exasol", "opengauss", "oceanbase-oracle", "gbase", "questdb"] as const) {
assert.equal(supportsDriverManagement(dbType), true, `${dbType} should be agent-managed`);
assert.equal(supportsDatabaseSearch(dbType), true, `${dbType} should support database search`);
}

View File

@ -46,7 +46,7 @@ function formatQueryToolResult(result: QueryResult, title?: string) {
}
export const DBX_CONNECTION_TYPE_DESCRIPTION =
"Database type: postgres, mysql, sqlite, rqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, gbase, h2, snowflake, trino, hive, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, tdengine, iotdb, xugu, jdbc, access";
"Database type: postgres, mysql, sqlite, rqlite, redis, duckdb, clickhouse, sqlserver, mongodb, oracle, elasticsearch, etcd, doris, starrocks, manticoresearch, redshift, dameng, kingbase, highgo, vastbase, goldendb, databend, gaussdb, kwdb, yashandb, databricks, saphana, teradata, vertica, firebird, exasol, opengauss, oceanbase-oracle, questdb, gbase, h2, snowflake, trino, hive, db2, informix, influxdb, iris, neo4j, cassandra, bigquery, kylin, sundb, tdengine, iotdb, xugu, jdbc, access";
const FILE_CAPABLE_CONNECTION_TYPES = new Set(["sqlite", "duckdb", "access", "h2"]);
export function createDbxMcpServer(backend: Backend, options: { isWebMode?: boolean } = {}): McpServer {

View File

@ -2,7 +2,7 @@ import { access, readFile } from "node:fs/promises";
import { bridgePortFilePath, dbPath, appDataDir } from "./paths.js";
import { inspectConnectionStore } from "./connections.js";
export const DIRECT_QUERY_TYPES = ["postgres", "redshift", "mysql", "doris", "starrocks", "manticoresearch", "sqlite", "rqlite", "gaussdb", "kwdb", "opengauss"] as const;
export const DIRECT_QUERY_TYPES = ["postgres", "redshift", "mysql", "doris", "starrocks", "manticoresearch", "sqlite", "rqlite", "gaussdb", "kwdb", "opengauss", "questdb"] as const;
export type DirectQueryType = (typeof DIRECT_QUERY_TYPES)[number];

View File

@ -287,6 +287,7 @@ pub async fn test_connection(state: State<'_, Arc<AppState>>, config: Connection
| DatabaseType::Redshift
| DatabaseType::Gaussdb
| DatabaseType::Kwdb
| DatabaseType::Questdb
| DatabaseType::OpenGauss => match db::postgres::connect(&url, connect_timeout).await {
Ok(pool) => {
pool.close();
@ -497,6 +498,7 @@ pub async fn connect_db(state: State<'_, Arc<AppState>>, config: ConnectionConfi
| 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())