From 8631ee5bf00bdd0c21ba313495dbb7966bb8f283 Mon Sep 17 00:00:00 2001 From: Yunus Koning Date: Thu, 12 Sep 2024 11:15:44 +0200 Subject: [PATCH] monetdb-dialect now passes 2.0.34 sqlalchemy test framework --- requirements.txt | 4 +-- setup.cfg | 4 +-- sqlalchemy_monetdb/compiler.py | 51 +++++++++------------------- sqlalchemy_monetdb/dialect.py | 53 +++++++++++++++++++++++++++--- sqlalchemy_monetdb/requirements.py | 2 +- test/test_suite.py | 9 +++-- tox.ini | 11 ------- 7 files changed, 76 insertions(+), 58 deletions(-) diff --git a/requirements.txt b/requirements.txt index fb366d3..c5969f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -pymonetdb==1.7.1 -sqlalchemy==2.0.20 +pymonetdb==1.8.2 +sqlalchemy==2.0.34 diff --git a/setup.cfg b/setup.cfg index 0e7967c..1845d26 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,8 +26,8 @@ license=MIT [options] packages = find: install_requires = - pymonetdb >= 1.7.1 - sqlalchemy > 2.0.0 + pymonetdb >= 1.8.2 + sqlalchemy > 2.0.34 python_requires = >=3.8 zip_safe = no test_suite = test.test_suite diff --git a/sqlalchemy_monetdb/compiler.py b/sqlalchemy_monetdb/compiler.py index 3955389..6a1f69c 100644 --- a/sqlalchemy_monetdb/compiler.py +++ b/sqlalchemy_monetdb/compiler.py @@ -75,16 +75,6 @@ def get_column_specification(self, column, **kwargs): colspec += " NOT NULL" return colspec - def visit_check_constraint(self, constraint, **kwargs): - # TODO: this turns out to be an error in pytest - # util.warn("Skipped unsupported check constraint %s" % constraint.name) - return None - - def visit_column_check_constraint(self, constraint, **kw): - # TODO: this turns out to be an error in pytest - # util.warn("Skipped unsupported check constraint %s" % constraint.name) - return None - def visit_create_index(self, create, **kw): preparer = self.preparer index = create.element @@ -346,32 +336,23 @@ def visit_regexp_replace_op_binary(self, binary, operator, **kw): self.render_literal_value(flags, sqltypes.STRINGTYPE), ) - def visit_json_getitem_op_binary( - self, binary, operator, _cast_applied=False, **kw - ): + def _render_json_extract_from_binary(self, binary, operator, _cast_applied=False, **kw): + if ( + not _cast_applied + and binary.type._type_affinity is not sqltypes.JSON + ): + kw["_cast_applied"] = True + return self.process(cast(binary, binary.type), **kw) + + left = self.process(binary.left, **kw) + right = self.process(binary.right, **kw) if binary.type._type_affinity is sqltypes.JSON: - expr = "cast (json.filter(%s, %s) as json)" + return "JSON.FILTER(%s, %s)" % (left, right) else: - expr = "json.filter(%s, %s)" + return "CASE JSON.FILTER(%s, %s) WHEN 'null' THEN NULL ELSE JSON.TEXT(JSON.FILTER(%s, %s)) END" % (left, right, left, right) - if not _cast_applied: - kw['_cast_applied'] = True - return self.process(cast(cast(binary, sqltypes.STRINGTYPE), binary.type), **kw) + def visit_json_getitem_op_binary(self, binary, operator, _cast_applied=False, **kw): + return self._render_json_extract_from_binary(binary, operator, _cast_applied, **kw) - return expr % ( - self.process(binary.left, **kw), - self.process(binary.right, **kw), - ) - - def visit_json_path_getitem_op_binary( - self, binary, operator, _cast_applied=False, **kw - ): - if not _cast_applied: - kw['_cast_applied'] = True - return self.process(cast(cast(binary, sqltypes.STRINGTYPE), binary.type), **kw) - - # pdb.set_trace() - return "json.filter(%s, %s)" % ( - self.process(binary.left, **kw), - self.process(binary.right, **kw), - ) + def visit_json_path_getitem_op_binary(self, binary, operator, _cast_applied=False, **kw): + return self._render_json_extract_from_binary(binary, operator, _cast_applied, **kw) diff --git a/sqlalchemy_monetdb/dialect.py b/sqlalchemy_monetdb/dialect.py index 30bceed..052f0b6 100644 --- a/sqlalchemy_monetdb/dialect.py +++ b/sqlalchemy_monetdb/dialect.py @@ -1,7 +1,7 @@ import json import re import typing -from typing import Optional +from typing import Optional, List, Any from collections import defaultdict from sqlalchemy import text @@ -11,6 +11,8 @@ from sqlalchemy import pool, exc from sqlalchemy.engine import default, reflection, ObjectScope, ObjectKind +from sqlalchemy.engine.interfaces import ReflectedCheckConstraint +from sqlalchemy.sql import sqltypes from sqlalchemy_monetdb.base import MonetExecutionContext, MonetIdentifierPreparer from sqlalchemy_monetdb.compiler import ( @@ -18,7 +20,7 @@ MonetTypeCompiler, MonetCompiler, ) -from sqlalchemy_monetdb.monetdb_types import MONETDB_TYPE_MAP +from sqlalchemy_monetdb.monetdb_types import MONETDB_TYPE_MAP, JSONPathType import pymonetdb @@ -67,6 +69,10 @@ class MonetDialect(default.DefaultDialect): type_compiler = MonetTypeCompiler default_paramstyle = "named" + colspecs = { + sqltypes.JSON.JSONPathType: JSONPathType, + } + def __init__(self, json_serializer=None, json_deserializer=None, **kwargs): default.DefaultDialect.__init__(self, **kwargs) self._json_serializer = json_serializer @@ -892,8 +898,47 @@ def get_unique_constraints( res = [{"column_names": c, "name": n} for n, c in col_dict.items()] return res - def get_check_constraints(self, connection, table_name, schema=None, **kw): - return [] + + def get_check_constraints(self, connection: "Connection", table_name: str, schema: str | None = None, **kw:Any) -> List[ReflectedCheckConstraint]: + """Return information about check constraints in `table_name`. + + Given a string `table_name` and an optional string `schema`, return + check constraint information as a list of dicts with these keys: + + name + name of check constraint + + sqltext + the check constraint’s SQL expression + + **kw + other options passed to the dialect's get_check_constraints() method. + + .. versionadded:: 2.0.0 + + """ + + q = """ + SELECT k.name name, sys.check_constraint(:schema, k.name) sqltext + FROM + sys.tables t, + sys.keys k + WHERE + k.table_id = t.id AND + t.id = :table_id AND + k.type = 4 + order by name + """ + + if schema is None: + schema = connection.execute(text("SELECT current_schema")).scalar() + + args = {"table_id": self._table_id(connection, table_name, schema), "schema": schema} + c = connection.execute(text(q), args) + table = c.fetchall() + + res = [{"name": name, "sqltext": sqltext} for name, sqltext in table] + return res def get_isolation_level_values(self, dbapi_conn): return ( diff --git a/sqlalchemy_monetdb/requirements.py b/sqlalchemy_monetdb/requirements.py index e0fffad..4ad1047 100644 --- a/sqlalchemy_monetdb/requirements.py +++ b/sqlalchemy_monetdb/requirements.py @@ -716,7 +716,7 @@ def unique_constraints_reflect_as_index(self): @property def check_constraint_reflection(self): """target dialect supports reflection of check constraints""" - return exclusions.closed() + return exclusions.open() @property def duplicate_key_raises_integrity_error(self): diff --git a/test/test_suite.py b/test/test_suite.py index 065b808..2594bad 100644 --- a/test/test_suite.py +++ b/test/test_suite.py @@ -19,6 +19,9 @@ def test_limit_render_multiple_times(*args, **kwargs): class CTETest(CTETest): pass -# @pytest.mark.skip(reason="The dialect is not supporting JSON type") -# class JSONTest(JSONTest): -# pass +class JSONTest: + @pytest.mark.skip(reason="MonetDB normalizes json input " + "by removing whitespace. " + "This is unexpected in this test.") + def test_round_trip_custom_json(self): + pass diff --git a/tox.ini b/tox.ini index 6843117..79bd1c8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,3 @@ -# [tox] -# envlist = py{3.7,3.8,3.9,3.10}-sqlalchemy{13,14}-{pymonetdb,monetdbe} - -# [testenv] -# deps = -# pymonetdb: pymonetdb -# monetdbe: monetdbe -# pytest -# coverage -# commands=py.test - [tox] minversion = 4.11.3 envlist = py{38, 39, 310, 311}, flake8