open-sdbl — библиотека и интерактивная консоль для запросов к информационным
базам 1С на PostgreSQL и Microsoft SQL Server. Проект читает служебные
метаданные 1С, связывает имена объектов и реквизитов с физической схемой, а
затем преобразует запросы SDBL в SQL.
Ядро open-sdbl не выполняет I/O и не имеет production-зависимостей. Оно
декодирует переданные приложением DBNames, Config и SchemaStorage, строит
снимок метаданных и генерирует SQL в выбранном диалекте. Подключения к СУБД,
транзакции, чтение метаданных и прав доступа вынесены в библиотеку
open-sdbl-db, которой может пользоваться любое приложение; интерактивный
терминал, разбор командной строки и работа с паролями остались в приложении
open-sdbl-cli.
Поддерживаются обе раскладки служебных таблиц платформы. В базах 8.2 и ранних
8.3 (до формата 8.3.8) Params/Config не имеют колонки PartNo, а таблиц
ConfigCAS и _ExtensionsRestruct нет; в современных базах крупные ресурсы
разбиты на части с номерами от нуля. Консоль определяет раскладку одним
каталожным запросом (StorageLayout), выбирает подходящие SELECT-выражения,
собирает части ресурса перед распаковкой и пропускает чтение расширений, если их
таблиц нет. База без SchemaStorage отклоняется типизированной ошибкой:
без неё физическую схему восстановить нельзя.
Компилируются только читающие запросы. Построчная сверка с Синтакс-помощником платформы 8.3.27 — в отдельном документе: Язык запросов 1С: статус поддержки в open-sdbl. Для каждой конструкции там указан статус (✅ поддерживается, 🟡 частично, ❌ нет), диагностика компилятора и страница справки, по которой конструкция проверена. Проект развивается и пока не является полной заменой языка запросов платформы 1С.
Описание объекта показывает логическое имя, GUID, физическую таблицу, реквизиты и индексы:
Перед исполнением консоль показывает сгенерированный SQL-запрос и отдельно измеряет генерацию SQL и выполнение в СУБД:
Виртуальные таблицы и представления ссылок компилируются с учётом реальных метаданных информационной базы:
При работе с Microsoft SQL Server консоль генерирует T-SQL, читает данные из таблиц расширений конфигурации и показывает время выполнения на MSSQL:
Требуется Rust 1.85 или новее:
git clone https://github.com/dobpilot/open-sdbl.git
cd open-sdbl
cargo build --releaseОбычная release-сборка из корня создаёт CLI в
target/release/open-sdbl. Для сборки только библиотеки используйте
cargo build --release --package open-sdbl.
Поддерживаются два провайдера:
| Провайдер | Порт | Драйвер | Источник пароля |
|---|---|---|---|
postgres |
5432 | tokio-postgres |
PGPASSWORD, PGPASSFILE, $HOME/.pgpass |
mssql |
1433 | Tiberius/TDS | MSSQL_PASSWORD |
PGPASSFILE="$HOME/.pgpass" ./target/release/open-sdbl console postgres \
--host db.example.local \
--database onec \
--user readerИз PowerShell:
$securePassword = Read-Host "PostgreSQL password" -AsSecureString
$env:PGPASSWORD = [System.Net.NetworkCredential]::new("", $securePassword).Password
try {
.\target\release\open-sdbl.exe console postgres `
--host db.example.local `
--database onec `
--user reader
}
finally {
Remove-Item Env:PGPASSWORD
}Пароль читается из PGPASSWORD, PGPASSFILE или $HOME/.pgpass. Команда
работает через tokio-postgres, не требует установленного psql и выполняет
запросы в проверенной read-only транзакции READ COMMITTED.
TLS включён по умолчанию в режиме verify-full: проверяются цепочка
сертификата и имя сервера. Доступны --sslmode require, verify-ca и
verify-full; значение PGSSLMODE используется, только если флаг не задан.
Для корпоративного или собственного CA укажите
--trust-ca-file company-ca.pem вместе с verify-ca или verify-full;
в этом случае доверие ограничивается сертификатами из указанного файла.
Отключение TLS возможно лишь явной парой
--sslmode disable --insecure-plaintext и не рекомендуется вне изолированной
сети разработки.
Рекомендуется отдельный SQL login, включённый в db_datareader, но не в
db_datawriter, db_owner или серверную роль sysadmin. Консоль этого не
требует и не проверяет: она запрашивает режим «только чтение», оборачивает
чтение в собственную транзакцию и откатывает её, а при неудачном откате
помечает сессию непригодной — компилятор же порождает только SELECT. Права учётной
записи выбирает администратор:
MSSQL_PASSWORD='secret' ./target/release/open-sdbl console mssql \
--host 192.168.122.222 \
--database demo \
--user open_sdbl_readerИз PowerShell:
$securePassword = Read-Host "MSSQL password" -AsSecureString
$env:MSSQL_PASSWORD = [System.Net.NetworkCredential]::new("", $securePassword).Password
try {
.\target\release\open-sdbl.exe console mssql `
--host sql.example.local `
--database demo `
--user open_sdbl_reader
}
finally {
Remove-Item Env:MSSQL_PASSWORD
}Для тестового сервера с самоподписанным сертификатом к команде можно добавить
--trust-server-certificate; этот флаг отключает проверку сертификата и не
подходит для production.
Предпочтительный вариант для частного CA — --trust-ca-file company-ca.pem:
шифрование и проверка сертификата при этом сохраняются.
CLI подключается через TDS, запрашивает ApplicationIntent=ReadOnly и
исполняет только фиксированные metadata-SELECT и SELECT, созданные
компилятором. SQL Server не имеет эквивалента PostgreSQL
READ ONLY для обычной транзакции, поэтому ограниченные права login —
обязательная граница безопасности. Перед каждым чтением CLI проверяет на
сервере @@TRANCOUNT, уровень изоляции и членство в read-only ролях; сессия с
незавершённой транзакцией или write-capable ролью отвергается.
Уровень диалекта T-SQL консоль определяет по SERVERPROPERTY('ProductVersion'):
SQL Server 2008/2008 R2 получают уровень 2008 (без функций 2012 года,
НАЧАЛОПЕРИОДА через DATEADD/DATEDIFF), 2012 и новее — 2012. Выбранный
уровень печатается при старте консоли (MSSQL dialect: 2008 (server 10.50.6000.34)), а флаг --mssql-dialect 2008|2012 переопределяет детекцию.
Поддерживаются SQL Server 2008 R2 и новее и PostgreSQL 13 и новее — версии,
которые поставляет 1С. Чтение каталога сохраняет запасной вариант без
LATERAL для серверов до 9.4, но генерируемый SQL рассчитан на 13+.
Смещение дат 1С читается из dbo._YearOffset: консоль автоматически преобразует
физические MSSQL datetime-значения и литералы в логические даты 1С.
Системная колонка _Version (timestamp/rowversion) проецируется без
серверного CAST/CONVERT; CLI отображает полученные восемь байт как
0x0123456789ABCDEF.
Такое значение можно использовать как нативный бинарный литерал в фильтре:
ГДЕ Version > 0x00000000000007D6. После 0x требуется ненулевое чётное
число шестнадцатеричных цифр.
Если расширение конфигурации 1С перенаправило строки объекта в таблицу с
суффиксом X/X1, MSSQL-компилятор автоматически объединяет её с канонической
таблицей. Это применяется к обычным источникам, разыменованию и функциям
ПРЕДСТАВЛЕНИЕ()/ПРЕДСТАВЛЕНИЕССЫЛКИ().
Пользователю базы нужны SELECT на схему dbo и видимость определений
объектов для чтения sys.tables, sys.columns и sys.indexes; не
добавляйте его в
db_datawriter и не выдавайте ALTER, CONTROL или EXECUTE.
Сертификат TLS проверяется по умолчанию. Опциональную integration-проверку метаданных можно запустить так:
OPEN_SDBL_MSSQL_TEST_USER=open_sdbl_reader MSSQL_PASSWORD='secret' \
cargo test -p open-sdbl-db reads_metadata_from_the_mssql_demo_database \
-- --ignoredПо умолчанию тест использует 192.168.122.222:1433/demo; хост, порт и базу можно
переопределить через OPEN_SDBL_MSSQL_TEST_HOST,
OPEN_SDBL_MSSQL_TEST_PORT и OPEN_SDBL_MSSQL_TEST_DATABASE.
Пароли не принимаются аргументами командной строки. При старте CLI забирает
PGPASSWORD, MSSQL_PASSWORD и SOCKS5_PASSWORD в очищаемую память и удаляет
переменные из окружения процесса. Файл .pgpass читается через один открытый
дескриптор; на Unix он должен быть обычным файлом текущего пользователя с
правами 0600 или строже.
Оба провайдера поддерживают --socks5-proxy HOST:PORT. Для прокси с
username/password укажите --socks5-user USER, а пароль передайте только через
SOCKS5_PASSWORD. Безопасность соединения с базой по-прежнему определяется
TLS-режимом провайдера: SOCKS5 сам по себе не заменяет TLS.
При запуске в терминале загрузка метаданных показывает progress bar с фазой,
числом ресурсов Config и объёмом сжатых данных. Config читается потоково и
декодируется параллельно на blocking-пуле Tokio с ограниченным числом задач.
Progress выводится в stderr, поэтому табличный вывод metadata в
stdout можно по-прежнему безопасно перенаправлять или обрабатывать скриптом.
Если база доступна через SOCKS5-прокси (например, через ssh -D), добавьте
--socks5-proxy 127.0.0.1:1080. Прокси получает исходное имя из --host и
разрешает его на своей стороне. Поддерживаются и анонимный доступ, и
аутентификация по RFC 1929: имя задаётся --socks5-user, пароль читается
только из SOCKS5_PASSWORD, и при заданных учётных данных клиент не предлагает
анонимный метод. Пароль выбранного провайдера по-прежнему читается только из
источников выше.
| Команда | Назначение |
|---|---|
\dt |
список таблиц и объектов метаданных |
\di |
список индексов |
\d <имя> |
реквизиты и индексы объекта |
\refresh |
перечитать метаданные |
\set <имя> <литерал> |
сохранить параметр запроса &имя (число, строка, ИСТИНА/ЛОЖЬ, NULL, ДАТАВРЕМЯ(…), 0x…, ЗНАЧЕНИЕ(Перечисление.X.Y), ЗНАЧЕНИЕ(….ПустаяСсылка), список в скобках или таблица значений с типизированными колонками ТАБЛИЦА(Код КАК СТРОКА, Количество КАК ЧИСЛО, Ссылка КАК Справочник.Номенклатура)(("A", 1, ЗНАЧЕНИЕ(…)), …) для ИЗ &имя; виды колонок — ЧИСЛО, СТРОКА, ДАТА, БУЛЕВО, ЛЮБАЯССЫЛКА, Вид.Объект или (Вид.Объект, …)) |
\params |
список сохранённых параметров: имя, литерал как введён, вид |
\unset <имя> |
удалить параметр |
\session [<имя> [=] <литерал> | clear] |
параметры сеанса: сохранить (литералы как у \set), показать список или очистить; их видят все запросы и ограничения доступа, параметр запроса с тем же именем имеет приоритет |
\restrict [<Вид>.<Объект>[.<ТабЧасть>] <условие> | clear] |
ограничения доступа для ВЫБРАТЬ РАЗРЕШЕННЫЕ: сохранить условие на SDBL для таблицы, показать список или очистить; к запросу применяются только ограничения таблиц, которые он читает |
\tables |
список временных таблиц сессии: имя и колонки с видами |
\help, \? |
справка |
\q |
выход |
В консоли работают история по стрелкам, подсветка синтаксиса и Tab-дополнение команд, ключевых слов, объектов, полей, виртуальных и временных таблиц.
ВЫБРАТЬ РАЗРЕШЕННЫЕ без сохранённых ограничений выполняется без фильтра.
\restrict Справочник.Номенклатура Организация = &Орг вместе с
\session Орг = ЗНАЧЕНИЕ(…) оборачивает справочник в отфильтрованную
производную таблицу; имя объекта проверяется сразу, текст условия — при
первом запросе, ошибка указывает позицию внутри условия и называет таблицу.
Временные таблицы живут в сессии консоли: оператор с ПОМЕСТИТЬ или
ДОБАВИТЬ показывает строку Количество, УНИЧТОЖИТЬ печатает список
удалённых таблиц и ничего не выполняет, \tables показывает доступные
таблицы, а \refresh забывает их вместе с метаданными. Пакет из
конфигуратора можно вставлять целиком: консоль выполняет операторы по одному
и приходит к тому же результату.
Значения читаются из СУБД в нативных типах и форматируются самой консолью по
одним правилам для PostgreSQL и MSSQL: бинарные данные и ссылки — 0x и
шестнадцатеричные цифры в верхнем регистре, булевы — true/false, даты —
YYYY-MM-DD HH:MM:SS без дробных секунд, числа — с объявленным масштабом
(15.50), UUID — канонически в нижнем регистре, отсутствующее значение —
NULL. Для PostgreSQL декодеры numeric, timestamp, date и uuid
реализованы в самой консоли без дополнительных зависимостей; колонка
неподдерживаемого типа приводит к ошибке данных с именем типа, а не к печати
мусора.
Пока crate не опубликован на crates.io, подключите Git-репозиторий или локальный путь:
[dependencies]
open-sdbl = { git = "https://github.com/dobpilot/open-sdbl.git" }
# Для разработки рядом с репозиторием:
# open-sdbl = { path = "../open-sdbl" }Приложение само получает бинарные ресурсы и каталоги СУБД, затем передаёт их в ядро:
use open_sdbl::metadata::{
LiveTable, MetadataError, ResolvedMetadata, parse_config_descriptors,
parse_db_names, parse_schema_storage, resolve_metadata,
};
fn build_metadata(
db_names_blob: &[u8],
config_rows: &[(String, Vec<u8>)],
schema_blob: &[u8],
live_tables: Vec<LiveTable>,
) -> Result<ResolvedMetadata, MetadataError> {
let db_names = parse_db_names(db_names_blob)?;
let schema = parse_schema_storage(schema_blob)?;
let mut descriptors = Vec::new();
for (file_name, binary_data) in config_rows {
descriptors.extend(parse_config_descriptors(file_name, binary_data)?);
}
Ok(resolve_metadata(
db_names,
descriptors,
schema,
live_tables,
))
}Аргументы build_metadata читает ваше приложение; config_rows содержит
ресурсы с голым GUID в FileName, собранные из всех частей PartNo (или
единственную строку в legacy-базе). Готовые SELECT-only выражения находятся в
PostgresMetadataQueries и MsSqlMetadataQueries: сначала выполните LAYOUT,
постройте StorageLayout::from_flags, затем берите выражения через
db_names(&layout), config(&layout), extension_resources(&layout); ядро
намеренно не знает о сети, паролях и async runtime. Полные варианты загрузки через
tokio-postgres и Tiberius есть в open-sdbl-db: по модулю на провайдера —
db/postgres и
db/mssql, где ведение сессии, чтение
метаданных и декодирование значений разнесены по отдельным файлам.
Если приложению нужны не только имена, но и сами ресурсы конфигурации,
open-sdbl-db читает их одним согласованным чтением:
use open_sdbl_db::{DatabaseSession, Limits, NoProgress};
let mut session = DatabaseSession::connect(&connection, &credentials, Limits::default()).await?;
let configuration = session.configuration(&mut NoProgress).await?;
// Снимок и отчёт — те же, что вернул бы `metadata()`.
let snapshot = &configuration.metadata;
// Каждый ресурс таблицы `Config`, собранный из всех `PartNo`, байтами как
// их хранит база: сырой DEFLATE, без распаковки и перекодирования.
for resource in &configuration.config_resources {
let _ = (&resource.file_name, &resource.compressed);
}
// Расширения в порядке применения платформой, каждое со своими ресурсами.
// `active` — `Option<bool>`: `None` означает, что запись расширения не в той
// форме, в которой флаг был измерен, и решение остаётся за потребителем.
for extension in &configuration.extensions {
if extension.active == Some(false) {
continue;
}
let _ = (&extension.identity, &extension.name, extension.order, &extension.resources);
}Всё это читается в одной read-only транзакции, которую открывает обычное
получение метаданных, и возвращаются те самые ресурсы, из которых разрешён
снимок: конфигурация читается один раз, а не дважды. Это не атомарный
снимок базы — транзакция идёт на READ COMMITTED, каждый оператор видит
зафиксированное на момент своего начала, и запись в базу во время чтения может
развести соседние операторы. Что операция убирает — это второе чтение
Config уже после закрытия сеанса; чтобы узнать, сдвинулась ли конфигурация,
сравните CONFIG_FINGERPRINT до и после.
Результат публикуется только после успешного завершения всего чтения; любая
ошибка откатывает транзакцию и не отдаёт частичный результат. Ресурсы
расширений группируются по корневому индексу каждого расширения, поэтому
одинаковые имена в двух расширениях не сливаются, а байты разделяются через
Arc<[u8]> и не копируются на каждого держателя. Цена — весь Config в
памяти, а рядом с ним хранилище расширений. Limits::config_resource_limit и
Limits::config_retained_byte_limit ограничивают каждое хранилище
по отдельности: сначала Config, затем самостоятельно ConfigCAS, — так что
пик равен потолку столько раз, сколько хранилищ собирается, а не одному потолку
на всё чтение. При превышении возвращается типизированная ошибка, а не OOM. Тем,
кому нужны только имена, по-прежнему подходит metadata().
resolve_metadata* возвращает ResolvedMetadata: поле snapshot используется
для компиляции, а report содержит детерминированный список
ResolutionFinding. Отчёт нужно проверять после каждого обновления метаданных:
он сообщает об отсутствующих Config-дескрипторах и физических таблицах,
неизвестных тегах колонок, повреждённых декларациях, дубликатах GUID и
расхождениях индексов. Такие находки не обязательно делают весь снимок
непригодным: согласованные объекты и поля остаются доступны, а обращение к
неживому объекту завершается типизированной диагностикой.
Коллекции MetadataSnapshot закрыты от внешней мутации и доступны как срезы
через db_names(), descriptors(), schema(), live_tables(), objects(),
fields(), values() и indexes().
Кроме имени, объект и поле несут то, что Config даёт им для показа:
локализованные синонимы в исходном порядке (synonyms) и комментарий
дескриптора (comment). Выбирать синоним вручную не нужно —
synonym("ru") сравнивает код языка без учёта регистра, обрезает пробелы
и считает пустой синоним отсутствующим, а presentation("ru") подставляет
имя метаданных, когда синонима нет. То же для объекта по идентификатору:
snapshot.object_synonym(id, "ru") и snapshot.object_presentation(id, "ru").
Синоним ничего не переименовывает: язык запросов по-прежнему адресует имя
метаданных.
let object = snapshot.object_by_id(id).unwrap();
assert_eq!(object.name.as_deref(), Some("КоррСчет"));
assert_eq!(object.presentation("ru"), Some("Корр. счет"));
assert_eq!(object.presentation("fr"), Some("КоррСчет")); // синонима нет — имя
``` `Prepared<B>` запоминает fingerprint
снимка: попытка завершить подготовленный запрос с другим снимком возвращает
`QueryDiagnosticKind::SnapshotMismatch`. Компиляция также имеет общий бюджет
работы для веток, проекций и разыменований, поэтому патологически большой
запрос завершается типизированной диагностикой вместо неограниченной работы.
Корневая библиотека собирается с `#![forbid(unsafe_code)]`; I/O, сеть и работа
с секретами остаются в CLI-crate.
Изменилась ли конфигурация, можно спросить, не загружая её:
`PostgresMetadataQueries::CONFIG_FINGERPRINT` и одноимённая константа для
MS SQL Server считают на сервере `(число ресурсов, сжатые байты, сумма
хешей)` по тем же ресурсам `Config`, что читает загрузка. Содержимое по
сети не передаётся, запрос один, обе раскладки хранилища работают.
`CONFIG_TOTALS` для этого не годится: перезапись ресурса тем же размером
его не меняет, а хеш — меняет.
`MetadataSnapshot::fingerprint()` отвечает на другой вопрос — «тот ли это
снимок, против которого компилировали»; вычислить его без полной загрузки
нельзя, поэтому для инвалидации кэша нужен запрос выше.
Основной API компиляции — `QueryCompiler<B>`, параметризованный
неизменяемым backend-value. PostgreSQL не имеет состояния, а MSSQL
хранит смещение дат и уровень диалекта:
```rust
use open_sdbl::{
metadata::MetadataSnapshot,
query::{
CompiledQuery, MsSqlBackend, PostgresBackend, QueryCompiler,
QueryDiagnostic,
},
};
fn compile_postgres(metadata: &MetadataSnapshot) -> Result<CompiledQuery, QueryDiagnostic> {
let query = "ВЫБРАТЬ Код, Наименование ИЗ Справочник.Договоры";
QueryCompiler::new(metadata, PostgresBackend).compile(query)
}
fn compile_mssql(
metadata: &MetadataSnapshot,
year_offset: i32,
) -> Result<CompiledQuery, Box<dyn std::error::Error>> {
let query = "ВЫБРАТЬ ПЕРВЫЕ 10 Код, Наименование ИЗ Справочник.Договоры";
let backend = MsSqlBackend::new(year_offset)?;
Ok(QueryCompiler::new(metadata, backend).compile(query)?)
}year_offset — значение dbo._YearOffset.Offset (0 или 2000). CLI читает его
автоматически; при встраивании библиотеки это делает вызывающее приложение.
MsSqlBackend::new возвращает Result и отклоняет смещения вне
0..=10_000; backend со смещением ноль можно получить через
MsSqlBackend::default().
Вторая часть значения backend — уровень диалекта MsSqlDialectLevel:
Sql2012 (по умолчанию) разрешает функции SQL Server 2012+, Sql2008
ограничивается SQL Server 2008/2008 R2 и эмулирует НАЧАЛОПЕРИОДА через
DATEADD/DATEDIFF от базы datetime2 '0001-01-01'; логические значения на
обоих уровнях совпадают, а все остальные выражения генерируются одинаково.
Уровень выбирается builder-ом и не влияет на смещение дат:
use open_sdbl::query::{MsSqlBackend, MsSqlDialectLevel};
let backend = MsSqlBackend::new(2000)?.with_dialect_level(MsSqlDialectLevel::Sql2008);
assert_eq!(backend.dialect_level(), MsSqlDialectLevel::Sql2008);MsSqlDialectLevel::from_product_version("10.50.6000.34") сопоставляет строку
SERVERPROPERTY('ProductVersion') с уровнем (major < 11 → Sql2008); enum
помечен #[non_exhaustive]. Генерируемый PostgreSQL-SQL рассчитан на
PostgreSQL 13 и новее без отдельного уровня.
Legacy free functions удалены: компиляция для обеих СУБД выполняется только
через QueryCompiler<B>.
CompiledQuery содержит SQL, описание выходных колонок, маркеры отложенных
представлений, служебные колонки и вложенные результаты табличных частей
(nested); структура помечена #[non_exhaustive]. Исполнение остаётся
ответственностью приложения.
&Имя в тексте запроса связывается со значением через CompileOptions:
значения инлайнятся в SQL типизированными литералами целевого диалекта,
плейсхолдеров нет. Имена сравниваются без учёта регистра; отсутствующее,
лишнее или продублированное значение, а также список вне В (…) дают
QueryDiagnosticKind::Parameter. prepare работает без значений (параметр
получает wildcard-вид), они нужны только на compile_with.
use open_sdbl::query::{
CompileOptions, ParameterDate, ParameterValue, PostgresBackend, QueryCompiler,
QueryParameter,
};
let parameters = [
QueryParameter::new("Начало", ParameterValue::Date(ParameterDate::new(2024, 1, 1, 0, 0, 0)?)),
QueryParameter::new("Сумма", ParameterValue::Number { unscaled: 1550, scale: 2 }),
QueryParameter::new("Склады", ParameterValue::List(vec![
ParameterValue::Reference { object: warehouse_object, id: warehouse_id },
])),
];
let compiled = QueryCompiler::new(&snapshot, PostgresBackend).compile_with(
"ВЫБРАТЬ Ссылка ИЗ Документ.Реализация ГДЕ Дата >= &Начало И Сумма > &Сумма И Склад В (&Склады)",
&CompileOptions::new().parameters(¶meters),
)?;ParameterValue::Number хранит число как unscaled × 10^-scale (как
BigDecimal), Date — проверенную дату (на MSSQL применяется смещение лет),
Reference — 16 байт RRRef и объект-цель (нужен для guard по RTRef),
Binary — сырые байты, List — список скаляров для В (&Список) (пустой
список даёт всегда ложный предикат, NULL внутри допустим), Table —
таблица значений для ИЗ &Таблица КАК Т: колонки ParameterColumn { name, kind } с объявленным видом и строки скаляров, проверяемых по нему; строки
встраиваются в CTE оператора (SELECT 1 AS "__row", CAST(… AS тип) … UNION ALL SELECT 2, …), так что колонка типизирована и у пустой таблицы. Скалярные параметры
рендерятся по типу колонки, как литералы, без проверки вида; строгость
только у ссылок.
Ссылочное поле можно сравнивать со значением в формате вывода консоли:
Ссылка = 0x9EBC4CED… (16 байт) для одночленной ссылки и 20 байт
RTRef ‖ RRRef для runtime-typed поля (Регистратор = 0x00000039…),
сравнение раскладывается на _RTRef/_RRRef; иная длина отклоняется
диагностикой. ЗНАЧЕНИЕ(<Вид>.<Объект>.ПустаяСсылка) даёт 16 нулевых байт
для любого ссылочного вида.
SessionParameters — значения, которые видит каждый запрос и каждое
ограничение доступа сеанса, аналог параметров сеанса 1С. &Имя сначала
ищется среди параметров запроса, затем среди параметров сеанса; параметр
сеанса, который запрос не упоминает, ошибкой не считается. Имена уникальны
без учёта регистра, set заменяет значение на месте:
use open_sdbl::query::{CompileOptions, ParameterValue, QueryParameter, SessionParameters};
let mut session = SessionParameters::new();
session.set(QueryParameter::new("ТекущийПользователь", ParameterValue::Reference {
object: users_object,
id: current_user_id,
}));
let options = CompileOptions::new().session(&session);ИЗ Константы (FROM Constants) — источник с одним полем на каждую
живую константу, как в 1С; стандартных полей нет, * допускается, ссылочные
константы разыменовываются обычным LEFT JOIN. В SQL читаются только
константы, которые упоминает оператор: по одной ветви UNION ALL на
константу из её таблицы _Const<N>, где колонки других констант —
CAST(NULL AS <тип колонки>), и
MAX по каждой колонке без GROUP BY, поэтому результат — ровно одна
строка, а незаписанная константа даёт NULL. На PostgreSQL для 1С MAX
определён для всех типов хранения (boolean, bytea, mchar, mvarchar
платформа создаёт в схеме public), на SQL Server булевы значения хранятся
как binary(1). Оператор без полей констант (КОЛИЧЕСТВО(*)) читает
однострочную заглушку. Разделители применяются внутри каждой ветви;
отключённый разделитель у используемой константы — диагностика
UnsupportedFeature, потому что строк по областям тогда несколько.
Отдельная константа читается и как таблица Константа.<Имя>
(Constant.<Name>) с единственным полем Значение / Value — именно так
её называет платформа; имя самой константы полем этой таблицы не является.
В консоли \d Константы перечисляет константы, а автодополнение
предлагает Константы в ИЗ. Имя таблицы имеет приоритет над временной
таблицей с таким же именем.
В базе с общими реквизитами-разделителями (Разделять) каждый индекс
каждой разделённой таблицы начинается с колонки разделителя _Fld<N>, и
без условия по ней отбор или соединение по ссылке не попадает в индекс.
Компилятор читает из Config настройки каждого разделителя: режим
(Независимо / Независимо и совместно) и привязанные параметры сеанса
значения и использования (в БСП это ОбластьДанныхЗначение и
ОбластьДанныхИспользование). Значение берётся из SessionParameters по
имени привязанного параметра, а если привязки в метаданных нет — по имени
самого общего реквизита. Для каждой физической таблицы, объявляющей
колонку разделителя, в SQL добавляется <псевдоним>._Fld<N> = <значение>:
для первого источника и сохраняемой стороны одностороннего внешнего
соединения — в WHERE, для присоединяемой стороны, разыменований и
представлений — в ON, для виртуальных таблиц, ветвей UNION ALL
расширений, вложенных запросов и обёрток РАЗРЕШЕННЫЕ — внутри их
подзапросов. Литерал повторяется у каждой таблицы, поэтому каждая получает
поиск по индексу.
Без значения в сеансе разделитель Независимо и совместно фильтруется
пустым значением своего типа (0, "", ЛОЖЬ, пустая дата), то есть
запрос видит только общие данные; разделитель Независимо без значения —
диагностика Parameter с именем ожидаемого параметра сеанса. Параметр
использования, равный ЛОЖЬ, отключает предикат этого разделителя, как
ИспользованиеРазделителя = НеИспользовать в 1С. Параметры запроса
значение разделителя не задают. База без разделителей и таблица без
колонки разделителя дают прежний SQL байт в байт. В консоли достаточно
\session ОбластьДанныхЗначение = 7; \d показывает разделители как
поля вида DataSeparator, а MetadataField::separation и
MetadataSnapshot::separators отдают их настройки приложению.
ВЫБРАТЬ РАЗРЕШЕННЫЕ (SELECT ALLOWED) включает построчную фильтрацию
таблиц оператора по правилам приложения. Слово допустимо только после
первого ВЫБРАТЬ оператора верхнего уровня, перед РАЗЛИЧНЫЕ и ПЕРВЫЕ,
и действует на все его вложенные запросы, подзапросы В (…), ветки
объединения и виртуальные таблицы; операторы пакета независимы, поэтому
ПОМЕСТИТЬ под этим словом кладёт во временную таблицу уже отфильтрованные
строки. Библиотека не знает прав пользователя и не выполняет ввод-вывод, так
что фильтры запрашиваются у приложения в две фазы, как представления:
QueryCompiler::prepare()
│
├── RestrictionRequest { RestrictionTarget { object, table_part }, … }
│ │
│ callback приложения
│ │
◄── [AccessRestriction { object, table_part, условие на SDBL }]
│
▼
Prepared::compile_with(&CompileOptions::new().restrictions(&…).session(&…))
Условие пишется на языке запросов в стиле шаблонов ограничений ролей 1С:
поля целевой таблицы без квалификатора, одношаговые разыменования
(Владелец.Ответственный = &ТекущийПользователь), В (ВЫБРАТЬ …) и
параметры сеанса; параметры запроса ограничению не видны, а таблицы,
которые читает само условие, не фильтруются. Принимается и полная форма
платформы ТекущаяТаблица [КАК Т] ГДЕ …: ТекущаяТаблица и псевдоним
квалифицируют поля целевой таблицы, в том числе внутри вложенных запросов
условия (… ГДЕ Ключи.Объект = ТекущаяТаблица.Ссылка); соединения перед
ГДЕ не поддерживаются. Сырой SQL в ограничении невозможен.
Сами тексты ограничений ролей библиотека читает из конфигурации и разворачивает (см. «Пользователи, роли и ограничения» ниже).
use open_sdbl::query::{AccessRestriction, CompileOptions, PostgresBackend, QueryCompiler};
let prepared = QueryCompiler::new(&snapshot, PostgresBackend)
.prepare("ВЫБРАТЬ РАЗРЕШЕННЫЕ Ссылка, Наименование ИЗ Справочник.Номенклатура")?;
let restrictions = prepared
.restriction_request()
.targets
.iter()
.map(|target| AccessRestriction::new(target.object, "Организация В (&Организации)"))
.collect::<Vec<_>>();
let compiled = prepared.compile_with(
&snapshot,
&CompileOptions::new().restrictions(&restrictions).session(&session),
)?;Обычная таблица оборачивается в производную:
(SELECT <все колонки> FROM "_Reference31" AS "__restricted" [LEFT JOIN …] WHERE <условие>) AS <псевдоним> — одинаково для ИЗ и всех видов
соединений, включая таблицы с расширениями и табличные части (цель тогда
содержит имя табличной части). Срезы, остатки и обороты добавляют условие
конъюнкцией к предикату виртуальной таблицы и принимают только прямые поля,
как и собственное условие таблицы. С отборами запроса ничего не сливается:
цель без ограничения даёт байт в байт тот же SQL, что и без слова
РАЗРЕШЕННЫЕ. Ошибка в тексте условия, ограничение для таблицы, которую
ни один оператор с РАЗРЕШЕННЫЕ не читает, или два ограничения одной
таблицы дают QueryDiagnosticKind::Restriction; позиция указывает внутрь
текста условия, сообщение называет таблицу. В этом режиме разыменованные
таблицы (Т.Контрагент.Наименование) не фильтруются — для гарантии
защищённого чтения есть отдельный режим, описанный ниже.
РАЗРЕШЕННЫЕ — свойство текста запроса: приложение не может потребовать
фильтрации произвольного текста, а разыменования остаются вне запроса
ограничений. RestrictionMode::Restricted переносит защиту с текста на
саму компиляцию и выбирается при подготовке:
use open_sdbl::query::{
AccessDecision, CompileOptions, PostgresBackend, PrepareOptions, QueryCompiler,
};
let prepared = QueryCompiler::new(&snapshot, PostgresBackend)
.prepare_with_options(source, &PrepareOptions::new().restricted())?;
// Каждая цель требует явного ответа: можно всё, можно по условию, нельзя.
let decisions = prepared
.restriction_request()
.targets
.iter()
.map(|target| AccessDecision::unrestricted(target.clone()))
.collect::<Vec<_>>();
let compiled = prepared.compile_with(
&snapshot,
&CompileOptions::new().decisions(&decisions).session(&session),
)?;Режим хранится в Prepared и применяется при каждой компиляции; понизить
его нечем — CompileOptions поля режима не имеет. Он действует на каждый
оператор пакета, вложенные запросы, ветки объединения, явные соединения,
виртуальные таблицы и чтение временных таблиц, независимо от наличия
слова РАЗРЕШЕННЫЕ, и ключевое слово в текст не подставляется. Одноразовые
compile/compile_with остаются нерестриктивными: у них нет фазы запроса,
а значит и решений.
Что попадает в запрос ограничений дополнительно к источникам оператора:
цель разыменования (Т.Контрагент.Наименование), каждый кандидат
составной ссылки и цель представления ссылки. Решение по такой цели
действительно применяется — соединение читает обёрнутую таблицу, поэтому
строки, которых решение не допускает, не дают значений.
Цель без решения — QueryDiagnosticKind::Restriction с именем объекта и,
если цель — табличная часть, её именем. Запрет (AccessDecision::Denied)
даёт ту же обёртку с ложным предикатом, а не отсутствие фильтра; ошибка в
условии прекращает компиляцию, а не снимает фильтр.
Конструкции, безопасное чтение которых пока не реализовано, режим
отклоняет QueryDiagnosticKind::UnsupportedFeature до генерации SQL:
обход иерархии (В ИЕРАРХИИ, ИТОГИ … ПО … ИЕРАРХИЯ), источник
КритерийОтбора, источник Константы, проекция вложенной табличной
части, отложенное представление ссылки и временная таблица, определённая
вне такого же режима. Временная таблица, заполненная внутри того же
пакета, читается: её определяющий оператор сам был отфильтрован.
Последние две конструкции отклоняются потому, что их строки приходят
вторым запросом, который выполняет само приложение (CompiledQuery::nested
и compile_presentation_lookup), — решения этой компиляции до него не
доходят. Сам compile_presentation_lookup режима не имеет и ничего не
фильтрует.
Граница доверия сохраняется: условие ограничения — вход приложения, а не пользователя. Таблицы, которые читает само условие, не фильтруются рекурсивно и в запрос не попадают, а параметры запроса условию не видны; текст пользователя в этот путь не попадает. За условия отвечает приложение.
open-sdbl-db отвечает на запрос функцией user_decisions: роли без
ограничения дают Unrestricted, с ограничением — развёрнутое условие,
отсутствие права — Denied. Отсутствие текущего пользователя, роль с
непрочитанными правами и любая ошибка разворачивания прекращают ответ
целиком: частично развёрнутый набор не считается достаточным, а нехватка
данных не читается как разрешение. Аутентификация пользователя остаётся
за приложением.
Библиотека читает права из самой базы, без платформы, и складывает их в три слоя.
- Роли конфигурации. Список ролей — коллекция корневого дескриптора
Config, имена — дескрипторы<guid>; права роли — запись<guid>.0в скобочном формате.parse_role_rightsотдаётRoleRights: по каждому объекту (или его реквизиту, табличной части, команде) права с признаком «дано/отказано» и текстами ограничений на уровне записей, шаблоны ограничений и флаги шапки.Rightназывает стандартные права (Read/Чтение,Insert,Update,Delete,View, права проведения, истории данных, конфигурации…); соответствие гвидов именам выведено из порядка, в котором платформа пишет права (порядок редактора ролей), и подтверждено ролями-одиночками БСП (ЗапускТонкогоКлиентаи т. п.) на 8.3.27; неизвестный гвид остаётсяRight::Other.PostgresMetadataQueries::role_rights/MsSqlMetadataQueries::role_rightsстроят запрос только по нужным ролям — записей.0в базе десятки тысяч. - Пользователи ИБ.
USERSчитаетv8users;decode_user_dataразбирает колонкуData(XOR с ключом из самого блоба, затем скобочная запись) в идентификатор, имя, полное имя и гвиды ролей, хеши паролей не отдаёт.InfoBaseUserсоединяет строку таблицы с данными и называет роли черезRoleCatalog. - Разворачивание ограничений (
open_sdbl::access).parse_templateразбирает текст ограничения или тело шаблона в узлыTemplateNode— текст,#Еслис ветвями, вызовы шаблонов с аргументами,#Параметр(N)и прочие имена, — каждый со смещением в байтах; текст с несбалансированными директивами даёт ошибку с позицией.expand_restrictionподставляет тела шаблонов роли (#ДляОбъекта("Владелец"),#Параметр(N)и именованные параметры),#ТекущаяТаблица(имя таблицы),#ИмяТекущейТаблицы(оно же строковым значением, в кавычках),#ИмяТекущегоПраваДоступа,##как один#, разрешает#Если … #Тогда … #ИначеЕсли … #Иначе … #КонецЕслипо параметрам сеанса (строки, булевы,СтрСодержит,+,=,<>,Не,И,Или,Значение(…ПустаяСсылка)); параметр без значения — ошибка с его именем, сообщение шаблона видаОшибка: …— ошибка с этим текстом. Результат — условие в полной форме платформыТекущаяТаблица [КАК Т] ГДЕ …, которое принимает компилятор; полная форма платформы может описывать таблицу послеИЗ(ТекущаяТаблица ИЗ #ТекущаяТаблица КАК ТекущаяТаблица ГДЕ …) — так пишут шаблоны «1С:Документооборота», — и тогда псевдоним берётся оттуда. Соединения полной формы (ЛЕВОЕ/ВНУТРЕННЕЕ СОЕДИНЕНИЕ … ПО …) компилятор разворачивает в коррелированныйEXISTSс одной строкой-якорем: строка ограничиваемой таблицы проходит, если среди присоединённых к ней строк есть удовлетворяющая условию, — так соединение не размножает строки таблицы. Ограничения двух ролей объединяются черезИЛИ, только если соединяет не больше одного из них.read_accessобъединяет роли пользователя по объекту и праву: право есть, если его даёт хоть одна роль; роль без ограничения снимает все; ограничения ролей соединяются черезИЛИ. Проверено на шаблонах БСП конфигурации «Бухгалтерия предприятия 3.0».
Консоль показывает всё это и применяет к запросам:
\users пользователи базы
\user Петрова (бухгалтер) реквизиты и роли пользователя
\roles Продаж роли конфигурации (с фильтром по подстроке)
\role ПолныеПрава Справочник.Организации
права роли на объекте и тексты ограничений
(без объекта — ещё и сигнатуры шаблонов)
\template ЧтениеЭД ДляРегистра
из чего состоит тело шаблона и само тело; без
имени — шаблоны роли, размер тела и разбирается
ли оно
\rls Справочник.Номенклатура [Чтение]
ограничения ролей и развёрнутый доступ
текущего пользователя (или всех ролей)
\rls список ограничений в уже прочитанных правах:
роль, объект, право
\as Петрова (бухгалтер) дальше ВЫБРАТЬ РАЗРЕШЕННЫЕ выполняется от её
имени: ограничения её ролей на Чтение
разворачиваются в \restrict, остальные таблицы
получают ограничение ролей при компиляции,
а приглашение консоли называет пользователя
Параметры сеанса, которые читают шаблоны БСП, \as берёт из самой базы:
регистр сведений ПараметрыОграниченияДоступа хранит их в
ХранилищеЗначения (на MSSQL — прямо в колонке, на PostgreSQL — ссылкой
STORHDR на части в binarydata; дальше скобочная сериализация), оттуда приходят ВерсииШаблоновОграниченияДоступа и четыре
списка с ограничением; ТекущийПользователь — элемент
Справочник.Пользователи по идентификатору пользователя ИБ,
ТекущийВнешнийПользователь — пустую ссылку.
Значение, заданное руками через \session, не перетирается. Если регистр
в конфигурации есть, а прочитать его не удалось — например, он разделён и
значение разделителя не задано, — \as называет причину, чтобы её можно
было устранить и повторить. На демо-базе
УНФ этого хватает, чтобы развернуть 375 ограничений из 470 — остальным
нужны параметры, которых в базе нет.
\as разворачивает ограничения ролей пользователя по параметрам сеанса и
кладёт их в тот же список, что и \restrict, — одной строкой на объект,
на языке 1С:
open-sdbl=> \as Петрова (бухгалтер)
Current user: Петрова (бухгалтер) (4 roles): …
17 restrictions derived into \restrict.
3 objects not expanded: session parameter «ТекущийВнешнийПользователь» has no value
Петрова (бухгалтер)=> \restrict
* Справочник.Номенклатура ИСТИНА В (ВЫБРАТЬ ПЕРВЫЕ 1 ИСТИНА ИЗ …)
# * derived from the roles of the current user
Так видно, какое именно условие получит запрос, и любое из них можно
заменить своим: \restrict для того же объекта перекрывает развёрнутое и
становится обычным ограничением. \as clear и \refresh забывают
развёрнутые, \session разворачивает их заново — условие никогда не
старше параметров, по которым оно получено. Объекты, которые роли не
ограничивают, и табличные части по-прежнему получают доступ при
компиляции запроса.
Пользователей и права ролей консоль читает при первой команде, которой
они нужны, тем же путём «только чтение», что и запросы, и забывает при
\refresh. Роль, ресурса прав которой нет в Config — удалённая из
конфигурации или принадлежащая расширению, — ничего не даёт: \as
называет такие роли и продолжает, \role и \template сообщают об этом
по имени. Права вне Чтения в запросах не участвуют, но в модели
сохраняются. Табличная часть получает доступ владельца через
Ссылка В (ВЫБРАТЬ … ИЗ <владелец> ГДЕ …); запрещённая таблица даёт
условие ЛОЖЬ; ошибка разворачивания (нет параметра сеанса, устаревший
шаблон) прерывает запрос с сообщением — параметры задаются \session.
Роли расширений читаются из их хранилища: _ExtensionsInfo хранит ключ
корневого ресурса расширения, корень — индекс имя → ключ (base64 от
двадцати байт), а ConfigCas адресует ресурсы по шестнадцатеричному ключу.
Роль, которой нет в Config, консоль ищет там: берёт <guid>.0 как права
и <guid> как описатель, поэтому роль расширения и называется, и
ограничивает.
Базы-копии открыты только на чтение, поэтому ПОМЕСТИТЬ не создаёт таблицу в
СУБД: определение компилируется в CTE vt1, vt2, … и подставляется в
WITH каждого следующего оператора, который её читает. ДОБАВИТЬ образует
новую CTE SELECT … FROM vtK UNION ALL <оператор> и переключает имя на неё,
УНИЧТОЖИТЬ SQL не порождает и лишь скрывает имя. Определения хранит
TempTablesManager — аналог МенеджерВременныхТаблиц:
use open_sdbl::query::{CompileOptions, PostgresBackend, QueryCompiler, TempTablesManager};
let compiler = QueryCompiler::new(&snapshot, PostgresBackend);
let options = CompileOptions::new();
let mut tables = TempTablesManager::new();
// Возвращает одну строку «Количество», как Запрос.Выполнить() в 1С.
compiler.compile_batch(
"ВЫБРАТЬ Ссылка КАК Товар, СУММА(Количество) КАК Итог ПОМЕСТИТЬ Обороты
ИЗ РегистрНакопления.Продажи СГРУППИРОВАТЬ ПО Ссылка;",
&options,
&mut tables,
)?;
// WITH "vt1" AS (…) SELECT … FROM "vt1" AS "Т"
let query = compiler
.compile_batch("ВЫБРАТЬ Т.Товар, Т.Итог ИЗ Обороты КАК Т;", &options, &mut tables)?
.expect("оператор возвращает строки");compile_batch возвращает None, когда пакет заканчивается УНИЧТОЖИТЬ:
исполнять нечего, как Неопределено в 1С. Менеджер обновляется только при
успешной компиляции всего пакета и привязывается к диалекту и снимку
метаданных первого определения. prepare_with собирает цели представлений с
учётом уже существующих таблиц, Prepared::compile_batch компилирует
подготовленный пакет. Обычные compile и prepare тоже принимают пакет, но
работают с временным менеджером и дают диагностику TemporaryTable, если
пакет не возвращает строк.
Правила определения совпадают с правилами вложенного запроса: * и
отложенные представления запрещены, УПОРЯДОЧИТЬ ПО — только вместе с
ПЕРВЫЕ, даты остаются в домене хранения, поэтому смещение лет MSSQL
применяется ровно один раз в итоговой проекции. ДОБАВИТЬ требует точного
совпадения структуры: то же число колонок, совместимые виды, у ссылок
одинаковые цели и ширина; расширения типа, в отличие от ОБЪЕДИНИТЬ, не
происходит. ИНДЕКСИРОВАТЬ ПО и ИНДЕКСИРОВАТЬ ПО НАБОРАМ разбираются и
проверяются по списку выборки, но SQL не порождают: у CTE индексов нет.
В WITH попадают только те таблицы, которые достижимы из последнего
оператора.
Значения параметров инлайнятся в определение в момент компиляции, поэтому
последующее чтение таблицы не требует значений, но и не видит изменений
\set: чтобы обновить данные, таблицу нужно уничтожить и создать заново.
Табличная часть, названная в списке выборки, — это таблица внутри ячейки: платформа отвечает на такую колонку отдельным результатом для каждой строки владельца, и одним оператором SQL это не выражается. Компилятор возвращает главный оператор и по одному вложенному на каждую спроецированную часть:
let compiled = QueryCompiler::new(&snapshot, PostgresBackend)
.compile("ВЫБРАТЬ Д.Номер, Д.Товары ИЗ Документ.Продажа КАК Д;")?;
for nested in &compiled.nested {
// nested.sql — SELECT строк табличной части, уже ограниченный
// владельцами главного оператора и упорядоченный
// по владельцу и номеру строки;
// nested.columns — её колонки;
// nested.label — имя колонки в логическом результате;
// nested.position — её место среди колонок;
// nested.owner_column / nested.key_column — индексы колонок, по
// которым строки связываются с главным результатом.
}Ключ владельца добавляется в главный оператор служебной колонкой, её индекс
перечислен в CompiledQuery::service_columns — потребитель, показывающий
результат человеку, её не печатает. Связь описана данными, а не текстом
запроса: приложение со своим планировщиком (коннектор БД) связывает два
результата, не читая сгенерированный SQL. ПОМЕСТИТЬ, ИТОГИ и объединение
вместе с проекцией табличной части — диагностика.
Ядро не приводит значения к тексту: колонки, скаляры и агрегаты возвращаются в
нативных типах СУБД, а форматирование выполняет клиент. Каждый элемент
CompiledQuery::columns — это CompiledColumn { label, kind }, где
ColumnKind описывает значение структурно:
| Вариант | Данные | Источник |
|---|---|---|
Reference { targets, runtime_typed } |
ObjectId возможных целей; пусто для универсальной ссылки |
SchemaStorage R и индекс снимка |
Binary { length } |
длина, если задана каталогом | bytea, binary(n), varbinary(n), rowversion |
String { length } |
длина, если задана | character varying(n), mvarchar(n), nvarchar(n) |
Number { precision, scale } |
точность и масштаб, если заданы | numeric(p,s), целые (scale = 0), float без параметров |
Boolean, DateTime, Uuid |
— | boolean/bit, timestamp/datetime2, uuid/uniqueidentifier |
Null |
— | литерал NULL; совместим с любым типом в ОБЪЕДИНИТЬ |
Undefined |
— | литерал НЕОПРЕДЕЛЕНО; тоже совместим с любым типом |
Type |
— | ТИП(…) и ТИПЗНАЧЕНИЯ(…): пять байт, разбирает TypeValue::decode |
Unknown { data_type } |
имя типа каталога | всё остальное |
Ссылка всегда занимает одну колонку. Поле без члена _RTRef возвращает 16
байт RRRef; поле с _RTRef (составной тип или универсальная ссылка) —
20 байт: 4 байта номера таблицы (big-endian) и 16 байт ссылки, а kind
помечен runtime_typed. Прочие члены составного поля (_TYPE, _N, _S,
_L, _T) остаются отдельными колонками со своими типами.
Исключения из «нативных типов» ровно два: для MSSQL к датам применяется
DATEADD(year, -YearOffset, …), чтобы вернуть логическую дату, а строковые
значения PostgreSQL приводятся к text, так как бинарный формат передачи
1С-типов mchar/mvarchar не документирован. Приведение применяется и к
колонке такого типа, и к вычисляемому выражению вида String (ЕСТЬNULL,
ВЫБОР, агрегат), потому что операнд-расширение делает таким весь результат,
причём и во вложенных запросах. ПРЕДСТАВЛЕНИЕ по-прежнему возвращает
строку. Ветви ОБЪЕДИНИТЬ с разными вариантами ColumnKind в одной позиции
отклоняются типизированной диагностикой до выполнения.
Backend — sealed trait, реализованный библиотекой для PostgresBackend и
MsSqlBackend. Он позволяет писать общий код без динамической диспетчеризации,
но намеренно не является точкой расширения для сторонних SQL-диалектов:
use open_sdbl::{
metadata::MetadataSnapshot,
query::{Backend, CompiledQuery, QueryCompiler, QueryDiagnostic},
};
fn compile<B: Backend>(
metadata: &MetadataSnapshot,
backend: B,
source: &str,
) -> Result<CompiledQuery, QueryDiagnostic> {
QueryCompiler::new(metadata, backend).compile(source)
}Не классифицируйте ошибки по тексту. QueryDiagnostic::kind() возвращает
QueryDiagnosticKind для лексической или синтаксической ошибки, неизвестного
или неоднозначного объекта, поля или значения, неживой таблицы,
неподдерживаемой возможности, ошибок presentation-плана и пакета
представлений, связывания параметров (Parameter), временных таблиц
(TemporaryTable), ограничений доступа (Restriction), исчерпания бюджета
разбора или компиляции (TooDeep, WorkBudgetExceeded) и компиляции против
чужого снимка метаданных (SnapshotMismatch). Enum помечен
#[non_exhaustive], поэтому при match необходима fallback-ветка.
offset() измеряется в байтах исходного SDBL, line() и column() — позиции
для вывода пользователю. Для обёрнутых lexer/lookup-ошибок стандартный
std::error::Error::source() сохраняет исходную причину.
Ошибки декодирования метаданных аналогично доступны через
MetadataError::kind() и MetadataErrorKind. Если offset() присутствует,
offset_unit() явно различает битовое смещение DEFLATE и байтовое смещение
brace-serialized/UTF-8 данных. ResolutionReport отличается от
MetadataError: первый описывает восстановимые расхождения уже построенного
снимка, второй означает, что конкретный вход декодировать или проверить не
удалось.
Представление ссылки зависит от прикладной политики: одному приложению нужен
Наименование (Код), другому — другой шаблон или язык. Поэтому
.Представление, ПРЕДСТАВЛЕНИЕССЫЛКИ() и ПРЕДСТАВЛЕНИЕ() компилируются в две
фазы:
В проекции соединённого запроса функция может принимать поле, полученное одним
разыменованием, например Ссылка.ДоговорКонтрагента или
ЦФО.Сам_БизнесРегион. Компилятор переиспользует JOIN разыменования и строит
следующий presentation-JOIN от его alias, а не от исходной таблицы.
Аргументом может быть агрегат (ПРЕДСТАВЛЕНИЕ(МАКСИМУМ(Т.Клиент))): такая
проекция считается агрегатной, и ветвь агрегирует. Может быть и ВЫБОР с
ветвями разных типов — представление проталкивается в ветви, и строковая
отвечает своей строкой, а ссылочная — представлением по плану приложения.
SDBL + MetadataSnapshot
│
▼
QueryCompiler::new(snapshot, backend).prepare()
│
├── PresentationRequest { ObjectId/GUID возможных типов ссылок }
│ │
│ callback приложения
│ │
◄── PresentationPlan { FieldId[], структурированный шаблон }
│
▼
Prepared<PostgresBackend>::compile()
или Prepared<MsSqlBackend>::compile()
│
▼
CompiledQuery { sql, columns, deferred_presentations }
Если SchemaStorage оставляет цель R пустой, конкретный тип универсальной
ссылки находится в _RTRef каждой строки. В таком случае
CompiledQuery::deferred_presentations отмечает колонки с 20-байтовым
бинарным значением RTRef ‖ RRRef (ColumnKind::Reference с
runtime_typed): CLI после основного ограниченного запроса группирует только
фактически возвращённые ссылки по типу и получает их представления пакетами до
512 значений; ключевая колонка __reference пакетного запроса — сырые 16 байт
_IDRRef. ПЕРВЫЕ/LIMIT и фильтры остаются в основном SQL; предварительного
сканирования таблицы и JOIN ко всем объектам конфигурации нет. SQL пакетного
lookup строит QueryCompiler::compile_presentation_lookup(), а конкретный SQL
определяется типом backend, поэтому приложение не склеивает физические
идентификаторы самостоятельно.
Контракт использует идентификаторы, а не имена:
| Тип | Стабильное значение | Назначение |
|---|---|---|
ObjectId |
16 байт реального GUID 1С | возможный тип ссылочного значения |
AttributeId |
16 байт реального GUID 1С | пользовательский реквизит |
StandardFieldId |
#[repr(u32)] |
стандартное поле без GUID: код, наименование, номер, дата и другие |
FieldId |
Metadata(AttributeId) или Standard(StandardFieldId) |
любое поле шаблона |
PresentationExpression |
Field, Literal, Concat |
безопасное дерево выражения без сырого SQL |
Lookup-методы MetadataSnapshot::object_id() и
MetadataSnapshot::field_id() преобразуют имя в ID при настройке политики.
Индексы снимка дают ожидаемый поиск O(1), не считая нормализации имени.
Пример callback-политики Наименование (Код):
use open_sdbl::{
metadata::{LookupError, MetadataSnapshot},
query::{
CompiledQuery, PostgresBackend, PresentationExpression,
PresentationPlan, QueryCompiler,
},
};
fn compile_with_presentations(
source: &str,
metadata: &MetadataSnapshot,
) -> Result<CompiledQuery, Box<dyn std::error::Error>> {
let prepared = QueryCompiler::new(metadata, PostgresBackend).prepare(source)?;
// Это callback приложения. Запрос содержит дедуплицированный набор GUID
// всех возможных типов ссылок, найденных ядром в SDBL.
let plans = prepared
.presentation_request()
.targets
.iter()
.map(|target| -> Result<PresentationPlan, LookupError> {
let name = metadata.field_id(target.object, "Наименование")?;
let code = metadata.field_id(target.object, "Код")?;
Ok(PresentationPlan {
object: target.object,
fields: vec![name, code],
expression: PresentationExpression::Concat(vec![
PresentationExpression::Field(name),
PresentationExpression::Literal(" (".into()),
PresentationExpression::Field(code),
PresentationExpression::Literal(")".into()),
]),
})
})
.collect::<Result<Vec<_>, _>>()?;
Ok(prepared.compile(metadata, &plans)?)
}Для MSSQL используются те же PresentationRequest, PresentationPlan и
callback-политика. На первой фазе вызовите
QueryCompiler::new(metadata, MsSqlBackend::new(year_offset)?).prepare(source),
передав значение _YearOffset в backend. Конструктор отклоняет значения вне
диапазона 0..=10000.
Ядро проверяет, что на каждый запрошенный ObjectId получен ровно один план,
все FieldId действительно принадлежат объекту, а шаблон использует только
разрешённые поля. Для типов, известных из SchemaStorage, оно добавляет
необходимые LEFT JOIN, CASE и SQL-выражение представления. Для универсальных
ссылок та же проверка выполняется при пакетном lookup после основного запроса.
Callback вызывается для типа, а не для каждой строки результата, поэтому
приложение может кешировать планы по GUID и поколению метаданных. CLI использует
для этого ограниченный кеш Moka.
Note
Здесь ABI означает типизированный контракт между ядром и приложением. Сейчас
это публичный Rust API; стабильного extern "C" ABI для подключения из других
языков в проекте пока нет. ObjectId::as_bytes() и AttributeId::as_bytes()
позволяют построить такой адаптер без передачи строковых имён.



