Meridian Base — настольные базы данных
Документ приложения по соглашениям раздела 8 библии (
docs/00-foundation.md). Версия 0.1 · 2026-10-03. Фаза 5 · редакции Pro и выше (Feature::Base; просмотр и ввод данных в готовые формы.mbx— Home,Feature::BaseRuntime) · формат.mbx· цвет--app-base#A61E4D· значок «цилиндр» · идентификаторы экрановBS-xx· макетmockups/base.html.
Содержание: 1. Назначение и аналоги · 2. Функции · 3. Экраны · 4. Модель данных · 5. Алгоритмы и скрипты · 6. Форматы · 7. Горячие клавиши · 8. Критерии готовности · 9. Риски · 10. Вопросы к библии.
1. Назначение и аналоги
Meridian Base — настольная СУБД для пользователя, а не для
администратора: таблицы, запросы (визуальный конструктор и SQL), формы
(WYSIWYG), отчёты (полосы с группировкой и итогами), схема данных,
макросы-действия без кода и MeridianScript. Основной движок — встроенный
SQLite в файле .mbx; для серверных данных — коннекторы
PostgreSQL, MySQL/MariaDB, Microsoft SQL Server, ODBC и внешние SQLite в
режиме «связанные таблицы».
| Аналог | Сильные стороны | Слабости, которые закрываем |
|---|---|---|
| Microsoft Access | формы/отчёты/запросы в одном файле, DAO/VBA, огромная база пользователей | только Windows, Jet/ACE — один файл на сетевом диске, 2 ГБ, VBA-макровирусы, закрытый формат |
| LibreOffice Base | открытость, HSQLDB/Firebird, коннекторы | медленно, формы на Writer-движке, нестабильность, слабые отчёты |
| FileMaker | формы и раскладки, кроссплатформенность | дорогой, закрытый, проприетарный сервер |
| Airtable / NocoDB | веб, совместная работа | онлайн, нет отчётов «для печати», нет локальных файлов |
| DBeaver / pgAdmin | администрирование | не для конечного пользователя, нет форм/отчётов |
Отличия Meridian Base: открытый .mbx (обычный SQLite —
читается любым инструментом), три ОС, движок выражений и диаграмм общий
с Sheets, формы на той же сцене, что Draw/Slides, отчёты через Meridian
Print, импорт .accdb/.mdb (таблицы, запросы, связи;
формы/отчёты — с ограничениями), DAO-подобный MeridianScript с таблицей
переноса VBA, шифрование SQLCipher, мастер миграции на PostgreSQL для
многопользовательской работы.
Связи в пакете: Sheets (импорт/экспорт, «открыть таблицу в Sheets», сводные по запросам), Write (слияние документов из запросов — P1), Mail (отправка отчётов, контакты как связанный источник), Forms (ответы → таблица), Chart (диаграммы в формах/отчётах), Hub (недавние, шаблоны баз «Контакты», «Склад», «Заказы», «Проекты»).
Код: kernel/crates/meridian-base/ (модель объектов, QBE,
SQL-транслятор, курсоры, формы/отчёты как модели),
kernel/crates/meridian-db/ (бэкенды: sqlite/rusqlite +
SQLCipher, postgres, mysql, mssql/tiberius, odbc),
codecs/accdb/ (парсер Jet/ACE),
shell/src/base/ (QML/C++: сетка, конструкторы).
2. Функции
2.1 Хранилище и подключения
| Функция | Описание | Приоритет |
|---|---|---|
Встроенный SQLite .mbx |
один файл: данные + служебные таблицы meridian_*; WAL;
application_id MBX1; STRICT-таблицы (SQLite ≥
3.37); FTS5 для полнотекстового поиска по таблицам |
P0 |
| Внешний SQLite | подключение .sqlite/.db как связанных таблиц или
открытие «как есть» (без служебных таблиц — создаются по запросу) |
P0 |
| PostgreSQL | 12+: связанные таблицы/представления, запросы на сервере,
транзакции, роли, LISTEN/NOTIFY для обновления форм (P1);
SSL |
P0 |
| MySQL / MariaDB | 8.0+/10.5+: связанные таблицы, запросы, транзакции | P1 |
| Microsoft SQL Server | 2016+/Azure SQL через TDS (tiberius),
Windows-аутентификация (SSPI) на Windows, SQL-аутентификация везде |
P1 |
| ODBC | любой драйвер (odbc-api): DSN/строка подключения;
ограниченный диалект (ANSI SQL-92) |
P1 |
| Режим «связанные таблицы» | таблица источника видна в навигаторе со значком связи; данные не копируются; запрос по одному источнику выполняется на нём (транслятор SQL в диалект); смешанные запросы — через виртуальные таблицы SQLite с проталкиванием фильтров | P0 (смешанные — P1) |
| Импорт с сервера | копирование таблицы/результата запроса в локальную | P0 |
| Менеджер связей | повторное связывание при переносе, обновление структуры, пароли в хранилище ОС | P0 |
| Пул соединений, таймауты, повторные попытки | на источник; индикатор состояния | P0 |
2.2 Таблицы
| Функция | Описание | Приоритет |
|---|---|---|
| Режим таблицы (datasheet) | сетка с виртуализацией (1 М строк), сортировка/фильтр по столбцам (проталкивание в SQL), фильтр по выделению/исключению/форме, поиск и замена, итоги (строка итогов: Sum/Avg/Count/Min/Max/StDev), подтаблицы (подчинённые по связи), ширина/порядок/скрытие/закрепление столбцов, форматирование (цвета, шрифт), условное форматирование (P1), копирование в/из Sheets | P0 |
| Конструктор таблицы | поля: имя, тип, описание; свойства: размер, формат, маска ввода, подпись, значение по умолчанию (выражение), условие на значение (CHECK) и сообщение, обязательное, индексированное (да/без повторов), подстановка (lookup), уникальность; первичный ключ (одиночный/составной), индексы (составные, частичные — P1), автонумерация (INTEGER PRIMARY KEY, ULID/UUID — P1) | P0 |
| Типы данных | Короткий текст, Длинный текст (с форматированием — HTML/Write-фрагмент), Число (байт/целое/длинное целое/одинарное/двойное/десятичное — подтипы в метаданных), Большое число (64 бит), Денежный (масштаб 10⁴), Дата/время (ISO-8601 текст с нормализацией), Дата, Время, Логический, Счётчик, Вложение, Гиперссылка, Вычисляемое (generated column), GUID, Многозначное (через таблицу связи, импорт из Access), Изображение (BLOB с миниатюрой), JSON | P0 (JSON, многозначные — P1) |
| Вложения | несколько файлов на запись (meridian_attachments),
просмотр через кодеки, миниатюры |
P0 |
| Подстановки (lookup) | поле со списком из другой таблицы/запроса/списка значений; отображаемый и сохраняемый столбцы; «разрешить несколько» (многозначное) | P0 |
| Изменение структуры | безопасная миграция SQLite (ALTER TABLE где можно,
иначе копирование таблицы с сохранением индексов/связей/триггеров),
предпросмотр потерь данных |
P0 |
| Триггеры и журнал изменений | «Журналировать изменения» таблицы (триггеры в
meridian_audit), «кто/когда» для серверных источников |
P1 |
| Полнотекстовый поиск | FTS5 по выбранным текстовым столбцам, обновление триггерами | P1 |
2.3 Запросы
| Функция | Описание | Приоритет |
|---|---|---|
| Конструктор QBE | область таблиц (перетаскивание, связи автоматически из схемы, типы
соединения: внутреннее/левое/правое/полное — полное через
UNION в SQLite), сетка: Поле · Таблица · Сортировка · Вывод
· Условие отбора (строки «или») · Групповая операция
(Группировка/Sum/Avg/Min/Max/Count/StDev/Var/First/Last/Выражение/Условие)
· Псевдоним; вычисляемые поля с выражениями (движок выражений
Sheets-синтаксиса + SQL-функции) |
P0 |
| SQL-режим | редактор с подсветкой, автодополнением имён, форматированием,
объяснением плана (EXPLAIN QUERY PLAN), выполнение
выделенного; двусторонний переход QBE ↔︎ SQL (разбор SQL в QBE для
поддерживаемого подмножества) |
P0 |
| Типы запросов | выборка; с итогами; перекрёстный (crosstab); обновление; добавление; удаление; создание таблицы; объединение (UNION); подчинённые запросы; передача на сервер (pass-through) | P0 (pass-through — P1) |
| Параметры | [Введите дату начала] в условиях → диалог с типами;
параметры из формы (Forms!frm!ctl); именованные параметры в
SQL (:start) |
P0 |
| Свойства запроса | уникальные значения (DISTINCT), верхние N/%, блокировки, таймаут, источник (локально/сервер) | P0 |
| Сохранённые представления | запрос как VIEW в SQLite (для внешних инструментов) | P1 |
2.4 Формы
| Функция | Описание | Приоритет |
|---|---|---|
| Конструктор WYSIWYG | сетка 4/8 px, выравнивание и распределение, направляющие, якоря (anchor) для изменения размера окна, разделы (заголовок/область данных/примечание формы, заголовок/примечание страницы для печати), вкладки свойств (Данные · Макет · События · Другие), список полей источника перетаскиванием, галерея элементов, стили MDS | P0 |
| Элементы управления | поле (текст, число, дата с календарём, маска), подпись, список, поле со списком (с поиском по мере ввода), флажок, переключатель (группа), выключатель, вкладки, подчинённая форма (связь по полям), кнопка (действие/макрос/скрипт), изображение (статическое/из поля), диаграмма (Meridian Chart из источника), линия, прямоугольник, гиперссылка, вложение (список файлов), «Длинный текст» с форматированием (Write), веб-просмотр (P2), навигационная панель, разрыв страницы, подчинённый отчёт (P1), календарь-выбор (P1), ползунок (P2) | P0 |
| Привязка | источник записей (таблица/запрос/SQL), привязка элементов к полям
или выражениям (=[Цена]*[Кол]), фильтр и порядок по
умолчанию, «разрешить добавление/удаление/изменение», «только ввод
данных» |
P0 |
| Виды | одиночная (один экземпляр), ленточная (continuous), табличная (datasheet), разделённая (форма + таблица), модальная/диалоговая, всплывающая; режимы: Форма / Макет (правка с данными) / Конструктор | P0 |
| Навигация | кнопки записи (первая/назад/№/вперёд/последняя/новая), поиск, фильтр по форме, сортировка, счётчик «запись N из M» | P0 |
| События | Open, Load, Current, BeforeInsert, AfterInsert, BeforeUpdate, AfterUpdate, Delete, BeforeDelConfirm, AfterDelConfirm, Close, Unload, Activate, Deactivate, Timer, Resize, Error, Filter, ApplyFilter; у элементов: Click, DblClick, Change, Enter, Exit, GotFocus, LostFocus, BeforeUpdate, AfterUpdate, KeyDown/Up/Press, MouseDown/Up/Move, NotInList, Dirty, Undo — на MeridianScript или макрос | P0 |
| Макросы-действия | конструктор без кода: последовательность действий с аргументами,
условия If/Else, подмакросы, обработка ошибок; 40 действий
(раздел 5.8); встроенные (в событие) и именованные |
P0 |
| Валидация | правила столбцов (CHECK) + правила элементов + маска ввода +
обязательность; сообщения; подсветка ошибок, отмена записи
Esc; «грязные» индикаторы |
P0 |
| Условное форматирование | правила на элементах (значение/выражение/фокус) | P1 |
| Печать формы | через Meridian Print (как отчёт одиночной записи) | P1 |
| Темы | темы форм (MDS светлая/тёмная, компактная), пользовательские стили | P1 |
2.5 Отчёты
| Функция | Описание | Приоритет |
|---|---|---|
| Конструктор с полосами | Заголовок отчёта · Верхний колонтитул · Заголовок группы (×10 уровней) · Область данных · Примечание группы · Нижний колонтитул · Примечание отчёта; «не разрывать» (группу/с первой записью/с заголовком), «новая страница до/после», «повторять заголовок группы», «скрывать повторы», «расширение/сжатие по содержимому» (can grow/shrink) | P0 |
| Группировка и сортировка | панель: поле/выражение, порядок, интервал группировки (по значению/первой букве/году/кварталу/месяцу/неделе/дню/интервалу чисел), заголовок/примечание вкл/выкл | P0 |
| Итоги | агрегаты по группам/отчёту/странице, нарастающий итог (по группе/по
отчёту), номер страницы/Страница N из M, вычисляемые поля с
выражениями, условное форматирование |
P0 |
| Элементы | те же, что в формах (без ввода) + подотчёт (связанный по полям), диаграмма по группе, штрихкод/QR (P1) | P0 |
| Этикетки | мастер почтовых наклеек: размеры Avery/Herma/свои, N колонок, порядок заполнения, поля из источника | P0 |
| Режимы | Отчёт (с данными, интерактивно: сортировка/фильтр щелчком — P1), Предпросмотр печати, Макет, Конструктор | P0 |
| Вывод | печать (Meridian Print), PDF/A, отправка по почте (Mail), экспорт в Write/Sheets/HTML/CSV | P0 |
| Мастера | мастер отчёта (поля → группировка → сортировка/итоги → макет → стиль), автоотчёт по таблице/запросу | P0 |
2.6 Схема данных, импорт/экспорт, безопасность, многопользовательский режим
| Функция | Описание | Приоритет |
|---|---|---|
| Схема данных | диаграмма таблиц и связей (один-ко-многим, один-к-одному, многие-ко-многим через таблицу связи), целостность (FOREIGN KEY), каскадное обновление/удаление, автораскладка, печать схемы | P0 |
| Импорт | Sheets .msx/Excel .xlsx (лист/диапазон,
первая строка — имена, определение типов), CSV/TSV (кодировка,
разделитель, кавычки, предпросмотр), JSON (массив объектов/NDJSON,
сплющивание), XML (XSD-подобный разбор повторяющихся элементов),
.accdb/.mdb (таблицы, данные, связи, запросы, подстановки;
формы/отчёты — конвертер с ограничениями), .odb (встроенный
HSQLDB — через SQL-дамп/script файл; Firebird — R*), другая
.mbx, с сервера |
P0 (.odb — P1) |
| Экспорт | Sheets/Excel, CSV, JSON, XML, SQL-дамп (DDL+DML, диалекты), PDF отчётов, HTML, Write (слияние) | P0 |
| Связывание с файлами | CSV/Excel как связанная таблица (только чтение, обновление по требованию) | P1 |
| Шифрование | SQLCipher 4 (AES-256-CBC, PBKDF2-HMAC-SHA512 256 000 итераций, HMAC страниц) — пароль на базу; смена пароля; «запомнить в хранилище ОС» | P0 |
| Пароли и роли | для серверных СУБД — серверные роли; в .mbx — один
пароль на файл (без ролей; роли уровня приложения в формах — P2) |
P0 |
| Многопользовательский режим | SQLite — один писатель: WAL даёт одновременное чтение; на сетевых
дисках (SMB/NFS) — предупреждение при открытии и рекомендация миграции;
«разделение базы» (front-end .mbx с формами + back-end с
данными) |
P0 |
| Мастер миграции | .mbx → PostgreSQL/MySQL/MS SQL: создание схемы (типы,
ключи, индексы, ограничения, значения по умолчанию), перенос данных
пакетами с прогрессом, перелинковка таблиц, проверка запросов на
совместимость диалекта, отчёт |
P0 (PostgreSQL), P1 (прочие) |
| Резервные копии | «Сохранить копию», автокопии при изменении структуры
(.mbx.bak), VACUUM/сжатие и
восстановление |
P0 |
| Документация базы | отчёт по объектам (таблицы, поля, связи, запросы) | P1 |
2.7 Прочее
Навигатор объектов с группами и поиском; шаблоны баз из Hub;
«Параметры запуска» (форма при открытии, скрыть навигатор, заголовок
приложения, значок) — режим «приложение» для конечных пользователей
(Home с BaseRuntime); командная палитра; Assist (P2:
«запрос по описанию», «форма по таблице»); печать; доступность;
MeridianScript-редактор с отладчиком (общий с пакетом).
3. Экраны
Каркас окна — по разделу 5.3 библии. Ribbon Base: Файл ·
Главная · Создание · Внешние данные · Работа с базой данных ·
Скрипты + контекстные вкладки по объекту (Поля/Таблица;
Конструктор запросов; Конструктор форм: Конструктор/Упорядочить/Формат;
Конструктор отчётов: Конструктор/Упорядочить/Формат/Параметры страницы;
Предпросмотр печати). Слева — навигатор объектов (F11),
объекты открываются во вкладках документа.
| ID | Экран | Описание |
|---|---|---|
| BS-01 | Главное окно | Навигатор объектов: группы Таблицы · Запросы · Формы · Отчёты · Макросы · Скрипты (+ пользовательские группы, «Связанные» со значком источника), поиск, сортировка; вкладки открытых объектов; строка состояния: режим объекта · «Запись 12 из 1 240» · фильтр · источник (локально/PostgreSQL) · замок шифрования |
| BS-02 | Таблица — режим таблицы | Сетка с заголовками (значок типа, стрелки сортировки, воронка фильтра), строка «новая запись» со звёздочкой, строка итогов, подтаблицы (+), навигатор записей внизу (первая/назад/№/вперёд/последняя/новая, поиск, «фильтр применён»), контекстное меню столбца (сортировка, фильтр по значению/тексту/дате, скрыть, закрепить, ширина, формат, переименовать, вставить поле слева), меню ячейки (копировать, фильтр по выделению, «=»/«≠», подстановка) |
| BS-03 | Конструктор таблицы | Верх: сетка полей (Имя · Тип · Описание), ключ слева; низ: свойства выбранного поля (Общие/Подстановка): размер, формат, маска, подпись, по умолчанию, условие, сообщение, обязательное, индексировано, уникальное, выравнивание; справа — панель «Индексы» и «Ограничения»; кнопка «Проверить изменения» (предпросмотр миграции с потерями) |
| BS-04 | Конструктор запросов (QBE) | Верх: область таблиц (карточки с полями, ключи, линии связей с типом
соединения — двойной щелчок меняет), низ: сетка QBE (Поле · Таблица ·
Сортировка · Вывод · Групповая операция · Условие · Или…); панель
«Свойства запроса»; кнопки Выполнить (!), Тип запроса,
Итоги (Σ), Параметры, SQL, Схема; результат открывается в BS-02 |
| BS-05 | SQL-режим | Редактор (подсветка, автодополнение по схеме, форматирование
Ctrl+Shift+F, сворачивание), результаты в сетке, вкладки
«Сообщения» и «План» (EXPLAIN QUERY PLAN визуально), выполнение
выделенного, история запросов; переключатель «Конструктор ↔︎ SQL» (при
невозможности разбора — предупреждение «Конструктор недоступен для этого
SQL») |
| BS-06 | Параметры запроса (диалог) | Список параметров с типами и значениями при выполнении; «запомнить значения» |
| BS-07 | Конструктор форм | Холст с разделами (полосы с заголовками, растягиваемые), сетка и направляющие; слева — галерея элементов и «Список полей» источника; справа — панель свойств (вкладки Данные/Макет/События/Другие) с поиском свойства; линейки; выделение группой, выравнивание (по левому/верхнему/сетке), распределение, порядок обхода (Tab order), «Автоформат» |
| BS-08 | Форма — режим просмотра | Форма по макету; навигатор записей, индикатор «грязной» записи (карандаш), ошибки валидации (красная рамка + сообщение), фильтр/сортировка из контекстного меню поля; режим «Макет» — правка с данными |
| BS-09 | Панель свойств элемента | Данные: источник/поле/выражение/маска/значение по умолчанию/условие/блокировка; Макет: позиция, размер, якоря, шрифт, цвет, видимость, формат, условное форматирование; События: список событий с «…» (макрос/скрипт/выражение); Другие: имя, подсказка, порядок обхода, статусная подсказка |
| BS-10 | Конструктор макросов | Дерево действий с аргументами (раскрывающиеся формы),
If/Else If/Else, подмакросы, группы, «Перехват ошибок»;
каталог действий справа с поиском; «Выполнить», «Пошагово»;
преобразовать в скрипт |
| BS-11 | Конструктор отчётов | Полосы (Заголовок отчёта / Верхний колонтитул / Заголовок группы «Клиент» / Область данных / Примечание группы / Нижний колонтитул / Примечание отчёта), линейки, панель «Группировка, сортировка и итоги» внизу (строки уровней с настройками), «Список полей», панель свойств; предпросмотр по кнопке |
| BS-12 | Предпросмотр отчёта | Страницы (одна/две/миниатюры), масштаб, навигация страниц, параметры страницы (размер, поля, ориентация, колонки), «Печать», «PDF», «Отправить», «Экспорт в Write/Sheets»; интерактивный режим отчёта (P1) |
| BS-13 | Мастер этикеток | Шаблон (производитель/код/размеры, своя сетка), шрифт, поля-строки (конструктор текста наклейки с полями), сортировка, предпросмотр |
| BS-14 | Схема данных | Диаграмма: таблицы (поля, ключи), связи с символами 1/∞, перетаскивание поля на поле создаёт связь; диалог связи: поля, целостность, каскады, тип соединения; автораскладка, печать, «Показать все/прямые связи» |
| BS-15 | Импорт/экспорт (мастер) | Источник/назначение → параметры (лист/диапазон, разделитель,
кодировка, первая строка — имена, типы столбцов с предпросмотром 100
строк, ключ) → в новую/существующую таблицу/связанную → прогресс и отчёт
ошибок (строки с проблемами в таблицу _import_errors) ;
«Сохранить шаги импорта» |
| BS-16 | Подключение к серверу | Тип (PostgreSQL/MySQL/MS SQL/ODBC/SQLite), хост/порт/БД/пользователь, SSL, «проверить», сохранить пароль в хранилище ОС; выбор таблиц/представлений для связывания с предпросмотром; менеджер связанных таблиц (обновить, пересвязать, удалить связь) |
| BS-17 | Шифрование и пароль | Установить/сменить/снять пароль базы (SQLCipher); сила пароля; предупреждение о невосстановимости; «запомнить на этом устройстве» |
| BS-18 | Мастер миграции | Выбор цели и подключения → анализ (таблицы, типы, несовместимости запросов, объём) → параметры (имена схем, регистр, индексы) → выполнение с прогрессом → отчёт → «Перелинковать объекты» |
| BS-19 | Редактор скриптов | Общий редактор MeridianScript пакета: файлы скриптов базы, автодополнение по DAO-API и схеме, точки останова, консоль, «Привязать к событию» |
| BS-20 | Параметры Base | Общие (формат по умолчанию, подтверждения действий), Таблицы (шрифт сетки, поведение Enter/Tab), Конструкторы (сетка, единицы), Запросы (таймаут, лимит строк предпросмотра), Параметры запуска текущей базы (форма, заголовок, навигатор), Серверы (пулы, таймауты), Безопасность (доверенные базы для скриптов) |
| BS-21 | Мастер подстановок | Источник (таблица/запрос/список значений) → столбцы (отображаемый/сохраняемый, скрыть ключ) → ширины → подпись, «разрешить несколько значений», «ограничиться списком» |
| BS-22 | Поиск и замена | Что/где (поле/таблица), совпадение (любая часть/целиком/с начала), регистр, направление, формат; замена с подтверждением |
| BS-23 | Фильтр по форме | Пустая форма/сетка для ввода условий (с подсказкой значений), вкладки «Или», «Применить», сохранить как запрос |
| BS-24 | Мастер форм / отчётов | Источник → поля (включая из связанных таблиц) → вид (одиночная/ленточная/табличная; для отчётов — группировка/сортировка/итоги/макет) → стиль → имя, «открыть/изменить макет» |
| BS-25 | Диалог связи | Таблица/связанная таблица, пары полей, «обеспечение целостности», каскадное обновление/удаление, тип объединения |
| BS-26 | Монитор запросов и блокировок | Активные подключения/запросы (сервер), «кто открыл» (для
.mbx на сетевом диске — список lock-файлов), отмена
запроса |
4. Модель данных
4.1 Файл .mbx
.mbx — обычная база SQLite
(PRAGMA application_id = 0x4D425831 'MBX1',
user_version = версия схемы метаданных,
journal_mode = WAL, foreign_keys = ON,
кодировка UTF-8, page_size = 4096). Пользовательские
таблицы — обычные SQLite-таблицы (STRICT, если включено); все метаданные
Base — в таблицах с префиксом meridian_. Любой инструмент
SQLite открывает данные; Base без служебных таблиц создаёт их при первом
открытии. Зашифрованный файл — формат SQLCipher 4 (первые 16 байт — соль
вместо SQLite format 3\0; тип определяется по расширению и
попытке открытия с паролем).
4.2 Служебные таблицы
-- Реестр объектов / Object registry
CREATE TABLE meridian_objects (
id INTEGER PRIMARY KEY,
kind TEXT NOT NULL CHECK(kind IN ('table','linked_table','query','form','report','macro','script','module','group')),
name TEXT NOT NULL, -- отображаемое имя / display name
sql_name TEXT, -- имя в SQLite (для таблиц/представлений) / SQLite identifier
group_id INTEGER REFERENCES meridian_objects(id), -- пользовательская группа навигатора
description TEXT, hidden INTEGER DEFAULT 0, system INTEGER DEFAULT 0,
created TEXT NOT NULL, modified TEXT NOT NULL, created_by TEXT, modified_by TEXT,
properties TEXT, -- JSON: произвольные свойства (ширины столбцов, порядок, формат)
UNIQUE(kind, name)
);
-- Метаданные столбцов (то, чего нет в SQLite): подтипы, подписи, маски, подстановки
-- / Column metadata beyond SQLite types
CREATE TABLE meridian_columns (
table_id INTEGER NOT NULL REFERENCES meridian_objects(id) ON DELETE CASCADE,
name TEXT NOT NULL, ord INTEGER NOT NULL,
subtype TEXT NOT NULL, -- short_text|long_text|rich_text|byte|integer|long|bigint|single|double|decimal|currency|
-- datetime|date|time|boolean|autonumber|attachment|hyperlink|calculated|guid|multi|image|json
size INTEGER, precision INTEGER, scale INTEGER,
caption TEXT, description TEXT, format TEXT, input_mask TEXT, default_expr TEXT,
validation_rule TEXT, validation_text TEXT, required INTEGER DEFAULT 0, allow_zero_length INTEGER DEFAULT 1,
lookup_id INTEGER REFERENCES meridian_lookups(id), display_width INTEGER, hidden INTEGER DEFAULT 0, align TEXT,
PRIMARY KEY(table_id, name)
);
-- Подстановки / Lookups
CREATE TABLE meridian_lookups (
id INTEGER PRIMARY KEY, kind TEXT CHECK(kind IN ('table','query','values')), source TEXT, -- имя таблицы/запроса или JSON-список
bound_column INTEGER DEFAULT 1, display_columns TEXT, column_widths TEXT, limit_to_list INTEGER DEFAULT 1,
allow_multiple INTEGER DEFAULT 0, row_source_sql TEXT
);
-- Связи и целостность (дублируют FOREIGN KEY для редактирования и для связанных таблиц)
-- / Relationships (mirror of FOREIGN KEY, plus linked tables)
CREATE TABLE meridian_relationships (
id INTEGER PRIMARY KEY, name TEXT UNIQUE,
parent_table TEXT NOT NULL, parent_columns TEXT NOT NULL, -- JSON ["id"]
child_table TEXT NOT NULL, child_columns TEXT NOT NULL, -- JSON ["customer_id"]
kind TEXT CHECK(kind IN ('one_to_many','one_to_one','many_to_many')) DEFAULT 'one_to_many',
enforce INTEGER DEFAULT 1, cascade_update INTEGER DEFAULT 0, cascade_delete INTEGER DEFAULT 0,
join_type TEXT CHECK(join_type IN ('inner','left','right')) DEFAULT 'inner',
diagram_pos TEXT -- JSON: позиции таблиц на схеме
);
-- Запросы / Queries
CREATE TABLE meridian_queries (
object_id INTEGER PRIMARY KEY REFERENCES meridian_objects(id) ON DELETE CASCADE,
kind TEXT NOT NULL CHECK(kind IN ('select','totals','crosstab','update','append','delete','make_table','union','passthrough','ddl')),
design_xml TEXT, -- модель QBE (urn:meridian:office:base:1), NULL для чистого SQL
sql TEXT NOT NULL, -- сгенерированный или введённый SQL / generated or hand-written
params TEXT, -- JSON [{name,type,prompt}]
source TEXT, -- NULL = локально, иначе id связи сервера / NULL = local
properties TEXT -- JSON: distinct, top, timeout, ...
);
-- Формы / Forms (XML)
CREATE TABLE meridian_forms (
object_id INTEGER PRIMARY KEY REFERENCES meridian_objects(id) ON DELETE CASCADE,
form_xml TEXT NOT NULL, -- urn:meridian:office:base:1 <form>
record_source TEXT, view TEXT CHECK(view IN ('single','continuous','datasheet','split')) DEFAULT 'single',
thumbnail BLOB
);
-- Отчёты / Reports (XML)
CREATE TABLE meridian_reports (
object_id INTEGER PRIMARY KEY REFERENCES meridian_objects(id) ON DELETE CASCADE,
report_xml TEXT NOT NULL, record_source TEXT, page_setup TEXT, thumbnail BLOB
);
-- Макросы и скрипты / Macros and scripts
CREATE TABLE meridian_macros (object_id INTEGER PRIMARY KEY REFERENCES meridian_objects(id) ON DELETE CASCADE, macro_xml TEXT NOT NULL);
CREATE TABLE meridian_scripts (object_id INTEGER PRIMARY KEY REFERENCES meridian_objects(id) ON DELETE CASCADE,
language TEXT CHECK(language IN ('ts','js','py')) DEFAULT 'ts', source TEXT NOT NULL, compiled BLOB, signature BLOB, permissions TEXT);
-- Связанные источники / Linked sources
CREATE TABLE meridian_links (
id INTEGER PRIMARY KEY, name TEXT UNIQUE, driver TEXT CHECK(driver IN ('postgres','mysql','mssql','odbc','sqlite','csv','xlsx')),
conn TEXT NOT NULL, -- строка подключения без пароля / connection string without secret
credential_ref TEXT, -- ключ в хранилище ОС / OS keystore key
options TEXT
);
CREATE TABLE meridian_linked_tables (
object_id INTEGER PRIMARY KEY REFERENCES meridian_objects(id) ON DELETE CASCADE,
link_id INTEGER REFERENCES meridian_links(id), remote_schema TEXT, remote_name TEXT, remote_pk TEXT,
columns_cache TEXT, cached_at TEXT, read_only INTEGER DEFAULT 0
);
-- Вложения / Attachments
CREATE TABLE meridian_attachments (
id INTEGER PRIMARY KEY, table_name TEXT NOT NULL, column_name TEXT NOT NULL, row_id INTEGER NOT NULL,
file_name TEXT NOT NULL, mime_type TEXT, size INTEGER, sha256 TEXT, data BLOB, thumb BLOB, added TEXT
);
CREATE INDEX meridian_attachments_row ON meridian_attachments(table_name, column_name, row_id);
-- Настройки базы / Database settings
CREATE TABLE meridian_settings (key TEXT PRIMARY KEY, value TEXT);
-- ключи: startup.form, startup.title, startup.icon, startup.hide_navigator, app.version, schema.strict, audit.enabled, ...
-- Журнал изменений (опционально) / Audit log (optional)
CREATE TABLE meridian_audit (id INTEGER PRIMARY KEY, ts TEXT, user TEXT, table_name TEXT, row_id INTEGER, op TEXT, before TEXT, after TEXT);
-- Ошибки импорта / Import errors
CREATE TABLE meridian_import_errors (id INTEGER PRIMARY KEY, run_id TEXT, row_no INTEGER, column_name TEXT, value TEXT, error TEXT);4.3 Сопоставление типов Access → SQLite
| Access (Jet/ACE) | subtype |
SQLite (STRICT) | Примечания |
|---|---|---|---|
| Короткий текст (≤ 255) | short_text |
TEXT + CHECK(length(x) <= N) |
размер в meridian_columns.size |
| Длинный текст (Memo) | long_text / rich_text |
TEXT |
rich — HTML-фрагмент Write |
| Число: байт / целое / длинное целое | byte / integer / long |
INTEGER |
диапазон — CHECK |
| Большое число | bigint |
INTEGER |
64 бит |
| Число: одинарное / двойное | single / double |
REAL |
|
| Число: десятичное (precision, scale) | decimal |
TEXT (каноническая десятичная строка) или
INTEGER масштабированный при scale ≤ 4 |
арифметика через decimal в движке выражений |
| Денежный | currency |
INTEGER (×10 000) |
формат отображения по локали |
| Дата/время | datetime |
TEXT ISO-8601 YYYY-MM-DDTHH:MM:SS.SSS |
сортируется лексикографически, функции
date()/julianday() |
| Дата/время расширенный | datetime |
TEXT с наносекундами |
|
| Логический | boolean |
INTEGER 0/1 + CHECK |
|
| Счётчик (AutoNumber) | autonumber |
INTEGER PRIMARY KEY AUTOINCREMENT |
случайные/реплика — guid |
| Код репликации / GUID | guid |
TEXT (36 символов) или BLOB(16) |
|
| Поле объекта OLE | image / attachment |
BLOB |
OLE-обёртка снимается (извлечение изображения из
OLE package) |
| Вложение | attachment |
строки в meridian_attachments |
столбец в таблице — INTEGER счётчик |
| Гиперссылка | hyperlink |
TEXT display#address#sub#tip → JSON |
|
| Вычисляемое | calculated |
GENERATED ALWAYS AS (expr) VIRTUAL |
выражение транслируется в SQL |
| Многозначное (Complex) | multi |
таблица связи <table>__<col> |
импорт из Access разворачивает |
| Подстановка | любой + lookup_id |
базовый тип + FOREIGN KEY |
4.4 Форма (form_xml)
<form xmlns="urn:meridian:office:base:1" name="Заказы" view="single" record-source="SELECT * FROM orders ORDER BY date DESC"
allow-add="true" allow-edit="true" allow-delete="true" width="720" caption="Заказы" popup="false" modal="false"
filter="" order-by="" default-view="single" nav-buttons="true" record-selectors="true">
<events on-open="macro:ПриОткрытии" on-current="script:orders.ts#onCurrent" on-before-update="script:orders.ts#validate"/>
<section kind="header" height="48" back="#F5F6F8">
<control type="label" name="lblTitle" x="16" y="12" w="300" h="24" text="Заказ" font="Inter 18 600"/>
</section>
<section kind="detail" height="320">
<control type="textbox" name="txtNumber" x="16" y="16" w="160" h="28" field="number" locked="true" tab-index="0"/>
<control type="datepicker" name="dtDate" x="200" y="16" w="160" h="28" field="date" format="dd.MM.yyyy" default="=Date()"/>
<control type="combobox" name="cboCustomer" x="16" y="60" w="344" h="28" field="customer_id"
row-source="SELECT id, name FROM customers ORDER BY name" bound-column="1" columns="0;1" widths="0;320" limit-to-list="true">
<events on-not-in-list="script:orders.ts#addCustomer"/>
</control>
<control type="textbox" name="txtTotal" x="16" y="104" w="160" h="28" expr="=DSum('qty*price','order_items','order_id=' & [id])" format="currency" locked="true"/>
<control type="checkbox" name="chkPaid" x="200" y="104" w="120" h="28" field="paid" caption="Оплачен"/>
<control type="tab" name="tabs" x="16" y="148" w="688" h="160">
<page caption="Позиции">
<control type="subform" name="sfItems" x="8" y="8" w="664" h="130" source-object="form:Позиции заказа"
link-master="id" link-child="order_id" view="datasheet"/>
</page>
<page caption="Примечания"><control type="richtext" name="txtNotes" x="8" y="8" w="664" h="130" field="notes"/></page>
</control>
<control type="button" name="btnPrint" x="600" y="16" w="104" h="28" caption="Печать" icon="print">
<events on-click="macro:OpenReport(report='Счёт', view='preview', where='id=' & [id])"/>
</control>
</section>
<section kind="footer" height="40"/>
</form>4.5 Отчёт (report_xml)
<report xmlns="urn:meridian:office:base:1" name="Продажи по клиентам" record-source="qSalesByCustomer"
page="A4" orientation="portrait" margins="20 15 20 15" columns="1" units="mm">
<grouping>
<level field="customer_name" order="asc" header="true" footer="true" keep-together="whole-group" repeat-header="true"/>
<level field="date" interval="month" order="asc" header="true" footer="false"/>
<sort field="date" order="asc"/>
</grouping>
<band kind="report-header" height="30"><control type="label" x="0" y="0" w="180" h="12" text="Продажи по клиентам" font="Inter 16 600"/></band>
<band kind="page-header" height="10">
<control type="label" x="0" y="0" w="60" h="8" text="Дата"/><control type="label" x="60" y="0" w="80" h="8" text="Товар"/>
<control type="label" x="140" y="0" w="30" h="8" text="Сумма" align="right"/>
</band>
<band kind="group-header" level="1" height="12" new-page="before" force-new-page="none">
<control type="textbox" x="0" y="2" w="180" h="8" field="customer_name" font="Inter 11 600"/>
</band>
<band kind="group-header" level="2" height="8"><control type="textbox" x="0" y="0" w="80" h="8" expr="=Format([date],'MMMM yyyy')"/></band>
<band kind="detail" height="8" can-grow="true" hide-duplicates="date">
<control type="textbox" x="0" y="0" w="60" h="8" field="date" format="dd.MM.yyyy"/>
<control type="textbox" x="60" y="0" w="80" h="8" field="product" can-grow="true"/>
<control type="textbox" x="140" y="0" w="30" h="8" field="amount" format="currency" align="right"/>
</band>
<band kind="group-footer" level="1" height="10">
<control type="textbox" x="100" y="1" w="40" h="8" text="Итого:" align="right"/>
<control type="textbox" x="140" y="1" w="30" h="8" expr="=Sum([amount])" format="currency" align="right" running-sum="none"/>
</band>
<band kind="page-footer" height="8"><control type="textbox" x="140" y="0" w="30" h="8" expr="='Стр. ' & [Page] & ' из ' & [Pages]" align="right"/></band>
<band kind="report-footer" height="12"><control type="textbox" x="140" y="2" w="30" h="8" expr="=Sum([amount])" format="currency" align="right"/></band>
</report>4.6 Макрос (macro_xml)
<macro xmlns="urn:meridian:office:base:1" name="ПриОткрытии">
<action name="SetTempVar" var="opened_at" value="=Now()"/>
<if condition="=IsNull([Forms]![Заказы]![cboCustomer])">
<action name="MessageBox" text="Выберите клиента" type="warning"/>
<action name="GoToControl" control="cboCustomer"/>
<else/>
<action name="ApplyFilter" where="paid = 0"/>
</if>
<on-error goto="next"/>
</macro>4.7 Структуры ядра (Rust)
// kernel/crates/meridian-base/src/query.rs — модель QBE / QBE model
pub struct QueryDesign {
pub kind: QueryKind, // Select | Totals | Crosstab | Update | Append | Delete | MakeTable | Union
pub sources: Vec<SourceRef>, // таблицы/запросы с псевдонимами / tables & queries with aliases
pub joins: Vec<Join>, // { left, right, on: Vec<(Col, Col)>, kind: Inner|Left|Right|Full }
pub columns: Vec<QbeColumn>, // строки сетки по столбцам / grid columns
pub criteria: Vec<Vec<Option<Expr>>>, // criteria[row][col]: строки — OR, столбцы — AND / rows OR, cols AND
pub params: Vec<Param>,
pub distinct: bool, pub top: Option<Top>,
pub crosstab: Option<Crosstab>, // { row_headings, column_heading, value, agg, fixed_headings }
pub target: Option<String>, // для Append/MakeTable / for Append/MakeTable
}
pub struct QbeColumn {
pub expr: Expr, // поле или выражение / field or expression
pub alias: Option<String>, pub source: Option<String>,
pub show: bool, pub sort: Option<SortDir>,
pub total: Option<Total>, // GroupBy | Sum | Avg | Min | Max | Count | StDev | Var | First | Last | Expression | Where
pub update_to: Option<Expr>, // для Update / for Update
pub append_to: Option<String>, // для Append / for Append
}
// kernel/crates/meridian-db/src/backend.rs — единый интерфейс источников / unified backend trait
#[async_trait]
pub trait DbBackend: Send + Sync {
fn dialect(&self) -> Dialect; // Sqlite | Postgres | MySql | MsSql | Ansi
async fn schema(&self) -> Result<Schema>;
async fn query(&self, sql: &str, params: &[Value]) -> Result<Box<dyn Cursor>>;
async fn execute(&self, sql: &str, params: &[Value]) -> Result<u64>;
async fn begin(&self) -> Result<Box<dyn Transaction>>;
fn capabilities(&self) -> Caps; // returning, savepoints, window functions, pivot, keyset, ...
}5. Алгоритмы и скрипты
5.1 Генерация SQL из QBE
pub fn qbe_to_sql(q: &QueryDesign, d: Dialect) -> Result<String> {
let mut sql = String::new();
match q.kind {
QueryKind::Select | QueryKind::Totals | QueryKind::Crosstab => {
if q.kind == QueryKind::Crosstab { return crosstab_sql(q, d); }
sql.push_str("SELECT "); if q.distinct { sql.push_str("DISTINCT "); }
if let (Some(top), Dialect::MsSql) = (&q.top, d) { sql.push_str(&format!("TOP {} ", top.n)); }
// Столбцы: показываемые; при итогах — агрегаты / columns: shown; totals → aggregates
let cols: Vec<String> = q.columns.iter().filter(|c| c.show).map(|c| {
let e = render_expr(&c.expr, d);
let e = match c.total { Some(Total::GroupBy) | None => e, Some(Total::Expression) => e,
Some(t) => format!("{}({})", agg_name(t, d), e) };
match &c.alias { Some(a) => format!("{} AS {}", e, quote_ident(a, d)), None => e }
}).collect();
sql.push_str(&cols.join(", "));
sql.push_str(" FROM "); sql.push_str(&render_from(&q.sources, &q.joins, d)?); // FULL JOIN → UNION для SQLite/MySQL
// WHERE: условия столбцов без агрегата (или Total::Where); строки — OR, столбцы — AND
let (where_rows, having_rows) = split_criteria(q);
if let Some(w) = render_criteria(&where_rows, d) { sql.push_str(" WHERE "); sql.push_str(&w); }
if q.kind == QueryKind::Totals {
let groups: Vec<_> = q.columns.iter().filter(|c| c.total == Some(Total::GroupBy)).map(|c| render_expr(&c.expr, d)).collect();
if !groups.is_empty() { sql.push_str(" GROUP BY "); sql.push_str(&groups.join(", ")); }
if let Some(h) = render_criteria(&having_rows, d) { sql.push_str(" HAVING "); sql.push_str(&h); }
}
let order: Vec<_> = q.columns.iter().filter(|c| c.sort.is_some())
.map(|c| format!("{} {}", c.alias.as_ref().map(|a| quote_ident(a, d)).unwrap_or_else(|| render_expr(&c.expr, d)), if c.sort == Some(SortDir::Desc) {"DESC"} else {"ASC"})).collect();
if !order.is_empty() { sql.push_str(" ORDER BY "); sql.push_str(&order.join(", ")); }
if let (Some(top), false) = (&q.top, d == Dialect::MsSql) { sql.push_str(&format!(" LIMIT {}", top.n)); } // TOP N% → подзапрос с COUNT
}
QueryKind::Update => {
sql.push_str(&format!("UPDATE {} SET ", render_single_source(q, d)?));
let sets: Vec<_> = q.columns.iter().filter_map(|c| c.update_to.as_ref().map(|u| format!("{} = {}", render_expr(&c.expr, d), render_expr(u, d)))).collect();
sql.push_str(&sets.join(", "));
if let Some(w) = render_criteria(&q.criteria, d) { sql.push_str(" WHERE "); sql.push_str(&w); } // UPDATE … FROM для соединений (SQLite 3.33+, PG)
}
QueryKind::Delete => { sql.push_str(&format!("DELETE FROM {}", render_single_source(q, d)?)); if let Some(w) = render_criteria(&q.criteria, d) { sql.push_str(" WHERE "); sql.push_str(&w); } }
QueryKind::Append => { sql.push_str(&format!("INSERT INTO {} ({}) ", quote_ident(q.target.as_ref().unwrap(), d), append_targets(q, d))); sql.push_str(&qbe_to_sql(&q.as_select(), d)?); }
QueryKind::MakeTable => { sql.push_str(&format!("CREATE TABLE {} AS ", quote_ident(q.target.as_ref().unwrap(), d))); sql.push_str(&qbe_to_sql(&q.as_select(), d)?); }
QueryKind::Union => { sql = q.union_parts.iter().map(|p| qbe_to_sql(p, d)).collect::<Result<Vec<_>>>()?.join(" UNION ALL "); }
}
Ok(sql)
}Выражения (Expr) — общий язык выражений пакета
(синтаксис Sheets/Access: [Поле],
Forms![Заказы]![cbo], функции IIf,
Nz, Format, DateAdd,
Left, DLookup…) транслируются в SQL диалекта:
Nz(x, 0) → COALESCE(x, 0),
IIf(c, a, b) → CASE WHEN c THEN a ELSE b END,
Format([d], 'yyyy-MM') → strftime('%Y-%m', d)
(SQLite) / to_char(d, 'YYYY-MM') (PostgreSQL),
Left(s, n) → substr(s, 1, n); ссылки на формы
и параметры становятся связанными параметрами
?/$1; функции без SQL-эквивалента
(DLookup) выполняются в клиентской пост-обработке (столбец
помечается «вычисляется клиентом») или запрещаются в серверных
запросах.
5.2 Разбор SQL в QBE
Парсер — sqlparser-rs (диалекты
SQLite/PostgreSQL/MySQL/MsSql) → AST → QueryDesign, если
запрос входит в поддерживаемое подмножество:
SELECT [DISTINCT] <список> FROM <таблица|подзапрос как источник> [JOIN … ON a = b]* [WHERE] [GROUP BY] [HAVING] [ORDER BY] [LIMIT];- условия
WHERE— нормализуются в ДНФ (дизъюнкция конъюнкций) глубиной ≤ 10 строк «или»; каждый конъюнкт должен ссылаться на один столбец/выражение сетки (иначе — отдельный столбец сshow=false); - агрегаты в
SELECT→Total; столбцыGROUP BY→Total::GroupBy;HAVING→ условия на агрегатных столбцах; UPDATE/DELETE/INSERT … SELECT/CREATE TABLE … AS→ соответствующие типы;- не разбирается (остаётся SQL-только): оконные функции, CTE,
UNIONс разными источниками, коррелированные подзапросы вSELECT,CASEвON, нестандартные соединения. При любом не поддерживаемом узле —Err(Unsupported(node))и вкладка «Конструктор» недоступна с объяснением. - Обратимость:
qbe_to_sql(sql_to_qbe(s)) ≡ sсемантически (тест на 500 запросов корпуса Northwind/AdventureWorks-lite).
5.3 Перекрёстный запрос (crosstab)
crosstab_sql(q, dialect):
headings = q.crosstab.fixed_headings // если заданы «Заголовки столбцов» — один проход
если пусто:
headings = execute("SELECT DISTINCT <col_heading_expr> FROM <from> WHERE <where> ORDER BY 1 LIMIT 256") // проход 1
если dialect == MsSql и headings ≤ 100:
"SELECT <row_headings>, [h1], [h2], … FROM (SELECT <row_headings>, <col_heading> AS c, <value> AS v FROM …) s
PIVOT (<agg>(v) FOR c IN ([h1], [h2], …)) p"
иначе (SQLite, PostgreSQL, MySQL — без зависимости от tablefunc): // проход 2
"SELECT <row_headings>,
<agg>(CASE WHEN <col_heading> = 'h1' THEN <value> END) AS "h1",
<agg>(CASE WHEN <col_heading> = 'h2' THEN <value> END) AS "h2", …,
<agg>(<value>) AS "Итого" // если включён «итог по строке»
FROM <from> WHERE <where> GROUP BY <row_headings> ORDER BY <row_headings>"
NULL-заголовок → столбец "<>" ; > 256 заголовков → ошибка «слишком много столбцов, задайте фиксированные заголовки»
5.4 Курсоры, постраничная выборка и виртуализация сетки
Сетка на 1 М строк не держит данные в памяти: модель
RowWindow запрашивает страницы по 200 строк.
open_cursor(sql, order, filter):
base = "SELECT <pk>, <columns> FROM (<sql>) q WHERE <filter>"
если order детерминирован и заканчивается на pk (добавляем pk в конец всегда):
режим KEYSET: страница N+1 = "… WHERE (<order_cols>, pk) > (<last_values>) ORDER BY <order> LIMIT 200"
(для DESC — инвертируем сравнение; NULL-ы — через COALESCE с сентинелом по типу)
назад: инвертированный порядок + разворот страницы
иначе: режим OFFSET: "… ORDER BY <order> LIMIT 200 OFFSET N*200" (предупреждение о производительности > 100k)
count: "SELECT COUNT(*) FROM (<base>)" асинхронно с таймаутом 2 с → до завершения показываем «≈» и
оценку из EXPLAIN/статистики (`sqlite_stat1`, `pg_class.reltuples`)
кэш: LRU 20 страниц; предзагрузка ±2 страницы от видимого окна; прокрутка полосы — по номеру страницы
правка: изменения пишутся по pk ("UPDATE t SET c=? WHERE pk=?" с RETURNING для вычисляемых), страница инвалидируется точечно
вставка: новая строка помещается в конец видимого окна до фиксации, затем сетка переходит к ней по pk
источники без pk: SQLite — rowid; серверные представления без ключа — только чтение
Виртуализация в QML: TableView с моделью, реализующей
canFetchMore/fetchMore и data() через окно;
ширины столбцов — по выборке первых 200 строк; рендер ячейки — делегаты
по типу (текст/число/дата/флажок/ подстановка/вложение); редактор —
всплывающий по типу; «грязные» строки хранятся до фиксации в
оверлее.
5.5 Транзакции и отмена
- Правка записи в сетке/форме:
SAVEPOINT rec; изменения столбцов пишутся при выходе из записи (илиShift+Enter):UPDATE … WHERE pk = ?→RELEASE rec;Esc—ROLLBACK TO rec(первое нажатие — поле, второе — запись). - Undo-стек (на объект, глубина 100): элемент = обратная операция
(
before imageстроки по pk: для UPDATE — старые значения, для DELETE — вся строка, для INSERT — pk). Отмена выполняет обратный SQL в транзакции. - Массовые запросы (UPDATE/DELETE/APPEND): предварительный
SELECT COUNT(*)и подтверждение «Будет изменено N строк»; before-image сохраняется во временную таблицуtemp.undo_<n>, если N ≤ 100 000 — иначе предупреждение «отмена невозможна»; выполнение в одной транзакции, при ошибке — откат. - Изменение структуры:
BEGIN→ копирование таблицы (CREATE TABLE new … ; INSERT … SELECT …; DROP; RENAME) с пересозданием индексов/триггеров/внешних ключей (PRAGMA foreign_keys=OFFна время, затемPRAGMA foreign_key_check) →COMMIT; автокопия.mbx.bakперед операцией. - Серверные источники: транзакции бэкенда; при потере соединения — локальный буфер «неотправленные изменения» с предложением повторить.
5.6 Индексирование
Автоматически: индексы для внешних ключей
(idx_<table>_<col>), для полей с
«Индексировано: Да» и уникальных; советник: после выполнения запроса
EXPLAIN QUERY PLAN (SQLite) / EXPLAIN (PG) —
при полном сканировании таблицы > 10 000 строк с условием по столбцу
без индекса предлагается «Создать индекс по col» (в
статусной строке); составные индексы — по порядку условий;
ANALYZE после массового импорта.
5.7 Сортировка и фильтрация в сетке с проталкиванием в SQL
Фильтр по значению/выделению/тексту/дате строит
FilterExpr (AND по столбцам, внутри столбца — IN/LIKE/
диапазон/NULL); «Фильтр по форме» — ДНФ (вкладки «Или»).
FilterExpr → WHERE диалекта с параметрами
(LIKE с ESCAPE, регистронезависимость: SQLite
— COLLATE NOCASE/lower() с ICU-расширением для
кириллицы, PG — ILIKE); сортировка → ORDER BY
с COLLATE локали (ICU) для текста; для связанных CSV/Excel
— локальная таблица-кэш. Строка итогов → отдельный запрос агрегатов по
текущему фильтру.
5.8 Макросы-действия (40)
| Категория | Действия |
|---|---|
| Объекты и навигация (9) | OpenForm(form, view, where, filter, mode),
OpenReport(report, view, where),
OpenQuery(query, view, mode),
OpenTable(table, view, mode),
CloseWindow(object, save),
SelectObject(kind, name), `GoToRecord(object, name, record:
first |
| Данные (10) | ApplyFilter(filter, where),
SetFilter(where), SetOrderBy(expr),
FindRecord(text, match, case, direction, field),
FindNext(), Requery(control?),
Refresh(), SaveRecord(),
UndoRecord(), DeleteRecord() |
| Значения и переменные (6) | SetValue(item, expr),
SetProperty(control, property, value),
SetTempVar(name, expr), RemoveTempVar(name),
RemoveAllTempVars(),
RunSQL(sql, transaction) |
| Поток выполнения (7) | RunMacro(name, repeat, condition),
StopMacro(), StopAllMacros(),
RunScript(script, function, args), `OnError(goto: next |
| Интерфейс (3) | MessageBox(text, type, title, buttons) → TempVars.result,
Beep(), Hourglass(on) |
| Ввод и вывод (5) | ExportTo(object, format, path, open),
ImportFrom(path, format, table, mode),
PrintObject(object, range, copies),
SendEmail(to, subject, body, attach_object, format) (через
Mail), `OpenDialog(kind: prompt |
Исполнение: интерпретатор в ядре; If/Else If/Else с
выражениями; on-error; макрос компилируется в тот же AST,
что и скрипт, поэтому «Преобразовать в скрипт» даёт эквивалентный
TypeScript. Условия и аргументы — выражения с доступом к
[Forms]![…]![…], [TempVars]![…],
[CurrentRecord].
5.9 Формы: привязка данных, отслеживание изменений, валидация
FormRuntime:
source = open_cursor(record_source, order_by, filter) // раздел 5.4
record = current row (Map<field, Value>) + original (копия для dirty/undo)
bindings: control.name → Binding { field | expr, format, mask, locked }
on current(row): для каждого binding: control.value = format(row[field]) или eval(expr, row); dirty=false; fire Current
on control.change: pending[control] = parse(control.text, mask, type) → ошибки парсинга → подсветка, запись не грязная
on control.exit: validate_control(control) // маска → тип → правило элемента (ValidationRule) → BeforeUpdate(control)
record[field] = pending; dirty = record != original; fire AfterUpdate(control); пересчитать expr-привязки
on record.exit | save:
if !dirty → ничего
validate_record: правила столбцов (CHECK из meridian_columns.validation_rule; NOT NULL/required), BeforeUpdate(form) (может cancel)
write: UPDATE/INSERT по pk в SAVEPOINT; ошибки БД (UNIQUE, FK, CHECK) → сопоставление с полем → сообщение; при успехе AfterUpdate/AfterInsert, dirty=false
on delete: BeforeDelConfirm → DELETE → AfterDelConfirm; каскады по FK
subform: link-master/link-child → при смене записи родителя: subform.filter = child = master_value; новые записи получают child=master
requery: переоткрыть курсор, вернуться к записи по pk
combobox row-source: ленивый курсор; «не в списке» → NotInList
вычисляемые элементы (expr): пересчёт при изменении зависимых полей (граф зависимостей выражений, как в Sheets)
Маски ввода — синтаксис Access (000-00-00;0;_),
числа/даты парсятся по локали (ICU4X), пустая строка → NULL (если
allow_zero_length = 0).
5.10 Отчёты: раскладка полос, итоги за один проход, пагинация
render_report(report, params):
rows = execute(record_source ORDER BY <group levels…>, <sort>) // данные уже отсортированы по группам
needs_two_pass = любой агрегат в group-header или [Pages] в выражениях или итоги «по отчёту» в заголовке
if needs_two_pass: pass1 = scan rows → aggregates[level][group_key] (Sum/Count/…), total_pages неизвестно → рендер с заглушкой и повторный проход только для страниц с [Pages]
page = new_page(); emit(report-header); emit(page-header)
acc = Accumulators { report, page, group[level] } // накопители итогов / running accumulators
prev_keys = none
for row in rows:
keys = [group_expr(level, row) for level] // с интервалами: year(date), left(name,1), floor(x/100)…
// закрытие групп от самого глубокого уровня, где ключ изменился / close groups from the deepest changed level
changed = first level where keys[level] != prev_keys[level]
if prev_keys: for level in reverse(changed..L): emit(group-footer[level], acc.group[level]); acc.group[level].reset()
for level in changed..L: if force_new_page[level]: page_break(); emit(group-header[level], aggregates?.[level][keys[level]])
// область данных / detail
band = layout(detail, row) // can-grow: measure text via text-layout; hide-duplicates
if band.height > page.remaining:
if keep_together(level) and group_buffer.fits_on_new_page(): page_break() // «не разрывать группу»: буферизуем группу до её конца,
else: page_break() // если группа больше страницы — печатаем как есть
emit(band); acc.*.add(row) // Sum/Count/Avg/Min/Max, running-sum (over group|all)
prev_keys = keys
for level in reverse(0..L): emit(group-footer[level])
emit(report-footer); emit(page-footer); finish()
page_break(): emit(page-footer with acc.page) ; acc.page.reset(); page = new_page(); emit(page-header); если repeat-header — повторить group-header текущих уровней
keep_together: «вся группа» — группа буферизуется в память (лимит 5 000 строк, иначе деградация до «с первой записью»);
«с первой записью» — заголовок группы не остаётся последним на странице (widow control)
columns > 1 (этикетки/многоколоночные): страница делится на колонки, порядок «сверху вниз» или «слева направо»;
подотчёт: render_report(sub, where link-child = link-master) → вставляется как полоса с can-grow;
измерение: единицы отчёта мм → pt; текст — text-layout Write (переносы, шрифты); изображения — по режиму (обрезка/вписать/растянуть)
вывод: display list → Meridian Print (экран/принтер/PDF); [Page]/[Pages] — подстановка на финальном проходе
Один проход покрывает итоги в примечаниях групп/страниц/отчёта и
нарастающие суммы; второй проход нужен только для агрегатов в заголовках
и [Pages] — отчёт на 100 000 строк рендерится потоково,
страницы отдаются в предпросмотр по мере готовности.
5.11 Мастер миграции
.mbx → PostgreSQL
- Анализ: таблицы, типы (таблица 4.3 → PG:
TEXT,INTEGER/BIGINT,DOUBLE PRECISION,NUMERIC(p,s),TIMESTAMP,DATE,TIME,BOOLEAN,BYTEA,UUID,JSONB,GENERATED ALWAYS AS … STORED), ключи, индексы, связи, значения по умолчанию (выражения → PG), CHECK; запросы — прогон через транслятор диалекта с отчётом несовместимостей; размер и оценка времени. - Создание схемы в транзакции (
CREATE SCHEMA, идентификаторы — в нижнем регистре с кавычками при необходимости, сопоставление имён сохраняется вmeridian_linked_tables). - Перенос данных:
COPY … FROM STDIN(binary) пакетами по 10 000 строк, последовательности выставляются (setval), FK создаются после данных; прогресс и возобновление после обрыва (по таблицам). - Перелинковка: локальные таблицы переименовываются в
_local_<name>(или удаляются по выбору), создаются связанные таблицы; формы/отчёты/запросы продолжают работать по именам. - Проверка: COUNT по таблицам, контрольные суммы по выборке, отчёт.
5.12 MeridianScript API (DAO-подобный)
Глобальный объект App (Base). Все вызовы к данным
асинхронные (await); синхронные обёртки для выражений форм
(DLookup и т.п.) предоставляются движком выражений, а не
скриптом.
interface BaseApp {
currentDb(): Database; // CurrentDb()
openDatabase(path: string, opts?: { password?: string; readOnly?: boolean }): Promise<Database>;
forms: Record<string, Form> & { open(name: string, opts?: OpenFormOptions): Promise<Form> }; // Forms!…
reports: Record<string, Report> & { open(name: string, opts?: OpenReportOptions): Promise<Report> };
tempVars: Record<string, unknown>;
ui: { message(text: string, opts?: { type?: 'info'|'warning'|'error'|'question'; buttons?: ('ok'|'cancel'|'yes'|'no')[] }): Promise<string>;
prompt(label: string, def?: string): Promise<string | null>; status(text: string): void; hourglass(on: boolean): void };
on(event: 'databaseOpened' | 'beforeQuit', h: (e: any) => void): Disposable;
}
interface Database {
name: string; path: string; tableDefs: TableDef[]; queryDefs: QueryDef[]; relations: Relation[];
openRecordset(source: string, opts?: { type?: 'dynaset'|'snapshot'|'table'; params?: Record<string, unknown>; readOnly?: boolean }): Promise<Recordset>;
execute(sql: string, params?: unknown[] | Record<string, unknown>): Promise<{ affected: number; lastId?: number }>;
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>; // небольшие выборки целиком / small result sets
lookup(field: string, table: string, where?: string): Promise<unknown>; // DLookup
count(table: string, where?: string): Promise<number>; // DCount
sum(field: string, table: string, where?: string): Promise<number>; // DSum (также avg/min/max)
transaction<T>(fn: (tx: Database) => Promise<T>): Promise<T>; // BeginTrans/CommitTrans
createTableDef(def: TableDefSpec): Promise<TableDef>; createQueryDef(name: string, sql: string): Promise<QueryDef>;
links: LinkedSource[]; close(): Promise<void>;
}
interface Recordset {
fields: Record<string, Field>; eof: boolean; bof: boolean; recordCount: number; absolutePosition: number; bookmark: unknown;
moveFirst(): Promise<void>; moveLast(): Promise<void>; moveNext(): Promise<void>; movePrevious(): Promise<void>; move(n: number): Promise<void>;
findFirst(criteria: string): Promise<boolean>; findNext(criteria: string): Promise<boolean>; seek(index: string, ...values: unknown[]): Promise<boolean>;
edit(): void; addNew(): void; update(): Promise<void>; cancelUpdate(): void; delete(): Promise<void>;
requery(): Promise<void>; filter: string; sort: string; clone(): Recordset; close(): Promise<void>;
[Symbol.asyncIterator](): AsyncIterator<Record<string, unknown>>; // for await (const row of rs)
}
interface Field { name: string; type: string; value: unknown; originalValue: unknown; size: number; required: boolean; }
interface Form {
name: string; controls: Record<string, Control>; recordset: Recordset; recordSource: string; filter: string; filterOn: boolean; orderBy: string;
dirty: boolean; newRecord: boolean; currentRecord: number; caption: string; visible: boolean; view: 'form'|'datasheet'|'layout'|'design';
requery(): Promise<void>; refresh(): Promise<void>; recalc(): void; undo(): void; save(): Promise<void>; close(save?: boolean): Promise<void>;
gotoRecord(where: 'first'|'last'|'next'|'previous'|'new' | number): Promise<void>; setFocus(): void;
on(event: FormEvent, h: (e: FormEventArgs) => void | Promise<void>): Disposable; // 'open'|'load'|'current'|'beforeUpdate'|'afterUpdate'|'delete'|'close'|...
parent: Form | null; subforms: Record<string, Form>; openArgs: unknown;
}
interface Control {
name: string; type: string; value: unknown; text: string; visible: boolean; enabled: boolean; locked: boolean;
controlSource: string; rowSource: string; format: string; backColor: string; foreColor: string; fontWeight: string;
left: number; top: number; width: number; height: number; tag: string; itemsSelected: unknown[]; listCount: number;
setFocus(): void; requery(): Promise<void>; undo(): void;
on(event: ControlEvent, h: (e: ControlEventArgs) => void | Promise<void>): Disposable; // 'click'|'dblClick'|'change'|'beforeUpdate'|'afterUpdate'|'enter'|'exit'|'gotFocus'|'lostFocus'|'keyDown'|'notInList'|...
}
interface Report { name: string; recordSource: string; filter: string; orderBy: string; controls: Record<string, Control>; page: number; pages: number;
on(event: 'open'|'format'|'print'|'noData'|'page'|'close', h: (e: { band: string; cancel: boolean }) => void): Disposable;
print(opts?: { copies?: number; printer?: string }): Promise<void>; exportPdf(path: string): Promise<void>; close(): Promise<void>; }Примеры:
// 1. Пересчёт остатков по складу в транзакции / Recalculate stock in a transaction
const db = App.currentDb();
await db.transaction(async tx => {
const rs = await tx.openRecordset('SELECT id, sku FROM products');
for await (const p of rs) {
const inQty = await tx.sum('qty', 'moves', `product_id = ${p.id} AND kind = 'in'`);
const outQty = await tx.sum('qty', 'moves', `product_id = ${p.id} AND kind = 'out'`);
await tx.execute('UPDATE products SET stock = ? WHERE id = ?', [inQty - outQty, p.id]);
}
});
// 2. Событие формы: запрет сохранения заказа без позиций / Form event: block saving an order without items
const frm = App.forms['Заказы'];
frm.on('beforeUpdate', async e => {
const n = await App.currentDb().count('order_items', `order_id = ${frm.controls.txtId.value}`);
if (n === 0) { e.cancel = true; await App.ui.message('Добавьте хотя бы одну позицию', { type: 'warning' }); frm.controls.sfItems.setFocus(); }
});
// 3. Кнопка «Не в списке»: создать клиента на лету / NotInList: create a customer on the fly
frm.controls.cboCustomer.on('notInList', async e => {
const ok = await App.ui.message(`Клиента «${e.newData}» нет. Создать?`, { type: 'question', buttons: ['yes', 'no'] });
if (ok === 'yes') { await App.currentDb().execute('INSERT INTO customers(name) VALUES (?)', [e.newData]); e.response = 'added'; }
else e.response = 'continue';
});
// 4. Отчёт в PDF и письмо / Report to PDF and e-mail via Mail
const rep = await App.reports.open('Счёт', { where: `id = ${frm.controls.txtId.value}`, view: 'hidden' });
await rep.exportPdf('~/Документы/Счёт.pdf');
await Meridian.mail.compose({ to: frm.controls.txtEmail.value as string, subject: 'Счёт', attachments: ['~/Документы/Счёт.pdf'] });Таблица переноса VBA/DAO → MeridianScript:
| VBA / DAO / Access | MeridianScript |
|---|---|
CurrentDb() |
App.currentDb() |
Set rs = db.OpenRecordset("SELECT …") |
const rs = await db.openRecordset('SELECT …') |
Do Until rs.EOF … rs.MoveNext … Loop |
for await (const row of rs) { … } |
rs!Field / rs.Fields("Field").Value |
rs.fields.Field.value или row.Field |
rs.Edit … rs.Update |
rs.edit(); rs.fields.x.value = …; await rs.update() |
rs.AddNew … rs.Update |
rs.addNew(); …; await rs.update() |
rs.FindFirst "id = 5" / rs.NoMatch |
const found = await rs.findFirst('id = 5') |
db.Execute "UPDATE …", dbFailOnError |
await db.execute('UPDATE …') (ошибки — исключения) |
DoCmd.RunSQL |
await db.execute(sql) |
DoCmd.OpenForm "frm", , , "id=5" |
await App.forms.open('frm', { where: 'id=5' }) |
DoCmd.OpenReport "rep", acViewPreview |
await App.reports.open('rep', { view: 'preview' }) |
DoCmd.Close acForm, "frm" |
await App.forms.frm.close() |
DoCmd.GoToRecord , , acNewRec |
await form.gotoRecord('new') |
Me.txtName.Value / Me!txtName |
form.controls.txtName.value |
Me.Requery / Me.Refresh /
Me.Dirty |
await form.requery() /
await form.refresh() / form.dirty |
Forms!frmA!ctl |
App.forms.frmA.controls.ctl |
Me.Parent / Me.sfItems.Form |
form.parent / form.subforms.sfItems |
DLookup("name","customers","id=5") |
await db.lookup('name', 'customers', 'id=5') |
DCount / DSum / DAvg /
DMax |
db.count / db.sum / db.avg /
db.max |
Nz(x, 0) |
x ?? 0 |
IIf(c, a, b) |
c ? a : b |
MsgBox "…", vbYesNo |
await App.ui.message('…', { buttons: ['yes','no'] }) |
InputBox |
await App.ui.prompt(…) |
TempVars!x |
App.tempVars.x |
Format(d, "dd.MM.yyyy") |
Meridian.fmt.date(d, 'dd.MM.yyyy') |
Date() / Now() |
Meridian.fmt.today() / new Date() |
On Error Resume Next / On Error GoTo |
try { … } catch (e) { … } |
BeginTrans / CommitTrans / Rollback |
await db.transaction(async tx => { … }) |
DoCmd.SetWarnings False |
не требуется (подтверждения — только в UI-действиях) |
DoCmd.TransferSpreadsheet |
await db.import({ format: 'xlsx', path, table }) /
db.export(...) |
Application.Quit |
App.quit() |
Private Sub btn_Click() |
form.controls.btn.on('click', …) |
Form_BeforeUpdate(Cancel) |
form.on('beforeUpdate', e => { e.cancel = true }) |
Транслятор VBA → MeridianScript (Meridian Script, библия 4) покрывает эти соответствия автоматически для простых модулей; остальное — вручную по таблице.
6. Форматы
6.1 Свой формат .mbx
SQLite-база (раздел 4.1). Совместимость: любой SQLite ≥ 3.37 читает
данные; служебные таблицы meridian_* игнорируются чужими
инструментами. Версионирование — user_version; миграции
схемы метаданных — вперёд автоматически с резервной копией.
Зашифрованный — SQLCipher 4. Плоского/шаблонного варианта нет; «шаблон
базы» = .mbx без данных (галерея Hub хранит их как обычные
файлы).
6.2 Чтение
.accdb / .mdb (парсер Jet/ACE,
codecs/accdb/)
Собственный парсер на Rust (референсы: mdbtools, Jackcess, спецификации сообщества). Только чтение.
| Элемент | Описание |
|---|---|
| Версии | Jet 3 (Access 97, .mdb, страницы 2048
байт), Jet 4 (Access 2000–2003, .mdb,
4096), ACE 12/14/16 (Access 2007–2019/365,
.accdb, 4096); версия — байт 0x14 страницы 0
(0x00 Jet3, 0x01 Jet4, 0x02 ACE
2007, 0x03 ACE 2010, 0x05 ACE 2016) |
| Страница 0 (Database Definition) | сигнатура 00 01 00 00 +
"Standard Jet DB"/"Standard ACE DB"; участок
заголовка со смещения 0x18 (126/128 байт) замаскирован RC4
с фиксированным, общеизвестным ключом (разным для Jet 3 и Jet 4/ACE) —
снимаем маску; из заголовка: кодовая страница (Jet 3), сортировка,
пароль базы (Jet: хранится в заголовке XOR-ом — для чтения не
требуется), ключ шифрования |
| Типы страниц (байт 0) | 0x00 определение БД, 0x01 страница данных,
0x02 определение таблицы (TDEF), 0x03
промежуточная страница индекса, 0x04 листовая страница
индекса, 0x05 карта использования (usage bitmap) |
| TDEF | длина определения (с продолжением на следующих TDEF-страницах),
число строк, значение автонумерации, тип таблицы (0x4E
пользовательская, 0x53 системная), число
столбцов/индексов/«реальных» индексов, указатель на карту использования
страниц таблицы, карту свободного места; столбцы: тип (1 байт), номер,
смещение (fixed/variable), флаги (0x01 fixed,
0x02 can be null, 0x04 autonumber long,
0x10 auto GUID, 0x20 hyperlink,
0x40 compressed Unicode, 0x80 ... ), длина;
имена столбцов (Jet 3 — ANSI, Jet 4/ACE — UCS-2 LE с 2-байтной длиной);
индексы: столбцы с порядком, флаги (0x01 unique,
0x02 ignore nulls, 0x08 required,
0x10 primary), тип (обычный/первичный/внешний) |
| Типы столбцов | 0x01 BOOL (бит в null-маске), 0x02 BYTE,
0x03 INT16, 0x04 INT32, 0x05
MONEY (8 байт, ×10⁴), 0x06 FLOAT32, 0x07
FLOAT64, 0x08 DATETIME (double: дни от 1899-12-30),
0x09 BINARY, 0x0A TEXT (Jet 4: UCS-2 с
«компрессией» — префикс FF FE и переключение 0x00),
0x0B OLE, 0x0C MEMO, 0x0F GUID
(REPID), 0x10 NUMERIC (17 байт: знак, scale, precision, 16
байт мантиссы), 0x12 COMPLEX (ACE: вложения, многозначные,
история версий — ссылки на скрытые таблицы
f_<guid>_…), 0x13 BIGINT (ACE 2016),
0x14 DATETIME EXTENDED |
| Страница данных | заголовок (тип, свободное место, указатель TDEF, число записей), в
конце страницы — таблица смещений записей (2 байта; старшие биты:
удалена 0x8000, lookup 0x4000); запись: число
столбцов (Jet 3 — 1 байт, Jet 4 — 2), фиксированные столбцы, переменные
(Jet 3 — jump-таблица, Jet 4 — таблица смещений с конца), null-маска
(бит на столбец, 1 = не NULL); LVAL (MEMO/OLE/длинный BINARY): 12 байт —
длина и флаги (0x80000000 inline, 0x40000000
одна LVAL-страница, иначе цепочка), указатель страница/строка;
LVAL-страницы — тип 0x01 с меткой LVAL |
| Карты использования | глобальная (страница 1 и далее) и у каждой таблицы: формат «битовая карта от стартовой страницы» или «список страниц-карт»; по ней перечисляются все страницы данных таблицы (индексы для полного чтения не нужны) |
Индексы (страницы 0x03/0x04) |
B+-дерево, листовые страницы с префиксно-сжатыми ключами; читаются только для извлечения метаданных (сами индексы пересоздаются по TDEF) |
| Системные таблицы | MSysObjects (Id, Name, Type: 1 таблица,
3 контейнер, 4 связанная ODBC, 5
запрос, 6 связанная таблица, 8 ... ;
отрицательные — объекты Access: -32768 форма,
-32764 отчёт, -32766 макрос,
-32761 модуль, -32758 ... ; ParentId, Flags
(0x2/0x80000000 скрытые/системные),
DateCreate/DateUpdate, Database/Connect для связанных, LvProp — свойства
(подписи столбцов, форматы, маски, описания — разбираем бинарный формат
«property blocks»)), MSysQueries (строки запроса: ObjectId,
Attribute, Expression, Flag, Name1, Name2, Order; атрибуты:
0 флаги запроса/тип
(SELECT/UPDATE/APPEND/DELETE/CROSSTAB/MAKETABLE/UNION/PASSTHROUGH/DDL),
3 параметры (Name1, Flag=тип), 5 таблицы
(Name1=таблица, Name2=псевдоним), 6 столбцы (Expression,
Name1=псевдоним, Flag=«показывать»), 7 соединения
(Expression=условие, Flag=тип 1/2/3 inner/left/right), 8
WHERE, 9 GROUP BY, 10 HAVING, 11
ORDER BY, 254 — crosstab заголовки/значение),
MSysRelationships (szRelationship, szObject (дочерняя),
szColumn, szReferencedObject, szReferencedColumn, ccolumn, icolumn,
grbit — DAO RelationAttributeEnum: 1 unique,
2 don't enforce, 4 inherited, 256
cascade update, 4096 cascade delete, 16777216
left join, 33554432 right join), MSysACEs
(права — игнорируются), MSysNavPaneGroups* (группы
навигатора → meridian_objects.group_id),
MSysComplexColumns/MSysComplexType_*
(многозначные и вложения) |
| Формы/отчёты/макросы/модули | хранятся бинарными потоками (OLE-подобные
«LvModule»/MSysAccessStorage в ACE), формат не
документирован. Конвертер (P1, с ограничениями): имя, тип, источник
записей (RecordSource/RowSource по сигнатурам
свойств), список элементов управления с
именем/типом/привязкой/геометрией — через распознавание блоков свойств;
события и VBA — как текст модуля (из LvModule, если не
защищён) для ручного переноса по таблице 5.12; всё, что не распознано —
в отчёт импорта |
| Шифрование | .mdb с паролем — читается (пароль не нужен для
разбора); .accdb с шифрованием Office (RC4 2007 / AES
2010+, структура как в OOXML Agile encryption, ключ из пароля) —
поддержка пароля P1; Jet-«encrypted database» (RC4 всех страниц ключом
из заголовка) — поддерживается |
| Кодировки/даты | Jet 3 — ANSI по кодовой странице заголовка (cp1251 для русских баз);
Jet 4/ACE — UCS-2; даты → ISO-8601, MONEY →
currency, NUMERIC → decimal |
Что переносится: таблицы (структура, данные, ключи, индексы, значения
по умолчанию, условия на значения, подписи/описания/форматы/маски из
свойств), связи (с целостностью/каскадами/типом соединения), запросы
(через MSysQueries → QBE/SQL с переводом Jet-SQL:
* подстановка в LIKE → %, #дата#
→ строка ISO, функции IIf/Nz/Format/DateDiff/… → движок
выражений, параметры), подстановки (из свойств столбцов
RowSource/BoundColumn/ColumnWidths), вложения и
многозначные поля (через COMPLEX), группы навигатора.
Формы/отчёты — ограниченно (см. выше); пароли/права пользователей
(.mdw) — нет.
6.3 Другие форматы
| Формат | Чтение | Запись | Особенности |
|---|---|---|---|
.sqlite/.db |
да | да | открытие «как есть» или связывание; служебные таблицы создаются по согласию |
.odb (LibreOffice Base) |
да (R*) | — | ZIP: database/ — встроенный HSQLDB 1.8
(script — SQL DDL/INSERT → разбираем;
data/backup — не нужны) или Firebird
(firebird.fbk — через встроенный Firebird-клиент, P2);
content.xml — запросы (SQL) и связи (XML);
forms/, reports/ — документы ODF с привязками:
переносим источник, элементы и геометрию, события Basic — нет |
Sheets .msx / Excel .xlsx |
да | да | лист/диапазон/таблица → таблица; типы по первым 1 000 строкам; «связать» — только чтение |
| CSV/TSV | да | да | RFC 4180 + вариации; кодировка (UTF-8/BOM, cp1251, определение), разделитель, кавычки, даты по локали |
| JSON / NDJSON | да | да | массив объектов → таблица, вложенные объекты → столбцы
a.b / отдельные таблицы, массивы → таблицы связи |
| XML | да | да | повторяющиеся элементы → строки, атрибуты/дочерние → столбцы; XSD-типы при наличии |
| SQL-дамп | — | да | DDL+DML в диалекте (SQLite/PostgreSQL/MySQL/MS SQL), транзакции, пакетные INSERT |
| — | да | отчёты через Meridian Print (PDF/A) | |
| HTML | — | да | таблицы/отчёты |
| ODBC/PostgreSQL/MySQL/MS SQL | да | да | связанные таблицы, импорт/экспорт, миграция |
7. Горячие клавиши
Схема по умолчанию — Access.
| Действие | Клавиши |
|---|---|
| Навигатор объектов показать/скрыть | F11 |
| Новая таблица / запрос / форма / отчёт | Ctrl+T / Ctrl+Q /
Ctrl+Shift+F / Ctrl+Shift+R |
| Открыть объект / конструктор объекта | Enter / Ctrl+Enter в навигаторе |
| Переключить режим (таблица ↔︎ конструктор, форма ↔︎ конструктор) | Ctrl+Shift+D (цикл) |
| Сохранить объект / сохранить как | Ctrl+S / F12 |
| Выполнить запрос / показать SQL / конструктор | Ctrl+R (или F5 в SQL) /
Ctrl+Shift+Q / Ctrl+Shift+E |
| Новая запись / удалить запись / сохранить запись | Ctrl++ / Ctrl+- /
Shift+Enter |
| Перейти к записи № / первая / последняя | F5 (в сетке) / Ctrl+Home /
Ctrl+End |
| Следующее/предыдущее поле; следующая/предыдущая запись | Tab/Shift+Tab;
↓/↑ (в форме
PgDn/PgUp) |
| Режим редактирования ячейки / выход | F2 / Esc (первое — поле, второе —
запись) |
| Копировать значение из предыдущей записи | Ctrl+' |
| Вставить текущую дату / время / значение по умолчанию | Ctrl+; / Ctrl+Shift+; /
Ctrl+Alt+Space |
| Увеличить поле (Zoom) | Shift+F2 |
| Раскрыть список (поле со списком) | F4 или Alt+↓ |
| Найти / заменить / найти далее | Ctrl+F / Ctrl+H /
Shift+F4 |
| Фильтр по выделению / снять фильтр / фильтр по форме | Ctrl+Shift+L (контекст) / Ctrl+Shift+Alt+L
/ Ctrl+Shift+F (в сетке) |
| Сортировка по возрастанию / убыванию | Ctrl+Shift+↑ / Ctrl+Shift+↓ |
| Обновить (Refresh) / Запросить заново (Requery) | F9 / Shift+F9 |
| Отмена / повтор | Ctrl+Z / Ctrl+Y |
| Проверка орфографии | F7 |
| Схема данных | Ctrl+Shift+S |
| Свойства (панель) / список полей | F4 (в конструкторе) / Alt+F8 |
| Конструктор: выделить всё / выравнивание по сетке / порядок обхода | Ctrl+A / Ctrl+Shift+G /
Ctrl+Shift+T |
| Конструктор: сдвиг на 1 px / на шаг сетки; размер | Ctrl+Стрелки / Стрелки;
Shift+Стрелки |
| Предпросмотр отчёта / печать / PDF | Ctrl+Shift+P / Ctrl+P /
Ctrl+Shift+E |
| Редактор скриптов / выполнить макрос | Alt+F11 / Alt+F8 |
| Командная палитра | Ctrl+K |
| Закрыть объект / закрыть базу | Ctrl+W / Ctrl+Shift+W |
8. Критерии готовности
8.1 Base 1.0 (P0 закрыты)
- Таблица на 1 000 000 строк × 20 столбцов: открытие сетки ≤ 300 мс, прокрутка 60 к/с, сортировка по индексированному столбцу ≤ 200 мс, фильтр по тексту (LIKE) ≤ 2 с, правка ячейки ≤ 50 мс.
- QBE ↔︎ SQL: 500 запросов корпуса (Northwind, свои) — генерация корректна (сравнение результатов с эталонным SQL), обратный разбор для ≥ 90 % запросов корпуса; перекрёстные запросы на SQLite/PostgreSQL/MS SQL дают одинаковый результат.
- Формы: 30 эталонных форм (одиночные, ленточные, с подформами, вкладками, списками) — привязка, валидация, события, подформы; ввод 10 000 записей оператором без ошибок состояния «грязной» записи (автотест).
- Отчёты: 30 эталонных отчётов (группы 3 уровней, итоги, нарастающие, подотчёты, этикетки, многоколоночные) — снимочные тесты PDF; отчёт на 100 000 строк — первая страница ≤ 1 с, полный PDF ≤ 60 с.
- Макросы: все 40 действий покрыты тестами; «преобразовать в скрипт» даёт эквивалентное поведение.
- Импорт
.accdb/.mdb: корпус 100 баз (Access 97–365, включая Northwind всех версий, cp1251-базы) — 100 % таблиц и данных, 100 % связей, ≥ 95 % запросов исполняются; защищённые паролем.mdb; отчёт импорта. - Коннекторы: PostgreSQL — связанные таблицы, запросы, транзакции, формы на серверных данных (тест на PG 12–17); миграция Northwind в PostgreSQL ≤ 2 мин с совпадением COUNT и контрольных сумм.
- SQLCipher: шифрование/смена пароля; файл не читается
sqlite3без пароля (тест). - Изменение структуры таблицы с 1 М строк (добавить/удалить/переименовать столбец, сменить тип) с сохранением индексов и связей ≤ 30 с, резервная копия создаётся.
- MeridianScript: примеры 5.12 работают; транслятор VBA покрывает таблицу соответствий.
- Многопользовательский: два клиента на одном
.mbxв WAL (локальный диск) — чтение/запись без порчи; на SMB — предупреждение; мастер миграции доступен из предупреждения. - Снимочные UI-тесты экранов BS-01…BS-26 на трёх ОС; доступность сетки и форм (экранный диктор).
8.2 Base 1.x (P1)
MySQL/MariaDB, MS SQL, ODBC; смешанные запросы через виртуальные
таблицы; конвертер форм/отчётов Access; .odb; журнал
изменений; FTS5; условное форматирование; интерактивные отчёты;
связанные CSV/Excel; документация базы; штрихкоды в отчётах.
9. Риски
| Риск | Вероятность | Влияние | Снижение |
|---|---|---|---|
| Формат Jet/ACE восстановлен сообществом, не документирован Microsoft; редкие структуры (COMPLEX, ACE 2016 BIGINT, шифрование) | высокая | неполный импорт некоторых баз | корпус из 100+ реальных баз, сверка с mdbtools/Jackcess в CI, отчёт импорта с перечнем непрочитанного, фаззинг |
| Формы/отчёты Access не переносятся полностью | высокая | ожидание пользователей «открыть accdb как есть» не оправдывается | честное обозначение «конвертер с ограничениями» в библии 3.3 (R*), мастер «пересоздать форму по источнику» (автоформа), таблица переноса VBA |
Семантика выражений Access (Jet SQL, Nz, даты,
Format) ≠ SQL диалектов |
средняя | запросы работают иначе | единый движок выражений с тестами совместимости, клиентская пост-обработка, предупреждения при переводе |
SQLite — один писатель; пользователи кладут .mbx на
сетевой диск (как привыкли с Access) |
высокая | блокировки/порча | детекция сетевого пути при открытии, предупреждение, разделение базы, мастер миграции в PostgreSQL |
| Диалекты серверных СУБД (типы, функции, PIVOT, кавычки, регистр) | средняя | ошибки на серверных источниках | транслятор AST → диалект с матрицей тестов (Docker: PG, MySQL, MS SQL в CI) |
| Производительность форм с подформами и вычисляемыми элементами на серверных источниках (сетевые задержки) | средняя | медленные формы | пакетные выборки, кэш подстановок, асинхронные запросы с
индикатором, ограничение requery |
| Транзакции и undo для массовых операций | низкая | «не отменить» | явные подтверждения с числом строк, before-image до лимита |
Ожидание Access-подобного Me.-синтаксиса в
скриптах |
средняя | порог входа | транслятор VBA, сниппеты, таблица 5.12, автодополнение |
| Редакционная граница (Base только в Pro) ограничивает «приложения на Base» для конечных пользователей | средняя | компании не могут раздать форму ввода сотрудникам с Home | Feature::BaseRuntime в Home (вопрос 10.1) |
10. Вопросы к библии
- Редакция для исполнения баз. Base входит только в
Pro+. Предлагаем
Feature::BaseRuntimeдля Home (и, возможно, Free): открытие.mbxв режиме «приложение» — формы, отчёты, ввод данных без конструкторов. Требует правки таблицы 9.1. - Единый движок выражений. Формы/отчёты/запросы Base
используют язык выражений Sheets (
[Поле],IIf,Nz,Format…) с Access-совместимыми функциями — предлагаем зафиксировать в библии, чтоcalc-engineядра содержит слой совместимости с выражениями Access/VBA (как и с Excel). - Пространство имён. В таблице 3.2 у Base нет
пространства имён (формы/запросы/отчёты как XML). Используем
urn:meridian:office:base:1для XML в служебных таблицах — просим добавить в таблицу. - SQLCipher — лицензия BSD, но сборка требует
отдельного варианта SQLite; решение о статической сборке в
meridian-dbдля всех платформ стоит отразить в03-architecture.md.