sqlalchemy-cubrid 1.4.2__tar.gz → 1.5.0__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_cubrid-1.4.2/sqlalchemy_cubrid.egg-info → sqlalchemy_cubrid-1.5.0}/PKG-INFO +43 -32
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/README.md +33 -22
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/pyproject.toml +10 -14
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/__init__.py +1 -1
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/_compat.py +24 -10
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/aio_pycubrid_dialect.py +15 -5
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/alembic_impl.py +5 -3
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/base.py +4 -4
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/compiler.py +113 -20
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/dialect.py +103 -21
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/pycubrid_dialect.py +6 -8
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/requirements.py +3 -3
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/trace.py +17 -4
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/types.py +41 -4
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0/sqlalchemy_cubrid.egg-info}/PKG-INFO +43 -32
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid.egg-info/SOURCES.txt +2 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid.egg-info/requires.txt +3 -3
- sqlalchemy_cubrid-1.5.0/test/test_aio_integration.py +365 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_aio_pycubrid_dialect.py +10 -1
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_alembic.py +19 -3
- sqlalchemy_cubrid-1.5.0/test/test_alembic_roundtrip.py +296 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_compiler.py +199 -32
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_dialect_offline.py +112 -2
- sqlalchemy_cubrid-1.5.0/test/test_reflection_golden.py +291 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_requirements.py +2 -2
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_show_create_table.py +1 -1
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_trace.py +10 -5
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_types.py +5 -0
- sqlalchemy_cubrid-1.4.2/test/test_aio_integration.py +0 -278
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/AUTHORS +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/LICENSE +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/setup.cfg +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/dml.py +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/py.typed +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid.egg-info/entry_points.txt +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid.egg-info/top_level.txt +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_base.py +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_dialects.py +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_integration.py +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_json.py +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_logging.py +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_packaging.py +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_stress_pool.py +0 -0
- {sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/test/test_suite.py +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-cubrid
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
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
|
|
7
|
-
Project-URL: Homepage, https://github.com/cubrid-
|
|
8
|
-
Project-URL: Repository, https://github.com/cubrid-
|
|
9
|
-
Project-URL: Documentation, https://github.com/cubrid-
|
|
10
|
-
Project-URL: Changelog, https://github.com/cubrid-
|
|
11
|
-
Project-URL: Issues, https://github.com/cubrid-
|
|
7
|
+
Project-URL: Homepage, https://github.com/cubrid-lab/sqlalchemy-cubrid
|
|
8
|
+
Project-URL: Repository, https://github.com/cubrid-lab/sqlalchemy-cubrid
|
|
9
|
+
Project-URL: Documentation, https://github.com/cubrid-lab/sqlalchemy-cubrid/tree/main/docs
|
|
10
|
+
Project-URL: Changelog, https://github.com/cubrid-lab/sqlalchemy-cubrid/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: Issues, https://github.com/cubrid-lab/sqlalchemy-cubrid/issues
|
|
12
12
|
Keywords: SQLAlchemy,CUBRID,dialect
|
|
13
|
-
Classifier: Development Status ::
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
14
|
Classifier: Environment :: Console
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
@@ -29,15 +29,15 @@ Requires-Python: >=3.10
|
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
License-File: LICENSE
|
|
31
31
|
License-File: AUTHORS
|
|
32
|
-
Requires-Dist: sqlalchemy<2.
|
|
32
|
+
Requires-Dist: sqlalchemy<2.3,>=2.0
|
|
33
33
|
Provides-Extra: cubrid
|
|
34
34
|
Requires-Dist: CUBRID-Python; extra == "cubrid"
|
|
35
35
|
Provides-Extra: dev
|
|
36
36
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
38
38
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
39
|
-
Requires-Dist: ruff==0.15.
|
|
40
|
-
Requires-Dist: mypy==1.
|
|
39
|
+
Requires-Dist: ruff==0.15.13; extra == "dev"
|
|
40
|
+
Requires-Dist: mypy==2.1.0; extra == "dev"
|
|
41
41
|
Requires-Dist: bandit[toml]==1.9.4; extra == "dev"
|
|
42
42
|
Requires-Dist: pre-commit; extra == "dev"
|
|
43
43
|
Requires-Dist: tox; extra == "dev"
|
|
@@ -50,42 +50,50 @@ Dynamic: license-file
|
|
|
50
50
|
|
|
51
51
|
# sqlalchemy-cubrid
|
|
52
52
|
|
|
53
|
-
**SQLAlchemy 2.0–2.
|
|
53
|
+
**SQLAlchemy 2.0–2.2 dialect for the CUBRID database** — Python ORM, schema reflection, Alembic migrations, and type mapping for SQLAlchemy and CUBRID-specific types.
|
|
54
54
|
|
|
55
55
|
[🇰🇷 한국어](docs/README.ko.md) · [🇺🇸 English](README.md) · [🇨🇳 中文](docs/README.zh.md) · [🇮🇳 हिन्दी](docs/README.hi.md) · [🇩🇪 Deutsch](docs/README.de.md) · [🇷🇺 Русский](docs/README.ru.md)
|
|
56
56
|
|
|
57
57
|
<!-- BADGES:START -->
|
|
58
58
|
[](https://pypi.org/project/sqlalchemy-cubrid)
|
|
59
59
|
[](https://www.python.org)
|
|
60
|
-
[](https://cubrid-
|
|
60
|
+
[](https://github.com/cubrid-lab/sqlalchemy-cubrid/actions/workflows/ci.yml)
|
|
61
|
+
[](https://github.com/cubrid-lab/sqlalchemy-cubrid/actions/workflows/integration-full.yml)
|
|
62
|
+
[](https://codecov.io/gh/cubrid-lab/sqlalchemy-cubrid)
|
|
63
|
+
[](https://github.com/cubrid-lab/sqlalchemy-cubrid/blob/main/LICENSE)
|
|
64
|
+
[](https://github.com/cubrid-lab/sqlalchemy-cubrid)
|
|
65
|
+
[](https://cubrid-lab.github.io/sqlalchemy-cubrid/)
|
|
66
66
|
<!-- BADGES:END -->
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
70
|
-
> **Status:
|
|
70
|
+
> **Status: Production/Stable** — `sqlalchemy-cubrid` is a maintained SQLAlchemy dialect for CUBRID supporting SQLAlchemy 2.0–2.2 and CUBRID 10.2–11.4.
|
|
71
71
|
|
|
72
72
|
## Why sqlalchemy-cubrid?
|
|
73
73
|
|
|
74
74
|
CUBRID is a high-performance open-source relational database, widely adopted in
|
|
75
75
|
Korean public-sector and enterprise applications. Until now, there was no
|
|
76
|
-
actively maintained SQLAlchemy dialect that supports the modern 2.0–2.
|
|
76
|
+
actively maintained SQLAlchemy dialect that supports the modern 2.0–2.2 API.
|
|
77
77
|
|
|
78
78
|
**sqlalchemy-cubrid** bridges that gap:
|
|
79
79
|
|
|
80
|
-
- Full SQLAlchemy 2.0–2.
|
|
81
|
-
- **
|
|
80
|
+
- Full SQLAlchemy 2.0–2.2 dialect with **statement caching** and **PEP 561 typing**
|
|
81
|
+
- **Extensive offline test suite** with **high code coverage** ([CI badge above](https://github.com/cubrid-lab/sqlalchemy-cubrid/actions/workflows/ci.yml)) — no database required to run them
|
|
82
82
|
- **Concurrency stress tests** — `QueuePool` sync threaded + asyncio.gather workloads validated against live CUBRID
|
|
83
|
-
- **SQLAlchemy 2.2-ready compat shim** — private API access wrapped in `_compat.py
|
|
83
|
+
- **SQLAlchemy 2.2-ready compat shim** — private API access wrapped in `_compat.py`; dependency pin now `>=2.0,<2.3` covering SA 2.0, 2.1, and the upcoming 2.2 release line
|
|
84
84
|
- Tested against **4 CUBRID versions** (10.2, 11.0, 11.2, 11.4) across **Python 3.10 -- 3.14**
|
|
85
85
|
- CUBRID-specific DML constructs: `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`
|
|
86
86
|
- Alembic migration support out of the box
|
|
87
87
|
- **Three driver options** — C-extension (`cubrid://`), pure Python (`cubrid+pycubrid://`), or async pure Python (`cubrid+aiopycubrid://`)
|
|
88
88
|
|
|
89
|
+
## Support Status
|
|
90
|
+
|
|
91
|
+
- **Status**: Production/Stable [](https://pypi.org/project/sqlalchemy-cubrid)
|
|
92
|
+
- Supported matrix: SQLAlchemy `>=2.0,<2.3`, CUBRID `10.2`, `11.0`, `11.2`, `11.4`, Python `3.10`–`3.14`
|
|
93
|
+
- Integration CI exercises Python 3.10 and 3.14 against all four CUBRID versions on every PR; intermediate versions (3.11–3.13) are supported and validated via the offline test suite
|
|
94
|
+
- SQLAlchemy `2.2` remains canary-only until explicitly added to the supported matrix
|
|
95
|
+
- See [Known Limitations](#known-limitations) for behavior boundaries and unsupported features
|
|
96
|
+
|
|
89
97
|
## Architecture
|
|
90
98
|
|
|
91
99
|
```mermaid
|
|
@@ -108,7 +116,7 @@ flowchart TD
|
|
|
108
116
|
## Requirements
|
|
109
117
|
|
|
110
118
|
- Python 3.10+
|
|
111
|
-
- SQLAlchemy 2.0 – 2.
|
|
119
|
+
- SQLAlchemy 2.0 – 2.2
|
|
112
120
|
- [CUBRID-Python](https://github.com/CUBRID/cubrid-python) (C-extension) **or** [pycubrid](https://github.com/sqlalchemy-cubrid/pycubrid) (pure Python)
|
|
113
121
|
|
|
114
122
|
## Installation
|
|
@@ -197,12 +205,15 @@ async with AsyncSession(engine) as session:
|
|
|
197
205
|
|
|
198
206
|
## Known Limitations
|
|
199
207
|
|
|
200
|
-
- **No `RETURNING`** — `INSERT/UPDATE/DELETE ... RETURNING` not supported; use `cursor.lastrowid` or `LAST_INSERT_ID()`
|
|
208
|
+
- **No `RETURNING`** — `INSERT/UPDATE/DELETE ... RETURNING` not supported; use `cursor.lastrowid` or `SELECT LAST_INSERT_ID()` after the statement
|
|
201
209
|
- **No sequences** — CUBRID uses `AUTO_INCREMENT` only
|
|
202
210
|
- **No multi-schema** — single schema per database
|
|
203
|
-
- **DDL auto-commits** — migrations are not transactional (`transactional_ddl = False`)
|
|
204
|
-
- **SQLAlchemy 2.0–2.
|
|
211
|
+
- **DDL auto-commits** — migrations are not transactional (`transactional_ddl = False`); use Alembic batch migrations and test rollback scenarios manually
|
|
212
|
+
- **SQLAlchemy 2.0–2.2 only** — pinned to `<2.3`; SA 2.2 is forward-supported via shims and a `--pre` canary CI job ([details](docs/ARCHITECTURE.md))
|
|
205
213
|
- **Async requires pycubrid >= 1.3.2,<2.0** — the `cubrid+aiopycubrid://` driver needs the async-capable pycubrid package line currently supported by this project
|
|
214
|
+
- **CARDINALITY() broken** — `func.cardinality()` raises `CompileError` with workaround guidance; the CUBRID server has a [known bug](https://github.com/cubrid-lab/.github/issues/3)
|
|
215
|
+
- **Reserved words auto-quoted** — Column names matching CUBRID reserved words (`day`, `count`, `value`, etc.) are automatically double-quoted in DDL; see [reserved word list](https://github.com/cubrid-lab/.github/issues/5)
|
|
216
|
+
- **Timezone type fidelity** — CUBRID `TIMESTAMPTZ` is reflected as `TIMESTAMP`; timezone offset is preserved in the stored value but the dialect does not distinguish `TZ` vs `LTZ` semantics at the SQLAlchemy type level. In practice this means `datetime` objects round-trip correctly but explicit TZ/LTZ type introspection is not available
|
|
206
217
|
|
|
207
218
|
## Documentation
|
|
208
219
|
|
|
@@ -226,7 +237,7 @@ async with AsyncSession(engine) as session:
|
|
|
226
237
|
|---|---|
|
|
227
238
|
| Python | 3.10, 3.11, 3.12, 3.13, 3.14 |
|
|
228
239
|
| CUBRID | 10.2, 11.0, 11.2, 11.4 |
|
|
229
|
-
| SQLAlchemy | 2.0–2.
|
|
240
|
+
| SQLAlchemy | 2.0–2.2 |
|
|
230
241
|
| Alembic | >=1.7 |
|
|
231
242
|
| pycubrid (sync) | >=1.3.2,<2.0 |
|
|
232
243
|
| pycubrid (async) | >=1.3.2,<2.0 |
|
|
@@ -242,9 +253,9 @@ engine = create_engine("cubrid://dba:password@localhost:33000/demodb")
|
|
|
242
253
|
|
|
243
254
|
For the pure Python driver (no C build needed): `create_engine("cubrid+pycubrid://dba@localhost:33000/demodb")`
|
|
244
255
|
|
|
245
|
-
### Does sqlalchemy-cubrid support SQLAlchemy 2.0–2.
|
|
256
|
+
### Does sqlalchemy-cubrid support SQLAlchemy 2.0–2.2?
|
|
246
257
|
|
|
247
|
-
Yes. sqlalchemy-cubrid is built for SQLAlchemy 2.0–2.
|
|
258
|
+
Yes. sqlalchemy-cubrid is built for SQLAlchemy 2.0–2.2 and supports the 2.0-style API including `Session.execute()`, typed `Mapped[]` columns, and statement caching.
|
|
248
259
|
|
|
249
260
|
### Does sqlalchemy-cubrid support Alembic migrations?
|
|
250
261
|
|
|
@@ -276,14 +287,14 @@ Yes. Use `create_async_engine("cubrid+aiopycubrid://...")` with the pycubrid asy
|
|
|
276
287
|
|
|
277
288
|
## Related Projects
|
|
278
289
|
|
|
279
|
-
- [pycubrid](https://github.com/cubrid-
|
|
280
|
-
- [cubrid-cookbook-python](https://github.com/cubrid-
|
|
290
|
+
- [pycubrid](https://github.com/cubrid-lab/pycubrid) — Pure Python DB-API 2.0 driver for CUBRID
|
|
291
|
+
- [cubrid-cookbook-python](https://github.com/cubrid-lab/cubrid-cookbook-python) — Production-ready Python examples for CUBRID
|
|
281
292
|
|
|
282
293
|
## Roadmap
|
|
283
294
|
|
|
284
295
|
See [`ROADMAP.md`](ROADMAP.md) for this project's direction and next milestones.
|
|
285
296
|
|
|
286
|
-
For the ecosystem-wide view, see the [CUBRID Labs Ecosystem Roadmap](https://github.com/cubrid-
|
|
297
|
+
For the ecosystem-wide view, see the [CUBRID Labs Ecosystem Roadmap](https://github.com/cubrid-lab/.github/blob/main/ROADMAP.md) and [Project Board](https://github.com/orgs/cubrid-lab/projects/2).
|
|
287
298
|
|
|
288
299
|
## Contributing
|
|
289
300
|
|
|
@@ -1,41 +1,49 @@
|
|
|
1
1
|
# sqlalchemy-cubrid
|
|
2
2
|
|
|
3
|
-
**SQLAlchemy 2.0–2.
|
|
3
|
+
**SQLAlchemy 2.0–2.2 dialect for the CUBRID database** — Python ORM, schema reflection, Alembic migrations, and type mapping for SQLAlchemy and CUBRID-specific types.
|
|
4
4
|
|
|
5
5
|
[🇰🇷 한국어](docs/README.ko.md) · [🇺🇸 English](README.md) · [🇨🇳 中文](docs/README.zh.md) · [🇮🇳 हिन्दी](docs/README.hi.md) · [🇩🇪 Deutsch](docs/README.de.md) · [🇷🇺 Русский](docs/README.ru.md)
|
|
6
6
|
|
|
7
7
|
<!-- BADGES:START -->
|
|
8
8
|
[](https://pypi.org/project/sqlalchemy-cubrid)
|
|
9
9
|
[](https://www.python.org)
|
|
10
|
-
[](https://cubrid-
|
|
10
|
+
[](https://github.com/cubrid-lab/sqlalchemy-cubrid/actions/workflows/ci.yml)
|
|
11
|
+
[](https://github.com/cubrid-lab/sqlalchemy-cubrid/actions/workflows/integration-full.yml)
|
|
12
|
+
[](https://codecov.io/gh/cubrid-lab/sqlalchemy-cubrid)
|
|
13
|
+
[](https://github.com/cubrid-lab/sqlalchemy-cubrid/blob/main/LICENSE)
|
|
14
|
+
[](https://github.com/cubrid-lab/sqlalchemy-cubrid)
|
|
15
|
+
[](https://cubrid-lab.github.io/sqlalchemy-cubrid/)
|
|
16
16
|
<!-- BADGES:END -->
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
> **Status:
|
|
20
|
+
> **Status: Production/Stable** — `sqlalchemy-cubrid` is a maintained SQLAlchemy dialect for CUBRID supporting SQLAlchemy 2.0–2.2 and CUBRID 10.2–11.4.
|
|
21
21
|
|
|
22
22
|
## Why sqlalchemy-cubrid?
|
|
23
23
|
|
|
24
24
|
CUBRID is a high-performance open-source relational database, widely adopted in
|
|
25
25
|
Korean public-sector and enterprise applications. Until now, there was no
|
|
26
|
-
actively maintained SQLAlchemy dialect that supports the modern 2.0–2.
|
|
26
|
+
actively maintained SQLAlchemy dialect that supports the modern 2.0–2.2 API.
|
|
27
27
|
|
|
28
28
|
**sqlalchemy-cubrid** bridges that gap:
|
|
29
29
|
|
|
30
|
-
- Full SQLAlchemy 2.0–2.
|
|
31
|
-
- **
|
|
30
|
+
- Full SQLAlchemy 2.0–2.2 dialect with **statement caching** and **PEP 561 typing**
|
|
31
|
+
- **Extensive offline test suite** with **high code coverage** ([CI badge above](https://github.com/cubrid-lab/sqlalchemy-cubrid/actions/workflows/ci.yml)) — no database required to run them
|
|
32
32
|
- **Concurrency stress tests** — `QueuePool` sync threaded + asyncio.gather workloads validated against live CUBRID
|
|
33
|
-
- **SQLAlchemy 2.2-ready compat shim** — private API access wrapped in `_compat.py
|
|
33
|
+
- **SQLAlchemy 2.2-ready compat shim** — private API access wrapped in `_compat.py`; dependency pin now `>=2.0,<2.3` covering SA 2.0, 2.1, and the upcoming 2.2 release line
|
|
34
34
|
- Tested against **4 CUBRID versions** (10.2, 11.0, 11.2, 11.4) across **Python 3.10 -- 3.14**
|
|
35
35
|
- CUBRID-specific DML constructs: `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`
|
|
36
36
|
- Alembic migration support out of the box
|
|
37
37
|
- **Three driver options** — C-extension (`cubrid://`), pure Python (`cubrid+pycubrid://`), or async pure Python (`cubrid+aiopycubrid://`)
|
|
38
38
|
|
|
39
|
+
## Support Status
|
|
40
|
+
|
|
41
|
+
- **Status**: Production/Stable [](https://pypi.org/project/sqlalchemy-cubrid)
|
|
42
|
+
- Supported matrix: SQLAlchemy `>=2.0,<2.3`, CUBRID `10.2`, `11.0`, `11.2`, `11.4`, Python `3.10`–`3.14`
|
|
43
|
+
- Integration CI exercises Python 3.10 and 3.14 against all four CUBRID versions on every PR; intermediate versions (3.11–3.13) are supported and validated via the offline test suite
|
|
44
|
+
- SQLAlchemy `2.2` remains canary-only until explicitly added to the supported matrix
|
|
45
|
+
- See [Known Limitations](#known-limitations) for behavior boundaries and unsupported features
|
|
46
|
+
|
|
39
47
|
## Architecture
|
|
40
48
|
|
|
41
49
|
```mermaid
|
|
@@ -58,7 +66,7 @@ flowchart TD
|
|
|
58
66
|
## Requirements
|
|
59
67
|
|
|
60
68
|
- Python 3.10+
|
|
61
|
-
- SQLAlchemy 2.0 – 2.
|
|
69
|
+
- SQLAlchemy 2.0 – 2.2
|
|
62
70
|
- [CUBRID-Python](https://github.com/CUBRID/cubrid-python) (C-extension) **or** [pycubrid](https://github.com/sqlalchemy-cubrid/pycubrid) (pure Python)
|
|
63
71
|
|
|
64
72
|
## Installation
|
|
@@ -147,12 +155,15 @@ async with AsyncSession(engine) as session:
|
|
|
147
155
|
|
|
148
156
|
## Known Limitations
|
|
149
157
|
|
|
150
|
-
- **No `RETURNING`** — `INSERT/UPDATE/DELETE ... RETURNING` not supported; use `cursor.lastrowid` or `LAST_INSERT_ID()`
|
|
158
|
+
- **No `RETURNING`** — `INSERT/UPDATE/DELETE ... RETURNING` not supported; use `cursor.lastrowid` or `SELECT LAST_INSERT_ID()` after the statement
|
|
151
159
|
- **No sequences** — CUBRID uses `AUTO_INCREMENT` only
|
|
152
160
|
- **No multi-schema** — single schema per database
|
|
153
|
-
- **DDL auto-commits** — migrations are not transactional (`transactional_ddl = False`)
|
|
154
|
-
- **SQLAlchemy 2.0–2.
|
|
161
|
+
- **DDL auto-commits** — migrations are not transactional (`transactional_ddl = False`); use Alembic batch migrations and test rollback scenarios manually
|
|
162
|
+
- **SQLAlchemy 2.0–2.2 only** — pinned to `<2.3`; SA 2.2 is forward-supported via shims and a `--pre` canary CI job ([details](docs/ARCHITECTURE.md))
|
|
155
163
|
- **Async requires pycubrid >= 1.3.2,<2.0** — the `cubrid+aiopycubrid://` driver needs the async-capable pycubrid package line currently supported by this project
|
|
164
|
+
- **CARDINALITY() broken** — `func.cardinality()` raises `CompileError` with workaround guidance; the CUBRID server has a [known bug](https://github.com/cubrid-lab/.github/issues/3)
|
|
165
|
+
- **Reserved words auto-quoted** — Column names matching CUBRID reserved words (`day`, `count`, `value`, etc.) are automatically double-quoted in DDL; see [reserved word list](https://github.com/cubrid-lab/.github/issues/5)
|
|
166
|
+
- **Timezone type fidelity** — CUBRID `TIMESTAMPTZ` is reflected as `TIMESTAMP`; timezone offset is preserved in the stored value but the dialect does not distinguish `TZ` vs `LTZ` semantics at the SQLAlchemy type level. In practice this means `datetime` objects round-trip correctly but explicit TZ/LTZ type introspection is not available
|
|
156
167
|
|
|
157
168
|
## Documentation
|
|
158
169
|
|
|
@@ -176,7 +187,7 @@ async with AsyncSession(engine) as session:
|
|
|
176
187
|
|---|---|
|
|
177
188
|
| Python | 3.10, 3.11, 3.12, 3.13, 3.14 |
|
|
178
189
|
| CUBRID | 10.2, 11.0, 11.2, 11.4 |
|
|
179
|
-
| SQLAlchemy | 2.0–2.
|
|
190
|
+
| SQLAlchemy | 2.0–2.2 |
|
|
180
191
|
| Alembic | >=1.7 |
|
|
181
192
|
| pycubrid (sync) | >=1.3.2,<2.0 |
|
|
182
193
|
| pycubrid (async) | >=1.3.2,<2.0 |
|
|
@@ -192,9 +203,9 @@ engine = create_engine("cubrid://dba:password@localhost:33000/demodb")
|
|
|
192
203
|
|
|
193
204
|
For the pure Python driver (no C build needed): `create_engine("cubrid+pycubrid://dba@localhost:33000/demodb")`
|
|
194
205
|
|
|
195
|
-
### Does sqlalchemy-cubrid support SQLAlchemy 2.0–2.
|
|
206
|
+
### Does sqlalchemy-cubrid support SQLAlchemy 2.0–2.2?
|
|
196
207
|
|
|
197
|
-
Yes. sqlalchemy-cubrid is built for SQLAlchemy 2.0–2.
|
|
208
|
+
Yes. sqlalchemy-cubrid is built for SQLAlchemy 2.0–2.2 and supports the 2.0-style API including `Session.execute()`, typed `Mapped[]` columns, and statement caching.
|
|
198
209
|
|
|
199
210
|
### Does sqlalchemy-cubrid support Alembic migrations?
|
|
200
211
|
|
|
@@ -226,14 +237,14 @@ Yes. Use `create_async_engine("cubrid+aiopycubrid://...")` with the pycubrid asy
|
|
|
226
237
|
|
|
227
238
|
## Related Projects
|
|
228
239
|
|
|
229
|
-
- [pycubrid](https://github.com/cubrid-
|
|
230
|
-
- [cubrid-cookbook-python](https://github.com/cubrid-
|
|
240
|
+
- [pycubrid](https://github.com/cubrid-lab/pycubrid) — Pure Python DB-API 2.0 driver for CUBRID
|
|
241
|
+
- [cubrid-cookbook-python](https://github.com/cubrid-lab/cubrid-cookbook-python) — Production-ready Python examples for CUBRID
|
|
231
242
|
|
|
232
243
|
## Roadmap
|
|
233
244
|
|
|
234
245
|
See [`ROADMAP.md`](ROADMAP.md) for this project's direction and next milestones.
|
|
235
246
|
|
|
236
|
-
For the ecosystem-wide view, see the [CUBRID Labs Ecosystem Roadmap](https://github.com/cubrid-
|
|
247
|
+
For the ecosystem-wide view, see the [CUBRID Labs Ecosystem Roadmap](https://github.com/cubrid-lab/.github/blob/main/ROADMAP.md) and [Project Board](https://github.com/orgs/cubrid-lab/projects/2).
|
|
237
248
|
|
|
238
249
|
## Contributing
|
|
239
250
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlalchemy-cubrid"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.5.0"
|
|
8
8
|
description = "CUBRID dialect for SQLAlchemy"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -15,7 +15,7 @@ authors = [
|
|
|
15
15
|
]
|
|
16
16
|
keywords = ["SQLAlchemy", "CUBRID", "dialect"]
|
|
17
17
|
classifiers = [
|
|
18
|
-
"Development Status ::
|
|
18
|
+
"Development Status :: 5 - Production/Stable",
|
|
19
19
|
"Environment :: Console",
|
|
20
20
|
"Intended Audience :: Developers",
|
|
21
21
|
"Operating System :: OS Independent",
|
|
@@ -32,7 +32,7 @@ classifiers = [
|
|
|
32
32
|
"Topic :: Database :: Front-Ends",
|
|
33
33
|
]
|
|
34
34
|
dependencies = [
|
|
35
|
-
"sqlalchemy>=2.0,<2.
|
|
35
|
+
"sqlalchemy>=2.0,<2.3",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[project.optional-dependencies]
|
|
@@ -43,8 +43,8 @@ dev = [
|
|
|
43
43
|
"pytest>=7.0",
|
|
44
44
|
"pytest-cov",
|
|
45
45
|
"pytest-asyncio",
|
|
46
|
-
"ruff==0.15.
|
|
47
|
-
"mypy==1.
|
|
46
|
+
"ruff==0.15.13",
|
|
47
|
+
"mypy==2.1.0",
|
|
48
48
|
"bandit[toml]==1.9.4",
|
|
49
49
|
"pre-commit",
|
|
50
50
|
"tox",
|
|
@@ -58,11 +58,11 @@ pycubrid = [
|
|
|
58
58
|
]
|
|
59
59
|
|
|
60
60
|
[project.urls]
|
|
61
|
-
Homepage = "https://github.com/cubrid-
|
|
62
|
-
Repository = "https://github.com/cubrid-
|
|
63
|
-
Documentation = "https://github.com/cubrid-
|
|
64
|
-
Changelog = "https://github.com/cubrid-
|
|
65
|
-
Issues = "https://github.com/cubrid-
|
|
61
|
+
Homepage = "https://github.com/cubrid-lab/sqlalchemy-cubrid"
|
|
62
|
+
Repository = "https://github.com/cubrid-lab/sqlalchemy-cubrid"
|
|
63
|
+
Documentation = "https://github.com/cubrid-lab/sqlalchemy-cubrid/tree/main/docs"
|
|
64
|
+
Changelog = "https://github.com/cubrid-lab/sqlalchemy-cubrid/blob/main/CHANGELOG.md"
|
|
65
|
+
Issues = "https://github.com/cubrid-lab/sqlalchemy-cubrid/issues"
|
|
66
66
|
|
|
67
67
|
[project.entry-points."sqlalchemy.dialects"]
|
|
68
68
|
cubrid = "sqlalchemy_cubrid.dialect:CubridDialect"
|
|
@@ -90,10 +90,6 @@ strict = true
|
|
|
90
90
|
warn_return_any = true
|
|
91
91
|
warn_unused_configs = true
|
|
92
92
|
|
|
93
|
-
[[tool.mypy.overrides]]
|
|
94
|
-
module = "test.*"
|
|
95
|
-
ignore_errors = true
|
|
96
|
-
|
|
97
93
|
[tool.bandit]
|
|
98
94
|
exclude_dirs = ["test"]
|
|
99
95
|
skips = ["B101"]
|
|
@@ -4,6 +4,22 @@ from typing import Any
|
|
|
4
4
|
|
|
5
5
|
from sqlalchemy.sql import elements, visitors
|
|
6
6
|
|
|
7
|
+
# DBAPIModule was added in SQLAlchemy 2.1; fall back to Any for 2.0 compat.
|
|
8
|
+
try:
|
|
9
|
+
from sqlalchemy.engine.interfaces import DBAPIModule
|
|
10
|
+
except ImportError: # pragma: no cover — SA 2.0
|
|
11
|
+
DBAPIModule = Any # type: ignore[assignment,misc]
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DBAPIModule",
|
|
15
|
+
"is_literal_value",
|
|
16
|
+
"bind_with_type",
|
|
17
|
+
"get_for_update_arg",
|
|
18
|
+
"get_limit_clause",
|
|
19
|
+
"get_offset_clause",
|
|
20
|
+
"get_distinct",
|
|
21
|
+
]
|
|
22
|
+
|
|
7
23
|
|
|
8
24
|
def is_literal_value(value: Any) -> bool:
|
|
9
25
|
"""Check if value is a plain Python literal (not a SQL element or schema object)."""
|
|
@@ -15,16 +31,10 @@ def is_literal_value(value: Any) -> bool:
|
|
|
15
31
|
|
|
16
32
|
|
|
17
33
|
def bind_with_type(element: elements.BindParameter[Any], type_: Any) -> elements.BindParameter[Any]:
|
|
18
|
-
"""Create a
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
type_=type_,
|
|
23
|
-
unique=element.unique,
|
|
24
|
-
expanding=element.expanding,
|
|
25
|
-
literal_execute=element.literal_execute,
|
|
26
|
-
isoutparam=element.isoutparam,
|
|
27
|
-
)
|
|
34
|
+
"""Create a copy of *element* with *type_* overridden, preserving all internal state."""
|
|
35
|
+
cloned = element._clone()
|
|
36
|
+
cloned.type = type_
|
|
37
|
+
return cloned
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
def get_for_update_arg(select: Any) -> Any | None:
|
|
@@ -37,3 +47,7 @@ def get_limit_clause(select: Any) -> Any | None:
|
|
|
37
47
|
|
|
38
48
|
def get_offset_clause(select: Any) -> Any | None:
|
|
39
49
|
return getattr(select, "_offset_clause", None)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_distinct(select: Any) -> Any:
|
|
53
|
+
return getattr(select, "_distinct", False)
|
{sqlalchemy_cubrid-1.4.2 → sqlalchemy_cubrid-1.5.0}/sqlalchemy_cubrid/aio_pycubrid_dialect.py
RENAMED
|
@@ -13,10 +13,16 @@ from typing import Any, Callable, cast
|
|
|
13
13
|
from sqlalchemy.connectors.asyncio import (
|
|
14
14
|
AsyncAdapt_dbapi_connection,
|
|
15
15
|
AsyncAdapt_dbapi_cursor,
|
|
16
|
-
AsyncAdapt_dbapi_module,
|
|
17
16
|
)
|
|
17
|
+
|
|
18
|
+
# AsyncAdapt_dbapi_module was added in SQLAlchemy 2.1.
|
|
19
|
+
try:
|
|
20
|
+
from sqlalchemy.connectors.asyncio import AsyncAdapt_dbapi_module
|
|
21
|
+
except ImportError: # pragma: no cover — SA 2.0
|
|
22
|
+
AsyncAdapt_dbapi_module = object # type: ignore[assignment,misc]
|
|
18
23
|
from sqlalchemy import pool as pool_module
|
|
19
|
-
from sqlalchemy.engine.interfaces import ConnectArgsType
|
|
24
|
+
from sqlalchemy.engine.interfaces import ConnectArgsType
|
|
25
|
+
from sqlalchemy_cubrid._compat import DBAPIModule
|
|
20
26
|
from sqlalchemy.engine.url import URL
|
|
21
27
|
from sqlalchemy.util.concurrency import await_only
|
|
22
28
|
|
|
@@ -35,17 +41,21 @@ class AsyncAdapt_pycubrid_cursor(AsyncAdapt_dbapi_cursor):
|
|
|
35
41
|
|
|
36
42
|
class AsyncAdapt_pycubrid_connection(AsyncAdapt_dbapi_connection):
|
|
37
43
|
_cursor_cls = AsyncAdapt_pycubrid_cursor
|
|
44
|
+
# SA 2.0 exposed ``await_`` on AsyncAdapt_dbapi_connection; SA 2.1 dropped
|
|
45
|
+
# it in favour of the module-level helper. Redeclare so ``self.await_(...)``
|
|
46
|
+
# works on both versions and remains patchable in tests.
|
|
47
|
+
await_ = staticmethod(await_only)
|
|
38
48
|
|
|
39
49
|
@property
|
|
40
50
|
def autocommit(self) -> bool:
|
|
41
|
-
return self._connection.autocommit
|
|
51
|
+
return bool(self._connection.autocommit)
|
|
42
52
|
|
|
43
53
|
@autocommit.setter
|
|
44
54
|
def autocommit(self, value: bool) -> None:
|
|
45
|
-
self.await_(self._connection.set_autocommit(value))
|
|
55
|
+
self.await_(self._connection.set_autocommit(value))
|
|
46
56
|
|
|
47
57
|
def ping(self, reconnect: bool = True) -> bool:
|
|
48
|
-
return self.await_(self._connection.ping(reconnect))
|
|
58
|
+
return bool(self.await_(self._connection.ping(reconnect)))
|
|
49
59
|
|
|
50
60
|
|
|
51
61
|
class AsyncAdapt_pycubrid_dbapi(AsyncAdapt_dbapi_module):
|
|
@@ -81,7 +81,7 @@ class CubridImpl(DefaultImpl):
|
|
|
81
81
|
# ``Text`` / ``CLOB`` / ``STRING`` column it sees a VARCHAR with that
|
|
82
82
|
# exact length, which trips Alembic's default compare_type into
|
|
83
83
|
# reporting a spurious type change on every autogenerate run
|
|
84
|
-
# (see cubrid-
|
|
84
|
+
# (see cubrid-lab/sqlalchemy-cubrid#120).
|
|
85
85
|
_CUBRID_UNBOUNDED_VARCHAR_LENGTH: int = 1073741823
|
|
86
86
|
_unbounded_string_type_names: set[str] = {"TEXT", "CLOB", "STRING"}
|
|
87
87
|
|
|
@@ -90,7 +90,7 @@ class CubridImpl(DefaultImpl):
|
|
|
90
90
|
if isinstance(value, str):
|
|
91
91
|
return value.strip().lower()
|
|
92
92
|
if isinstance(value, sa.types.TypeEngine):
|
|
93
|
-
return value.
|
|
93
|
+
return repr(value).strip().lower()
|
|
94
94
|
return repr(value).strip().lower()
|
|
95
95
|
|
|
96
96
|
def render_type(
|
|
@@ -110,7 +110,9 @@ class CubridImpl(DefaultImpl):
|
|
|
110
110
|
if isinstance(value, str):
|
|
111
111
|
rendered_values.append(repr(value))
|
|
112
112
|
elif isinstance(value, sa.types.TypeEngine):
|
|
113
|
-
|
|
113
|
+
# Render as sa.TypeName(...) preserving constructor args
|
|
114
|
+
autogen_context.imports.add("import sqlalchemy as sa")
|
|
115
|
+
rendered_values.append(f"sa.{repr(value)}")
|
|
114
116
|
else:
|
|
115
117
|
rendered_values.append(repr(value))
|
|
116
118
|
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
13
|
import re
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
15
15
|
|
|
16
16
|
from sqlalchemy.engine import default
|
|
17
17
|
from sqlalchemy.sql import compiler
|
|
@@ -402,7 +402,7 @@ class CubridExecutionContext(default.DefaultExecutionContext):
|
|
|
402
402
|
def should_autocommit_text(self, statement: str) -> Any:
|
|
403
403
|
return AUTOCOMMIT_REGEXP.match(statement)
|
|
404
404
|
|
|
405
|
-
def get_lastrowid(self) -> int:
|
|
405
|
+
def get_lastrowid(self) -> int | None: # type: ignore[override]
|
|
406
406
|
"""Return the last inserted row ID.
|
|
407
407
|
|
|
408
408
|
CUBRID's Python driver does not expose ``cursor.lastrowid``.
|
|
@@ -415,7 +415,7 @@ class CubridExecutionContext(default.DefaultExecutionContext):
|
|
|
415
415
|
raw_conn = self.root_connection.connection.dbapi_connection
|
|
416
416
|
if raw_conn is not None and hasattr(raw_conn, "get_last_insert_id"):
|
|
417
417
|
last_id = raw_conn.get_last_insert_id()
|
|
418
|
-
return
|
|
418
|
+
return None if last_id is None else int(last_id)
|
|
419
419
|
except Exception: # nosec B110 — fallback to SQL when driver lacks method
|
|
420
420
|
log.debug("get_last_insert_id via driver failed, falling back to SQL", exc_info=True)
|
|
421
421
|
|
|
@@ -428,4 +428,4 @@ class CubridExecutionContext(default.DefaultExecutionContext):
|
|
|
428
428
|
return int(row[0])
|
|
429
429
|
finally:
|
|
430
430
|
cursor.close()
|
|
431
|
-
return
|
|
431
|
+
return None
|