Форматы вывода (CSV, JSON, SQL…)
В TDC нет фиксированного списка экспортёров, и это осознанно.
Блок вывода собирает текст:
<before> / <after>
обрамляют всю сборку, <block> — одна запись, а
<data> — сырой текст. Поэтому один и тот же движок
выдаёт CSV, JSON, SQL, YAML, NDJSON и что угодно ещё, собранное из символов.
Плата за эту свободу в том, что правила синтаксиса формата держите вы. TDC не знает, что
вы собираете JSON, и не поставит за вас ни запятую, ни кавычку, ни null — но и не поставит
их там, где не надо. Этот гайд проходит три самых частых формата и ровно те места, где каждый
из них ломается, а затем показывает, как собрать любую другую форму.
Выводы ниже иллюстративны — конкретные значения зависят от версии ядра и сида. Важна форма
каждого формата, а не имена и числа. Проверяйте готовый файл настоящим парсером
(python -m json.tool, sqlite3, любой CSV-ридер), а не глазами: каждая поломка ниже при
беглом просмотре выглядит нормально.
CSV
CSV — самая дружелюбная цель: заголовок один раз, дальше по строке на запись. Кусаются две вещи — место заголовка и любое значение, где встречается разделитель или кавычка.
Заголовок — один раз, через <before>
Заголовку место в <before>: он печатается ровно
один раз, над всеми записями, и неважно, count у вас 3 или 3 миллиона.
<tdc>
<env count="3" seed="demo">
<before><line><data>id,name,category</data></line></before>
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="Name"><gen type="text" value="Ручка,Кружка,Блокнот"/></sequence>
<sequence name="Cat"><gen type="text" value="Офис,Кухня,Офис"/></sequence>
</env>
<block>
<line><data>${{Id}},${{Name}},${{Cat}}</data></line>
</block>
</tdc>
id,name,category 1,Блокнот,Офис 2,Кружка,Офис 3,Ручка,Кухня
Заголовок в <block> повторился бы на каждой строке; в <before> он печатается один раз.
Так же используйте <after> для итоговой строки в конце или закрывающей метки.
csv — значение с запятой или кавычкой
Задача. Название товара вроде Набор ножей, 3 шт содержит разделитель полей. Записанная
как есть, эта запятая становится разрывом колонки: у строки появляется лишнее поле,
категории съезжают в цены, и — хуже всего — файл всё равно открывается без ошибки. Ущерб
вылезает только когда файл читает программа.
<block>
<line><data>${{Id}},${{Name}},${{Cat}},${{Price}}</data></line>
</block>
# глазами всё нормально: 7,"Набор ножей, 3 шт",Посуда,3200 # что на самом деле видит CSV-ридер — 5 полей вместо 4: 7 | Набор ножей | 3 шт | Посуда | 3200
Инструмент. Фильтр csv берёт поле в кавычки и удваивает
внутренние — ровно как требует RFC 4180. Кавычит он каждое поле, на которое
поставлен, а не только те, что иначе развалились бы: закавыченное поле всегда валидный
CSV, и читатель разницы не заметит. Ставьте его на каждое текстовое
поле, пришедшее извне вашего конфига:
<block>
<line><data>${{Id}},${{Name | csv}},${{Cat}},${{Price}}</data></line>
</block>
id,name,category,price 7,"Набор ножей, 3 шт",Посуда,3200 8,"Кофе ""Арабика"" 250 г",Бакалея,540 9,"Кружка",Кухня,410
Запятые и кавычки теперь надёжно живут внутри полей в кавычках; ридер получает четыре колонки в каждой строке. Применяйте, когда значение может нести запятую, кавычку или перенос строки — а это почти всегда так у имён, названий и адресов, вытянутых из настоящего файла данных.
Другой разделитель
Разделитель — это просто символ, который вы печатаете. Нужны точки с запятой (частый выбор,
когда значение и так содержит запятые) или табы? Поменяйте текст в <data> и заголовок под
него — переключать никакой режим не надо.
<env count="3" seed="demo">
<before><line><data>id;name;category</data></line></before>
<!-- последовательности Id / Name / Cat, без изменений -->
</env>
<block><line><data>${{Id}};${{Name}};${{Cat}}</data></line></block>
id;name;category 1;Ручка;Офис 2;Кружка;Кухня
(Атрибут delimiter — это про другое: он говорит генератору
file, как читать входной CSV. На выходе вы просто пишете нужный
символ.)
JSON
У JSON в TDC тоже нет «режима» — это текст, как и CSV. Но у него четыре правила синтаксиса, которые простой сборщик текста радостно нарушит. Вот все четыре и лекарство для каждого.
Массив без висящей запятой
Задача. Три объекта — ещё не JSON, пока что-то не соединит их в массив. Напрашивающийся ход — запятая в конце каждого объекта — ставит запятую и после последнего, а её не прощает ни один парсер:
"age": 21 }, ] JSONDecodeError: Expecting value: line 17 column 1
Два чистых решения.
Первое — условный кусок. Второй <data if="!_last">
печатает запятую в каждой записи, кроме последней (_last — встроенный флаг «последняя
запись»), и JSON закрывается корректно:
<tdc>
<env count="3" seed="demo" local="ru">
<before><line><data>[</data></line></before>
<after><line><data>]</data></line></after>
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="Name"><gen type="template" value="person.male.firstName"/></sequence>
</env>
<block>
<line><data> {"id": ${{Id}}, "name": "${{Name}}"}</data><data if="!_last">,</data></line>
</block>
</tdc>
[
{"id": 1, "name": "Владимир"},
{"id": 2, "name": "Сергей"},
{"id": 3, "name": "Александр"}
]Второе — отдельная фикстура. <delimiter_block>
печатает свой текст между записями — count - 1 раз, никогда после последней, — а это ровно
и есть «запятая между элементами массива»:
<env count="3" seed="demo">
<before><line><data>[</data></line></before>
<delimiter_block><line><data>,</data></line></delimiter_block>
<after><line><data>]</data></line></after>
...
</env>
Берите if="!_last", когда запятая едет на той же строке, что и объект; тянитесь к
<delimiter_block>, когда запись занимает несколько строк и разделителю нужна своя. Запятая
одна на строке выглядит непривычно, но это валидный JSON — переносы строк между элементами
массива стандарту безразличны. Хотите красиво — пропустите файл через jq ..
Кавычки ставите вы
Обратите внимание на кавычки выше: у "${{Name}}" они есть, у ${{Id}} — нет. TDC про типы
JSON ничего не знает — кавычки ставите вы, и именно так число остаётся числом, а строка строкой.
<line><data> "id": ${{Id}}, "zip": "${{Zip}}", "age": ${{Age}}</data></line>
"id": 1, "zip": "101000", "age": 34
zip в кавычках намеренно: почтовый индекс — не число (арифметику с ним не делают, а ведущий
ноль пропал бы). id и age остаются без кавычек, чтобы разбираться как целые.
Вложенность — это просто ещё строки
Вложенному объекту не нужен особый синтаксис — это ещё несколько строк <data> с нужными
скобками и отступами:
<line><data> "address": {</data></line>
<line><data> "city": "${{City}}",</data></line>
<line><data> "zip": "${{Zip}}"</data></line>
<line><data> },</data></line>
"address": {
"city": "Москва",
"zip": "101000"
},Массив внутри объекта
Несколько значений в одном поле даёт repeat, а
separator — то, что встанет между ними. JSON хочет ", " —
с кавычками, — и тут вы упираетесь в стену: грамматика не даёт положить " внутрь значения
атрибута, а " проходит буквально.
Обход: разделить нейтральным символом, которого в значениях никогда нет, а потом превратить его
в JSON фильтром replace, где кавычки писать можно:
<sequence name="Tags">
<gen type="text" value="sale,new,gift,vip" repeat="1..3" separator="~"/>
</sequence>
<line><data> "tags": ["${{Tags | replace:~,", "}}"],</data></line>
"tags": ["vip"], "tags": ["sale", "gift"], "tags": ["gift", "vip", "new"]
Для массива чисел ничего этого не нужно — кавычек там нет, хватает separator=","
напрямую. И учтите: repeat берёт каждое значение независимо,
так что один тег может выпасть в строке дважды; если так не нужно, задайте фиксированный набор
через <mix>.
null — это слово, а не пустота
Задача. Для необязательного поля напрашивается missing,
но missing даёт пустое значение, а в JSON пустоты не существует:
<gen type="number" value="1..100" missing="0.5"/>
{"id": 1, "score": }
JSONDecodeError: Expecting value: line 1 column 20(В CSV та же пустота как раз правильна — пустая ячейка означает «нет значения». Форматы разные.)
Инструмент. Напишите слово null явно, как одну из веток <mix>,
которая заодно делает долю точной — на 50 000 строк вы получите ровно 10 000 null, а не
«примерно 20%»:
<mix name="Score" percent="80,20">
<case><gen type="number" value="1..100"/></case>
<case><data>null</data></case>
</mix>
{"id": 1, "score": 56}
{"id": 2, "score": null}
{"id": 3, "score": 12}Кавычка из чужих данных
Последняя поломка приходит из значений, которые вы не контролируете, — из имени, вытянутого из
файла. Товар Кофе "Арабика" 250 г попадает в строку как есть, и вторая
кавычка закрывает её раньше времени:
{"name": "Кофе "Арабика" 250 г"}
JSONDecodeError: Expecting ',' delimiterПо стандарту JSON внутреннюю кавычку экранируют обратным слэшем. Тот же фильтр
replace это и делает — это единственное место, где слэш пишете вы сами:
<data>{"name": "${{Name | replace:",\"}}"}</data>
{"name": "Кофе \"Арабика\" 250 г"}Это разбирается обратно в исходную строку, кавычки на месте.
Если в ваших данных может встретиться и сам обратный слэш, удвойте его первым (проход
replace для \ → \\), и только потом экранируйте кавычки — иначе второй проход испортит
экранирование, добавленное первым.
Когда записей много: NDJSON
У массива есть потолок: чтобы прочитать один объект, парсер обязан загрузить весь файл, так
что миллион записей — это гигабайты в памяти. Отраслевой ответ — NDJSON: один объект на
строку, без обёртки и без запятых. Его читают pandas, ClickHouse, BigQuery и jq.
Достаточно убрать три фикстуры и уложить каждую запись в одну строку:
<block><line>
<data>{"id": ${{Id}}, "name": "${{Name}}", "city": "${{City}}", "score": ${{Score}}}</data>
</line></block>
{"id": 1, "name": "Пётр", "city": "Москва", "score": 3}
{"id": 2, "name": "Дмитрий", "city": "Париж", "score": null}
{"id": 3, "name": "Андрей", "city": "Москва", "score": 12}Потребитель читает файл по строке за раз, и памяти нужно на одну запись, а не на весь файл — это формат выбора начиная с нескольких сотен тысяч строк. Как TDC пишет такое на диск, не держа файл в памяти, — см. Большие выгрузки и потоковая запись.
SQL
Для SQL вы выдаёте команды, которые база выполняет напрямую — TDC ни к чему не подключается, он
пишет файл команд, который вы заливаете через sqlite3, psql или миграцию. Две заботы о
формате: экранировать апострофы и обернуть сборку в схему и транзакцию.
sql — апостроф в значении
Задача. В SQL строковый литерал ограничен одинарными кавычками, так что фамилия вроде
O'Brien закрывает строку раньше времени, и команда не выполнится:
INSERT INTO users (id, last) VALUES (2, 'O'Brien'); Error: near "Brien": syntax error
Инструмент. Фильтр sql удваивает апостроф — так SQL экранирует его
внутри литерала:
<tdc>
<env count="3" seed="demo">
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="Last"><gen type="text" value="Chisholm,O'Brien,Foster" order="sequential"/></sequence>
</env>
<block>
<line><data>INSERT INTO users (id, last) VALUES (${{Id}}, '${{Last | sql}}');</data></line>
</block>
</tdc>
INSERT INTO users (id, last) VALUES (1, 'Chisholm'); INSERT INTO users (id, last) VALUES (2, 'O''Brien'); INSERT INTO users (id, last) VALUES (3, 'Foster');
'O''Brien' загружается обратно как O'Brien. В штатных пакетах данных апострофов не встречается —
поэтому пример задаёт свой список; но стоит подключить свой набор имён — и они появятся
(ирландские и итальянские фамилии ими полны), — поэтому ставьте sql на каждое текстовое поле,
идущее в команду. Стоит это ничего, а спасает от упавшей заливки позже.
Схема сверху, данные снизу — <before> / <after>
CREATE TABLE выполняется один раз, поэтому ему место в
<before>, который печатается впереди всех строк:
<env count="3" seed="shop" local="ru">
<before>
<line><data>CREATE TABLE customers (</data></line>
<line><data> id INTEGER PRIMARY KEY,</data></line>
<line><data> name TEXT NOT NULL,</data></line>
<line><data> city TEXT NOT NULL</data></line>
<line><data>);</data></line>
</before>
<sequence name="Id"><gen type="increment" value="1"/></sequence>
<sequence name="First"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="Last"><gen type="template" value="person.male.lastName"/></sequence>
<sequence name="City"><gen type="text" value="Москва,Париж,Берлин" percent="50,30,20"/></sequence>
</env>
<block><line>
<data>INSERT INTO customers VALUES (${{Id}}, '${{First}} ${{Last | sql}}', '${{City}}');</data>
</line></block>
CREATE TABLE customers ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT NOT NULL ); INSERT INTO customers VALUES (1, 'Захар Петров', 'Берлин'); INSERT INTO customers VALUES (2, 'Демьян Вишневский', 'Париж'); INSERT INTO customers VALUES (3, 'Денис Фролов', 'Москва');
Обернуть в одну транзакцию
Тысяча голых INSERT — это тысяча транзакций, а значит медленно. Оберните сборку в
BEGIN / COMMIT, чтобы она загрузилась как одна, добавив по строке в каждую фикстуру:
<env count="3" seed="demo">
<before>… схема …<line><data>BEGIN;</data></line></before>
<after><line><data>COMMIT;</data></line></after>
<!-- последовательности, без изменений -->
</env>
Затем заливаете:
# SQLite sqlite3 shop.db < shop.sql # PostgreSQL — тот же файл psql -f shop.sql
Для связанных таблиц — заказов, которые ссылаются на существующих клиентов, с всегда валидным
внешним ключом — генерируйте по строке на элемент списка через
each; полный приём — в
Связные и реляционные данные.
Что угодно ещё
Те же три части собирают любую текстовую форму — вы управляете каждым символом:
- YAML —
-перед каждым элементом списка, отступ в два пробела для вложенности. - Таблица Markdown — заголовок с разделителем
|плюс строка-правило|---|в<before>, строки с|в<block>. - Отчёт фиксированной ширины — дополняйте поля до заданной ширины фильтрами
mask/ срезами и выравнивайте колонки.
<env count="3" seed="demo">
<before>
<line><data>| id | name | city |</data></line>
<line><data>|----|------|--------|</data></line>
</before>
<!-- последовательности, без изменений -->
</env>
<block><line><data>| ${{Id}} | ${{Name}} | ${{City}} |</data></line></block>
| id | name | city | |----|------|--------| | 1 | Иван | Москва | | 2 | Пётр | Казань |
TDC лишь подставляет значения — формат таков, каким вы его написали.
Смотрите также
- Вывод и форматирование —
<block>,<line>,<data>, фикстуры и условиеif. - Маски и регистр — фильтры экранирования
csv/sql/replaceцеликом. - Чтение файлов и CSV — тяните свои значения из файла генератором
file. - Связные и реляционные данные — связанные таблицы, внешние ключи и
атрибуты
each/weight/row. - Большие выгрузки и потоковая запись — миллионы строк и NDJSON на диске.