]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
implement iso date literals for all backends
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 28 Mar 2022 17:29:23 +0000 (13:29 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 8 Apr 2022 18:13:51 +0000 (14:13 -0400)
Added modified ISO-8601 rendering (i.e. ISO-8601 with the T converted to a
space) when using ``literal_binds`` with the SQL compilers provided by the
PostgreSQL, MySQL, MariaDB, MSSQL, Oracle dialects. For Oracle, the ISO
format is wrapped inside of an appropriate TO_DATE() function call.
Previously this rendering was not implemented for dialect-specific
compilation.

Fixes: #5052
Change-Id: I7af15a51fedf5c5a8e76e645f7c3be997ece35f0

13 files changed:
doc/build/changelog/unreleased_20/5052.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/dialects/mssql/pyodbc.py
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/mysql/types.py
lib/sqlalchemy/dialects/oracle/base.py
lib/sqlalchemy/dialects/oracle/cx_oracle.py
lib/sqlalchemy/dialects/postgresql/pg8000.py
lib/sqlalchemy/dialects/postgresql/psycopg.py
lib/sqlalchemy/engine/default.py
lib/sqlalchemy/sql/sqltypes.py
test/dialect/oracle/test_types.py
test/requirements.py

diff --git a/doc/build/changelog/unreleased_20/5052.rst b/doc/build/changelog/unreleased_20/5052.rst
new file mode 100644 (file)
index 0000000..235f547
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: usecase, datatypes
+    :tickets: 5052
+
+    Added modified ISO-8601 rendering (i.e. ISO-8601 with the T converted to a
+    space) when using ``literal_binds`` with the SQL compilers provided by the
+    PostgreSQL, MySQL, MariaDB, MSSQL, Oracle dialects. For Oracle, the ISO
+    format is wrapped inside of an appropriate TO_DATE() function call.
+    Previously this rendering was not implemented for dialect-specific
+    compilation.
index ac02b98a04d6fdf94fb89c5900dd9fe71fb14505..35428b659a86972c848b48fff560edf2d8690fb4 100644 (file)
@@ -823,7 +823,6 @@ from ... import schema as sa_schema
 from ... import Sequence
 from ... import sql
 from ... import text
-from ... import types as sqltypes
 from ... import util
 from ...engine import cursor as _cursor
 from ...engine import default
@@ -835,6 +834,7 @@ from ...sql import expression
 from ...sql import func
 from ...sql import quoted_name
 from ...sql import roles
+from ...sql import sqltypes
 from ...sql import util as sql_util
 from ...sql._typing import is_sql_compiler
 from ...types import BIGINT
index 530a0a48096e29108faede74fee4dc1b8272a407..f6c9afc49be3eaa55e36c6b655138938249494c7 100644 (file)
@@ -310,6 +310,7 @@ import decimal
 import re
 import struct
 
+from .base import _MSDateTime
 from .base import BINARY
 from .base import DATETIMEOFFSET
 from .base import MSDialect
@@ -447,7 +448,7 @@ class _ODBCDateTimeBindProcessor:
         return process
 
 
-class _ODBCDateTime(_ODBCDateTimeBindProcessor, sqltypes.DateTime):
+class _ODBCDateTime(_ODBCDateTimeBindProcessor, _MSDateTime):
     pass
 
 
index cfdc3deb2baa8acabf19e72d118c5dc1db74e7b6..25f4c6945aa8561f9744a1dc617e03bd75b968f5 100644 (file)
@@ -1020,7 +1020,6 @@ from ... import exc
 from ... import log
 from ... import schema as sa_schema
 from ... import sql
-from ... import types as sqltypes
 from ... import util
 from ...engine import default
 from ...engine import reflection
@@ -1030,6 +1029,7 @@ from ...sql import elements
 from ...sql import functions
 from ...sql import operators
 from ...sql import roles
+from ...sql import sqltypes
 from ...sql import util as sql_util
 from ...sql.sqltypes import Unicode
 from ...types import BINARY
index 2a4b4ad5fbe23c92e57aa2679e521d5149f90195..95e85f602e4ea1675ae0dfcaef6112d733788983 100644 (file)
@@ -8,8 +8,8 @@
 import datetime
 
 from ... import exc
-from ... import types as sqltypes
 from ... import util
+from ...sql import sqltypes
 
 
 class _NumericType:
index 39a542cce87d88b020c092bc064a6e4b6ba15d9e..7cd4603a846332701c330a88b5a8286467339046 100644 (file)
@@ -688,7 +688,28 @@ class LONG(sqltypes.Text):
     __visit_name__ = "LONG"
 
 
-class DATE(sqltypes.DateTime):
+class _OracleDateLiteralRender:
+    def literal_processor(self, dialect):
+        def process(value):
+            if value is not None:
+                if getattr(value, "microsecond", None):
+                    value = (
+                        f"""TO_TIMESTAMP"""
+                        f"""('{value.isoformat().replace("T", " ")}', """
+                        """'YYYY-MM-DD HH24:MI:SS.FF')"""
+                    )
+                else:
+                    value = (
+                        f"""TO_DATE"""
+                        f"""('{value.isoformat().replace("T", " ")}', """
+                        """'YYYY-MM-DD HH24:MI:SS')"""
+                    )
+            return value
+
+        return process
+
+
+class DATE(_OracleDateLiteralRender, sqltypes.DateTime):
     """Provide the oracle DATE type.
 
     This type has no special Python behavior, except that it subclasses
@@ -705,6 +726,10 @@ class DATE(sqltypes.DateTime):
         return other._type_affinity in (sqltypes.DateTime, sqltypes.Date)
 
 
+class _OracleDate(_OracleDateLiteralRender, sqltypes.Date):
+    pass
+
+
 class INTERVAL(sqltypes.NativeForEmulated, sqltypes._AbstractInterval):
     __visit_name__ = "INTERVAL"
 
@@ -763,6 +788,7 @@ colspecs = {
     sqltypes.Boolean: _OracleBoolean,
     sqltypes.Interval: INTERVAL,
     sqltypes.DateTime: DATE,
+    sqltypes.Date: _OracleDate,
 }
 
 ischema_names = {
index 9f3394533167fde6f67070c17cce078351ff1209..f8cdd50e4e4425b4289e03b11dfcf9a22d4840bb 100644 (file)
@@ -440,11 +440,11 @@ from .base import OracleCompiler
 from .base import OracleDialect
 from .base import OracleExecutionContext
 from ... import exc
-from ... import types as sqltypes
 from ... import util
 from ...engine import cursor as _cursor
 from ...engine import interfaces
 from ...engine import processors
+from ...sql import sqltypes
 from ...sql._typing import is_sql_compiler
 
 
@@ -563,7 +563,7 @@ class _OracleNUMBER(_OracleNumeric):
     is_number = True
 
 
-class _OracleDate(sqltypes.Date):
+class _CXOracleDate(oracle._OracleDate):
     def bind_processor(self, dialect):
         return None
 
@@ -577,6 +577,10 @@ class _OracleDate(sqltypes.Date):
         return process
 
 
+class _CXOracleTIMESTAMP(oracle._OracleDateLiteralRender, sqltypes.TIMESTAMP):
+    pass
+
+
 # TODO: the names used across CHAR / VARCHAR / NCHAR / NVARCHAR
 # here are inconsistent and not very good
 class _OracleChar(sqltypes.CHAR):
@@ -847,31 +851,35 @@ class OracleDialect_cx_oracle(OracleDialect):
 
     driver = "cx_oracle"
 
-    colspecs = {
-        sqltypes.Numeric: _OracleNumeric,
-        sqltypes.Float: _OracleNumeric,
-        oracle.BINARY_FLOAT: _OracleBINARY_FLOAT,
-        oracle.BINARY_DOUBLE: _OracleBINARY_DOUBLE,
-        sqltypes.Integer: _OracleInteger,
-        oracle.NUMBER: _OracleNUMBER,
-        sqltypes.Date: _OracleDate,
-        sqltypes.LargeBinary: _OracleBinary,
-        sqltypes.Boolean: oracle._OracleBoolean,
-        sqltypes.Interval: _OracleInterval,
-        oracle.INTERVAL: _OracleInterval,
-        sqltypes.Text: _OracleText,
-        sqltypes.String: _OracleString,
-        sqltypes.UnicodeText: _OracleUnicodeTextCLOB,
-        sqltypes.CHAR: _OracleChar,
-        sqltypes.NCHAR: _OracleNChar,
-        sqltypes.Enum: _OracleEnum,
-        oracle.LONG: _OracleLong,
-        oracle.RAW: _OracleRaw,
-        sqltypes.Unicode: _OracleUnicodeStringCHAR,
-        sqltypes.NVARCHAR: _OracleUnicodeStringNCHAR,
-        oracle.NCLOB: _OracleUnicodeTextNCLOB,
-        oracle.ROWID: _OracleRowid,
-    }
+    colspecs = OracleDialect.colspecs
+    colspecs.update(
+        {
+            sqltypes.TIMESTAMP: _CXOracleTIMESTAMP,
+            sqltypes.Numeric: _OracleNumeric,
+            sqltypes.Float: _OracleNumeric,
+            oracle.BINARY_FLOAT: _OracleBINARY_FLOAT,
+            oracle.BINARY_DOUBLE: _OracleBINARY_DOUBLE,
+            sqltypes.Integer: _OracleInteger,
+            oracle.NUMBER: _OracleNUMBER,
+            sqltypes.Date: _CXOracleDate,
+            sqltypes.LargeBinary: _OracleBinary,
+            sqltypes.Boolean: oracle._OracleBoolean,
+            sqltypes.Interval: _OracleInterval,
+            oracle.INTERVAL: _OracleInterval,
+            sqltypes.Text: _OracleText,
+            sqltypes.String: _OracleString,
+            sqltypes.UnicodeText: _OracleUnicodeTextCLOB,
+            sqltypes.CHAR: _OracleChar,
+            sqltypes.NCHAR: _OracleNChar,
+            sqltypes.Enum: _OracleEnum,
+            oracle.LONG: _OracleLong,
+            oracle.RAW: _OracleRaw,
+            sqltypes.Unicode: _OracleUnicodeStringCHAR,
+            sqltypes.NVARCHAR: _OracleUnicodeStringNCHAR,
+            oracle.NCLOB: _OracleUnicodeTextNCLOB,
+            oracle.ROWID: _OracleRowid,
+        }
+    )
 
     execute_sequence_format = list
 
index 372b8639e05549b582ad649f8644d2040235a0b9..1ae649ef9e1e0c88e2f279b8745fc9c56ea04ab5 100644 (file)
@@ -108,9 +108,9 @@ from .json import JSON
 from .json import JSONB
 from .json import JSONPathType
 from ... import exc
-from ... import types as sqltypes
 from ... import util
 from ...engine import processors
+from ...sql import sqltypes
 from ...sql.elements import quoted_name
 
 
index 33dc65afc54d1e9324483967acb7320931ea525a..9207221df6d706b2ac252298d9e0a00affe3106c 100644 (file)
@@ -69,9 +69,9 @@ from .json import JSON
 from .json import JSONB
 from .json import JSONPathType
 from ... import pool
-from ... import types as sqltypes
 from ... import util
 from ...engine import AdaptedConnection
+from ...sql import sqltypes
 from ...util.concurrency import await_fallback
 from ...util.concurrency import await_only
 
index 4d700866fe07f770c90998d176a16677a6b7eb29..b30cf9c0884d05b06716a37c99b6b61ff47eb275 100644 (file)
@@ -47,10 +47,10 @@ from .interfaces import ExecutionContext
 from .. import event
 from .. import exc
 from .. import pool
-from .. import types as sqltypes
 from .. import util
 from ..sql import compiler
 from ..sql import expression
+from ..sql import type_api
 from ..sql._typing import is_tuple_type
 from ..sql.compiler import DDLCompiler
 from ..sql.compiler import SQLCompiler
@@ -498,7 +498,7 @@ class DefaultDialect(Dialect):
         and passes on to :func:`_types.adapt_type`.
 
         """
-        return sqltypes.adapt_type(typeobj, self.colspecs)
+        return type_api.adapt_type(typeobj, self.colspecs)
 
     def has_index(self, connection, table_name, index_name, schema=None):
         if not self.has_table(connection, table_name, schema=schema):
@@ -746,26 +746,6 @@ class DefaultDialect(Dialect):
         return connection
 
 
-class _RendersLiteral:
-    def literal_processor(self, dialect):
-        def process(value):
-            return "'%s'" % value
-
-        return process
-
-
-class _StrDateTime(_RendersLiteral, sqltypes.DateTime):
-    pass
-
-
-class _StrDate(_RendersLiteral, sqltypes.Date):
-    pass
-
-
-class _StrTime(_RendersLiteral, sqltypes.Time):
-    pass
-
-
 class StrCompileDialect(DefaultDialect):
 
     statement_compiler = compiler.StrSQLCompiler
@@ -787,12 +767,6 @@ class StrCompileDialect(DefaultDialect):
     supports_multivalues_insert = True
     supports_simple_order_by_label = True
 
-    colspecs = {
-        sqltypes.DateTime: _StrDateTime,
-        sqltypes.Date: _StrDate,
-        sqltypes.Time: _StrTime,
-    }
-
 
 class DefaultExecutionContext(ExecutionContext):
     isinsert = False
index 1a6de34b03a429b38921e3599f3da76fb70507f0..803e85654894cdc3f0810c74fc26e9f5482fbfda 100644 (file)
@@ -700,7 +700,19 @@ class Double(Float[_N]):
     __visit_name__ = "double"
 
 
-class DateTime(HasExpressionLookup, TypeEngine[dt.datetime]):
+class _RenderISO8601NoT:
+    def literal_processor(self, dialect):
+        def process(value):
+            if value is not None:
+                value = f"""'{value.isoformat().replace("T", " ")}'"""
+            return value
+
+        return process
+
+
+class DateTime(
+    _RenderISO8601NoT, HasExpressionLookup, TypeEngine[dt.datetime]
+):
 
     """A type for ``datetime.datetime()`` objects.
 
@@ -762,7 +774,7 @@ class DateTime(HasExpressionLookup, TypeEngine[dt.datetime]):
         }
 
 
-class Date(HasExpressionLookup, TypeEngine[dt.date]):
+class Date(_RenderISO8601NoT, HasExpressionLookup, TypeEngine[dt.date]):
 
     """A type for ``datetime.date()`` objects."""
 
@@ -800,7 +812,7 @@ class Date(HasExpressionLookup, TypeEngine[dt.date]):
         }
 
 
-class Time(HasExpressionLookup, TypeEngine[dt.time]):
+class Time(_RenderISO8601NoT, HasExpressionLookup, TypeEngine[dt.time]):
 
     """A type for ``datetime.time()`` objects."""
 
index d3c522f2593c12dcd0f5faaeb21d8fde3117576d..8302ed5233b85bf8fd89d264dea6cc1d59e383df 100644 (file)
@@ -84,11 +84,11 @@ class DialectTypesTest(fixtures.TestBase, AssertsCompiledSQL):
         self.assert_compile(oracle.LONG(), "LONG")
 
     @testing.combinations(
-        (Date(), cx_oracle._OracleDate),
+        (Date(), cx_oracle._CXOracleDate),
         (oracle.OracleRaw(), cx_oracle._OracleRaw),
         (String(), String),
         (VARCHAR(), cx_oracle._OracleString),
-        (DATE(), cx_oracle._OracleDate),
+        (DATE(), cx_oracle._CXOracleDate),
         (oracle.DATE(), oracle.DATE),
         (String(50), cx_oracle._OracleString),
         (Unicode(), cx_oracle._OracleUnicodeStringCHAR),
index df6b5d62f5362ceedbb9759706ea7a9f06f20b85..6f85aff2c4ecc5a32aba8d8f7868a74fd6790375 100644 (file)
@@ -1047,8 +1047,7 @@ class DefaultRequirements(SuiteRequirements):
         literal string, e.g. via the TypeEngine.literal_processor() method.
 
         """
-
-        return fails_on_everything_except("sqlite")
+        return exclusions.open()
 
     @property
     def datetime(self):