Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Changed

* Reworked the README layout, with badges, a compatibility table and a contributing section. The content is unchanged.

## [0.2.1] - 2026-09-29

Docs only. No code changes.
Expand Down
86 changes: 61 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
<div align="center">

# sqlalchemy-d1

A **SQLAlchemy dialect** for **Cloudflare D1** that keeps the `d1://` connection string working in **Apache Superset**.

Since version 0.2.0 this package is a thin layer over [sqlalchemy-cloudflare-d1](https://github.com/CollierKing/sqlalchemy-cloudflare-d1). That project is the real dialect and the real driver. This package registers it under the `d1` name and adds the few things Superset needs.
[![PyPI](https://img.shields.io/pypi/v/sqlalchemy-d1)](https://pypi.org/project/sqlalchemy-d1/) [![Python](https://img.shields.io/pypi/pyversions/sqlalchemy-d1)](https://pypi.org/project/sqlalchemy-d1/) [![CI](https://github.com/sqlalchemy-cf-d1/sqlalchemy-d1/actions/workflows/ci.yml/badge.svg)](https://github.com/sqlalchemy-cf-d1/sqlalchemy-d1/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](https://github.com/sqlalchemy-cf-d1/sqlalchemy-d1/blob/main/LICENSE) [![Superset docs](https://img.shields.io/badge/Superset%20docs-Cloudflare%20D1-20A7C9)](https://superset.apache.org/user-docs/databases/supported/cloudflare-d1/)

[PyPI](https://pypi.org/project/sqlalchemy-d1/) · [Changelog](https://github.com/sqlalchemy-cf-d1/sqlalchemy-d1/blob/main/CHANGELOG.md) · [Superset D1 docs](https://superset.apache.org/user-docs/databases/supported/cloudflare-d1/) · [Issues](https://github.com/sqlalchemy-cf-d1/sqlalchemy-d1/issues)

---
</div>

<br>

Since version 0.2.0 this package is a thin layer over [sqlalchemy-cloudflare-d1](https://github.com/CollierKing/sqlalchemy-cloudflare-d1). That project is the real dialect and the real driver. This package registers it under the `d1` name and adds the few things Superset needs.

## Why this package exists

Superset ships a built-in **Cloudflare D1** engine spec that expects `d1://` connection strings. See the [Superset D1 docs page](https://superset.apache.org/user-docs/databases/supported/cloudflare-d1/).

Version 0.1.0 of this package only worked with SQLAlchemy 1.4. When Superset moved to SQLAlchemy 2.0, it held its `d1` extra back because of that. Version 0.2.0 works with SQLAlchemy 2.0. Since [apache/superset#44505](https://github.com/apache/superset/pull/44505), merged on 29 September 2026, the `d1` extra installs this package alone. See Superset's [UPDATING.md](https://github.com/apache/superset/blob/master/UPDATING.md).

If you do not use Superset, install `sqlalchemy-cloudflare-d1` directly and use its `cloudflare_d1://` connection string.

---
> **Not using Superset?** Install `sqlalchemy-cloudflare-d1` directly and use its `cloudflare_d1://` connection string.

## Installation

Expand All @@ -24,16 +30,35 @@ pip install sqlalchemy-d1

This also installs `sqlalchemy-cloudflare-d1` and SQLAlchemy 2.0. Python 3.11 or newer is required.

If your Superset still uses **SQLAlchemy 1.4** (Superset 6.1.0, for example), stay on version 0.1.0. Newer versions would upgrade SQLAlchemy and break it.
> **Superset on SQLAlchemy 1.4?** Superset 6.1.0, for example. Stay on version 0.1.0. Newer versions would upgrade SQLAlchemy and break it.
>
> ```bash
> pip install "sqlalchemy-d1==0.1.0"
> ```

```bash
pip install "sqlalchemy-d1==0.1.0"
```
### Compatibility

---
| sqlalchemy-d1 | SQLAlchemy | Python | Driver |
|---------------|------------|--------|--------|
| **0.2.x** | 2.0 | 3.11 or newer | `sqlalchemy-cloudflare-d1` |
| **0.1.0** | 1.4 | 3.11 | `dbapi-d1` |

## Usage

### Connection string

```
d1://<CF_ACCOUNT_ID>:<CF_API_TOKEN>@<D1_DB_ID>
```

| Part | Value |
|------|-------|
| `CF_ACCOUNT_ID` | Your Cloudflare account ID |
| `CF_API_TOKEN` | A Cloudflare [API token](https://developers.cloudflare.com/fundamentals/api/get-started/create-token/) with access to D1 |
| `D1_DB_ID` | The ID of your D1 database |

### SQLAlchemy

```python
from sqlalchemy import create_engine, text

Expand All @@ -43,9 +68,9 @@ with engine.connect() as conn:
print(conn.execute(text("SELECT 1")).scalar())
```

In Superset, use the same URL as the SQLAlchemy URI of the database connection.
### Apache Superset

---
In Superset, open **Settings > Database Connections > + Database** and use the same URL as the SQLAlchemy URI of the database connection. Your tables are listed under the `main` schema.

## What it adds

Expand All @@ -61,17 +86,14 @@ In Superset, use the same URL as the SQLAlchemy URI of the database connection.

Everything else comes from `sqlalchemy-cloudflare-d1` unchanged. That includes the connection, the cursor, the SQL compiler, and the reflection of foreign keys, indexes and unique constraints.

---

## Known limits

Tables **created through SQLAlchemy** are identical to the ones upstream creates. A `DateTime` column is declared as `TEXT` and a `Boolean` column as `INTEGER`, so they are reflected as text and integer afterwards. Values still round trip correctly when you use the same `Table` object.

Tables created with plain SQL and a `DATETIME` column, which is the normal case for D1, are reflected as dates. A table reflected from D1 keeps its declared types when SQLAlchemy creates it again on D1. On another database the same columns compile like SQLAlchemy's generic date and boolean types.

Only the first word of a declared type is matched, so `TIMESTAMP WITH TIME ZONE` is a date and `UPDATED_INT` is an integer.
### In this package

**CHECK constraints** are not reflected. `get_check_constraints` returns an empty list, and `get_table_comment` returns no comment because SQLite has none.
* **Tables created through SQLAlchemy** are identical to the ones upstream creates. A `DateTime` column is declared as `TEXT` and a `Boolean` column as `INTEGER`, so they are reflected as text and integer afterwards. Values still round trip correctly when you use the same `Table` object.
* **Tables created with plain SQL** and a `DATETIME` column, which is the normal case for D1, are reflected as dates. A table reflected from D1 keeps its declared types when SQLAlchemy creates it again on D1. On another database the same columns compile like SQLAlchemy's generic date and boolean types.
* **Only the first word of a declared type is matched**, so `TIMESTAMP WITH TIME ZONE` is a date and `UPDATED_INT` is an integer.
* **CHECK constraints** are not reflected. `get_check_constraints` returns an empty list, and `get_table_comment` returns no comment because SQLite has none.

### From sqlalchemy-cloudflare-d1

Expand All @@ -81,25 +103,29 @@ These come from the upstream driver and dialect, so this package has them too.
* **Statements that do not start with `SELECT`, `PRAGMA` or `WITH`**, and have no `RETURNING`, come back without column names, so SQLAlchemy raises `This result object does not return rows`. That includes a query that starts with a comment, `EXPLAIN QUERY PLAN` and `VALUES`. Put a leading comment at the end of the query instead, with no semicolon after it. Reported as [#32](https://github.com/CollierKing/sqlalchemy-cloudflare-d1/issues/32).
* **Date and time values are written with a `T`**, such as `2026-09-21T09:00:00`. Data from other tools often has a space instead, and Superset writes its time filters with a space. SQLite compares both forms as text and a `T` sorts after a space, so comparing one form with the other can give the wrong rows. Keep dates in one form. SQLite's `datetime()` turns either form into the one with a space.

---

## Development

### Setup

```bash
git clone https://github.com/sqlalchemy-cf-d1/sqlalchemy-d1.git
cd sqlalchemy-d1
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```

With **Poetry**, run `poetry install --all-extras` instead.

Run the checks:
### Checks

```bash
ruff check . && ruff format --check . && mypy src
pytest -m "not integration" --disable-socket
```

### Integration tests

The integration tests need a real D1 database. Use a throwaway one. They create and drop their own tables.

```bash
Expand All @@ -109,10 +135,20 @@ export CF_D1_DATABASE_ID=<D1_DB_ID>
pytest -m integration
```

---
## Contributing

Issues and pull requests are welcome on [GitHub](https://github.com/sqlalchemy-cf-d1/sqlalchemy-d1). CI runs the checks and unit tests above on Python 3.11 to 3.14 for every pull request. It also runs them once a week, so a new upstream release that breaks the dialect is noticed.

Some changes belong in another project:

| Change | Where |
|--------|-------|
| The `d1://` dialect and its reflection | This repository |
| The D1 engine spec in Superset | [apache/superset](https://github.com/apache/superset) |
| The DBAPI driver, the cursor and the SQL compiler | [CollierKing/sqlalchemy-cloudflare-d1](https://github.com/CollierKing/sqlalchemy-cloudflare-d1) |

## License

This package is licensed under the **Apache License 2.0**. See [LICENSE](LICENSE).
This package is licensed under the **Apache License 2.0**. See [LICENSE](https://github.com/sqlalchemy-cf-d1/sqlalchemy-d1/blob/main/LICENSE).

`sqlalchemy-cloudflare-d1` is a separate project under the MIT license. It is a dependency and none of its code is copied here.
Loading