sqlalchemy-cubrid 1.2.1__tar.gz → 1.2.3__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.1/sqlalchemy_cubrid.egg-info → sqlalchemy_cubrid-1.2.3}/PKG-INFO +2 -1
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/pyproject.toml +2 -1
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/__init__.py +1 -1
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/alembic_impl.py +40 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/dialect.py +94 -66
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3/sqlalchemy_cubrid.egg-info}/PKG-INFO +2 -1
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid.egg-info/requires.txt +1 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_aio_integration.py +8 -23
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_alembic.py +40 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_dialect_offline.py +74 -21
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/AUTHORS +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/LICENSE +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/README.md +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/setup.cfg +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/aio_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/base.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/compiler.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/dml.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/py.typed +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/requirements.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/trace.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/types.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid.egg-info/SOURCES.txt +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid.egg-info/entry_points.txt +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid.egg-info/top_level.txt +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_aio_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_base.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_compiler.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_dialects.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_integration.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_json.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_requirements.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_suite.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/test/test_trace.py +0 -0
- {sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/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
|
+
Version: 1.2.3
|
|
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
|
|
@@ -35,6 +35,7 @@ Requires-Dist: CUBRID-Python; extra == "cubrid"
|
|
|
35
35
|
Provides-Extra: dev
|
|
36
36
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
38
39
|
Requires-Dist: ruff==0.15.8; extra == "dev"
|
|
39
40
|
Requires-Dist: mypy==1.19.1; extra == "dev"
|
|
40
41
|
Requires-Dist: bandit[toml]==1.9.4; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlalchemy-cubrid"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.3"
|
|
8
8
|
description = "CUBRID dialect for SQLAlchemy"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -42,6 +42,7 @@ cubrid = [
|
|
|
42
42
|
dev = [
|
|
43
43
|
"pytest>=7.0",
|
|
44
44
|
"pytest-cov",
|
|
45
|
+
"pytest-asyncio",
|
|
45
46
|
"ruff==0.15.8",
|
|
46
47
|
"mypy==1.19.1",
|
|
47
48
|
"bandit[toml]==1.9.4",
|
|
@@ -76,6 +76,15 @@ class CubridImpl(DefaultImpl):
|
|
|
76
76
|
|
|
77
77
|
_collection_type_names: set[str] = {"SET", "MULTISET", "SEQUENCE"}
|
|
78
78
|
|
|
79
|
+
# CUBRID's STRING / CLOB / Text are stored physically as
|
|
80
|
+
# VARCHAR(1073741823). When SQLAlchemy's reflection round-trips a
|
|
81
|
+
# ``Text`` / ``CLOB`` / ``STRING`` column it sees a VARCHAR with that
|
|
82
|
+
# exact length, which trips Alembic's default compare_type into
|
|
83
|
+
# reporting a spurious type change on every autogenerate run
|
|
84
|
+
# (see cubrid-labs/sqlalchemy-cubrid#120).
|
|
85
|
+
_CUBRID_UNBOUNDED_VARCHAR_LENGTH: int = 1073741823
|
|
86
|
+
_unbounded_string_type_names: set[str] = {"TEXT", "CLOB", "STRING"}
|
|
87
|
+
|
|
79
88
|
@staticmethod
|
|
80
89
|
def _normalize_collection_value(value: object) -> str:
|
|
81
90
|
if isinstance(value, str):
|
|
@@ -115,6 +124,11 @@ class CubridImpl(DefaultImpl):
|
|
|
115
124
|
inspector_type = inspector_column.type
|
|
116
125
|
metadata_type = metadata_column.type
|
|
117
126
|
|
|
127
|
+
# Suppress false-positive Text/CLOB/STRING vs VARCHAR(max) diffs.
|
|
128
|
+
# See ``_CUBRID_UNBOUNDED_VARCHAR_LENGTH`` docstring above.
|
|
129
|
+
if self._is_unbounded_string_match(inspector_type, metadata_type):
|
|
130
|
+
return False
|
|
131
|
+
|
|
118
132
|
inspector_name = inspector_type.__class__.__name__
|
|
119
133
|
metadata_name = metadata_type.__class__.__name__
|
|
120
134
|
|
|
@@ -143,3 +157,29 @@ class CubridImpl(DefaultImpl):
|
|
|
143
157
|
return inspector_values != metadata_values
|
|
144
158
|
|
|
145
159
|
return set(inspector_values) != set(metadata_values)
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def _is_unbounded_string_match(
|
|
163
|
+
cls, inspector_type: TypeEngine[Any], metadata_type: TypeEngine[Any]
|
|
164
|
+
) -> bool:
|
|
165
|
+
"""Return ``True`` when one side is an unbounded string type and the other is a VARCHAR sized to CUBRID's STRING maximum length."""
|
|
166
|
+
return cls._matches_unbounded_pair(
|
|
167
|
+
inspector_type, metadata_type
|
|
168
|
+
) or cls._matches_unbounded_pair(metadata_type, inspector_type)
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def _matches_unbounded_pair(
|
|
172
|
+
cls, varchar_side: TypeEngine[Any], unbounded_side: TypeEngine[Any]
|
|
173
|
+
) -> bool:
|
|
174
|
+
if varchar_side.__class__.__name__ != "VARCHAR":
|
|
175
|
+
return False
|
|
176
|
+
if getattr(varchar_side, "length", None) != cls._CUBRID_UNBOUNDED_VARCHAR_LENGTH:
|
|
177
|
+
return False
|
|
178
|
+
unbounded_name = unbounded_side.__class__.__name__.upper()
|
|
179
|
+
if unbounded_name in cls._unbounded_string_type_names:
|
|
180
|
+
return True
|
|
181
|
+
# Plain SQLAlchemy String() with no length declared also maps to
|
|
182
|
+
# VARCHAR(1073741823) on CUBRID.
|
|
183
|
+
if unbounded_name == "STRING" or unbounded_name.endswith("STRING"):
|
|
184
|
+
return getattr(unbounded_side, "length", None) is None
|
|
185
|
+
return False
|
|
@@ -78,6 +78,29 @@ _RE_TYPE_PARAMS = re.compile(r"\([\d,]+\)")
|
|
|
78
78
|
_RE_LENGTH = re.compile(r"\((\d+)\)")
|
|
79
79
|
_RE_PRECISION_SCALE = re.compile(r"\((\d+)(?:,\s*(\d+))?\)")
|
|
80
80
|
|
|
81
|
+
# CUBRID's ``SHOW CREATE TABLE`` emits foreign-key clauses such as::
|
|
82
|
+
#
|
|
83
|
+
# CONSTRAINT [fk_name] FOREIGN KEY ([col1], [col2]) REFERENCES
|
|
84
|
+
# [owner.ref_table] ([rcol1], [rcol2]) ON DELETE ... ON UPDATE ...
|
|
85
|
+
#
|
|
86
|
+
# We parse this DDL fragment because CUBRID exposes no queryable view that
|
|
87
|
+
# carries the referenced table/columns alongside the constraint name.
|
|
88
|
+
_RE_FOREIGN_KEY = re.compile(
|
|
89
|
+
r"CONSTRAINT\s+\[(?P<name>[^\]]+)\]\s+FOREIGN\s+KEY\s*"
|
|
90
|
+
r"\((?P<cols>[^)]+)\)\s+REFERENCES\s+"
|
|
91
|
+
r"\[(?P<ref_table>[^\]]+)\]\s*\((?P<ref_cols>[^)]+)\)",
|
|
92
|
+
re.IGNORECASE,
|
|
93
|
+
)
|
|
94
|
+
# Parses ``CONSTRAINT [name] UNIQUE KEY ([col1], [col2])`` from
|
|
95
|
+
# ``SHOW CREATE TABLE`` output. Same rationale as ``_RE_FOREIGN_KEY`` —
|
|
96
|
+
# CUBRID's ``db_constraint`` view is not queryable in 11.x.
|
|
97
|
+
_RE_UNIQUE_KEY = re.compile(
|
|
98
|
+
r"CONSTRAINT\s+\[(?P<name>[^\]]+)\]\s+UNIQUE\s+KEY\s*"
|
|
99
|
+
r"\((?P<cols>[^)]+)\)",
|
|
100
|
+
re.IGNORECASE,
|
|
101
|
+
)
|
|
102
|
+
_RE_BRACKET_IDENT = re.compile(r"\[([^\]]+)\]")
|
|
103
|
+
|
|
81
104
|
|
|
82
105
|
# -----------------------------------------------------------------------
|
|
83
106
|
# Column-spec and ischema_names mappings
|
|
@@ -374,45 +397,43 @@ class CubridDialect(default.DefaultDialect):
|
|
|
374
397
|
) -> list[ReflectedForeignKeyConstraint]:
|
|
375
398
|
"""Return foreign key information for *table_name*.
|
|
376
399
|
|
|
377
|
-
|
|
400
|
+
Parses ``SHOW CREATE TABLE`` output to extract FK constraints.
|
|
401
|
+
CUBRID exposes no queryable ``db_constraint`` view (despite older
|
|
402
|
+
documentation referencing it), so the DDL string is the only
|
|
403
|
+
reliable source for FK metadata that includes the referenced table
|
|
404
|
+
and columns. See cubrid-labs/sqlalchemy-cubrid#120.
|
|
378
405
|
"""
|
|
379
406
|
foreign_keys: list[ReflectedForeignKeyConstraint] = []
|
|
380
407
|
try:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
408
|
+
quoted = self.identifier_preparer.quote_identifier(table_name)
|
|
409
|
+
result = connection.execute(text(f"SHOW CREATE TABLE {quoted}"))
|
|
410
|
+
row = result.fetchone()
|
|
411
|
+
except Exception: # nosec B110 — graceful fallback when DDL unavailable
|
|
412
|
+
return foreign_keys
|
|
413
|
+
if row is None:
|
|
414
|
+
return foreign_keys
|
|
415
|
+
ddl = str(row[1]) if len(row) > 1 else str(row[0])
|
|
416
|
+
for fk_match in _RE_FOREIGN_KEY.finditer(ddl):
|
|
417
|
+
constraint_name = fk_match.group("name")
|
|
418
|
+
constrained_columns = [
|
|
419
|
+
col.strip() for col in _RE_BRACKET_IDENT.findall(fk_match.group("cols"))
|
|
420
|
+
]
|
|
421
|
+
ref_table_raw = fk_match.group("ref_table")
|
|
422
|
+
# CUBRID prefixes referenced tables with the owner (e.g.
|
|
423
|
+
# ``dba.budget_categories``) — strip it for SQLAlchemy.
|
|
424
|
+
ref_table = ref_table_raw.split(".", 1)[-1]
|
|
425
|
+
referred_columns = [
|
|
426
|
+
col.strip() for col in _RE_BRACKET_IDENT.findall(fk_match.group("ref_cols"))
|
|
427
|
+
]
|
|
428
|
+
foreign_keys.append(
|
|
429
|
+
{
|
|
430
|
+
"name": constraint_name,
|
|
431
|
+
"constrained_columns": constrained_columns,
|
|
432
|
+
"referred_schema": schema,
|
|
433
|
+
"referred_table": ref_table,
|
|
434
|
+
"referred_columns": referred_columns,
|
|
435
|
+
}
|
|
395
436
|
)
|
|
396
|
-
|
|
397
|
-
fk_dict: dict[str, ReflectedForeignKeyConstraint] = {}
|
|
398
|
-
for row in result:
|
|
399
|
-
name = row[0]
|
|
400
|
-
if name not in fk_dict:
|
|
401
|
-
fk_dict[name] = {
|
|
402
|
-
"name": name,
|
|
403
|
-
"constrained_columns": [],
|
|
404
|
-
"referred_schema": schema,
|
|
405
|
-
"referred_table": row[3],
|
|
406
|
-
"referred_columns": [],
|
|
407
|
-
}
|
|
408
|
-
fk_dict[name]["constrained_columns"].append(row[2])
|
|
409
|
-
if row[4]:
|
|
410
|
-
fk_dict[name]["referred_columns"].append(row[4])
|
|
411
|
-
|
|
412
|
-
foreign_keys = list(fk_dict.values())
|
|
413
|
-
except Exception: # nosec B110 — graceful fallback when FK info unavailable
|
|
414
|
-
pass
|
|
415
|
-
|
|
416
437
|
return foreign_keys
|
|
417
438
|
|
|
418
439
|
@reflection.cache
|
|
@@ -469,31 +490,43 @@ class CubridDialect(default.DefaultDialect):
|
|
|
469
490
|
"""Return index information for *table_name*."""
|
|
470
491
|
idict: dict[str, ReflectedIndex] = {}
|
|
471
492
|
|
|
472
|
-
# Batch-fetch primary key flags for all indexes on
|
|
473
|
-
#
|
|
493
|
+
# Batch-fetch primary-key and foreign-key flags for all indexes on
|
|
494
|
+
# this table from CUBRID's ``_db_index`` catalog (single query for
|
|
495
|
+
# both, instead of N+1 lookups).
|
|
496
|
+
#
|
|
497
|
+
# PK indexes are filtered because SQLAlchemy reports the PK via
|
|
498
|
+
# ``get_pk_constraint`` separately. FK indexes are filtered because
|
|
499
|
+
# CUBRID auto-creates an index for every foreign key (with the same
|
|
500
|
+
# name as the FK constraint) and these are an implementation detail
|
|
501
|
+
# — if reported they cause Alembic autogenerate to emit spurious
|
|
502
|
+
# ``op.drop_index`` / ``op.create_index`` diffs on every run.
|
|
503
|
+
# See cubrid-labs/sqlalchemy-cubrid#120.
|
|
474
504
|
pk_indexes: set[str] = set()
|
|
505
|
+
fk_indexes: set[str] = set()
|
|
475
506
|
try:
|
|
476
|
-
|
|
507
|
+
flag_result = connection.execute(
|
|
477
508
|
text(
|
|
478
|
-
"SELECT index_name, is_primary_key
|
|
479
|
-
"WHERE class_of.class_name = :table"
|
|
509
|
+
"SELECT index_name, is_primary_key, is_foreign_key "
|
|
510
|
+
"FROM _db_index WHERE class_of.class_name = :table"
|
|
480
511
|
),
|
|
481
512
|
{"table": table_name},
|
|
482
513
|
)
|
|
483
|
-
for
|
|
484
|
-
if
|
|
485
|
-
pk_indexes.add(
|
|
514
|
+
for flag_row in flag_result:
|
|
515
|
+
if flag_row[1]:
|
|
516
|
+
pk_indexes.add(flag_row[0])
|
|
517
|
+
if flag_row[2]:
|
|
518
|
+
fk_indexes.add(flag_row[0])
|
|
486
519
|
except Exception:
|
|
487
|
-
# Fallback: if catalog query fails,
|
|
488
|
-
#
|
|
489
|
-
log.debug("Batch
|
|
520
|
+
# Fallback: if the catalog query fails, both sets stay empty so
|
|
521
|
+
# no indexes will be wrongly excluded.
|
|
522
|
+
log.debug("Batch index-flag query failed for table %s, falling back", table_name)
|
|
490
523
|
|
|
491
524
|
quoted = self.identifier_preparer.quote_identifier(table_name)
|
|
492
525
|
result = connection.execute(text(f"SHOW INDEXES IN {quoted}"))
|
|
493
526
|
for row in result:
|
|
494
527
|
index_name = row[2]
|
|
495
528
|
|
|
496
|
-
if index_name not in pk_indexes:
|
|
529
|
+
if index_name not in pk_indexes and index_name not in fk_indexes:
|
|
497
530
|
if index_name in idict:
|
|
498
531
|
idict[index_name]["column_names"].append(row[4])
|
|
499
532
|
else:
|
|
@@ -516,25 +549,20 @@ class CubridDialect(default.DefaultDialect):
|
|
|
516
549
|
"""Return unique constraints for *table_name*."""
|
|
517
550
|
unique_constraints: list[ReflectedUniqueConstraint] = []
|
|
518
551
|
try:
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
)
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
uc_dict[name] = {"name": name, "column_names": []}
|
|
534
|
-
uc_dict[name]["column_names"].append(row[1])
|
|
535
|
-
unique_constraints = list(uc_dict.values())
|
|
536
|
-
except Exception: # nosec B110 — graceful fallback when UC info unavailable
|
|
537
|
-
pass
|
|
552
|
+
quoted = self.identifier_preparer.quote_identifier(table_name)
|
|
553
|
+
result = connection.execute(text(f"SHOW CREATE TABLE {quoted}"))
|
|
554
|
+
row = result.fetchone()
|
|
555
|
+
except Exception: # nosec B110 — graceful fallback when DDL unavailable
|
|
556
|
+
return unique_constraints
|
|
557
|
+
if row is None:
|
|
558
|
+
return unique_constraints
|
|
559
|
+
ddl = str(row[1]) if len(row) > 1 else str(row[0])
|
|
560
|
+
for uc_match in _RE_UNIQUE_KEY.finditer(ddl):
|
|
561
|
+
constraint_name = uc_match.group("name")
|
|
562
|
+
column_names = [
|
|
563
|
+
col.strip() for col in _RE_BRACKET_IDENT.findall(uc_match.group("cols"))
|
|
564
|
+
]
|
|
565
|
+
unique_constraints.append({"name": constraint_name, "column_names": column_names})
|
|
538
566
|
return unique_constraints
|
|
539
567
|
|
|
540
568
|
@reflection.cache
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-cubrid
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
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
|
|
@@ -35,6 +35,7 @@ Requires-Dist: CUBRID-Python; extra == "cubrid"
|
|
|
35
35
|
Provides-Extra: dev
|
|
36
36
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
38
39
|
Requires-Dist: ruff==0.15.8; extra == "dev"
|
|
39
40
|
Requires-Dist: mypy==1.19.1; extra == "dev"
|
|
40
41
|
Requires-Dist: bandit[toml]==1.9.4; extra == "dev"
|
|
@@ -27,8 +27,7 @@ import os
|
|
|
27
27
|
import pytest
|
|
28
28
|
import pytest_asyncio
|
|
29
29
|
from sqlalchemy import Column, Integer, MetaData, String, Table, text
|
|
30
|
-
from sqlalchemy.ext.asyncio import
|
|
31
|
-
from sqlalchemy.orm import sessionmaker
|
|
30
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
32
31
|
|
|
33
32
|
# ---------------------------------------------------------------------------
|
|
34
33
|
# Fixtures
|
|
@@ -129,13 +128,9 @@ class TestAsyncCRUD:
|
|
|
129
128
|
async def test_update(self, engine, metadata):
|
|
130
129
|
users = metadata.tables["aio_test_users"]
|
|
131
130
|
async with engine.begin() as conn:
|
|
132
|
-
await conn.execute(
|
|
133
|
-
users.update().where(users.c.name == "alice").values(value=100)
|
|
134
|
-
)
|
|
131
|
+
await conn.execute(users.update().where(users.c.name == "alice").values(value=100))
|
|
135
132
|
async with engine.connect() as conn:
|
|
136
|
-
result = await conn.execute(
|
|
137
|
-
users.select().where(users.c.name == "alice")
|
|
138
|
-
)
|
|
133
|
+
result = await conn.execute(users.select().where(users.c.name == "alice"))
|
|
139
134
|
row = result.fetchone()
|
|
140
135
|
assert row is not None and row.value == 100
|
|
141
136
|
|
|
@@ -144,26 +139,20 @@ class TestAsyncCRUD:
|
|
|
144
139
|
async with engine.begin() as conn:
|
|
145
140
|
await conn.execute(users.delete().where(users.c.name == "bob"))
|
|
146
141
|
async with engine.connect() as conn:
|
|
147
|
-
result = await conn.execute(
|
|
148
|
-
users.select().where(users.c.name == "bob")
|
|
149
|
-
)
|
|
142
|
+
result = await conn.execute(users.select().where(users.c.name == "bob"))
|
|
150
143
|
assert result.fetchone() is None
|
|
151
144
|
|
|
152
145
|
async def test_transaction_rollback(self, engine, metadata):
|
|
153
146
|
users = metadata.tables["aio_test_users"]
|
|
154
147
|
try:
|
|
155
148
|
async with engine.begin() as conn:
|
|
156
|
-
await conn.execute(
|
|
157
|
-
users.insert().values(name="will_rollback", value=999)
|
|
158
|
-
)
|
|
149
|
+
await conn.execute(users.insert().values(name="will_rollback", value=999))
|
|
159
150
|
raise RuntimeError("force rollback")
|
|
160
151
|
except RuntimeError:
|
|
161
152
|
pass
|
|
162
153
|
|
|
163
154
|
async with engine.connect() as conn:
|
|
164
|
-
result = await conn.execute(
|
|
165
|
-
users.select().where(users.c.name == "will_rollback")
|
|
166
|
-
)
|
|
155
|
+
result = await conn.execute(users.select().where(users.c.name == "will_rollback"))
|
|
167
156
|
assert result.fetchone() is None
|
|
168
157
|
|
|
169
158
|
async def test_concurrent_pool(self, engine):
|
|
@@ -243,9 +232,7 @@ class TestAsyncJSON:
|
|
|
243
232
|
|
|
244
233
|
async def test_null_json(self, engine):
|
|
245
234
|
async with engine.begin() as conn:
|
|
246
|
-
await conn.execute(
|
|
247
|
-
text("INSERT INTO aio_test_json (payload) VALUES (NULL)")
|
|
248
|
-
)
|
|
235
|
+
await conn.execute(text("INSERT INTO aio_test_json (payload) VALUES (NULL)"))
|
|
249
236
|
assert await self._last_json(engine) is None
|
|
250
237
|
|
|
251
238
|
async def test_empty_object(self, engine):
|
|
@@ -258,9 +245,7 @@ class TestAsyncJSON:
|
|
|
258
245
|
|
|
259
246
|
async def test_json_extract(self, engine):
|
|
260
247
|
async with engine.connect() as conn:
|
|
261
|
-
r = await conn.execute(
|
|
262
|
-
text("SELECT JSON_EXTRACT('{\"a\": 1}', '$.a')")
|
|
263
|
-
)
|
|
248
|
+
r = await conn.execute(text("SELECT JSON_EXTRACT('{\"a\": 1}', '$.a')"))
|
|
264
249
|
assert r.scalar() is not None
|
|
265
250
|
|
|
266
251
|
async def test_orm_json_type(self, engine):
|
|
@@ -259,3 +259,43 @@ class TestCubridImplAutogenerate:
|
|
|
259
259
|
metadata_column = sa.Column("v", cubrid_types.MULTISET("a", "b"))
|
|
260
260
|
|
|
261
261
|
assert impl.compare_type(inspector_column, metadata_column) is True
|
|
262
|
+
|
|
263
|
+
def test_compare_type_text_vs_varchar_max_no_diff(self):
|
|
264
|
+
"""Text() vs VARCHAR(1073741823) must NOT be reported as a type change.
|
|
265
|
+
|
|
266
|
+
See cubrid-labs/sqlalchemy-cubrid#120 — CUBRID stores Text/CLOB/STRING
|
|
267
|
+
as VARCHAR(1073741823) so reflection round-trips trip Alembic's
|
|
268
|
+
default compare_type.
|
|
269
|
+
"""
|
|
270
|
+
from sqlalchemy_cubrid.alembic_impl import CubridImpl
|
|
271
|
+
|
|
272
|
+
impl = object.__new__(CubridImpl)
|
|
273
|
+
inspector_column = sa.Column("v", cubrid_types.VARCHAR(1073741823))
|
|
274
|
+
for metadata_type in (sa.Text(), cubrid_types.CLOB(), cubrid_types.STRING()):
|
|
275
|
+
metadata_column = sa.Column("v", metadata_type)
|
|
276
|
+
assert impl.compare_type(inspector_column, metadata_column) is False
|
|
277
|
+
# Reverse direction must also hold.
|
|
278
|
+
assert impl.compare_type(metadata_column, inspector_column) is False
|
|
279
|
+
|
|
280
|
+
def test_compare_type_varchar_max_vs_string_no_length_no_diff(self):
|
|
281
|
+
"""Plain String() (no length) maps to VARCHAR(max) on CUBRID."""
|
|
282
|
+
from sqlalchemy_cubrid.alembic_impl import CubridImpl
|
|
283
|
+
|
|
284
|
+
impl = object.__new__(CubridImpl)
|
|
285
|
+
inspector_column = sa.Column("v", cubrid_types.VARCHAR(1073741823))
|
|
286
|
+
metadata_column = sa.Column("v", sa.String())
|
|
287
|
+
assert impl.compare_type(inspector_column, metadata_column) is False
|
|
288
|
+
|
|
289
|
+
def test_compare_type_varchar_bounded_still_compared(self):
|
|
290
|
+
"""VARCHAR(100) vs Text() must still be detected as a real diff."""
|
|
291
|
+
from alembic.ddl.impl import DefaultImpl
|
|
292
|
+
|
|
293
|
+
from sqlalchemy_cubrid.alembic_impl import CubridImpl
|
|
294
|
+
|
|
295
|
+
impl = object.__new__(CubridImpl)
|
|
296
|
+
inspector_column = sa.Column("v", cubrid_types.VARCHAR(100))
|
|
297
|
+
metadata_column = sa.Column("v", sa.Text())
|
|
298
|
+
with mock.patch.object(DefaultImpl, "compare_type", return_value=True) as m:
|
|
299
|
+
result = impl.compare_type(inspector_column, metadata_column)
|
|
300
|
+
assert result is True
|
|
301
|
+
m.assert_called_once()
|
|
@@ -338,14 +338,24 @@ class TestReflectionMethods:
|
|
|
338
338
|
def test_get_foreign_keys_success_and_exception(self):
|
|
339
339
|
dialect = CubridDialect()
|
|
340
340
|
|
|
341
|
+
ddl = (
|
|
342
|
+
"CREATE TABLE [orders] (\n"
|
|
343
|
+
" [id] INTEGER NOT NULL,\n"
|
|
344
|
+
" CONSTRAINT [pk_orders] PRIMARY KEY ([id]),\n"
|
|
345
|
+
" CONSTRAINT [fk_order_user] FOREIGN KEY ([user_id], [tenant_id]) "
|
|
346
|
+
"REFERENCES [dba.users] ([id], [tenant_id]) "
|
|
347
|
+
"ON DELETE RESTRICT ON UPDATE RESTRICT,\n"
|
|
348
|
+
" CONSTRAINT [fk_order_legacy] FOREIGN KEY ([legacy_id]) "
|
|
349
|
+
"REFERENCES [legacy] ([id])\n"
|
|
350
|
+
")"
|
|
351
|
+
)
|
|
352
|
+
|
|
341
353
|
success_conn = MagicMock()
|
|
342
354
|
success_conn.info_cache = {}
|
|
343
355
|
success_conn.dialect_options = {}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
("fk_no_ref_col", "orders", "legacy_id", "legacy", None),
|
|
348
|
-
]
|
|
356
|
+
success_result = MagicMock()
|
|
357
|
+
success_result.fetchone.return_value = ("orders", ddl)
|
|
358
|
+
success_conn.execute.return_value = success_result
|
|
349
359
|
|
|
350
360
|
fks = _invoke_reflection(
|
|
351
361
|
dialect,
|
|
@@ -358,12 +368,14 @@ class TestReflectionMethods:
|
|
|
358
368
|
assert len(fks) == 2
|
|
359
369
|
first = next(item for item in fks if item["name"] == "fk_order_user")
|
|
360
370
|
assert first["constrained_columns"] == ["user_id", "tenant_id"]
|
|
371
|
+
# Owner prefix (``dba.``) must be stripped from the referenced table.
|
|
361
372
|
assert first["referred_table"] == "users"
|
|
362
373
|
assert first["referred_columns"] == ["id", "tenant_id"]
|
|
363
374
|
assert first["referred_schema"] == "main"
|
|
364
375
|
|
|
365
|
-
second = next(item for item in fks if item["name"] == "
|
|
366
|
-
assert second["
|
|
376
|
+
second = next(item for item in fks if item["name"] == "fk_order_legacy")
|
|
377
|
+
assert second["referred_table"] == "legacy"
|
|
378
|
+
assert second["referred_columns"] == ["id"]
|
|
367
379
|
|
|
368
380
|
failed_conn = MagicMock()
|
|
369
381
|
failed_conn.info_cache = {}
|
|
@@ -420,11 +432,13 @@ class TestReflectionMethods:
|
|
|
420
432
|
connection.info_cache = {}
|
|
421
433
|
connection.dialect_options = {}
|
|
422
434
|
|
|
423
|
-
#
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
("
|
|
435
|
+
# Single batch query returns (index_name, is_primary_key, is_foreign_key)
|
|
436
|
+
# tuples for every index on the table. PK and FK auto-indexes are
|
|
437
|
+
# filtered from the SHOW INDEXES output.
|
|
438
|
+
flag_rows = [
|
|
439
|
+
("uq_name", 0, 0),
|
|
440
|
+
("pk_users", 1, 0),
|
|
441
|
+
("idx_email", 0, 0),
|
|
428
442
|
]
|
|
429
443
|
|
|
430
444
|
show_indexes_rows = [
|
|
@@ -435,7 +449,7 @@ class TestReflectionMethods:
|
|
|
435
449
|
]
|
|
436
450
|
|
|
437
451
|
connection.execute.side_effect = [
|
|
438
|
-
|
|
452
|
+
flag_rows, # batch _db_index query
|
|
439
453
|
show_indexes_rows, # SHOW INDEXES
|
|
440
454
|
]
|
|
441
455
|
|
|
@@ -447,7 +461,7 @@ class TestReflectionMethods:
|
|
|
447
461
|
]
|
|
448
462
|
|
|
449
463
|
def test_get_indexes_batch_pk_query_failure(self):
|
|
450
|
-
"""When the batch
|
|
464
|
+
"""When the batch catalog query fails, all indexes are returned."""
|
|
451
465
|
dialect = CubridDialect()
|
|
452
466
|
connection = MagicMock()
|
|
453
467
|
connection.info_cache = {}
|
|
@@ -459,28 +473,67 @@ class TestReflectionMethods:
|
|
|
459
473
|
]
|
|
460
474
|
|
|
461
475
|
connection.execute.side_effect = [
|
|
462
|
-
RuntimeError("catalog unavailable"), # batch
|
|
476
|
+
RuntimeError("catalog unavailable"), # batch flag query fails
|
|
463
477
|
show_indexes_rows, # SHOW INDEXES
|
|
464
478
|
]
|
|
465
479
|
|
|
466
480
|
indexes = _invoke_reflection(dialect, "get_indexes", connection, "users")
|
|
467
481
|
|
|
468
|
-
# Both indexes returned since PK detection failed gracefully
|
|
482
|
+
# Both indexes returned since PK/FK detection failed gracefully.
|
|
469
483
|
assert len(indexes) == 2
|
|
470
484
|
assert indexes[0]["name"] == "uq_name"
|
|
471
485
|
assert indexes[1]["name"] == "pk_users"
|
|
472
486
|
|
|
487
|
+
def test_get_indexes_excludes_fk_auto_indexes(self):
|
|
488
|
+
"""FK auto-indexes (``_db_index.is_foreign_key`` true) are filtered.
|
|
489
|
+
|
|
490
|
+
See cubrid-labs/sqlalchemy-cubrid#120 — otherwise Alembic
|
|
491
|
+
autogenerate emits spurious drop_index/create_index diffs.
|
|
492
|
+
"""
|
|
493
|
+
dialect = CubridDialect()
|
|
494
|
+
connection = MagicMock()
|
|
495
|
+
connection.info_cache = {}
|
|
496
|
+
connection.dialect_options = {}
|
|
497
|
+
|
|
498
|
+
flag_rows = [
|
|
499
|
+
("fk_orders_user", 0, 1),
|
|
500
|
+
("fk_orders_product", 0, 1),
|
|
501
|
+
("idx_orders_status", 0, 0),
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
connection.execute.side_effect = [
|
|
505
|
+
flag_rows,
|
|
506
|
+
[
|
|
507
|
+
(None, 1, "fk_orders_user", None, "user_id"),
|
|
508
|
+
(None, 1, "fk_orders_product", None, "product_id"),
|
|
509
|
+
(None, 1, "idx_orders_status", None, "status"),
|
|
510
|
+
],
|
|
511
|
+
]
|
|
512
|
+
|
|
513
|
+
indexes = _invoke_reflection(dialect, "get_indexes", connection, "orders")
|
|
514
|
+
|
|
515
|
+
assert indexes == [
|
|
516
|
+
{"name": "idx_orders_status", "column_names": ["status"], "unique": False},
|
|
517
|
+
]
|
|
518
|
+
|
|
473
519
|
def test_get_unique_constraints_success_and_exception(self):
|
|
474
520
|
dialect = CubridDialect()
|
|
475
521
|
|
|
522
|
+
ddl = (
|
|
523
|
+
"CREATE TABLE [users] (\n"
|
|
524
|
+
" [id] INTEGER NOT NULL,\n"
|
|
525
|
+
" CONSTRAINT [pk_users] PRIMARY KEY ([id]),\n"
|
|
526
|
+
" CONSTRAINT [uq_users_email] UNIQUE KEY ([email], [tenant_id]),\n"
|
|
527
|
+
" CONSTRAINT [uq_users_name] UNIQUE KEY ([name])\n"
|
|
528
|
+
")"
|
|
529
|
+
)
|
|
530
|
+
|
|
476
531
|
success_conn = MagicMock()
|
|
477
532
|
success_conn.info_cache = {}
|
|
478
533
|
success_conn.dialect_options = {}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
("uq_users_name", "name"),
|
|
483
|
-
]
|
|
534
|
+
success_result = MagicMock()
|
|
535
|
+
success_result.fetchone.return_value = ("users", ddl)
|
|
536
|
+
success_conn.execute.return_value = success_result
|
|
484
537
|
|
|
485
538
|
unique_constraints = _invoke_reflection(
|
|
486
539
|
dialect,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid/aio_pycubrid_dialect.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{sqlalchemy_cubrid-1.2.1 → sqlalchemy_cubrid-1.2.3}/sqlalchemy_cubrid.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|