sqlalchemy-cloudflare-d1 0.3.8__tar.gz → 0.3.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/CHANGELOG.md +22 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/PKG-INFO +1 -1
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/src/entry.py +629 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/uv.lock +1 -1
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/pyproject.toml +1 -1
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/__init__.py +3 -1
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/compiler.py +54 -34
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/connection.py +44 -30
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/dialect.py +90 -11
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/dialect_async.py +31 -39
- sqlalchemy_cloudflare_d1-0.3.10/tests/integration/test_base_url_integration.py +175 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/integration/test_restapi_integration.py +301 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/integration/test_worker_integration.py +188 -1
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/unit/test_dialect.py +60 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/uv.lock +1 -1
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.github/DEVELOPMENT.md +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.github/workflows/ci.yml +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.github/workflows/release.yml +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.gitignore +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.pre-commit-config.yaml +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.python-version +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/LICENSE +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/Makefile +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/README.md +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/assets/d1-logo.png +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/assets/sqlalchemy-logo.png +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/restapi/example.py +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/README.md +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/db_init.sql +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/pyproject.toml +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/wrangler.jsonc +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/py.typed +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/__init__.py +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/conftest.py +0 -0
- {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/test_utils.py +0 -0
|
@@ -13,6 +13,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
13
13
|
### Fixed
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
## [0.3.10]
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `UUID` and `Enum` column type support ([#24](https://github.com/CollierKing/sqlalchemy-cloudflare-d1/issues/24))
|
|
21
|
+
- Added `D1UUID` type processor that converts `uuid.UUID` objects to hyphenated strings on bind and parses them back on result; supports UUID as primary key and nullable columns
|
|
22
|
+
- Added `D1Enum` type processor that converts Python `enum.Enum` objects to their string values on bind and reconstructs them on result; supports both string-value enums and Python enum classes
|
|
23
|
+
- Both types store as `TEXT` in D1 (SQLite-compatible)
|
|
24
|
+
- Works in both REST API and Worker modes
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## [0.3.9]
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Configurable base API URL for local D1 proxy support ([#22](https://github.com/CollierKing/sqlalchemy-cloudflare-d1/issues/22))
|
|
32
|
+
- `Connection` and `AsyncConnection` now accept a `base_url` kwarg to override the Cloudflare endpoint
|
|
33
|
+
- Falls back to the `CF_D1_BASE_URL` environment variable, then the default Cloudflare URL
|
|
34
|
+
- Works with `create_engine(..., connect_args={"base_url": "http://localhost:8787"})` for the cleanest integration
|
|
35
|
+
- Enables local development against a `wrangler dev` D1 proxy without modifying source code
|
|
36
|
+
|
|
37
|
+
|
|
16
38
|
## [0.3.8]
|
|
17
39
|
|
|
18
40
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-cloudflare-d1
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
4
4
|
Summary: A SQLAlchemy dialect for Cloudflare's D1 Serverless SQLite Database
|
|
5
5
|
Project-URL: Homepage, https://github.com/collierking/sqlalchemy-cloudflare-d1
|
|
6
6
|
Project-URL: Repository, https://github.com/collierking/sqlalchemy-cloudflare-d1
|
{sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/src/entry.py
RENAMED
|
@@ -145,6 +145,29 @@ class Default(WorkerEntrypoint):
|
|
|
145
145
|
return await self.test_time_nullable()
|
|
146
146
|
elif path == "time-orm":
|
|
147
147
|
return await self.test_time_orm()
|
|
148
|
+
# UUID tests (GitHub issue #24)
|
|
149
|
+
elif path == "uuid-basic":
|
|
150
|
+
return await self.test_uuid_basic()
|
|
151
|
+
elif path == "uuid-nullable":
|
|
152
|
+
return await self.test_uuid_nullable()
|
|
153
|
+
elif path == "uuid-orm":
|
|
154
|
+
return await self.test_uuid_orm()
|
|
155
|
+
# Enum tests (GitHub issue #24)
|
|
156
|
+
elif path == "enum-basic":
|
|
157
|
+
return await self.test_enum_basic()
|
|
158
|
+
elif path == "enum-python-class":
|
|
159
|
+
return await self.test_enum_python_class()
|
|
160
|
+
elif path == "enum-nullable":
|
|
161
|
+
return await self.test_enum_nullable()
|
|
162
|
+
elif path == "enum-orm":
|
|
163
|
+
return await self.test_enum_orm()
|
|
164
|
+
elif path == "uuid-pk":
|
|
165
|
+
return await self.test_uuid_pk()
|
|
166
|
+
# Parallel query tests (GitHub issue #20)
|
|
167
|
+
elif path == "parallel-queries-engine":
|
|
168
|
+
return await self.test_parallel_queries_engine()
|
|
169
|
+
elif path == "parallel-queries-async":
|
|
170
|
+
return await self.test_parallel_queries_async()
|
|
148
171
|
else:
|
|
149
172
|
return await self.index()
|
|
150
173
|
|
|
@@ -206,6 +229,8 @@ class Default(WorkerEntrypoint):
|
|
|
206
229
|
"/time-basic": "Test Time column insert/retrieve",
|
|
207
230
|
"/time-nullable": "Test nullable Time columns",
|
|
208
231
|
"/time-orm": "Test Time via ORM session",
|
|
232
|
+
"/parallel-queries-engine": "Timing: create_engine_from_binding() blocks per query (sequential)",
|
|
233
|
+
"/parallel-queries-async": "Timing: WorkerConnection async is truly concurrent",
|
|
209
234
|
},
|
|
210
235
|
"package": "sqlalchemy-cloudflare-d1",
|
|
211
236
|
"connection_type": "WorkerConnection (D1 binding)",
|
|
@@ -3634,3 +3659,607 @@ class Default(WorkerEntrypoint):
|
|
|
3634
3659
|
},
|
|
3635
3660
|
status=500,
|
|
3636
3661
|
)
|
|
3662
|
+
|
|
3663
|
+
# MARK: - UUID Tests (GitHub issue #24)
|
|
3664
|
+
|
|
3665
|
+
async def test_uuid_basic(self):
|
|
3666
|
+
"""Test UUID column insert and retrieve."""
|
|
3667
|
+
import uuid as uuid_module
|
|
3668
|
+
from sqlalchemy import Column, Integer, MetaData, String, Table, Uuid, select
|
|
3669
|
+
|
|
3670
|
+
table_name = f"test_uuid_{uuid_module.uuid4().hex[:8]}"
|
|
3671
|
+
|
|
3672
|
+
try:
|
|
3673
|
+
engine = self.get_engine()
|
|
3674
|
+
metadata = MetaData()
|
|
3675
|
+
|
|
3676
|
+
test_table = Table(
|
|
3677
|
+
table_name,
|
|
3678
|
+
metadata,
|
|
3679
|
+
Column("id", Integer, primary_key=True),
|
|
3680
|
+
Column("title", String(127)),
|
|
3681
|
+
Column("item_uuid", Uuid(as_uuid=True)),
|
|
3682
|
+
)
|
|
3683
|
+
|
|
3684
|
+
metadata.create_all(engine)
|
|
3685
|
+
|
|
3686
|
+
test_uuid = uuid_module.uuid4()
|
|
3687
|
+
|
|
3688
|
+
with engine.connect() as conn:
|
|
3689
|
+
conn.execute(
|
|
3690
|
+
test_table.insert().values(title="Test", item_uuid=test_uuid)
|
|
3691
|
+
)
|
|
3692
|
+
conn.commit()
|
|
3693
|
+
|
|
3694
|
+
result = conn.execute(
|
|
3695
|
+
select(test_table.c.title, test_table.c.item_uuid)
|
|
3696
|
+
)
|
|
3697
|
+
row = result.fetchone()
|
|
3698
|
+
|
|
3699
|
+
metadata.drop_all(engine)
|
|
3700
|
+
|
|
3701
|
+
success = (
|
|
3702
|
+
row is not None
|
|
3703
|
+
and row[0] == "Test"
|
|
3704
|
+
and isinstance(row[1], uuid_module.UUID)
|
|
3705
|
+
and row[1] == test_uuid
|
|
3706
|
+
)
|
|
3707
|
+
|
|
3708
|
+
return Response.json(
|
|
3709
|
+
{
|
|
3710
|
+
"test": "uuid_basic",
|
|
3711
|
+
"success": success,
|
|
3712
|
+
"uuid_type": type(row[1]).__name__ if row else None,
|
|
3713
|
+
"uuid_match": row[1] == test_uuid if row else False,
|
|
3714
|
+
}
|
|
3715
|
+
)
|
|
3716
|
+
except Exception as e:
|
|
3717
|
+
try:
|
|
3718
|
+
metadata.drop_all(engine)
|
|
3719
|
+
except Exception:
|
|
3720
|
+
pass
|
|
3721
|
+
return Response.json(
|
|
3722
|
+
{"test": "uuid_basic", "success": False, "error": str(e)},
|
|
3723
|
+
status=500,
|
|
3724
|
+
)
|
|
3725
|
+
|
|
3726
|
+
async def test_uuid_nullable(self):
|
|
3727
|
+
"""Test nullable UUID columns handle NULL correctly."""
|
|
3728
|
+
import uuid as uuid_module
|
|
3729
|
+
from sqlalchemy import Column, Integer, MetaData, String, Table, Uuid, select
|
|
3730
|
+
|
|
3731
|
+
table_name = f"test_uuid_null_{uuid_module.uuid4().hex[:8]}"
|
|
3732
|
+
|
|
3733
|
+
try:
|
|
3734
|
+
engine = self.get_engine()
|
|
3735
|
+
metadata = MetaData()
|
|
3736
|
+
|
|
3737
|
+
test_table = Table(
|
|
3738
|
+
table_name,
|
|
3739
|
+
metadata,
|
|
3740
|
+
Column("id", Integer, primary_key=True),
|
|
3741
|
+
Column("title", String(127)),
|
|
3742
|
+
Column("item_uuid", Uuid(as_uuid=True), nullable=True),
|
|
3743
|
+
)
|
|
3744
|
+
|
|
3745
|
+
metadata.create_all(engine)
|
|
3746
|
+
|
|
3747
|
+
with engine.connect() as conn:
|
|
3748
|
+
conn.execute(
|
|
3749
|
+
test_table.insert().values(
|
|
3750
|
+
title="With UUID", item_uuid=uuid_module.uuid4()
|
|
3751
|
+
)
|
|
3752
|
+
)
|
|
3753
|
+
conn.execute(
|
|
3754
|
+
test_table.insert().values(title="No UUID", item_uuid=None)
|
|
3755
|
+
)
|
|
3756
|
+
conn.commit()
|
|
3757
|
+
|
|
3758
|
+
result = conn.execute(
|
|
3759
|
+
select(test_table.c.title, test_table.c.item_uuid).order_by(
|
|
3760
|
+
test_table.c.id
|
|
3761
|
+
)
|
|
3762
|
+
)
|
|
3763
|
+
rows = result.fetchall()
|
|
3764
|
+
|
|
3765
|
+
metadata.drop_all(engine)
|
|
3766
|
+
|
|
3767
|
+
success = (
|
|
3768
|
+
len(rows) == 2
|
|
3769
|
+
and isinstance(rows[0][1], uuid_module.UUID)
|
|
3770
|
+
and rows[1][1] is None
|
|
3771
|
+
)
|
|
3772
|
+
|
|
3773
|
+
return Response.json(
|
|
3774
|
+
{
|
|
3775
|
+
"test": "uuid_nullable",
|
|
3776
|
+
"success": success,
|
|
3777
|
+
"with_uuid_is_uuid": isinstance(rows[0][1], uuid_module.UUID)
|
|
3778
|
+
if rows
|
|
3779
|
+
else False,
|
|
3780
|
+
"no_uuid_is_none": rows[1][1] is None if len(rows) > 1 else False,
|
|
3781
|
+
}
|
|
3782
|
+
)
|
|
3783
|
+
except Exception as e:
|
|
3784
|
+
try:
|
|
3785
|
+
metadata.drop_all(engine)
|
|
3786
|
+
except Exception:
|
|
3787
|
+
pass
|
|
3788
|
+
return Response.json(
|
|
3789
|
+
{"test": "uuid_nullable", "success": False, "error": str(e)},
|
|
3790
|
+
status=500,
|
|
3791
|
+
)
|
|
3792
|
+
|
|
3793
|
+
async def test_uuid_orm(self):
|
|
3794
|
+
"""Test UUID via ORM session."""
|
|
3795
|
+
import uuid as uuid_module
|
|
3796
|
+
from sqlalchemy import Integer, String, Uuid
|
|
3797
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
|
|
3798
|
+
|
|
3799
|
+
engine = create_engine_from_binding(self.env.DB)
|
|
3800
|
+
table_name = f"test_uuid_orm_{uuid_module.uuid4().hex[:8]}"
|
|
3801
|
+
|
|
3802
|
+
try:
|
|
3803
|
+
|
|
3804
|
+
class Base(DeclarativeBase):
|
|
3805
|
+
pass
|
|
3806
|
+
|
|
3807
|
+
class Item(Base):
|
|
3808
|
+
__tablename__ = table_name
|
|
3809
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
3810
|
+
title: Mapped[str] = mapped_column(String(127))
|
|
3811
|
+
item_uuid: Mapped[uuid_module.UUID] = mapped_column(Uuid(as_uuid=True))
|
|
3812
|
+
|
|
3813
|
+
Base.metadata.create_all(engine)
|
|
3814
|
+
|
|
3815
|
+
test_uuid = uuid_module.uuid4()
|
|
3816
|
+
|
|
3817
|
+
with Session(engine) as session:
|
|
3818
|
+
item = Item(title="UUID ORM Test", item_uuid=test_uuid)
|
|
3819
|
+
session.add(item)
|
|
3820
|
+
session.commit()
|
|
3821
|
+
session.refresh(item)
|
|
3822
|
+
entry_title = item.title
|
|
3823
|
+
uuid_is_uuid = isinstance(item.item_uuid, uuid_module.UUID)
|
|
3824
|
+
uuid_match = item.item_uuid == test_uuid
|
|
3825
|
+
|
|
3826
|
+
Base.metadata.drop_all(engine)
|
|
3827
|
+
|
|
3828
|
+
return Response.json(
|
|
3829
|
+
{
|
|
3830
|
+
"test": "uuid_orm",
|
|
3831
|
+
"success": uuid_is_uuid and uuid_match,
|
|
3832
|
+
"entry_title": entry_title,
|
|
3833
|
+
"uuid_is_uuid": uuid_is_uuid,
|
|
3834
|
+
"uuid_match": uuid_match,
|
|
3835
|
+
}
|
|
3836
|
+
)
|
|
3837
|
+
except Exception as e:
|
|
3838
|
+
try:
|
|
3839
|
+
from sqlalchemy import MetaData, Table
|
|
3840
|
+
|
|
3841
|
+
md = MetaData()
|
|
3842
|
+
Table(table_name, md)
|
|
3843
|
+
md.drop_all(engine)
|
|
3844
|
+
except Exception:
|
|
3845
|
+
pass
|
|
3846
|
+
return Response.json(
|
|
3847
|
+
{"test": "uuid_orm", "success": False, "error": str(e)},
|
|
3848
|
+
status=500,
|
|
3849
|
+
)
|
|
3850
|
+
|
|
3851
|
+
async def test_uuid_pk(self):
|
|
3852
|
+
"""Test UUID used as primary key."""
|
|
3853
|
+
import uuid as uuid_module
|
|
3854
|
+
from sqlalchemy import Column, MetaData, String, Table, Uuid, select
|
|
3855
|
+
|
|
3856
|
+
table_name = f"test_uuid_pk_{uuid_module.uuid4().hex[:8]}"
|
|
3857
|
+
|
|
3858
|
+
try:
|
|
3859
|
+
engine = self.get_engine()
|
|
3860
|
+
metadata = MetaData()
|
|
3861
|
+
|
|
3862
|
+
test_table = Table(
|
|
3863
|
+
table_name,
|
|
3864
|
+
metadata,
|
|
3865
|
+
Column("id", Uuid(as_uuid=True), primary_key=True),
|
|
3866
|
+
Column("title", String(127)),
|
|
3867
|
+
)
|
|
3868
|
+
|
|
3869
|
+
metadata.create_all(engine)
|
|
3870
|
+
|
|
3871
|
+
pk_uuid = uuid_module.uuid4()
|
|
3872
|
+
|
|
3873
|
+
with engine.connect() as conn:
|
|
3874
|
+
conn.execute(test_table.insert().values(id=pk_uuid, title="PK Test"))
|
|
3875
|
+
conn.commit()
|
|
3876
|
+
|
|
3877
|
+
result = conn.execute(
|
|
3878
|
+
select(test_table).where(test_table.c.id == pk_uuid)
|
|
3879
|
+
)
|
|
3880
|
+
row = result.fetchone()
|
|
3881
|
+
|
|
3882
|
+
metadata.drop_all(engine)
|
|
3883
|
+
|
|
3884
|
+
success = (
|
|
3885
|
+
row is not None
|
|
3886
|
+
and isinstance(row[0], uuid_module.UUID)
|
|
3887
|
+
and row[0] == pk_uuid
|
|
3888
|
+
and row[1] == "PK Test"
|
|
3889
|
+
)
|
|
3890
|
+
|
|
3891
|
+
return Response.json(
|
|
3892
|
+
{
|
|
3893
|
+
"test": "uuid_pk",
|
|
3894
|
+
"success": success,
|
|
3895
|
+
"pk_is_uuid": isinstance(row[0], uuid_module.UUID)
|
|
3896
|
+
if row
|
|
3897
|
+
else False,
|
|
3898
|
+
"pk_match": row[0] == pk_uuid if row else False,
|
|
3899
|
+
}
|
|
3900
|
+
)
|
|
3901
|
+
except Exception as e:
|
|
3902
|
+
try:
|
|
3903
|
+
metadata.drop_all(engine)
|
|
3904
|
+
except Exception:
|
|
3905
|
+
pass
|
|
3906
|
+
return Response.json(
|
|
3907
|
+
{"test": "uuid_pk", "success": False, "error": str(e)},
|
|
3908
|
+
status=500,
|
|
3909
|
+
)
|
|
3910
|
+
|
|
3911
|
+
# MARK: - Enum Tests (GitHub issue #24)
|
|
3912
|
+
|
|
3913
|
+
async def test_enum_basic(self):
|
|
3914
|
+
"""Test Enum column with string values insert and retrieve."""
|
|
3915
|
+
import uuid as uuid_module
|
|
3916
|
+
from sqlalchemy import Column, Enum, Integer, MetaData, String, Table, select
|
|
3917
|
+
|
|
3918
|
+
table_name = f"test_enum_{uuid_module.uuid4().hex[:8]}"
|
|
3919
|
+
|
|
3920
|
+
try:
|
|
3921
|
+
engine = self.get_engine()
|
|
3922
|
+
metadata = MetaData()
|
|
3923
|
+
|
|
3924
|
+
test_table = Table(
|
|
3925
|
+
table_name,
|
|
3926
|
+
metadata,
|
|
3927
|
+
Column("id", Integer, primary_key=True),
|
|
3928
|
+
Column("title", String(127)),
|
|
3929
|
+
Column("status", Enum("active", "inactive", "pending")),
|
|
3930
|
+
)
|
|
3931
|
+
|
|
3932
|
+
metadata.create_all(engine)
|
|
3933
|
+
|
|
3934
|
+
with engine.connect() as conn:
|
|
3935
|
+
conn.execute(test_table.insert().values(title="Test", status="active"))
|
|
3936
|
+
conn.commit()
|
|
3937
|
+
|
|
3938
|
+
result = conn.execute(select(test_table.c.title, test_table.c.status))
|
|
3939
|
+
row = result.fetchone()
|
|
3940
|
+
|
|
3941
|
+
metadata.drop_all(engine)
|
|
3942
|
+
|
|
3943
|
+
success = row is not None and row[0] == "Test" and row[1] == "active"
|
|
3944
|
+
|
|
3945
|
+
return Response.json(
|
|
3946
|
+
{
|
|
3947
|
+
"test": "enum_basic",
|
|
3948
|
+
"success": success,
|
|
3949
|
+
"status_value": row[1] if row else None,
|
|
3950
|
+
"status_type": type(row[1]).__name__ if row else None,
|
|
3951
|
+
}
|
|
3952
|
+
)
|
|
3953
|
+
except Exception as e:
|
|
3954
|
+
try:
|
|
3955
|
+
metadata.drop_all(engine)
|
|
3956
|
+
except Exception:
|
|
3957
|
+
pass
|
|
3958
|
+
return Response.json(
|
|
3959
|
+
{"test": "enum_basic", "success": False, "error": str(e)},
|
|
3960
|
+
status=500,
|
|
3961
|
+
)
|
|
3962
|
+
|
|
3963
|
+
async def test_enum_python_class(self):
|
|
3964
|
+
"""Test Enum column with Python enum.Enum class."""
|
|
3965
|
+
import enum as enum_module
|
|
3966
|
+
import uuid as uuid_module
|
|
3967
|
+
from sqlalchemy import Column, Enum, Integer, MetaData, String, Table, select
|
|
3968
|
+
|
|
3969
|
+
class Status(enum_module.Enum):
|
|
3970
|
+
active = "active"
|
|
3971
|
+
inactive = "inactive"
|
|
3972
|
+
pending = "pending"
|
|
3973
|
+
|
|
3974
|
+
table_name = f"test_enum_cls_{uuid_module.uuid4().hex[:8]}"
|
|
3975
|
+
|
|
3976
|
+
try:
|
|
3977
|
+
engine = self.get_engine()
|
|
3978
|
+
metadata = MetaData()
|
|
3979
|
+
|
|
3980
|
+
test_table = Table(
|
|
3981
|
+
table_name,
|
|
3982
|
+
metadata,
|
|
3983
|
+
Column("id", Integer, primary_key=True),
|
|
3984
|
+
Column("title", String(127)),
|
|
3985
|
+
Column("status", Enum(Status)),
|
|
3986
|
+
)
|
|
3987
|
+
|
|
3988
|
+
metadata.create_all(engine)
|
|
3989
|
+
|
|
3990
|
+
with engine.connect() as conn:
|
|
3991
|
+
conn.execute(
|
|
3992
|
+
test_table.insert().values(title="Test", status=Status.active)
|
|
3993
|
+
)
|
|
3994
|
+
conn.commit()
|
|
3995
|
+
|
|
3996
|
+
result = conn.execute(select(test_table.c.title, test_table.c.status))
|
|
3997
|
+
row = result.fetchone()
|
|
3998
|
+
|
|
3999
|
+
metadata.drop_all(engine)
|
|
4000
|
+
|
|
4001
|
+
success = row is not None and row[0] == "Test" and row[1] == Status.active
|
|
4002
|
+
|
|
4003
|
+
return Response.json(
|
|
4004
|
+
{
|
|
4005
|
+
"test": "enum_python_class",
|
|
4006
|
+
"success": success,
|
|
4007
|
+
"status_is_enum": isinstance(row[1], Status) if row else False,
|
|
4008
|
+
"status_value": row[1].value
|
|
4009
|
+
if row and isinstance(row[1], Status)
|
|
4010
|
+
else str(row[1])
|
|
4011
|
+
if row
|
|
4012
|
+
else None,
|
|
4013
|
+
}
|
|
4014
|
+
)
|
|
4015
|
+
except Exception as e:
|
|
4016
|
+
try:
|
|
4017
|
+
metadata.drop_all(engine)
|
|
4018
|
+
except Exception:
|
|
4019
|
+
pass
|
|
4020
|
+
return Response.json(
|
|
4021
|
+
{"test": "enum_python_class", "success": False, "error": str(e)},
|
|
4022
|
+
status=500,
|
|
4023
|
+
)
|
|
4024
|
+
|
|
4025
|
+
async def test_enum_nullable(self):
|
|
4026
|
+
"""Test nullable Enum columns handle NULL correctly."""
|
|
4027
|
+
import uuid as uuid_module
|
|
4028
|
+
from sqlalchemy import Column, Enum, Integer, MetaData, String, Table, select
|
|
4029
|
+
|
|
4030
|
+
table_name = f"test_enum_null_{uuid_module.uuid4().hex[:8]}"
|
|
4031
|
+
|
|
4032
|
+
try:
|
|
4033
|
+
engine = self.get_engine()
|
|
4034
|
+
metadata = MetaData()
|
|
4035
|
+
|
|
4036
|
+
test_table = Table(
|
|
4037
|
+
table_name,
|
|
4038
|
+
metadata,
|
|
4039
|
+
Column("id", Integer, primary_key=True),
|
|
4040
|
+
Column("title", String(127)),
|
|
4041
|
+
Column("status", Enum("active", "inactive"), nullable=True),
|
|
4042
|
+
)
|
|
4043
|
+
|
|
4044
|
+
metadata.create_all(engine)
|
|
4045
|
+
|
|
4046
|
+
with engine.connect() as conn:
|
|
4047
|
+
conn.execute(
|
|
4048
|
+
test_table.insert().values(title="With Status", status="active")
|
|
4049
|
+
)
|
|
4050
|
+
conn.execute(test_table.insert().values(title="No Status", status=None))
|
|
4051
|
+
conn.commit()
|
|
4052
|
+
|
|
4053
|
+
result = conn.execute(
|
|
4054
|
+
select(test_table.c.title, test_table.c.status).order_by(
|
|
4055
|
+
test_table.c.id
|
|
4056
|
+
)
|
|
4057
|
+
)
|
|
4058
|
+
rows = result.fetchall()
|
|
4059
|
+
|
|
4060
|
+
metadata.drop_all(engine)
|
|
4061
|
+
|
|
4062
|
+
success = len(rows) == 2 and rows[0][1] == "active" and rows[1][1] is None
|
|
4063
|
+
|
|
4064
|
+
return Response.json(
|
|
4065
|
+
{
|
|
4066
|
+
"test": "enum_nullable",
|
|
4067
|
+
"success": success,
|
|
4068
|
+
"with_status_value": rows[0][1] if rows else None,
|
|
4069
|
+
"no_status_is_none": rows[1][1] is None if len(rows) > 1 else False,
|
|
4070
|
+
}
|
|
4071
|
+
)
|
|
4072
|
+
except Exception as e:
|
|
4073
|
+
try:
|
|
4074
|
+
metadata.drop_all(engine)
|
|
4075
|
+
except Exception:
|
|
4076
|
+
pass
|
|
4077
|
+
return Response.json(
|
|
4078
|
+
{"test": "enum_nullable", "success": False, "error": str(e)},
|
|
4079
|
+
status=500,
|
|
4080
|
+
)
|
|
4081
|
+
|
|
4082
|
+
async def test_enum_orm(self):
|
|
4083
|
+
"""Test Enum via ORM session with Python enum class."""
|
|
4084
|
+
import enum as enum_module
|
|
4085
|
+
import uuid as uuid_module
|
|
4086
|
+
from sqlalchemy import Enum, Integer, String
|
|
4087
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
|
|
4088
|
+
|
|
4089
|
+
class Priority(enum_module.Enum):
|
|
4090
|
+
low = "low"
|
|
4091
|
+
medium = "medium"
|
|
4092
|
+
high = "high"
|
|
4093
|
+
|
|
4094
|
+
engine = create_engine_from_binding(self.env.DB)
|
|
4095
|
+
table_name = f"test_enum_orm_{uuid_module.uuid4().hex[:8]}"
|
|
4096
|
+
|
|
4097
|
+
try:
|
|
4098
|
+
|
|
4099
|
+
class Base(DeclarativeBase):
|
|
4100
|
+
pass
|
|
4101
|
+
|
|
4102
|
+
class Task(Base):
|
|
4103
|
+
__tablename__ = table_name
|
|
4104
|
+
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
|
4105
|
+
title: Mapped[str] = mapped_column(String(127))
|
|
4106
|
+
priority: Mapped[Priority] = mapped_column(Enum(Priority))
|
|
4107
|
+
|
|
4108
|
+
Base.metadata.create_all(engine)
|
|
4109
|
+
|
|
4110
|
+
with Session(engine) as session:
|
|
4111
|
+
task = Task(title="Enum ORM Task", priority=Priority.high)
|
|
4112
|
+
session.add(task)
|
|
4113
|
+
session.commit()
|
|
4114
|
+
session.refresh(task)
|
|
4115
|
+
entry_title = task.title
|
|
4116
|
+
priority_is_enum = isinstance(task.priority, Priority)
|
|
4117
|
+
priority_match = task.priority == Priority.high
|
|
4118
|
+
|
|
4119
|
+
Base.metadata.drop_all(engine)
|
|
4120
|
+
|
|
4121
|
+
return Response.json(
|
|
4122
|
+
{
|
|
4123
|
+
"test": "enum_orm",
|
|
4124
|
+
"success": priority_is_enum and priority_match,
|
|
4125
|
+
"entry_title": entry_title,
|
|
4126
|
+
"priority_is_enum": priority_is_enum,
|
|
4127
|
+
"priority_match": priority_match,
|
|
4128
|
+
}
|
|
4129
|
+
)
|
|
4130
|
+
except Exception as e:
|
|
4131
|
+
try:
|
|
4132
|
+
from sqlalchemy import MetaData, Table
|
|
4133
|
+
|
|
4134
|
+
md = MetaData()
|
|
4135
|
+
Table(table_name, md)
|
|
4136
|
+
md.drop_all(engine)
|
|
4137
|
+
except Exception:
|
|
4138
|
+
pass
|
|
4139
|
+
return Response.json(
|
|
4140
|
+
{"test": "enum_orm", "success": False, "error": str(e)},
|
|
4141
|
+
status=500,
|
|
4142
|
+
)
|
|
4143
|
+
|
|
4144
|
+
# MARK: - Parallel Query Tests (GitHub issue #20)
|
|
4145
|
+
|
|
4146
|
+
async def test_parallel_queries_engine(self):
|
|
4147
|
+
"""Timing test showing create_engine_from_binding() queries run concurrently.
|
|
4148
|
+
|
|
4149
|
+
Although SyncWorkerConnection uses pyodide.ffi.run_sync() internally,
|
|
4150
|
+
run_sync() drives the JS event loop rather than truly blocking it.
|
|
4151
|
+
asyncio.gather() can therefore interleave multiple D1 round-trips.
|
|
4152
|
+
|
|
4153
|
+
Uses N unique parameterized queries (SELECT :n) so D1 cannot cache
|
|
4154
|
+
results across runs. Sequential time = N * one_query_latency.
|
|
4155
|
+
If concurrent, parallel time ≈ one_query_latency.
|
|
4156
|
+
"""
|
|
4157
|
+
import asyncio
|
|
4158
|
+
import time
|
|
4159
|
+
from sqlalchemy import text
|
|
4160
|
+
|
|
4161
|
+
N_QUERIES = 10
|
|
4162
|
+
|
|
4163
|
+
try:
|
|
4164
|
+
engine = self.get_engine()
|
|
4165
|
+
|
|
4166
|
+
async def single_query(i):
|
|
4167
|
+
with engine.connect() as conn:
|
|
4168
|
+
result = conn.execute(text("SELECT :n as n"), {"n": i})
|
|
4169
|
+
return result.fetchone()[0]
|
|
4170
|
+
|
|
4171
|
+
seq_start = time.time()
|
|
4172
|
+
for i in range(N_QUERIES):
|
|
4173
|
+
await single_query(i)
|
|
4174
|
+
sequential_time = time.time() - seq_start
|
|
4175
|
+
|
|
4176
|
+
par_start = time.time()
|
|
4177
|
+
results = await asyncio.gather(*[single_query(i) for i in range(N_QUERIES)])
|
|
4178
|
+
parallel_time = time.time() - par_start
|
|
4179
|
+
|
|
4180
|
+
results_correct = results == list(range(N_QUERIES))
|
|
4181
|
+
# Expect genuine speedup — parallel < 80% of sequential
|
|
4182
|
+
is_concurrent = parallel_time < sequential_time * 0.8
|
|
4183
|
+
|
|
4184
|
+
return Response.json(
|
|
4185
|
+
{
|
|
4186
|
+
"test": "parallel_queries_engine",
|
|
4187
|
+
"success": results_correct and is_concurrent,
|
|
4188
|
+
"results_correct": results_correct,
|
|
4189
|
+
"is_concurrent": is_concurrent,
|
|
4190
|
+
"sequential_time_s": round(sequential_time, 3),
|
|
4191
|
+
"parallel_time_s": round(parallel_time, 3),
|
|
4192
|
+
"n_queries": N_QUERIES,
|
|
4193
|
+
}
|
|
4194
|
+
)
|
|
4195
|
+
except Exception as e:
|
|
4196
|
+
return Response.json(
|
|
4197
|
+
{
|
|
4198
|
+
"test": "parallel_queries_engine",
|
|
4199
|
+
"success": False,
|
|
4200
|
+
"error": str(e),
|
|
4201
|
+
"error_type": type(e).__name__,
|
|
4202
|
+
},
|
|
4203
|
+
status=500,
|
|
4204
|
+
)
|
|
4205
|
+
|
|
4206
|
+
async def test_parallel_queries_async(self):
|
|
4207
|
+
"""Timing test showing WorkerConnection async queries are truly concurrent.
|
|
4208
|
+
|
|
4209
|
+
Instead of a CPU-bound CTE (which D1 serializes server-side regardless),
|
|
4210
|
+
we run many lightweight round-trips where network latency is the bottleneck.
|
|
4211
|
+
N_QUERIES simple SELECTs are fired sequentially then concurrently via
|
|
4212
|
+
asyncio.gather(). If WorkerConnection truly yields to the event loop,
|
|
4213
|
+
the parallel run should be faster because multiple D1 round-trips overlap.
|
|
4214
|
+
"""
|
|
4215
|
+
import asyncio
|
|
4216
|
+
import time
|
|
4217
|
+
|
|
4218
|
+
N_QUERIES = 10
|
|
4219
|
+
|
|
4220
|
+
try:
|
|
4221
|
+
|
|
4222
|
+
async def single_query(i):
|
|
4223
|
+
conn = self.get_connection()
|
|
4224
|
+
cursor = conn.cursor()
|
|
4225
|
+
await cursor.execute_async("SELECT ? as n", (i,))
|
|
4226
|
+
row = cursor.fetchone()
|
|
4227
|
+
conn.close()
|
|
4228
|
+
return row[0]
|
|
4229
|
+
|
|
4230
|
+
# Sequential: N_QUERIES round-trips one after another
|
|
4231
|
+
seq_start = time.time()
|
|
4232
|
+
for i in range(N_QUERIES):
|
|
4233
|
+
await single_query(i)
|
|
4234
|
+
sequential_time = time.time() - seq_start
|
|
4235
|
+
|
|
4236
|
+
# Parallel: all N_QUERIES in flight at once
|
|
4237
|
+
par_start = time.time()
|
|
4238
|
+
results = await asyncio.gather(*[single_query(i) for i in range(N_QUERIES)])
|
|
4239
|
+
parallel_time = time.time() - par_start
|
|
4240
|
+
|
|
4241
|
+
results_correct = results == list(range(N_QUERIES))
|
|
4242
|
+
# Expect genuine speedup — parallel < 80% of sequential
|
|
4243
|
+
is_concurrent = parallel_time < sequential_time * 0.8
|
|
4244
|
+
|
|
4245
|
+
return Response.json(
|
|
4246
|
+
{
|
|
4247
|
+
"test": "parallel_queries_async",
|
|
4248
|
+
"success": results_correct and is_concurrent,
|
|
4249
|
+
"results_correct": results_correct,
|
|
4250
|
+
"is_concurrent": is_concurrent,
|
|
4251
|
+
"sequential_time_s": round(sequential_time, 3),
|
|
4252
|
+
"parallel_time_s": round(parallel_time, 3),
|
|
4253
|
+
"n_queries": N_QUERIES,
|
|
4254
|
+
}
|
|
4255
|
+
)
|
|
4256
|
+
except Exception as e:
|
|
4257
|
+
return Response.json(
|
|
4258
|
+
{
|
|
4259
|
+
"test": "parallel_queries_async",
|
|
4260
|
+
"success": False,
|
|
4261
|
+
"error": str(e),
|
|
4262
|
+
"error_type": type(e).__name__,
|
|
4263
|
+
},
|
|
4264
|
+
status=500,
|
|
4265
|
+
)
|