Что нового в SQLAlchemy 1.1?

О данном документе

В данном документе описаны изменения между SQLAlchemy версии 1.0 и SQLAlchemy версии 1.1.

Введение

В данном руководстве рассказывается о том, что нового появилось в SQLAlchemy версии 1.1, а также документируются изменения, которые коснутся пользователей, переносящих свои приложения с SQLAlchemy серии 1.0 на 1.1.

Пожалуйста, внимательно изучите разделы, посвященные изменениям в поведении, на предмет потенциально несовместимых с обратным ходом изменений в поведении.

Изменения в платформе / инсталляторе

Setuptools теперь требуется для установки

Файл setup.py SQLAlchemy уже много лет поддерживает работу как с установленными Setuptools, так и без них, поддерживая «запасной» режим, в котором используется только Distutils. Поскольку Python-окружение без Setuptools сейчас не встречается, и для более полной поддержки набора возможностей Setuptools, в частности, для поддержки интеграции py.test с ним, а также таких вещей, как «extras», setup.py теперь полностью зависит от Setuptools.

#3489

Включение/отключение сборки расширений C осуществляется только через переменную окружения

Расширения C собираются по умолчанию во время установки, пока это возможно. Для отключения сборки расширений C начиная с версии SQLAlchemy 0.8.6 / 0.9.4 стала доступна переменная окружения DISABLE_SQLALCHEMY_CEXT. Прежний подход с использованием аргумента --without-cextensions был удален, так как он опирался на устаревшие возможности setuptools.

#3500

Новые возможности и усовершенствования - ORM

Новые события жизненного цикла сеанса

В Session уже давно поддерживаются события, позволяющие в той или иной степени отслеживать изменения состояния объектов, включая SessionEvents.before_attach(), SessionEvents.after_attach() и SessionEvents.before_flush(). Документация по сессиям также документирует основные состояния объектов по адресу Краткое введение в состояния объектов. Однако никогда не существовало системы, позволяющей отслеживать, как именно объекты проходят через эти переходы. Кроме того, статус «удаленных» объектов исторически был неясен, поскольку объекты действуют где-то между состояниями «persistent» и «detached».

Для того чтобы навести порядок в этой области и сделать сферу переходов между состояниями сессии полностью прозрачной, была добавлена новая серия событий, призванная охватить все возможные способы перехода объекта из одного состояния в другое, а статус «удален» получил собственное официальное название состояния в сфере состояний объектов сессии.

События перехода в новое состояние

Переходы между всеми состояниями объекта, такими как persistent, pending и другими, теперь могут быть перехвачены в терминах события сеансового уровня, предназначенного для конкретного перехода. Переходы при переходе объектов в состояние Session, выходе из состояния Session и даже все переходы, происходящие при откате транзакции с помощью Session.rollback(), явно присутствуют в интерфейсе SessionEvents.

Всего появилось десять новых событий. Краткое описание этих событий приведено в новом разделе документации События жизненного цикла объекта.

Добавлено новое состояние объекта «удален», удаленные объекты больше не являются «постоянными»

Состояние persistent объекта в Session всегда документировалось как объект, имеющий действительный идентификатор базы данных; однако в случае объектов, которые были удалены в результате промывки, они всегда находились в серой зоне, где они еще не совсем «отделены» от Session, поскольку все еще могут быть восстановлены в рамках отката, но не являются действительно «постоянными», поскольку их идентификатор базы данных был удален и они не присутствуют в карте идентификации.

Чтобы разрешить эту серую зону с учетом новых событий, вводится новое состояние объекта deleted. Это состояние находится между состояниями «persistent» и «detached». Объект, помеченный на удаление через Session.delete(), остается в состоянии «persistent» до тех пор, пока не произойдет flush; в этот момент он удаляется из карты идентификации, переходит в состояние «deleted», и вызывается хук SessionEvents.persistent_to_deleted(). Если транзакция объекта Session откатывается, то объект восстанавливается как постоянный; вызывается переход SessionEvents.deleted_to_persistent(). В противном случае, если транзакция объекта Session зафиксирована, вызывается переход SessionEvents.deleted_to_detached().

Кроме того, аксессуар InstanceState.persistent больше не возвращает True для объекта, находящегося в новом состоянии «удален»; вместо него был усовершенствован аксессуар InstanceState.deleted, который надежно сообщает об этом новом состоянии. Когда объект удален, аксессор InstanceState.deleted возвращает False, а аксессор InstanceState.detached - True. Для определения того, был ли объект удален в текущей или предыдущей транзакции, используется аксессор InstanceState.was_deleted.

Сильная карта идентичности устарела

Одним из стимулов для создания новой серии событий перехода было желание обеспечить герметичное отслеживание объектов при их перемещении в карте идентификации и из нее, чтобы можно было поддерживать «сильную ссылку», зеркально отражающую перемещение объекта в карте и из нее. Благодаря этой новой возможности отпала необходимость в параметре Session.weak_identity_map и соответствующем объекте StrongIdentityMap. Эта возможность оставалась в SQLAlchemy в течение многих лет, поскольку поведение «strong-referencing» было единственно возможным, и многие приложения были написаны с учетом этого поведения. Уже давно было рекомендовано, чтобы отслеживание объектов по сильным ссылкам не было внутренним делом Session, а было конструкцией на уровне приложения, создаваемой по мере необходимости; новая модель событий позволяет воспроизвести даже точное поведение карты сильной идентификации. Новый рецепт, иллюстрирующий замену карты сильной идентичности, приведен в Поведение при обращении к сеансам.

#2677

Новое событие init_scalar() перехватывает значения по умолчанию на уровне ORM

ORM выдает значение None при первом обращении к атрибуту, который не был установлен, для непостоянного объекта:

>>> obj = MyObj()
>>> obj.some_value
None

Есть вариант, когда это значение в Python должно соответствовать значению по умолчанию, сгенерированному в Core, еще до того, как объект будет сохранен. Для этого случая добавлено новое событие AttributeEvents.init_scalar(). Новый пример active_column_defaults.py в Инструментарий атрибутов иллюстрирует пример использования, поэтому вместо него может быть использован эффект:

>>> obj = MyObj()
>>> obj.some_value
"my default"

#1311

Изменения, касающиеся «нехешируемых» типов, влияют на дедупликацию строк ORM

Объект Query обладает хорошо известным поведением «вычитания» возвращаемых строк, содержащих хотя бы одну ORM-сопоставленную сущность (например, полный сопоставленный объект, а не отдельные значения столбцов). Это делается в первую очередь для того, чтобы работа с сущностями работала гладко в сочетании с картой идентичности, в том числе для учета дублирования сущностей, обычно представленных в объединенной загрузке eager, а также при использовании объединений для фильтрации по дополнительным столбцам.

Эта дедупликация основана на хэшируемости элементов в строке. С появлением в PostgreSQL специальных типов ARRAY, HSTORE и JSON опыт нехешируемых типов внутри строк и возникновения проблем здесь более распространен, чем раньше.

Действительно, в SQLAlchemy, начиная с версии 0.8, был установлен флаг для типов данных, отмеченных как «unhashable», однако этот флаг не использовался последовательно для встроенных типов. Как описано в В типах ARRAY и JSON теперь корректно указывается «unhashable», теперь этот флаг последовательно устанавливается для всех «структурных» типов PostgreSQL.

Флаг «unhashable» устанавливается и на типе NullType, так как NullType используется для обозначения любого выражения неизвестного типа.

Поскольку NullType применяется к большинству случаев использования func, так как func в большинстве случаев ничего не знает о заданных именах функций, использование func() часто отключает вычитание строк, если не применяется явная типизация. Следующие примеры иллюстрируют применение func.substr() к строковому выражению и func.date() к выражению datetime; оба примера вернут дублирующиеся строки из-за объединенной ускоренной загрузки, если не будет применена явная типизация:

result = (
    session.query(func.substr(A.some_thing, 0, 4), A).options(joinedload(A.bs)).all()
)

users = (
    session.query(
        func.date(User.date_created, "start of month").label("month"),
        User,
    )
    .options(joinedload(User.orders))
    .all()
)

Приведенные примеры, чтобы сохранить дедупликацию, должны быть указаны как:

result = (
    session.query(func.substr(A.some_thing, 0, 4, type_=String), A)
    .options(joinedload(A.bs))
    .all()
)

users = (
    session.query(
        func.date(User.date_created, "start of month", type_=DateTime).label("month"),
        User,
    )
    .options(joinedload(User.orders))
    .all()
)

Кроме того, обращение с так называемым «нехешируемым» типом несколько отличается от того, что было в предыдущих версиях; внутри мы используем функцию id() для получения «хеш-значения» из этих структур, как и для любого обычного отображаемого объекта. Это заменяет предыдущий подход, при котором к объекту применялся счетчик.

#3499

Добавлены специальные проверки для передачи сопоставленных классов и экземпляров в качестве SQL-литералов

Система типизации теперь имеет специальные проверки на передачу «проверяемых» объектов SQLAlchemy в контекстах, где в противном случае они будут обрабатываться как литеральные значения. Любой встроенный объект SQLAlchemy, который разрешено передавать в качестве SQL-значения (который еще не является экземпляром ClauseElement), включает метод __clause_element__(), который предоставляет допустимое SQL-выражение для этого объекта. Для объектов SQLAlchemy, которые этого не делают, таких как сопоставленные классы, мапперы и сопоставленные экземпляры, выдается более информативное сообщение об ошибке вместо того, чтобы позволить DBAPI получить объект и впоследствии потерпеть неудачу. Ниже показан пример, когда строковый атрибут User.name сравнивается с полным экземпляром User(), а не со строковым значением:

>>> some_user = User()
>>> q = s.query(User).filter(User.name == some_user)
sqlalchemy.exc.ArgumentError: Object <__main__.User object at 0x103167e90> is not legal as a SQL literal value

Теперь исключение происходит непосредственно при сравнении User.name == some_user. Ранее сравнение, подобное описанному выше, приводило к SQL-выражению, которое давало сбой только после преобразования в вызов выполнения DBAPI; сопоставленный объект User в конечном итоге становился связанным параметром, который отвергался DBAPI.

Обратите внимание, что в приведенном примере выражение не работает, поскольку User.name является строковым (например, ориентированным на столбец) атрибутом. Данное изменение не влияет на обычный случай сравнения атрибута отношения «многие-к-одному» с объектом, который обрабатывается по-другому:

>>> # Address.user refers to the User mapper, so
>>> # this is of course still OK!
>>> q = s.query(Address).filter(Address.user == some_user)

#3321

Новое расширение Indexable ORM

Расширение Индексируемый - это расширение функции гибридных атрибутов, позволяющее создавать атрибуты, ссылающиеся на определенные элементы «индексируемого» типа данных, такие как массив или поле JSON:

class Person(Base):
    __tablename__ = "person"

    id = Column(Integer, primary_key=True)
    data = Column(JSON)

    name = index_property("data", "name")

Выше, атрибут name будет читать/записывать поле "name" из JSON-столбца data, после инициализации его пустым словарем:

>>> person = Person(name="foobar")
>>> person.name
foobar

Расширение также запускает событие change при изменении атрибута, так что нет необходимости использовать MutableDict для отслеживания этого изменения.

См.также

Индексируемый

Новые опции, позволяющие явно сохранять NULL вместо значения по умолчанию

В связи с новой поддержкой JSON-NULL, добавленной в PostgreSQL в рамках JSON «null» вставляется, как и ожидалось, при операциях ORM, и опускается, если не присутствует, базовый класс TypeEngine теперь поддерживает метод TypeEngine.evaluates_none(), который позволяет сохранять положительный набор значений None в атрибуте как NULL, вместо того чтобы опускать столбец из оператора INSERT, что приводит к использованию значения по умолчанию на уровне столбца. Это позволяет на уровне маппера настроить существующую на объектном уровне технику присвоения атрибуту значения null().

#3250

Дополнительные исправления в запросе наследования одной таблицы

В продолжение темы Изменение критерия наследования одной таблицы при использовании from_self(), count() в версии 1.0, Query больше не должен неуместно добавлять критерий «единственного наследования», когда запрос выполняется против выражения подзапроса, такого как exists:

class Widget(Base):
    __tablename__ = "widget"
    id = Column(Integer, primary_key=True)
    type = Column(String)
    data = Column(String)
    __mapper_args__ = {"polymorphic_on": type}


class FooWidget(Widget):
    __mapper_args__ = {"polymorphic_identity": "foo"}


q = session.query(FooWidget).filter(FooWidget.data == "bar").exists()

session.query(q).all()

Производит:

SELECT EXISTS (SELECT 1
FROM widget
WHERE widget.data = :data_1 AND widget.type IN (:type_1)) AS anon_1

Предложение IN внутри уместно для того, чтобы ограничиться объектами FooWidget, однако ранее предложение IN также генерировалось второй раз во внешней части подзапроса.

#3582

Улучшено состояние сеанса при отмене SAVEPOINT базой данных

В MySQL часто встречается ситуация, когда SAVEPOINT отменяется при возникновении тупиковой ситуации внутри транзакции. В Session были внесены изменения для более изящного решения этой проблемы, в результате чего внешняя транзакция, не имеющая точки сохранения, остается пригодной для использования:

s = Session()
s.begin_nested()

s.add(SomeObject())

try:
    # assume the flush fails, flush goes to rollback to the
    # savepoint and that also fails
    s.flush()
except Exception as err:
    print("Something broke, and our SAVEPOINT vanished too")

# this is the SAVEPOINT transaction, marked as
# DEACTIVE so the rollback() call succeeds
s.rollback()

# this is the outermost transaction, remains ACTIVE
# so rollback() or commit() can succeed
s.rollback()

Этот выпуск является продолжением выпуска #2696, в котором мы выдаем предупреждение, чтобы исходная ошибка была видна при работе на Python 2, хотя исключение SAVEPOINT имеет приоритет. В Python 3 исключения объединяются в цепочку, поэтому оба сбоя сообщаются по отдельности.

#3680

Исправлены ошибки смывания «новый экземпляр X конфликтует с постоянным экземпляром Y»

Метод Session.rollback() отвечает за удаление объектов, которые были INSERTed в базу данных, например, переведены из состояния pending в состояние persistent, в рамках этой транзакции, которая теперь откатывается. Объекты, у которых произошло такое изменение состояния, отслеживаются в коллекции со слабыми ссылками, и если объект собирается из этой коллекции, то метод Session больше не беспокоится о нем (иначе он не сможет масштабироваться для операций вставки большого количества новых объектов в рамках транзакции). Однако возникает проблема, если приложение повторно загрузит ту же самую собранную в мусор строку внутри транзакции, до того как произойдет откат; если сильная ссылка на этот объект останется в следующей транзакции, то факт того, что этот объект не был вставлен и должен быть удален, будет потерян, и flush будет некорректно выдавать ошибку:

from sqlalchemy import Column, create_engine
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)


e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)

s = Session(e)

# persist an object
s.add(A(id=1))
s.flush()

# rollback buffer loses reference to A

# load it again, rollback buffer knows nothing
# about it
a1 = s.query(A).first()

# roll back the transaction; all state is expired but the
# "a1" reference remains
s.rollback()

# previous "a1" conflicts with the new one because we aren't
# checking that it never got committed
s.add(A(id=1))
s.commit()

Вышеуказанная программа позволит повысить:

FlushError: New instance <User at 0x7f0287eca4d0> with identity key
(<class 'test.orm.test_transaction.User'>, ('u1',)) conflicts
with persistent instance <User at 0x7f02889c70d0>

Ошибка заключается в том, что при возникновении указанного выше исключения единица работы оперирует исходным объектом, полагая, что это живая строка, в то время как на самом деле срок действия объекта истек и при проверке выясняется, что он исчез. Исправление теперь проверяет это условие, поэтому в журнале SQL мы видим:

BEGIN (implicit)

INSERT INTO a (id) VALUES (?)
(1,)

SELECT a.id AS a_id FROM a LIMIT ? OFFSET ?
(1, 0)

ROLLBACK

BEGIN (implicit)

SELECT a.id AS a_id FROM a WHERE a.id = ?
(1,)

INSERT INTO a (id) VALUES (?)
(1,)

COMMIT

Выше, теперь блок работы выполняет SELECT для строки, для которой мы собираемся сообщить о конфликте, видит, что ее не существует, и продолжает работу в обычном режиме. Затраты на этот SELECT возникают только в том случае, если мы в любом случае ошибочно вызвали бы исключение.

#3677

функция passive_deletes для сопоставлений с объединенным наследованием

Теперь в связке наследования объединенных таблиц можно выполнить DELETE в результате применения опции Session.delete(), которая выдает DELETE только для базовой таблицы, а не для таблицы подкласса, позволяя сконфигурировать ON DELETE CASCADE для настроенных внешних ключей. Это настраивается с помощью опции mapper.passive_deletes:

from sqlalchemy import Column, Integer, String, ForeignKey, create_engine
from sqlalchemy.orm import Session
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"
    id = Column("id", Integer, primary_key=True)
    type = Column(String)

    __mapper_args__ = {
        "polymorphic_on": type,
        "polymorphic_identity": "a",
        "passive_deletes": True,
    }


class B(A):
    __tablename__ = "b"
    b_table_id = Column("b_table_id", Integer, primary_key=True)
    bid = Column("bid", Integer, ForeignKey("a.id", ondelete="CASCADE"))
    data = Column("data", String)

    __mapper_args__ = {"polymorphic_identity": "b"}

В приведенном выше отображении опция mapper.passive_deletes настроена на базовом картографе; она действует для всех небазовых картографов, являющихся потомками картографа с установленной опцией. При DELETE для объекта типа B больше не нужно получать значение первичного ключа b_table_id, если он выгружен, и не нужно выдавать оператор DELETE для самой таблицы:

session.delete(some_b)
session.commit()

Выдаст SQL в виде:

DELETE FROM a WHERE a.id = %(id)s
-- {'id': 1}
COMMIT

Как обычно, целевая база данных должна иметь поддержку внешних ключей с включенной функцией ON DELETE CASCADE.

#2349

Одноименные обратные ссылки не будут вызывать ошибку при применении к конкретным подклассам наследования

Следующее отображение всегда было возможно без проблем:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b = relationship("B", foreign_keys="B.a_id", backref="a")


class A1(A):
    __tablename__ = "a1"
    id = Column(Integer, primary_key=True)
    b = relationship("B", foreign_keys="B.a1_id", backref="a1")
    __mapper_args__ = {"concrete": True}


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)

    a_id = Column(ForeignKey("a.id"))
    a1_id = Column(ForeignKey("a1.id"))

Выше, несмотря на то, что класс A и класс A1 имеют отношения с именем b, предупреждения или ошибки о конфликте не возникает, поскольку класс A1 помечен как «конкретный».

Однако если бы отношения были настроены по-другому, то возникла бы ошибка:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)


class A1(A):
    __tablename__ = "a1"
    id = Column(Integer, primary_key=True)
    __mapper_args__ = {"concrete": True}


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)

    a_id = Column(ForeignKey("a.id"))
    a1_id = Column(ForeignKey("a1.id"))

    a = relationship("A", backref="b")
    a1 = relationship("A1", backref="b")

В исправлении улучшена функция backref, чтобы не выдавалась ошибка, а также добавлена дополнительная проверка в логике маппера, чтобы обойти предупреждение о замене атрибута.

#3630

Одноименные отношения в наследующих картографах больше не предупреждают

При создании двух мапперов в сценарии наследования размещение отношения с одинаковым именем в обоих мапперах выдает предупреждение «relationship „<name>“ on mapper <name> supersates the same relationship on inherited mapper „<name>“; this can cause dependency problems during flush». В качестве примера можно привести следующий пример:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    bs = relationship("B")


class ASub(A):
    __tablename__ = "a_sub"
    id = Column(Integer, ForeignKey("a.id"), primary_key=True)
    bs = relationship("B")


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    a_id = Column(ForeignKey("a.id"))

Данное предупреждение относится к серии 0.4 2007 года и основано на версии кода единицы работы, которая с тех пор была полностью переписана. В настоящее время не существует известной проблемы, связанной с размещением одноименных отношений на базовом классе и классе-потомке, поэтому предупреждение снято. Однако следует отметить, что в реальной жизни этот вариант использования, скорее всего, не распространен, поэтому предупреждение снято. Хотя для этого случая добавлена элементарная тестовая поддержка, не исключено, что может быть выявлена какая-либо новая проблема, связанная с этим шаблоном.

Добавлено в версии 1.1.0b3.

#3749

Гибридные свойства и методы теперь распространяют документальную строку, а также .info

Гибридный метод или свойство теперь будет отражать значение __doc__, присутствующее в исходной строке docstring:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)

    name = Column(String)

    @hybrid_property
    def some_name(self):
        """The name field"""
        return self.name

Приведенное выше значение A.some_name.__doc__ теперь выполняется:

>>> A.some_name.__doc__
The name field

Однако для этого необходимо усложнить механику гибридных свойств. Раньше аксессор уровня класса для гибрида был простым проходом, т.е. этот тест был бы успешным:

>>> assert A.name is A.some_name

В результате изменения выражение, возвращаемое командой A.some_name, заворачивается внутрь своей собственной обертки QueryableAttribute:

>>> A.some_name
<sqlalchemy.orm.attributes.hybrid_propertyProxy object at 0x7fde03888230>

Было проведено много тестов для обеспечения корректной работы этой обертки, в том числе для сложных схем, таких как рецепт Custom Value Object, однако мы будем следить за тем, чтобы у пользователей не возникало других регрессий.

В рамках этого изменения коллекция hybrid_property.info теперь также распространяется из самого гибридного дескриптора, а не из нижележащего выражения. То есть обращение к A.some_name.info теперь возвращает тот же словарь, что и обращение к inspect(A).all_orm_descriptors['some_name'].info:

>>> A.some_name.info["foo"] = "bar"
>>> from sqlalchemy import inspect
>>> inspect(A).all_orm_descriptors["some_name"].info
{'foo': 'bar'}

Обратите внимание, что этот словарь .info отделен от словаря отображенного атрибута, который гибридный дескриптор может проксировать напрямую; это изменение в поведении по сравнению с 1.0. Обертка по-прежнему будет проксировать другие полезные атрибуты зеркального атрибута, такие как QueryableAttribute.property и QueryableAttribute.class_.

#3653

Session.merge разрешает отложенные конфликты так же, как и постоянные

Метод Session.merge() теперь будет отслеживать идентичность объектов, заданных в графе, для сохранения уникальности первичного ключа перед выдачей INSERT. Если встречаются дубликаты объектов с одинаковыми идентификаторами, то атрибуты, не являющиеся первичными ключами, перезаписываются по мере появления объектов, что, по сути, не является детерминированным. Такое поведение соответствует тому, как обрабатываются постоянные объекты, т.е. объекты, которые уже находятся в базе данных по первичному ключу, поэтому такое поведение является более внутренне последовательным.

Дано:

u1 = User(id=7, name="x")
u1.orders = [
    Order(description="o1", address=Address(id=1, email_address="a")),
    Order(description="o2", address=Address(id=1, email_address="b")),
    Order(description="o3", address=Address(id=1, email_address="c")),
]

sess = Session()
sess.merge(u1)

Выше мы объединили объект User с тремя новыми объектами Order, каждый из которых ссылается на отдельный объект Address, однако каждому из них присвоен один и тот же первичный ключ. Текущее поведение Session.merge() заключается в том, что он ищет в карте идентификаторов этот объект Address и использует его в качестве цели. Если объект присутствует, то есть в базе данных уже есть строка для Address с первичным ключом «1», мы видим, что поле email_address в Address будет перезаписано три раза, в данном случае со значениями a, b и, наконец, c.

Однако если бы строка Address для первичного ключа «1» отсутствовала, то вместо этого Session.merge() создал бы три отдельных экземпляра Address, и мы получили бы конфликт первичных ключей при INSERT. Новое поведение заключается в том, что предполагаемые первичные ключи для этих Address объектов отслеживаются в отдельном словаре, так что мы объединяем состояние трех предполагаемых Address объектов в один Address объект для вставки.

Возможно, было бы предпочтительнее, если бы исходный случай выдавал какое-либо предупреждение о наличии конфликтующих данных в одном дереве слияния, однако недетерминированное слияние значений уже много лет является поведением для постоянного случая; теперь оно соответствует и для отложенного случая. Функция, предупреждающая о наличии конфликтующих значений, может быть реализована для обоих случаев, но это приведет к значительному увеличению производительности, поскольку при слиянии придется сравнивать значения каждого столбца.

#3601

Исправление, связанное с перемещением объектов «многие к одному» при инициированных пользователем манипуляциях с внешними ключами

Исправлена ошибка, связанная с механикой замены ссылки на объект «многие-к-одному» на другой объект. Теперь при выполнении операции атрибутирования для определения местоположения объекта, на который была сделана ссылка, используется не текущее значение внешнего ключа, а зафиксированное в базе данных значение внешнего ключа. Основной эффект от исправления заключается в том, что событие обратной ссылки на коллекцию будет срабатывать более точно при изменении «многие к одному», даже если перед этим атрибут внешнего ключа был вручную переведен в новое значение. Предположим отображение классов Parent и SomeClass, где SomeClass.parent ссылается на Parent, а Parent.items - на коллекцию объектов SomeClass:

some_object = SomeClass()
session.add(some_object)
some_object.parent_id = some_parent.id
some_object.parent = some_parent

Выше мы создали отложенный объект some_object, манипулировали его внешним ключом Parent для ссылки на него, после чего собственно и установили связь. До исправления ошибки обратная ссылка не срабатывала:

# before the fix
assert some_object not in some_parent.items

Теперь исправление заключается в том, что при поиске предыдущего значения some_object.parent мы не обращаем внимания на родительский идентификатор, который был установлен вручную, и ищем зафиксированное в базе данных значение. В данном случае это None, поскольку объект находится в состоянии ожидания, поэтому система событий регистрирует some_object.parent как чистое изменение:

# after the fix, backref fired off for some_object.parent = some_parent
assert some_object in some_parent.items

Хотя манипулировать атрибутами внешних ключей, управляемых отношениями, не рекомендуется, поддержка такого варианта использования ограничена. Приложения, которые манипулируют внешними ключами для обеспечения возможности загрузки, часто используют функции Session.enable_relationship_loading() и RelationshipProperty.load_on_pending, которые заставляют отношения выполнять ленивую загрузку на основе значений внешних ключей, не сохраняемых в памяти. Независимо от того, используются эти функции или нет, теперь это улучшение поведения будет очевидным.

#3708

Улучшения в методе Query.correlate при работе с полиморфными сущностями

В последних версиях SQLAlchemy SQL, генерируемый многими формами «полиморфных» запросов, имеет более «плоскую» форму, чем раньше, когда JOIN из нескольких таблиц уже не связывается в подзапрос безусловно. Чтобы учесть это, метод Query.correlate() теперь извлекает отдельные таблицы из такого полиморфного selectable и гарантирует, что все они являются частью «коррелята» для подзапроса. При использовании настройки Person/Manager/Engineer->Company из документации по отображению, с помощью метода with_polymorphic:

sess.query(Person.name).filter(
    sess.query(Company.name)
    .filter(Company.company_id == Person.company_id)
    .correlate(Person)
    .as_scalar()
    == "Elbonia, Inc."
)

Теперь в результате выполнения приведенного выше запроса получается:

SELECT people.name AS people_name
FROM people
LEFT OUTER JOIN engineers ON people.person_id = engineers.person_id
LEFT OUTER JOIN managers ON people.person_id = managers.person_id
WHERE (SELECT companies.name
FROM companies
WHERE companies.company_id = people.company_id) = ?

До исправления вызов correlate(Person) непреднамеренно пытался коррелировать с соединением Person, Engineer и Manager как единое целое, поэтому Person не коррелировался:

-- old, incorrect query
SELECT people.name AS people_name
FROM people
LEFT OUTER JOIN engineers ON people.person_id = engineers.person_id
LEFT OUTER JOIN managers ON people.person_id = managers.person_id
WHERE (SELECT companies.name
FROM companies, people
WHERE companies.company_id = people.company_id) = ?

Использование коррелированных подзапросов против полиморфных отображений все еще имеет некоторые неотшлифованные грани. Если, например, Person полиморфно связан с так называемым запросом «конкретного полиморфного объединения», то приведенный выше подзапрос может некорректно ссылаться на этот подзапрос. Во всех случаях способом полноценного обращения к «полиморфной» сущности является создание из нее сначала объекта aliased():

# works with all SQLAlchemy versions and all types of polymorphic
# aliasing.

paliased = aliased(Person)
sess.query(paliased.name).filter(
    sess.query(Company.name)
    .filter(Company.company_id == paliased.company_id)
    .correlate(paliased)
    .as_scalar()
    == "Elbonia, Inc."
)

Конструкция aliased() гарантирует, что «полиморфный selectable» обернут в подзапрос. Явное обращение к нему в коррелирующем подзапросе позволяет корректно использовать полиморфную форму.

#3662

Строчная обработка запроса будет обращаться к сессии для получения правильного диалекта

Вызов str() на объекте Query обращается к Session на предмет правильного «связывания», чтобы отобразить SQL, который будет передан в базу данных. В частности, это позволяет отображать Query, ссылающиеся на диалектные SQL-конструкции, при условии, что Query связан с соответствующим Session. Ранее такое поведение было возможно только в том случае, если MetaData, с которым были связаны отображения, сам был привязан к целевому Engine.

Если ни базовый MetaData, ни Session не связаны с каким-либо связанным Engine, то для генерации SQL-строки используется диалект «по умолчанию».

#3081

Реализована ускоренная загрузка, когда одна и та же сущность присутствует несколько раз в одном ряду

Исправлена ситуация, при которой атрибут будет загружен через объединенную ускоренную загрузку, даже если сущность уже была загружена из строки по другому «пути», не включающему атрибут. Это глубокий сценарий использования, который трудно воспроизвести, но общая идея такова:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)
    b_id = Column(ForeignKey("b.id"))
    c_id = Column(ForeignKey("c.id"))

    b = relationship("B")
    c = relationship("C")


class B(Base):
    __tablename__ = "b"
    id = Column(Integer, primary_key=True)
    c_id = Column(ForeignKey("c.id"))

    c = relationship("C")


class C(Base):
    __tablename__ = "c"
    id = Column(Integer, primary_key=True)
    d_id = Column(ForeignKey("d.id"))
    d = relationship("D")


class D(Base):
    __tablename__ = "d"
    id = Column(Integer, primary_key=True)


c_alias_1 = aliased(C)
c_alias_2 = aliased(C)

q = s.query(A)
q = q.join(A.b).join(c_alias_1, B.c).join(c_alias_1.d)
q = q.options(
    contains_eager(A.b).contains_eager(B.c, alias=c_alias_1).contains_eager(C.d)
)
q = q.join(c_alias_2, A.c)
q = q.options(contains_eager(A.c, alias=c_alias_2))

Приведенный выше запрос выдает SQL следующего вида:

SELECT
    d.id AS d_id,
    c_1.id AS c_1_id, c_1.d_id AS c_1_d_id,
    b.id AS b_id, b.c_id AS b_c_id,
    c_2.id AS c_2_id, c_2.d_id AS c_2_d_id,
    a.id AS a_id, a.b_id AS a_b_id, a.c_id AS a_c_id
FROM
    a
    JOIN b ON b.id = a.b_id
    JOIN c AS c_1 ON c_1.id = b.c_id
    JOIN d ON d.id = c_1.d_id
    JOIN c AS c_2 ON c_2.id = a.c_id

Видно, что таблица c выбирается из нее дважды: один раз в контексте A.b.c -> c_alias_1, а другой - в контексте A.c -> c_alias_2. Также мы видим, что вполне возможно, что идентификатор C для одной строки является одинаковым как для c_alias_1, так и для c_alias_2, то есть два набора столбцов в одной строке приводят к добавлению в карту идентификаторов только одного нового объекта.

Приведенные выше варианты запроса требуют загрузки атрибута C.d только в контексте c_alias_1, но не c_alias_2. Поэтому то, будет ли загружен атрибут C.d в конечный объект C, который мы получим в карте идентичности, зависит от того, как происходит обход отображений, который хотя и не является полностью случайным, но по сути своей недетерминирован. Исправление заключается в том, что даже если загрузчик для c_alias_1 обрабатывается после загрузчика для c_alias_2 для одной строки, где они оба ссылаются на один и тот же идентификатор, элемент C.d все равно будет загружен. Ранее загрузчик не стремился изменить загрузку сущности, которая уже была загружена по другому пути. Загрузчик, который первым достигает сущности, всегда был недетерминированным, поэтому данное исправление может быть обнаружено как изменение поведения в одних ситуациях, а в других - нет.

Исправление включает в себя тесты для двух вариантов случая «несколько путей к одной сущности», и, надеюсь, исправление должно покрыть все остальные сценарии такого рода.

#3431

В расширение для отслеживания мутаций добавлены новые помощники MutableList и MutableSet

В расширение Отслеживание мутаций добавлены новые классы-помощники MutableList и MutableSet, дополняющие существующий помощник MutableDict.

#3297

Новые стратегии загрузчика «raise» / «raise_on_sql»

Для предотвращения нежелательных ленивых загрузок после загрузки ряда объектов можно применить к атрибуту отношения новые стратегии «lazy=“raise“» и «lazy=“raise_on_sql“» и соответствующую опцию загрузчика raiseload(), которая заставит его поднимать InvalidRequestError при обращении к атрибуту, не являющемуся ленивым, для чтения. Эти два варианта проверяют либо ленивую загрузку любого типа, включая те, которые возвращают только None или извлекают из карты идентичности:

>>> from sqlalchemy.orm import raiseload
>>> a1 = s.query(A).options(raiseload(A.some_b)).first()
>>> a1.some_b
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'A.some_b' is not available due to lazy='raise'

Или только ленивая загрузка, при которой будет выдаваться SQL:

>>> from sqlalchemy.orm import raiseload
>>> a1 = s.query(A).options(raiseload(A.some_b, sql_only=True)).first()
>>> a1.some_b
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'A.bs' is not available due to lazy='raise_on_sql'

#3512

Mapper.order_by устарел

Этот старый параметр из самых первых версий SQLAlchemy был частью первоначального дизайна ORM, в котором объект Mapper использовался в качестве общедоступной структуры запроса. Эта роль уже давно заменена объектом Query, где мы используем Query.order_by() для указания порядка результатов таким образом, чтобы он работал последовательно для любой комбинации операторов SELECT, сущностей и SQL-выражений. Существует множество областей, в которых Mapper.order_by работает не так, как ожидается (или то, что ожидается, неясно), например, когда запросы объединяются в союзы; эти случаи не поддерживаются.

#3394

Новые возможности и усовершенствования - Ядро

Двигатели теперь аннулируют соединения, запускают обработчики ошибок для BaseException

Добавлено в версии 1.1: Это изменение было добавлено в серию 1.1 незадолго до выхода 1.1 final и отсутствует в бета-версиях 1.1.

Класс Python BaseException находится ниже класса Exception, но является идентифицируемой базой для исключений системного уровня, таких как KeyboardInterrupt, SystemExit и, в частности, исключения GreenletExit, используемого в eventlet и gevent. Этот класс исключений теперь перехватывается подпрограммами обработки исключений Connection, а также включает обработку событием ConnectionEvents.handle_error(). В случае исключения системного уровня, не являющегося подклассом Connection, по умолчанию Exception теперь invalidated, так как предполагается, что операция была прервана и соединение может находиться в непригодном для использования состоянии. В наибольшей степени это изменение касается драйверов MySQL, однако оно распространяется на все DBAPI.

Отметим, что при аннулировании непосредственное DBAPI-соединение, используемое Connection, утилизируется, а Connection, если оно все еще используется после возникновения исключения, при следующем использовании будет использовать новое DBAPI-соединение для последующих операций; однако состояние любой выполняемой транзакции теряется, и перед повторным использованием необходимо вызвать соответствующий метод .rollback(), если он применим.

Для того чтобы выявить это изменение, достаточно продемонстрировать, как соединение pymysql или mysqlclient / MySQL-Python переходит в поврежденное состояние, когда эти исключения возникают в середине работы соединения; соединение возвращается в пул соединений, где последующие его использования будут неудачными, или даже до возвращения в пул будут возникать вторичные сбои в менеджерах контекста, которые вызывают .rollback() после перехвата исключения. Предполагается, что такое поведение позволит снизить частоту возникновения ошибки MySQL «Commands out of sync», а также ResourceClosedError, которая может возникнуть, когда драйвер MySQL некорректно сообщает cursor.description, при работе в условиях гринлета, когда гринлеты убиваются, или когда KeyboardInterrupt исключения обрабатываются без полного выхода из программы.

От обычной автопроверки это поведение отличается тем, что не предполагает, что внутренняя база данных была закрыта или перезапущена, и не перерабатывает весь пул соединений, как это происходит при обычных исключениях разъединения DBAPI.

Это изменение должно стать чистым улучшением для всех пользователей, за исключением любого приложения, которое в настоящее время перехватывает ``KeyboardInterrupt`` или ``GreenletExit`` и желает продолжить работу в рамках той же транзакции. Теоретически такая операция возможна и с другими DBAPI, которые, как оказалось, не подвержены влиянию KeyboardInterrupt, например, psycopg2. Для этих DBAPI следующее обходное решение запретит повторное использование соединения для определенных исключений:

engine = create_engine("postgresql+psycopg2://")


@event.listens_for(engine, "handle_error")
def cancel_disconnect(ctx):
    if isinstance(ctx.original_exception, KeyboardInterrupt):
        ctx.is_disconnect = False

#3803

Поддержка CTE для INSERT, UPDATE, DELETE

Одна из наиболее часто запрашиваемых функций - поддержка общих табличных выражений (CTE), работающих с INSERT, UPDATE, DELETE, - теперь реализована. INSERT/UPDATE/DELETE может как опираться на предложение WITH, которое указывается в верхней части SQL, так и использоваться в качестве CTE в контексте более крупного оператора.

В рамках этого изменения при INSERT из SELECT, включающем CTE, CTE теперь будет отображаться в верхней части всего оператора, а не вложенным в оператор SELECT, как это было в версии 1.0.

Ниже приведен пример, в котором UPDATE, INSERT и SELECT выполняются в одном операторе:

>>> from sqlalchemy import table, column, select, literal, exists
>>> orders = table(
...     "orders",
...     column("region"),
...     column("amount"),
...     column("product"),
...     column("quantity"),
... )
>>>
>>> upsert = (
...     orders.update()
...     .where(orders.c.region == "Region1")
...     .values(amount=1.0, product="Product1", quantity=1)
...     .returning(*(orders.c._all_columns))
...     .cte("upsert")
... )
>>>
>>> insert = orders.insert().from_select(
...     orders.c.keys(),
...     select([literal("Region1"), literal(1.0), literal("Product1"), literal(1)]).where(
...         ~exists(upsert.select())
...     ),
... )
>>>
>>> print(insert)  # Note: formatting added for clarity
{printsql}WITH upsert AS
(UPDATE orders SET amount=:amount, product=:product, quantity=:quantity
 WHERE orders.region = :region_1
 RETURNING orders.region, orders.amount, orders.product, orders.quantity
)
INSERT INTO orders (region, amount, product, quantity)
SELECT
    :param_1 AS anon_1, :param_2 AS anon_2,
    :param_3 AS anon_3, :param_4 AS anon_4
WHERE NOT (
    EXISTS (
        SELECT upsert.region, upsert.amount,
               upsert.product, upsert.quantity
        FROM upsert))

#2551

Поддержка спецификации RANGE и ROWS в оконных функциях

Новые параметры over.range_ и over.rows позволяют использовать выражения RANGE и ROWS для оконных функций:

>>> from sqlalchemy import func

>>> print(func.row_number().over(order_by="x", range_=(-5, 10)))
{printsql}row_number() OVER (ORDER BY x RANGE BETWEEN :param_1 PRECEDING AND :param_2 FOLLOWING){stop}

>>> print(func.row_number().over(order_by="x", rows=(None, 0)))
{printsql}row_number() OVER (ORDER BY x ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW){stop}

>>> print(func.row_number().over(order_by="x", range_=(-2, None)))
{printsql}row_number() OVER (ORDER BY x RANGE BETWEEN :param_1 PRECEDING AND UNBOUNDED FOLLOWING){stop}

over.range_ и over.rows задаются в виде двух кортежей и указывают отрицательные и положительные значения для определенных диапазонов, 0 для «CURRENT ROW» и None для UNBOUNDED.

#3049

Поддержка ключевого слова SQL LATERAL

В настоящее время известно, что ключевое слово LATERAL поддерживается только в PostgreSQL 9.3 и выше, однако, поскольку оно является частью стандарта SQL, поддержка этого слова добавлена в Core. Реализация Select.lateral() использует специальную логику, выходящую за рамки простого отображения ключевого слова LATERAL, чтобы обеспечить корреляцию таблиц, полученных из того же предложения FROM, что и selectable, например, боковую корреляцию:

>>> from sqlalchemy import table, column, select, true
>>> people = table("people", column("people_id"), column("age"), column("name"))
>>> books = table("books", column("book_id"), column("owner_id"))
>>> subq = (
...     select([books.c.book_id])
...     .where(books.c.owner_id == people.c.people_id)
...     .lateral("book_subq")
... )
>>> print(select([people]).select_from(people.join(subq, true())))
{printsql}SELECT people.people_id, people.age, people.name
FROM people JOIN LATERAL (SELECT books.book_id AS book_id
FROM books WHERE books.owner_id = people.people_id)
AS book_subq ON true

#2857

Поддержка TABLESAMPLE

Стандарт SQL TABLESAMPLE может быть выведен с помощью метода FromClause.tablesample(), который возвращает конструкцию TableSample, аналогичную псевдониму:

from sqlalchemy import func

selectable = people.tablesample(func.bernoulli(1), name="alias", seed=func.random())
stmt = select([selectable.c.people_id])

Если предположить, что people имеет столбец people_id, то приведенное выше утверждение будет выглядеть так:

SELECT alias.people_id FROM
people AS alias TABLESAMPLE bernoulli(:bernoulli_1)
REPEATABLE (random())

#3718

Директива .autoincrement больше не включается неявно для составного столбца первичного ключа

В SQLAlchemy всегда была удобная возможность включить функцию «автоинкремента» базы данных бэкенда для целочисленного первичного ключа в одном столбце; под «автоинкрементом» мы подразумеваем, что столбец базы данных будет включать любые DDL-директивы, которые база данных предоставляет для указания автоинкрементного целочисленного идентификатора, например, ключевое слово SERIAL в PostgreSQL или AUTO_INCREMENT в MySQL, и дополнительно диалект будет получать эти генерируемые значения от выполнения конструкции Table.insert(), используя методы, соответствующие данному бэкенду.

Изменения заключаются в том, что эта функция больше не включается автоматически для композитного первичного ключа; ранее определение таблицы, такое как:

Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True),
)

При этом семантика «автоинкремента» будет применяться к столбцу 'x' только потому, что он первый в списке столбцов первичного ключа. Для того чтобы отключить это, необходимо отключить autoincrement для всех столбцов:

# old way
Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True, autoincrement=False),
    Column("y", Integer, primary_key=True, autoincrement=False),
)

При новом поведении составной первичный ключ не будет иметь семантики автоинкремента, если столбец не помечен явным образом символом autoincrement=True:

# column 'y' will be SERIAL/AUTO_INCREMENT/ auto-generating
Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True, autoincrement=True),
)

Для того чтобы предусмотреть некоторые потенциальные сценарии обратной несовместимости, конструкция Table.insert() будет выполнять более тщательную проверку на отсутствие значений первичного ключа для составных столбцов первичного ключа, в которых не установлен автоинкремент; для такой таблицы, как:

Table(
    "b",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True),
)

При выполнении INSERT с отсутствием значений для этой таблицы будет выдано данное предупреждение:

SAWarning: Column 'b.x' is marked as a member of the primary
key for table 'b', but has no Python-side or server-side default
generator indicated, nor does it indicate 'autoincrement=True',
and no explicit value is passed.  Primary key columns may not
store NULL. Note that as of SQLAlchemy 1.1, 'autoincrement=True'
must be indicated explicitly for composite (e.g. multicolumn)
primary keys if AUTO_INCREMENT/SERIAL/IDENTITY behavior is
expected for one of the columns in the primary key. CREATE TABLE
statements are impacted by this change as well on most backends.

Для столбца, получающего значения первичного ключа по умолчанию на стороне сервера или менее распространенного, например, триггера, наличие генератора значений может быть указано с помощью FetchedValue:

Table(
    "b",
    metadata,
    Column("x", Integer, primary_key=True, server_default=FetchedValue()),
    Column("y", Integer, primary_key=True, server_default=FetchedValue()),
)

Для очень маловероятного случая, когда составной первичный ключ на самом деле предназначен для хранения NULL в одном или нескольких своих столбцах (поддерживается только в SQLite и MySQL), укажите этот столбец с помощью nullable=True:

Table(
    "b",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True, nullable=True),
)

В связи с этим изменением флаг autoincrement может быть установлен в True для столбца, имеющего значение по умолчанию на стороне клиента или на стороне сервера. Как правило, это не оказывает существенного влияния на поведение столбца при INSERT.

#3216

Поддержка IS DISTINCT FROM и IS NOT DISTINCT FROM

Новые операторы ColumnOperators.is_distinct_from() и ColumnOperators.isnot_distinct_from() позволяют выполнять sql-операции IS DISTINCT FROM и IS NOT DISTINCT FROM:

>>> print(column("x").is_distinct_from(None))
{printsql}x IS DISTINCT FROM NULL{stop}

Предусмотрена обработка NULL, True и False:

>>> print(column("x").isnot_distinct_from(False))
{printsql}x IS NOT DISTINCT FROM false{stop}

Для SQLite, в котором нет этого оператора, используется оператор «IS» / «IS NOT», который в SQLite, в отличие от других бэкендов, работает для NULL:

>>> from sqlalchemy.dialects import sqlite
>>> print(column("x").is_distinct_from(None).compile(dialect=sqlite.dialect()))
{printsql}x IS NOT NULL{stop}

Поддержка FULL OUTER JOIN в ядре и ORM

Новый флаг FromClause.outerjoin.full, доступный на уровне Core и ORM, предписывает компилятору выводить FULL OUTER JOIN там, где обычно выводится LEFT OUTER JOIN:

stmt = select([t1]).select_from(t1.outerjoin(t2, full=True))

Флаг также работает на уровне ORM:

q = session.query(MyClass).outerjoin(MyOtherClass, full=True)

#1957

Расширение возможностей согласования столбцов ResultSet; позиционная настройка столбцов для текстового SQL

В серии 1.0 в систему ResultProxy был внесен ряд улучшений в рамках #918, которые реорганизуют внутреннее устройство для позиционного, а не по именам, сопоставления связанных с курсором столбцов результата с метаданными таблицы/ОРМ для компилируемых SQL-конструкций, содержащих полную информацию о возвращаемых строках результата. Это позволяет значительно снизить накладные расходы на Python, а также повысить точность привязки выражений ORM и Core SQL к строкам результатов. В версии 1.1 эта реорганизация получила дальнейшее внутреннее развитие, а также стала доступной для чисто текстовых SQL-конструкций благодаря использованию недавно добавленного метода TextClause.columns().

TextAsFrom.columns() теперь работает позиционно

Метод TextClause.columns(), добавленный в 0.9, принимает аргументы, основанные на столбцах, позиционно; в 1.1, когда все столбцы передаются позиционно, соотнесение этих столбцов с конечным набором результатов также выполняется позиционно. Основное преимущество заключается в том, что текстовый SQL теперь может быть связан с набором результатов на уровне ORM без необходимости иметь дело с неоднозначными или дублирующимися именами столбцов, а также со схемами разметки, соответствующими схемам разметки на уровне ORM. Все, что теперь требуется, - это то же упорядочивание столбцов в текстовом SQL и аргументы столбцов, передаваемые в TextClause.columns():

from sqlalchemy import text

stmt = text(
    "SELECT users.id, addresses.id, users.id, "
    "users.name, addresses.email_address AS email "
    "FROM users JOIN addresses ON users.id=addresses.user_id "
    "WHERE users.id = 1"
).columns(User.id, Address.id, Address.user_id, User.name, Address.email_address)

query = session.query(User).from_statement(stmt).options(contains_eager(User.addresses))
result = query.all()

Выше в текстовом SQL трижды встречается столбец «id», что в обычных условиях было бы неоднозначно. Используя новую возможность, мы можем напрямую применять сопоставленные столбцы из классов User и Address, даже связать столбец Address.user_id со столбцом users.id в текстовом SQL, и объект Query будет получать строки, которые будут корректно таргетироваться по мере необходимости, в том числе и при ускоренной загрузке.

Это изменение обратно несовместимо с кодом, передающим в метод столбцы с порядком, отличным от того, что присутствует в текстовом выражении. Есть надежда, что это влияние будет незначительным, поскольку этот метод всегда документировался, иллюстрируя передачу столбцов в том же порядке, что и в текстовом SQL-операторе, что казалось бы интуитивно понятным, хотя внутреннее устройство не проверяло этого. В любом случае, сам метод был добавлен только в версии 0.9 и, возможно, еще не получил широкого распространения. Заметки о том, как именно следует поступать с этим изменением поведения в приложениях, использующих его, находятся по адресу TextClause.columns() при позиционной передаче будет сопоставлять столбцы не по имени, а позиционно.

Для конструкций Core/ORM SQL позиционное сопоставление является более надежным, чем сопоставление по имени

Другим аспектом этого изменения является то, что правила сопоставления столбцов также были изменены, чтобы более полно использовать «позиционное» сопоставление и для компилируемых конструкций SQL. В случае следующего оператора:

ua = users.alias("ua")
stmt = select([users.c.user_id, ua.c.user_id])

Приведенное выше утверждение компилируется в:

SELECT users.user_id, ua.user_id FROM users, users AS ua

В версии 1.0 приведенное выше утверждение при выполнении будет сопоставлено с исходной скомпилированной конструкцией с помощью позиционного соответствия, однако, поскольку в утверждении дублируется метка 'user_id', правило «неоднозначного столбца» все равно будет задействовано и не позволит извлечь столбцы из строки. Начиная с версии 1.1 правило «неоднозначного столбца» не влияет на точное совпадение конструкции столбца с SQL-столбцом, что и используется в ORM для извлечения столбцов:

result = conn.execute(stmt)
row = result.first()

# these both match positionally, so no error
user_id = row[users.c.user_id]
ua_id = row[ua.c.user_id]

# this still raises, however
user_id = row["user_id"]

Значительно меньше вероятность получения сообщения об ошибке «неоднозначный столбец»

В рамках этого изменения была изменена формулировка сообщения об ошибке Ambiguous column name '<name>' in result set! try 'use_labels' option on select statement.; поскольку теперь это сообщение должно появляться крайне редко при использовании ORM или скомпилированных в Core конструкций SQL, оно просто указывает Ambiguous column name '<name>' in result set column descriptions, и только в том случае, когда столбец результата извлекается с использованием строкового имени, которое на самом деле является неоднозначным, например row['user_id'] в приведенном выше примере. Кроме того, теперь она ссылается на фактическое неоднозначное имя из самого SQL-оператора, а не указывает на ключ или имя, локальное для конструкции, используемой для выборки.

#3501

Поддержка родного для Python типа enum и совместимых с ним форм

Тип Enum теперь может быть построен с использованием любого перечислимого типа, совместимого с PEP-435. При использовании этого режима входными и возвращаемыми значениями являются собственно перечислимые объекты, а не строковые/целые/и т.д. значения:

import enum
from sqlalchemy import Table, MetaData, Column, Enum, create_engine


class MyEnum(enum.Enum):
    one = 1
    two = 2
    three = 3


t = Table("data", MetaData(), Column("value", Enum(MyEnum)))

e = create_engine("sqlite://")
t.create(e)

e.execute(t.insert(), {"value": MyEnum.two})
assert e.scalar(t.select()) is MyEnum.two

Коллекция Enum.enums теперь является списком, а не кортежем

В рамках изменений в Enum коллекция элементов Enum.enums теперь представляет собой список, а не кортеж. Это связано с тем, что списки подходят для последовательностей однородных элементов переменной длины, где позиция элемента не является семантически значимой.

#3292

Индексы с отрицательными целыми числами учитываются в строках результатов Core

Объект RowProxy теперь работает с одиночными отрицательными целыми индексами, как обычная последовательность Python, как в чистом Python, так и в C-расширении. Ранее отрицательные значения работали только в срезах:

>>> from sqlalchemy import create_engine
>>> e = create_engine("sqlite://")
>>> row = e.execute("select 1, 2, 3").first()
>>> row[-1], row[-2], row[1], row[-2:2]
3 2 2 (2,)

Тип Enum теперь выполняет проверку значений в Python

Для того чтобы обеспечить работу с перечислимыми объектами, являющимися родными для Python, а также для таких крайних случаев, как использование неродного типа ENUM в ARRAY и невыполнимость ограничения CHECK, тип данных Enum теперь добавляет проверку входных значений в Python при использовании флага Enum.validate_strings (1.1.0b2):

>>> from sqlalchemy import Table, MetaData, Column, Enum, create_engine
>>> t = Table(
...     "data",
...     MetaData(),
...     Column("value", Enum("one", "two", "three", validate_strings=True)),
... )
>>> e = create_engine("sqlite://")
>>> t.create(e)
>>> e.execute(t.insert(), {"value": "four"})
Traceback (most recent call last):
  ...
sqlalchemy.exc.StatementError: (exceptions.LookupError)
"four" is not among the defined enum values
[SQL: u'INSERT INTO data (value) VALUES (?)']
[parameters: [{'value': 'four'}]]

По умолчанию эта проверка отключена, поскольку уже выявлены случаи, когда она не нужна пользователям (например, при сравнении строк). Для нестроковых типов она обязательно выполняется во всех случаях. Проверка также безусловно выполняется и на стороне обработки результатов, когда возвращаются значения из базы данных.

Эта проверка дополняет существующее поведение создания ограничения CHECK при использовании неродного перечислимого типа. Теперь создание этого CHECK-ограничения можно отключить с помощью нового флага Enum.create_constraint.

#3095

Неродные булевые целочисленные значения во всех случаях принудительно приводятся к нулю/одну/Нулю

Тип данных Boolean приводит булевые значения Python к целочисленным значениям для бэкендов, не имеющих собственных булевых типов, таких как SQLite и MySQL. На этих платформах обычно устанавливается ограничение CHECK, которое гарантирует, что значения в базе данных действительно являются одним из этих двух значений. Однако MySQL игнорирует ограничения CHECK, ограничение является необязательным, и существующая база данных может не иметь этого ограничения. Тип данных Boolean был исправлен таким образом, что входящее значение со стороны Python, которое уже является целым числом, принудительно приводится к нулю или единице, а не просто передается как есть; кроме того, версия C-расширения процессора int-to-boolean для результатов теперь использует ту же самую булеву интерпретацию значения со стороны Python, а не утверждает, что значение точно равно единице или нулю. Это соответствует чисто питоновскому процессору int-to-boolean и более щадяще относится к существующим данным в базе данных. Значения None/NULL, как и прежде, сохраняются как None/NULL.

Примечание

Это изменение привело к непреднамеренному побочному эффекту: интерпретация нецелых значений, таких как строки, также изменилась, и строковое значение "0" стало интерпретироваться как «true», но только на тех платформах, которые не имеют собственного типа данных boolean - на «родных» платформах boolean, таких как PostgreSQL, строковое значение "0" передается непосредственно в драйвер и интерпретируется как «false». Это несоответствие, которого не было в предыдущей реализации. Следует отметить, что передача строк или любых других значений за пределами None, True, False, 1, 0 в тип данных Boolean не поддерживается, и версия 1.2 будет выдавать ошибку при таком сценарии (или, возможно, просто выдавать предупреждение, TBD). См. также #4102.

#3730

Большие значения параметров и строк теперь усекаются при выводе логов и исключений

Большое значение, присутствующее в качестве связанного параметра SQL-оператора, а также большое значение, присутствующее в строке результата, теперь будет усекаться при отображении в логах, отчетах об исключениях, а также repr() самой строки:

>>> from sqlalchemy import create_engine
>>> import random
>>> e = create_engine("sqlite://", echo="debug")
>>> some_value = "".join(chr(random.randint(52, 85)) for i in range(5000))
>>> row = e.execute("select ?", [some_value]).first()
... # (lines are wrapped for clarity) ...
2016-02-17 13:23:03,027 INFO sqlalchemy.engine.base.Engine select ?
2016-02-17 13:23:03,027 INFO sqlalchemy.engine.base.Engine
('E6@?>9HPOJB<<BHR:@=TS:5ILU=;JLM<4?B9<S48PTNG9>:=TSTLA;9K;9FPM4M8M@;NM6GU
LUAEBT9QGHNHTHR5EP75@OER4?SKC;D:TFUMD:M>;C6U:JLM6R67GEK<A6@S@C@J7>4=4:P
GJ7HQ6 ... (4702 characters truncated) ... J6IK546AJMB4N6S9L;;9AKI;=RJP
HDSSOTNBUEEC9@Q:RCL:I@5?FO<9K>KJAGAO@E6@A7JI8O:J7B69T6<8;F:S;4BEIJS9HM
K:;5OLPM@JR;R:J6<SOTTT=>Q>7T@I::OTDC:CC<=NGP6C>BC8N',)
2016-02-17 13:23:03,027 DEBUG sqlalchemy.engine.base.Engine Col ('?',)
2016-02-17 13:23:03,027 DEBUG sqlalchemy.engine.base.Engine
Row (u'E6@?>9HPOJB<<BHR:@=TS:5ILU=;JLM<4?B9<S48PTNG9>:=TSTLA;9K;9FPM4M8M@;
NM6GULUAEBT9QGHNHTHR5EP75@OER4?SKC;D:TFUMD:M>;C6U:JLM6R67GEK<A6@S@C@J7
>4=4:PGJ7HQ ... (4703 characters truncated) ... J6IK546AJMB4N6S9L;;9AKI;=
RJPHDSSOTNBUEEC9@Q:RCL:I@5?FO<9K>KJAGAO@E6@A7JI8O:J7B69T6<8;F:S;4BEIJS9HM
K:;5OLPM@JR;R:J6<SOTTT=>Q>7T@I::OTDC:CC<=NGP6C>BC8N',)
>>> print(row)
(u'E6@?>9HPOJB<<BHR:@=TS:5ILU=;JLM<4?B9<S48PTNG9>:=TSTLA;9K;9FPM4M8M@;NM6
GULUAEBT9QGHNHTHR5EP75@OER4?SKC;D:TFUMD:M>;C6U:JLM6R67GEK<A6@S@C@J7>4
=4:PGJ7HQ ... (4703 characters truncated) ... J6IK546AJMB4N6S9L;;9AKI;
=RJPHDSSOTNBUEEC9@Q:RCL:I@5?FO<9K>KJAGAO@E6@A7JI8O:J7B69T6<8;F:S;4BEIJS9H
MK:;5OLPM@JR;R:J6<SOTTT=>Q>7T@I::OTDC:CC<=NGP6C>BC8N',)

#2837

В Core добавлена поддержка JSON

Поскольку MySQL теперь имеет тип данных JSON в дополнение к типу данных PostgreSQL JSON, в ядре появился тип данных sqlalchemy.types.JSON, который является основой для обоих типов. Использование этого типа позволяет получить доступ к оператору «getitem», а также к оператору «getpath» таким образом, что это не зависит от PostgreSQL и MySQL.

В новом типе данных также реализован ряд улучшений в работе со значениями NULL, а также в работе с выражениями.

#3619

JSON «null» вставляется, как и ожидалось, при операциях ORM, и опускается, если не присутствует

Тип JSON и его потомки JSON и JSON имеют флаг JSON.none_as_null, который при установке в True указывает, что значение Python None должно преобразовываться в SQL NULL, а не в JSON NULL. По умолчанию этот флаг равен False, что означает, что значение Python None должно приводить к значению JSON NULL.

Эта логика дала бы сбой, и теперь она исправлена, при следующих обстоятельствах:

1. When the column also contained a default or server_default value, a positive value of None on the mapped attribute that expects to persist JSON «null» would still result in the column-level default being triggered, replacing the None value:

class MyObject(Base):
    # ...

    json_value = Column(JSON(none_as_null=False), default="some default")


# would insert "some default" instead of "'null'",
# now will insert "'null'"
obj = MyObject(json_value=None)
session.add(obj)
session.commit()

2. When the column did not contain a default or server_default value, a missing value on a JSON column configured with none_as_null=False would still render JSON NULL rather than falling back to not inserting any value, behaving inconsistently vs. all other datatypes:

class MyObject(Base):
    # ...

    some_other_value = Column(String(50))
    json_value = Column(JSON(none_as_null=False))


# would result in NULL for some_other_value,
# but json "'null'" for json_value.  Now results in NULL for both
# (the json_value is omitted from the INSERT)
obj = MyObject()
session.add(obj)
session.commit()

Это изменение поведения, которое является несовместимым с обратным ходом событий для приложений, которые полагались на это, чтобы по умолчанию принять отсутствующее значение как JSON null. По сути, это устанавливает, что отсутствующее значение отличается от присутствующего значения None. Более подробная информация приведена в разделе JSON Columns не будет вставлять JSON NULL, если не указано значение и не установлено значение по умолчанию.

3. When the Session.bulk_insert_mappings() method were used, None would be ignored in all cases:

# would insert SQL NULL and/or trigger defaults,
# now inserts "'null'"
session.bulk_insert_mappings(MyObject, [{"json_value": None}])

Тип JSON теперь реализует флаг TypeEngine.should_evaluate_none, указывающий на то, что здесь не следует игнорировать None, он настраивается автоматически на основе значения JSON.none_as_null. Благодаря #3061 мы можем различать, когда значение None активно задано пользователем, а когда оно вообще не задавалось.

Это свойство распространяется как на новый базовый тип JSON, так и на его потомков.

#3514

Добавлена новая константа JSON.NULL

Для того чтобы приложение всегда могло полностью контролировать на уровне значений, должен ли столбец JSON, JSON, JSON или JSONB принимать значение SQL NULL или JSON "null", была добавлена константа JSON.NULL, которая в сочетании с null() может использоваться для полного определения между SQL NULL и JSON "null", независимо от того, какое значение установлено в JSON.none_as_null:

from sqlalchemy import null
from sqlalchemy.dialects.postgresql import JSON

obj1 = MyObject(json_value=null())  # will *always* insert SQL NULL
obj2 = MyObject(json_value=JSON.NULL)  # will *always* insert JSON string "null"

session.add_all([obj1, obj2])
session.commit()

Это свойство распространяется как на новый базовый тип JSON, так и на его потомков.

#3514

В Core добавлена поддержка массивов; новые операторы ANY и ALL

Вместе с улучшениями, внесенными в тип PostgreSQL ARRAY, описанными в Корректные типы SQL устанавливаются из индексированного доступа к ARRAY, JSON, HSTORE, базовый класс ARRAY был перенесен в Core в новый класс ARRAY.

Массивы являются частью стандарта SQL, как и несколько функций, ориентированных на массивы, таких как array_agg() и unnest(). Для поддержки этих конструкций не только для PostgreSQL, но и, возможно, для других бэкендов с поддержкой массивов, таких как DB2, большая часть логики массивов для SQL-выражений теперь находится в Core. Тип ARRAY по-прежнему работает только в PostgreSQL, однако его можно использовать напрямую, поддерживая специальные случаи использования массивов, такие как индексированный доступ, а также поддержку ANY и ALL:

mytable = Table("mytable", metadata, Column("data", ARRAY(Integer, dimensions=2)))

expr = mytable.c.data[5][6]

expr = mytable.c.data[5].any(12)

Для поддержки ANY и ALL тип ARRAY сохраняет те же методы Comparator.any() и Comparator.all(), что и тип PostgreSQL, но при этом экспортирует эти операции в новые отдельные операторные функции any_() и all_(). Эти две функции работают в более традиционном для SQL ключе, позволяя использовать правую часть выражения, например:

from sqlalchemy import any_, all_

select([mytable]).where(12 == any_(mytable.c.data[5]))

Для специфических для PostgreSQL операторов «contains», «contained_by» и «overlaps» следует продолжать использовать непосредственно тип ARRAY, который также обеспечивает всю функциональность типа ARRAY.

Операторы any_() и all_() являются произвольными на уровне Core, однако их интерпретация базами данных бэкенда ограничена. В бэкенде PostgreSQL эти два оператора принимают только значения массивов. В то время как в бэкенде MySQL они принимают только значения подзапросов. В MySQL можно использовать выражение типа:

from sqlalchemy import any_, all_

subq = select([mytable.c.value])
select([mytable]).where(12 > any_(subq))

#3516

Новые функции, «WITHIN GROUP», array_agg и агрегатные функции set

С помощью нового типа ARRAY мы также можем реализовать претипизированную функцию для SQL-функции array_agg(), возвращающей массив, которая теперь доступна с помощью array_agg:

from sqlalchemy import func

stmt = select([func.array_agg(table.c.value)])

Также добавлен элемент PostgreSQL для агрегированного ORDER BY через aggregate_order_by:

from sqlalchemy.dialects.postgresql import aggregate_order_by

expr = func.array_agg(aggregate_order_by(table.c.a, table.c.b.desc()))
stmt = select([expr])

Продюсирование:

SELECT array_agg(table1.a ORDER BY table1.b DESC) AS array_agg_1 FROM table1

Сам диалект PG также предоставляет обертку array_agg() для обеспечения ARRAY типа:

from sqlalchemy.dialects.postgresql import array_agg

stmt = select([array_agg(table.c.value).contains("foo")])

Кроме того, такие функции, как percentile_cont(), percentile_disc(), rank(), dense_rank() и другие, требующие упорядочивания через WITHIN GROUP (ORDER BY <expr>), теперь доступны через модификатор FunctionElement.within_group():

from sqlalchemy import func

stmt = select(
    [
        department.c.id,
        func.percentile_cont(0.5).within_group(department.c.salary.desc()),
    ]
)

Вышеприведенный оператор выдаст SQL, аналогичный:

SELECT department.id, percentile_cont(0.5)
WITHIN GROUP (ORDER BY department.salary DESC)

Теперь для этих функций предусмотрены плейсхолдеры с корректными типами возврата, к которым относятся percentile_cont, percentile_disc, rank, dense_rank, mode, percent_rank и cume_dist.

#3132 #1370

TypeDecorator теперь автоматически работает с типами Enum, Boolean, «schema»

К типам SchemaType относятся такие типы, как Enum и Boolean, которые, помимо соответствия типу базы данных, также генерируют либо ограничение CHECK, либо, в случае PostgreSQL ENUM, новый оператор CREATE TYPE. Теперь рецепты TypeDecorator будут работать автоматически. Ранее TypeDecorator для ENUM должен был выглядеть следующим образом:

# old way
class MyEnum(TypeDecorator, SchemaType):
    impl = postgresql.ENUM("one", "two", "three", name="myenum")

    def _set_table(self, table):
        self.impl._set_table(table)

Теперь TypeDecorator распространяет эти дополнительные события, поэтому его можно выполнять как любой другой тип:

# new way
class MyEnum(TypeDecorator):
    impl = postgresql.ENUM("one", "two", "three", name="myenum")

#2919

Многопользовательская трансляция схем для объектов таблиц

Для поддержки приложения, использующего один и тот же набор объектов Table во многих схемах, например, схему для пользователя, добавлена новая опция выполнения Connection.execution_options.schema_translate_map. С помощью этого отображения набор объектов Table можно сделать так, чтобы на основе каждого соединения они ссылались на любой набор схем, а не на ту Table.schema, которой они были назначены. Трансляция работает как при генерации DDL и SQL, так и при использовании ORM.

Например, если классу User была присвоена схема «per_user»:

class User(Base):
    __tablename__ = "user"
    id = Column(Integer, primary_key=True)

    __table_args__ = {"schema": "per_user"}

При каждом запросе Session может быть настроено так, чтобы каждый раз ссылаться на другую схему:

session = Session()
session.connection(
    execution_options={"schema_translate_map": {"per_user": "account_one"}}
)

# will query from the ``account_one.user`` table
session.query(User).get(5)

#2685

«Дружественная» структуризация конструкций Core SQL без диалекта

Вызов str() в конструкции Core SQL теперь выдает строку в большем количестве случаев, чем раньше, поддерживая различные SQL-конструкции, обычно не присутствующие в стандартном SQL, такие как RETURNING, индексы массивов и нестандартные типы данных:

>>> from sqlalchemy import table, column
t>>> t = table('x', column('a'), column('b'))
>>> print(t.insert().returning(t.c.a, t.c.b))
{printsql}INSERT INTO x (a, b) VALUES (:a, :b) RETURNING x.a, x.b

Функция str() теперь вызывает совершенно отдельный диалект/компилятор, предназначенный только для печати обычных строк, не требующих настройки диалекта, поэтому по мере появления новых случаев «просто покажи мне строку!» они могут быть добавлены в этот диалект/компилятор, не влияя на поведение реальных диалектов.

#3631

Функция type_coerce теперь является постоянным элементом SQL

Функция type_coerce() ранее возвращала объект либо типа BindParameter, либо Label, в зависимости от входных данных. Это приводило к тому, что в случае использования преобразований выражений, например, при преобразовании элемента из Column в BindParameter, что критично для ленивой загрузки на уровне ORM, информация о коэрцитивности типов не использовалась бы, поскольку она уже была бы потеряна.

Для улучшения этого поведения функция теперь возвращает постоянный контейнер TypeCoerce вокруг заданного выражения, который сам остается незатронутым; эта конструкция явно оценивается компилятором SQL. Это позволяет сохранять принудительность внутреннего выражения независимо от того, как модифицируется оператор, в том числе при замене содержащегося в нем элемента на другой, как это принято в ORM при ленивой загрузке.

В тестовом примере, иллюстрирующем этот эффект, используется неоднородное условие primaryjoin в сочетании с пользовательскими типами и ленивой загрузкой. Дается пользовательский тип, который применяет CAST в качестве «выражения связывания»:

class StringAsInt(TypeDecorator):
    impl = String

    def column_expression(self, col):
        return cast(col, Integer)

    def bind_expression(self, value):
        return cast(value, String)

Затем, отображение, в котором мы приравниваем строковый столбец «id» в одной таблице к целочисленному столбцу «id» в другой:

class Person(Base):
    __tablename__ = "person"
    id = Column(StringAsInt, primary_key=True)

    pets = relationship(
        "Pets",
        primaryjoin=(
            "foreign(Pets.person_id)" "==cast(type_coerce(Person.id, Integer), Integer)"
        ),
    )


class Pets(Base):
    __tablename__ = "pets"
    id = Column("id", Integer, primary_key=True)
    person_id = Column("person_id", Integer)

Выше, в выражении relationship.primaryjoin, мы используем type_coerce() для обработки связанных параметров, передаваемых через lazyloading, как целых чисел, поскольку мы уже знаем, что они будут получены из нашего типа StringAsInt, который сохраняет значение как целое число в Python. Затем мы используем cast(), чтобы в качестве SQL-выражения столбец VARCHAR «id» был CAST в целое число для обычного неконвертируемого объединения, как в Query.join() или joinedload(). То есть объединенная загрузка .pets имеет вид:

SELECT person.id AS person_id, pets_1.id AS pets_1_id,
       pets_1.person_id AS pets_1_person_id
FROM person
LEFT OUTER JOIN pets AS pets_1
ON pets_1.person_id = CAST(person.id AS INTEGER)

Без CAST в предложении ON join сильно типизированные базы данных, такие как PostgreSQL, откажутся неявно сравнивать целые числа и выйдут из строя.

Ленивая загрузка в случае .pets основана на замене столбца Person.id во время загрузки на связанный параметр, который получает загруженное в Python значение. Именно при такой замене теряется смысл нашей функции type_coerce(). До внесения изменений эта ленивая загрузка выглядела так:

SELECT pets.id AS pets_id, pets.person_id AS pets_person_id
FROM pets
WHERE pets.person_id = CAST(CAST(%(param_1)s AS VARCHAR) AS INTEGER)
-- {'param_1': 5}

Там, где выше, мы видим, что наше значение 5 в языке Python сначала CAST в VARCHAR, а затем обратно в INTEGER в SQL; двойной CAST, который работает, но, тем не менее, не является тем, что мы просили.

Благодаря этому изменению функция type_coerce() сохраняет обертку даже после замены столбца на связанный параметр, и запрос теперь выглядит следующим образом:

SELECT pets.id AS pets_id, pets.person_id AS pets_person_id
FROM pets
WHERE pets.person_id = CAST(%(param_1)s AS INTEGER)
-- {'param_1': 5}

Где наш внешний CAST, находящийся в primaryjoin, продолжает действовать, а ненужный CAST, находящийся в части пользовательского типа StringAsInt, удаляется, как и предполагалось функцией type_coerce().

#3531

Ключевые изменения в поведении - ORM

JSON Columns не будет вставлять JSON NULL, если не указано значение и не установлено значение по умолчанию

Как подробно описано в JSON «null» вставляется, как и ожидалось, при операциях ORM, и опускается, если не присутствует, JSON не будет отображать «нулевое» значение JSON, если значение отсутствует полностью. Для предотвращения SQL NULL необходимо установить значение по умолчанию. Учитывая следующее отображение:

class MyObject(Base):
    # ...

    json_value = Column(JSON(none_as_null=False), nullable=False)

Следующая операция промывки завершится с ошибкой целостности:

obj = MyObject()  # note no json_value
session.add(obj)
session.commit()  # will fail with integrity error

Если по умолчанию для столбца должен быть JSON NULL, установите это значение в параметре Column:

class MyObject(Base):
    # ...

    json_value = Column(JSON(none_as_null=False), nullable=False, default=JSON.NULL)

Или убедитесь, что значение присутствует в объекте:

obj = MyObject(json_value=None)
session.add(obj)
session.commit()  # will insert JSON NULL

Обратите внимание, что установка флага None по умолчанию равносильна его полному отсутствию; флаг JSON.none_as_null не влияет на значение None, передаваемое в Column.default или Column.server_default:

# default=None is the same as omitting it entirely, does not apply JSON NULL
json_value = Column(JSON(none_as_null=False), nullable=False, default=None)

Столбцы больше не добавляются избыточно с помощью DISTINCT + ORDER BY

Запрос, подобный следующему, теперь будет дополнять только те столбцы, которые отсутствуют в списке SELECT, без дублирования:

q = (
    session.query(User.id, User.name.label("name"))
    .distinct()
    .order_by(User.id, User.name, User.fullname)
)

Производит:

SELECT DISTINCT user.id AS a_id, user.name AS name,
 user.fullname AS a_fullname
FROM a ORDER BY user.id, user.name, user.fullname

Ранее он производил:

SELECT DISTINCT user.id AS a_id, user.name AS name, user.name AS a_name,
  user.fullname AS a_fullname
FROM a ORDER BY user.id, user.name, user.fullname

Там, где указано выше, столбец user.name добавляется без необходимости. На результат это не повлияет, так как дополнительные столбцы в любом случае не входят в результат, но эти столбцы лишние.

Кроме того, при использовании формата PostgreSQL DISTINCT ON путем передачи выражений в Query.distinct(), описанная выше логика «добавления столбцов» полностью отключается.

Когда запрос объединяется в подзапрос для целей объединенной ускоренной загрузки, правила «дополнения списка столбцов» обязательно должны быть более агрессивными, чтобы ORDER BY все еще мог быть удовлетворен, поэтому данный случай остается неизменным.

#3641

Одноименные декораторы @validates теперь будут вызывать исключение

Декоратор validates() должен создаваться только один раз в классе для конкретного имени атрибута. Создание нескольких декораторов теперь приводит к ошибке, тогда как раньше он молча выбирал только последний определенный валидатор:

class A(Base):
    __tablename__ = "a"
    id = Column(Integer, primary_key=True)

    data = Column(String)

    @validates("data")
    def _validate_data_one(self):
        assert "x" in data

    @validates("data")
    def _validate_data_two(self):
        assert "y" in data


configure_mappers()

Будет повышать:

sqlalchemy.exc.InvalidRequestError: A validation function for mapped attribute 'data'
on mapper Mapper|A|a already exists.

#3776

Ключевые изменения в поведении - Основные

TextClause.columns() при позиционной передаче будет сопоставлять столбцы не по имени, а позиционно

Новое поведение метода TextClause.columns(), который сам был недавно добавлен в версии 0.9, заключается в том, что когда столбцы передаются позиционно без дополнительных аргументов в виде ключевых слов, они привязываются к столбцам конечного результирующего набора позиционно, а не по имени. Предполагается, что влияние этого изменения будет незначительным, поскольку метод всегда документировался, иллюстрируя передачу столбцов в том же порядке, что и в текстовом SQL-запросе, что кажется интуитивно понятным, хотя внутренние средства не проверяли этого.

Приложение, использующее этот метод путем позиционной передачи ему объектов Column, должно убедиться, что позиция этих объектов Column соответствует позиции, в которой эти столбцы указаны в текстовом SQL.

Например, код, подобный следующему:

stmt = text("SELECT id, name, description FROM table")

# no longer matches by name
stmt = stmt.columns(my_table.c.name, my_table.c.description, my_table.c.id)

Больше не будет работать так, как ожидалось; порядок заданных столбцов теперь имеет значение:

# correct version
stmt = stmt.columns(my_table.c.id, my_table.c.name, my_table.c.description)

Возможно, более вероятным является заявление следующего содержания:

stmt = text("SELECT * FROM table")
stmt = stmt.columns(my_table.c.id, my_table.c.name, my_table.c.description)

теперь несколько рискованно, так как спецификация «*» обычно передает столбцы в том порядке, в котором они присутствуют в самой таблице. Если структура таблицы изменится в результате изменения схемы, то этот порядок может перестать быть таковым. Поэтому при использовании TextClause.columns() рекомендуется явно указывать в текстовом SQL нужные столбцы, хотя о самих именах в текстовом SQL беспокоиться уже не стоит.

Строка server_default теперь литерал в кавычках

Серверное значение по умолчанию, переданное в Column.server_default как обычная строка Python, в которую вставлены кавычки, теперь передается через систему литерального цитирования:

>>> from sqlalchemy.schema import MetaData, Table, Column, CreateTable
>>> from sqlalchemy.types import String
>>> t = Table("t", MetaData(), Column("x", String(), server_default="hi ' there"))
>>> print(CreateTable(t))
{printsql}CREATE TABLE t (
    x VARCHAR DEFAULT 'hi '' there'
)

Ранее цитата отображалась напрямую. Это изменение может быть несовместимо с обратным ходом событий для приложений с подобным сценарием использования, которые работали в обход этой проблемы.

#3809

При объединении или подобном объединении SELECT с LIMIT/OFFSET/ORDER BY встроенные select’ы теперь заключаются в круглые скобки

Проблема, которая, как и другие, долгое время была обусловлена недостаточными возможностями SQLite, теперь усовершенствована для работы на всех поддерживающих бэкендах. Мы имеем в виду запрос, представляющий собой объединение операторов SELECT, которые сами содержат функции ограничения или упорядочивания строк, такие как LIMIT, OFFSET и/или ORDER BY:

(SELECT x FROM table1 ORDER BY y LIMIT 1) UNION
(SELECT x FROM table2 ORDER BY y LIMIT 2)

В приведенном выше запросе для корректной группировки вложенных результатов необходимо использовать круглые скобки внутри каждого подвыбора. В SQLAlchemy Core приведенное выше утверждение выглядит следующим образом:

stmt1 = select([table1.c.x]).order_by(table1.c.y).limit(1)
stmt2 = select([table1.c.x]).order_by(table2.c.y).limit(2)

stmt = union(stmt1, stmt2)

Ранее в приведенной выше конструкции внутренние операторы SELECT не заключались в круглые скобки, что приводило к сбою запроса на всех бэкендах.

Приведенные выше форматы будут продолжать работать на SQLite; кроме того, формат, включающий ORDER BY, но не содержащий LIMIT/SELECT, будет продолжать работать на Oracle. Это не является обратно-несовместимым изменением, поскольку запросы не работают и без скобок; с исправлением запросы, по крайней мере, работают во всех других базах данных.

Во всех случаях для создания UNION из ограниченных операторов SELECT, который также работает на SQLite и во всех случаях на Oracle, подзапросы должны представлять собой SELECT из ALIAS:

stmt1 = select([table1.c.x]).order_by(table1.c.y).limit(1).alias().select()
stmt2 = select([table2.c.x]).order_by(table2.c.y).limit(2).alias().select()

stmt = union(stmt1, stmt2)

Этот обходной путь работает на всех версиях SQLAlchemy. В ORM это выглядит следующим образом:

stmt1 = session.query(Model1).order_by(Model1.y).limit(1).subquery().select()
stmt2 = session.query(Model2).order_by(Model2.y).limit(1).subquery().select()

stmt = session.query(Model1).from_statement(stmt1.union(stmt2))

Поведение здесь имеет много параллелей с поведением «переписывания соединений», представленным в SQLAlchemy 0.9 в Многие выражения JOIN и LEFT OUTER JOIN больше не будут обернуты в (SELECT * FROM …) AS ANON_1; однако в данном случае мы решили не добавлять новое поведение переписывания, чтобы учесть этот случай для SQLite. Существующее поведение переписывания и так очень сложно, а случай UNION с парентезированными операторами SELECT встречается гораздо реже, чем случай использования этой возможности в «право-вложенном соединении».

#2528

Улучшения и изменения в диалекте - PostgreSQL

Поддержка INSERT..ON CONFLICT (DO UPDATE | DO NOTHING)

Предложение ON CONFLICT в INSERT, добавленное в PostgreSQL начиная с версии 9.5, теперь поддерживается с помощью специфичной для PostgreSQL версии объекта Insert, через sqlalchemy.dialects.postgresql.dml.insert(). Этот подкласс Insert добавляет два новых метода Insert.on_conflict_do_update() и Insert.on_conflict_do_nothing(), которые реализуют полный синтаксис, поддерживаемый PostgreSQL 9.5 в этой области:

from sqlalchemy.dialects.postgresql import insert

insert_stmt = insert(my_table).values(id="some_id", data="some data to insert")

do_update_stmt = insert_stmt.on_conflict_do_update(
    index_elements=[my_table.c.id], set_=dict(data="some data to update")
)

conn.execute(do_update_stmt)

Вышеприведенные данные будут отображаться:

INSERT INTO my_table (id, data)
VALUES (:id, :data)
ON CONFLICT id DO UPDATE SET data=:data_2

#3529

В типах ARRAY и JSON теперь корректно указывается «unhashable»

Как описано в Изменения, касающиеся «нехешируемых» типов, влияют на дедупликацию строк ORM, ORM полагается на возможность создания хэш-функции для значений столбцов, когда в выбранных сущностях запроса смешиваются полные сущности ORM с выражениями столбцов. Флаг hashable=False теперь корректно устанавливается для всех типов «структуры данных» PG, включая ARRAY и JSON. Типы JSONB и HSTORE уже включали этот флаг. Для ARRAY этот флаг является условным, основанным на флаге ARRAY.as_tuple, однако для того, чтобы значение массива присутствовало в составленной ORM-строке, устанавливать этот флаг больше не нужно.

#3499

Корректные типы SQL устанавливаются из индексированного доступа к ARRAY, JSON, HSTORE

Для всех трех вариантов ARRAY, JSON и HSTORE тип SQL, присвоенный выражению, возвращаемому при индексированном доступе, например col[someindex], должен быть корректным во всех случаях.

К ним относятся:

  • Тип SQL-выражения, назначаемый для индексированного доступа к ARRAY, учитывает количество настроенных размерностей. Столбец ARRAY с тремя измерениями вернет SQL-выражение с типом ARRAY на одно измерение меньше. Теперь, имея столбец с типом ARRAY(Integer, dimensions=3), мы можем выполнить такое выражение:

    int_expr = col[5][6][7]  # returns an Integer expression object

    Ранее индексированный доступ к col[5] возвращал выражение типа Integer, в котором мы уже не могли выполнить индексированный доступ для остальных размерностей, если только не использовали cast() или type_coerce().

  • Типы JSON и JSONB теперь являются зеркальным отражением того, что делает сам PostgreSQL для индексированного доступа. Это означает, что весь индексированный доступ для типа JSON или JSONB возвращает выражение, которое само по себе всегда JSON или JSONB, если только не используется модификатор Comparator.astext. Это означает, что независимо от того, на какую структуру JSON в конечном итоге ссылается индексированный доступ - строку, список, число или другую структуру JSON, PostgreSQL всегда считает ее самой JSON, если только она явно не приведена к другому виду. Как и в случае с типом ARRAY, это означает, что теперь можно легко создавать JSON-выражения с несколькими уровнями индексированного доступа:

    json_expr = json_col["key1"]["attr1"][5]
  • Тип «textual», возвращаемый при индексированном доступе к HSTORE, а также тип «textual», возвращаемый при индексированном доступе к JSON и JSONB в сочетании с модификатором Comparator.astext, теперь является настраиваемым; по умолчанию в обоих случаях он равен TextClause, но может быть установлен на заданный пользователем тип с помощью параметров JSON.astext_type или HSTORE.text_type.

#3499 #3487

Операция JSON cast() теперь требует явного вызова .astext

В рамках изменений в Корректные типы SQL устанавливаются из индексированного доступа к ARRAY, JSON, HSTORE работа оператора ColumnElement.cast() над JSON и JSONB больше не приводит к неявному обращению к модификатору Comparator.astext; типы JSON/JSONB в PostgreSQL поддерживают операции CAST друг к другу без аспекта «astext».

Это означает, что в большинстве случаев приложение, которое делало это:

expr = json_col["somekey"].cast(Integer)

Теперь необходимо изменить на следующее:

expr = json_col["somekey"].astext.cast(Integer)

ARRAY с ENUM теперь будет выдавать CREATE TYPE для ENUM

Теперь определение таблицы, подобное следующему, будет выдавать CREATE TYPE, как и ожидалось:

enum = Enum(
    "manager",
    "place_admin",
    "carwash_admin",
    "parking_admin",
    "service_admin",
    "tire_admin",
    "mechanic",
    "carwasher",
    "tire_mechanic",
    name="work_place_roles",
)


class WorkPlacement(Base):
    __tablename__ = "work_placement"
    id = Column(Integer, primary_key=True)
    roles = Column(ARRAY(enum))


e = create_engine("postgresql://scott:tiger@localhost/test", echo=True)
Base.metadata.create_all(e)

излучает:

CREATE TYPE work_place_roles AS ENUM (
    'manager', 'place_admin', 'carwash_admin', 'parking_admin',
    'service_admin', 'tire_admin', 'mechanic', 'carwasher',
    'tire_mechanic')

CREATE TABLE work_placement (
    id SERIAL NOT NULL,
    roles work_place_roles[],
    PRIMARY KEY (id)
)

#2729

Контрольные ограничения теперь отражают

Диалект PostgreSQL теперь поддерживает отражение ограничений CHECK как в рамках метода Inspector.get_check_constraints(), так и в рамках отражения Table в коллекции Table.constraints.

«Обычные» и «материализованные» представления могут быть проинспектированы отдельно

Новый аргумент PGInspector.get_view_names.include позволяет указать, какие подтипы представлений должны быть возвращены:

from sqlalchemy import inspect

insp = inspect(engine)

plain_views = insp.get_view_names(include="plain")
all_views = insp.get_view_names(include=("plain", "materialized"))

#3588

Добавлена опция табличного пространства в Index

Объект Index теперь принимает аргумент postgresql_tablespace для указания TABLESPACE, аналогично тому, как он принимается объектом Table.

#3720

Поддержка PyGreSQL

Теперь поддерживается интерфейс PyGreSQL DBAPI.

Модуль «postgres» удален

Модуль sqlalchemy.dialects.postgres, давно устаревший, удален; он уже много лет выдает предупреждение, и проекты должны обращаться к sqlalchemy.dialects.postgresql. Однако URL-адреса движков в виде postgres:// будут продолжать работать.

Поддержка FOR UPDATE SKIP LOCKED / FOR NO KEY UPDATE / FOR KEY SHARE

Новые параметры GenerativeSelect.with_for_update.skip_locked и GenerativeSelect.with_for_update.key_share как в Core, так и в ORM применяют модификацию запроса «SELECT…FOR UPDATE» или «SELECT…FOR SHARE» на бэкенде PostgreSQL:

  • ВЫБЕРИТЕ ДЛЯ ОБНОВЛЕНИЯ БЕЗ КЛЮЧА:

    stmt = select([table]).with_for_update(key_share=True)
  • ВЫБРАТЬ ДЛЯ ОБНОВЛЕНИЯ ПРОПУСТИТЬ ЗАБЛОКИРОВАНО:

    stmt = select([table]).with_for_update(skip_locked=True)
  • ВЫБРАТЬ ДЛЯ СОВМЕСТНОГО ИСПОЛЬЗОВАНИЯ КЛЮЧЕЙ:

    stmt = select([table]).with_for_update(read=True, key_share=True)

Улучшения и изменения в диалекте - MySQL

Поддержка MySQL JSON

В диалект MySQL добавлен новый тип JSON, поддерживающий тип JSON, недавно добавленный в MySQL 5.7. Этот тип обеспечивает как сохранение JSON, так и элементарный индексированный доступ, используя внутреннюю функцию JSON_EXTRACT. Индексируемый JSON-столбец, работающий в MySQL и PostgreSQL, может быть получен с помощью типа данных JSON, общего для MySQL и PostgreSQL.

#3547

Добавлена поддержка «уровня изоляции» AUTOCOMMIT

Диалект MySQL теперь принимает значение «AUTOCOMMIT» для параметров create_engine.isolation_level и Connection.execution_options.isolation_level:

connection = engine.connect()
connection = connection.execution_options(isolation_level="AUTOCOMMIT")

Уровень изоляции использует различные атрибуты «autocommit», предоставляемые большинством DBAPI MySQL.

#3332

Больше не нужно генерировать неявный KEY для составного первичного ключа с AUTO_INCREMENT

В диалекте MySQL было реализовано такое поведение, что если составной первичный ключ в таблице InnoDB имел AUTO_INCREMENT в одном из своих столбцов, который не был первым, например:

t = Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True, autoincrement=False),
    Column("y", Integer, primary_key=True, autoincrement=True),
    mysql_engine="InnoDB",
)

Будет сгенерирован DDL следующего вида:

CREATE TABLE some_table (
    x INTEGER NOT NULL,
    y INTEGER NOT NULL AUTO_INCREMENT,
    PRIMARY KEY (x, y),
    KEY idx_autoinc_y (y)
)ENGINE=InnoDB

Обратите внимание на приведенный выше «KEY» с автоматически генерируемым именем; это изменение, которое появилось в диалекте много лет назад в ответ на проблему, связанную с тем, что AUTO_INCREMENT без этого дополнительного KEY в InnoDB не выполнялся.

Этот обходной путь был удален и заменен на гораздо более эффективную систему простого указания столбца AUTO_INCREMENT первым в первичном ключе:

CREATE TABLE some_table (
    x INTEGER NOT NULL,
    y INTEGER NOT NULL AUTO_INCREMENT,
    PRIMARY KEY (y, x)
)ENGINE=InnoDB

Чтобы сохранить явный контроль над упорядочиванием столбцов первичного ключа, используйте конструкцию PrimaryKeyConstraint в явном виде (1.1.0b2) (вместе с KEY для столбца с автоинкрементом, как того требует MySQL), например:

t = Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True, autoincrement=True),
    PrimaryKeyConstraint("x", "y"),
    UniqueConstraint("y"),
    mysql_engine="InnoDB",
)

Вместе с изменением Директива .autoincrement больше не включается неявно для составного столбца первичного ключа теперь проще задавать составные первичные ключи с автоматическим инкрементом или без него; Column.autoincrement теперь по умолчанию принимает значение "auto", а директивы autoincrement=False больше не нужны:

t = Table(
    "some_table",
    metadata,
    Column("x", Integer, primary_key=True),
    Column("y", Integer, primary_key=True, autoincrement=True),
    mysql_engine="InnoDB",
)

Улучшения и изменения в диалекте - SQLite

Обходное решение для вложенных соединений с правой стороны отменено для SQLite версии 3.7.16

В версии 0.9 функция, представленная Многие выражения JOIN и LEFT OUTER JOIN больше не будут обернуты в (SELECT * FROM …) AS ANON_1, была реализована с большим трудом, чтобы поддержать переписывание джойнов на SQLite, чтобы всегда использовать подзапросы для достижения эффекта «right-nested-join», поскольку SQLite не поддерживал этот синтаксис в течение многих лет. По иронии судьбы, версия SQLite, упомянутая в этой миграционной заметке, 3.7.15.2, была последней версией SQLite, в которой это ограничение действительно существовало! В следующем выпуске 3.7.16 была добавлена поддержка вложенных справа джойнов. В версии 1.1 была проведена работа по определению конкретной версии SQLite и исходного коммита, в котором было сделано это изменение (в журнале изменений SQLite оно упоминается загадочной фразой «Enhance the query optimizer to exploit transitive join constraints» без ссылки на номер проблемы, номер изменения или дальнейшее объяснение), и теперь обходные пути, присутствующие в этом изменении, отменены для SQLite, когда DBAPI сообщает, что используется версия 3.7.16 или выше.

#3634

Обходное решение для точечных имен столбцов отменено для SQLite версии 3.10.0

В диалекте SQLite уже давно существует обходной путь для решения проблемы, когда драйвер базы данных не сообщает правильные имена столбцов для некоторых наборов результатов SQL, в частности, при использовании UNION. Обходной путь подробно описан в Имена столбцов с точками, и требует, чтобы SQLAlchemy считала, что любое имя столбца с точкой в нем на самом деле является комбинацией tablename.columnname, передаваемой через это ошибочное поведение, с возможностью его отключения через опцию выполнения sqlite_raw_colnames.

Начиная с версии SQLite 3.10.0, ошибка в UNION и других запросах была исправлена; как и изменение, описанное в Обходное решение для вложенных соединений с правой стороны отменено для SQLite версии 3.7.16, в журнале изменений SQLite оно обозначено лишь критически: «Добавлено поле colUsed в sqlite3_index_info для использования методом sqlite3_module.xBestIndex», однако трансляция SQLAlchemy этих точечных имен столбцов больше не требуется в этой версии, поэтому отключается при обнаружении версии 3.10.0 или выше.

В целом, SQLAlchemy ResultProxy начиная с версии 1.0 гораздо меньше полагается на имена столбцов в результирующих наборах при выдаче результатов для SQL-конструкций Core и ORM, поэтому важность этой проблемы в любом случае уже снизилась.

#3633

Улучшена поддержка удаленных схем

Диалект SQLite теперь реализует Inspector.get_schema_names() и дополнительно улучшена поддержка таблиц и индексов, создаваемых и отражаемых из удаленной схемы, которая в SQLite представляет собой базу данных, которой присваивается имя с помощью оператора ATTACH; ранее DDL``CREATE INDEX`` работал некорректно для таблиц, привязанных к схеме, а теперь метод Inspector.get_foreign_keys() будет указывать в результатах заданную схему. Межсхемные внешние ключи не поддерживаются.

Отражение имени ограничений PRIMARY KEY

Бэкенд SQLite теперь использует представление «sqlite_master» SQLite для извлечения имени ограничения первичного ключа таблицы из исходного DDL, аналогично тому, как это сделано для ограничений внешнего ключа в последних версиях SQLAlchemy.

#3629

Контрольные ограничения теперь отражают

Диалект SQLite теперь поддерживает отражение ограничений CHECK как в рамках метода Inspector.get_check_constraints(), так и в рамках отражения Table в коллекции Table.constraints.

Фразы внешнего ключа ON DELETE и ON UPDATE теперь отражают

Теперь Inspector будет содержать фразы ON DELETE и ON UPDATE из ограничений внешнего ключа на диалекте SQLite, а объект ForeignKeyConstraint, отраженный как часть Table, также будет указывать на эти фразы.

Улучшения и изменения в диалекте - SQL Server

Добавлена поддержка уровня изоляции транзакций для SQL Server

Все диалекты SQL Server поддерживают настройку уровня изоляции транзакций с помощью параметров create_engine.isolation_level и Connection.execution_options.isolation_level. Поддерживаются четыре стандартных уровня, а также SNAPSHOT:

engine = create_engine(
    "mssql+pyodbc://scott:tiger@ms_2008", isolation_level="REPEATABLE READ"
)

#3534

Типы String / varlength больше не представляют «max» явным образом при отражении

При отражении типа String, TextClause и т.д., включающего длину, «не удлиненный» тип под SQL Server будет копировать параметр «длина» как значение "max":

>>> from sqlalchemy import create_engine, inspect
>>> engine = create_engine("mssql+pyodbc://scott:tiger@ms_2008", echo=True)
>>> engine.execute("create table s (x varchar(max), y varbinary(max))")
>>> insp = inspect(engine)
>>> for col in insp.get_columns("s"):
...     print(col["type"].__class__, col["type"].length)
<class 'sqlalchemy.sql.sqltypes.VARCHAR'> max
<class 'sqlalchemy.dialects.mssql.base.VARBINARY'> max

Параметр «length» в базовых типах должен быть целым числом или только None; None означает неограниченную длину, которую диалект SQL Server интерпретирует как «max». Исправление заключается в том, чтобы эти длины выводились как None, чтобы объекты типа работали в контекстах, отличных от SQL Server:

>>> for col in insp.get_columns("s"):
...     print(col["type"].__class__, col["type"].length)
<class 'sqlalchemy.sql.sqltypes.VARCHAR'> None
<class 'sqlalchemy.dialects.mssql.base.VARBINARY'> None

Приложения, которые могли полагаться на прямое сравнение значения «length» со строкой «max», должны считать, что значение None означает то же самое.

#3504

Поддержка «non clustered» для первичного ключа для разрешения кластеризации в других местах

Флаг mssql_clustered, доступный для UniqueConstraint, PrimaryKeyConstraint, Index, теперь имеет значение по умолчанию None и может быть установлен в False, что приведет к тому, что ключевое слово NONCLUSTERED будет использоваться в частности для первичного ключа, позволяя использовать другой индекс в качестве «кластерного».

Флаг legacy_schema_aliasing теперь имеет значение False

В SQLAlchemy 1.0.5 для диалекта MSSQL был введен флаг legacy_schema_aliasing, позволяющий отключить так называемый «legacy mode» алиасинг. Этот режим позволяет превратить таблицы с критериями схемы в псевдонимы; для таблицы типа:

account_table = Table(
    "account",
    metadata,
    Column("id", Integer, primary_key=True),
    Column("info", String(100)),
    schema="customer_schema",
)

Унаследованный режим поведения будет пытаться превратить имя таблицы, заданное схемой, в псевдоним:

>>> eng = create_engine("mssql+pymssql://mydsn", legacy_schema_aliasing=True)
>>> print(account_table.select().compile(eng))
{printsql}SELECT account_1.id, account_1.info
FROM customer_schema.account AS account_1

Однако, как было показано, такое алиасинг не является необходимым и во многих случаях приводит к некорректному SQL.

В SQLAlchemy 1.1 флаг legacy_schema_aliasing теперь имеет значение False, что отключает этот режим поведения и позволяет диалекту MSSQL нормально работать с таблицами, имеющими квалификацию схемы. Для приложений, которые могут полагаться на такое поведение, установите флаг обратно в значение True.

#3434

Улучшения и изменения в диалекте - Oracle

Поддержка SKIP LOCKED

Новый параметр GenerativeSelect.with_for_update.skip_locked как в Core, так и в ORM будет генерировать суффикс «SKIP LOCKED» для запроса «SELECT…FOR UPDATE» или «SELECT… FOR SHARE».

Вернуться на верх