sqlalchemy-cubrid 1.1.0__tar.gz → 1.2.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.1.0/sqlalchemy_cubrid.egg-info → sqlalchemy_cubrid-1.2.0}/PKG-INFO +2 -2
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/README.md +1 -1
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/pyproject.toml +1 -1
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/__init__.py +7 -1
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/aio_pycubrid_dialect.py +1 -2
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/compiler.py +54 -1
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/dialect.py +8 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/types.py +89 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0/sqlalchemy_cubrid.egg-info}/PKG-INFO +2 -2
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/SOURCES.txt +1 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_aio_pycubrid_dialect.py +0 -2
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_integration.py +6 -9
- sqlalchemy_cubrid-1.2.0/test/test_json.py +312 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/AUTHORS +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/LICENSE +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/setup.cfg +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/alembic_impl.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/base.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/dml.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/py.typed +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/requirements.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid/trace.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/dependency_links.txt +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/entry_points.txt +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/requires.txt +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/top_level.txt +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_alembic.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_base.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_compiler.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_dialect_offline.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_dialects.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_pycubrid_dialect.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_requirements.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_suite.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/test/test_trace.py +0 -0
- {sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.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.2.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
|
|
@@ -180,7 +180,7 @@ async with AsyncSession(engine) as session:
|
|
|
180
180
|
|
|
181
181
|
## Features
|
|
182
182
|
|
|
183
|
-
- Complete type system -- numeric, string, date/time, bit, LOB, and
|
|
183
|
+
- Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
184
184
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
185
185
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
186
186
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
@@ -131,7 +131,7 @@ async with AsyncSession(engine) as session:
|
|
|
131
131
|
|
|
132
132
|
## Features
|
|
133
133
|
|
|
134
|
-
- Complete type system -- numeric, string, date/time, bit, LOB, and
|
|
134
|
+
- Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
135
135
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
136
136
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
137
137
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
@@ -25,6 +25,9 @@ from .types import (
|
|
|
25
25
|
DOUBLE,
|
|
26
26
|
DOUBLE_PRECISION,
|
|
27
27
|
FLOAT,
|
|
28
|
+
JSON,
|
|
29
|
+
JSONIndexType,
|
|
30
|
+
JSONPathType,
|
|
28
31
|
MONETARY,
|
|
29
32
|
MULTISET,
|
|
30
33
|
NCHAR,
|
|
@@ -49,7 +52,7 @@ from sqlalchemy.sql.sqltypes import (
|
|
|
49
52
|
TIMESTAMP,
|
|
50
53
|
)
|
|
51
54
|
|
|
52
|
-
__version__ = "1.
|
|
55
|
+
__version__ = "1.2.0"
|
|
53
56
|
|
|
54
57
|
__all__ = (
|
|
55
58
|
"insert",
|
|
@@ -82,4 +85,7 @@ __all__ = (
|
|
|
82
85
|
"MULTISET",
|
|
83
86
|
"SEQUENCE",
|
|
84
87
|
"OBJECT",
|
|
88
|
+
"JSON",
|
|
89
|
+
"JSONIndexType",
|
|
90
|
+
"JSONPathType",
|
|
85
91
|
)
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
-
from sqlalchemy.sql import compiler
|
|
14
|
+
from sqlalchemy.sql import compiler, elements
|
|
15
15
|
from sqlalchemy.sql import sqltypes
|
|
16
16
|
|
|
17
17
|
|
|
@@ -320,6 +320,56 @@ class CubridCompiler(compiler.SQLCompiler):
|
|
|
320
320
|
return "REPLACE" + text[len("INSERT") :]
|
|
321
321
|
return text
|
|
322
322
|
|
|
323
|
+
def _render_json_extract_from_binary(
|
|
324
|
+
self, binary: elements.BinaryExpression[Any], operator: Any, **kw: Any
|
|
325
|
+
) -> str:
|
|
326
|
+
if binary.type._type_affinity is sqltypes.JSON:
|
|
327
|
+
return "JSON_EXTRACT(%s, %s)" % (
|
|
328
|
+
self.process(binary.left, **kw),
|
|
329
|
+
self.process(binary.right, **kw),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# When the target type is not JSON, convert JSON 'null' to SQL NULL
|
|
333
|
+
case_expression = "CASE JSON_EXTRACT(%s, %s) WHEN 'null' THEN NULL" % (
|
|
334
|
+
self.process(binary.left, **kw),
|
|
335
|
+
self.process(binary.right, **kw),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if binary.type._type_affinity is sqltypes.Boolean:
|
|
339
|
+
type_expression = (
|
|
340
|
+
"WHEN 'true' THEN 1 WHEN 'false' THEN 0 ELSE CAST(JSON_EXTRACT(%s, %s) AS INTEGER)"
|
|
341
|
+
) % (
|
|
342
|
+
self.process(binary.left, **kw),
|
|
343
|
+
self.process(binary.right, **kw),
|
|
344
|
+
)
|
|
345
|
+
elif binary.type._type_affinity is sqltypes.Integer:
|
|
346
|
+
type_expression = "ELSE CAST(JSON_EXTRACT(%s, %s) AS INTEGER)" % (
|
|
347
|
+
self.process(binary.left, **kw),
|
|
348
|
+
self.process(binary.right, **kw),
|
|
349
|
+
)
|
|
350
|
+
elif binary.type._type_affinity is sqltypes.Numeric:
|
|
351
|
+
type_expression = "ELSE CAST(JSON_EXTRACT(%s, %s) AS DOUBLE)" % (
|
|
352
|
+
self.process(binary.left, **kw),
|
|
353
|
+
self.process(binary.right, **kw),
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
type_expression = "ELSE JSON_UNQUOTE(JSON_EXTRACT(%s, %s))" % (
|
|
357
|
+
self.process(binary.left, **kw),
|
|
358
|
+
self.process(binary.right, **kw),
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
return case_expression + " " + type_expression + " END"
|
|
362
|
+
|
|
363
|
+
def visit_json_getitem_op_binary(
|
|
364
|
+
self, binary: elements.BinaryExpression[Any], operator: Any, **kw: Any
|
|
365
|
+
) -> str:
|
|
366
|
+
return self._render_json_extract_from_binary(binary, operator, **kw)
|
|
367
|
+
|
|
368
|
+
def visit_json_path_getitem_op_binary(
|
|
369
|
+
self, binary: elements.BinaryExpression[Any], operator: Any, **kw: Any
|
|
370
|
+
) -> str:
|
|
371
|
+
return self._render_json_extract_from_binary(binary, operator, **kw)
|
|
372
|
+
|
|
323
373
|
|
|
324
374
|
class CubridDDLCompiler(compiler.DDLCompiler):
|
|
325
375
|
"""DDLCompiler subclass for CUBRID.
|
|
@@ -534,3 +584,6 @@ class CubridTypeCompiler(compiler.GenericTypeCompiler):
|
|
|
534
584
|
else:
|
|
535
585
|
parts.append(value.__visit_name__)
|
|
536
586
|
return f"{collection_type}({','.join(parts)})"
|
|
587
|
+
|
|
588
|
+
def visit_JSON(self, type_: Any, **kw: Any) -> str:
|
|
589
|
+
return "JSON"
|
|
@@ -48,6 +48,9 @@ from sqlalchemy_cubrid.types import (
|
|
|
48
48
|
DOUBLE,
|
|
49
49
|
DOUBLE_PRECISION,
|
|
50
50
|
FLOAT,
|
|
51
|
+
JSON,
|
|
52
|
+
JSONIndexType,
|
|
53
|
+
JSONPathType,
|
|
51
54
|
MULTISET,
|
|
52
55
|
NCHAR,
|
|
53
56
|
NUMERIC,
|
|
@@ -84,6 +87,9 @@ colspecs = {
|
|
|
84
87
|
sqltypes.Numeric: NUMERIC,
|
|
85
88
|
sqltypes.Float: FLOAT,
|
|
86
89
|
sqltypes.Time: TIME,
|
|
90
|
+
sqltypes.JSON: JSON,
|
|
91
|
+
sqltypes.JSON.JSONIndexType: JSONIndexType,
|
|
92
|
+
sqltypes.JSON.JSONPathType: JSONPathType,
|
|
87
93
|
}
|
|
88
94
|
|
|
89
95
|
# ischema_names maps CUBRID type names from SHOW COLUMNS to SA types.
|
|
@@ -120,6 +126,8 @@ ischema_names = {
|
|
|
120
126
|
"SET": SET,
|
|
121
127
|
"MULTISET": MULTISET,
|
|
122
128
|
"SEQUENCE": SEQUENCE,
|
|
129
|
+
# JSON (CUBRID 10.2+)
|
|
130
|
+
"JSON": JSON,
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
|
|
@@ -354,3 +354,92 @@ class OBJECT(sqltypes.TypeEngine[Any]):
|
|
|
354
354
|
"""
|
|
355
355
|
|
|
356
356
|
__visit_name__ = "OBJECT"
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---------------------------------------------------------------------------
|
|
360
|
+
# JSON Type (CUBRID 10.2+)
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class _FormatTypeMixin:
|
|
365
|
+
"""Mixin for formatting JSON path index/path values for bind parameters."""
|
|
366
|
+
|
|
367
|
+
def _format_value(self, value: Any) -> str:
|
|
368
|
+
raise NotImplementedError()
|
|
369
|
+
|
|
370
|
+
def bind_processor(self, dialect: Any) -> Any:
|
|
371
|
+
super_proc = self.string_bind_processor(dialect) # type: ignore[attr-defined] # noqa: E501
|
|
372
|
+
|
|
373
|
+
def process(value: Any) -> Any:
|
|
374
|
+
value = self._format_value(value)
|
|
375
|
+
if super_proc:
|
|
376
|
+
value = super_proc(value)
|
|
377
|
+
return value
|
|
378
|
+
|
|
379
|
+
return process
|
|
380
|
+
|
|
381
|
+
def literal_processor(self, dialect: Any) -> Any:
|
|
382
|
+
super_proc = self.string_literal_processor(dialect) # type: ignore[attr-defined] # noqa: E501
|
|
383
|
+
|
|
384
|
+
def process(value: Any) -> str:
|
|
385
|
+
value = self._format_value(value)
|
|
386
|
+
if super_proc:
|
|
387
|
+
value = super_proc(value)
|
|
388
|
+
return value
|
|
389
|
+
|
|
390
|
+
return process
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class JSONIndexType(_FormatTypeMixin, sqltypes.JSON.JSONIndexType):
|
|
394
|
+
"""CUBRID JSON index type for single-key access.
|
|
395
|
+
|
|
396
|
+
Converts Python index values to CUBRID JSON path syntax:
|
|
397
|
+
- Integer index: ``$[0]``
|
|
398
|
+
- String key: ``$."key"``
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
def _format_value(self, value: Any) -> str:
|
|
402
|
+
if isinstance(value, int):
|
|
403
|
+
return "$[%s]" % value
|
|
404
|
+
else:
|
|
405
|
+
return '$."%s"' % str(value).replace('"', '""')
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class JSONPathType(_FormatTypeMixin, sqltypes.JSON.JSONPathType):
|
|
409
|
+
"""CUBRID JSON path type for multi-level access.
|
|
410
|
+
|
|
411
|
+
Converts Python tuple paths to CUBRID JSON path syntax:
|
|
412
|
+
- ``("a", 1, "b")`` → ``$."a"[1]."b"``
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
def _format_value(self, value: Any) -> str:
|
|
416
|
+
return "$%s" % (
|
|
417
|
+
"".join(
|
|
418
|
+
"[%s]" % elem if isinstance(elem, int) else '."%s"' % str(elem).replace('"', '""')
|
|
419
|
+
for elem in value
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
class JSON(sqltypes.JSON): # type: ignore[type-arg]
|
|
425
|
+
"""CUBRID JSON type.
|
|
426
|
+
|
|
427
|
+
CUBRID supports JSON as of version 10.2 (RFC 7159 compliant).
|
|
428
|
+
|
|
429
|
+
:class:`_cubrid.JSON` is used automatically whenever the base
|
|
430
|
+
:class:`_types.JSON` datatype is used against a CUBRID backend.
|
|
431
|
+
|
|
432
|
+
The :class:`.cubrid.JSON` type supports persistence of JSON values
|
|
433
|
+
as well as the core index operations provided by :class:`_types.JSON`
|
|
434
|
+
datatype, by adapting the operations to render the ``JSON_EXTRACT``
|
|
435
|
+
function at the database level.
|
|
436
|
+
|
|
437
|
+
.. seealso::
|
|
438
|
+
|
|
439
|
+
:class:`_types.JSON` - main documentation for the generic
|
|
440
|
+
cross-platform JSON datatype.
|
|
441
|
+
|
|
442
|
+
.. versionadded:: 1.2.0
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
__visit_name__ = "JSON"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlalchemy-cubrid
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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
|
|
@@ -180,7 +180,7 @@ async with AsyncSession(engine) as session:
|
|
|
180
180
|
|
|
181
181
|
## Features
|
|
182
182
|
|
|
183
|
-
- Complete type system -- numeric, string, date/time, bit, LOB, and
|
|
183
|
+
- Complete type system -- numeric, string, date/time, bit, LOB, collection, and JSON types
|
|
184
184
|
- SQL compilation -- SELECT, JOIN, CAST, LIMIT/OFFSET, subqueries, CTEs, window functions
|
|
185
185
|
- DML extensions -- `ON DUPLICATE KEY UPDATE`, `MERGE`, `REPLACE INTO`, `FOR UPDATE`, `TRUNCATE`
|
|
186
186
|
- DDL support -- `COMMENT`, `IF NOT EXISTS` / `IF EXISTS`, `AUTO_INCREMENT`
|
|
@@ -36,7 +36,7 @@ from sqlalchemy import (
|
|
|
36
36
|
select,
|
|
37
37
|
text,
|
|
38
38
|
)
|
|
39
|
-
from sqlalchemy.orm import Session
|
|
39
|
+
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
|
|
40
40
|
|
|
41
41
|
# ---------------------------------------------------------------------------
|
|
42
42
|
# Fixtures
|
|
@@ -587,7 +587,6 @@ class TestFetchShapeCompatibility:
|
|
|
587
587
|
execute successfully against a live CUBRID instance, confirming
|
|
588
588
|
the dialect's use_insertmanyvalues=True is honored end-to-end.
|
|
589
589
|
"""
|
|
590
|
-
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
591
590
|
|
|
592
591
|
class _Base(DeclarativeBase):
|
|
593
592
|
pass
|
|
@@ -595,22 +594,20 @@ class TestFetchShapeCompatibility:
|
|
|
595
594
|
class BulkUser(_Base):
|
|
596
595
|
__tablename__ = "imv_bulk_test"
|
|
597
596
|
id: Mapped[int] = mapped_column(
|
|
598
|
-
Integer,
|
|
597
|
+
Integer,
|
|
598
|
+
primary_key=True,
|
|
599
|
+
autoincrement=True,
|
|
599
600
|
)
|
|
600
601
|
name: Mapped[str] = mapped_column(String(100))
|
|
601
602
|
|
|
602
603
|
_Base.metadata.create_all(engine)
|
|
603
604
|
try:
|
|
604
605
|
with Session(engine) as session:
|
|
605
|
-
session.add_all(
|
|
606
|
-
[BulkUser(name=f"user_{i}") for i in range(10)]
|
|
607
|
-
)
|
|
606
|
+
session.add_all([BulkUser(name=f"user_{i}") for i in range(10)])
|
|
608
607
|
session.commit()
|
|
609
608
|
|
|
610
609
|
with engine.connect() as conn:
|
|
611
|
-
count = conn.execute(
|
|
612
|
-
text("SELECT COUNT(*) FROM imv_bulk_test")
|
|
613
|
-
).scalar()
|
|
610
|
+
count = conn.execute(text("SELECT COUNT(*) FROM imv_bulk_test")).scalar()
|
|
614
611
|
assert count == 10
|
|
615
612
|
finally:
|
|
616
613
|
_Base.metadata.drop_all(engine)
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# test/test_json.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import sqlalchemy as sa
|
|
7
|
+
from sqlalchemy import Column, Integer, MetaData, String, Table, func, select
|
|
8
|
+
|
|
9
|
+
from sqlalchemy_cubrid.dialect import CubridDialect
|
|
10
|
+
from sqlalchemy_cubrid.types import JSON, JSONIndexType, JSONPathType
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _compile(stmt, dialect=None):
|
|
14
|
+
if dialect is None:
|
|
15
|
+
dialect = CubridDialect()
|
|
16
|
+
return stmt.compile(dialect=dialect, compile_kwargs={"literal_binds": True}).string
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
metadata = MetaData()
|
|
20
|
+
json_table = Table(
|
|
21
|
+
"json_test",
|
|
22
|
+
metadata,
|
|
23
|
+
Column("id", Integer, primary_key=True),
|
|
24
|
+
Column("data", JSON),
|
|
25
|
+
Column("name", String(100)),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestJSONType:
|
|
30
|
+
def test_visit_name(self):
|
|
31
|
+
assert JSON.__visit_name__ == "JSON"
|
|
32
|
+
|
|
33
|
+
def test_is_subclass_of_sa_json(self):
|
|
34
|
+
from sqlalchemy.sql import sqltypes
|
|
35
|
+
|
|
36
|
+
assert issubclass(JSON, sqltypes.JSON)
|
|
37
|
+
|
|
38
|
+
def test_instantiation(self):
|
|
39
|
+
t = JSON()
|
|
40
|
+
assert t is not None
|
|
41
|
+
|
|
42
|
+
def test_none_as_null_parameter(self):
|
|
43
|
+
t = JSON(none_as_null=True)
|
|
44
|
+
assert t.none_as_null is True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestJSONDDLCompilation:
|
|
48
|
+
def test_json_column_ddl(self):
|
|
49
|
+
dialect = CubridDialect()
|
|
50
|
+
compiled = dialect.type_compiler_instance.process(JSON())
|
|
51
|
+
assert compiled == "JSON"
|
|
52
|
+
|
|
53
|
+
def test_generic_sa_json_compiles_to_json(self):
|
|
54
|
+
tbl = Table(
|
|
55
|
+
"ddl_test",
|
|
56
|
+
MetaData(),
|
|
57
|
+
Column("id", Integer, primary_key=True),
|
|
58
|
+
Column("payload", sa.JSON),
|
|
59
|
+
)
|
|
60
|
+
dialect = CubridDialect()
|
|
61
|
+
create_ddl = sa.schema.CreateTable(tbl).compile(dialect=dialect).string
|
|
62
|
+
assert "JSON" in create_ddl
|
|
63
|
+
|
|
64
|
+
def test_cubrid_json_compiles_to_json(self):
|
|
65
|
+
tbl = Table(
|
|
66
|
+
"ddl_test2",
|
|
67
|
+
MetaData(),
|
|
68
|
+
Column("id", Integer, primary_key=True),
|
|
69
|
+
Column("payload", JSON),
|
|
70
|
+
)
|
|
71
|
+
dialect = CubridDialect()
|
|
72
|
+
create_ddl = sa.schema.CreateTable(tbl).compile(dialect=dialect).string
|
|
73
|
+
assert "JSON" in create_ddl
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestJSONIndexType:
|
|
77
|
+
def test_format_integer_index(self):
|
|
78
|
+
idx = JSONIndexType()
|
|
79
|
+
assert idx._format_value(0) == "$[0]"
|
|
80
|
+
assert idx._format_value(3) == "$[3]"
|
|
81
|
+
|
|
82
|
+
def test_format_string_key(self):
|
|
83
|
+
idx = JSONIndexType()
|
|
84
|
+
assert idx._format_value("name") == '$."name"'
|
|
85
|
+
assert idx._format_value("address") == '$."address"'
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestJSONPathType:
|
|
89
|
+
def test_format_simple_path(self):
|
|
90
|
+
pth = JSONPathType()
|
|
91
|
+
assert pth._format_value(("a",)) == '$."a"'
|
|
92
|
+
|
|
93
|
+
def test_format_nested_path(self):
|
|
94
|
+
pth = JSONPathType()
|
|
95
|
+
assert pth._format_value(("a", "b", "c")) == '$."a"."b"."c"'
|
|
96
|
+
|
|
97
|
+
def test_format_mixed_path(self):
|
|
98
|
+
pth = JSONPathType()
|
|
99
|
+
assert pth._format_value(("a", 1, "b")) == '$."a"[1]."b"'
|
|
100
|
+
|
|
101
|
+
def test_format_array_only_path(self):
|
|
102
|
+
pth = JSONPathType()
|
|
103
|
+
assert pth._format_value((0, 1, 2)) == "$[0][1][2]"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestJSONPathExpressionCompilation:
|
|
107
|
+
def test_json_getitem_string_key(self):
|
|
108
|
+
stmt = select(json_table.c.data["name"])
|
|
109
|
+
sql = _compile(stmt)
|
|
110
|
+
assert "JSON_EXTRACT" in sql
|
|
111
|
+
|
|
112
|
+
def test_json_getitem_integer_index(self):
|
|
113
|
+
stmt = select(json_table.c.data[0])
|
|
114
|
+
sql = _compile(stmt)
|
|
115
|
+
assert "JSON_EXTRACT" in sql
|
|
116
|
+
|
|
117
|
+
def test_json_path_getitem(self):
|
|
118
|
+
stmt = select(json_table.c.data[("a", "b")])
|
|
119
|
+
sql = _compile(stmt)
|
|
120
|
+
assert "JSON_EXTRACT" in sql
|
|
121
|
+
|
|
122
|
+
def test_json_getitem_in_where(self):
|
|
123
|
+
stmt = select(json_table).where(json_table.c.data["status"].as_string() == "active")
|
|
124
|
+
sql = _compile(stmt)
|
|
125
|
+
assert "JSON_EXTRACT" in sql
|
|
126
|
+
assert "JSON_UNQUOTE" in sql
|
|
127
|
+
|
|
128
|
+
def test_json_getitem_as_integer(self):
|
|
129
|
+
stmt = select(json_table).where(json_table.c.data["count"].as_integer() > 5)
|
|
130
|
+
sql = _compile(stmt)
|
|
131
|
+
assert "JSON_EXTRACT" in sql
|
|
132
|
+
assert "CAST" in sql
|
|
133
|
+
assert "INTEGER" in sql
|
|
134
|
+
|
|
135
|
+
def test_json_getitem_as_float(self):
|
|
136
|
+
stmt = select(json_table).where(json_table.c.data["score"].as_float() > 3.5)
|
|
137
|
+
sql = _compile(stmt)
|
|
138
|
+
assert "JSON_EXTRACT" in sql
|
|
139
|
+
assert "CAST" in sql
|
|
140
|
+
assert "DOUBLE" in sql
|
|
141
|
+
|
|
142
|
+
def test_json_null_handling_for_string(self):
|
|
143
|
+
stmt = select(json_table.c.data["name"].as_string())
|
|
144
|
+
sql = _compile(stmt)
|
|
145
|
+
assert "WHEN 'null' THEN NULL" in sql
|
|
146
|
+
|
|
147
|
+
def test_json_getitem_as_boolean(self):
|
|
148
|
+
stmt = select(json_table).where(
|
|
149
|
+
json_table.c.data["active"].as_boolean() == True # noqa: E712
|
|
150
|
+
)
|
|
151
|
+
sql = _compile(stmt)
|
|
152
|
+
assert "WHEN 'true' THEN 1" in sql
|
|
153
|
+
assert "WHEN 'false' THEN 0" in sql
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestJSONKeyEscaping:
|
|
157
|
+
def test_key_with_embedded_quote(self):
|
|
158
|
+
idx = JSONIndexType()
|
|
159
|
+
assert idx._format_value('a"b') == '$."a""b"'
|
|
160
|
+
|
|
161
|
+
def test_key_with_dot(self):
|
|
162
|
+
idx = JSONIndexType()
|
|
163
|
+
assert idx._format_value("a.b") == '$."a.b"'
|
|
164
|
+
|
|
165
|
+
def test_key_with_space(self):
|
|
166
|
+
idx = JSONIndexType()
|
|
167
|
+
assert idx._format_value("a b") == '$."a b"'
|
|
168
|
+
|
|
169
|
+
def test_empty_string_key(self):
|
|
170
|
+
idx = JSONIndexType()
|
|
171
|
+
assert idx._format_value("") == '$.""'
|
|
172
|
+
|
|
173
|
+
def test_path_with_embedded_quote(self):
|
|
174
|
+
pth = JSONPathType()
|
|
175
|
+
assert pth._format_value(('a"b', "c")) == '$."a""b"."c"'
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class TestFuncJSONExtract:
|
|
179
|
+
def test_func_json_extract(self):
|
|
180
|
+
stmt = select(func.JSON_EXTRACT(json_table.c.data, "$.name"))
|
|
181
|
+
sql = _compile(stmt)
|
|
182
|
+
assert "JSON_EXTRACT" in sql
|
|
183
|
+
assert "$.name" in sql
|
|
184
|
+
|
|
185
|
+
def test_func_json_contains(self):
|
|
186
|
+
stmt = select(json_table).where(
|
|
187
|
+
func.JSON_CONTAINS(json_table.c.data, '"value"', "$.key") == 1
|
|
188
|
+
)
|
|
189
|
+
sql = _compile(stmt)
|
|
190
|
+
assert "JSON_CONTAINS" in sql
|
|
191
|
+
|
|
192
|
+
def test_func_json_object(self):
|
|
193
|
+
stmt = select(func.JSON_OBJECT("key", "value"))
|
|
194
|
+
sql = _compile(stmt)
|
|
195
|
+
assert "JSON_OBJECT" in sql
|
|
196
|
+
|
|
197
|
+
def test_func_json_array(self):
|
|
198
|
+
stmt = select(func.JSON_ARRAY(1, 2, 3))
|
|
199
|
+
sql = _compile(stmt)
|
|
200
|
+
assert "JSON_ARRAY" in sql
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class TestColspecs:
|
|
204
|
+
def test_generic_json_maps_to_cubrid_json(self):
|
|
205
|
+
from sqlalchemy_cubrid.dialect import colspecs
|
|
206
|
+
from sqlalchemy.sql import sqltypes
|
|
207
|
+
|
|
208
|
+
assert sqltypes.JSON in colspecs
|
|
209
|
+
assert colspecs[sqltypes.JSON] is JSON
|
|
210
|
+
|
|
211
|
+
def test_json_index_type_mapped(self):
|
|
212
|
+
from sqlalchemy_cubrid.dialect import colspecs
|
|
213
|
+
from sqlalchemy.sql import sqltypes
|
|
214
|
+
|
|
215
|
+
assert sqltypes.JSON.JSONIndexType in colspecs
|
|
216
|
+
assert colspecs[sqltypes.JSON.JSONIndexType] is JSONIndexType
|
|
217
|
+
|
|
218
|
+
def test_json_path_type_mapped(self):
|
|
219
|
+
from sqlalchemy_cubrid.dialect import colspecs
|
|
220
|
+
from sqlalchemy.sql import sqltypes
|
|
221
|
+
|
|
222
|
+
assert sqltypes.JSON.JSONPathType in colspecs
|
|
223
|
+
assert colspecs[sqltypes.JSON.JSONPathType] is JSONPathType
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TestIschemaNames:
|
|
227
|
+
def test_json_in_ischema_names(self):
|
|
228
|
+
from sqlalchemy_cubrid.dialect import ischema_names
|
|
229
|
+
|
|
230
|
+
assert "JSON" in ischema_names
|
|
231
|
+
assert ischema_names["JSON"] is JSON
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class TestJSONExport:
|
|
235
|
+
def test_json_in_init_all(self):
|
|
236
|
+
import sqlalchemy_cubrid
|
|
237
|
+
|
|
238
|
+
assert "JSON" in sqlalchemy_cubrid.__all__
|
|
239
|
+
assert "JSONIndexType" in sqlalchemy_cubrid.__all__
|
|
240
|
+
assert "JSONPathType" in sqlalchemy_cubrid.__all__
|
|
241
|
+
|
|
242
|
+
def test_json_importable(self):
|
|
243
|
+
from sqlalchemy_cubrid import JSON as ImportedJSON
|
|
244
|
+
|
|
245
|
+
assert ImportedJSON is JSON
|
|
246
|
+
|
|
247
|
+
def test_json_index_type_importable(self):
|
|
248
|
+
from sqlalchemy_cubrid import JSONIndexType as ImportedJIT
|
|
249
|
+
|
|
250
|
+
assert ImportedJIT is JSONIndexType
|
|
251
|
+
|
|
252
|
+
def test_json_path_type_importable(self):
|
|
253
|
+
from sqlalchemy_cubrid import JSONPathType as ImportedJPT
|
|
254
|
+
|
|
255
|
+
assert ImportedJPT is JSONPathType
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class TestFormatTypeMixinProcessors:
|
|
259
|
+
def _dialect(self):
|
|
260
|
+
from sqlalchemy_cubrid.dialect import CubridDialect
|
|
261
|
+
|
|
262
|
+
return CubridDialect()
|
|
263
|
+
|
|
264
|
+
def test_format_value_not_implemented(self):
|
|
265
|
+
from sqlalchemy_cubrid.types import _FormatTypeMixin
|
|
266
|
+
|
|
267
|
+
with pytest.raises(NotImplementedError):
|
|
268
|
+
_FormatTypeMixin()._format_value("anything")
|
|
269
|
+
|
|
270
|
+
def test_index_bind_processor_int(self):
|
|
271
|
+
proc = JSONIndexType().bind_processor(self._dialect())
|
|
272
|
+
assert proc is not None
|
|
273
|
+
assert proc(0) == "$[0]"
|
|
274
|
+
|
|
275
|
+
def test_index_bind_processor_string_escapes(self):
|
|
276
|
+
proc = JSONIndexType().bind_processor(self._dialect())
|
|
277
|
+
assert proc('a"b') == '$."a""b"'
|
|
278
|
+
|
|
279
|
+
def test_index_literal_processor_int(self):
|
|
280
|
+
proc = JSONIndexType().literal_processor(self._dialect())
|
|
281
|
+
assert proc is not None
|
|
282
|
+
result = proc(2)
|
|
283
|
+
assert "$[2]" in result
|
|
284
|
+
|
|
285
|
+
def test_index_literal_processor_string(self):
|
|
286
|
+
proc = JSONIndexType().literal_processor(self._dialect())
|
|
287
|
+
result = proc("name")
|
|
288
|
+
assert '$."name"' in result
|
|
289
|
+
|
|
290
|
+
def test_path_bind_processor(self):
|
|
291
|
+
proc = JSONPathType().bind_processor(self._dialect())
|
|
292
|
+
assert proc is not None
|
|
293
|
+
assert proc(("a", 1, "b")) == '$."a"[1]."b"'
|
|
294
|
+
|
|
295
|
+
def test_path_literal_processor(self):
|
|
296
|
+
proc = JSONPathType().literal_processor(self._dialect())
|
|
297
|
+
result = proc(("x", 0))
|
|
298
|
+
assert '$."x"[0]' in result
|
|
299
|
+
|
|
300
|
+
def test_index_bind_processor_no_super(self, monkeypatch):
|
|
301
|
+
"""Cover the branch where string_bind_processor returns None."""
|
|
302
|
+
idx = JSONIndexType()
|
|
303
|
+
monkeypatch.setattr(idx, "string_bind_processor", lambda dialect: None)
|
|
304
|
+
proc = idx.bind_processor(self._dialect())
|
|
305
|
+
assert proc("k") == '$."k"'
|
|
306
|
+
|
|
307
|
+
def test_index_literal_processor_no_super(self, monkeypatch):
|
|
308
|
+
"""Cover the branch where string_literal_processor returns None."""
|
|
309
|
+
idx = JSONIndexType()
|
|
310
|
+
monkeypatch.setattr(idx, "string_literal_processor", lambda dialect: None)
|
|
311
|
+
proc = idx.literal_processor(self._dialect())
|
|
312
|
+
assert proc("k") == '$."k"'
|
|
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.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/sqlalchemy_cubrid.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{sqlalchemy_cubrid-1.1.0 → sqlalchemy_cubrid-1.2.0}/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
|