Ошибки компиляции¶
В предыдущих главах мы не раз нарочно ошибались и смотрели, что ответит компилятор. Эта глава собирает такие ошибки вместе. Её не обязательно читать подряд: удобнее вернуться к ней, когда компилятор выдаст непонятное сообщение, и найти здесь похожее.
Сначала о том, как такие сообщения читать. Ошибка GHC состоит из нескольких
пунктов, и каждый начинается с •. Первый пункт — это само сообщение
pg-schema: какая таблица, какое поле и что с ним не так. Следующие пункты
(In a stmt of a 'do' block, In the first argument of …) показывают, где в
коде произошла ошибка, и обычно указывают на вызов операции (selectSch,
insertJSON и так далее), а не на определение типа: тип строки проверяется
там, где он встречается вместе с аннотацией. Поэтому одна ошибка в типе может
дать несколько одинаковых сообщений — по одному на каждый вызов, где этот тип
используется. Исправлять её нужно один раз, в определении типа, и все эти
сообщения исчезнут. Найти нужный тип помогают таблица и поле из первого
пункта.
Во всех примерах ниже показан только первый пункт сообщения.
Имена¶
Самая частая ошибка — опечатка в имени. Если не найдена таблица, сообщение короткое:
• In schema Sch.Sch the table NameNS "tut" "Users" is not defined.
Если не найдено поле или связь, компилятор перечисляет все колонки и внешние ключи таблицы, так что правильное имя обычно видно сразу:
• In schema Sch
for table "tut" ->> "tasks"
name "titel" is not defined.
Valid values are:
Fields: project_id, seq, title, priority, payload, watchers.
Foreign key constraints: tasks_project_id_fkey, task_events_project_id_seq_fkey.
Your source or target type or renamer is probably invalid.
И имя в сообщении, и список допустимых имён — это имена в базе. Имя из вашего
кода сначала проходит через ренеймер (из имени Haskell он делает имя в базе),
и в сообщение попадает уже результат. С titel это незаметно, а вот если
написать "ownerID" вместо "ownerId", ренеймер CamelToSnake превратит его
в owner_i_d, и компилятор сообщит name "owner_i_d" is not defined. Если в
сообщении неожиданное имя, проверьте, во что ренеймер превращает то, что вы
написали. Такое же сообщение бывает и для имён в условиях и сортировке, и для
полей рекордов, и для полей PgTag.
Для имени связи в qPath сообщение показывает и имя в базе (в кавычках), и
то, что было написано в коде (в скобках), а допустимые связи разделяет по
направлению:
• In schema Sch
for table NameNS "tut" "projects"
relation "tasks_project_fkey" ("tasksProjectFkey" after applying the renamer) is not defined.
Valid values are:
Foreign key constraints to parent tables (qPathFromHere or qPath): projects_owner_id_fkey.
Foreign key constraints from child tables (qPathToHere or qPath): project_log_project_id_fkey, tasks_project_id_fkey.
Для связи таблицы с самой собой qPath не годится, и компилятор подскажет,
что использовать вместо него (подробнее — в главе «Select»):
• qPath cannot be used for self-reference relation.
Use qPathFromHere or qPathToHere.
Table: NameNS "tut" "users"
Relation: "users_manager_id_fkey"
Типы полей и NULL¶
Тип поля в Haskell должен соответствовать типу колонки. Если колонка
допускает NULL, поле должно быть Maybe, а если не допускает — не должно:
• You can't use Maybe for mandatory fields
Table: NameNS "tut" "users"
DB Field name: "name"
DB Field type: NameNS "pg_catalog" "text"
Haskell Field type: Maybe Text
• You have to use Maybe for nullable fields
Table: NameNS "tut" "users"
DB Field name: "email"
DB Field type: NameNS "pg_catalog" "text"
Haskell Field type: Text
То же правило действует и для значений в условиях: сравнивать nullable-колонку
нужно со значением типа Maybe, например "managerId" =? Just 1. Сообщение
будет тем же, «You have to use Maybe for nullable fields».
Если же тип Haskell просто не подходит к типу колонки, сообщение менее дружелюбное:
• Could not solve: ‘CanConvert1
Sch
(NameNS "tut" "users")
"email"
(NameNS "pg_catalog" "text")
(TypDef "S" Nothing '[])
Int’
Читать его стоит так: для таблицы tut.users и поля email с типом
pg_catalog.text не нашлось способа преобразования в Int (TypDef "S" … —
это описание типа в базе, S означает строковый тип). Значит, тип поля нужно
поменять на подходящий, здесь — на Text. Какие типы Haskell подходят к
каким типам PostgreSQL, сведено в таблицу в
приложении «Типы данных».
Похожим образом выглядит ошибка, если тип значения в условии не задан,
например "priority" <? 50 без аннотации: числовой литерал может быть любого
типа, и компилятор не знает, какой выбрать. В сообщении вместо типа Haskell
будет переменная вида v0:
• Could not deduce ‘CanConvertMaybe
Sch
(NameNS "tut" "tasks")
"priority"
(NameNS "pg_catalog" "int4")
False
(TypDef "N" Nothing '[])
v0’
arising from a use of ‘<?’
Достаточно указать тип: "priority" <? (50 :: Int32).
Если же для рекорда забыли вывести Generic, сообщение получается длинным и
непонятным, с внутренними именами pg-schema вроде WalkLevelAnn и
GToJSONCols. Узнать его можно по фрагменту GHC.Internal.Generics.Rep Task:
Rep — это представление типа через Generic, и раз оно не вычисляется,
инстанса нет.
Агрегаты¶
Для агрегатов проверяется тип результата SQL-функции. Например, avg по
колонке integer возвращает numeric, и Double к нему не подходит:
• Aggregate 'avg' over a field of type 'int4' returns SQL type 'numeric',
but Haskell type Double is not compatible with it.
Use Scientific or Rational instead.
Table: NameNS "tut" "tasks"
DB Field name: "priority"
DB Field type: NameNS "pg_catalog" "int4"
Haskell Field type: Aggr AAvg (Maybe Double)
Напомним (см. главу «Select»), что Aggr используется в запросах без группировки и
требует Maybe внутри, а Aggr' — в запросах с группировкой. Если Maybe
нужен, он ставится внутрь агрегата (Aggr AMax (Maybe Int32)), а не снаружи
(Maybe (Aggr' AMax Int32)):
• You can't use Maybe for aggregates. Use Maybe inside Aggr'
Table: NameNS "tut" "tasks"
DB Field name: "priority"
DB Field type: NameNS "pg_catalog" "int4"
Haskell Field type: Aggr' AMax Int32
Обязательные колонки и ключи¶
Для вставки во входных данных должны быть все обязательные колонки, то есть
объявленные NOT NULL и не имеющие значения по умолчанию:
• We can't insert data because not all mandatory fields are present.
Table: NameNS "tut" "projects"
Missing mandatory fields: '["title"]
Та же ошибка появится после того, как в таблицу добавят обязательную колонку и
перегенерируют Sch.hs: компилятор покажет все места, где в эту таблицу
вставляются строки. Ту же ошибку выдаст и upsertByKey, если в записи есть
только ключ: строка может оказаться новой, и вставить её будет нечем. Чтобы
изменить строку, указав только ключ, нужен updateByKey. (upsertJSON в такой
ситуации поступает иначе: строку без всех обязательных колонок он не
вставляет, а только изменяет по ключу, поэтому в его результате и появляется
Maybe.)
Изменение строк по ключу — это updateByKey, updateJSON, а также
upsertJSON для строк, в которых есть не все обязательные колонки. Всем им
нужен полный первичный или уникальный ключ. Если его нет, компилятор перечислит возможные ключи:
• We can't upsert data because for table NameNS "tut" "tasks"
not all mandatory fields are present and no full primary or
unique key is covered by record fields.
Missing mandatory fields: ["seq", "title"]
Possible keys: '[["project_id", "seq"]]
Сообщение общее для нескольких операций, поэтому в нём говорится об upsert и
об обязательных колонках, даже если вы вызывали updateByKey или
updateJSON. Для них важна последняя строка: один из перечисленных ключей
должен целиком оказаться в типе. В операциях со связанными таблицами колонки,
которые приходят от родителя (здесь project_id), в типе указывать не нужно.
Форма результата операций со связанными таблицами¶
В upsertJSON и updateJSON строка, которая изменяется по ключу, может не
найтись, и тогда на её месте в результате будет Nothing. Поэтому тип такой
строки в результате должен быть Maybe. Слова «update or key-only upsert» в
сообщении означают как раз это: updateJSON или строку upsertJSON, в
которой есть только ключ. Для дочерних строк компилятор скажет об этом так:
• Returning must use Maybe here (update or key-only upsert).
At path: '[]
Table: NameNS "tut" "projects"
Field: "tasksProjectIdFkey"
Got: ("seq" := Int32) :. ("priority" := Int32)
А для корня upsertJSON, в котором нет всех обязательных колонок, сообщение
объясняет причину подробнее:
• Upsert returning row must be wrapped in Maybe:
the input row lacks mandatory fields ["owner_id", "title"]
so it is only updated by key and a row that is not found is returned as null.
Table: NameNS "tut" "projects"
Got: ("id" := Int64)
:. ("tasksProjectIdFkey"
:= [("seq" := Int32) :. ("title" := Text)])
В первом случае исправленный тип выглядит так:
"tasksProjectIdFkey" := [Maybe ("seq" := Int32 :. "priority" := Int32)],
то есть Maybe оборачивает каждый элемент списка, а не весь список. Корень
результата updateJSON тоже должен быть Maybe. А вот у строки, в которой
есть все обязательные колонки, Maybe запрещён: такая строка вставляется или
изменяется через insert … on conflict и есть в результате всегда. Все эти
правила сведены в таблицу в справочнике.
Структура запроса¶
qLimit, qOffset и qDistinct разрешены только в корне запроса и в
qPath к дочерним строкам. В qPath к родителю они не имеют смысла: родитель
у строки один, и ограничивать нечего. Сообщение об этом, к сожалению, не самое
понятное:
• Couldn't match type ‘pg-schema-0.8.1.1:PgSchema.Schema.RelOne’
with ‘pg-schema-0.8.1.1:PgSchema.Schema.RelMany’
arising from a use of ‘qLimit’
Если вы видите RelOne и RelMany, проверьте, не стоит ли qLimit,
qOffset или qDistinct внутри qPath к родителю.
Вспомогательные функции¶
named из главы «Изменение данных в одной таблице» приписывает список имён полей значениям каждой строки. Он
проверяет, что имён столько же, сколько значений в строке. Если имён меньше,
последнему имени достались бы сразу несколько значений:
• named: there are fewer field names than fields in the row.
The last name "status" is given to more than one field: PGEnum
Sch ("tut" ->> "project_status")
:. PgArr Text
UnsafeCol из главы «Select» описывает поле результата произвольным SQL-выражением.
Он проверяет, что знаков ? в выражении столько же, сколько колонок в списке.
Если колонок больше, сообщение такое (а если больше знаков ? — аналогичное,
с обратной формулировкой):
• Count of field names in the list of fields is more than count of question marks in expression.
Резюме¶
- Первый пункт сообщения — это сообщение
pg-schema, остальные показывают место в коде. - Имя в сообщении — имя в базе, то есть после ренеймера.
- Одна ошибка в типе даёт по сообщению на каждый вызов, а исправляется один раз.
- Для
NULL-колонок нуженMaybeи в типе поля, и в значении условия. - Сообщение о ключах читается по строке
Possible keys. RelOne/RelManyрядом сqLimit,qOffsetилиqDistinctозначает их в ветке к родителю.
Что дальше¶
Эта глава пригодится как справочник по сообщениям компилятора. О том, как
жить с pg-schema в настоящем проекте, где схема базы меняется, рассказывает
глава «На практике», а правила для типов результата и
ограничения собраны в справочнике.