implement iso date literals for all backends

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
This commit is contained in:
Mike Bayer
2022-03-28 13:29:23 -04:00
parent 4acf50c8e8
commit 952383f9ee
13 changed files with 99 additions and 69 deletions
+10
View File
@@ -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.
+1 -1
View 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
+2 -1
View 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
+1 -1
View 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
+1 -1
View File
@@ -8,8 +8,8 @@
import datetime
from ... import exc
from ... import types as sqltypes
from ... import util
from ...sql import sqltypes
class _NumericType:
+27 -1
View 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 = {
+35 -27
View 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
+1 -1
View 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
@@ -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
+2 -28
View 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
+15 -3
View 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."""
+2 -2
View 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),
+1 -2
View 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):