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.
Files changed (35) hide show
  1. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/CHANGELOG.md +22 -0
  2. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/PKG-INFO +1 -1
  3. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/src/entry.py +629 -0
  4. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/uv.lock +1 -1
  5. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/pyproject.toml +1 -1
  6. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/__init__.py +3 -1
  7. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/compiler.py +54 -34
  8. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/connection.py +44 -30
  9. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/dialect.py +90 -11
  10. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/dialect_async.py +31 -39
  11. sqlalchemy_cloudflare_d1-0.3.10/tests/integration/test_base_url_integration.py +175 -0
  12. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/integration/test_restapi_integration.py +301 -0
  13. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/integration/test_worker_integration.py +188 -1
  14. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/unit/test_dialect.py +60 -0
  15. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/uv.lock +1 -1
  16. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.github/DEVELOPMENT.md +0 -0
  17. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.github/workflows/ci.yml +0 -0
  18. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.github/workflows/release.yml +0 -0
  19. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.gitignore +0 -0
  20. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.pre-commit-config.yaml +0 -0
  21. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/.python-version +0 -0
  22. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/LICENSE +0 -0
  23. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/Makefile +0 -0
  24. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/README.md +0 -0
  25. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/assets/d1-logo.png +0 -0
  26. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/assets/sqlalchemy-logo.png +0 -0
  27. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/restapi/example.py +0 -0
  28. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/README.md +0 -0
  29. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/db_init.sql +0 -0
  30. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/pyproject.toml +0 -0
  31. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/examples/workers/wrangler.jsonc +0 -0
  32. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/src/sqlalchemy_cloudflare_d1/py.typed +0 -0
  33. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/__init__.py +0 -0
  34. {sqlalchemy_cloudflare_d1-0.3.8 → sqlalchemy_cloudflare_d1-0.3.10}/tests/conftest.py +0 -0
  35. {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.8
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
@@ -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
+ )
@@ -614,7 +614,7 @@ wheels = [
614
614
 
615
615
  [[package]]
616
616
  name = "sqlalchemy-cloudflare-d1"
617
- version = "0.3.8"
617
+ version = "0.3.10"
618
618
  source = { editable = "../../" }
619
619
  dependencies = [
620
620
  { name = "httpx" },
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sqlalchemy-cloudflare-d1"
3
- version = "0.3.8"
3
+ version = "0.3.10"
4
4
  description = "A SQLAlchemy dialect for Cloudflare's D1 Serverless SQLite Database"
5
5
  readme = "README.md"
6
6
  authors = [