mirror of
https://gitlab.com/MoonTestUse1/AdministrationItDepartmens.git
synced 2025-08-14 00:25:46 +02:00
Проверка 09.02.2025
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# dialects/mysql/base.py
|
||||
# Copyright (C) 2005-2024 the SQLAlchemy authors and contributors
|
||||
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
|
||||
# <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
@@ -11,7 +11,6 @@ r"""
|
||||
|
||||
.. dialect:: mysql
|
||||
:name: MySQL / MariaDB
|
||||
:full_support: 5.6, 5.7, 8.0 / 10.8, 10.9
|
||||
:normal_support: 5.6+ / 10+
|
||||
:best_effort: 5.0.2+ / 5.0.2+
|
||||
|
||||
@@ -35,7 +34,9 @@ syntactical and behavioral differences that SQLAlchemy accommodates automaticall
|
||||
To connect to a MariaDB database, no changes to the database URL are required::
|
||||
|
||||
|
||||
engine = create_engine("mysql+pymysql://user:pass@some_mariadb/dbname?charset=utf8mb4")
|
||||
engine = create_engine(
|
||||
"mysql+pymysql://user:pass@some_mariadb/dbname?charset=utf8mb4"
|
||||
)
|
||||
|
||||
Upon first connect, the SQLAlchemy dialect employs a
|
||||
server version detection scheme that determines if the
|
||||
@@ -53,7 +54,9 @@ useful for the case where an application makes use of MariaDB-specific features
|
||||
and is not compatible with a MySQL database. To use this mode of operation,
|
||||
replace the "mysql" token in the above URL with "mariadb"::
|
||||
|
||||
engine = create_engine("mariadb+pymysql://user:pass@some_mariadb/dbname?charset=utf8mb4")
|
||||
engine = create_engine(
|
||||
"mariadb+pymysql://user:pass@some_mariadb/dbname?charset=utf8mb4"
|
||||
)
|
||||
|
||||
The above engine, upon first connect, will raise an error if the server version
|
||||
detection detects that the backing database is not MariaDB.
|
||||
@@ -99,7 +102,7 @@ the :paramref:`_sa.create_engine.pool_recycle` option which ensures that
|
||||
a connection will be discarded and replaced with a new one if it has been
|
||||
present in the pool for a fixed number of seconds::
|
||||
|
||||
engine = create_engine('mysql+mysqldb://...', pool_recycle=3600)
|
||||
engine = create_engine("mysql+mysqldb://...", pool_recycle=3600)
|
||||
|
||||
For more comprehensive disconnect detection of pooled connections, including
|
||||
accommodation of server restarts and network issues, a pre-ping approach may
|
||||
@@ -123,12 +126,14 @@ To accommodate the rendering of these arguments, specify the form
|
||||
``ENGINE`` of ``InnoDB``, ``CHARSET`` of ``utf8mb4``, and ``KEY_BLOCK_SIZE``
|
||||
of ``1024``::
|
||||
|
||||
Table('mytable', metadata,
|
||||
Column('data', String(32)),
|
||||
mysql_engine='InnoDB',
|
||||
mysql_charset='utf8mb4',
|
||||
mysql_key_block_size="1024"
|
||||
)
|
||||
Table(
|
||||
"mytable",
|
||||
metadata,
|
||||
Column("data", String(32)),
|
||||
mysql_engine="InnoDB",
|
||||
mysql_charset="utf8mb4",
|
||||
mysql_key_block_size="1024",
|
||||
)
|
||||
|
||||
When supporting :ref:`mysql_mariadb_only_mode` mode, similar keys against
|
||||
the "mariadb" prefix must be included as well. The values can of course
|
||||
@@ -137,19 +142,17 @@ be maintained::
|
||||
|
||||
# support both "mysql" and "mariadb-only" engine URLs
|
||||
|
||||
Table('mytable', metadata,
|
||||
Column('data', String(32)),
|
||||
|
||||
mysql_engine='InnoDB',
|
||||
mariadb_engine='InnoDB',
|
||||
|
||||
mysql_charset='utf8mb4',
|
||||
mariadb_charset='utf8',
|
||||
|
||||
mysql_key_block_size="1024"
|
||||
mariadb_key_block_size="1024"
|
||||
|
||||
)
|
||||
Table(
|
||||
"mytable",
|
||||
metadata,
|
||||
Column("data", String(32)),
|
||||
mysql_engine="InnoDB",
|
||||
mariadb_engine="InnoDB",
|
||||
mysql_charset="utf8mb4",
|
||||
mariadb_charset="utf8",
|
||||
mysql_key_block_size="1024",
|
||||
mariadb_key_block_size="1024",
|
||||
)
|
||||
|
||||
The MySQL / MariaDB dialects will normally transfer any keyword specified as
|
||||
``mysql_keyword_name`` to be rendered as ``KEYWORD_NAME`` in the
|
||||
@@ -215,16 +218,14 @@ techniques are used.
|
||||
To set isolation level using :func:`_sa.create_engine`::
|
||||
|
||||
engine = create_engine(
|
||||
"mysql+mysqldb://scott:tiger@localhost/test",
|
||||
isolation_level="READ UNCOMMITTED"
|
||||
)
|
||||
"mysql+mysqldb://scott:tiger@localhost/test",
|
||||
isolation_level="READ UNCOMMITTED",
|
||||
)
|
||||
|
||||
To set using per-connection execution options::
|
||||
|
||||
connection = engine.connect()
|
||||
connection = connection.execution_options(
|
||||
isolation_level="READ COMMITTED"
|
||||
)
|
||||
connection = connection.execution_options(isolation_level="READ COMMITTED")
|
||||
|
||||
Valid values for ``isolation_level`` include:
|
||||
|
||||
@@ -256,8 +257,8 @@ When creating tables, SQLAlchemy will automatically set ``AUTO_INCREMENT`` on
|
||||
the first :class:`.Integer` primary key column which is not marked as a
|
||||
foreign key::
|
||||
|
||||
>>> t = Table('mytable', metadata,
|
||||
... Column('mytable_id', Integer, primary_key=True)
|
||||
>>> t = Table(
|
||||
... "mytable", metadata, Column("mytable_id", Integer, primary_key=True)
|
||||
... )
|
||||
>>> t.create()
|
||||
CREATE TABLE mytable (
|
||||
@@ -271,10 +272,12 @@ This flag
|
||||
can also be used to enable auto-increment on a secondary column in a
|
||||
multi-column key for some storage engines::
|
||||
|
||||
Table('mytable', metadata,
|
||||
Column('gid', Integer, primary_key=True, autoincrement=False),
|
||||
Column('id', Integer, primary_key=True)
|
||||
)
|
||||
Table(
|
||||
"mytable",
|
||||
metadata,
|
||||
Column("gid", Integer, primary_key=True, autoincrement=False),
|
||||
Column("id", Integer, primary_key=True),
|
||||
)
|
||||
|
||||
.. _mysql_ss_cursors:
|
||||
|
||||
@@ -292,7 +295,9 @@ Server side cursors are enabled on a per-statement basis by using the
|
||||
option::
|
||||
|
||||
with engine.connect() as conn:
|
||||
result = conn.execution_options(stream_results=True).execute(text("select * from table"))
|
||||
result = conn.execution_options(stream_results=True).execute(
|
||||
text("select * from table")
|
||||
)
|
||||
|
||||
Note that some kinds of SQL statements may not be supported with
|
||||
server side cursors; generally, only SQL statements that return rows should be
|
||||
@@ -320,7 +325,8 @@ a connection. This is typically delivered using the ``charset`` parameter
|
||||
in the URL, such as::
|
||||
|
||||
e = create_engine(
|
||||
"mysql+pymysql://scott:tiger@localhost/test?charset=utf8mb4")
|
||||
"mysql+pymysql://scott:tiger@localhost/test?charset=utf8mb4"
|
||||
)
|
||||
|
||||
This charset is the **client character set** for the connection. Some
|
||||
MySQL DBAPIs will default this to a value such as ``latin1``, and some
|
||||
@@ -340,7 +346,8 @@ charset is preferred, if supported by both the database as well as the client
|
||||
DBAPI, as in::
|
||||
|
||||
e = create_engine(
|
||||
"mysql+pymysql://scott:tiger@localhost/test?charset=utf8mb4")
|
||||
"mysql+pymysql://scott:tiger@localhost/test?charset=utf8mb4"
|
||||
)
|
||||
|
||||
All modern DBAPIs should support the ``utf8mb4`` charset.
|
||||
|
||||
@@ -362,7 +369,9 @@ Dealing with Binary Data Warnings and Unicode
|
||||
MySQL versions 5.6, 5.7 and later (not MariaDB at the time of this writing) now
|
||||
emit a warning when attempting to pass binary data to the database, while a
|
||||
character set encoding is also in place, when the binary data itself is not
|
||||
valid for that encoding::
|
||||
valid for that encoding:
|
||||
|
||||
.. sourcecode:: text
|
||||
|
||||
default.py:509: Warning: (1300, "Invalid utf8mb4 character string:
|
||||
'F9876A'")
|
||||
@@ -372,7 +381,9 @@ This warning is due to the fact that the MySQL client library is attempting to
|
||||
interpret the binary string as a unicode object even if a datatype such
|
||||
as :class:`.LargeBinary` is in use. To resolve this, the SQL statement requires
|
||||
a binary "character set introducer" be present before any non-NULL value
|
||||
that renders like this::
|
||||
that renders like this:
|
||||
|
||||
.. sourcecode:: sql
|
||||
|
||||
INSERT INTO table (data) VALUES (_binary %s)
|
||||
|
||||
@@ -382,12 +393,13 @@ string parameter ``binary_prefix=true`` to the URL to repair this warning::
|
||||
|
||||
# mysqlclient
|
||||
engine = create_engine(
|
||||
"mysql+mysqldb://scott:tiger@localhost/test?charset=utf8mb4&binary_prefix=true")
|
||||
"mysql+mysqldb://scott:tiger@localhost/test?charset=utf8mb4&binary_prefix=true"
|
||||
)
|
||||
|
||||
# PyMySQL
|
||||
engine = create_engine(
|
||||
"mysql+pymysql://scott:tiger@localhost/test?charset=utf8mb4&binary_prefix=true")
|
||||
|
||||
"mysql+pymysql://scott:tiger@localhost/test?charset=utf8mb4&binary_prefix=true"
|
||||
)
|
||||
|
||||
The ``binary_prefix`` flag may or may not be supported by other MySQL drivers.
|
||||
|
||||
@@ -430,7 +442,10 @@ the ``first_connect`` and ``connect`` events::
|
||||
|
||||
from sqlalchemy import create_engine, event
|
||||
|
||||
eng = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo='debug')
|
||||
eng = create_engine(
|
||||
"mysql+mysqldb://scott:tiger@localhost/test", echo="debug"
|
||||
)
|
||||
|
||||
|
||||
# `insert=True` will ensure this is the very first listener to run
|
||||
@event.listens_for(eng, "connect", insert=True)
|
||||
@@ -438,6 +453,7 @@ the ``first_connect`` and ``connect`` events::
|
||||
cursor = dbapi_connection.cursor()
|
||||
cursor.execute("SET sql_mode = 'STRICT_ALL_TABLES'")
|
||||
|
||||
|
||||
conn = eng.connect()
|
||||
|
||||
In the example illustrated above, the "connect" event will invoke the "SET"
|
||||
@@ -454,8 +470,8 @@ MySQL / MariaDB SQL Extensions
|
||||
Many of the MySQL / MariaDB SQL extensions are handled through SQLAlchemy's generic
|
||||
function and operator support::
|
||||
|
||||
table.select(table.c.password==func.md5('plaintext'))
|
||||
table.select(table.c.username.op('regexp')('^[a-d]'))
|
||||
table.select(table.c.password == func.md5("plaintext"))
|
||||
table.select(table.c.username.op("regexp")("^[a-d]"))
|
||||
|
||||
And of course any valid SQL statement can be executed as a string as well.
|
||||
|
||||
@@ -468,11 +484,18 @@ available.
|
||||
* SELECT pragma, use :meth:`_expression.Select.prefix_with` and
|
||||
:meth:`_query.Query.prefix_with`::
|
||||
|
||||
select(...).prefix_with(['HIGH_PRIORITY', 'SQL_SMALL_RESULT'])
|
||||
select(...).prefix_with(["HIGH_PRIORITY", "SQL_SMALL_RESULT"])
|
||||
|
||||
* UPDATE with LIMIT::
|
||||
|
||||
update(..., mysql_limit=10, mariadb_limit=10)
|
||||
update(...).with_dialect_options(mysql_limit=10, mariadb_limit=10)
|
||||
|
||||
* DELETE
|
||||
with LIMIT::
|
||||
|
||||
delete(...).with_dialect_options(mysql_limit=10, mariadb_limit=10)
|
||||
|
||||
.. versionadded:: 2.0.37 Added delete with limit
|
||||
|
||||
* optimizer hints, use :meth:`_expression.Select.prefix_with` and
|
||||
:meth:`_query.Query.prefix_with`::
|
||||
@@ -484,14 +507,16 @@ available.
|
||||
|
||||
select(...).with_hint(some_table, "USE INDEX xyz")
|
||||
|
||||
* MATCH operator support::
|
||||
* MATCH
|
||||
operator support::
|
||||
|
||||
from sqlalchemy.dialects.mysql import match
|
||||
select(...).where(match(col1, col2, against="some expr").in_boolean_mode())
|
||||
from sqlalchemy.dialects.mysql import match
|
||||
|
||||
.. seealso::
|
||||
select(...).where(match(col1, col2, against="some expr").in_boolean_mode())
|
||||
|
||||
:class:`_mysql.match`
|
||||
.. seealso::
|
||||
|
||||
:class:`_mysql.match`
|
||||
|
||||
INSERT/DELETE...RETURNING
|
||||
-------------------------
|
||||
@@ -508,17 +533,15 @@ To specify an explicit ``RETURNING`` clause, use the
|
||||
|
||||
# INSERT..RETURNING
|
||||
result = connection.execute(
|
||||
table.insert().
|
||||
values(name='foo').
|
||||
returning(table.c.col1, table.c.col2)
|
||||
table.insert().values(name="foo").returning(table.c.col1, table.c.col2)
|
||||
)
|
||||
print(result.all())
|
||||
|
||||
# DELETE..RETURNING
|
||||
result = connection.execute(
|
||||
table.delete().
|
||||
where(table.c.name=='foo').
|
||||
returning(table.c.col1, table.c.col2)
|
||||
table.delete()
|
||||
.where(table.c.name == "foo")
|
||||
.returning(table.c.col1, table.c.col2)
|
||||
)
|
||||
print(result.all())
|
||||
|
||||
@@ -545,12 +568,11 @@ the generative method :meth:`~.mysql.Insert.on_duplicate_key_update`:
|
||||
>>> from sqlalchemy.dialects.mysql import insert
|
||||
|
||||
>>> insert_stmt = insert(my_table).values(
|
||||
... id='some_existing_id',
|
||||
... data='inserted value')
|
||||
... id="some_existing_id", data="inserted value"
|
||||
... )
|
||||
|
||||
>>> on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
|
||||
... data=insert_stmt.inserted.data,
|
||||
... status='U'
|
||||
... data=insert_stmt.inserted.data, status="U"
|
||||
... )
|
||||
>>> print(on_duplicate_key_stmt)
|
||||
{printsql}INSERT INTO my_table (id, data) VALUES (%s, %s)
|
||||
@@ -575,8 +597,8 @@ as values:
|
||||
.. sourcecode:: pycon+sql
|
||||
|
||||
>>> insert_stmt = insert(my_table).values(
|
||||
... id='some_existing_id',
|
||||
... data='inserted value')
|
||||
... id="some_existing_id", data="inserted value"
|
||||
... )
|
||||
|
||||
>>> on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
|
||||
... data="some data",
|
||||
@@ -639,13 +661,11 @@ table:
|
||||
.. sourcecode:: pycon+sql
|
||||
|
||||
>>> stmt = insert(my_table).values(
|
||||
... id='some_id',
|
||||
... data='inserted value',
|
||||
... author='jlh')
|
||||
... id="some_id", data="inserted value", author="jlh"
|
||||
... )
|
||||
|
||||
>>> do_update_stmt = stmt.on_duplicate_key_update(
|
||||
... data="updated value",
|
||||
... author=stmt.inserted.author
|
||||
... data="updated value", author=stmt.inserted.author
|
||||
... )
|
||||
|
||||
>>> print(do_update_stmt)
|
||||
@@ -690,13 +710,13 @@ MySQL and MariaDB both provide an option to create index entries with a certain
|
||||
become part of the index. SQLAlchemy provides this feature via the
|
||||
``mysql_length`` and/or ``mariadb_length`` parameters::
|
||||
|
||||
Index('my_index', my_table.c.data, mysql_length=10, mariadb_length=10)
|
||||
Index("my_index", my_table.c.data, mysql_length=10, mariadb_length=10)
|
||||
|
||||
Index('a_b_idx', my_table.c.a, my_table.c.b, mysql_length={'a': 4,
|
||||
'b': 9})
|
||||
Index("a_b_idx", my_table.c.a, my_table.c.b, mysql_length={"a": 4, "b": 9})
|
||||
|
||||
Index('a_b_idx', my_table.c.a, my_table.c.b, mariadb_length={'a': 4,
|
||||
'b': 9})
|
||||
Index(
|
||||
"a_b_idx", my_table.c.a, my_table.c.b, mariadb_length={"a": 4, "b": 9}
|
||||
)
|
||||
|
||||
Prefix lengths are given in characters for nonbinary string types and in bytes
|
||||
for binary string types. The value passed to the keyword argument *must* be
|
||||
@@ -713,7 +733,7 @@ MySQL storage engines permit you to specify an index prefix when creating
|
||||
an index. SQLAlchemy provides this feature via the
|
||||
``mysql_prefix`` parameter on :class:`.Index`::
|
||||
|
||||
Index('my_index', my_table.c.data, mysql_prefix='FULLTEXT')
|
||||
Index("my_index", my_table.c.data, mysql_prefix="FULLTEXT")
|
||||
|
||||
The value passed to the keyword argument will be simply passed through to the
|
||||
underlying CREATE INDEX, so it *must* be a valid index prefix for your MySQL
|
||||
@@ -730,11 +750,13 @@ Some MySQL storage engines permit you to specify an index type when creating
|
||||
an index or primary key constraint. SQLAlchemy provides this feature via the
|
||||
``mysql_using`` parameter on :class:`.Index`::
|
||||
|
||||
Index('my_index', my_table.c.data, mysql_using='hash', mariadb_using='hash')
|
||||
Index(
|
||||
"my_index", my_table.c.data, mysql_using="hash", mariadb_using="hash"
|
||||
)
|
||||
|
||||
As well as the ``mysql_using`` parameter on :class:`.PrimaryKeyConstraint`::
|
||||
|
||||
PrimaryKeyConstraint("data", mysql_using='hash', mariadb_using='hash')
|
||||
PrimaryKeyConstraint("data", mysql_using="hash", mariadb_using="hash")
|
||||
|
||||
The value passed to the keyword argument will be simply passed through to the
|
||||
underlying CREATE INDEX or PRIMARY KEY clause, so it *must* be a valid index
|
||||
@@ -753,9 +775,12 @@ CREATE FULLTEXT INDEX in MySQL also supports a "WITH PARSER" option. This
|
||||
is available using the keyword argument ``mysql_with_parser``::
|
||||
|
||||
Index(
|
||||
'my_index', my_table.c.data,
|
||||
mysql_prefix='FULLTEXT', mysql_with_parser="ngram",
|
||||
mariadb_prefix='FULLTEXT', mariadb_with_parser="ngram",
|
||||
"my_index",
|
||||
my_table.c.data,
|
||||
mysql_prefix="FULLTEXT",
|
||||
mysql_with_parser="ngram",
|
||||
mariadb_prefix="FULLTEXT",
|
||||
mariadb_with_parser="ngram",
|
||||
)
|
||||
|
||||
.. versionadded:: 1.3
|
||||
@@ -782,6 +807,7 @@ them ignored on a MySQL / MariaDB backend, use a custom compile rule::
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.schema import ForeignKeyConstraint
|
||||
|
||||
|
||||
@compiles(ForeignKeyConstraint, "mysql", "mariadb")
|
||||
def process(element, compiler, **kw):
|
||||
element.deferrable = element.initially = None
|
||||
@@ -803,10 +829,12 @@ very common ``MyISAM`` MySQL storage engine, the information loaded by table
|
||||
reflection will not include foreign keys. For these tables, you may supply a
|
||||
:class:`~sqlalchemy.ForeignKeyConstraint` at reflection time::
|
||||
|
||||
Table('mytable', metadata,
|
||||
ForeignKeyConstraint(['other_id'], ['othertable.other_id']),
|
||||
autoload_with=engine
|
||||
)
|
||||
Table(
|
||||
"mytable",
|
||||
metadata,
|
||||
ForeignKeyConstraint(["other_id"], ["othertable.other_id"]),
|
||||
autoload_with=engine,
|
||||
)
|
||||
|
||||
.. seealso::
|
||||
|
||||
@@ -878,13 +906,15 @@ parameter and pass a textual clause that also includes the ON UPDATE clause::
|
||||
mytable = Table(
|
||||
"mytable",
|
||||
metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('data', String(50)),
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("data", String(50)),
|
||||
Column(
|
||||
'last_updated',
|
||||
"last_updated",
|
||||
TIMESTAMP,
|
||||
server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
|
||||
)
|
||||
server_default=text(
|
||||
"CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
The same instructions apply to use of the :class:`_types.DateTime` and
|
||||
@@ -895,34 +925,37 @@ The same instructions apply to use of the :class:`_types.DateTime` and
|
||||
mytable = Table(
|
||||
"mytable",
|
||||
metadata,
|
||||
Column('id', Integer, primary_key=True),
|
||||
Column('data', String(50)),
|
||||
Column("id", Integer, primary_key=True),
|
||||
Column("data", String(50)),
|
||||
Column(
|
||||
'last_updated',
|
||||
"last_updated",
|
||||
DateTime,
|
||||
server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
|
||||
)
|
||||
server_default=text(
|
||||
"CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
Even though the :paramref:`_schema.Column.server_onupdate` feature does not
|
||||
generate this DDL, it still may be desirable to signal to the ORM that this
|
||||
updated value should be fetched. This syntax looks like the following::
|
||||
|
||||
from sqlalchemy.schema import FetchedValue
|
||||
|
||||
|
||||
class MyClass(Base):
|
||||
__tablename__ = 'mytable'
|
||||
__tablename__ = "mytable"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
data = Column(String(50))
|
||||
last_updated = Column(
|
||||
TIMESTAMP,
|
||||
server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"),
|
||||
server_onupdate=FetchedValue()
|
||||
server_default=text(
|
||||
"CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"
|
||||
),
|
||||
server_onupdate=FetchedValue(),
|
||||
)
|
||||
|
||||
|
||||
.. _mysql_timestamp_null:
|
||||
|
||||
TIMESTAMP Columns and NULL
|
||||
@@ -932,7 +965,9 @@ MySQL historically enforces that a column which specifies the
|
||||
TIMESTAMP datatype implicitly includes a default value of
|
||||
CURRENT_TIMESTAMP, even though this is not stated, and additionally
|
||||
sets the column as NOT NULL, the opposite behavior vs. that of all
|
||||
other datatypes::
|
||||
other datatypes:
|
||||
|
||||
.. sourcecode:: text
|
||||
|
||||
mysql> CREATE TABLE ts_test (
|
||||
-> a INTEGER,
|
||||
@@ -977,19 +1012,24 @@ SQLAlchemy also emits NOT NULL for TIMESTAMP columns that do specify
|
||||
from sqlalchemy.dialects.mysql import TIMESTAMP
|
||||
|
||||
m = MetaData()
|
||||
t = Table('ts_test', m,
|
||||
Column('a', Integer),
|
||||
Column('b', Integer, nullable=False),
|
||||
Column('c', TIMESTAMP),
|
||||
Column('d', TIMESTAMP, nullable=False)
|
||||
)
|
||||
t = Table(
|
||||
"ts_test",
|
||||
m,
|
||||
Column("a", Integer),
|
||||
Column("b", Integer, nullable=False),
|
||||
Column("c", TIMESTAMP),
|
||||
Column("d", TIMESTAMP, nullable=False),
|
||||
)
|
||||
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
e = create_engine("mysql+mysqldb://scott:tiger@localhost/test", echo=True)
|
||||
m.create_all(e)
|
||||
|
||||
output::
|
||||
output:
|
||||
|
||||
.. sourcecode:: sql
|
||||
|
||||
CREATE TABLE ts_test (
|
||||
a INTEGER,
|
||||
@@ -1349,7 +1389,7 @@ class MySQLCompiler(compiler.SQLCompiler):
|
||||
|
||||
clauses = []
|
||||
|
||||
requires_mysql8_alias = (
|
||||
requires_mysql8_alias = statement.select is None and (
|
||||
self.dialect._requires_alias_for_on_duplicate_key
|
||||
)
|
||||
|
||||
@@ -1359,10 +1399,17 @@ class MySQLCompiler(compiler.SQLCompiler):
|
||||
else:
|
||||
_on_dup_alias_name = "new"
|
||||
|
||||
# traverses through all table columns to preserve table column order
|
||||
for column in (col for col in cols if col.key in on_duplicate.update):
|
||||
val = on_duplicate.update[column.key]
|
||||
on_duplicate_update = {
|
||||
coercions.expect_as_key(roles.DMLColumnRole, key): value
|
||||
for key, value in on_duplicate.update.items()
|
||||
}
|
||||
|
||||
# traverses through all table columns to preserve table column order
|
||||
for column in (col for col in cols if col.key in on_duplicate_update):
|
||||
val = on_duplicate_update[column.key]
|
||||
|
||||
# TODO: this coercion should be up front. we can't cache
|
||||
# SQL constructs with non-bound literals buried in them
|
||||
if coercions._is_literal(val):
|
||||
val = elements.BindParameter(None, val, type_=column.type)
|
||||
value_text = self.process(val.self_group(), use_schema=False)
|
||||
@@ -1400,7 +1447,7 @@ class MySQLCompiler(compiler.SQLCompiler):
|
||||
name_text = self.preparer.quote(column.name)
|
||||
clauses.append("%s = %s" % (name_text, value_text))
|
||||
|
||||
non_matching = set(on_duplicate.update) - {c.key for c in cols}
|
||||
non_matching = set(on_duplicate_update) - {c.key for c in cols}
|
||||
if non_matching:
|
||||
util.warn(
|
||||
"Additional column names not matching "
|
||||
@@ -1678,8 +1725,15 @@ class MySQLCompiler(compiler.SQLCompiler):
|
||||
|
||||
def update_limit_clause(self, update_stmt):
|
||||
limit = update_stmt.kwargs.get("%s_limit" % self.dialect.name, None)
|
||||
if limit:
|
||||
return "LIMIT %s" % limit
|
||||
if limit is not None:
|
||||
return f"LIMIT {int(limit)}"
|
||||
else:
|
||||
return None
|
||||
|
||||
def delete_limit_clause(self, delete_stmt):
|
||||
limit = delete_stmt.kwargs.get("%s_limit" % self.dialect.name, None)
|
||||
if limit is not None:
|
||||
return f"LIMIT {int(limit)}"
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -1850,7 +1904,15 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
|
||||
else:
|
||||
default = self.get_column_default_string(column)
|
||||
if default is not None:
|
||||
colspec.append("DEFAULT " + default)
|
||||
if (
|
||||
isinstance(
|
||||
column.server_default.arg, functions.FunctionElement
|
||||
)
|
||||
and self.dialect._support_default_function
|
||||
):
|
||||
colspec.append(f"DEFAULT ({default})")
|
||||
else:
|
||||
colspec.append("DEFAULT " + default)
|
||||
return " ".join(colspec)
|
||||
|
||||
def post_create_table(self, table):
|
||||
@@ -2380,6 +2442,8 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
|
||||
def _visit_enumerated_values(self, name, type_, enumerated_values):
|
||||
quoted_enums = []
|
||||
for e in enumerated_values:
|
||||
if self.dialect.identifier_preparer._double_percents:
|
||||
e = e.replace("%", "%%")
|
||||
quoted_enums.append("'%s'" % e.replace("'", "''"))
|
||||
return self._extend_string(
|
||||
type_, {}, "%s(%s)" % (name, ",".join(quoted_enums))
|
||||
@@ -2493,6 +2557,7 @@ class MySQLDialect(default.DefaultDialect):
|
||||
construct_arguments = [
|
||||
(sa_schema.Table, {"*": None}),
|
||||
(sql.Update, {"limit": None}),
|
||||
(sql.Delete, {"limit": None}),
|
||||
(sa_schema.PrimaryKeyConstraint, {"using": None}),
|
||||
(
|
||||
sa_schema.Index,
|
||||
@@ -2893,6 +2958,17 @@ class MySQLDialect(default.DefaultDialect):
|
||||
# ref https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-17.html#mysqld-8-0-17-feature # noqa
|
||||
return self.server_version_info >= (8, 0, 17)
|
||||
|
||||
@property
|
||||
def _support_default_function(self):
|
||||
if not self.server_version_info:
|
||||
return False
|
||||
elif self.is_mariadb:
|
||||
# ref https://mariadb.com/kb/en/mariadb-1021-release-notes/
|
||||
return self.server_version_info >= (10, 2, 1)
|
||||
else:
|
||||
# ref https://dev.mysql.com/doc/refman/8.0/en/data-type-defaults.html # noqa
|
||||
return self.server_version_info >= (8, 0, 13)
|
||||
|
||||
@property
|
||||
def _is_mariadb(self):
|
||||
return self.is_mariadb
|
||||
@@ -3050,29 +3126,47 @@ class MySQLDialect(default.DefaultDialect):
|
||||
return s
|
||||
|
||||
default_schema_name = connection.dialect.default_schema_name
|
||||
col_tuples = [
|
||||
(
|
||||
lower(rec["referred_schema"] or default_schema_name),
|
||||
lower(rec["referred_table"]),
|
||||
col_name,
|
||||
)
|
||||
for rec in fkeys
|
||||
for col_name in rec["referred_columns"]
|
||||
]
|
||||
|
||||
if col_tuples:
|
||||
correct_for_wrong_fk_case = connection.execute(
|
||||
sql.text(
|
||||
"""
|
||||
select table_schema, table_name, column_name
|
||||
from information_schema.columns
|
||||
where (table_schema, table_name, lower(column_name)) in
|
||||
:table_data;
|
||||
"""
|
||||
).bindparams(sql.bindparam("table_data", expanding=True)),
|
||||
dict(table_data=col_tuples),
|
||||
# NOTE: using (table_schema, table_name, lower(column_name)) in (...)
|
||||
# is very slow since mysql does not seem able to properly use indexse.
|
||||
# Unpack the where condition instead.
|
||||
schema_by_table_by_column = defaultdict(lambda: defaultdict(list))
|
||||
for rec in fkeys:
|
||||
sch = lower(rec["referred_schema"] or default_schema_name)
|
||||
tbl = lower(rec["referred_table"])
|
||||
for col_name in rec["referred_columns"]:
|
||||
schema_by_table_by_column[sch][tbl].append(col_name)
|
||||
|
||||
if schema_by_table_by_column:
|
||||
|
||||
condition = sql.or_(
|
||||
*(
|
||||
sql.and_(
|
||||
_info_columns.c.table_schema == schema,
|
||||
sql.or_(
|
||||
*(
|
||||
sql.and_(
|
||||
_info_columns.c.table_name == table,
|
||||
sql.func.lower(
|
||||
_info_columns.c.column_name
|
||||
).in_(columns),
|
||||
)
|
||||
for table, columns in tables.items()
|
||||
)
|
||||
),
|
||||
)
|
||||
for schema, tables in schema_by_table_by_column.items()
|
||||
)
|
||||
)
|
||||
|
||||
select = sql.select(
|
||||
_info_columns.c.table_schema,
|
||||
_info_columns.c.table_name,
|
||||
_info_columns.c.column_name,
|
||||
).where(condition)
|
||||
|
||||
correct_for_wrong_fk_case = connection.execute(select)
|
||||
|
||||
# in casing=0, table name and schema name come back in their
|
||||
# exact case.
|
||||
# in casing=1, table name and schema name come back in lower
|
||||
@@ -3445,3 +3539,12 @@ class _DecodingRow:
|
||||
return item.decode(self.charset)
|
||||
else:
|
||||
return item
|
||||
|
||||
|
||||
_info_columns = sql.table(
|
||||
"columns",
|
||||
sql.column("table_schema", VARCHAR(64)),
|
||||
sql.column("table_name", VARCHAR(64)),
|
||||
sql.column("column_name", VARCHAR(64)),
|
||||
schema="information_schema",
|
||||
)
|
||||
|
Reference in New Issue
Block a user