Skip to content

Commit 625e26f

Browse files
committed
Rename project to django-cf and update README.md
1 parent 9d6a2a0 commit 625e26f

File tree

3 files changed

+102
-46
lines changed

3 files changed

+102
-46
lines changed

Diff for: README.md

+33-8
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
# django-d1
2-
Cloudflare D1 backend engine for Django
3-
4-
This package is still in tests, and issues will probably appear!
5-
Please report any issues you find in the [Issues tab](https://github.com/G4brym/django-d1/issues).
1+
# django-cf
2+
Django database engine for Cloudflare D1 and Durable Objects
63

74
## Installation
85

96
```bash
10-
pip install django-d1db
7+
pip install django-cf
118
```
129

13-
## Usage
10+
## Using with D1
11+
12+
The D1 engine uses the HTTP api directly from Cloudflare, meaning you only need to create a new D1 database, then
13+
create an API token with `D1 read` permission, and you are good to go!
14+
15+
But using an HTTP endpoint for executing queries one by one is very slow, and currently there is no way to accelerate
16+
it, until a websocket endpoint is available.
17+
18+
The HTTP api doesn't support transactions, meaning all execute queries are final and rollbacks are not available.
1419

1520
```python
1621
DATABASES = {
@@ -27,9 +32,29 @@ The Cloudflare token requires D1 Edit permissions.
2732

2833
A simple tutorial is [available here](https://massadas.com/posts/django-meets-cloudflare-d1/) for you to read.
2934

35+
## Using with Durable Objects
36+
37+
The DO engine, requires you to deploy [workers-dbms](https://github.com/G4brym/workers-dbms) on your cloudflare account.
38+
This engine uses a websocket endpoint to keep the connection alive, meaning sql queries are executed way faster.
39+
40+
workers-dbms have an experimental transaction support, everything should be working out of the box, but you should keep
41+
an eye out for weird issues and report them back.
42+
43+
44+
```python
45+
DATABASES = {
46+
'default': {
47+
'ENGINE': 'django_dbms',
48+
'WORKERS_DBMS_ENDPOINT': '<websocket_endpoint>', # This should start with wss://
49+
'WORKERS_DBMS_ACCESS_ID': '<access_id>', # Optional, but highly recommended!
50+
'WORKERS_DBMS_ACCESS_SECRET': '<access_secret>', # Optional, but highly recommended!
51+
}
52+
}
53+
```
54+
3055

3156
## Limitations
3257

33-
Due to the remote nature of D1, queries can take some time to execute.
58+
When using D1 engine, queries are expected to be slow, and transactions are disabled.
3459

3560
Read all Django limitations for SQLite [databases here](https://docs.djangoproject.com/en/5.0/ref/databases/#sqlite-notes).

Diff for: django_dbms/base.py

+63-33
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import datetime
2+
import json
23

4+
import sqlparse
5+
import websocket # Using websocket-client library for synchronous operations
36
from django.db import IntegrityError, DatabaseError
47
from django.db.backends.sqlite3.base import DatabaseWrapper as SQLiteDatabaseWrapper
5-
from django.db.backends.sqlite3.features import DatabaseFeatures as SQLiteDatabaseFeatures
6-
from django.db.backends.sqlite3.operations import DatabaseOperations as SQLiteDatabaseOperations
7-
from django.db.backends.sqlite3.schema import DatabaseSchemaEditor as SQLiteDatabaseSchemaEditor
88
from django.db.backends.sqlite3.client import DatabaseClient as SQLiteDatabaseClient
99
from django.db.backends.sqlite3.creation import DatabaseCreation as SQLiteDatabaseCreation
10+
from django.db.backends.sqlite3.features import DatabaseFeatures as SQLiteDatabaseFeatures
1011
from django.db.backends.sqlite3.introspection import DatabaseIntrospection as SQLiteDatabaseIntrospection
11-
12-
import websocket # Using websocket-client library for synchronous operations
13-
import json
14-
12+
from django.db.backends.sqlite3.operations import DatabaseOperations as SQLiteDatabaseOperations
13+
from django.db.backends.sqlite3.schema import DatabaseSchemaEditor as SQLiteDatabaseSchemaEditor
1514
from django.utils import timezone
15+
from sqlparse.sql import IdentifierList, Identifier
16+
from sqlparse.tokens import DML
1617

1718

1819
class DatabaseFeatures(SQLiteDatabaseFeatures):
@@ -27,33 +28,63 @@ def _quote_columns(self, sql):
2728
"""
2829
Ensure column names are properly quoted and aliased to avoid collisions.
2930
"""
30-
# Split the SQL to find the SELECT and FROM clauses
31-
select_start = sql.lower().find('select')
32-
from_start = sql.lower().find('from')
33-
34-
if select_start == -1 or from_start == -1:
35-
return sql # Not a SELECT query, skip processing
36-
37-
# Extract the columns between SELECT and FROM
38-
columns_section = sql[select_start + len('select'):from_start].strip()
39-
columns = columns_section.split(',')
40-
41-
# Quote and alias columns
42-
aliased_columns = []
43-
for column in columns:
44-
column = column.strip()
45-
46-
if '.' in column: # It's a "table.column" format
47-
table, col = column.split('.')
48-
aliased_columns.append((f'{self.quote_name(table)}.{self.quote_name(col)} AS {table}_{col}').replace('"', ''))
31+
parsed = sqlparse.parse(sql)
32+
if not parsed:
33+
return sql # Unable to parse, return original SQL
34+
35+
stmt = parsed[0]
36+
new_tokens = []
37+
select_seen = False
38+
39+
for token in stmt.tokens:
40+
if token.ttype is DML and token.value.upper() == 'SELECT':
41+
select_seen = True
42+
new_tokens.append(token)
43+
elif select_seen:
44+
if isinstance(token, IdentifierList):
45+
# Process each identifier in the SELECT clause
46+
new_identifiers = []
47+
for identifier in token.get_identifiers():
48+
new_identifiers.append(self._process_identifier(identifier))
49+
# Rebuild the IdentifierList
50+
new_token = IdentifierList(new_identifiers)
51+
new_tokens.append(new_token)
52+
elif isinstance(token, Identifier):
53+
# Single column without commas
54+
new_token = self._process_identifier(token)
55+
new_tokens.append(new_token)
56+
else:
57+
new_tokens.append(token)
58+
select_seen = False # Assuming SELECT clause is only once
4959
else:
50-
aliased_columns.append(column)
60+
new_tokens.append(token)
5161

52-
# Rebuild the SQL with quoted and aliased columns
53-
new_columns_section = ', '.join(aliased_columns)
54-
new_sql = f'SELECT {new_columns_section} {sql[from_start:]}'
62+
# Reconstruct the SQL statement
63+
new_sql = ''.join(str(token) for token in new_tokens)
5564
return new_sql
5665

66+
def _process_identifier(self, identifier):
67+
# Get the real name and alias if present
68+
real_name = identifier.get_real_name()
69+
alias = identifier.get_alias()
70+
parent_name = identifier.get_parent_name()
71+
72+
if real_name:
73+
if parent_name:
74+
# Format: table.column
75+
table = self.quote_name(parent_name)
76+
column = self.quote_name(real_name)
77+
new_alias = f"{parent_name}_{real_name}"
78+
return f"{table}.{column} AS {new_alias}"
79+
else:
80+
# Simple column
81+
column = self.quote_name(real_name)
82+
new_alias = real_name
83+
return f"{column} AS {new_alias}"
84+
else:
85+
# Complex expression (e.g., functions), return as is
86+
return str(identifier)
87+
5788
def _format_params(self, sql, params):
5889
def quote_param(param):
5990
if isinstance(param, str):
@@ -95,7 +126,6 @@ def _parse_datetime(self, value):
95126

96127
return value # If it's not a datetime string, return the original value
97128

98-
99129
def _convert_results(self, results):
100130
"""
101131
Convert any datetime strings in the result set to actual timezone-aware datetime objects.
@@ -244,7 +274,6 @@ def bulk_insert_sql(self, fields, placeholder_rows):
244274
return "VALUES " + values_sql
245275

246276

247-
248277
class DatabaseWrapper(SQLiteDatabaseWrapper):
249278
vendor = 'websocket'
250279

@@ -281,7 +310,8 @@ def get_new_connection(self, conn_params):
281310
header=headers
282311
)
283312
except Exception as e:
284-
raise ValueError("Unable to connect to websocket, please check your credentials and make sure you have websockets enabled in your domain")
313+
raise ValueError(
314+
"Unable to connect to websocket, please check your credentials and make sure you have websockets enabled in your domain")
285315

286316
return self._websocket
287317

Diff for: pyproject.toml

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
[project]
2-
name = "django-d1db"
3-
version = "0.0.10"
2+
name = "django-cf"
3+
version = "0.0.11"
44
authors = [
55
{ name="Gabriel Massadas" },
66
]
77
dependencies = [
88
'websocket-client',
9+
'sqlparse',
910
]
10-
description = "Django backend for Cloudflare D1"
11+
description = "Django backend for Cloudflare D1 and Durable Objects"
1112
readme = "README.md"
1213
requires-python = ">=3.10"
1314
classifiers = [
@@ -24,8 +25,8 @@ classifiers = [
2425
]
2526

2627
[project.urls]
27-
"Bug Reports" = "https://github.com/G4brym/django-d1/issues"
28-
"Source" = "https://github.com/G4brym/django-d1"
28+
"Bug Reports" = "https://github.com/G4brym/django-cf/issues"
29+
"Source" = "https://github.com/G4brym/django-cf"
2930

3031
[tool.setuptools]
3132
packages = ["django_d1", "django_dbms"]

0 commit comments

Comments
 (0)