diff options
author | mike bayer <mike_mp@zzzcomputing.com> | 2022-06-01 16:13:36 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@ci3.zzzcomputing.com> | 2022-06-01 16:13:36 +0000 |
commit | 7b6fb299bb6b47dfeb22a5650b95af7fa0b35ec2 (patch) | |
tree | 84d683a496c9951838adb4efc09687f7c55b05af /lib/sqlalchemy/sql | |
parent | 79dbe94bb4ccd75888d57f388195a3ba4fa6117e (diff) | |
parent | 349a7c5e0e2aeeac98fad789b0043a4bdfeed837 (diff) | |
download | sqlalchemy-7b6fb299bb6b47dfeb22a5650b95af7fa0b35ec2.tar.gz |
Merge "add backend agnostic UUID datatype" into main
Diffstat (limited to 'lib/sqlalchemy/sql')
-rw-r--r-- | lib/sqlalchemy/sql/_elements_constructors.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/coercions.py | 16 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 45 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/elements.py | 23 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/sqltypes.py | 257 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/type_api.py | 8 |
6 files changed, 325 insertions, 26 deletions
diff --git a/lib/sqlalchemy/sql/_elements_constructors.py b/lib/sqlalchemy/sql/_elements_constructors.py index 8aa8f12cc..f6dd92865 100644 --- a/lib/sqlalchemy/sql/_elements_constructors.py +++ b/lib/sqlalchemy/sql/_elements_constructors.py @@ -390,7 +390,7 @@ def not_(clause: _ColumnExpressionArgument[_T]) -> ColumnElement[_T]: def bindparam( key: Optional[str], value: Any = _NoArg.NO_ARG, - type_: Optional[TypeEngine[_T]] = None, + type_: Optional[_TypeEngineArgument[_T]] = None, unique: bool = False, required: Union[bool, Literal[_NoArg.NO_ARG]] = _NoArg.NO_ARG, quote: Optional[bool] = None, diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py index 501188b12..d56035db7 100644 --- a/lib/sqlalchemy/sql/coercions.py +++ b/lib/sqlalchemy/sql/coercions.py @@ -629,14 +629,26 @@ class LiteralValueImpl(RoleImpl): _resolve_literal_only = True def _implicit_coercions( - self, element, resolved, argname, type_=None, **kw + self, + element, + resolved, + argname, + type_=None, + literal_execute=False, + **kw, ): if not _is_literal(resolved): self._raise_for_expected( element, resolved=resolved, argname=argname, **kw ) - return elements.BindParameter(None, element, type_=type_, unique=True) + return elements.BindParameter( + None, + element, + type_=type_, + unique=True, + literal_execute=literal_execute, + ) def _literal_coercion(self, element, argname=None, type_=None, **kw): return element diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 12a598717..3685751b0 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -3013,14 +3013,14 @@ class SQLCompiler(Compiled): def bindparam_string( self, - name, - positional_names=None, - post_compile=False, - expanding=False, - escaped_from=None, - bindparam_type=None, - **kw, - ): + name: str, + positional_names: Optional[List[str]] = None, + post_compile: bool = False, + expanding: bool = False, + escaped_from: Optional[str] = None, + bindparam_type: Optional[TypeEngine[Any]] = None, + **kw: Any, + ) -> str: if self.positional: if positional_names is not None: @@ -3045,9 +3045,23 @@ class SQLCompiler(Compiled): {escaped_from: name} ) if post_compile: - return "__[POSTCOMPILE_%s]" % name - - ret = self.bindtemplate % {"name": name} + ret = "__[POSTCOMPILE_%s]" % name + if expanding: + # for expanding, bound parameters or literal values will be + # rendered per item + return ret + + # otherwise, for non-expanding "literal execute", apply + # bind casts as determined by the datatype + if bindparam_type is not None: + type_impl = bindparam_type._unwrapped_dialect_impl( + self.dialect + ) + if type_impl.render_literal_cast: + ret = self.render_bind_cast(bindparam_type, type_impl, ret) + return ret + else: + ret = self.bindtemplate % {"name": name} if ( bindparam_type is not None @@ -5432,10 +5446,12 @@ class GenericTypeCompiler(TypeCompiler): def visit_NCLOB(self, type_, **kw): return "NCLOB" - def _render_string_type(self, type_, name): + def _render_string_type(self, type_, name, length_override=None): text = name - if type_.length: + if length_override: + text += "(%d)" % length_override + elif type_.length: text += "(%d)" % type_.length if type_.collation: text += ' COLLATE "%s"' % type_.collation @@ -5468,6 +5484,9 @@ class GenericTypeCompiler(TypeCompiler): def visit_BOOLEAN(self, type_, **kw): return "BOOLEAN" + def visit_uuid(self, type_, **kw): + return self._render_string_type(type_, "CHAR", length_override=32) + def visit_large_binary(self, type_, **kw): return self.visit_BLOB(type_, **kw) diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 61c5379d8..ce08a0a10 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -122,7 +122,9 @@ _NMT = TypeVar("_NMT", bound="_NUMBER") def literal( - value: Any, type_: Optional[_TypeEngineArgument[_T]] = None + value: Any, + type_: Optional[_TypeEngineArgument[_T]] = None, + literal_execute: bool = False, ) -> BindParameter[_T]: r"""Return a literal clause, bound to a bind parameter. @@ -136,13 +138,24 @@ def literal( :class:`BindParameter` with a bound value. :param value: the value to be bound. Can be any Python object supported by - the underlying DB-API, or is translatable via the given type argument. + the underlying DB-API, or is translatable via the given type argument. - :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` which - will provide bind-parameter translation for this literal. + :param type\_: an optional :class:`~sqlalchemy.types.TypeEngine` which will + provide bind-parameter translation for this literal. + + :param literal_execute: optional bool, when True, the SQL engine will + attempt to render the bound value directly in the SQL statement at + execution time rather than providing as a parameter value. + + .. versionadded:: 2.0 """ - return coercions.expect(roles.LiteralValueRole, value, type_=type_) + return coercions.expect( + roles.LiteralValueRole, + value, + type_=type_, + literal_execute=literal_execute, + ) def literal_column( diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 90b4b9c9e..50cb32503 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -30,6 +30,7 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union +from uuid import UUID as _python_UUID from . import coercions from . import elements @@ -689,11 +690,30 @@ class Double(Float[_N]): class _RenderISO8601NoT: - def literal_processor(self, dialect): - def process(value): - if value is not None: - value = f"""'{value.isoformat().replace("T", " ")}'""" - return value + def _literal_processor_datetime(self, dialect): + return self._literal_processor_portion(dialect, None) + + def _literal_processor_date(self, dialect): + return self._literal_processor_portion(dialect, 0) + + def _literal_processor_time(self, dialect): + return self._literal_processor_portion(dialect, -1) + + def _literal_processor_portion(self, dialect, _portion=None): + assert _portion in (None, 0, -1) + if _portion is not None: + + def process(value): + if value is not None: + value = f"""'{value.isoformat().split("T")[_portion]}'""" + return value + + else: + + def process(value): + if value is not None: + value = f"""'{value.isoformat().replace("T", " ")}'""" + return value return process @@ -746,6 +766,9 @@ class DateTime( else: return self + def literal_processor(self, dialect): + return self._literal_processor_datetime(dialect) + @property def python_type(self): return dt.datetime @@ -775,6 +798,9 @@ class Date(_RenderISO8601NoT, HasExpressionLookup, TypeEngine[dt.date]): def python_type(self): return dt.date + def literal_processor(self, dialect): + return self._literal_processor_date(dialect) + @util.memoized_property def _expression_adaptations(self): # Based on https://www.postgresql.org/docs/current/\ @@ -833,6 +859,9 @@ class Time(_RenderISO8601NoT, HasExpressionLookup, TypeEngine[dt.time]): operators.sub: {Time: Interval, Interval: self.__class__}, } + def literal_processor(self, dialect): + return self._literal_processor_time(dialect) + class _Binary(TypeEngine[bytes]): @@ -3302,6 +3331,223 @@ class MatchType(Boolean): """ +_UUID_RETURN = TypeVar("_UUID_RETURN", str, _python_UUID) + + +class Uuid(TypeEngine[_UUID_RETURN]): + + """Represent a database agnostic UUID datatype. + + For backends that have no "native" UUID datatype, the value will + make use of ``CHAR(32)`` and store the UUID as a 32-character alphanumeric + hex string. + + For backends which are known to support ``UUID`` directly or a similar + uuid-storing datatype such as SQL Server's ``UNIQUEIDENTIFIER``, a + "native" mode enabled by default allows these types will be used on those + backends. + + .. versionadded:: 2.0 + + .. seealso:: + + :class:`_sqltypes.UUID` - represents exactly the ``UUID`` datatype + without any backend-agnostic behaviors. + + """ + + __visit_name__ = "uuid" + + collation = None + + @overload + def __init__( + self: "Uuid[_python_UUID]", + as_uuid: Literal[True] = ..., + native_uuid: bool = ..., + ): + ... + + @overload + def __init__( + self: "Uuid[str]", + as_uuid: Literal[False] = ..., + native_uuid: bool = ..., + ): + ... + + def __init__(self, as_uuid: bool = True, native_uuid: bool = True): + """Construct a :class:`_sqltypes.Uuid` type. + + :param as_uuid=True: if True, values will be interpreted + as Python uuid objects, converting to/from string via the + DBAPI. + + .. versionchanged: 2.0 ``as_uuid`` now defaults to ``True``. + + :param native_uuid=True: if True, backends that support either the + ``UUID`` datatype directly, or a UUID-storing value + (such as SQL Server's ``UNIQUEIDENTIFIER`` will be used by those + backends. If False, a ``CHAR(32)`` datatype will be used for + all backends regardless of native support. + + """ + self.as_uuid = as_uuid + self.native_uuid = native_uuid + + @property + def python_type(self): + return _python_UUID if self.as_uuid else str + + def coerce_compared_value(self, op, value): + """See :meth:`.TypeEngine.coerce_compared_value` for a description.""" + + if isinstance(value, str): + return self + else: + return super().coerce_compared_value(op, value) + + def bind_processor(self, dialect): + character_based_uuid = ( + not dialect.supports_native_uuid or not self.native_uuid + ) + + if character_based_uuid: + if self.as_uuid: + + def process(value): + if value is not None: + value = value.hex + return value + + return process + else: + + def process(value): + if value is not None: + value = value.replace("-", "") + return value + + return process + else: + return None + + def result_processor(self, dialect, coltype): + character_based_uuid = ( + not dialect.supports_native_uuid or not self.native_uuid + ) + + if character_based_uuid: + if self.as_uuid: + + def process(value): + if value is not None: + value = _python_UUID(value) + return value + + return process + else: + + def process(value): + if value is not None: + value = str(_python_UUID(value)) + return value + + return process + else: + + if not self.as_uuid: + + def process(value): + if value is not None: + value = str(value) + return value + + return process + else: + return None + + def literal_processor(self, dialect): + character_based_uuid = ( + not dialect.supports_native_uuid or not self.native_uuid + ) + + if not self.as_uuid: + + def process(value): + if value is not None: + value = ( + f"""'{value.replace("-", "").replace("'", "''")}'""" + ) + return value + + return process + else: + if character_based_uuid: + + def process(value): + if value is not None: + value = f"""'{value.hex}'""" + return value + + return process + else: + + def process(value): + if value is not None: + value = f"""'{str(value).replace("'", "''")}'""" + return value + + return process + + +class UUID(Uuid[_UUID_RETURN]): + + """Represent the SQL UUID type. + + This is the SQL-native form of the :class:`_types.Uuid` database agnostic + datatype, and is backwards compatible with the previous PostgreSQL-only + version of ``UUID``. + + The :class:`_sqltypes.UUID` datatype only works on databases that have a + SQL datatype named ``UUID``. It will not function for backends which don't + have this exact-named type, including SQL Server. For backend-agnostic UUID + values with native support, including for SQL Server's ``UNIQUEIDENTIFIER`` + datatype, use the :class:`_sqltypes.Uuid` datatype. + + .. versionadded:: 2.0 + + .. seealso:: + + :class:`_sqltypes.Uuid` + + """ + + __visit_name__ = "UUID" + + @overload + def __init__(self: "UUID[_python_UUID]", as_uuid: Literal[True] = ...): + ... + + @overload + def __init__(self: "UUID[str]", as_uuid: Literal[False] = ...): + ... + + def __init__(self, as_uuid: bool = True): + """Construct a :class:`_sqltypes.UUID` type. + + + :param as_uuid=True: if True, values will be interpreted + as Python uuid objects, converting to/from string via the + DBAPI. + + .. versionchanged: 2.0 ``as_uuid`` now defaults to ``True``. + + """ + self.as_uuid = as_uuid + self.native_uuid = True + + NULLTYPE = NullType() BOOLEANTYPE = Boolean() STRINGTYPE = String() @@ -3319,6 +3565,7 @@ _type_map: Dict[Type[Any], TypeEngine[Any]] = { int: Integer(), float: Float(), bool: BOOLEANTYPE, + _python_UUID: Uuid(), decimal.Decimal: Numeric(), dt.date: Date(), dt.datetime: _DATETIME, diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py index b9847d406..00bae17bc 100644 --- a/lib/sqlalchemy/sql/type_api.py +++ b/lib/sqlalchemy/sql/type_api.py @@ -134,6 +134,14 @@ class TypeEngine(Visitable, Generic[_T]): """ + render_literal_cast = False + """render casts when rendering a value as an inline literal, + e.g. with :meth:`.TypeEngine.literal_processor`. + + .. versionadded:: 2.0 + + """ + class Comparator( ColumnOperators, Generic[_CT], |