Модели по существующей базе¶
hare inspectdb [tables...] [--connection ALIAS] [--schema SCHEMA] читает таблицы существующей базы —
PostgreSQL, SQLite или ClickHouse — и печатает модуль моделей для них: поля, связи, индексы, ограничения
и опции таблиц, так же, как их написал бы makemigrations. То же доступно из кода
(ниже).
Что восстанавливается¶
inspectdb сообщает об ошибке, а не создаёт молча неверную модель, если встречает совпадение имён,
которое нельзя выразить: две таблицы, различающиеся только регистром, иначе дали бы два одинаковых имени
классов Python (один молча заменил бы другой), а колонка внешнего ключа, имя атрибута которой после
отбрасывания суффикса (author_id → author) совпадает с другой обычной колонкой, иначе потеряла бы
одно из двух полей. Оба случая дают явную ошибку. Он также восстанавливает составной внешний ключ одним
ForeignKeyField (а не полем на каждую колонку) и индекс по выражению SQL, а не по списку колонок, — оба
превращаются в настоящий код модели, а не в комментарий # TODO. Составной внешний ключ становится
связью, когда он ссылается на весь первичный ключ цели под именами колонок, которые есть у составной
связи, — <field>_<target key column> (owner_a, owner_b для цели с ключом a, b); ключ под
другими именами (a, b) остаётся обычными полями с # TODO, где он назван: составная связь называет
свои колонки только по имени поля, поэтому чтобы объявить её, колонки нужно переименовать. Индекс или
ограничение уникальности по колонкам связи называет саму связь (Index(fields=["owner", "width"])), если
они идут подряд в порядке её ключа и в одном направлении, — вразбивку, в обратном порядке или в разных
направлениях каждая колонка называется сама (["owner_b", "owner_a"]), и такой индекс остаётся
отдельным, а не индексом связи и не связью один-к-одному; комментарий, записанный на каждую её колонку,
становится её description. hare drift читает индексы так же. Таблица, в
имени которой есть точка (её не может содержать Meta.table), пропускается с комментарием об этом.
Внешний ключ, объявленный с ON DELETE SET DEFAULT, восстанавливается как on_delete=SET_DEFAULT с
DEFAULT колонки в качестве db_default (ForeignKeyField с настоящим ограничением его требует). Если
у колонки нет DEFAULT, база на самом деле сбрасывает её в NULL (или останавливает удаление, если
колонка NOT NULL), поэтому inspectdb записывает SET_NULL (или NO_ACTION) с комментарием # TODO
вместо поля SET_DEFAULT, которое нельзя было бы импортировать. ON DELETE SET NULL у колонки
NOT NULL, на котором удаление в базе останавливается, так же восстанавливается как NO_ACTION с
# TODO.
ON DELETE NO ACTION DEFERRABLE INITIALLY IMMEDIATE в PostgreSQL — ограничение, которое создаёт
on_delete=PROTECT, — восстанавливается как PROTECT. То, что связь не объявляет, — действие
ON UPDATE, другая отложенность (DEFERRABLE INITIALLY DEFERRED), — называется в # TODO: миграция
модели создаёт ограничение без этого, а drift сообщает о расхождении (SQLite сообщает только действие
ON UPDATE, без отложенности).
Другие правила восстановления:
- Колонка внешнего ключа, которая одновременно первичный ключ таблицы, становится
OneToOneField(..., primary_key=True); колонка внешнего ключа без суффикса_id(owner_code) получаетsource_field="owner_code". - Если у таблицы две и больше связей с одной и той же таблицей (в том числе пара ссылок таблицы на саму
себя), каждая получает
related_name="<table>_<field>_set", чтобы их обратные связи не совпадали. - Частичный индекс сохраняет своё условие
WHEREкакPartialIndex(condition=RawSQLTerm(...))— в том виде, в каком его сообщает база, и в PostgreSQL, и в SQLite. Индекс по выражению (lower(note)) восстанавливается какIndex(RawSQLTerm(...))на обоих диалектах — ключ без скобок, которые SQLite хранит вокруг него ((lower(note))), — а составной внешний ключ восстанавливается и в SQLite. - Индекс или ограничение сохраняют своё имя в базе (
Index(..., name="book_published_idx")), если это не имя, которое hare создаёт для такого объявления, и не имя, которое PostgreSQL даёт безымянному индексу или ограничениюUNIQUE(<table>_<columns>_idx/_key); обычный индекс по одной колонке со своим именем становится именованной записьюMeta.indexes, а неdb_index=True. - Уникальный индекс восстанавливается тем объявлением, которому принадлежит его имя: безымянным
UniqueConstraint(fields=...)при имени, которое hare создаёт для такого ограничения (или которое PostgreSQL даёт безымянномуUNIQUE),Index(unique=True)при имени, которое hare создаёт для такого индекса, иначе — именованнымUniqueConstraintсо своимcondition=(частичный уникальный индекс, условие — готовый SQL) иdeferrable=/initially_deferred=(в том числе уDEFERRABLE UNIQUEпо одной колонке). - Условие
CHECKвосстанавливается без пары скобок, в которую его оборачивает PostgreSQL (CheckConstraint(check=RawSQLTerm("age >= 0"))); списокINпо колонкеvarchar— уCharEnumField— восстанавливается так, как был объявлен (color IN ('red', 'blue')), а не как(color)::text = ANY (...)PostgreSQL: объявленный заново, этот текст PostgreSQL выводит иначе. - Индекс восстанавливается с колонками
INCLUDE (...)(include=), порядкомDESC/NULLS FIRSTкаждого ключа ("-a",F("a").desc(nulls_last=True), ключ-выражение — какOrderedIndexKey(RawSQLTerm("lower(c)"), Order.DESC_NULLS_FIRST)), ключом сCOLLATEкак выражением (RawSQLTerm('d COLLATE "C"')) иNULLS NOT DISTINCT(UniqueConstraint(nulls_distinct=False)). То, что не может объявить ни один индекс hare, — порядок ключа сCOLLATEв SQLite, — в восстановленный индекс не попадает и перечисляется в# TODO. - Внешний ключ на таблицу другой схемы, чем проверяемая, остаётся обычной колонкой с
# TODO, называющим цель: связь может указывать только на модель из того же запуска. - Партиция партиционированной таблицы PostgreSQL не показывается (её заменяет сама партиционированная
таблица), если её не назвать явно;
driftтоже не считает партиции неизвестными таблицами. Таблица, партиционированная так, как не объявляет ни одинPostgresqlTableOptions(partitioning=...), — по выражению или с партицией, партиционированной в свою очередь, — получает# TODOс еёPARTITION BY: миграция модели создаёт таблицу без партиций. - Представление и материализованное представление — не модели:
inspectdbвсей схемы называет каждое в# NOTEв конце модуля — его объявляют вMeta.views/Meta.materialized_viewsмодели, которую оно читает, — вместе с триггерами на нём (INSTEAD OF), которые добавляет миграцияRunSQL. - Колонка идентификатора PostgreSQL (
GENERATED ... AS IDENTITY, не первичный ключ) становится целочисленным полем сgenerated=Trueи# TODO: hare никогда не записывает её и читает присвоенное значение обратно, но собственных команд для колонки идентификатора у него нет: миграция, которая её создаёт, иhare drift, который создаёт таблицы моделей в своей тени, такое поле отклоняют — объявите её модель сMeta.managed = False, чтобы drift работал для остальной схемы. Числовое значение по умолчанию в кавычках ('-1.5'::double precision) восстанавливается числом. Литеральное значение по умолчанию — значение, которым его читает поле ('7'целочисленной колонки —7,1или't'булевой —True); то, которого не может хранить ни одно значение поля, —'NaN'вещественной,'infinity'даты,'1 day'интервала, — восстанавливается своим текстом:SqlDefault("'NaN'"). - Целочисленный первичный ключ, которому база не даёт значения, — в PostgreSQL без последовательности и
без идентификатора, в SQLite другого типа, чем
INTEGER(только он — идентификатор строки), — восстанавливается сgenerated=False, чтобыcreate()передавал ключ. - Вычисляемая колонка становится
GeneratedFieldсо своим выражением — кроме хранимогоtsvectorPostgreSQL, выражение которого записываетTSVectorField: он восстанавливается этимTSVectorFieldс егоsource_fields,configиweights, и__searchсохраняет свою конфигурацию. - Колонка
UNIQUEили с индексом, тип поля которой нельзя индексировать (JSONField), теряет индекс с# TODO, а не даёт модель, которую нельзя импортировать. - Внешний ключ с
UNIQUEпо одной колонке восстанавливается какOneToOneField. - Собственный индекс внешнего ключа (обычный индекс ровно по его колонкам ключа) не записывается — у
ForeignKeyFieldон есть по умолчанию; внешний ключ без такого индекса и без другого индекса, начинающегося с его колонок, получаетdb_index=False. - Значение по умолчанию колонки-массива (
'{1,2}'::integer[]) восстанавливается списком (db_default=[1, 2]), элементы читаются по типу элемента массива. - Колонка-массив PostgreSQL восстанавливается с объявленным числом измерений:
integer[][]— какArrayField(base_field=ArrayField(base_field=IntField())). У колонки, созданной без объявления (черезCREATE TABLE ... AS), число измерений не записано, и она восстанавливается одномерной. - Колонка типа, который не пишет в точности ни одно поле hare (
real,json,numericбез размеров,BOOLEAN/NUMERIC/DECIMALв SQLite — десятичная колонка SQLite хранит целое или число с плавающей точкой, поэтому она становитсяFloatField), восстанавливается ближайшим полем с# TODO, где названы оба типа:driftсообщает о разнице, пока колонку не изменят на тип поля. Так же восстанавливается колонка домена PostgreSQL — полем типа домена — и первичный ключ identity (GENERATED ... AS IDENTITY) — генерируемым ключом, который hare объявляет через последовательность. - В SQLite hare объявляет колонку
BooleanFieldкакINT, так же, какIntField: такая колонка восстанавливается какIntFieldс# TODO, где сказано, что она может бытьBooleanField. Колонка SQLite с синонимом, который SQLite хранит так же, —DATETIMEдляTIMESTAMPhare,DOUBLE/DOUBLE PRECISION/FLOATдля егоREAL— читается так, как её объявляет hare: её поле восстанавливается без заметки, аdriftничего не сообщает. - Промежуточная таблица связи многие-ко-многим — две колонки, каждая внешний ключ на первичный ключ
таблицы того же запуска — восстанавливается как
ManyToManyFieldна модели таблицы её первой колонки:through=называет таблицу,forward_key/backward_key— её колонки, поле названо по таблице без имени таблицы владельца (booking_people→people),related_name="<таблица>_set". Сама таблица вместо класса получает# NOTEс именем поля. Таблица связи другой формы или такая, чьей второй стороны нет в запуске, пропускается с# NOTE, где сказано, как её объявить. - Колонка типа
ENUMPostgreSQL — и массив такого типа — восстанавливается какNativeEnumFieldнад перечислением, которое модуль объявляет из меток типа в их порядке (Mood = StrEnum('Mood', {'SAD': 'sad', 'OK': 'ok'}); метка, которая не является именем, превращается в имя:so-so→SO_SO,1st→VALUE_1ST), сtype_name=иdescription=''— иначе поле описало бы свои члены в комментарии, которого у колонки нет. Перечисление и его члены можно переименовать; значения — это метки. Другой пользовательский тип (составной) восстанавливается какTextFieldс# TODO, где он назван. - Колонка
Enum8/Enum16ClickHouse восстанавливается полем-перечислением, которое пишет тот же тип, над перечислением, которое модуль объявляет и называет по колонке (StatusLabels), сdescription='':CharEnumFieldнадStrEnum, когда у меток те номера, которыеCharEnumFieldдаёт своим значениям (таблица, созданная hare), иIntEnumFieldнадIntEnumиз меток и их номеров, когда каждая метка — имя (Enum8('LOW' = 1, 'HIGH' = 2)→IntEnum('LevelLabels', {'LOW': 1, 'HIGH': 2})). Любое другое перечисление — метки, которые не являются именами, илиEnum16, номера которого помещаются вEnum8, — восстанавливается какCharFieldсвоих меток с# TODO. timestampиtimeбез часового пояса,intervalиcharacter(n)в PostgreSQL восстанавливаются какNaiveDatetimeField,NaiveTimeField,IntervalFieldиFixedCharField(max_length=n), массив таких значений (иinet,cidr,macaddr) — какArrayFieldэтого поля, а элементы точности, которую не хранит ни одно поле (timestamptz(2)[]), получают# TODO;Date,DateTimeиDateTime64в ClickHouse, кромеDateTime64(6, 'UTC')поляDatetimeField, — какClickhouseDateFieldиClickhouseDatetimeFieldс точностью и часовым поясом колонки.- Колонка типа, для которого у hare нет поля, —
bit,money,xmlв PostgreSQL,ANYвSTRICT-таблице SQLite — восстанавливается какTextFieldс# TODO, где назван тип. - Защита на уровне строк (row level security) таблицы PostgreSQL восстанавливается как
Meta.row_level_security(RowLevelSecurity.ENABLEDилиFORCED, если она фильтрует и строки владельца), её политики — какMeta.policies(каждое условие —RawSQLTermс текстом, который хранит сервер), привилегии на неё и на её колонки, выданные ролям, кроме владельца, — какMeta.grants(одинGrantна роль и право передачи; колоночный — на набор колонок с одинаковыми привилегиями; привилегия, которой нет вPrivilege, получает# TODO), а последовательности, которыми владеет её колонка (OWNED BY) или из которых, без владельца, она берёт значение по умолчанию, — какMeta.sequences; последовательностьSERIALили identity первичного ключа из одной колонки к ним не относится: генерируемый ключ берёт свою, — если только из неё не берёт значение по умолчанию и другая колонка. Другая колонкаSERIAL— вне первичного ключа или колонка составного ключа — сохраняет там свою последовательность иdb_default=SqlDefault("nextval(...)"). Последовательность без владельца, из которой берут значения по умолчанию несколько таблиц, объявляет модель первой из них по имени — из тех, что читают её не первичным ключом из одной колонки; у остальных —# TODOс именем этой таблицы. - Движок ClickHouse
Distributed, читающий таблицы инспектируемой базы, называет еёcurrentDatabase(), как пишет hare, — модель создаёт его над таблицами той базы, в которой её мигрируют. - Колонка ClickHouse
EPHEMERAL, которую пишут, но нигде не хранят, восстанавливается своим полем с# TODO: ни одно поле её не объявляет, и миграция модели создаёт её хранимой. Запуск по всей схеме называет каждый словарь ClickHouse в# NOTE, как представление. - Запуск по всей схеме называет каждую функцию и каждую последовательность, которой не владеет и из
которой не берёт значение по умолчанию ни одна колонка, в
# NOTEс её объявлениемDatabaseFunction(...)/DatabaseSequence(...)(аргументы функции — со значениями по умолчанию иOUT-аргументами) — его кладут вMeta.functions/Meta.sequencesмодели, которая её использует: таблицы, к которой она относится, нет, — и сGrant(...)на каждую выданную на неё привилегию — дляMeta.grants. - Значение по умолчанию PostgreSQL «момент оператора» (
statement_timestamp()) восстанавливается какNow(), который так и пишется;now()иCURRENT_TIMESTAMP— начало транзакции — остаютсяSqlDefault("now()"), чтобы миграция сохранила время колонки. - Таблица без первичного ключа, единственный ключ которой — один составной внешний ключ под
UNIQUE, — это отдельная модель (OneToOneFieldиMeta.primary_key = None), а не промежуточная таблица связи многие-ко-многим, которая соединяет две стороны. - Значение по умолчанию колонки JSON (
'{}'::jsonb) восстанавливается самим документом (db_default={}), а выражение по умолчанию в SQLite (DEFAULT (datetime('now'))) сохраняет свои скобки:SqlDefault("(datetime('now'))"). - Значение по умолчанию ClickHouse, которое hare записал типизированным литералом — число с плавающей
точкой по его байтам,
toDecimal128('1.50', 2),toIPv6('::1'), приведение к типуDynamicилиVariant, — восстанавливается самим значением (db_default=1.5), а значение enum — так, как его хранит поле: уCharEnumFieldметкой, уIntEnumFieldчислом, какую бы сторону члена ни хранил сервер. - Колонка или таблица, имя которой в Python совпало бы с одним из импортов создаваемого модуля или с
атрибутом модели (колонка
fieldsилиpk, таблицаmodel), переименовывается — к полю добавляется_field(fields_fieldсsource_field="fields"), к классу — подчёркивание в конце (Model_сMeta.table = "model"), — а связи указывают на переименованный класс. Колонка с именем ключевого слова тоже получает_field(from_field), а серия подчёркиваний в имени колонки становится одним: имя поля не содержит__и не оканчивается на_(см. Типы полей).
Код моделей из программы¶
hare inspectdb печатает то, что возвращает hare.inspectdb.SchemaInspector:
from hare.inspectdb import SchemaInspector
source = await SchemaInspector.inspect(connection, ["books", "authors"], schema="public")
class SchemaInspector:
@staticmethod
async def inspect(connection: DatabaseClient, tables: list[str] | None = None, schema: str | None = None) -> str: ...
@staticmethod
def render_module(
table_infos: list[TableInfo], dialect: str, views: list[tuple[str, bool, tuple[str, ...]]] | None = None
) -> str: ...
inspect() создаёт один модуль, готовый к импорту, для этих таблиц (без tables — для всех таблиц схемы
schema, кроме служебных таблиц hare hare_migrations и hare_distributed_decisions; schema при None —
схема подключения по умолчанию; в SQLite это подключённая база, в ClickHouse — база сервера; таблица не схемы
по умолчанию получает свой Meta.schema). Ошибки: UnsupportedDialectError для диалекта, который не умеет
читать схему базы, SchemaNotFoundError/TableNotFoundError для отсутствующей схемы или таблицы и
DuplicateModelClassNameError для двух таблиц с одним именем класса. render_module() строит код для таблиц,
уже прочитанных через DatabaseCatalog (см. Чтение таблицы:
DatabaseCatalog); views — имя каждого представления,
материализовано ли оно и его триггеры — это представления, которые он называет в # NOTE в конце.
Каждая таблица сначала превращается в такое же состояние модели, какое есть у объявленной модели
(hare.inspectdb.InspectedModelBuilder(table_info, ModelGenerationOptions(dialect, app_label)).build()
возвращает InspectedModel, где state — ModelState миграций из настоящих объектов полей, индексов и
ограничений, — а также заметки # TODO и раскладку Meta). Код — это то же состояние, записанное так,
как его записывает файл миграции: каждое поле, индекс и ограничение — вызовом из своего deconstruct();
ModelFactory строит класс из того же самого состояния, поэтому сгенерированный код и класс, построенный
во время работы, никогда не расходятся.