sqlalchemy-cubrid 1.2.0__tar.gz → 1.2.1__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 (38) hide show
  1. {sqlalchemy_cubrid-1.2.0/sqlalchemy_cubrid.egg-info → sqlalchemy_cubrid-1.2.1}/PKG-INFO +1 -1
  2. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/pyproject.toml +1 -1
  3. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/__init__.py +1 -1
  4. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/aio_pycubrid_dialect.py +5 -0
  5. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/dialect.py +9 -1
  6. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1/sqlalchemy_cubrid.egg-info}/PKG-INFO +1 -1
  7. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid.egg-info/SOURCES.txt +1 -0
  8. sqlalchemy_cubrid-1.2.1/test/test_aio_integration.py +293 -0
  9. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/AUTHORS +0 -0
  10. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/LICENSE +0 -0
  11. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/README.md +0 -0
  12. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/setup.cfg +0 -0
  13. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/alembic_impl.py +0 -0
  14. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/base.py +0 -0
  15. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/compiler.py +0 -0
  16. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/dml.py +0 -0
  17. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/py.typed +0 -0
  18. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/pycubrid_dialect.py +0 -0
  19. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/requirements.py +0 -0
  20. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/trace.py +0 -0
  21. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid/types.py +0 -0
  22. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid.egg-info/dependency_links.txt +0 -0
  23. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid.egg-info/entry_points.txt +0 -0
  24. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid.egg-info/requires.txt +0 -0
  25. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/sqlalchemy_cubrid.egg-info/top_level.txt +0 -0
  26. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_aio_pycubrid_dialect.py +0 -0
  27. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_alembic.py +0 -0
  28. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_base.py +0 -0
  29. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_compiler.py +0 -0
  30. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_dialect_offline.py +0 -0
  31. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_dialects.py +0 -0
  32. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_integration.py +0 -0
  33. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_json.py +0 -0
  34. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_pycubrid_dialect.py +0 -0
  35. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_requirements.py +0 -0
  36. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_suite.py +0 -0
  37. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_trace.py +0 -0
  38. {sqlalchemy_cubrid-1.2.0 → sqlalchemy_cubrid-1.2.1}/test/test_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemy-cubrid
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: CUBRID dialect for SQLAlchemy
5
5
  Author-email: Yeongseon Choe <yeongseon.choe@gmail.com>, Gyeongjun Paik <paikend@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sqlalchemy-cubrid"
7
- version = "1.2.0"
7
+ version = "1.2.1"
8
8
  description = "CUBRID dialect for SQLAlchemy"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -52,7 +52,7 @@ from sqlalchemy.sql.sqltypes import (
52
52
  TIMESTAMP,
53
53
  )
54
54
 
55
- __version__ = "1.2.0"
55
+ __version__ = "1.2.1"
56
56
 
57
57
  __all__ = (
58
58
  "insert",
@@ -14,6 +14,7 @@ from sqlalchemy.connectors.asyncio import (
14
14
  AsyncAdapt_dbapi_cursor,
15
15
  AsyncAdapt_dbapi_module,
16
16
  )
17
+ from sqlalchemy import pool as pool_module
17
18
  from sqlalchemy.engine.interfaces import ConnectArgsType, DBAPIModule
18
19
  from sqlalchemy.engine.url import URL
19
20
  from sqlalchemy.util.concurrency import await_only
@@ -79,6 +80,10 @@ class PyCubridAsyncDialect(PyCubridDialect):
79
80
  supports_statement_cache = True
80
81
  execution_ctx_cls = PyCubridExecutionContext
81
82
 
83
+ @classmethod
84
+ def get_pool_class(cls, url: URL) -> type[pool_module.Pool]:
85
+ return pool_module.AsyncAdaptedQueuePool
86
+
82
87
  @classmethod
83
88
  def import_dbapi(cls) -> DBAPIModule:
84
89
  import pycubrid.aio as aio_module
@@ -198,9 +198,17 @@ class CubridDialect(default.DefaultDialect):
198
198
 
199
199
  postfetch_lastrowid = True
200
200
 
201
- def __init__(self, isolation_level: str | None = None, **kwargs: Any) -> None:
201
+ def __init__(
202
+ self,
203
+ isolation_level: str | None = None,
204
+ json_serializer: Any = None,
205
+ json_deserializer: Any = None,
206
+ **kwargs: Any,
207
+ ) -> None:
202
208
  super().__init__(**kwargs)
203
209
  self.isolation_level = isolation_level
210
+ self._json_serializer = json_serializer
211
+ self._json_deserializer = json_deserializer
204
212
 
205
213
  @classmethod
206
214
  def import_dbapi(cls) -> DBAPIModule:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlalchemy-cubrid
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: CUBRID dialect for SQLAlchemy
5
5
  Author-email: Yeongseon Choe <yeongseon.choe@gmail.com>, Gyeongjun Paik <paikend@gmail.com>
6
6
  License-Expression: MIT
@@ -20,6 +20,7 @@ sqlalchemy_cubrid.egg-info/dependency_links.txt
20
20
  sqlalchemy_cubrid.egg-info/entry_points.txt
21
21
  sqlalchemy_cubrid.egg-info/requires.txt
22
22
  sqlalchemy_cubrid.egg-info/top_level.txt
23
+ test/test_aio_integration.py
23
24
  test/test_aio_pycubrid_dialect.py
24
25
  test/test_alembic.py
25
26
  test/test_base.py
@@ -0,0 +1,293 @@
1
+ # test/test_aio_integration.py
2
+ # Copyright (C) 2021-2026 by sqlalchemy-cubrid authors and contributors
3
+ # <see AUTHORS file>
4
+ #
5
+ # This module is part of sqlalchemy-cubrid and is released under
6
+ # the MIT License: http://www.opensource.org/licenses/mit-license.php
7
+
8
+ """Async integration tests against a live CUBRID instance.
9
+
10
+ These tests require a running CUBRID database. They are skipped
11
+ automatically when no CUBRID connection is available.
12
+
13
+ Set the environment variable ``CUBRID_TEST_URL`` to the **sync**
14
+ connection URL. The async URL is derived automatically::
15
+
16
+ export CUBRID_TEST_URL="cubrid://dba@localhost:33000/testdb"
17
+
18
+ The async dialect uses ``cubrid+aiopycubrid://`` as scheme.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import asyncio
24
+ import json
25
+ import os
26
+
27
+ import pytest
28
+ import pytest_asyncio
29
+ from sqlalchemy import Column, Integer, MetaData, String, Table, text
30
+ from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
31
+ from sqlalchemy.orm import sessionmaker
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Fixtures
35
+ # ---------------------------------------------------------------------------
36
+
37
+ _DEFAULT_SYNC_URL = "cubrid://dba@localhost:33000/testdb"
38
+
39
+
40
+ def _async_url() -> str:
41
+ """Derive the async URL from the sync one."""
42
+ sync = os.environ.get("CUBRID_TEST_URL", _DEFAULT_SYNC_URL)
43
+ return sync.replace("cubrid://", "cubrid+aiopycubrid://", 1)
44
+
45
+
46
+ def _can_connect_async() -> bool:
47
+ """Return True if a CUBRID instance is reachable via async."""
48
+ try:
49
+ engine = create_async_engine(_async_url())
50
+
51
+ async def _probe() -> bool:
52
+ async with engine.connect() as conn:
53
+ await conn.execute(text("SELECT 1"))
54
+ await engine.dispose()
55
+ return True
56
+
57
+ return asyncio.get_event_loop().run_until_complete(_probe())
58
+ except Exception:
59
+ return False
60
+
61
+
62
+ pytestmark = [
63
+ pytest.mark.skipif(
64
+ not _can_connect_async(),
65
+ reason="CUBRID async instance not available (set CUBRID_TEST_URL)",
66
+ ),
67
+ pytest.mark.asyncio,
68
+ ]
69
+
70
+
71
+ @pytest_asyncio.fixture(scope="module")
72
+ def event_loop():
73
+ """Create a single event loop for the module."""
74
+ loop = asyncio.new_event_loop()
75
+ yield loop
76
+ loop.close()
77
+
78
+
79
+ @pytest_asyncio.fixture(scope="module")
80
+ async def engine():
81
+ eng = create_async_engine(_async_url(), echo=False)
82
+ yield eng
83
+ await eng.dispose()
84
+
85
+
86
+ @pytest_asyncio.fixture(scope="module")
87
+ async def metadata(engine):
88
+ meta = MetaData()
89
+ Table(
90
+ "aio_test_users",
91
+ meta,
92
+ Column("id", Integer, primary_key=True, autoincrement=True),
93
+ Column("name", String(100), nullable=False),
94
+ Column("value", Integer),
95
+ )
96
+ async with engine.begin() as conn:
97
+ await conn.execute(text("DROP TABLE IF EXISTS aio_test_users"))
98
+ await conn.run_sync(meta.create_all)
99
+ yield meta
100
+ async with engine.begin() as conn:
101
+ await conn.execute(text("DROP TABLE IF EXISTS aio_test_users"))
102
+
103
+
104
+ # ---------------------------------------------------------------------------
105
+ # Phase 2: Async CRUD + Transaction
106
+ # ---------------------------------------------------------------------------
107
+
108
+
109
+ class TestAsyncCRUD:
110
+ """Async CRUD round-trip tests."""
111
+
112
+ async def test_connect(self, engine):
113
+ async with engine.connect() as conn:
114
+ result = await conn.execute(text("SELECT 1"))
115
+ assert result.fetchone() == (1,)
116
+
117
+ async def test_insert_and_select(self, engine, metadata):
118
+ users = metadata.tables["aio_test_users"]
119
+ async with engine.begin() as conn:
120
+ await conn.execute(
121
+ users.insert(),
122
+ [{"name": "alice", "value": 10}, {"name": "bob", "value": 20}],
123
+ )
124
+ async with engine.connect() as conn:
125
+ result = await conn.execute(users.select())
126
+ rows = result.fetchall()
127
+ assert len(rows) >= 2
128
+
129
+ async def test_update(self, engine, metadata):
130
+ users = metadata.tables["aio_test_users"]
131
+ async with engine.begin() as conn:
132
+ await conn.execute(
133
+ users.update().where(users.c.name == "alice").values(value=100)
134
+ )
135
+ async with engine.connect() as conn:
136
+ result = await conn.execute(
137
+ users.select().where(users.c.name == "alice")
138
+ )
139
+ row = result.fetchone()
140
+ assert row is not None and row.value == 100
141
+
142
+ async def test_delete(self, engine, metadata):
143
+ users = metadata.tables["aio_test_users"]
144
+ async with engine.begin() as conn:
145
+ await conn.execute(users.delete().where(users.c.name == "bob"))
146
+ async with engine.connect() as conn:
147
+ result = await conn.execute(
148
+ users.select().where(users.c.name == "bob")
149
+ )
150
+ assert result.fetchone() is None
151
+
152
+ async def test_transaction_rollback(self, engine, metadata):
153
+ users = metadata.tables["aio_test_users"]
154
+ try:
155
+ async with engine.begin() as conn:
156
+ await conn.execute(
157
+ users.insert().values(name="will_rollback", value=999)
158
+ )
159
+ raise RuntimeError("force rollback")
160
+ except RuntimeError:
161
+ pass
162
+
163
+ async with engine.connect() as conn:
164
+ result = await conn.execute(
165
+ users.select().where(users.c.name == "will_rollback")
166
+ )
167
+ assert result.fetchone() is None
168
+
169
+ async def test_concurrent_pool(self, engine):
170
+ async def worker(i: int) -> int:
171
+ async with engine.connect() as conn:
172
+ r = await conn.execute(text(f"SELECT {i}"))
173
+ return r.scalar()
174
+
175
+ results = await asyncio.gather(*[worker(i) for i in range(5)])
176
+ assert sorted(results) == [0, 1, 2, 3, 4]
177
+
178
+ async def test_bad_sql_raises(self, engine):
179
+ with pytest.raises(Exception):
180
+ async with engine.connect() as conn:
181
+ await conn.execute(text("SELECT * FROM nonexistent_xyz"))
182
+
183
+ async def test_autocommit_toggle(self, engine):
184
+ async with engine.connect() as conn:
185
+ raw = await conn.get_raw_connection()
186
+ raw.autocommit = True
187
+ raw.autocommit = False
188
+
189
+
190
+ # ---------------------------------------------------------------------------
191
+ # Phase 3: JSON
192
+ # ---------------------------------------------------------------------------
193
+
194
+
195
+ class TestAsyncJSON:
196
+ """JSON type round-trip tests."""
197
+
198
+ @pytest_asyncio.fixture(autouse=True)
199
+ async def _json_table(self, engine):
200
+ async with engine.begin() as conn:
201
+ await conn.execute(text("DROP TABLE IF EXISTS aio_test_json"))
202
+ await conn.execute(
203
+ text(
204
+ "CREATE TABLE aio_test_json ("
205
+ " id INT AUTO_INCREMENT PRIMARY KEY,"
206
+ " payload JSON"
207
+ ")"
208
+ )
209
+ )
210
+ yield
211
+ async with engine.begin() as conn:
212
+ await conn.execute(text("DROP TABLE IF EXISTS aio_test_json"))
213
+
214
+ async def _insert_json(self, engine, value):
215
+ async with engine.begin() as conn:
216
+ await conn.execute(
217
+ text("INSERT INTO aio_test_json (payload) VALUES (:p)"),
218
+ {"p": json.dumps(value) if value is not None else None},
219
+ )
220
+
221
+ async def _last_json(self, engine):
222
+ async with engine.connect() as conn:
223
+ r = await conn.execute(
224
+ text("SELECT payload FROM aio_test_json ORDER BY id DESC LIMIT 1")
225
+ )
226
+ raw = r.scalar()
227
+ return json.loads(raw) if isinstance(raw, str) else raw
228
+
229
+ async def test_dict_roundtrip(self, engine):
230
+ d = {"key": "value", "n": 42}
231
+ await self._insert_json(engine, d)
232
+ assert await self._last_json(engine) == d
233
+
234
+ async def test_list_roundtrip(self, engine):
235
+ lst = [1, "two", 3.0, None]
236
+ await self._insert_json(engine, lst)
237
+ assert await self._last_json(engine) == lst
238
+
239
+ async def test_nested_roundtrip(self, engine):
240
+ nested = {"a": {"b": [1, {"c": True}]}}
241
+ await self._insert_json(engine, nested)
242
+ assert await self._last_json(engine) == nested
243
+
244
+ async def test_null_json(self, engine):
245
+ async with engine.begin() as conn:
246
+ await conn.execute(
247
+ text("INSERT INTO aio_test_json (payload) VALUES (NULL)")
248
+ )
249
+ assert await self._last_json(engine) is None
250
+
251
+ async def test_empty_object(self, engine):
252
+ await self._insert_json(engine, {})
253
+ assert await self._last_json(engine) == {}
254
+
255
+ async def test_empty_array(self, engine):
256
+ await self._insert_json(engine, [])
257
+ assert await self._last_json(engine) == []
258
+
259
+ async def test_json_extract(self, engine):
260
+ async with engine.connect() as conn:
261
+ r = await conn.execute(
262
+ text("SELECT JSON_EXTRACT('{\"a\": 1}', '$.a')")
263
+ )
264
+ assert r.scalar() is not None
265
+
266
+ async def test_orm_json_type(self, engine):
267
+ from sqlalchemy_cubrid.types import JSON as CubridJSON
268
+
269
+ meta = MetaData()
270
+ t = Table(
271
+ "aio_test_json_orm",
272
+ meta,
273
+ Column("id", Integer, primary_key=True, autoincrement=True),
274
+ Column("data", CubridJSON),
275
+ )
276
+ async with engine.begin() as conn:
277
+ await conn.execute(text("DROP TABLE IF EXISTS aio_test_json_orm"))
278
+ await conn.run_sync(meta.create_all)
279
+
280
+ test_data = {"items": [1, 2, 3]}
281
+ async with engine.begin() as conn:
282
+ await conn.execute(t.insert().values(data=test_data))
283
+
284
+ async with engine.connect() as conn:
285
+ r = await conn.execute(t.select())
286
+ row = r.fetchone()
287
+ val = row.data
288
+ if isinstance(val, str):
289
+ val = json.loads(val)
290
+ assert val == test_data
291
+
292
+ async with engine.begin() as conn:
293
+ await conn.execute(text("DROP TABLE IF EXISTS aio_test_json_orm"))