diff --git a/sqeleton/abcs/__init__.py b/sqeleton/abcs/__init__.py index 8659875..6359a7f 100644 --- a/sqeleton/abcs/__init__.py +++ b/sqeleton/abcs/__init__.py @@ -10,5 +10,6 @@ PrecisionType, StringType, Boolean, + JSONType, ) from .compiler import AbstractCompiler, Compilable diff --git a/sqeleton/abcs/database_types.py b/sqeleton/abcs/database_types.py index db591a8..c182b0e 100644 --- a/sqeleton/abcs/database_types.py +++ b/sqeleton/abcs/database_types.py @@ -134,6 +134,22 @@ class Text(StringType): supported = False +class JSONType(ColType): + pass + + +class RedShiftSuper(JSONType): + pass + + +class PostgresqlJSON(JSONType): + pass + + +class PostgresqlJSONB(JSONType): + pass + + @dataclass class Integer(NumericType, IKey): precision: int = 0 diff --git a/sqeleton/abcs/mixins.py b/sqeleton/abcs/mixins.py index a98844c..f6aaa4d 100644 --- a/sqeleton/abcs/mixins.py +++ b/sqeleton/abcs/mixins.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from .database_types import TemporalType, FractionalType, ColType_UUID, Boolean, ColType, String_UUID +from .database_types import TemporalType, FractionalType, ColType_UUID, Boolean, ColType, String_UUID, JSONType from .compiler import Compilable @@ -49,6 +49,10 @@ def normalize_uuid(self, value: str, coltype: ColType_UUID) -> str: return f"TRIM({value})" return self.to_string(value) + def normalize_json(self, value: str, _coltype: JSONType) -> str: + """Creates an SQL expression, that converts 'value' to its minified json string representation.""" + raise NotImplementedError() + def normalize_value_by_type(self, value: str, coltype: ColType) -> str: """Creates an SQL expression, that converts 'value' to a normalized representation. @@ -73,6 +77,8 @@ def normalize_value_by_type(self, value: str, coltype: ColType) -> str: return self.normalize_uuid(value, coltype) elif isinstance(coltype, Boolean): return self.normalize_boolean(value, coltype) + elif isinstance(coltype, JSONType): + return self.normalize_json(value, coltype) return self.to_string(value) diff --git a/sqeleton/databases/base.py b/sqeleton/databases/base.py index 514a9de..68a30f2 100644 --- a/sqeleton/databases/base.py +++ b/sqeleton/databases/base.py @@ -35,6 +35,7 @@ DbTime, DbPath, Boolean, + JSONType ) from ..abcs.mixins import Compilable from ..abcs.mixins import ( @@ -259,6 +260,9 @@ def parse_type( elif issubclass(cls, (Text, Native_UUID)): return cls() + elif issubclass(cls, JSONType): + return cls() + raise TypeError(f"Parsing {type_repr} returned an unknown type '{cls}'.") def _convert_db_precision_to_digits(self, p: int) -> int: diff --git a/sqeleton/databases/postgresql.py b/sqeleton/databases/postgresql.py index 7809770..eef2485 100644 --- a/sqeleton/databases/postgresql.py +++ b/sqeleton/databases/postgresql.py @@ -11,6 +11,8 @@ FractionalType, Boolean, Date, + PostgresqlJSON, + PostgresqlJSONB ) from ..abcs.mixins import AbstractMixin_MD5, AbstractMixin_NormalizeValue from .base import BaseDialect, ThreadedDatabase, import_helper, ConnectError, Mixin_Schema @@ -49,6 +51,9 @@ def normalize_number(self, value: str, coltype: FractionalType) -> str: def normalize_boolean(self, value: str, _coltype: Boolean) -> str: return self.to_string(f"{value}::int") + def normalize_json(self, value: str, _coltype: PostgresqlJSON) -> str: + return f"{value}::text" + class PostgresqlDialect(BaseDialect, Mixin_Schema): name = "PostgreSQL" @@ -76,6 +81,9 @@ class PostgresqlDialect(BaseDialect, Mixin_Schema): "character varying": Text, "varchar": Text, "text": Text, + # JSON + "json": PostgresqlJSON, + "jsonb": PostgresqlJSONB, # UUID "uuid": Native_UUID, # Boolean diff --git a/sqeleton/databases/redshift.py b/sqeleton/databases/redshift.py index 350218e..f0d03be 100644 --- a/sqeleton/databases/redshift.py +++ b/sqeleton/databases/redshift.py @@ -1,5 +1,12 @@ from typing import List, Dict -from ..abcs.database_types import Float, TemporalType, FractionalType, DbPath, TimestampTZ +from ..abcs.database_types import ( + Float, + TemporalType, + FractionalType, + DbPath, + TimestampTZ, + RedShiftSuper +) from ..abcs.mixins import AbstractMixin_MD5 from .postgresql import ( PostgreSQL, @@ -40,6 +47,9 @@ def normalize_timestamp(self, value: str, coltype: TemporalType) -> str: def normalize_number(self, value: str, coltype: FractionalType) -> str: return self.to_string(f"{value}::decimal(38,{coltype.precision})") + def normalize_json(self, value: str, _coltype: RedShiftSuper) -> str: + return f'nvl2({value}, json_serialize({value}), NULL)' + class Dialect(PostgresqlDialect): name = "Redshift" @@ -47,6 +57,8 @@ class Dialect(PostgresqlDialect): **PostgresqlDialect.TYPE_CLASSES, "double": Float, "real": Float, + # JSON + "super": RedShiftSuper } SUPPORTS_INDEXES = False