sqlalchemy-cubrid 1.2.3__tar.gz → 1.3.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.3.0}/PKG-INFO +19 -6
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/README.md +17 -4
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/pyproject.toml +2 -2
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/__init__.py +1 -1
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/alembic_impl.py +34 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/base.py +3 -2
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/compiler.py +6 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/dialect.py +35 -11
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0/sqlalchemy_cubrid.egg-info}/PKG-INFO +19 -6
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid.egg-info/SOURCES.txt +2 -0
- sqlalchemy_cubrid-1.3.0/test/test_alembic.py +675 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_compiler.py +29 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_dialect_offline.py +23 -1
- sqlalchemy_cubrid-1.3.0/test/test_packaging.py +169 -0
- sqlalchemy_cubrid-1.3.0/test/test_show_create_table.py +608 -0
- sqlalchemy_cubrid-1.2.3/test/test_alembic.py +0 -301
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/AUTHORS +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/LICENSE +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/setup.cfg +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/aio_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/dml.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/py.typed +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/requirements.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/trace.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/types.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid.egg-info/entry_points.txt +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid.egg-info/requires.txt +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid.egg-info/top_level.txt +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_aio_integration.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_aio_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_base.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_dialects.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_integration.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_json.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_requirements.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_suite.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_trace.py +0 -0
- {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.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.3.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
|
|
@@ -50,7 +50,7 @@ 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,7 @@ 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
|
+
- **577 offline tests** with **99%+ code coverage** — no database required to run them
|
|
79
81
|
- Tested against **4 CUBRID versions** (10.2, 11.0, 11.2, 11.4) across **Python 3.10 -- 3.14**
|
|
80
82
|
- CUBRID-specific DML constructs: `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`
|
|
81
83
|
- Alembic migration support out of the box
|
|
@@ -181,14 +183,23 @@ async with AsyncSession(engine) as session:
|
|
|
181
183
|
|
|
182
184
|
## Features
|
|
183
185
|
|
|
184
|
-
-
|
|
186
|
+
- Type mapping for SQLAlchemy standard and CUBRID-specific types — numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
185
187
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
186
188
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
187
189
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
188
190
|
- Schema reflection -- tables, views, columns, PKs, FKs, indexes, unique constraints, comments
|
|
189
191
|
- Alembic migrations via `CubridImpl` (auto-discovered entry point)
|
|
190
192
|
- All 6 CUBRID isolation levels (dual-granularity: class-level + instance-level)
|
|
191
|
-
- Async support — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
193
|
+
- Async support (available since v1.1.0) — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
194
|
+
|
|
195
|
+
## Known Limitations
|
|
196
|
+
|
|
197
|
+
- **No `RETURNING`** — `INSERT/UPDATE/DELETE ... RETURNING` not supported; use `cursor.lastrowid` or `LAST_INSERT_ID()`
|
|
198
|
+
- **No sequences** — CUBRID uses `AUTO_INCREMENT` only
|
|
199
|
+
- **No multi-schema** — single schema per database
|
|
200
|
+
- **DDL auto-commits** — migrations are not transactional (`transactional_ddl = False`)
|
|
201
|
+
- **SQLAlchemy 2.0–2.1 only** — pinned to `<2.2` due to internal API dependencies ([details](docs/ARCHITECTURE.md))
|
|
202
|
+
- **Async requires pycubrid ≥ 1.1.0** — the `cubrid+aiopycubrid://` driver needs the async-capable pycubrid
|
|
192
203
|
|
|
193
204
|
## Documentation
|
|
194
205
|
|
|
@@ -214,6 +225,8 @@ async with AsyncSession(engine) as session:
|
|
|
214
225
|
| CUBRID | 10.2, 11.0, 11.2, 11.4 |
|
|
215
226
|
| SQLAlchemy | 2.0–2.1 |
|
|
216
227
|
| Alembic | >=1.7 |
|
|
228
|
+
| pycubrid (sync) | >=1.2.0 |
|
|
229
|
+
| pycubrid (async) | >=1.2.0 |
|
|
217
230
|
|
|
218
231
|
## FAQ
|
|
219
232
|
|
|
@@ -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,7 @@ 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
|
+
- **577 offline tests** with **99%+ code coverage** — no database required to run them
|
|
29
31
|
- Tested against **4 CUBRID versions** (10.2, 11.0, 11.2, 11.4) across **Python 3.10 -- 3.14**
|
|
30
32
|
- CUBRID-specific DML constructs: `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`
|
|
31
33
|
- Alembic migration support out of the box
|
|
@@ -131,14 +133,23 @@ async with AsyncSession(engine) as session:
|
|
|
131
133
|
|
|
132
134
|
## Features
|
|
133
135
|
|
|
134
|
-
-
|
|
136
|
+
- Type mapping for SQLAlchemy standard and CUBRID-specific types — numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
135
137
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
136
138
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
137
139
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
138
140
|
- Schema reflection -- tables, views, columns, PKs, FKs, indexes, unique constraints, comments
|
|
139
141
|
- Alembic migrations via `CubridImpl` (auto-discovered entry point)
|
|
140
142
|
- All 6 CUBRID isolation levels (dual-granularity: class-level + instance-level)
|
|
141
|
-
- Async support — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
143
|
+
- Async support (available since v1.1.0) — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
144
|
+
|
|
145
|
+
## Known Limitations
|
|
146
|
+
|
|
147
|
+
- **No `RETURNING`** — `INSERT/UPDATE/DELETE ... RETURNING` not supported; use `cursor.lastrowid` or `LAST_INSERT_ID()`
|
|
148
|
+
- **No sequences** — CUBRID uses `AUTO_INCREMENT` only
|
|
149
|
+
- **No multi-schema** — single schema per database
|
|
150
|
+
- **DDL auto-commits** — migrations are not transactional (`transactional_ddl = False`)
|
|
151
|
+
- **SQLAlchemy 2.0–2.1 only** — pinned to `<2.2` due to internal API dependencies ([details](docs/ARCHITECTURE.md))
|
|
152
|
+
- **Async requires pycubrid ≥ 1.1.0** — the `cubrid+aiopycubrid://` driver needs the async-capable pycubrid
|
|
142
153
|
|
|
143
154
|
## Documentation
|
|
144
155
|
|
|
@@ -164,6 +175,8 @@ async with AsyncSession(engine) as session:
|
|
|
164
175
|
| CUBRID | 10.2, 11.0, 11.2, 11.4 |
|
|
165
176
|
| SQLAlchemy | 2.0–2.1 |
|
|
166
177
|
| Alembic | >=1.7 |
|
|
178
|
+
| pycubrid (sync) | >=1.2.0 |
|
|
179
|
+
| pycubrid (async) | >=1.2.0 |
|
|
167
180
|
|
|
168
181
|
## FAQ
|
|
169
182
|
|
|
@@ -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.3.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",
|
|
@@ -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,6 +11,7 @@ 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
|
|
|
@@ -56,6 +57,8 @@ class CubridCompiler(compiler.SQLCompiler):
|
|
|
56
57
|
**kwargs: Any,
|
|
57
58
|
) -> str:
|
|
58
59
|
# https://www.cubrid.org/manual/en/11.0/sql/query/select.html#join-query
|
|
60
|
+
if getattr(join, "full", False):
|
|
61
|
+
raise CompileError("CUBRID does not support FULL OUTER JOIN")
|
|
59
62
|
return "".join(
|
|
60
63
|
(
|
|
61
64
|
self.process(join.left, asfrom=True, **kwargs),
|
|
@@ -66,6 +69,9 @@ class CubridCompiler(compiler.SQLCompiler):
|
|
|
66
69
|
)
|
|
67
70
|
)
|
|
68
71
|
|
|
72
|
+
def visit_lateral(self, lateral_: Any, **kw: Any) -> str:
|
|
73
|
+
raise CompileError("CUBRID does not support LATERAL")
|
|
74
|
+
|
|
69
75
|
def for_update_clause(self, select: Any, **kw: Any) -> str: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
70
76
|
"""Render FOR UPDATE clause.
|
|
71
77
|
|
|
@@ -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
|
|
@@ -341,6 +344,7 @@ class CubridDialect(default.DefaultDialect):
|
|
|
341
344
|
)
|
|
342
345
|
comment_map = {row[0]: row[1] for row in comment_result}
|
|
343
346
|
except Exception:
|
|
347
|
+
log.debug("Column comment query failed for %s", table_name, exc_info=True)
|
|
344
348
|
comment_map = {}
|
|
345
349
|
|
|
346
350
|
for column in columns:
|
|
@@ -380,7 +384,7 @@ class CubridDialect(default.DefaultDialect):
|
|
|
380
384
|
if row:
|
|
381
385
|
constraint_name = row[0]
|
|
382
386
|
except Exception: # nosec B110 — constraint name is optional metadata
|
|
383
|
-
|
|
387
|
+
log.debug("PK constraint name query failed for %s", table_name, exc_info=True)
|
|
384
388
|
|
|
385
389
|
return {
|
|
386
390
|
"name": constraint_name,
|
|
@@ -409,6 +413,11 @@ class CubridDialect(default.DefaultDialect):
|
|
|
409
413
|
result = connection.execute(text(f"SHOW CREATE TABLE {quoted}"))
|
|
410
414
|
row = result.fetchone()
|
|
411
415
|
except Exception: # nosec B110 — graceful fallback when DDL unavailable
|
|
416
|
+
log.warning(
|
|
417
|
+
"SHOW CREATE TABLE failed for %s; foreign keys will be empty",
|
|
418
|
+
table_name,
|
|
419
|
+
exc_info=True,
|
|
420
|
+
)
|
|
412
421
|
return foreign_keys
|
|
413
422
|
if row is None:
|
|
414
423
|
return foreign_keys
|
|
@@ -425,10 +434,16 @@ class CubridDialect(default.DefaultDialect):
|
|
|
425
434
|
referred_columns = [
|
|
426
435
|
col.strip() for col in _RE_BRACKET_IDENT.findall(fk_match.group("ref_cols"))
|
|
427
436
|
]
|
|
437
|
+
options: dict[str, str] = {}
|
|
438
|
+
if fk_match.group("ondelete"):
|
|
439
|
+
options["ondelete"] = fk_match.group("ondelete").upper()
|
|
440
|
+
if fk_match.group("onupdate"):
|
|
441
|
+
options["onupdate"] = fk_match.group("onupdate").upper()
|
|
428
442
|
foreign_keys.append(
|
|
429
443
|
{
|
|
430
444
|
"name": constraint_name,
|
|
431
445
|
"constrained_columns": constrained_columns,
|
|
446
|
+
"options": options,
|
|
432
447
|
"referred_schema": schema,
|
|
433
448
|
"referred_table": ref_table,
|
|
434
449
|
"referred_columns": referred_columns,
|
|
@@ -553,6 +568,11 @@ class CubridDialect(default.DefaultDialect):
|
|
|
553
568
|
result = connection.execute(text(f"SHOW CREATE TABLE {quoted}"))
|
|
554
569
|
row = result.fetchone()
|
|
555
570
|
except Exception: # nosec B110 — graceful fallback when DDL unavailable
|
|
571
|
+
log.warning(
|
|
572
|
+
"SHOW CREATE TABLE failed for %s; unique constraints will be empty",
|
|
573
|
+
table_name,
|
|
574
|
+
exc_info=True,
|
|
575
|
+
)
|
|
556
576
|
return unique_constraints
|
|
557
577
|
if row is None:
|
|
558
578
|
return unique_constraints
|
|
@@ -574,7 +594,9 @@ class CubridDialect(default.DefaultDialect):
|
|
|
574
594
|
**kw: Any,
|
|
575
595
|
) -> list[ReflectedCheckConstraint]:
|
|
576
596
|
"""Return check constraints for *table_name*."""
|
|
577
|
-
# CUBRID
|
|
597
|
+
# CUBRID parses CHECK constraint syntax but does not enforce them
|
|
598
|
+
# at runtime (official CUBRID behavior). Reflecting them would be
|
|
599
|
+
# misleading, so we return an empty list.
|
|
578
600
|
return []
|
|
579
601
|
|
|
580
602
|
@reflection.cache
|
|
@@ -624,14 +646,18 @@ class CubridDialect(default.DefaultDialect):
|
|
|
624
646
|
schema: str | None = None,
|
|
625
647
|
**kw: Any,
|
|
626
648
|
) -> bool:
|
|
627
|
-
"""Check if an index exists on *table_name*."""
|
|
649
|
+
"""Check if an index named *index_name* exists on *table_name*."""
|
|
628
650
|
try:
|
|
629
651
|
result = connection.execute(
|
|
630
|
-
text(
|
|
631
|
-
|
|
652
|
+
text(
|
|
653
|
+
"SELECT COUNT(*) FROM _db_index "
|
|
654
|
+
"WHERE class_of.class_name = :table AND index_name = :name"
|
|
655
|
+
),
|
|
656
|
+
{"table": table_name, "name": index_name},
|
|
632
657
|
)
|
|
633
658
|
return bool(result.scalar())
|
|
634
659
|
except Exception:
|
|
660
|
+
log.debug("has_index query failed for %s.%s", table_name, index_name, exc_info=True)
|
|
635
661
|
return False
|
|
636
662
|
|
|
637
663
|
def has_sequence(
|
|
@@ -708,7 +734,7 @@ class CubridDialect(default.DefaultDialect):
|
|
|
708
734
|
cursor.execute("SELECT X")
|
|
709
735
|
row = cursor.fetchone()
|
|
710
736
|
if row is None:
|
|
711
|
-
return "READ COMMITTED"
|
|
737
|
+
return "REPEATABLE READ SCHEMA, READ COMMITTED INSTANCES"
|
|
712
738
|
val = row[0]
|
|
713
739
|
cursor.close()
|
|
714
740
|
# CUBRID returns numeric level; map to string for SA
|
|
@@ -755,10 +781,8 @@ class CubridDialect(default.DefaultDialect):
|
|
|
755
781
|
cursor.close()
|
|
756
782
|
|
|
757
783
|
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")
|
|
784
|
+
"""Revert isolation level to the CUBRID default (level 4)."""
|
|
785
|
+
self.set_isolation_level(dbapi_conn, "REPEATABLE READ SCHEMA, READ COMMITTED INSTANCES")
|
|
762
786
|
|
|
763
787
|
def do_release_savepoint(self, connection: Any, name: str) -> None:
|
|
764
788
|
"""CUBRID does not support RELEASE SAVEPOINT; no-op."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-cubrid
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.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
|
|
@@ -50,7 +50,7 @@ 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,7 @@ 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
|
+
- **577 offline tests** with **99%+ code coverage** — no database required to run them
|
|
79
81
|
- Tested against **4 CUBRID versions** (10.2, 11.0, 11.2, 11.4) across **Python 3.10 -- 3.14**
|
|
80
82
|
- CUBRID-specific DML constructs: `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`
|
|
81
83
|
- Alembic migration support out of the box
|
|
@@ -181,14 +183,23 @@ async with AsyncSession(engine) as session:
|
|
|
181
183
|
|
|
182
184
|
## Features
|
|
183
185
|
|
|
184
|
-
-
|
|
186
|
+
- Type mapping for SQLAlchemy standard and CUBRID-specific types — numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
185
187
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
186
188
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
187
189
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
188
190
|
- Schema reflection -- tables, views, columns, PKs, FKs, indexes, unique constraints, comments
|
|
189
191
|
- Alembic migrations via `CubridImpl` (auto-discovered entry point)
|
|
190
192
|
- All 6 CUBRID isolation levels (dual-granularity: class-level + instance-level)
|
|
191
|
-
- Async support — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
193
|
+
- Async support (available since v1.1.0) — `create_async_engine("cubrid+aiopycubrid://...")` via pycubrid.aio
|
|
194
|
+
|
|
195
|
+
## Known Limitations
|
|
196
|
+
|
|
197
|
+
- **No `RETURNING`** — `INSERT/UPDATE/DELETE ... RETURNING` not supported; use `cursor.lastrowid` or `LAST_INSERT_ID()`
|
|
198
|
+
- **No sequences** — CUBRID uses `AUTO_INCREMENT` only
|
|
199
|
+
- **No multi-schema** — single schema per database
|
|
200
|
+
- **DDL auto-commits** — migrations are not transactional (`transactional_ddl = False`)
|
|
201
|
+
- **SQLAlchemy 2.0–2.1 only** — pinned to `<2.2` due to internal API dependencies ([details](docs/ARCHITECTURE.md))
|
|
202
|
+
- **Async requires pycubrid ≥ 1.1.0** — the `cubrid+aiopycubrid://` driver needs the async-capable pycubrid
|
|
192
203
|
|
|
193
204
|
## Documentation
|
|
194
205
|
|
|
@@ -214,6 +225,8 @@ async with AsyncSession(engine) as session:
|
|
|
214
225
|
| CUBRID | 10.2, 11.0, 11.2, 11.4 |
|
|
215
226
|
| SQLAlchemy | 2.0–2.1 |
|
|
216
227
|
| Alembic | >=1.7 |
|
|
228
|
+
| pycubrid (sync) | >=1.2.0 |
|
|
229
|
+
| pycubrid (async) | >=1.2.0 |
|
|
217
230
|
|
|
218
231
|
## FAQ
|
|
219
232
|
|
|
@@ -29,8 +29,10 @@ test/test_dialect_offline.py
|
|
|
29
29
|
test/test_dialects.py
|
|
30
30
|
test/test_integration.py
|
|
31
31
|
test/test_json.py
|
|
32
|
+
test/test_packaging.py
|
|
32
33
|
test/test_pycubrid_dialect.py
|
|
33
34
|
test/test_requirements.py
|
|
35
|
+
test/test_show_create_table.py
|
|
34
36
|
test/test_suite.py
|
|
35
37
|
test/test_trace.py
|
|
36
38
|
test/test_types.py
|