>> let result = query_builder()
>> .select(["uid", "nickname", field_name])
>> .from(USERS_TABLE)
>> .where(less(length("nickname"), 5))
>> .execute(SQL_CONNECTION);
> Ну с селектом-то всё просто. Если уж показывать, то джойны, агрегаты, вложенные
> запросы. =) Мне сложно навскидку предложить решение для php. Там нужны ссылки на поля из разных таблиц, но как это представить в синтаксисе php, чтобы это было бы не совсем уродливо? В php есть tuple? Можно ли написать, что-то типа:
select([("table1", "id"), ("table2", "name")]), имея ввиду "SELECT table1.id, table2.name"?
Всё равно уродливо выходит?
А, или это не надо? Если наш escape_string, не будет искейпить точки из имени, и оставлять table1.id неизменным -- это будет дырой для злоумышленной веб-макаки, которая намеренно пишет код, который содержит sql-injection, или нет?
Вложенные запросы реализуются элементарно, просто надо научить тип QueryBuilder принимать Query в качестве аргумента. Немного ООП, и QueryBuilder'у даже не надо будет знать, что он получил Query в качестве аргумента from, а не EscapedSqlString.
Да блин, хочешь я тебе сейчас прямо здесь напидаю простейшую реализацию? Я не буду парится на тему того, чтобы писать на пхп -- я его не помню, я псевдокодом.
Смотри.
Во-первых, метод для наших объектов. Если встроенным типам php возможно этот метод добавить, то надо его добавить им, и тогда все встроенные типы должны преобразовываться к String, после чего полностью экранироваться.
method sql_escape(self) -> EscapedString;
EscapedString тоже должна иметь этот метод:
function EscapedString::sql_escape(self) -> EscapedString {
return self;
}
Затем мы пишем функцию:
function my_sql_escape(obj) -> EscapedString {
if obj.has_method("sql_escape") {
return obj.sql_escape();
} elseif obj.has_method("to_string") /* or maybe object is a string */{
// я подразумеваю здесь, что метод from реально экранирует всё
return EscapedString::from(obj.to_string());
} else {
// может быть нам надо ещё какие-то типы обработать, скажем int или float, но мне лень
panic("I'm a stupid programmer, who cannot into program");
}
}
И дальше пишется
class Query {
query: String,
}
пачка функций вида:
function Query::select(self, obj) -> Query {
// только эти функции будут писать в self.query нативные php-строки,
// это ограниченный объём кода, и его возможно проверить вручную глазами, чтобы тут
// не писалось бы ничего глупого в запрос
self.query.append("SELECT ");
self.query.append(my_sql_escape(obj).to_string());
self.query.append(" ");
return self;
}
Чтобы Query можно было бы втыкать подзапросом:
function Query::sql_escape(self) -> EscapedString {
// тут мы вернём EscapedString, которая будет содержать реально non-escaped string
return EscapedString::from_string_unchecked(self.query);
}
С функциями типа less, or, and и тп, надо подумать как сделать. Надо ли тут создавать тип NaryFunCall, который будет хранить имя функции ("<", ">=", "or", "and"), аргументы и флаг infix_notation, чтобы все эти less/or/and сделать конструкторами этого типа? Или может надо сделать как-то ещё?
Но, в общем, это ТУПЕЙШАЯ реализация, и она даже получилась лучше, чем я ждал от такой реализации. Строку Query::query придётся перевыделять постоянно в процессе конкатенаций, но это вероятно можно обойти, предвыделив килобайт памяти под строку, и может быть даже хранить такую строку от запроса к запросу, чтобы не выделять её каждый раз снова, а потом не удалять.
Эта ТУПЕЙШАЯ реализация не проверяет SQL синтаксис, и позволяет генерить невалидные запросы, скажем, "WHERE hello FROM world". Более глубокая проверка потребует кучи дополнительных усилий. Но sql-injection здесь либо вырезана уже, либо её возможно вырезать. Опять же утверждать не возьмусь, поскольку язык для этого псевдокода я придумывал по ходу дела.
В ней есть ещё косяк, связанный с тем, что она легко может позволять ситуацию, когда одно и то же значение имеет несколько mutable ссылок. Может я зря беспокоюсь, а может и нет.
Ещё она создаёт лишние и ненужные копии строк. Вероятно это можно победить, как-нибудь, если прокидывать везде строку Query::query, в которую надо дописывать значения, вместо того, чтобы создавать их в куче и возвращать, чтобы они тут же были сконкатенированы с Query::query и выброшены в мусор.