Meridian Office

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=' &amp; [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=' &amp; [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="='Стр. ' &amp; [Page] &amp; ' из ' &amp; [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, если запрос входит в поддерживаемое подмножество:

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 Транзакции и отмена

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

  1. Анализ: таблицы, типы (таблица 4.3 → PG: TEXT, INTEGER/BIGINT, DOUBLE PRECISION, NUMERIC(p,s), TIMESTAMP, DATE, TIME, BOOLEAN, BYTEA, UUID, JSONB, GENERATED ALWAYS AS … STORED), ключи, индексы, связи, значения по умолчанию (выражения → PG), CHECK; запросы — прогон через транслятор диалекта с отчётом несовместимостей; размер и оценка времени.
  2. Создание схемы в транзакции (CREATE SCHEMA, идентификаторы — в нижнем регистре с кавычками при необходимости, сопоставление имён сохраняется в meridian_linked_tables).
  3. Перенос данных: COPY … FROM STDIN (binary) пакетами по 10 000 строк, последовательности выставляются (setval), FK создаются после данных; прогресс и возобновление после обрыва (по таблицам).
  4. Перелинковка: локальные таблицы переименовываются в _local_<name> (или удаляются по выбору), создаются связанные таблицы; формы/отчёты/запросы продолжают работать по именам.
  5. Проверка: 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
PDF — да отчёты через 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. Таблица на 1 000 000 строк × 20 столбцов: открытие сетки ≤ 300 мс, прокрутка 60 к/с, сортировка по индексированному столбцу ≤ 200 мс, фильтр по тексту (LIKE) ≤ 2 с, правка ячейки ≤ 50 мс.
  2. QBE ↔︎ SQL: 500 запросов корпуса (Northwind, свои) — генерация корректна (сравнение результатов с эталонным SQL), обратный разбор для ≥ 90 % запросов корпуса; перекрёстные запросы на SQLite/PostgreSQL/MS SQL дают одинаковый результат.
  3. Формы: 30 эталонных форм (одиночные, ленточные, с подформами, вкладками, списками) — привязка, валидация, события, подформы; ввод 10 000 записей оператором без ошибок состояния «грязной» записи (автотест).
  4. Отчёты: 30 эталонных отчётов (группы 3 уровней, итоги, нарастающие, подотчёты, этикетки, многоколоночные) — снимочные тесты PDF; отчёт на 100 000 строк — первая страница ≤ 1 с, полный PDF ≤ 60 с.
  5. Макросы: все 40 действий покрыты тестами; «преобразовать в скрипт» даёт эквивалентное поведение.
  6. Импорт .accdb/.mdb: корпус 100 баз (Access 97–365, включая Northwind всех версий, cp1251-базы) — 100 % таблиц и данных, 100 % связей, ≥ 95 % запросов исполняются; защищённые паролем .mdb; отчёт импорта.
  7. Коннекторы: PostgreSQL — связанные таблицы, запросы, транзакции, формы на серверных данных (тест на PG 12–17); миграция Northwind в PostgreSQL ≤ 2 мин с совпадением COUNT и контрольных сумм.
  8. SQLCipher: шифрование/смена пароля; файл не читается sqlite3 без пароля (тест).
  9. Изменение структуры таблицы с 1 М строк (добавить/удалить/переименовать столбец, сменить тип) с сохранением индексов и связей ≤ 30 с, резервная копия создаётся.
  10. MeridianScript: примеры 5.12 работают; транслятор VBA покрывает таблицу соответствий.
  11. Многопользовательский: два клиента на одном .mbx в WAL (локальный диск) — чтение/запись без порчи; на SMB — предупреждение; мастер миграции доступен из предупреждения.
  12. Снимочные 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. Вопросы к библии

  1. Редакция для исполнения баз. Base входит только в Pro+. Предлагаем Feature::BaseRuntime для Home (и, возможно, Free): открытие .mbx в режиме «приложение» — формы, отчёты, ввод данных без конструкторов. Требует правки таблицы 9.1.
  2. Единый движок выражений. Формы/отчёты/запросы Base используют язык выражений Sheets ([Поле], IIf, Nz, Format…) с Access-совместимыми функциями — предлагаем зафиксировать в библии, что calc-engine ядра содержит слой совместимости с выражениями Access/VBA (как и с Excel).
  3. Пространство имён. В таблице 3.2 у Base нет пространства имён (формы/запросы/отчёты как XML). Используем urn:meridian:office:base:1 для XML в служебных таблицах — просим добавить в таблицу.
  4. SQLCipher — лицензия BSD, но сборка требует отдельного варианта SQLite; решение о статической сборке в meridian-db для всех платформ стоит отразить в 03-architecture.md.