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.
Files changed (44) hide show
  1. {sqlalchemy_cubrid-1.2.3/sqlalchemy_cubrid.egg-info → sqlalchemy_cubrid-1.4.0}/PKG-INFO +22 -7
  2. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/README.md +19 -4
  3. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/pyproject.toml +3 -3
  4. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/__init__.py +1 -1
  5. sqlalchemy_cubrid-1.4.0/sqlalchemy_cubrid/_compat.py +39 -0
  6. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/alembic_impl.py +34 -0
  7. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/base.py +3 -2
  8. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/compiler.py +28 -13
  9. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/dialect.py +39 -11
  10. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/pycubrid_dialect.py +14 -1
  11. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0/sqlalchemy_cubrid.egg-info}/PKG-INFO +22 -7
  12. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid.egg-info/SOURCES.txt +5 -0
  13. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid.egg-info/requires.txt +1 -1
  14. sqlalchemy_cubrid-1.4.0/test/test_alembic.py +675 -0
  15. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_compiler.py +69 -17
  16. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_dialect_offline.py +23 -1
  17. sqlalchemy_cubrid-1.4.0/test/test_logging.py +95 -0
  18. sqlalchemy_cubrid-1.4.0/test/test_packaging.py +169 -0
  19. sqlalchemy_cubrid-1.4.0/test/test_show_create_table.py +608 -0
  20. sqlalchemy_cubrid-1.4.0/test/test_stress_pool.py +260 -0
  21. sqlalchemy_cubrid-1.2.3/test/test_alembic.py +0 -301
  22. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/AUTHORS +0 -0
  23. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/LICENSE +0 -0
  24. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/setup.cfg +0 -0
  25. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/aio_pycubrid_dialect.py +0 -0
  26. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/dml.py +0 -0
  27. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/py.typed +0 -0
  28. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/requirements.py +0 -0
  29. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/trace.py +0 -0
  30. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid/types.py +0 -0
  31. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid.egg-info/dependency_links.txt +0 -0
  32. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid.egg-info/entry_points.txt +0 -0
  33. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/sqlalchemy_cubrid.egg-info/top_level.txt +0 -0
  34. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_aio_integration.py +0 -0
  35. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_aio_pycubrid_dialect.py +0 -0
  36. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_base.py +0 -0
  37. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_dialects.py +0 -0
  38. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_integration.py +0 -0
  39. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_json.py +0 -0
  40. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_pycubrid_dialect.py +0 -0
  41. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_requirements.py +0 -0
  42. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_suite.py +0 -0
  43. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.4.0}/test/test_trace.py +0 -0
  44. {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.2.3
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 :: 5 - Production/Stable
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 full type system support.
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
- - **426 offline tests** with **99%+ code coverage** — no database required to run them
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
- - Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
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 full type system support.
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
- - **426 offline tests** with **99%+ code coverage** — no database required to run them
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
- - Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
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.2.3"
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 :: 5 - Production/Stable",
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]
@@ -52,7 +52,7 @@ from sqlalchemy.sql.sqltypes import (
52
52
  TIMESTAMP,
53
53
  )
54
54
 
55
- __version__ = "1.2.3"
55
+ __version__ = "1.4.0"
56
56
 
57
57
  __all__ = (
58
58
  "insert",
@@ -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
- pass
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
- if select._for_update_arg is None:
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 select._for_update_arg.of:
83
- text += " OF " + ", ".join(self.process(col, **kw) for col in select._for_update_arg.of)
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._limit_clause
90
- offset_clause = select._offset_clause
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
- elif limit_clause is None and offset_clause is not None:
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
- elif offset_clause is not None:
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
- else:
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 coercions._is_literal(val):
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._with_binary_element_type(captured_column.type)
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 coercions, elements
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 coercions._is_literal(value):
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
- pass
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 does not expose check constraint expressions easily
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("SELECT COUNT(*) FROM _db_index WHERE index_name = :name"),
631
- {"name": index_name},
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
- # CUBRID server default is typically level 4
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
- return (), {
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