summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql
diff options
context:
space:
mode:
authormike bayer <mike_mp@zzzcomputing.com>2022-06-01 16:13:36 +0000
committerGerrit Code Review <gerrit@ci3.zzzcomputing.com>2022-06-01 16:13:36 +0000
commit7b6fb299bb6b47dfeb22a5650b95af7fa0b35ec2 (patch)
tree84d683a496c9951838adb4efc09687f7c55b05af /lib/sqlalchemy/sql
parent79dbe94bb4ccd75888d57f388195a3ba4fa6117e (diff)
parent349a7c5e0e2aeeac98fad789b0043a4bdfeed837 (diff)
downloadsqlalchemy-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.py2
-rw-r--r--lib/sqlalchemy/sql/coercions.py16
-rw-r--r--lib/sqlalchemy/sql/compiler.py45
-rw-r--r--lib/sqlalchemy/sql/elements.py23
-rw-r--r--lib/sqlalchemy/sql/sqltypes.py257
-rw-r--r--lib/sqlalchemy/sql/type_api.py8
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],