Возвращаясь к теме Query Builder'ов

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Обход по дереву - задача, про которую много пишут, но в реальности мне за 15 лет она встречалась раз или два. Даже в ecommerce с каталогом в виде дерева работать не приходится - или выводишь все категории списком, или работаешь с одной.
Извиняюсь, не вполне аккуратно сформулировал: речь об обходе по этому самому AST. Внутри пакета это делают классы для обработки параметров и для построения SQL, в документации нарисовано, как можно получить, например, список всех таблиц, участвующих в запросе: https://github.com/sad-spirit/pg-builder/wiki/walkers

@Sad Spirit, прошло несколько лет, расскажи об опыте использования и юзкейсах на практике.

Я к тому, что штука клевая и проделана огромная работа, но я вот почти не вижу кейсов, когда классического write only билдера было бы недостаточно. Ну, то есть, я могу их специально придумать, но на практике не припоминаю.
Ну я врать не буду, большую часть времени он и используется как обычный. :-D Правда в плюс к этому обычному идёт автоматическая проверка синтаксиса.

Что не очень получится сделать с обычным:
  • Замена именованных параметров на нумерованные + получение типов параметров непосредственно из запроса. Первое теоретически делает и PDO, но там изрядно много "но". Второе для Postgres'а очень удобно для написания условий вида foo = any:)foo::integer[]), где параметр передаётся в виде массива.
  • Стартуем с текстового представления большого запроса, потом добавляем к нему кусочки в тот же WHERE.
  • При добавлении куска запроса проверяем, имеет ли запрос вид "SELECT count(*)"
  • Можно проверить, где конкретная таблица во FROM и сделать с ней JOIN. Или просто не добавлять её второй раз, если уже есть.

Ну и парсер отдельно можно использовать, например, для разбора определений VIEW --- они лежат как раз в виде запросов в системных каталогах.

Возможен, кстати, и подход, где товарищи решили не париться и для парсинга запросов Postgres'а подтащить сам парсер Postgres'а: https://github.com/lfittl/libpg_query Но тут проблема в том, что получается парсить запросы целиком, а куски уже не очень.

Ну и щас я смотрю, даже обычные Query Builder'ы дрейфуют в сторону чего-то похожего на AST, тот же Zend\Db\Sql
 

fixxxer

К.О.
Партнер клуба
Что не очень получится сделать с обычным:
Это-то я понимаю. Вопрос не в том, "что", а в том, "на хрена". :)

Я могу себе представить какой-нибудь генератор отчетов, где удобно стартануть с определенного запроса, а дальше его мутировать, да. Других кейсов вообще в голову не приходит.

UPD: а, ну вот еще подумал, что было бы удобно сделать генератор кода для какого-нибудь ларавеловского квери-билдера из SQL-запроса. Бывает, что сидишь, отлаживаешь SQL-запрос, а потом мучительно его переписываешь на квери-билдер; при этом логика рантайм "мутаций" укладывается в append-only и AST там не сдался, но вот это переписывание ручками - оно бесит.
 
Последнее редактирование:

Фанат

oncle terrible
Команда форума
Я могу себе представить какой-нибудь генератор отчетов, где удобно стартануть с определенного запроса, а дальше его мутировать, да. Других кейсов вообще в голову не приходит.
Ну вот классика - пагинацыя.
Начать с запроса на count, а потом мутировать в запрос с лимитом?
 

fixxxer

К.О.
Партнер клуба
Возможен, кстати, и подход, где товарищи решили не париться и для парсинга запросов Postgres'а подтащить сам парсер Postgres'а: https://github.com/lfittl/libpg_query Но тут проблема в том, что получается парсить запросы целиком, а куски уже не очень.
Тут сразу приходит в голову костыль вида "дополнить до полноценного запроса и вытащить поддерево". :)
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
UPD: а, ну вот еще подумал, что было бы удобно сделать генератор кода для какого-нибудь ларавеловского квери-билдера из SQL-запроса. Бывает, что сидишь, отлаживаешь SQL-запрос, а потом мучительно его переписываешь на квери-билдер; при этом логика рантайм "мутаций" укладывается в append-only и AST там не сдался, но вот это переписывание ручками - оно бесит.
AST как раз удобен и для append-only, прямо из readme же:
PHP:
/* @var $query Select */
$query      = $factory->createFromString(
    'select n.* from news as n order by news_added desc limit 5'
);
// we also need pictures for these...
$query->list[] = 'p.*';
$query->from[0]->leftJoin('pictures as p')->on = 'n.picture_id = p.picture_id';
// ...and need to limit them to only specific rubrics
$query->from[] = 'objects_rubrics as ro';
$query->where->and_('ro.rubric_id = any(:rubric::integer[]) and ro.obj_id = n.news_id');
Именно вот этот use case, когда у нас есть написанный руками и отлаженный запрос, а мы его хотим потом ещё слегка доработать, и решается связкой parser + builder.

Начать с запроса на count, а потом мутировать в запрос с лимитом?
Да-да, я выше как раз и пишу. У нас есть запрос, который может быть как count(*), так и на выборку. Метод, который добавляет в него кусок, смотрит: ага, у нас count(*), поля в $select->list добавлять не будем, а вот критерии в $select->where вполне себе.
 

fixxxer

К.О.
Партнер клуба
@Sad Spirit, ну так это и обычным квери-билдером типа доктриновского или ларавеловского вполне себе удобно, разница чисто эстетическая. (С отлаженным запросом понятно, но тут опять же вопрос, а чего бы этим парсером не сгенерить один раз код для того же доктриновского билдера).

В личный проект я бы втащил (потому что такой подход мне больше нравится). Но пока что не вижу кейсов, которыми мог бы кого-то убедить.
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Но пока что не вижу кейсов, которыми мог бы кого-то убедить.
Ну сейчас моя задача была прокукарекать проанонсировать, в т.ч. в pgsql-announce, а там хоть не рассветай.

Но на будущее я тебя понял, посмотрю ещё раз наиболее популярные из Query Builder'ов и добавлю в доки сравнение.
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
(С отлаженным запросом понятно, но тут опять же вопрос, а чего бы этим парсером не сгенерить один раз код для того же доктриновского билдера).
Ну и кстати флаг в руки, пиши свой LaravelBuilderWalker, генери по AST PHP'шный код.
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Если не ограничиваться своими проектами, то для начала мне придется сделать форк и добавить грамматику mysql, хехе.
Вот кстати, кому не лениво (мне лениво), попробуйте погонять моё поделие с запросами MySQL. Есессно при использовании спецфич MySQL типа обратных кавычек для идентификаторов оно подавится ещё на этапе Lexer'а, но кое-что может дойти до Parser'а. На этапе генерации SQL'я жопа будет гарантированно.
 

Фанат

oncle terrible
Команда форума
Красивый отзыв
This is extremely cool. It claims to be a "partial" reimplementation of the grammar (which I usually read as "Coming soon in version 2 - support for joins!") but this correctly handled a query with a window function, correlated subquery, lateral left join and locking clause.
Solid work.
 

fixxxer

К.О.
Партнер клуба
Analyze and transform the query
Вот тут, кстати, прямо напрашивается какой-то высокоуровневый способ сопоставления двух AST. :)
Типа
PHP:
equals($select->list, $factory->select('count(*)')->list);
 

Sad Spirit

мизантроп (Старожил PHPClub)
Команда форума
Красивый отзыв
А это откуда? А то пассивные гомосексуалисты из Гугля убили оператор "link:" в поиске, так что понять, где народ обсуждает нереально. При этом пока я неделю был в отпуске, пакет таки набрал звёздочек на github'е, значит где-то обсуждают.

Вот тут, кстати, прямо напрашивается какой-то высокоуровневый способ сопоставления двух AST. :)
Типа
PHP:
equals($select->list, $factory->select('count(*)')->list);
Ну это можно щас через SqlBuilderWalker, он для части выражения же сработает.
PHP:
if ('count(*)' === implode(', ', $select->list->dispatch(new SqlBuilderWalker()))) {
    // ...
}
Вот что тут точно напрашивается, это приведение строки в конструкторе FunctionExpression к QualifiedName, проверок в два раза меньше будет. Но вот хоть убей не помню, почему я решил там оставлять строку?
 
Сверху