sqlalchemy-cubrid 1.0.0__tar.gz → 1.2.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.0.0/sqlalchemy_cubrid.egg-info → sqlalchemy_cubrid-1.2.0}/PKG-INFO +26 -5
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/README.md +25 -4
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/pyproject.toml +2 -1
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/__init__.py +7 -1
- sqlalchemy_cubrid-1.2.0/sqlalchemy_cubrid/aio_pycubrid_dialect.py +125 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/compiler.py +54 -1
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/dialect.py +26 -4
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/types.py +89 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0/sqlalchemy_cubrid.egg-info}/PKG-INFO +26 -5
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/SOURCES.txt +3 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/entry_points.txt +1 -0
- sqlalchemy_cubrid-1.2.0/test/test_aio_pycubrid_dialect.py +206 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_dialect_offline.py +1 -1
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_dialects.py +30 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_integration.py +33 -1
- sqlalchemy_cubrid-1.2.0/test/test_json.py +312 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_pycubrid_dialect.py +9 -2
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/AUTHORS +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/LICENSE +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/setup.cfg +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/alembic_impl.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/base.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/dml.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/py.typed +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/requirements.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/trace.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/requires.txt +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/top_level.txt +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_alembic.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_base.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_compiler.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_requirements.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_suite.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_trace.py +0 -0
- {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-cubrid
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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
|
|
@@ -78,7 +78,7 @@ production-ready SQLAlchemy dialect that supports the modern 2.0–2.1 API.
|
|
|
78
78
|
- Tested against **4 CUBRID versions** (10.2, 11.0, 11.2, 11.4) across **Python 3.10 -- 3.14**
|
|
79
79
|
- CUBRID-specific DML constructs: `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`
|
|
80
80
|
- Alembic migration support out of the box
|
|
81
|
-
- **
|
|
81
|
+
- **Three driver options** — C-extension (`cubrid://`), pure Python (`cubrid+pycubrid://`), or async pure Python (`cubrid+aiopycubrid://`)
|
|
82
82
|
|
|
83
83
|
## Architecture
|
|
84
84
|
|
|
@@ -88,8 +88,10 @@ flowchart TD
|
|
|
88
88
|
sa --> dialect["CubridDialect"]
|
|
89
89
|
dialect --> pycubrid["pycubrid driver"]
|
|
90
90
|
dialect --> cext["CUBRIDdb driver"]
|
|
91
|
+
dialect --> aio["pycubrid.aio async driver"]
|
|
91
92
|
pycubrid --> server["CUBRID Server"]
|
|
92
93
|
cext --> server
|
|
94
|
+
aio --> server
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
```mermaid
|
|
@@ -163,15 +165,29 @@ with Session(engine) as session:
|
|
|
163
165
|
session.commit()
|
|
164
166
|
```
|
|
165
167
|
|
|
168
|
+
### Async
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
172
|
+
from sqlalchemy import text
|
|
173
|
+
|
|
174
|
+
engine = create_async_engine("cubrid+aiopycubrid://dba:password@localhost:33000/demodb")
|
|
175
|
+
|
|
176
|
+
async with AsyncSession(engine) as session:
|
|
177
|
+
result = await session.execute(text("SELECT 1"))
|
|
178
|
+
print(result.scalar())
|
|
179
|
+
```
|
|
180
|
+
|
|
166
181
|
## Features
|
|
167
182
|
|
|
168
|
-
- Complete type system -- numeric, string, date/time, bit, LOB, and
|
|
183
|
+
- Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
169
184
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
170
185
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
171
186
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
172
187
|
- Schema reflection -- tables, views, columns, PKs, FKs, indexes, unique constraints, comments
|
|
173
188
|
- Alembic migrations via `CubridImpl` (auto-discovered entry point)
|
|
174
189
|
- All 6 CUBRID isolation levels (dual-granularity: class-level + instance-level)
|
|
190
|
+
- Async support — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
175
191
|
|
|
176
192
|
## Documentation
|
|
177
193
|
|
|
@@ -187,6 +203,7 @@ with Session(engine) as session:
|
|
|
187
203
|
| [Development](docs/DEVELOPMENT.md) | Dev setup, testing, Docker, coverage, CI/CD |
|
|
188
204
|
| [Driver Compatibility](docs/DRIVER_COMPAT.md) | CUBRID-Python driver versions and known issues |
|
|
189
205
|
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common issues, error solutions, debugging techniques |
|
|
206
|
+
| [Async Connection](docs/CONNECTION.md#async-connection) | Async engine setup with `cubrid+aiopycubrid://` |
|
|
190
207
|
|
|
191
208
|
## Compatibility Matrix
|
|
192
209
|
|
|
@@ -233,13 +250,17 @@ stmt = insert(users).values(name="Alice").on_duplicate_key_update(name="Alice Up
|
|
|
233
250
|
|
|
234
251
|
### What's the difference between `cubrid://` and `cubrid+pycubrid://`?
|
|
235
252
|
|
|
236
|
-
`cubrid://` uses the C-extension driver (CUBRIDdb) which requires compilation. `cubrid+pycubrid://` uses the pure Python driver which installs with pip alone — no build tools needed.
|
|
253
|
+
`cubrid://` uses the C-extension driver (CUBRIDdb) which requires compilation. `cubrid+pycubrid://` uses the pure Python driver which installs with pip alone — no build tools needed. `cubrid+aiopycubrid://` uses the async variant of the pure Python driver for use with `create_async_engine` and `AsyncSession`.
|
|
254
|
+
|
|
255
|
+
### Does sqlalchemy-cubrid support async?
|
|
256
|
+
|
|
257
|
+
Yes. Use `create_async_engine("cubrid+aiopycubrid://...")` with the pycubrid async driver. Requires `pycubrid>=1.1.0`. All Core and ORM features work with `AsyncSession`.
|
|
237
258
|
|
|
238
259
|
|
|
239
260
|
## Related Projects
|
|
240
261
|
|
|
241
262
|
- [pycubrid](https://github.com/cubrid-labs/pycubrid) — Pure Python DB-API 2.0 driver for CUBRID
|
|
242
|
-
- [cubrid-python
|
|
263
|
+
- [cubrid-cookbook-python](https://github.com/cubrid-labs/cubrid-cookbook-python) — Production-ready Python examples for CUBRID
|
|
243
264
|
|
|
244
265
|
## Roadmap
|
|
245
266
|
|
|
@@ -29,7 +29,7 @@ production-ready SQLAlchemy dialect that supports the modern 2.0–2.1 API.
|
|
|
29
29
|
- Tested against **4 CUBRID versions** (10.2, 11.0, 11.2, 11.4) across **Python 3.10 -- 3.14**
|
|
30
30
|
- CUBRID-specific DML constructs: `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`
|
|
31
31
|
- Alembic migration support out of the box
|
|
32
|
-
- **
|
|
32
|
+
- **Three driver options** — C-extension (`cubrid://`), pure Python (`cubrid+pycubrid://`), or async pure Python (`cubrid+aiopycubrid://`)
|
|
33
33
|
|
|
34
34
|
## Architecture
|
|
35
35
|
|
|
@@ -39,8 +39,10 @@ flowchart TD
|
|
|
39
39
|
sa --> dialect["CubridDialect"]
|
|
40
40
|
dialect --> pycubrid["pycubrid driver"]
|
|
41
41
|
dialect --> cext["CUBRIDdb driver"]
|
|
42
|
+
dialect --> aio["pycubrid.aio async driver"]
|
|
42
43
|
pycubrid --> server["CUBRID Server"]
|
|
43
44
|
cext --> server
|
|
45
|
+
aio --> server
|
|
44
46
|
```
|
|
45
47
|
|
|
46
48
|
```mermaid
|
|
@@ -114,15 +116,29 @@ with Session(engine) as session:
|
|
|
114
116
|
session.commit()
|
|
115
117
|
```
|
|
116
118
|
|
|
119
|
+
### Async
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
123
|
+
from sqlalchemy import text
|
|
124
|
+
|
|
125
|
+
engine = create_async_engine("cubrid+aiopycubrid://dba:password@localhost:33000/demodb")
|
|
126
|
+
|
|
127
|
+
async with AsyncSession(engine) as session:
|
|
128
|
+
result = await session.execute(text("SELECT 1"))
|
|
129
|
+
print(result.scalar())
|
|
130
|
+
```
|
|
131
|
+
|
|
117
132
|
## Features
|
|
118
133
|
|
|
119
|
-
- Complete type system -- numeric, string, date/time, bit, LOB, and
|
|
134
|
+
- Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
120
135
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
121
136
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
122
137
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
123
138
|
- Schema reflection -- tables, views, columns, PKs, FKs, indexes, unique constraints, comments
|
|
124
139
|
- Alembic migrations via `CubridImpl` (auto-discovered entry point)
|
|
125
140
|
- All 6 CUBRID isolation levels (dual-granularity: class-level + instance-level)
|
|
141
|
+
- Async support — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
126
142
|
|
|
127
143
|
## Documentation
|
|
128
144
|
|
|
@@ -138,6 +154,7 @@ with Session(engine) as session:
|
|
|
138
154
|
| [Development](docs/DEVELOPMENT.md) | Dev setup, testing, Docker, coverage, CI/CD |
|
|
139
155
|
| [Driver Compatibility](docs/DRIVER_COMPAT.md) | CUBRID-Python driver versions and known issues |
|
|
140
156
|
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common issues, error solutions, debugging techniques |
|
|
157
|
+
| [Async Connection](docs/CONNECTION.md#async-connection) | Async engine setup with `cubrid+aiopycubrid://` |
|
|
141
158
|
|
|
142
159
|
## Compatibility Matrix
|
|
143
160
|
|
|
@@ -184,13 +201,17 @@ stmt = insert(users).values(name="Alice").on_duplicate_key_update(name="Alice Up
|
|
|
184
201
|
|
|
185
202
|
### What's the difference between `cubrid://` and `cubrid+pycubrid://`?
|
|
186
203
|
|
|
187
|
-
`cubrid://` uses the C-extension driver (CUBRIDdb) which requires compilation. `cubrid+pycubrid://` uses the pure Python driver which installs with pip alone — no build tools needed.
|
|
204
|
+
`cubrid://` uses the C-extension driver (CUBRIDdb) which requires compilation. `cubrid+pycubrid://` uses the pure Python driver which installs with pip alone — no build tools needed. `cubrid+aiopycubrid://` uses the async variant of the pure Python driver for use with `create_async_engine` and `AsyncSession`.
|
|
205
|
+
|
|
206
|
+
### Does sqlalchemy-cubrid support async?
|
|
207
|
+
|
|
208
|
+
Yes. Use `create_async_engine("cubrid+aiopycubrid://...")` with the pycubrid async driver. Requires `pycubrid>=1.1.0`. All Core and ORM features work with `AsyncSession`.
|
|
188
209
|
|
|
189
210
|
|
|
190
211
|
## Related Projects
|
|
191
212
|
|
|
192
213
|
- [pycubrid](https://github.com/cubrid-labs/pycubrid) — Pure Python DB-API 2.0 driver for CUBRID
|
|
193
|
-
- [cubrid-python
|
|
214
|
+
- [cubrid-cookbook-python](https://github.com/cubrid-labs/cubrid-cookbook-python) — Production-ready Python examples for CUBRID
|
|
194
215
|
|
|
195
216
|
## Roadmap
|
|
196
217
|
|
|
@@ -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.2.0"
|
|
8
8
|
description = "CUBRID dialect for SQLAlchemy"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -67,6 +67,7 @@ Issues = "https://github.com/cubrid-labs/sqlalchemy-cubrid/issues"
|
|
|
67
67
|
cubrid = "sqlalchemy_cubrid.dialect:CubridDialect"
|
|
68
68
|
"cubrid.cubrid" = "sqlalchemy_cubrid.dialect:CubridDialect"
|
|
69
69
|
"cubrid.pycubrid" = "sqlalchemy_cubrid.pycubrid_dialect:PyCubridDialect"
|
|
70
|
+
"cubrid.aiopycubrid" = "sqlalchemy_cubrid.aio_pycubrid_dialect:PyCubridAsyncDialect"
|
|
70
71
|
|
|
71
72
|
[project.entry-points."alembic.ddl"]
|
|
72
73
|
cubrid = "sqlalchemy_cubrid.alembic_impl:CubridImpl"
|
|
@@ -25,6 +25,9 @@ from .types import (
|
|
|
25
25
|
DOUBLE,
|
|
26
26
|
DOUBLE_PRECISION,
|
|
27
27
|
FLOAT,
|
|
28
|
+
JSON,
|
|
29
|
+
JSONIndexType,
|
|
30
|
+
JSONPathType,
|
|
28
31
|
MONETARY,
|
|
29
32
|
MULTISET,
|
|
30
33
|
NCHAR,
|
|
@@ -49,7 +52,7 @@ from sqlalchemy.sql.sqltypes import (
|
|
|
49
52
|
TIMESTAMP,
|
|
50
53
|
)
|
|
51
54
|
|
|
52
|
-
__version__ = "1.
|
|
55
|
+
__version__ = "1.2.0"
|
|
53
56
|
|
|
54
57
|
__all__ = (
|
|
55
58
|
"insert",
|
|
@@ -82,4 +85,7 @@ __all__ = (
|
|
|
82
85
|
"MULTISET",
|
|
83
86
|
"SEQUENCE",
|
|
84
87
|
"OBJECT",
|
|
88
|
+
"JSON",
|
|
89
|
+
"JSONIndexType",
|
|
90
|
+
"JSONPathType",
|
|
85
91
|
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# sqlalchemy_cubrid/aio_pycubrid_dialect.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
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any, Callable, cast
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.connectors.asyncio import (
|
|
13
|
+
AsyncAdapt_dbapi_connection,
|
|
14
|
+
AsyncAdapt_dbapi_cursor,
|
|
15
|
+
AsyncAdapt_dbapi_module,
|
|
16
|
+
)
|
|
17
|
+
from sqlalchemy.engine.interfaces import ConnectArgsType, DBAPIModule
|
|
18
|
+
from sqlalchemy.engine.url import URL
|
|
19
|
+
from sqlalchemy.util.concurrency import await_only
|
|
20
|
+
|
|
21
|
+
from sqlalchemy_cubrid.pycubrid_dialect import PyCubridDialect, PyCubridExecutionContext
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AsyncAdapt_pycubrid_cursor(AsyncAdapt_dbapi_cursor):
|
|
25
|
+
_awaitable_cursor_close: bool = True
|
|
26
|
+
|
|
27
|
+
def setinputsizes(self, *inputsizes: Any) -> None:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
def nextset(self) -> None:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AsyncAdapt_pycubrid_connection(AsyncAdapt_dbapi_connection):
|
|
35
|
+
_cursor_cls = AsyncAdapt_pycubrid_cursor
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def autocommit(self) -> bool:
|
|
39
|
+
return self._connection.autocommit # type: ignore[union-attr]
|
|
40
|
+
|
|
41
|
+
@autocommit.setter
|
|
42
|
+
def autocommit(self, value: bool) -> None:
|
|
43
|
+
self.await_(self._connection.set_autocommit(value)) # type: ignore[union-attr]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AsyncAdapt_pycubrid_dbapi(AsyncAdapt_dbapi_module):
|
|
47
|
+
def __init__(self, aio_module: Any) -> None:
|
|
48
|
+
self._aio_module = aio_module
|
|
49
|
+
|
|
50
|
+
import pycubrid as sync_module
|
|
51
|
+
|
|
52
|
+
self.paramstyle = sync_module.paramstyle
|
|
53
|
+
self.Error = sync_module.Error
|
|
54
|
+
self.OperationalError = sync_module.OperationalError
|
|
55
|
+
self.InterfaceError = sync_module.InterfaceError
|
|
56
|
+
self.IntegrityError = sync_module.IntegrityError
|
|
57
|
+
self.ProgrammingError = sync_module.ProgrammingError
|
|
58
|
+
self.DatabaseError = sync_module.DatabaseError
|
|
59
|
+
self.InternalError = sync_module.InternalError
|
|
60
|
+
self.DataError = sync_module.DataError
|
|
61
|
+
self.NotSupportedError = sync_module.NotSupportedError
|
|
62
|
+
self.Warning = sync_module.Warning
|
|
63
|
+
|
|
64
|
+
self.STRING = sync_module.STRING
|
|
65
|
+
self.BINARY = sync_module.BINARY
|
|
66
|
+
self.NUMBER = sync_module.NUMBER
|
|
67
|
+
self.DATETIME = sync_module.DATETIME
|
|
68
|
+
self.ROWID = sync_module.ROWID
|
|
69
|
+
|
|
70
|
+
def connect(self, *arg: Any, **kw: Any) -> AsyncAdapt_pycubrid_connection:
|
|
71
|
+
creator_fn = kw.pop("async_creator_fn", self._aio_module.connect)
|
|
72
|
+
async_conn = await_only(creator_fn(*arg, **kw))
|
|
73
|
+
return AsyncAdapt_pycubrid_connection(self, async_conn)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PyCubridAsyncDialect(PyCubridDialect):
|
|
77
|
+
driver = "aiopycubrid"
|
|
78
|
+
is_async = True
|
|
79
|
+
supports_statement_cache = True
|
|
80
|
+
execution_ctx_cls = PyCubridExecutionContext
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def import_dbapi(cls) -> DBAPIModule:
|
|
84
|
+
import pycubrid.aio as aio_module
|
|
85
|
+
|
|
86
|
+
return cast(DBAPIModule, AsyncAdapt_pycubrid_dbapi(aio_module))
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def dbapi(cls) -> DBAPIModule: # type: ignore[override]
|
|
90
|
+
return cls.import_dbapi()
|
|
91
|
+
|
|
92
|
+
def create_connect_args(self, url: URL) -> ConnectArgsType:
|
|
93
|
+
if url is None:
|
|
94
|
+
raise ValueError("Unexpected database URL format")
|
|
95
|
+
|
|
96
|
+
opts = url.translate_connect_args(username="user", database="database")
|
|
97
|
+
return (), {
|
|
98
|
+
"host": opts.get("host", "localhost"),
|
|
99
|
+
"port": opts.get("port", 33000),
|
|
100
|
+
"database": opts.get("database", ""),
|
|
101
|
+
"user": opts.get("user", "dba"),
|
|
102
|
+
"password": opts.get("password", ""),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
def on_connect(self) -> Callable[[Any], None] | None:
|
|
106
|
+
isolation_level = self.isolation_level
|
|
107
|
+
|
|
108
|
+
def connect(conn: Any) -> None:
|
|
109
|
+
conn.autocommit = False
|
|
110
|
+
if isolation_level is not None:
|
|
111
|
+
self.set_isolation_level(conn, isolation_level)
|
|
112
|
+
|
|
113
|
+
return connect
|
|
114
|
+
|
|
115
|
+
def do_ping(self, dbapi_connection: Any) -> bool:
|
|
116
|
+
cursor = dbapi_connection.cursor()
|
|
117
|
+
try:
|
|
118
|
+
cursor.execute("SELECT 1")
|
|
119
|
+
cursor.fetchone()
|
|
120
|
+
finally:
|
|
121
|
+
cursor.close()
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
dialect = PyCubridAsyncDialect
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
-
from sqlalchemy.sql import compiler
|
|
14
|
+
from sqlalchemy.sql import compiler, elements
|
|
15
15
|
from sqlalchemy.sql import sqltypes
|
|
16
16
|
|
|
17
17
|
|
|
@@ -320,6 +320,56 @@ class CubridCompiler(compiler.SQLCompiler):
|
|
|
320
320
|
return "REPLACE" + text[len("INSERT") :]
|
|
321
321
|
return text
|
|
322
322
|
|
|
323
|
+
def _render_json_extract_from_binary(
|
|
324
|
+
self, binary: elements.BinaryExpression[Any], operator: Any, **kw: Any
|
|
325
|
+
) -> str:
|
|
326
|
+
if binary.type._type_affinity is sqltypes.JSON:
|
|
327
|
+
return "JSON_EXTRACT(%s, %s)" % (
|
|
328
|
+
self.process(binary.left, **kw),
|
|
329
|
+
self.process(binary.right, **kw),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# When the target type is not JSON, convert JSON 'null' to SQL NULL
|
|
333
|
+
case_expression = "CASE JSON_EXTRACT(%s, %s) WHEN 'null' THEN NULL" % (
|
|
334
|
+
self.process(binary.left, **kw),
|
|
335
|
+
self.process(binary.right, **kw),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if binary.type._type_affinity is sqltypes.Boolean:
|
|
339
|
+
type_expression = (
|
|
340
|
+
"WHEN 'true' THEN 1 WHEN 'false' THEN 0 ELSE CAST(JSON_EXTRACT(%s, %s) AS INTEGER)"
|
|
341
|
+
) % (
|
|
342
|
+
self.process(binary.left, **kw),
|
|
343
|
+
self.process(binary.right, **kw),
|
|
344
|
+
)
|
|
345
|
+
elif binary.type._type_affinity is sqltypes.Integer:
|
|
346
|
+
type_expression = "ELSE CAST(JSON_EXTRACT(%s, %s) AS INTEGER)" % (
|
|
347
|
+
self.process(binary.left, **kw),
|
|
348
|
+
self.process(binary.right, **kw),
|
|
349
|
+
)
|
|
350
|
+
elif binary.type._type_affinity is sqltypes.Numeric:
|
|
351
|
+
type_expression = "ELSE CAST(JSON_EXTRACT(%s, %s) AS DOUBLE)" % (
|
|
352
|
+
self.process(binary.left, **kw),
|
|
353
|
+
self.process(binary.right, **kw),
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
type_expression = "ELSE JSON_UNQUOTE(JSON_EXTRACT(%s, %s))" % (
|
|
357
|
+
self.process(binary.left, **kw),
|
|
358
|
+
self.process(binary.right, **kw),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return case_expression + " " + type_expression + " END"
|
|
362
|
+
|
|
363
|
+
def visit_json_getitem_op_binary(
|
|
364
|
+
self, binary: elements.BinaryExpression[Any], operator: Any, **kw: Any
|
|
365
|
+
) -> str:
|
|
366
|
+
return self._render_json_extract_from_binary(binary, operator, **kw)
|
|
367
|
+
|
|
368
|
+
def visit_json_path_getitem_op_binary(
|
|
369
|
+
self, binary: elements.BinaryExpression[Any], operator: Any, **kw: Any
|
|
370
|
+
) -> str:
|
|
371
|
+
return self._render_json_extract_from_binary(binary, operator, **kw)
|
|
372
|
+
|
|
323
373
|
|
|
324
374
|
class CubridDDLCompiler(compiler.DDLCompiler):
|
|
325
375
|
"""DDLCompiler subclass for CUBRID.
|
|
@@ -534,3 +584,6 @@ class CubridTypeCompiler(compiler.GenericTypeCompiler):
|
|
|
534
584
|
else:
|
|
535
585
|
parts.append(value.__visit_name__)
|
|
536
586
|
return f"{collection_type}({','.join(parts)})"
|
|
587
|
+
|
|
588
|
+
def visit_JSON(self, type_: Any, **kw: Any) -> str:
|
|
589
|
+
return "JSON"
|
|
@@ -30,6 +30,7 @@ from sqlalchemy.engine.interfaces import (
|
|
|
30
30
|
from sqlalchemy.engine.url import URL
|
|
31
31
|
from sqlalchemy.sql import text
|
|
32
32
|
from sqlalchemy.sql.compiler import IdentifierPreparer
|
|
33
|
+
from sqlalchemy.sql.compiler import InsertmanyvaluesSentinelOpts
|
|
33
34
|
|
|
34
35
|
from sqlalchemy_cubrid.base import CubridExecutionContext, CubridIdentifierPreparer
|
|
35
36
|
from sqlalchemy_cubrid.compiler import (
|
|
@@ -47,6 +48,9 @@ from sqlalchemy_cubrid.types import (
|
|
|
47
48
|
DOUBLE,
|
|
48
49
|
DOUBLE_PRECISION,
|
|
49
50
|
FLOAT,
|
|
51
|
+
JSON,
|
|
52
|
+
JSONIndexType,
|
|
53
|
+
JSONPathType,
|
|
50
54
|
MULTISET,
|
|
51
55
|
NCHAR,
|
|
52
56
|
NUMERIC,
|
|
@@ -83,6 +87,9 @@ colspecs = {
|
|
|
83
87
|
sqltypes.Numeric: NUMERIC,
|
|
84
88
|
sqltypes.Float: FLOAT,
|
|
85
89
|
sqltypes.Time: TIME,
|
|
90
|
+
sqltypes.JSON: JSON,
|
|
91
|
+
sqltypes.JSON.JSONIndexType: JSONIndexType,
|
|
92
|
+
sqltypes.JSON.JSONPathType: JSONPathType,
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
# ischema_names maps CUBRID type names from SHOW COLUMNS to SA types.
|
|
@@ -119,6 +126,8 @@ ischema_names = {
|
|
|
119
126
|
"SET": SET,
|
|
120
127
|
"MULTISET": MULTISET,
|
|
121
128
|
"SEQUENCE": SEQUENCE,
|
|
129
|
+
# JSON (CUBRID 10.2+)
|
|
130
|
+
"JSON": JSON,
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
|
|
@@ -177,6 +186,9 @@ class CubridDialect(default.DefaultDialect):
|
|
|
177
186
|
supports_default_metavalue = True
|
|
178
187
|
supports_empty_insert = True
|
|
179
188
|
supports_multivalues_insert = True
|
|
189
|
+
use_insertmanyvalues = True
|
|
190
|
+
use_insertmanyvalues_wo_returning = True
|
|
191
|
+
insertmanyvalues_implicit_sentinel = InsertmanyvaluesSentinelOpts.ANY_AUTOINCREMENT
|
|
180
192
|
supports_is_distinct_from = False
|
|
181
193
|
|
|
182
194
|
# RETURNING
|
|
@@ -427,12 +439,16 @@ class CubridDialect(default.DefaultDialect):
|
|
|
427
439
|
return [row[0] for row in result]
|
|
428
440
|
|
|
429
441
|
@reflection.cache
|
|
430
|
-
def get_view_definition(
|
|
442
|
+
def get_view_definition( # type: ignore[override]
|
|
443
|
+
self, connection: Any, view_name: str, schema: str | None = None, **kw: Any
|
|
444
|
+
) -> str:
|
|
431
445
|
"""Return the CREATE VIEW definition."""
|
|
432
446
|
quoted = self.identifier_preparer.quote_identifier(view_name)
|
|
433
447
|
result = connection.execute(text(f"SHOW CREATE VIEW {quoted}"))
|
|
434
448
|
row = result.fetchone()
|
|
435
|
-
|
|
449
|
+
if row is None:
|
|
450
|
+
return ""
|
|
451
|
+
return str(row[1])
|
|
436
452
|
|
|
437
453
|
@reflection.cache
|
|
438
454
|
def get_indexes(
|
|
@@ -664,7 +680,9 @@ class CubridDialect(default.DefaultDialect):
|
|
|
664
680
|
return self._ISOLATION_LEVEL_REVERSE.get(val, str(val))
|
|
665
681
|
return str(val)
|
|
666
682
|
|
|
667
|
-
def get_isolation_level_values(
|
|
683
|
+
def get_isolation_level_values( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
|
|
684
|
+
self, dbapi_conn: DBAPIConnection | None = None
|
|
685
|
+
) -> Sequence[str]:
|
|
668
686
|
"""Return the list of valid isolation level values."""
|
|
669
687
|
del dbapi_conn
|
|
670
688
|
return [
|
|
@@ -679,7 +697,11 @@ class CubridDialect(default.DefaultDialect):
|
|
|
679
697
|
"READ COMMITTED SCHEMA, READ UNCOMMITTED INSTANCES",
|
|
680
698
|
]
|
|
681
699
|
|
|
682
|
-
def set_isolation_level(
|
|
700
|
+
def set_isolation_level(
|
|
701
|
+
self,
|
|
702
|
+
dbapi_connection: DBAPIConnection,
|
|
703
|
+
level: str,
|
|
704
|
+
) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
683
705
|
"""Set the isolation level for *dbapi_conn*."""
|
|
684
706
|
# Note: do NOT unwrap dbapi_conn.connection — the inner C-level
|
|
685
707
|
# _cubrid.connection cursor cannot handle SET TRANSACTION SQL.
|
|
@@ -354,3 +354,92 @@ class OBJECT(sqltypes.TypeEngine[Any]):
|
|
|
354
354
|
"""
|
|
355
355
|
|
|
356
356
|
__visit_name__ = "OBJECT"
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# JSON Type (CUBRID 10.2+)
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class _FormatTypeMixin:
|
|
365
|
+
"""Mixin for formatting JSON path index/path values for bind parameters."""
|
|
366
|
+
|
|
367
|
+
def _format_value(self, value: Any) -> str:
|
|
368
|
+
raise NotImplementedError()
|
|
369
|
+
|
|
370
|
+
def bind_processor(self, dialect: Any) -> Any:
|
|
371
|
+
super_proc = self.string_bind_processor(dialect) # type: ignore[attr-defined] # noqa: E501
|
|
372
|
+
|
|
373
|
+
def process(value: Any) -> Any:
|
|
374
|
+
value = self._format_value(value)
|
|
375
|
+
if super_proc:
|
|
376
|
+
value = super_proc(value)
|
|
377
|
+
return value
|
|
378
|
+
|
|
379
|
+
return process
|
|
380
|
+
|
|
381
|
+
def literal_processor(self, dialect: Any) -> Any:
|
|
382
|
+
super_proc = self.string_literal_processor(dialect) # type: ignore[attr-defined] # noqa: E501
|
|
383
|
+
|
|
384
|
+
def process(value: Any) -> str:
|
|
385
|
+
value = self._format_value(value)
|
|
386
|
+
if super_proc:
|
|
387
|
+
value = super_proc(value)
|
|
388
|
+
return value
|
|
389
|
+
|
|
390
|
+
return process
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class JSONIndexType(_FormatTypeMixin, sqltypes.JSON.JSONIndexType):
|
|
394
|
+
"""CUBRID JSON index type for single-key access.
|
|
395
|
+
|
|
396
|
+
Converts Python index values to CUBRID JSON path syntax:
|
|
397
|
+
- Integer index: ``$[0]``
|
|
398
|
+
- String key: ``$."key"``
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
def _format_value(self, value: Any) -> str:
|
|
402
|
+
if isinstance(value, int):
|
|
403
|
+
return "$[%s]" % value
|
|
404
|
+
else:
|
|
405
|
+
return '$."%s"' % str(value).replace('"', '""')
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class JSONPathType(_FormatTypeMixin, sqltypes.JSON.JSONPathType):
|
|
409
|
+
"""CUBRID JSON path type for multi-level access.
|
|
410
|
+
|
|
411
|
+
Converts Python tuple paths to CUBRID JSON path syntax:
|
|
412
|
+
- ``("a", 1, "b")`` → ``$."a"[1]."b"``
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
def _format_value(self, value: Any) -> str:
|
|
416
|
+
return "$%s" % (
|
|
417
|
+
"".join(
|
|
418
|
+
"[%s]" % elem if isinstance(elem, int) else '."%s"' % str(elem).replace('"', '""')
|
|
419
|
+
for elem in value
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class JSON(sqltypes.JSON): # type: ignore[type-arg]
|
|
425
|
+
"""CUBRID JSON type.
|
|
426
|
+
|
|
427
|
+
CUBRID supports JSON as of version 10.2 (RFC 7159 compliant).
|
|
428
|
+
|
|
429
|
+
:class:`_cubrid.JSON` is used automatically whenever the base
|
|
430
|
+
:class:`_types.JSON` datatype is used against a CUBRID backend.
|
|
431
|
+
|
|
432
|
+
The :class:`.cubrid.JSON` type supports persistence of JSON values
|
|
433
|
+
as well as the core index operations provided by :class:`_types.JSON`
|
|
434
|
+
datatype, by adapting the operations to render the ``JSON_EXTRACT``
|
|
435
|
+
function at the database level.
|
|
436
|
+
|
|
437
|
+
.. seealso::
|
|
438
|
+
|
|
439
|
+
:class:`_types.JSON` - main documentation for the generic
|
|
440
|
+
cross-platform JSON datatype.
|
|
441
|
+
|
|
442
|
+
.. versionadded:: 1.2.0
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
__visit_name__ = "JSON"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-cubrid
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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
|
|
@@ -78,7 +78,7 @@ production-ready SQLAlchemy dialect that supports the modern 2.0–2.1 API.
|
|
|
78
78
|
- Tested against **4 CUBRID versions** (10.2, 11.0, 11.2, 11.4) across **Python 3.10 -- 3.14**
|
|
79
79
|
- CUBRID-specific DML constructs: `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`
|
|
80
80
|
- Alembic migration support out of the box
|
|
81
|
-
- **
|
|
81
|
+
- **Three driver options** — C-extension (`cubrid://`), pure Python (`cubrid+pycubrid://`), or async pure Python (`cubrid+aiopycubrid://`)
|
|
82
82
|
|
|
83
83
|
## Architecture
|
|
84
84
|
|
|
@@ -88,8 +88,10 @@ flowchart TD
|
|
|
88
88
|
sa --> dialect["CubridDialect"]
|
|
89
89
|
dialect --> pycubrid["pycubrid driver"]
|
|
90
90
|
dialect --> cext["CUBRIDdb driver"]
|
|
91
|
+
dialect --> aio["pycubrid.aio async driver"]
|
|
91
92
|
pycubrid --> server["CUBRID Server"]
|
|
92
93
|
cext --> server
|
|
94
|
+
aio --> server
|
|
93
95
|
```
|
|
94
96
|
|
|
95
97
|
```mermaid
|
|
@@ -163,15 +165,29 @@ with Session(engine) as session:
|
|
|
163
165
|
session.commit()
|
|
164
166
|
```
|
|
165
167
|
|
|
168
|
+
### Async
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
|
172
|
+
from sqlalchemy import text
|
|
173
|
+
|
|
174
|
+
engine = create_async_engine("cubrid+aiopycubrid://dba:password@localhost:33000/demodb")
|
|
175
|
+
|
|
176
|
+
async with AsyncSession(engine) as session:
|
|
177
|
+
result = await session.execute(text("SELECT 1"))
|
|
178
|
+
print(result.scalar())
|
|
179
|
+
```
|
|
180
|
+
|
|
166
181
|
## Features
|
|
167
182
|
|
|
168
|
-
- Complete type system -- numeric, string, date/time, bit, LOB, and
|
|
183
|
+
- Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
169
184
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
170
185
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
171
186
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
172
187
|
- Schema reflection -- tables, views, columns, PKs, FKs, indexes, unique constraints, comments
|
|
173
188
|
- Alembic migrations via `CubridImpl` (auto-discovered entry point)
|
|
174
189
|
- All 6 CUBRID isolation levels (dual-granularity: class-level + instance-level)
|
|
190
|
+
- Async support — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
175
191
|
|
|
176
192
|
## Documentation
|
|
177
193
|
|
|
@@ -187,6 +203,7 @@ with Session(engine) as session:
|
|
|
187
203
|
| [Development](docs/DEVELOPMENT.md) | Dev setup, testing, Docker, coverage, CI/CD |
|
|
188
204
|
| [Driver Compatibility](docs/DRIVER_COMPAT.md) | CUBRID-Python driver versions and known issues |
|
|
189
205
|
| [Troubleshooting](docs/TROUBLESHOOTING.md) | Common issues, error solutions, debugging techniques |
|
|
206
|
+
| [Async Connection](docs/CONNECTION.md#async-connection) | Async engine setup with `cubrid+aiopycubrid://` |
|
|
190
207
|
|
|
191
208
|
## Compatibility Matrix
|
|
192
209
|
|
|
@@ -233,13 +250,17 @@ stmt = insert(users).values(name="Alice").on_duplicate_key_update(name="Alice Up
|
|
|
233
250
|
|
|
234
251
|
### What's the difference between `cubrid://` and `cubrid+pycubrid://`?
|
|
235
252
|
|
|
236
|
-
`cubrid://` uses the C-extension driver (CUBRIDdb) which requires compilation. `cubrid+pycubrid://` uses the pure Python driver which installs with pip alone — no build tools needed.
|
|
253
|
+
`cubrid://` uses the C-extension driver (CUBRIDdb) which requires compilation. `cubrid+pycubrid://` uses the pure Python driver which installs with pip alone — no build tools needed. `cubrid+aiopycubrid://` uses the async variant of the pure Python driver for use with `create_async_engine` and `AsyncSession`.
|
|
254
|
+
|
|
255
|
+
### Does sqlalchemy-cubrid support async?
|
|
256
|
+
|
|
257
|
+
Yes. Use `create_async_engine("cubrid+aiopycubrid://...")` with the pycubrid async driver. Requires `pycubrid>=1.1.0`. All Core and ORM features work with `AsyncSession`.
|
|
237
258
|
|
|
238
259
|
|
|
239
260
|
## Related Projects
|
|
240
261
|
|
|
241
262
|
- [pycubrid](https://github.com/cubrid-labs/pycubrid) — Pure Python DB-API 2.0 driver for CUBRID
|
|
242
|
-
- [cubrid-python
|
|
263
|
+
- [cubrid-cookbook-python](https://github.com/cubrid-labs/cubrid-cookbook-python) — Production-ready Python examples for CUBRID
|
|
243
264
|
|
|
244
265
|
## Roadmap
|
|
245
266
|
|