sqlalchemy-cubrid 1.2.3__tar.gz → 1.4.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.2.3/sqlalchemy_cubrid.egg-info → sqlalchemy_cubrid-1.4.0}/PKG-INFO +22 -7
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/README.md +19 -4
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/pyproject.toml +3 -3
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/__init__.py +1 -1
- sqlalchemy_cubrid-1.4.0/sqlalchemy_cubrid/_compat.py +39 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/alembic_impl.py +34 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/base.py +3 -2
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/compiler.py +28 -13
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/dialect.py +39 -11
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/pycubrid_dialect.py +14 -1
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0/sqlalchemy_cubrid.egg-info}/PKG-INFO +22 -7
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid.egg-info/SOURCES.txt +5 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid.egg-info/requires.txt +1 -1
- sqlalchemy_cubrid-1.4.0/test/test_alembic.py +675 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_compiler.py +69 -17
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_dialect_offline.py +23 -1
- sqlalchemy_cubrid-1.4.0/test/test_logging.py +95 -0
- sqlalchemy_cubrid-1.4.0/test/test_packaging.py +169 -0
- sqlalchemy_cubrid-1.4.0/test/test_show_create_table.py +608 -0
- sqlalchemy_cubrid-1.4.0/test/test_stress_pool.py +260 -0
- sqlalchemy_cubrid-1.2.3/test/test_alembic.py +0 -301
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/AUTHORS +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/LICENSE +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/setup.cfg +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/aio_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/dml.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/py.typed +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/requirements.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/trace.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/types.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid.egg-info/entry_points.txt +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid.egg-info/top_level.txt +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_aio_integration.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_aio_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_base.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_dialects.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_integration.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_json.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_requirements.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_suite.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_trace.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.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.4.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
|
|
@@ -10,7 +10,7 @@ Project-URL: Documentation, https://github.com/cubrid-labs/sqlalchemy-cubrid/tre
|
|
|
10
10
|
Project-URL: Changelog, https://github.com/cubrid-labs/sqlalchemy-cubrid/blob/main/CHANGELOG.md
|
|
11
11
|
Project-URL: Issues, https://github.com/cubrid-labs/sqlalchemy-cubrid/issues
|
|
12
12
|
Keywords: SQLAlchemy,CUBRID,dialect
|
|
13
|
-
Classifier: Development Status ::
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
14
|
Classifier: Environment :: Console
|
|
15
15
|
Classifier: Intended Audience :: Developers
|
|
16
16
|
Classifier: Operating System :: OS Independent
|
|
@@ -45,12 +45,12 @@ Requires-Dist: alembic>=1.7; extra == "dev"
|
|
|
45
45
|
Provides-Extra: alembic
|
|
46
46
|
Requires-Dist: alembic>=1.7; extra == "alembic"
|
|
47
47
|
Provides-Extra: pycubrid
|
|
48
|
-
Requires-Dist: pycubrid<2.0,>=1.0; extra == "pycubrid"
|
|
48
|
+
Requires-Dist: pycubrid<2.0,>=1.2.0; extra == "pycubrid"
|
|
49
49
|
Dynamic: license-file
|
|
50
50
|
|
|
51
51
|
# sqlalchemy-cubrid
|
|
52
52
|
|
|
53
|
-
**SQLAlchemy 2.0–2.1 dialect for the CUBRID database** — Python ORM, schema reflection, Alembic migrations, and
|
|
53
|
+
**SQLAlchemy 2.0–2.1 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
|
|
|
@@ -66,6 +66,8 @@ Dynamic: license-file
|
|
|
66
66
|
|
|
67
67
|
---
|
|
68
68
|
|
|
69
|
+
> **Status: Beta.** The core public API follows semantic versioning; minor releases may add features and bug fixes while the project remains under active development.
|
|
70
|
+
|
|
69
71
|
## Why sqlalchemy-cubrid?
|
|
70
72
|
|
|
71
73
|
CUBRID is a high-performance open-source relational database, widely adopted in
|
|
@@ -75,7 +77,9 @@ production-ready SQLAlchemy dialect that supports the modern 2.0–2.1 API.
|
|
|
75
77
|
**sqlalchemy-cubrid** bridges that gap:
|
|
76
78
|
|
|
77
79
|
- Full SQLAlchemy 2.0–2.1 dialect with **statement caching** and **PEP 561 typing**
|
|
78
|
-
- **
|
|
80
|
+
- **597 offline tests** with **99%+ code coverage** — no database required to run them
|
|
81
|
+
- **Concurrency stress tests** — `QueuePool` sync threaded + asyncio.gather workloads validated against live CUBRID
|
|
82
|
+
- **SQLAlchemy 2.2-ready compat shim** — private API access wrapped in `_compat.py` (still pinned `<2.2` until full SA 2.2 validation)
|
|
79
83
|
- Tested against **4 CUBRID versions** (10.2, 11.0, 11.2, 11.4) across **Python 3.10 -- 3.14**
|
|
80
84
|
- CUBRID-specific DML constructs: `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`
|
|
81
85
|
- Alembic migration support out of the box
|
|
@@ -181,14 +185,23 @@ async with AsyncSession(engine) as session:
|
|
|
181
185
|
|
|
182
186
|
## Features
|
|
183
187
|
|
|
184
|
-
-
|
|
188
|
+
- Type mapping for SQLAlchemy standard and CUBRID-specific types — numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
185
189
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
186
190
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
187
191
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
188
192
|
- Schema reflection -- tables, views, columns, PKs, FKs, indexes, unique constraints, comments
|
|
189
193
|
- Alembic migrations via `CubridImpl` (auto-discovered entry point)
|
|
190
194
|
- All 6 CUBRID isolation levels (dual-granularity: class-level + instance-level)
|
|
191
|
-
- Async support — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
195
|
+
- Async support (available since v1.1.0) — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
196
|
+
|
|
197
|
+
## Known Limitations
|
|
198
|
+
|
|
199
|
+
- **No `RETURNING`** — `INSERT/UPDATE/DELETE ... RETURNING` not supported; use `cursor.lastrowid` or `LAST_INSERT_ID()`
|
|
200
|
+
- **No sequences** — CUBRID uses `AUTO_INCREMENT` only
|
|
201
|
+
- **No multi-schema** — single schema per database
|
|
202
|
+
- **DDL auto-commits** — migrations are not transactional (`transactional_ddl = False`)
|
|
203
|
+
- **SQLAlchemy 2.0–2.1 only** — pinned to `<2.2` due to internal API dependencies ([details](docs/ARCHITECTURE.md))
|
|
204
|
+
- **Async requires pycubrid ≥ 1.1.0** — the `cubrid+aiopycubrid://` driver needs the async-capable pycubrid
|
|
192
205
|
|
|
193
206
|
## Documentation
|
|
194
207
|
|
|
@@ -214,6 +227,8 @@ async with AsyncSession(engine) as session:
|
|
|
214
227
|
| CUBRID | 10.2, 11.0, 11.2, 11.4 |
|
|
215
228
|
| SQLAlchemy | 2.0–2.1 |
|
|
216
229
|
| Alembic | >=1.7 |
|
|
230
|
+
| pycubrid (sync) | >=1.2.0 |
|
|
231
|
+
| pycubrid (async) | >=1.2.0 |
|
|
217
232
|
|
|
218
233
|
## FAQ
|
|
219
234
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# sqlalchemy-cubrid
|
|
2
2
|
|
|
3
|
-
**SQLAlchemy 2.0–2.1 dialect for the CUBRID database** — Python ORM, schema reflection, Alembic migrations, and
|
|
3
|
+
**SQLAlchemy 2.0–2.1 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
|
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
> **Status: Beta.** The core public API follows semantic versioning; minor releases may add features and bug fixes while the project remains under active development.
|
|
20
|
+
|
|
19
21
|
## Why sqlalchemy-cubrid?
|
|
20
22
|
|
|
21
23
|
CUBRID is a high-performance open-source relational database, widely adopted in
|
|
@@ -25,7 +27,9 @@ production-ready SQLAlchemy dialect that supports the modern 2.0–2.1 API.
|
|
|
25
27
|
**sqlalchemy-cubrid** bridges that gap:
|
|
26
28
|
|
|
27
29
|
- Full SQLAlchemy 2.0–2.1 dialect with **statement caching** and **PEP 561 typing**
|
|
28
|
-
- **
|
|
30
|
+
- **597 offline tests** with **99%+ code coverage** — no database required to run them
|
|
31
|
+
- **Concurrency stress tests** — `QueuePool` sync threaded + asyncio.gather workloads validated against live CUBRID
|
|
32
|
+
- **SQLAlchemy 2.2-ready compat shim** — private API access wrapped in `_compat.py` (still pinned `<2.2` until full SA 2.2 validation)
|
|
29
33
|
- Tested against **4 CUBRID versions** (10.2, 11.0, 11.2, 11.4) across **Python 3.10 -- 3.14**
|
|
30
34
|
- CUBRID-specific DML constructs: `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`
|
|
31
35
|
- Alembic migration support out of the box
|
|
@@ -131,14 +135,23 @@ async with AsyncSession(engine) as session:
|
|
|
131
135
|
|
|
132
136
|
## Features
|
|
133
137
|
|
|
134
|
-
-
|
|
138
|
+
- Type mapping for SQLAlchemy standard and CUBRID-specific types — numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
135
139
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
136
140
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
137
141
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
138
142
|
- Schema reflection -- tables, views, columns, PKs, FKs, indexes, unique constraints, comments
|
|
139
143
|
- Alembic migrations via `CubridImpl` (auto-discovered entry point)
|
|
140
144
|
- All 6 CUBRID isolation levels (dual-granularity: class-level + instance-level)
|
|
141
|
-
- Async support — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
145
|
+
- Async support (available since v1.1.0) — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
146
|
+
|
|
147
|
+
## Known Limitations
|
|
148
|
+
|
|
149
|
+
- **No `RETURNING`** — `INSERT/UPDATE/DELETE ... RETURNING` not supported; use `cursor.lastrowid` or `LAST_INSERT_ID()`
|
|
150
|
+
- **No sequences** — CUBRID uses `AUTO_INCREMENT` only
|
|
151
|
+
- **No multi-schema** — single schema per database
|
|
152
|
+
- **DDL auto-commits** — migrations are not transactional (`transactional_ddl = False`)
|
|
153
|
+
- **SQLAlchemy 2.0–2.1 only** — pinned to `<2.2` due to internal API dependencies ([details](docs/ARCHITECTURE.md))
|
|
154
|
+
- **Async requires pycubrid ≥ 1.1.0** — the `cubrid+aiopycubrid://` driver needs the async-capable pycubrid
|
|
142
155
|
|
|
143
156
|
## Documentation
|
|
144
157
|
|
|
@@ -164,6 +177,8 @@ async with AsyncSession(engine) as session:
|
|
|
164
177
|
| CUBRID | 10.2, 11.0, 11.2, 11.4 |
|
|
165
178
|
| SQLAlchemy | 2.0–2.1 |
|
|
166
179
|
| Alembic | >=1.7 |
|
|
180
|
+
| pycubrid (sync) | >=1.2.0 |
|
|
181
|
+
| pycubrid (async) | >=1.2.0 |
|
|
167
182
|
|
|
168
183
|
## FAQ
|
|
169
184
|
|
|
@@ -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.4.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 :: 4 - Beta",
|
|
19
19
|
"Environment :: Console",
|
|
20
20
|
"Intended Audience :: Developers",
|
|
21
21
|
"Operating System :: OS Independent",
|
|
@@ -54,7 +54,7 @@ alembic = [
|
|
|
54
54
|
"alembic>=1.7",
|
|
55
55
|
]
|
|
56
56
|
pycubrid = [
|
|
57
|
-
"pycubrid>=1.0,<2.0",
|
|
57
|
+
"pycubrid>=1.2.0,<2.0",
|
|
58
58
|
]
|
|
59
59
|
|
|
60
60
|
[project.urls]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.sql import elements, visitors
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def is_literal_value(value: Any) -> bool:
|
|
9
|
+
"""Check if value is a plain Python literal (not a SQL element or schema object)."""
|
|
10
|
+
if isinstance(value, visitors.Visitable):
|
|
11
|
+
return False
|
|
12
|
+
if hasattr(value, "__clause_element__"):
|
|
13
|
+
return False
|
|
14
|
+
return True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def bind_with_type(element: elements.BindParameter[Any], type_: Any) -> elements.BindParameter[Any]:
|
|
18
|
+
"""Create a new BindParameter preserving all flags but with the given type."""
|
|
19
|
+
return elements.BindParameter(
|
|
20
|
+
element.key,
|
|
21
|
+
element.value,
|
|
22
|
+
type_=type_,
|
|
23
|
+
unique=element.unique,
|
|
24
|
+
expanding=element.expanding,
|
|
25
|
+
literal_execute=element.literal_execute,
|
|
26
|
+
isoutparam=element.isoutparam,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_for_update_arg(select: Any) -> Any | None:
|
|
31
|
+
return getattr(select, "_for_update_arg", None)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_limit_clause(select: Any) -> Any | None:
|
|
35
|
+
return getattr(select, "_limit_clause", None)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_offset_clause(select: Any) -> Any | None:
|
|
39
|
+
return getattr(select, "_offset_clause", None)
|
|
@@ -158,6 +158,40 @@ class CubridImpl(DefaultImpl):
|
|
|
158
158
|
|
|
159
159
|
return set(inspector_values) != set(metadata_values)
|
|
160
160
|
|
|
161
|
+
def alter_column(
|
|
162
|
+
self,
|
|
163
|
+
table_name: str,
|
|
164
|
+
column_name: str,
|
|
165
|
+
nullable: Any = None,
|
|
166
|
+
server_default: Any = False,
|
|
167
|
+
name: str | None = None,
|
|
168
|
+
type_: Any = None,
|
|
169
|
+
**kw: Any,
|
|
170
|
+
) -> None:
|
|
171
|
+
if type_ is not None:
|
|
172
|
+
raise NotImplementedError(
|
|
173
|
+
f"CUBRID does not support ALTER COLUMN TYPE for column "
|
|
174
|
+
f"'{column_name}' on table '{table_name}'. "
|
|
175
|
+
f"Use batch_alter_table() as a workaround: "
|
|
176
|
+
f"https://alembic.sqlalchemy.org/en/latest/batch.html"
|
|
177
|
+
)
|
|
178
|
+
if name is not None:
|
|
179
|
+
raise NotImplementedError(
|
|
180
|
+
f"CUBRID does not support RENAME COLUMN for column "
|
|
181
|
+
f"'{column_name}' on table '{table_name}'. "
|
|
182
|
+
f"Use batch_alter_table() as a workaround: "
|
|
183
|
+
f"https://alembic.sqlalchemy.org/en/latest/batch.html"
|
|
184
|
+
)
|
|
185
|
+
super().alter_column(
|
|
186
|
+
table_name,
|
|
187
|
+
column_name,
|
|
188
|
+
nullable=nullable,
|
|
189
|
+
server_default=server_default,
|
|
190
|
+
name=name,
|
|
191
|
+
type_=type_,
|
|
192
|
+
**kw,
|
|
193
|
+
)
|
|
194
|
+
|
|
161
195
|
@classmethod
|
|
162
196
|
def _is_unbounded_string_match(
|
|
163
197
|
cls, inspector_type: TypeEngine[Any], metadata_type: TypeEngine[Any]
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import logging
|
|
12
13
|
import re
|
|
13
14
|
from typing import Any, cast
|
|
14
15
|
|
|
15
16
|
from sqlalchemy.engine import default
|
|
16
17
|
from sqlalchemy.sql import compiler
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
19
20
|
AUTOCOMMIT_REGEXP = re.compile(
|
|
20
21
|
r"\s*(?:UPDATE|INSERT|CREATE|DELETE|DROP|ALTER|MERGE|TRUNCATE)", re.I | re.UNICODE
|
|
21
22
|
)
|
|
@@ -416,7 +417,7 @@ class CubridExecutionContext(default.DefaultExecutionContext):
|
|
|
416
417
|
last_id = raw_conn.get_last_insert_id()
|
|
417
418
|
return cast(int, None) if last_id is None else int(last_id) # pyright: ignore[reportInvalidCast]
|
|
418
419
|
except Exception: # nosec B110 — fallback to SQL when driver lacks method
|
|
419
|
-
|
|
420
|
+
log.debug("get_last_insert_id via driver failed, falling back to SQL", exc_info=True)
|
|
420
421
|
|
|
421
422
|
# Fallback: use SQL function
|
|
422
423
|
cursor = self.create_server_side_cursor()
|
|
@@ -11,9 +11,18 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from sqlalchemy.exc import CompileError
|
|
14
15
|
from sqlalchemy.sql import compiler, elements
|
|
15
16
|
from sqlalchemy.sql import sqltypes
|
|
16
17
|
|
|
18
|
+
from sqlalchemy_cubrid._compat import (
|
|
19
|
+
bind_with_type,
|
|
20
|
+
get_for_update_arg,
|
|
21
|
+
get_limit_clause,
|
|
22
|
+
get_offset_clause,
|
|
23
|
+
is_literal_value,
|
|
24
|
+
)
|
|
25
|
+
|
|
17
26
|
|
|
18
27
|
class CubridCompiler(compiler.SQLCompiler):
|
|
19
28
|
"""SQLCompiler subclass for CUBRID."""
|
|
@@ -56,6 +65,8 @@ class CubridCompiler(compiler.SQLCompiler):
|
|
|
56
65
|
**kwargs: Any,
|
|
57
66
|
) -> str:
|
|
58
67
|
# https://www.cubrid.org/manual/en/11.0/sql/query/select.html#join-query
|
|
68
|
+
if getattr(join, "full", False):
|
|
69
|
+
raise CompileError("CUBRID does not support FULL OUTER JOIN")
|
|
59
70
|
return "".join(
|
|
60
71
|
(
|
|
61
72
|
self.process(join.left, asfrom=True, **kwargs),
|
|
@@ -66,6 +77,9 @@ class CubridCompiler(compiler.SQLCompiler):
|
|
|
66
77
|
)
|
|
67
78
|
)
|
|
68
79
|
|
|
80
|
+
def visit_lateral(self, lateral_: Any, **kw: Any) -> str:
|
|
81
|
+
raise CompileError("CUBRID does not support LATERAL")
|
|
82
|
+
|
|
69
83
|
def for_update_clause(self, select: Any, **kw: Any) -> str: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
70
84
|
"""Render FOR UPDATE clause.
|
|
71
85
|
|
|
@@ -76,29 +90,30 @@ class CubridCompiler(compiler.SQLCompiler):
|
|
|
76
90
|
|
|
77
91
|
CUBRID does NOT support NOWAIT or SKIP LOCKED.
|
|
78
92
|
"""
|
|
79
|
-
|
|
93
|
+
for_update_arg = get_for_update_arg(select)
|
|
94
|
+
if for_update_arg is None:
|
|
80
95
|
return ""
|
|
81
96
|
text = " FOR UPDATE"
|
|
82
|
-
if
|
|
83
|
-
text += " OF " + ", ".join(self.process(col, **kw) for col in
|
|
97
|
+
if for_update_arg.of:
|
|
98
|
+
text += " OF " + ", ".join(self.process(col, **kw) for col in for_update_arg.of)
|
|
84
99
|
return text
|
|
85
100
|
|
|
86
101
|
def limit_clause(self, select: Any, **kw: Any) -> str:
|
|
87
102
|
# https://www.cubrid.org/manual/en/11.0/sql/query/select.html#limit-clause
|
|
88
103
|
# SA 2.0: _limit_clause / _offset_clause are ClauseElements, not raw ints.
|
|
89
|
-
limit_clause = select
|
|
90
|
-
offset_clause = select
|
|
104
|
+
limit_clause = get_limit_clause(select)
|
|
105
|
+
offset_clause = get_offset_clause(select)
|
|
91
106
|
if limit_clause is None and offset_clause is None:
|
|
92
107
|
return ""
|
|
93
|
-
|
|
108
|
+
if limit_clause is None:
|
|
109
|
+
assert offset_clause is not None
|
|
94
110
|
return " \n LIMIT %s, 1073741823" % (self.process(offset_clause, **kw),)
|
|
95
|
-
|
|
111
|
+
if offset_clause is not None:
|
|
96
112
|
return " \n LIMIT %s, %s" % (
|
|
97
113
|
self.process(offset_clause, **kw),
|
|
98
114
|
self.process(limit_clause, **kw),
|
|
99
115
|
)
|
|
100
|
-
|
|
101
|
-
return " \n LIMIT %s" % (self.process(limit_clause, **kw),)
|
|
116
|
+
return " \n LIMIT %s" % (self.process(limit_clause, **kw),)
|
|
102
117
|
|
|
103
118
|
def update_limit_clause(self, update_stmt: Any) -> str | None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
104
119
|
# https://www.cubrid.org/manual/en/11.0/sql/query/update.html
|
|
@@ -162,14 +177,14 @@ class CubridCompiler(compiler.SQLCompiler):
|
|
|
162
177
|
|
|
163
178
|
for column in (col for col in cols if col.key in on_duplicate_update):
|
|
164
179
|
val = on_duplicate_update[column.key]
|
|
165
|
-
if
|
|
180
|
+
if is_literal_value(val):
|
|
166
181
|
val = elements.BindParameter(None, val, type_=column.type)
|
|
167
182
|
value_text = self.process(val.self_group(), use_schema=False)
|
|
168
183
|
else:
|
|
169
184
|
|
|
170
185
|
def replace(element: Any, captured_column: Any = column, **kw: Any) -> Any | None:
|
|
171
186
|
if isinstance(element, elements.BindParameter) and element.type._isnull:
|
|
172
|
-
return element
|
|
187
|
+
return bind_with_type(element, captured_column.type)
|
|
173
188
|
elif (
|
|
174
189
|
isinstance(element, elements.ColumnClause)
|
|
175
190
|
and element.table is on_duplicate.inserted_alias
|
|
@@ -202,7 +217,7 @@ class CubridCompiler(compiler.SQLCompiler):
|
|
|
202
217
|
|
|
203
218
|
def visit_merge(self, merge_stmt: Any, **kw: Any) -> str:
|
|
204
219
|
from sqlalchemy import exc
|
|
205
|
-
from sqlalchemy.sql import
|
|
220
|
+
from sqlalchemy.sql import elements
|
|
206
221
|
|
|
207
222
|
target = merge_stmt._target
|
|
208
223
|
source = merge_stmt._using_source
|
|
@@ -240,7 +255,7 @@ class CubridCompiler(compiler.SQLCompiler):
|
|
|
240
255
|
return self.process(column_key, **kw)
|
|
241
256
|
|
|
242
257
|
def _render_value(value: Any, target_column: Any | None) -> str:
|
|
243
|
-
if
|
|
258
|
+
if is_literal_value(value):
|
|
244
259
|
value = elements.BindParameter(
|
|
245
260
|
None,
|
|
246
261
|
value,
|
|
@@ -88,7 +88,9 @@ _RE_PRECISION_SCALE = re.compile(r"\((\d+)(?:,\s*(\d+))?\)")
|
|
|
88
88
|
_RE_FOREIGN_KEY = re.compile(
|
|
89
89
|
r"CONSTRAINT\s+\[(?P<name>[^\]]+)\]\s+FOREIGN\s+KEY\s*"
|
|
90
90
|
r"\((?P<cols>[^)]+)\)\s+REFERENCES\s+"
|
|
91
|
-
r"\[(?P<ref_table>[^\]]+)\]\s*\((?P<ref_cols>[^)]+)\)"
|
|
91
|
+
r"\[(?P<ref_table>[^\]]+)\]\s*\((?P<ref_cols>[^)]+)\)"
|
|
92
|
+
r"(?:\s+ON\s+DELETE\s+(?P<ondelete>CASCADE|SET\s+NULL|NO\s+ACTION|RESTRICT))?"
|
|
93
|
+
r"(?:\s+ON\s+UPDATE\s+(?P<onupdate>CASCADE|SET\s+NULL|NO\s+ACTION|RESTRICT))?",
|
|
92
94
|
re.IGNORECASE,
|
|
93
95
|
)
|
|
94
96
|
# Parses ``CONSTRAINT [name] UNIQUE KEY ([col1], [col2])`` from
|
|
@@ -195,6 +197,7 @@ class CubridDialect(default.DefaultDialect):
|
|
|
195
197
|
supports_native_enum = False
|
|
196
198
|
supports_native_boolean = False # CUBRID uses SMALLINT for booleans
|
|
197
199
|
supports_native_decimal = True
|
|
200
|
+
supports_native_lateral = False
|
|
198
201
|
|
|
199
202
|
# Column options
|
|
200
203
|
supports_sequences = False
|
|
@@ -270,6 +273,10 @@ class CubridDialect(default.DefaultDialect):
|
|
|
270
273
|
|
|
271
274
|
def initialize(self, connection: Any) -> None:
|
|
272
275
|
super().initialize(connection)
|
|
276
|
+
log.debug(
|
|
277
|
+
"CUBRID dialect initialized: server_version=%s",
|
|
278
|
+
self.server_version_info,
|
|
279
|
+
)
|
|
273
280
|
|
|
274
281
|
# ----- Reflection methods -----
|
|
275
282
|
|
|
@@ -341,6 +348,7 @@ class CubridDialect(default.DefaultDialect):
|
|
|
341
348
|
)
|
|
342
349
|
comment_map = {row[0]: row[1] for row in comment_result}
|
|
343
350
|
except Exception:
|
|
351
|
+
log.debug("Column comment query failed for %s", table_name, exc_info=True)
|
|
344
352
|
comment_map = {}
|
|
345
353
|
|
|
346
354
|
for column in columns:
|
|
@@ -380,7 +388,7 @@ class CubridDialect(default.DefaultDialect):
|
|
|
380
388
|
if row:
|
|
381
389
|
constraint_name = row[0]
|
|
382
390
|
except Exception: # nosec B110 — constraint name is optional metadata
|
|
383
|
-
|
|
391
|
+
log.debug("PK constraint name query failed for %s", table_name, exc_info=True)
|
|
384
392
|
|
|
385
393
|
return {
|
|
386
394
|
"name": constraint_name,
|
|
@@ -409,6 +417,11 @@ class CubridDialect(default.DefaultDialect):
|
|
|
409
417
|
result = connection.execute(text(f"SHOW CREATE TABLE {quoted}"))
|
|
410
418
|
row = result.fetchone()
|
|
411
419
|
except Exception: # nosec B110 — graceful fallback when DDL unavailable
|
|
420
|
+
log.warning(
|
|
421
|
+
"SHOW CREATE TABLE failed for %s; foreign keys will be empty",
|
|
422
|
+
table_name,
|
|
423
|
+
exc_info=True,
|
|
424
|
+
)
|
|
412
425
|
return foreign_keys
|
|
413
426
|
if row is None:
|
|
414
427
|
return foreign_keys
|
|
@@ -425,10 +438,16 @@ class CubridDialect(default.DefaultDialect):
|
|
|
425
438
|
referred_columns = [
|
|
426
439
|
col.strip() for col in _RE_BRACKET_IDENT.findall(fk_match.group("ref_cols"))
|
|
427
440
|
]
|
|
441
|
+
options: dict[str, str] = {}
|
|
442
|
+
if fk_match.group("ondelete"):
|
|
443
|
+
options["ondelete"] = fk_match.group("ondelete").upper()
|
|
444
|
+
if fk_match.group("onupdate"):
|
|
445
|
+
options["onupdate"] = fk_match.group("onupdate").upper()
|
|
428
446
|
foreign_keys.append(
|
|
429
447
|
{
|
|
430
448
|
"name": constraint_name,
|
|
431
449
|
"constrained_columns": constrained_columns,
|
|
450
|
+
"options": options,
|
|
432
451
|
"referred_schema": schema,
|
|
433
452
|
"referred_table": ref_table,
|
|
434
453
|
"referred_columns": referred_columns,
|
|
@@ -553,6 +572,11 @@ class CubridDialect(default.DefaultDialect):
|
|
|
553
572
|
result = connection.execute(text(f"SHOW CREATE TABLE {quoted}"))
|
|
554
573
|
row = result.fetchone()
|
|
555
574
|
except Exception: # nosec B110 — graceful fallback when DDL unavailable
|
|
575
|
+
log.warning(
|
|
576
|
+
"SHOW CREATE TABLE failed for %s; unique constraints will be empty",
|
|
577
|
+
table_name,
|
|
578
|
+
exc_info=True,
|
|
579
|
+
)
|
|
556
580
|
return unique_constraints
|
|
557
581
|
if row is None:
|
|
558
582
|
return unique_constraints
|
|
@@ -574,7 +598,9 @@ class CubridDialect(default.DefaultDialect):
|
|
|
574
598
|
**kw: Any,
|
|
575
599
|
) -> list[ReflectedCheckConstraint]:
|
|
576
600
|
"""Return check constraints for *table_name*."""
|
|
577
|
-
# CUBRID
|
|
601
|
+
# CUBRID parses CHECK constraint syntax but does not enforce them
|
|
602
|
+
# at runtime (official CUBRID behavior). Reflecting them would be
|
|
603
|
+
# misleading, so we return an empty list.
|
|
578
604
|
return []
|
|
579
605
|
|
|
580
606
|
@reflection.cache
|
|
@@ -624,14 +650,18 @@ class CubridDialect(default.DefaultDialect):
|
|
|
624
650
|
schema: str | None = None,
|
|
625
651
|
**kw: Any,
|
|
626
652
|
) -> bool:
|
|
627
|
-
"""Check if an index exists on *table_name*."""
|
|
653
|
+
"""Check if an index named *index_name* exists on *table_name*."""
|
|
628
654
|
try:
|
|
629
655
|
result = connection.execute(
|
|
630
|
-
text(
|
|
631
|
-
|
|
656
|
+
text(
|
|
657
|
+
"SELECT COUNT(*) FROM _db_index "
|
|
658
|
+
"WHERE class_of.class_name = :table AND index_name = :name"
|
|
659
|
+
),
|
|
660
|
+
{"table": table_name, "name": index_name},
|
|
632
661
|
)
|
|
633
662
|
return bool(result.scalar())
|
|
634
663
|
except Exception:
|
|
664
|
+
log.debug("has_index query failed for %s.%s", table_name, index_name, exc_info=True)
|
|
635
665
|
return False
|
|
636
666
|
|
|
637
667
|
def has_sequence(
|
|
@@ -708,7 +738,7 @@ class CubridDialect(default.DefaultDialect):
|
|
|
708
738
|
cursor.execute("SELECT X")
|
|
709
739
|
row = cursor.fetchone()
|
|
710
740
|
if row is None:
|
|
711
|
-
return "READ COMMITTED"
|
|
741
|
+
return "REPEATABLE READ SCHEMA, READ COMMITTED INSTANCES"
|
|
712
742
|
val = row[0]
|
|
713
743
|
cursor.close()
|
|
714
744
|
# CUBRID returns numeric level; map to string for SA
|
|
@@ -755,10 +785,8 @@ class CubridDialect(default.DefaultDialect):
|
|
|
755
785
|
cursor.close()
|
|
756
786
|
|
|
757
787
|
def reset_isolation_level(self, dbapi_conn: DBAPIConnection) -> None:
|
|
758
|
-
"""Revert isolation level to the default."""
|
|
759
|
-
|
|
760
|
-
# (REPEATABLE READ SCHEMA, READ COMMITTED INSTANCES)
|
|
761
|
-
self.set_isolation_level(dbapi_conn, "READ COMMITTED")
|
|
788
|
+
"""Revert isolation level to the CUBRID default (level 4)."""
|
|
789
|
+
self.set_isolation_level(dbapi_conn, "REPEATABLE READ SCHEMA, READ COMMITTED INSTANCES")
|
|
762
790
|
|
|
763
791
|
def do_release_savepoint(self, connection: Any, name: str) -> None:
|
|
764
792
|
"""CUBRID does not support RELEASE SAVEPOINT; no-op."""
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import logging
|
|
12
13
|
from typing import Any, Callable, cast
|
|
13
14
|
|
|
14
15
|
from sqlalchemy.engine.interfaces import DBAPIConnection, DBAPIModule, ConnectArgsType
|
|
@@ -17,6 +18,8 @@ from sqlalchemy.engine.url import URL
|
|
|
17
18
|
from sqlalchemy_cubrid.base import CubridExecutionContext
|
|
18
19
|
from sqlalchemy_cubrid.dialect import CubridDialect
|
|
19
20
|
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
class PyCubridExecutionContext(CubridExecutionContext):
|
|
22
25
|
"""Execution context for pycubrid connections.
|
|
@@ -70,6 +73,7 @@ class PyCubridDialect(CubridDialect):
|
|
|
70
73
|
import pycubrid as dbapi_module
|
|
71
74
|
except ImportError as e:
|
|
72
75
|
raise e
|
|
76
|
+
log.debug("Loaded pycubrid DBAPI (version %s)", getattr(dbapi_module, "__version__", "?"))
|
|
73
77
|
return cast(DBAPIModule, dbapi_module) # pyright: ignore[reportInvalidCast]
|
|
74
78
|
|
|
75
79
|
# Keep legacy dbapi() for SA 1.x compat
|
|
@@ -88,13 +92,21 @@ class PyCubridDialect(CubridDialect):
|
|
|
88
92
|
raise ValueError("Unexpected database URL format")
|
|
89
93
|
|
|
90
94
|
opts = url.translate_connect_args(username="user", database="database")
|
|
91
|
-
|
|
95
|
+
kwargs = {
|
|
92
96
|
"host": opts.get("host", "localhost"),
|
|
93
97
|
"port": opts.get("port", 33000),
|
|
94
98
|
"database": opts.get("database", ""),
|
|
95
99
|
"user": opts.get("user", "dba"),
|
|
96
100
|
"password": opts.get("password", ""),
|
|
97
101
|
}
|
|
102
|
+
log.debug(
|
|
103
|
+
"connect args: host=%s port=%s database=%s user=%s",
|
|
104
|
+
kwargs["host"],
|
|
105
|
+
kwargs["port"],
|
|
106
|
+
kwargs["database"],
|
|
107
|
+
kwargs["user"],
|
|
108
|
+
)
|
|
109
|
+
return (), kwargs
|
|
98
110
|
|
|
99
111
|
def on_connect(self) -> Callable[[Any], None] | None:
|
|
100
112
|
"""Return a callable to set up a new pycubrid connection.
|
|
@@ -108,6 +120,7 @@ class PyCubridDialect(CubridDialect):
|
|
|
108
120
|
conn.autocommit = False
|
|
109
121
|
if isolation_level is not None:
|
|
110
122
|
self.set_isolation_level(conn, isolation_level)
|
|
123
|
+
log.debug("on_connect: autocommit=False isolation_level=%s", isolation_level)
|
|
111
124
|
|
|
112
125
|
return connect
|
|
113
126
|
|