Сразу оговорюсь: я из команды LibreDB Studio, и статья — про наши собственные грабли. LibreDB Studio — это self‑hosted IDE для СУБД, работающая в браузере: один инстанс разворачивается рядом с базами, а не устанавливается на ноутбук каждого разработчика. Команда открывает URL — и почти сразу упирается в вопрос, который сожрал у нас не один спринт: как показать результат из четырнадцати принципиально разных движков в одной таблице и при этом не начать выдавать пользователю выдумки за реальные данные.
Движков ровно 14. Классические реляционные — PostgreSQL, MySQL, Oracle, SQL Server, SQLite; документная MongoDB; ключ‑значение Redis; Couchbase; аналитические колоночные ClickHouse и Druid; поисковые Elasticsearch и OpenSearch; wide‑column Cassandra; и, наконец, Trino, который вообще не хранилище, а движок распределённых запросов. За счёт совместимости на уровне wire‑протокола часть этих драйверов — прежде всего MySQL‑, PostgreSQL‑, Redis‑, MongoDB‑ и Cassandra‑совместимые — дотягивается и до трёх с лишним десятков систем, известных под другими названиями: MariaDB, TiDB, CockroachDB, Valkey, FerretDB, ScyllaDB и так далее. SQLite, к примеру, встроенный и родственников по протоколу не имеет. И это именно «дотягивается», а не «полностью поддерживает» — об этом будет отдельный разговор ниже. Звучит как маркетинговый слайд, но за этой строчкой прячется куча неудобных инженерных решений.
Соблазн общего знаменателя
Первое, что хочется сделать, когда сводишь разные источники в одну таблицу, — привести всё к наименьшему общему знаменателю. Есть строки, есть колонки, рисуем таблицу, поехали. Проблема в том, что «строка» и «колонка» — это уже допущение реляционного мира. Документ в MongoDB — это дерево. Ответ Redis по ключу может быть строкой, списком, хэшем, множеством, отсортированным множеством или потоком (stream), не считая надстроек вроде bitmap, HyperLogLog и geo. Ответ Elasticsearch — это JSON с агрегациями, где данных в привычном табличном смысле может и не быть вовсе.
От идеи «одна модель данных на всех» мы отказались быстро. Вместо неё у нас есть контракт результата, который каждый драйвер заполняет по факту, в том числе полями «я этого не умею» и «я не знаю точно». Проще говоря, драйвер возвращает не только данные, но и карту своих возможностей (capability map) — примерно такой формы:
rowCountExact: bool // точное ли число строкeditable: bool // можно ли править строкиddl: { insert, update, delete, create }primaryKey: supported | not_applicableindexes: supported | not_applicablenotes: string
Cassandra‑драйвер выставит rowCountExact: false, Trino‑драйвер — primaryKey: not_applicable и indexes: not_applicable, а Druid, ES и OpenSearch сообщат, что правки отдельной строки нет. И здесь важна детализация: вместо булева «DDL: нет» у Druid отдельным флагом стоит, что INSERT/REPLACE через MSQ на уровне datasource есть, а UPDATE/DELETE строки — нет. UI рисует ровно то, что драйвер про себя сообщил, и не пытается додумывать за движок. Дальше — четыре истории про то, где эта дисциплина спасала нас от красивого вранья.
Druid, ES и OpenSearch, которые умеют только читать
Druid, Elasticsearch и OpenSearch — аналитические системы. В их диалектах SQL нет UPDATE, DELETE и привычного CREATE TABLE в том виде, к которому привык человек с PostgreSQL за спиной (у Druid, справедливости ради, есть INSERT/REPLACE через MSQ, но это про ingestion, а не про правку строки в таблице). Долгое время был соблазн подсунуть им эмуляцию: пользователь нажал «изменить ячейку», а мы под капотом что‑нибудь придумаем. Такая эмуляция рано или поздно взорвалась бы на проде у клиента, причём молча.
Поэтому решение скучное: если движок не поддерживает операцию, интерфейс так и пишет — «не поддерживается», и кнопка редактирования просто не появляется. Не серая, не с фейковым спиннером, который потом ничего не сделает. Её нет. Пользователь видит объектный браузер, видит данные, но с самого начала понимает, что здесь read‑only по природе движка, а не потому что ему не хватило прав.
Cassandra, которая не знает, сколько у неё строк
В реляционном мире SELECT count() — рефлекс. В Cassandra этого рефлекса лучше не иметь. Точного числа строк и размера таблицы там просто нет в готовом виде: данные размазаны по партициям, часть лежит в memtable, часть уже сброшена на диск в SSTable, сверху — tombstone’ы. Полный count() по большой таблице — это фактически скан всего кластера, которым легко положить прод.
Мы попробовали оценивать объём по метаданным SSTable и по system.size_estimates. Нюанс в том, что Cassandra считает партиции и их средний размер, а не строки как таковые. На тестовой таблице из 500 строк оценка выдала 143 — часть данных ещё жила в памяти и в оценку не попала. Развилка была такая: показать бодрое «≈143» или не показывать ничего.
Мы выбрали не показывать. В объектном браузере для Cassandra счётчик строк отсутствует, вместо него — пометка, что движок не публикует точное число. Число, которое ошибается в три с половиной раза, хуже отсутствия числа: инженер, который увидит «143» и построит на этом решение, потом придёт с претензией — и будет прав.
Trino, который вообще не база данных
Trino выпадает из общего ряда: это не хранилище, а движок распределённых запросов поверх чужих коннекторов. Своих таблиц у него нет, первичных ключей нет, индексов нет, потому что байты физически принадлежат тем системам, что стоят за коннекторами: S3, HDFS, Kafka, той же PostgreSQL. Hive‑коннектор, к слову, — это вообще про метастор и таблицы поверх объектного хранилища, а не про собственные данные Trino. А наш объектный браузер по привычке хотел показать «ключи» и «индексы» для каждой таблицы.
Пустая панель воспринимается как «данных нет» — а это вводит в заблуждение. Поэтому для Trino панели «Primary Key» и «Indexes» не пустые, а помеченные как неприменимые, с коротким объяснением почему. Разница между «здесь нет ключей» и «этого понятия тут не существует» для инженера огромна.
Wire‑совместимость — это обещание, а не факт
Самое коварное — движки, совместимые по протоколу. SingleStore говорит по протоколу MySQL, ScyllaDB — по протоколу Cassandra, FerretDB прикидывается MongoDB. Соблазн понятен: раз протокол тот же, наш MySQL‑драйвер «просто заработает». На практике совместимость протокола не означает совместимости поведения. Какой‑то системный запрос к information_schema возвращает не то, что ждёшь, какая‑то панель метаданных пуста, какой‑то тип маппится не так.
Поэтому строчки «поддерживаем 33 движка» без звёздочки у нас нет. Есть таблица совместимости, где по каждому wire‑совместимому движку панель за панелью проверено руками. Небольшой фрагмент, чтобы было понятно, о чём речь:
|
Движок (wire) |
Объектный браузер |
Метаданные |
EXPLAIN |
|---|---|---|---|
|
SingleStore (MySQL) |
да |
частично |
да |
|
StarRocks (MySQL) |
да |
частично |
да |
|
OceanBase (MySQL) |
да |
да |
частично |
|
ScyllaDB (Cassandra) |
да |
без счётчика строк |
неприменимо |
Не «должно работать по логике протокола», а «сели, прогнали, записали, что где отвалилось». Ячейка «частично» — это чей‑то потраченный вечер, а не предположение.
Что из этого вынести
Единая таблица результатов нужна не для того, чтобы всё выглядело одинаково, а чтобы показать, где движки на самом деле разные. Самой сложной частью оказался не рендеринг и не драйверы, а дисциплина: не рисовать кнопку, которая ничего не сделает, не показывать число, которое неверно, не обещать «ключ» там, где понятия ключа нет. Общий интерфейс хорош ровно настолько, насколько точно он отражает различия того, что под ним. Всё остальное — просто HTML‑таблица.
Исходники — https://github.com/libredb/libredb‑studio, там же ссылка на онлайн‑демо.
ссылка на оригинал статьи https://habr.com/ru/articles/1079790/