diff --git a/apps/desktop/src/components/icons/DatabaseIcon.vue b/apps/desktop/src/components/icons/DatabaseIcon.vue
index 9cac04e3c8..b99cab5ddd 100644
--- a/apps/desktop/src/components/icons/DatabaseIcon.vue
+++ b/apps/desktop/src/components/icons/DatabaseIcon.vue
@@ -14,6 +14,7 @@ const assetIcons: Record
= {
sqlite: "sqlite",
rqlite: "rqlite.png",
turso: "turso.png",
+ cloudflare_d1: "cloudflare-d1",
redis: "redis",
mongodb: "mongodb",
mongodb_legacy: "mongodb",
diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue
index 5c9d672569..654c4072f0 100644
--- a/apps/desktop/src/components/sidebar/TreeItem.vue
+++ b/apps/desktop/src/components/sidebar/TreeItem.vue
@@ -474,6 +474,8 @@ function connectionTooltipScheme(config: Pick {
expect(supportsTransaction("duckdb")).toBe(false);
expect(supportsTransaction("qdrant")).toBe(false);
expect(supportsTransaction("turso")).toBe(false);
+ expect(supportsTransaction("cloudflare-d1")).toBe(false);
expect(supportsTransaction("sqlite")).toBe(false);
expect(supportsTransaction("clickhouse")).toBe(false);
expect(supportsTransaction("sqlserver")).toBe(false);
diff --git a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts
index 48a27374c2..b68414d52a 100644
--- a/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts
+++ b/apps/desktop/src/lib/__tests__/sql/sqlFormatter.spec.ts
@@ -9,7 +9,7 @@ describe("sqlFormatter", () => {
});
it("maps SQLite-compatible database types to the sqlite formatter dialect", () => {
- for (const dbType of ["sqlite", "rqlite", "turso"]) {
+ for (const dbType of ["sqlite", "rqlite", "turso", "cloudflare-d1"]) {
expect(sqlFormatDialectForDbType(dbType)).toBe("sqlite");
}
});
diff --git a/apps/desktop/src/lib/connection/cloudflareD1.ts b/apps/desktop/src/lib/connection/cloudflareD1.ts
new file mode 100644
index 0000000000..d8a1b5788a
--- /dev/null
+++ b/apps/desktop/src/lib/connection/cloudflareD1.ts
@@ -0,0 +1,23 @@
+import type { ConnectionConfig } from "@/types/database";
+
+type CloudflareD1Config = Pick;
+type MutableCloudflareD1Config = CloudflareD1Config & Pick;
+
+export function isCloudflareD1Connection(config: Pick): boolean {
+ return config.db_type === "cloudflare-d1";
+}
+
+export function hasCloudflareD1Credentials(config: CloudflareD1Config): boolean {
+ return !!config.host.trim() && !!config.database?.trim() && !!config.password.trim();
+}
+
+export function normalizeCloudflareD1Connection(config: MutableCloudflareD1Config): void {
+ config.host = config.host.trim();
+ config.database = config.database?.trim() || undefined;
+ config.password = config.password.trim();
+ config.port = 443;
+ config.username = "";
+ config.ssl = true;
+ config.url_params = "";
+ config.transport_layers = [];
+}
diff --git a/apps/desktop/src/lib/connection/connectionPresentation.ts b/apps/desktop/src/lib/connection/connectionPresentation.ts
index ab35c6b989..31a5a7211d 100644
--- a/apps/desktop/src/lib/connection/connectionPresentation.ts
+++ b/apps/desktop/src/lib/connection/connectionPresentation.ts
@@ -17,6 +17,7 @@ export function connectionDriverLabel(connection?: Pick(["elasticsearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper"]);
+const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set(["cloudflare-d1", "elasticsearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper"]);
type VisibleDatabaseConnectionFields = Pick<
ConnectionConfig,
diff --git a/apps/desktop/src/lib/database/databaseCapabilitySets.ts b/apps/desktop/src/lib/database/databaseCapabilitySets.ts
index 7155c88ce4..3d4511fbe3 100644
--- a/apps/desktop/src/lib/database/databaseCapabilitySets.ts
+++ b/apps/desktop/src/lib/database/databaseCapabilitySets.ts
@@ -75,4 +75,4 @@ export const DATABASE_OBJECT_TREE_TYPES = new Set(["jdbc"]);
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", "questdb", "oceanbase-oracle"]);
+export const DIAGRAM_SQL_TYPES = new Set(["mysql", "postgres", "sqlite", "rqlite", "turso", "cloudflare-d1", "sqlserver", "oracle", "redshift", "dameng", "gaussdb", "kwdb", "opengauss", "questdb", "oceanbase-oracle"]);
diff --git a/apps/desktop/src/lib/database/databaseFeatureSupport.ts b/apps/desktop/src/lib/database/databaseFeatureSupport.ts
index a2aa8801d4..06b503400d 100644
--- a/apps/desktop/src/lib/database/databaseFeatureSupport.ts
+++ b/apps/desktop/src/lib/database/databaseFeatureSupport.ts
@@ -118,7 +118,7 @@ export function supportsObjectBrowserTreeNode(dbType: DatabaseType | undefined,
}
export function supportsTableTruncate(dbType?: DatabaseType): boolean {
- return !!dbType && dbType !== "sqlite" && dbType !== "rqlite" && dbType !== "turso" && dbType !== "duckdb" && dbType !== "influxdb" && dbType !== "manticoresearch";
+ return !!dbType && dbType !== "sqlite" && dbType !== "rqlite" && dbType !== "turso" && dbType !== "cloudflare-d1" && dbType !== "duckdb" && dbType !== "influxdb" && dbType !== "manticoresearch";
}
export function usesPostgresLikeStructureCopy(dbType?: DatabaseType): boolean {
diff --git a/apps/desktop/src/lib/database/databaseNamespaceCreation.ts b/apps/desktop/src/lib/database/databaseNamespaceCreation.ts
index fe0f897589..253afc6bb1 100644
--- a/apps/desktop/src/lib/database/databaseNamespaceCreation.ts
+++ b/apps/desktop/src/lib/database/databaseNamespaceCreation.ts
@@ -20,6 +20,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = {
sqlite: { deferred: "file-backed; create a new connection/file instead" },
rqlite: { deferred: "single SQLite-compatible database per node" },
turso: { deferred: "remote libSQL database lifecycle is provider-managed" },
+ "cloudflare-d1": { deferred: "Cloudflare D1 database lifecycle is provider-managed" },
redis: { deferred: "numbered logical databases are server-configured" },
duckdb: { connection: "attach" },
clickhouse: { connection: "database" },
diff --git a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts
index 381a4cab8c..dc4e134a28 100644
--- a/apps/desktop/src/lib/database/databaseObjectCapabilities.ts
+++ b/apps/desktop/src/lib/database/databaseObjectCapabilities.ts
@@ -35,6 +35,7 @@ const DATABASE_TYPE_OBJECTS = new Map([
["sqlite", TABLE_VIEW_OBJECTS],
["rqlite", TABLE_VIEW_OBJECTS],
["turso", TABLE_VIEW_OBJECTS],
+ ["cloudflare-d1", TABLE_VIEW_OBJECTS],
["duckdb", TABLE_VIEW_OBJECTS],
["clickhouse", TABLE_VIEW_OBJECTS],
["doris", TABLE_VIEW_OBJECTS],
diff --git a/apps/desktop/src/lib/database/databasePropertyEditing.ts b/apps/desktop/src/lib/database/databasePropertyEditing.ts
index 9012d3d832..f5e71b3aac 100644
--- a/apps/desktop/src/lib/database/databasePropertyEditing.ts
+++ b/apps/desktop/src/lib/database/databasePropertyEditing.ts
@@ -21,6 +21,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = {
sqlite: { deferred: "file-backed database properties are not edited in-place" },
rqlite: { deferred: "single SQLite-compatible database per node" },
turso: { deferred: "remote libSQL database lifecycle is provider-managed" },
+ "cloudflare-d1": { deferred: "Cloudflare D1 database lifecycle is provider-managed" },
redis: { deferred: "numbered logical databases are server-configured" },
duckdb: { deferred: "attached database file properties need a dedicated DuckDB workflow" },
clickhouse: { deferred: "database property editing not verified for first pass" },
diff --git a/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts b/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts
index 0b08911604..1dc6c1ea6f 100644
--- a/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts
+++ b/apps/desktop/src/lib/database/databaseTableDataCapabilities.ts
@@ -52,6 +52,7 @@ const NAVICAT_STYLE_TABLE_DATA_TYPES = new Set([
"sqlite",
"rqlite",
"turso",
+ "cloudflare-d1",
"duckdb",
"sqlserver",
"oracle",
diff --git a/apps/desktop/src/lib/database/defaultDatabase.ts b/apps/desktop/src/lib/database/defaultDatabase.ts
index ad7910967a..1a923eeecd 100644
--- a/apps/desktop/src/lib/database/defaultDatabase.ts
+++ b/apps/desktop/src/lib/database/defaultDatabase.ts
@@ -4,7 +4,8 @@ import { usesTreeSchemaMode } from "@/lib/database/databaseCapabilities";
export const TREE_SCHEMA_DEFAULT_DATABASE_SELECT_VALUE = "__dbx_tree_schema_default_database__";
export const EMPTY_DATABASE_SELECT_VALUE = "__dbx_empty_database__";
-export function resolveDefaultDatabase(connection: Pick, options: string[]): string {
+export function resolveDefaultDatabase(connection: Pick & Partial>, options: string[]): string {
+ if (connection.db_type === "cloudflare-d1") return "main";
return connection.database || options[0] || "";
}
@@ -29,6 +30,7 @@ export function formatDatabaseLabel(connection: Pick | undefined, database: string): boolean {
+export function isDefaultDatabase(connection: (Pick & Partial>) | undefined, database: string): boolean {
+ if (connection?.db_type === "cloudflare-d1") return database === "main";
return !!connection?.database && !!database && connection.database === database;
}
diff --git a/apps/desktop/src/lib/sql/cloudflareD1.ts b/apps/desktop/src/lib/sql/cloudflareD1.ts
new file mode 100644
index 0000000000..f52c6ac91c
--- /dev/null
+++ b/apps/desktop/src/lib/sql/cloudflareD1.ts
@@ -0,0 +1 @@
+export const CLOUDFLARE_D1_COMMON_FUNCTION_NAMES = new Set(["ABS", "AVG", "CAST", "CEIL", "COALESCE", "CONCAT", "COUNT", "FLOOR", "LENGTH", "LOWER", "LTRIM", "MAX", "MIN", "MOD", "NULLIF", "REPLACE", "ROUND", "RTRIM", "SIGN", "SQRT", "SUBSTR", "SUBSTRING", "SUM", "TRIM", "UPPER"]);
diff --git a/apps/desktop/src/lib/sql/semantic/dialect.ts b/apps/desktop/src/lib/sql/semantic/dialect.ts
index 8220a6519b..54199676f1 100644
--- a/apps/desktop/src/lib/sql/semantic/dialect.ts
+++ b/apps/desktop/src/lib/sql/semantic/dialect.ts
@@ -155,6 +155,7 @@ export function sqlSemanticDialectFor(options: { databaseType?: DatabaseType; di
case "sqlite":
case "rqlite":
case "turso":
+ case "cloudflare-d1":
return SQL_SEMANTIC_DIALECTS.sqlite;
case "duckdb":
return SQL_SEMANTIC_DIALECTS.duckdb;
diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts
index 0233910446..ddaf87e755 100644
--- a/apps/desktop/src/lib/sql/sqlCompletion.ts
+++ b/apps/desktop/src/lib/sql/sqlCompletion.ts
@@ -1,6 +1,7 @@
import { Cassandra, MariaSQL, MSSQL, MySQL, PLSQL, PostgreSQL, SQLite, StandardSQL } from "@codemirror/lang-sql";
import type { DatabaseType, SqlSnippet } from "@/types/database";
import { buildMongoCompletionItemsFromContext, type MongoCompletionItem } from "@/lib/mongo/mongoCompletion";
+import { CLOUDFLARE_D1_COMMON_FUNCTION_NAMES } from "@/lib/sql/cloudflareD1";
const SQL_KEYWORDS = [
"SELECT",
@@ -470,6 +471,7 @@ const DATABASE_SQL_KEYWORDS: Partial> = {
sqlite: SQLITE_SQL_KEYWORDS,
rqlite: SQLITE_SQL_KEYWORDS,
turso: SQLITE_SQL_KEYWORDS,
+ "cloudflare-d1": SQLITE_SQL_KEYWORDS,
sqlserver: SQLSERVER_SQL_KEYWORDS,
manticoresearch: MANTICORESEARCH_SQL_KEYWORDS,
};
@@ -896,6 +898,8 @@ const SQLITE_FUNCTION_SIGNATURES = new Map([
["NOW", []],
]);
+const CLOUDFLARE_D1_FUNCTION_SIGNATURES = new Map(Array.from(SQLITE_FUNCTION_SIGNATURES.entries()).filter(([name]) => name !== "NOW"));
+
const SQLSERVER_FUNCTION_SIGNATURES = new Map([
["TRY_CAST", ["expression AS type"]],
["TRY_CONVERT", ["type", "expression"]],
@@ -958,6 +962,7 @@ const DATABASE_FUNCTION_SIGNATURES: Partial {
- const signatures = databaseType ? new Map(Array.from(SQL_FUNCTION_SIGNATURES.entries()).filter(([name]) => COMMON_SQL_FUNCTION_NAMES.has(name))) : new Map(SQL_FUNCTION_SIGNATURES);
+ const commonFunctionNames = databaseType === "cloudflare-d1" ? CLOUDFLARE_D1_COMMON_FUNCTION_NAMES : COMMON_SQL_FUNCTION_NAMES;
+ const signatures = databaseType ? new Map(Array.from(SQL_FUNCTION_SIGNATURES.entries()).filter(([name]) => commonFunctionNames.has(name))) : new Map(SQL_FUNCTION_SIGNATURES);
const databaseSignatures = databaseType ? DATABASE_FUNCTION_SIGNATURES[databaseType] : undefined;
if (databaseSignatures) {
for (const [name, parameters] of databaseSignatures) signatures.set(name, parameters);
diff --git a/apps/desktop/src/lib/sql/sqlFormatter.ts b/apps/desktop/src/lib/sql/sqlFormatter.ts
index cf7d7a2bc0..1980de3cd5 100644
--- a/apps/desktop/src/lib/sql/sqlFormatter.ts
+++ b/apps/desktop/src/lib/sql/sqlFormatter.ts
@@ -30,6 +30,7 @@ export function sqlFormatDialectForDbType(dbType: string | null | undefined): Sq
case "sqlite":
case "rqlite":
case "turso":
+ case "cloudflare-d1":
return "sqlite";
case "sqlserver":
return "sqlserver";
diff --git a/apps/desktop/src/lib/table/objectRenameSql.ts b/apps/desktop/src/lib/table/objectRenameSql.ts
index 226fd7f980..be8279751c 100644
--- a/apps/desktop/src/lib/table/objectRenameSql.ts
+++ b/apps/desktop/src/lib/table/objectRenameSql.ts
@@ -21,7 +21,7 @@ export function supportsObjectRename(databaseType: DatabaseType | undefined, obj
if (objectType === "PROCEDURE" || objectType === "FUNCTION") {
return false;
}
- if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso") return objectType === "TABLE";
+ if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso" || databaseType === "cloudflare-d1") return objectType === "TABLE";
if (databaseType === "mysql" || databaseType === "goldendb") return objectType === "TABLE" || objectType === "VIEW";
if (postgresLikeRenameTypes.has(databaseType)) return objectType === "TABLE" || objectType === "VIEW" || objectType === "MATERIALIZED_VIEW";
if (oracleLikeRenameTypes.has(databaseType)) return objectType === "TABLE" || objectType === "VIEW" || objectType === "MATERIALIZED_VIEW";
diff --git a/apps/desktop/src/lib/table/tableImport.ts b/apps/desktop/src/lib/table/tableImport.ts
index 13b04c3f56..96586bda74 100644
--- a/apps/desktop/src/lib/table/tableImport.ts
+++ b/apps/desktop/src/lib/table/tableImport.ts
@@ -151,29 +151,29 @@ export function importDataTypeForDatabase(inferredType: ImportInferredType, data
case "boolean":
if (["mysql", "doris", "starrocks", "goldendb", "sundb", "databend"].includes(databaseType || "")) return "TINYINT(1)";
if (databaseType === "sqlserver") return "BIT";
- if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso") return "INTEGER";
+ if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso" || databaseType === "cloudflare-d1") return "INTEGER";
if (databaseType === "oracle" || databaseType === "oceanbase-oracle" || databaseType === "dameng") return "NUMBER(1)";
if (databaseType === "clickhouse") return "UInt8";
return "BOOLEAN";
case "integer":
- if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso") return "INTEGER";
+ if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso" || databaseType === "cloudflare-d1") return "INTEGER";
if (databaseType === "oracle" || databaseType === "oceanbase-oracle" || databaseType === "dameng") return "NUMBER(19)";
if (databaseType === "clickhouse") return "Int64";
return "BIGINT";
case "decimal":
if (["postgres", "gaussdb", "opengauss", "redshift", "kingbase", "highgo", "kwdb", "vastbase"].includes(databaseType || "")) return "DOUBLE PRECISION";
- if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso") return "REAL";
+ if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso" || databaseType === "cloudflare-d1") return "REAL";
if (databaseType === "oracle" || databaseType === "oceanbase-oracle" || databaseType === "dameng") return "BINARY_DOUBLE";
if (databaseType === "clickhouse") return "Float64";
return "DOUBLE";
case "date":
- if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso") return "TEXT";
+ if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso" || databaseType === "cloudflare-d1") return "TEXT";
if (databaseType === "clickhouse") return "Date";
return "DATE";
case "timestamp":
if (["mysql", "doris", "starrocks", "goldendb", "sundb", "databend"].includes(databaseType || "")) return "DATETIME";
if (databaseType === "sqlserver") return "DATETIME2";
- if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso") return "TEXT";
+ if (databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "turso" || databaseType === "cloudflare-d1") return "TEXT";
if (databaseType === "clickhouse") return "DateTime64";
return "TIMESTAMP";
case "json":
diff --git a/apps/desktop/src/lib/table/tableSqlTemplates.ts b/apps/desktop/src/lib/table/tableSqlTemplates.ts
index 0446e74e61..10244a9778 100644
--- a/apps/desktop/src/lib/table/tableSqlTemplates.ts
+++ b/apps/desktop/src/lib/table/tableSqlTemplates.ts
@@ -129,7 +129,7 @@ function columnPlaceholderValue(column: ColumnInfo, databaseType?: DatabaseType)
const colName = column.name;
if (typeLooksNumeric(dataType)) return "0";
- if (typeLooksBoolean(dataType)) return databaseType === "mysql" || databaseType === "sqlite" || databaseType === "rqlite" ? "1" : "TRUE";
+ if (typeLooksBoolean(dataType)) return databaseType === "mysql" || databaseType === "sqlite" || databaseType === "rqlite" || databaseType === "cloudflare-d1" ? "1" : "TRUE";
if (typeLooksDateOnly(dataType)) return "'2024-01-01'";
if (typeLooksTimeOnly(dataType)) return "'12:00:00'";
if (typeLooksTimestamp(dataType)) return databaseType === "tdengine" ? "NOW" : "CURRENT_TIMESTAMP";
diff --git a/apps/desktop/src/stores/connectionStore.ts b/apps/desktop/src/stores/connectionStore.ts
index 7ab6e955bb..1a12c05ab6 100644
--- a/apps/desktop/src/stores/connectionStore.ts
+++ b/apps/desktop/src/stores/connectionStore.ts
@@ -1692,6 +1692,7 @@ export const useConnectionStore = defineStore("connection", () => {
async function setDefaultDatabase(connectionId: string, database: string) {
const config = getConfig(connectionId);
+ if (config?.db_type === "cloudflare-d1") return;
if (!config || config.database === database) return;
await updateConnection({
...config,
@@ -1701,6 +1702,7 @@ export const useConnectionStore = defineStore("connection", () => {
async function clearDefaultDatabase(connectionId: string) {
const config = getConfig(connectionId);
+ if (config?.db_type === "cloudflare-d1") return;
if (!config || !config.database) return;
await updateConnection({
...config,
@@ -1709,7 +1711,9 @@ export const useConnectionStore = defineStore("connection", () => {
}
function isDefaultDatabase(connectionId: string, database: string): boolean {
- return getConfig(connectionId)?.database === database && database !== "";
+ const config = getConfig(connectionId);
+ if (config?.db_type === "cloudflare-d1") return database === "main";
+ return config?.database === database && database !== "";
}
async function setVisibleDatabases(connectionId: string, databaseNames: string[]) {
diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts
index f3fd3940d9..8453378dc8 100644
--- a/apps/desktop/src/types/database.ts
+++ b/apps/desktop/src/types/database.ts
@@ -4,6 +4,7 @@ export type DatabaseType =
| "sqlite"
| "rqlite"
| "turso"
+ | "cloudflare-d1"
| "redis"
| "duckdb"
| "clickhouse"
diff --git a/crates/dbx-core/assets/database-drivers.manifest.json b/crates/dbx-core/assets/database-drivers.manifest.json
index e66f3b4570..cd3c6b85c6 100644
--- a/crates/dbx-core/assets/database-drivers.manifest.json
+++ b/crates/dbx-core/assets/database-drivers.manifest.json
@@ -116,6 +116,35 @@
"driverManagement": false
}
},
+ {
+ "dbType": "cloudflare-d1",
+ "label": "Cloudflare D1",
+ "runtimeMode": "native",
+ "mcpMode": "bridge",
+ "singleConnectionPool": true,
+ "metadataConnectionScoped": false,
+ "skipTcpProbe": true,
+ "defaultPort": 443,
+ "supportLevel": "operate",
+ "capabilities": {
+ "queryExecution": true,
+ "metadataBrowse": true,
+ "objectBrowser": true,
+ "objectSource": true,
+ "schemaSearch": true,
+ "diagram": true,
+ "tableDataEdit": true,
+ "tableStructureEdit": false,
+ "tableImport": true,
+ "dataTransfer": true,
+ "sqlFileExecution": true,
+ "databaseCreate": false,
+ "fieldLineage": true,
+ "sqlExplain": false,
+ "userAdmin": false,
+ "driverManagement": false
+ }
+ },
{
"dbType": "redis",
"label": "Redis",
diff --git a/crates/dbx-core/src/connection.rs b/crates/dbx-core/src/connection.rs
index 9f4240a812..36dd3b1013 100644
--- a/crates/dbx-core/src/connection.rs
+++ b/crates/dbx-core/src/connection.rs
@@ -86,6 +86,7 @@ pub enum PoolKind {
Sqlite(db::sqlite::SqliteHandle),
Rqlite(db::rqlite_driver::RqliteClient),
Turso(db::turso_driver::TursoClient),
+ CloudflareD1(db::cloudflare_d1_driver::CloudflareD1Client),
Redis(db::redis_driver::RedisConnection),
DuckDb(DuckDbHandle),
DuckDbWorker(DuckDbWorkerHandle),
@@ -244,7 +245,11 @@ pub fn database_connection_config(config: &ConnectionConfig, database: Option<&s
if let Some(db) = database {
if !matches!(
db_config.db_type,
- DatabaseType::Oracle | DatabaseType::Dameng | DatabaseType::MongoDb | DatabaseType::OceanbaseOracle
+ DatabaseType::Oracle
+ | DatabaseType::Dameng
+ | DatabaseType::MongoDb
+ | DatabaseType::OceanbaseOracle
+ | DatabaseType::CloudflareD1
) {
db_config.database = Some(db.to_string());
}
@@ -1083,6 +1088,9 @@ impl AppState {
db::turso_driver::test_connection(&client, connect_timeout).await?;
PoolKind::Turso(client)
}
+ DatabaseType::CloudflareD1 => {
+ PoolKind::CloudflareD1(db::cloudflare_d1_driver::connect(&db_config, connect_timeout).await?)
+ }
DatabaseType::Redis => {
let con = if db_config.uses_redis_cluster() {
db::redis_driver::RedisConnection::Cluster(
@@ -1848,6 +1856,18 @@ impl AppState {
}
}
}
+ PoolKind::CloudflareD1(client) => {
+ let client = client.clone();
+ drop(connections);
+ let timeout = crate::db::connection_timeout();
+ match db::cloudflare_d1_driver::test_connection(&client, timeout).await {
+ Ok(()) => false,
+ Err(err) => {
+ log::warn!("Cloudflare D1 connection pool '{pool_key}' is stale: {err}");
+ true
+ }
+ }
+ }
PoolKind::Agent(client) => {
let client = client.clone();
drop(connections);
@@ -2360,6 +2380,15 @@ impl AppState {
false
}
},
+ PoolKind::CloudflareD1(client) => {
+ match db::cloudflare_d1_driver::test_connection(client, timeout).await {
+ Ok(()) => true,
+ Err(e) => {
+ log::warn!("Cloudflare D1 connection pool '{key}' is unhealthy: {e}");
+ false
+ }
+ }
+ }
PoolKind::Agent(client) => {
let mut agent = client.lock().await;
match agent.test_connection(serde_json::json!({})).await {
@@ -2769,12 +2798,13 @@ fn session_scoped_pool_key_for(
client_session_id: Option<&str>,
) -> String {
let shares_base_pool = config.is_some_and(|config| {
- config.db_type == DatabaseType::DuckDb
+ matches!(config.db_type, DatabaseType::DuckDb | DatabaseType::CloudflareD1)
|| (config.db_type == DatabaseType::Sqlite && db::sqlite::is_memory_database_path(&config.host))
});
if shares_base_pool {
- // In-memory SQLite databases only exist inside one connection. A session-scoped
- // handle would silently point query/data tabs at a different empty database.
+ // DuckDB and D1 already use connection-scoped handles. In-memory SQLite databases
+ // only exist inside one connection, so a session-scoped handle would point tabs at
+ // a different empty database.
return base_pool_key;
}
session_scoped_pool_key(base_pool_key, client_session_id)
@@ -2787,6 +2817,7 @@ fn clone_pool_kind(pool: &PoolKind) -> PoolKind {
PoolKind::Sqlite(p) => PoolKind::Sqlite(p.clone()),
PoolKind::Rqlite(client) => PoolKind::Rqlite(client.clone()),
PoolKind::Turso(client) => PoolKind::Turso(client.clone()),
+ PoolKind::CloudflareD1(client) => PoolKind::CloudflareD1(client.clone()),
#[cfg(feature = "duckdb-bundled")]
PoolKind::DuckDb(con) => PoolKind::DuckDb(con.clone()),
#[cfg(feature = "duckdb-bundled")]
@@ -2821,6 +2852,7 @@ pub async fn close_pool_kind(pool: PoolKind) {
PoolKind::Sqlite(_) => {}
PoolKind::Rqlite(_) => {}
PoolKind::Turso(_) => {}
+ PoolKind::CloudflareD1(_) => {}
PoolKind::Redis(conn) => {
drop(conn);
}
@@ -3939,6 +3971,20 @@ mod tests {
assert_eq!(scoped.database.as_deref(), Some("admin"));
}
+ #[test]
+ fn cloudflare_d1_query_namespace_does_not_replace_database_id() {
+ let mut config = mysql_config(Some("database-uuid"));
+ config.db_type = DatabaseType::CloudflareD1;
+
+ let scoped = database_connection_config(&config, Some("main"));
+
+ assert_eq!(scoped.database.as_deref(), Some("database-uuid"));
+ assert_eq!(
+ super::base_pool_key_for(Some(DatabaseType::CloudflareD1), "d1-conn", Some("main"), false),
+ "d1-conn"
+ );
+ }
+
#[test]
fn oracle_database_connection_ignores_requested_database() {
let mut config = mysql_config(Some("ORCL"));
@@ -4001,7 +4047,6 @@ mod tests {
super::session_scoped_pool_key_for(Some(&duckdb), "duckdb-conn".to_string(), Some("tab-1")),
"duckdb-conn"
);
-
let mut sqlite_memory = mysql_config(None);
sqlite_memory.db_type = DatabaseType::Sqlite;
sqlite_memory.host = " :MeMoRy: ".to_string();
@@ -4015,6 +4060,13 @@ mod tests {
super::session_scoped_pool_key_for(Some(&sqlite_memory), "sqlite-file".to_string(), Some("tab-1")),
"sqlite-file:session:tab-1"
);
+
+ let mut cloudflare_d1 = mysql_config(None);
+ cloudflare_d1.db_type = DatabaseType::CloudflareD1;
+ assert_eq!(
+ super::session_scoped_pool_key_for(Some(&cloudflare_d1), "d1-conn".to_string(), Some("tab-1")),
+ "d1-conn"
+ );
}
#[test]
diff --git a/crates/dbx-core/src/data_grid_sql.rs b/crates/dbx-core/src/data_grid_sql.rs
index 143939f2d9..9b8ee77e79 100644
--- a/crates/dbx-core/src/data_grid_sql.rs
+++ b/crates/dbx-core/src/data_grid_sql.rs
@@ -2088,6 +2088,7 @@ fn uses_keyless_row_predicate(database_type: Option) -> bool {
| DatabaseType::Sqlite
| DatabaseType::Rqlite
| DatabaseType::Turso
+ | DatabaseType::CloudflareD1
| DatabaseType::DuckDb
| DatabaseType::SqlServer
| DatabaseType::Oracle
diff --git a/crates/dbx-core/src/database_capabilities.rs b/crates/dbx-core/src/database_capabilities.rs
index a8cb923a6e..48ac74fe13 100644
--- a/crates/dbx-core/src/database_capabilities.rs
+++ b/crates/dbx-core/src/database_capabilities.rs
@@ -16,6 +16,7 @@ pub fn is_single_connection_pool(db_type: &DatabaseType) -> bool {
| DatabaseType::DuckDb
| DatabaseType::Rqlite
| DatabaseType::Turso
+ | DatabaseType::CloudflareD1
| DatabaseType::MongoDb
| DatabaseType::Oracle
| DatabaseType::Dameng
@@ -43,6 +44,7 @@ pub fn skips_tcp_probe(db_type: &DatabaseType) -> bool {
DatabaseType::Sqlite
| DatabaseType::DuckDb
| DatabaseType::Turso
+ | DatabaseType::CloudflareD1
| DatabaseType::Jdbc
| DatabaseType::MessageQueue
) || is_agent_type(db_type)
@@ -75,6 +77,13 @@ mod tests {
assert!(!is_local_file_db_type(&DatabaseType::Redis));
assert!(!is_local_file_db_type(&DatabaseType::MongoDb));
assert!(!is_local_file_db_type(&DatabaseType::Turso));
+ assert!(!is_local_file_db_type(&DatabaseType::CloudflareD1));
assert!(!is_local_file_db_type(&DatabaseType::Rqlite));
}
+
+ #[test]
+ fn cloudflare_d1_uses_a_single_http_pool_without_tcp_probe() {
+ assert!(is_single_connection_pool(&DatabaseType::CloudflareD1));
+ assert!(skips_tcp_probe(&DatabaseType::CloudflareD1));
+ }
}
diff --git a/crates/dbx-core/src/db/cloudflare_d1/import.rs b/crates/dbx-core/src/db/cloudflare_d1/import.rs
new file mode 100644
index 0000000000..69fd5436e7
--- /dev/null
+++ b/crates/dbx-core/src/db/cloudflare_d1/import.rs
@@ -0,0 +1,94 @@
+use crate::models::connection::DatabaseType;
+use crate::table_import::{mapping_indexes_for_columns, ImportSqlBatch, TableImportColumnMapping};
+use crate::transfer::generate_insert_typed;
+
+use super::sql_limits::build_sql_batches;
+
+pub(crate) fn build_import_insert_batches(
+ rows: &[Vec],
+ source_columns: &[String],
+ mappings: &[TableImportColumnMapping],
+ target_column_types: &[(String, String)],
+ table: &str,
+ schema: &str,
+ max_rows: usize,
+) -> Result, String> {
+ let mapped = mapping_indexes_for_columns(source_columns, mappings)?;
+ let columns = mapped.iter().map(|(_, target)| target.clone()).collect::>();
+ let column_types = columns
+ .iter()
+ .map(|column| {
+ target_column_types
+ .iter()
+ .find(|(name, _)| name.eq_ignore_ascii_case(column))
+ .map(|(_, data_type)| data_type.clone())
+ })
+ .collect::>();
+ let rows = rows
+ .iter()
+ .map(|row| {
+ mapped
+ .iter()
+ .map(|(source_index, _)| row.get(*source_index).cloned().unwrap_or(serde_json::Value::Null))
+ .collect::>()
+ })
+ .collect::>();
+
+ build_sql_batches(rows.len(), max_rows, "import row", |range| {
+ generate_insert_typed(&columns, &column_types, &rows[range], table, schema, &DatabaseType::CloudflareD1)
+ })
+ .map(|batches| {
+ batches.into_iter().map(|batch| ImportSqlBatch { sql: batch.sql, row_count: batch.item_count }).collect()
+ })
+}
+
+pub(crate) fn build_streaming_import_insert_batch(
+ rows: &[Vec],
+ source_columns: &[String],
+ mappings: &[TableImportColumnMapping],
+ target_column_types: &[(String, String)],
+ table: &str,
+ schema: &str,
+ max_rows: usize,
+) -> Result