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.
Files changed (41) hide show
  1. {sqlalchemy_cubrid-1.2.3/sqlalchemy_cubrid.egg-info → sqlalchemy_cubrid-1.3.0}/PKG-INFO +19 -6
  2. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/README.md +17 -4
  3. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/pyproject.toml +2 -2
  4. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/__init__.py +1 -1
  5. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/alembic_impl.py +34 -0
  6. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/base.py +3 -2
  7. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/compiler.py +6 -0
  8. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/dialect.py +35 -11
  9. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0/sqlalchemy_cubrid.egg-info}/PKG-INFO +19 -6
  10. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid.egg-info/SOURCES.txt +2 -0
  11. sqlalchemy_cubrid-1.3.0/test/test_alembic.py +675 -0
  12. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_compiler.py +29 -0
  13. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_dialect_offline.py +23 -1
  14. sqlalchemy_cubrid-1.3.0/test/test_packaging.py +169 -0
  15. sqlalchemy_cubrid-1.3.0/test/test_show_create_table.py +608 -0
  16. sqlalchemy_cubrid-1.2.3/test/test_alembic.py +0 -301
  17. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/AUTHORS +0 -0
  18. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/LICENSE +0 -0
  19. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/setup.cfg +0 -0
  20. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/aio_pycubrid_dialect.py +0 -0
  21. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/dml.py +0 -0
  22. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/py.typed +0 -0
  23. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/pycubrid_dialect.py +0 -0
  24. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/requirements.py +0 -0
  25. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/trace.py +0 -0
  26. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid/types.py +0 -0
  27. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid.egg-info/dependency_links.txt +0 -0
  28. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid.egg-info/entry_points.txt +0 -0
  29. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid.egg-info/requires.txt +0 -0
  30. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/sqlalchemy_cubrid.egg-info/top_level.txt +0 -0
  31. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_aio_integration.py +0 -0
  32. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_aio_pycubrid_dialect.py +0 -0
  33. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_base.py +0 -0
  34. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_dialects.py +0 -0
  35. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_integration.py +0 -0
  36. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_json.py +0 -0
  37. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_pycubrid_dialect.py +0 -0
  38. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_requirements.py +0 -0
  39. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_suite.py +0 -0
  40. {sqlalchemy_cubrid-1.2.3 → sqlalchemy_cubrid-1.3.0}/test/test_trace.py +0 -0
  41. {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.2.3
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 :: 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
@@ -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 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,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
- - **426 offline tests** with **99%+ code coverage** — no database required to run them
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
- - Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
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 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,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
- - **426 offline tests** with **99%+ code coverage** — no database required to run them
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
- - Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
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.2.3"
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 :: 5 - Production/Stable",
18
+ "Development Status :: 4 - Beta",
19
19
  "Environment :: Console",
20
20
  "Intended Audience :: Developers",
21
21
  "Operating System :: OS Independent",
@@ -52,7 +52,7 @@ from sqlalchemy.sql.sqltypes import (
52
52
  TIMESTAMP,
53
53
  )
54
54
 
55
- __version__ = "1.2.3"
55
+ __version__ = "1.3.0"
56
56
 
57
57
  __all__ = (
58
58
  "insert",
@@ -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,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
- pass
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 does not expose check constraint expressions easily
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("SELECT COUNT(*) FROM _db_index WHERE index_name = :name"),
631
- {"name": index_name},
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
- # CUBRID server default is typically level 4
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.2.3
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 :: 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
@@ -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 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,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
- - **426 offline tests** with **99%+ code coverage** — no database required to run them
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
- - Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
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