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.
Files changed (37) hide show
  1. {sqlalchemy_cubrid-1.0.0/sqlalchemy_cubrid.egg-info → sqlalchemy_cubrid-1.2.0}/PKG-INFO +26 -5
  2. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/README.md +25 -4
  3. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/pyproject.toml +2 -1
  4. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/__init__.py +7 -1
  5. sqlalchemy_cubrid-1.2.0/sqlalchemy_cubrid/aio_pycubrid_dialect.py +125 -0
  6. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/compiler.py +54 -1
  7. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/dialect.py +26 -4
  8. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/types.py +89 -0
  9. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0/sqlalchemy_cubrid.egg-info}/PKG-INFO +26 -5
  10. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/SOURCES.txt +3 -0
  11. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/entry_points.txt +1 -0
  12. sqlalchemy_cubrid-1.2.0/test/test_aio_pycubrid_dialect.py +206 -0
  13. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_dialect_offline.py +1 -1
  14. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_dialects.py +30 -0
  15. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_integration.py +33 -1
  16. sqlalchemy_cubrid-1.2.0/test/test_json.py +312 -0
  17. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_pycubrid_dialect.py +9 -2
  18. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/AUTHORS +0 -0
  19. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/LICENSE +0 -0
  20. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/setup.cfg +0 -0
  21. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/alembic_impl.py +0 -0
  22. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/base.py +0 -0
  23. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/dml.py +0 -0
  24. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/py.typed +0 -0
  25. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/pycubrid_dialect.py +0 -0
  26. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/requirements.py +0 -0
  27. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/trace.py +0 -0
  28. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/dependency_links.txt +0 -0
  29. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/requires.txt +0 -0
  30. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/top_level.txt +0 -0
  31. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_alembic.py +0 -0
  32. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_base.py +0 -0
  33. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_compiler.py +0 -0
  34. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_requirements.py +0 -0
  35. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_suite.py +0 -0
  36. {sqlalchemy_cubrid-1.0.0 → sqlalchemy_cubrid-1.2.0}/test/test_trace.py +0 -0
  37. {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.0.0
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
- - **Two driver options** — C-extension (`cubrid://`) or pure Python (`cubrid+pycubrid://`)
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 collection types
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-cookbook](https://github.com/cubrid-labs/cubrid-python-cookbook) — Production-ready Python examples for CUBRID
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
- - **Two driver options** — C-extension (`cubrid://`) or pure Python (`cubrid+pycubrid://`)
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 collection types
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-cookbook](https://github.com/cubrid-labs/cubrid-python-cookbook) — Production-ready Python examples for CUBRID
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.0.0"
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.0.0"
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(self, connection: Any, view_name: str, schema: str | None = None, **kw: Any) -> str | None: # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
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
- return row[1] if row else None
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(self, dbapi_conn: DBAPIConnection | None = None) -> Sequence[str]: # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride]
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(self, dbapi_connection: DBAPIConnection, level: str) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
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.0.0
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
- - **Two driver options** — C-extension (`cubrid://`) or pure Python (`cubrid+pycubrid://`)
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 collection types
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-cookbook](https://github.com/cubrid-labs/cubrid-python-cookbook) — Production-ready Python examples for CUBRID
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