Перейти к основному содержимому
Версия: 7.0

Rules: domain logic

Правила: свойства

Правила свойств

  1. Ассистент НЕ ДОЛЖЕН объявлять свойство, если оно используется только один раз.

    Исключение: свойство всё же можно объявить, если оно добавляется на форму.

  2. Каждый параметр свойства ДОЛЖЕН использоваться в его выражении. Неиспользуемые параметры запрещены.

  3. Ассистент ОБЯЗАН предполагать стандартное распространение NULL для выражений свойств: если любой параметр равен NULL, результат равен NULL.

    Исключения, которые НЕ обнуляют результат при одном операнде NULL: операторы выбора — OVERRIDE, возвращающий первый не-NULL операнд, и IF ... THEN ... ELSE, у которого устойчиво к NULL именно УСЛОВИЕ: условие NULL уводит в ветку ELSE, а не-NULL возвращает значение THEN как есть, поэтому IF TRUE THEN NULL ELSE 1 — это NULL, — MIN / MAX, устойчивая к NULL арифметика (+) / (-), конкатенация CONCAT (операнд NULL пропускается вместе со своим разделителем) и агрегаты GROUP (GROUP SUM, GROUP MAX и т. д.) — операнд или значение NULL пропускается, а не распространяется. OR, NOT и XOR тоже не распространяют: не-NULL операнд читается как TRUE, поэтому NULL OR TRUE — это TRUE, а NOT NULL — тоже TRUE. Распространяет как раз AND: он возвращает TRUE, только когда оба операнда не NULL, поэтому TRUE AND NULL — это NULL. GROUP LAST пропускает NULL, только пока у него нет WHERE: условием служит как раз не-NULL-ность агрегируемого выражения. При явном WHERE строка, ему удовлетворяющая, отдает свое значение, даже если это значение NULL.

    Эти исключения всё же дают NULL, когда:

    • все операнды или агрегируемые значения равны NULL — кроме NOT, весь смысл которого в том, чтобы ответить там TRUE;
    • (+) / (-) или GROUP SUM даёт 0 (нулевой результат возвращается как NULL).
  4. Ассистент НЕ ДОЛЖЕН использовать GROUP с блоком BY (в том числе GROUP AGGR) внутри выражений: в приведении типа, в арифметике (включая (+) / (-)), как аргумент другого свойства или как реализацию абстрактного свойства через +=.

    Такой оператор сам задаёт параметры результата, поэтому допустим только как определение свойства целиком: правая часть определения через = либо встроенное определение в квадратных скобках; в любой другой позиции платформа даёт ошибку BY clause in GROUP operator cannot be used in expressions. Чтобы использовать результат в выражении, ассистенту СЛЕДУЕТ прежде всего переписать оператор без BY, заменив каждую группировку условием равенства с верхним параметром (GROUP SUM f(x) IF g(x) = y); иначе — применить встроенную форму [GROUP ... BY ...](...) к аргументам или объявить отдельное свойство и обращаться к нему.

    Ограничение связано именно с блоком BY: GROUP без BY берёт параметры из внешнего контекста и может использоваться внутри выражений.

    Рассуждая о GROUP AGGR, ассистент ОБЯЗАН трактовать его как GROUP MAX с дополнительным ограничением.

  5. Ассистенту СЛЕДУЕТ избегать лишних условий, когда семантика языка уже даёт требуемый результат.

  6. Ассистент НЕ ДОЛЖЕН создавать свойство, выражение которого равно одному из его параметров.

  7. Ассистент НЕ ДОЛЖЕН создавать несколько свойств с идентичными выражениями.

  8. Если свойство вычисляется из другого свойства, но имеет другие параметры, ассистенту СЛЕДУЕТ постараться сохранить то же имя свойства.

  9. Чтобы проверить, что свойство равно NULL, ассистенту СЛЕДУЕТ использовать IF NOT property(...).

    Чтобы проверить, что оно не NULL, ассистенту СЛЕДУЕТ использовать IF property(...).

  10. Ассистенту СЛЕДУЕТ указывать CHARWIDTH в определении свойства, а не в дизайне формы.

Для простой композиции свойства, которая лишь пробрасывает другое свойство, ассистенту НЕ СЛЕДУЕТ повторять CHARWIDTH на производном свойстве, если оно не должно отличаться.

  1. Для статических объектов ассистент НЕ ДОЛЖЕН использовать свойства staticCaption или staticName.

    Вместо них ассистент ОБЯЗАН использовать caption и name.

    Это касается и записи: caption и name — простые композиции над хранимыми заголовком и именем, поэтому присваивание в них проходит в хранимое свойство. Заголовок статического объекта присваивается через caption; имя менять нельзя — изменение name запрещено системным ограничением.

    name возвращает каноническое имя статического объекта — <пространство_имён>_<Класс>.<объект>, а не короткий идентификатор. Если нужна часть после точки, ассистенту СЛЕДУЕТ использовать basicName из системного модуля Utils.

  2. Имена свойств СЛЕДУЕТ делать краткими и избегать лишних слов.

  3. Ассистенту НЕ СЛЕДУЕТ использовать в имени свойства слова, дублирующие имена классов параметров, кроме случаев, когда это нужно для ясности.

  4. Ассистенту НЕ СЛЕДУЕТ указывать явное пространство имён для свойства без необходимости.

  5. Создавая для собственного атрибута одного объекта DATA-свойство — или простую композицию от DATA-свойства (например, когда тянется имя связанного объекта), — ассистент ОБЯЗАН осознанно решить, относить ли его к системной группе id или base через IN.

    Атрибуты, образующие бизнес-идентификацию объекта и показываемые в его представлении, СЛЕДУЕТ относить к группе id; прочие основные атрибуты — к группе base (id вложена в base).

    Свойство НЕ СЛЕДУЕТ помещать в id или base, когда оно не является собственным основным атрибутом объекта.

  6. При делении значений целочисленных классов ассистент ОБЯЗАН приводить к NUMERIC один из операндов, а не результат.

    Отношение двух целых чисел — целочисленное деление, поэтому внешнее приведение вида NUMERIC[16,4](a * b / c) молча отбрасывает дробную часть; правильная форма — NUMERIC[16,4](a) * b / c.

  7. Класс в объявлении параметра (prop(SubClass x)) — это сигнатура, а не фильтр времени выполнения: он разрешает одноимённые свойства и задаёт сигнатуру, но вычисляемое множество определяется свойствами, использованными в выражении. Чтение свойства родительского класса с параметром, объявленным классом-потомком, всё равно идёт по ВСЕМ объектам родительского класса (например, в GROUP SUM — молча неверные итоги).

    Чтобы ограничить множество классом, ассистент ОБЯЗАН добавить явное условие x IS SubClass (или использовать свойство, объявленное на этом классе-потомке).

  8. В операторе GROUP ... BY ассистент НЕ ДОЛЖЕН перечислять в блоке BY верхние параметры, использованные в выражениях оператора: каждый такой параметр уже неявно является группировкой — параметром создаваемого свойства — и остаётся на своём месте в сигнатуре.

    При явном списке параметров слева BY-выражения отображаются по порядку только на параметры, не использованные в выражениях; несовпадение количества или классов даёт ошибку.

  9. MAX и MIN — префиксные операторы над списком операндов через запятую (MAX a, b), а не инфиксные: a MAX b не разбирается — платформа выдаёт no viable alternative at input 'MAX'.

    Список операндов тянется настолько, насколько позволяет выражение, поэтому всё после запятой принадлежит оператору: MAX a, b * c — это MAX(a, b * c), а x * MAX a, b корректно и без скобок. Там, где следующий оператор должен применяться к самому максимуму, оператор ОБЯЗАН быть взят в скобки: (MAX a, b) * c.

    Эти операторы сравнивают операнды одной строки; максимум по строкам — это GROUP MAX.

Правила абстрактных свойств (+=)

  1. Класс значения реализации += ОБЯЗАН укладываться в класс значения, объявленный у абстрактного свойства; неявного приведения нет — реализация с более широким классом отвергается при старте сервера с ошибкой «wrong value class of implementation», в строках specified и expected которой указаны класс реализации и объявленный класс.

    Чаще всего класс расширяет арифметика, причём сильнее, чем кажется:

    • + и - — как MIN / MAX и операторы выбора — дают общего предка, расширяя целую часть и шкалу независимо, поэтому результат может оказаться шире любого из операндов: NUMERIC[16,2] + NUMERIC[10,4] — это NUMERIC[18,4];
    • * складывает и целые части, и шкалы: NUMERIC[16,2] * NUMERIC[10,4] — это NUMERIC[26,6];
    • / расширяет катастрофически: при настройках по умолчанию его шкала всегда равна максимальной шкале NUMERIC (32), поэтому NUMERIC[16,2] / NUMERIC[16,2] — это NUMERIC[48,32].

    Агрегат GROUP в основном сохраняет класс того, что агрегирует, — GROUP SUM, GROUP MAX или GROUP LAST по NUMERIC[16,2] дают NUMERIC[16,2], — но выносит наружу то, до чего расширилось агрегируемое выражение. Сам по себе расширяет GROUP CONCAT: его результат — строка неограниченной длины (ISTRING при объявленном ISTRING[250]). Обычная конкатенация строк тоже расширяет, складывая длины операндов (ISTRING[326] при объявленном ISTRING[250]).

    Любое такое выражение ассистент ОБЯЗАН обернуть в явное приведение к объявленному классу: f(X x) += NUMERIC[16,2](a(x) / b(x)); f(X x) += ISTRING[250](a(x) + b(x));

    Для операндов целочисленных классов приведение ОБЯЗАНО сначала стоять на операнде, чтобы деление не оказалось целочисленным (см. правило 16 правил свойств); результат при этом всё равно расширяется до шкалы 32, как любое деление, поэтому внешнее приведение тоже нужно: f(X x) += NUMERIC[16,2](NUMERIC[16,2](a(x)) / b(x));

Правила упорядочивания (ORDER)

  1. Там, где две строки могут разделить ключ порядка, а ответ зависит от того, какая из них победит, — какая из двух строк одной даты станет GROUP LAST, какую из двух строк равного приоритета возьмет TOP 1, на какую шагнет назад PARTITION PREV, — ассистент ОБЯЗАН выписать различитель явно, обычно сам объект: ORDER date(d), d.

    Для части таких случаев платформа дополняет неполный порядок сама, поэтому симптомом будет не разброс от запуска к запуску, а то, что выбрана та строка, которую отбирает служебный порядок по интерфейсам, — а его предметная область не просила. Выписанный различитель и делает выбор задуманным.

  2. Накопительный PARTITION SUM ... ORDER без TOP и OFFSET — тот случай, когда различитель добавлять по привычке НЕ ДОЛЖНО. Его рамка по умолчанию дает всем строкам с одинаковым ключом порядка одно и то же накопленное значение. Добавленный различитель меняет результат — с итога на группу равных ключей на итог на строку, — а это решение о предметной области, а не мера предосторожности. Под TOP или OFFSET действует обычное правило 1: они отбирают строки, и какие именно — стоит сказать.

  3. PARTITION LAST не читает порядок, чтобы вычислить свое значение: это значение текущей строки. Отбирает по порядку GROUP LAST.

Правила: действия и присваивание

Правила действий

  1. Ассистент ОБЯЗАН избегать FOR, когда тот же результат можно выразить множественной (set-based) конструкцией.

    FOR итерирует строку за строкой, и к нему СЛЕДУЕТ прибегать в последнюю очередь, когда нет декларативной альтернативы. Единственное измеренное исключение работает в обратную сторону: когда присваиваемое значение — агрегат GROUP, границы которого коррелируют с обновляемой строкой, и оба множества велики, множественный вариант может скомпилироваться в запрос, материализующий всю корреляцию, и построчный FOR ... NOINLINE может оказаться быстрее — там, где индекс по агрегируемому классу позволяет отвечать на агрегат каждой строки индексным обращением.

    Предпочитайте множественные альтернативы, например:

    • агрегация или материализация множества -> GROUP SUM, GROUP CONCAT, GROUP MAX, GROUP LAST, GROUP AGGR
    • присваивание свойства по множеству -> прямое присваивание свойства с параметрами вместо цикла FOR ... DO
    • экспорт табличных или иерархических данных -> EXPORT FROM, EXPORT JSON FROM, EXPORT XML FROM, EXPORT CSV FROM
    • построение структурированных данных -> JSON FROM, XML FROM
    • массовые интеграционные записи -> NEW, DELETE или множественное изменение свойства вместо построчного FOR

    FOR приемлем, когда тело имеет настоящий построчный поток управления, такой как условный APPLY, MESSAGE, throwException или внешние вызовы, которые нельзя выразить как операцию над множеством.

  2. Параметры, вводимые в NEW alias = Class и FOR expr(p) [NEW alias = Class] DO { ... }, НЕ следуют обычным правилам лексической области видимости из мейнстримных языков программирования.

    Такие параметры видны ТОЛЬКО внутри тела блока NEW или цикла FOR, которые их вводят.

    Ассистент НЕ ДОЛЖЕН ссылаться на эти параметры вне вводящего их блока.

    Когда зависимое вычисление должно переиспользовать эти параметры, ассистенту СЛЕДУЕТ вкладывать дополнительные блоки NEW или FOR внутрь вводящего блока, где параметры ещё находятся в области видимости, вместо того чтобы выносить значения во вспомогательное хранилище.

    И наоборот, параметр, объявленный внутри агрегата GROUP, принадлежит этому агрегату и НЕ виден снаружи; в частности, он не может служить переменной цикла объемлющего FOR. Объявите переменную собственным параметром FOR, а агрегат используйте только как логическое условие по ней.

  3. Ассистенту СЛЕДУЕТ избегать введения свойств LOCAL без конкретной необходимости.

    LOCAL материализует временную таблицу в PostgreSQL, только когда содержит больше одной строки, поэтому стоимость выполнения, заметно превышающая стоимость стековой переменной в обычном языке, относится к LOCAL с параметрами (буферы, ключуемые номером строки, значения по объектам). LOCAL без параметров содержит не больше одной строки и всегда хранится в памяти, поэтому флаги и одиночные значения без параметров дёшевы; избегать их стоит не из-за стоимости, а чтобы не плодить сущности.

  4. LOCAL обычно оправдан, когда выполняются ОБА условия:

    • его значение нетривиально вычислить (агрегация, join'ы, многошаговая логика, внешние вызовы или другая работа, которую стоит материализовать), И
    • то же значение используется более одного раза, так что материализация позволяет избежать повторного вычисления.
  5. По возможности ассистенту СЛЕДУЕТ предпочитать альтернативы новому LOCAL:

    • встраивать выражение в каждое место использования, если оно дешёвое
    • вкладывать блоки NEW / FOR, чтобы промежуточные значения оставались в области видимости параметров
    • использовать обычное (не LOCAL) вычисляемое свойство, когда значение переиспользуется в нескольких действиях
  6. Это рекомендации, а не жёсткие запреты. Если ассистент не может найти рабочий синтаксис для конструкции без LOCAL или другой подход постоянно не получается и не выходит построить чистое действие, откат к LOCAL приемлем как последнее средство.

    Устоявшиеся паттерны LOCAL, предписанные другими правилами (например, промежуточное хранение при импорте, перенос между вложенными сессиями), остаются допустимыми; ассистенту СЛЕДУЕТ держать такие LOCAL минимальными по количеству и области.

  7. Параметры верхнеуровневых операторов тела действия разделяют один контекст параметров: одинаковые имена обозначают один и тот же параметр, а класс параметра объявляется только при первом использовании.

    В генерируемых скриптах (eval, наполнение данных) ассистенту СЛЕДУЕТ давать параметрам верхнеуровневых операторов уникальные имена, чтобы не зависеть от порядка операторов.

  8. Многие системные служебные действия возвращают результат через одноимённое локальное свойство без параметров (например, в Utils: действие fileExists[ISTRING[500]] пишет в свойство fileExists[]). Такой элемент — ДЕЙСТВИЕ, а не логическое свойство: ассистент ОБЯЗАН сначала вызвать действие, а затем прочитать свойство без параметров (fileExists(path); IF fileExists() THEN ...), и НЕ ДОЛЖЕН использовать форму с параметрами внутри выражения (IF fileExists(path) — неверно).

Правила присваивания (<-)

  1. Аргументы изменяемого свойства в левой части <- могут быть выражениями от параметров оператора (sentFolder(account(f)) <- f), но новые локальные параметры вводятся только как типизированные параметры, а не внутри выражений. Запись «по вычисляемому ключу» по аналогии с императивным map[key] = value легко нарушает это.

    Поэтому, перенаправляя ссылки-самоссылки при глубоком копировании графа объектов, ассистенту СЛЕДУЕТ держать обратное отображение и итерироваться с ЦЕЛЕВЫМ объектом в роли параметра — link(Copy n) <- newOf(link(srcOf(n))) WHERE spec(n); — а не писать link(newOf(x)) <- newOf(link(x));

  2. <- выражение IF условие присваивает всё выражение ВСЕМ объектам: там, где условие не выполняется, свойство перезаписывается NULL. Фактически это сброс-плюс-запись.

    ДОБАВЛЯЯ присваивание к свойству, уже заполненному ранее в том же действии, ассистент ОБЯЗАН использовать форму WHERE (prop(x) <- TRUE WHERE cond(x)), которая меняет только строки, подходящие под условие. Второе присваивание в форме IF тому же свойству ОБЯЗАНО рассматриваться как сигнал тревоги при ревью.

  3. Написанный прямо в теле действия или события PREV(<выражение>) переносит в состояние начала сессии ВСЁ обёрнутое выражение, включая подвыражения-аргументы: аргумент, вычисленный в текущей сессии (LOCAL, свойство объекта, созданного в сессии), внутри PREV читается как NULL, молча обнуляя результат.

    Чтобы прочитать предыдущие данные при текущих аргументах, ассистент ОБЯЗАН обернуть PREV в отдельное свойство — prevF(x) = PREV(f(x)); — и вызывать его, а не писать PREV(f(<вычисленный в сессии аргумент>)) в теле.

  4. Параметр, через который читается или изменяется свойство с именем, объявленным у нескольких классов, ОБЯЗАН быть аннотирован классом при первом использовании (date(Interaction i) <- ...): перегруженное имя разрешается по классам параметров, и нетипизированный параметр даёт ошибку «ambiguous name». Особенно это касается событий: их оператор — отдельный контекст параметров, в котором класс больше ниоткуда не выводится, а условие i IS Interaction класс параметра не задаёт.

Правила циклов (FOR, WHILE)

  1. FOR фиксирует свой набор до первой итерации: условие вычисляется один раз, подходящие строки читаются, и тело выполняется по разу на строку этого набора. То, что меняет тело, — включая данные под условием — не добавляет и не убирает итераций.

    Перечитывает набор WHILE, но делает это по ШАГАМ, а не по строкам: один шаг заново вычисляет условие, читает весь подходящий набор и выполняет тело для каждой его строки, и только потом набор читается снова; итерации прекращаются, когда он возвращается пустым. Поэтому строка, уже попавшая в текущий шаг, свою очередь получит, даже если более ранняя строка того же шага сделала условие для нее ложным.

  2. Без ORDER FOR обходит свой набор в произвольном порядке. Ассистент ОБЯЗАН задавать явный ORDER всюду, где результат зависит от последовательности — нумерация, накопительные итоги, любое чтение записанного предыдущей итерацией, — и всюду, где TOP ограничивает число взятых строк, и ОБЯЗАН завершать этот ORDER ключом, различающим любые две строки.

Правила потоков (NEWTHREAD, NEWEXECUTOR)

  1. Серверное поточное действие разделяет сессию изменений вызывающего кода, а сессии изменений не потокобезопасны.

    Поэтому ассистенту СЛЕДУЕТ оборачивать тело серверного NEWTHREAD в NEWSESSION, а когда ему нужна собственная транзакция базы — в NEWSESSION NEWSQL. Это размен: обычный NEWSESSION перестает видеть несохраненные изменения вызывающего, поэтому обертка опускается только там, где разделение сессии сделано намеренно И известно, что одновременно они не выполняются.

    Что дает обертка внутри транзакции APPLY, зависит от того, КОГДА тело стартует на самом деле. Пока транзакция открыта, NEWSESSION, включая NEWSQL, сессию не создает — действие откладывается в текущую, — а проверка происходит в момент выполнения тела, а не в момент его планирования. SCHEDULE DELAY — это число миллисекунд, а не барьер, ждущий применения, так что и он ничего не гарантирует. Ассистент НЕ ДОЛЖЕН рассчитывать, что поток, запущенный из глобального обработчика, окажется изолированным.

    Клиентский executor — обратный случай: действие доставляется в соединение пользователя и выполняется там в собственной свежей сессии, так что оборачивать его незачем.

Правила: события (WHEN)

Правила событий (WHEN)

  1. Событие WHEN срабатывает каждый раз, когда его условие становится истинным в текущей сессии, и записывает целевое свойство безусловно. Если то же целевое свойство также изменяется явно где-то ещё в сессии (ввод пользователя, присваивание в действии, импорт), событие перезаписывает это явное изменение.

  2. Когда задача события — лишь вывести или подставить значение по умолчанию из других входов, ассистенту СЛЕДУЕТ защищать условие через AND NOT CHANGED(<целевое>) для каждого целевого свойства, в которое пишет событие.

    Это предотвращает затирание событием явного изменения целевого свойства, сделанного в той же сессии.

  3. Защиту СЛЕДУЕТ опускать только когда событие должно принудительно перебивать любое явное изменение — например, поддерживаемые итоги, аудит-метки или инварианты, обходить которые пользователю не позволено.

  4. Правила 1-3 описывают форму события-действия WHEN <условие> DO <целевое> <- <выражение>. Форма вычисляемого события <целевое> <- <выражение> WHEN <условие> ведет себя иначе: его изменение вычисляется при обращении к целевому свойству, и явное изменение этого свойства в сессии приоритетнее изменения события.

    Поэтому для подстановки значения по умолчанию, уступающей явному изменению, достаточно самой формы вычисляемого события — защита не нужна. Проверять CHANGED(<целевое>) в его условии в любом случае нельзя: целевое свойство стало бы зависеть от собственного изменения, образуя цикл <целевое> -> CHANGED(<целевое>) -> <целевое>.

    При отсутствии явного изменения событие записывает значение выражения и тогда, когда оно NULL.

  5. Условие WHEN проверяется и на удаленных объектах. Удаление объекта сбрасывает его первичные свойства в NULL, поэтому условие, реагирующее на превращение значения в NULL, выполняется для каждого удаленного объекта, у которого значение было не NULL, и обработчик отрабатывает на уже несуществующем объекте.

    Какие это операторы изменения, решает переход, который каждый из них покрывает: DROPPED, CHANGED, DROPCHANGED и SETDROPPED включают переход из не-NULL в NULL и потому срабатывают на удалении; SET и SETCHANGED требуют, чтобы новое значение было не NULL, и не срабатывают.

    Там, где условие может сработать на пути в NULL, а обработчик не должен действовать на удалении или уходе объекта из класса, оно ОБЯЗАНО быть сужено через <объект> IS <Класс>.

Когда на самом деле выполняются локальные события

  1. Локальный обработчик события выполняется не в момент изменения данных, а в одной из точек жизни сессии: синхронизация формы, открытие формы, начало APPLY, создание вложенной сессии либо явный вызов System.executeLocalEvents[].

    Вне интерактивной формы — в действии, вызванном из внешней системы, в задании планировщика — из этих точек обычно случается только применение. Поэтому чтение свойства сразу после изменения данных, от которых оно зависит, вернет значение БЕЗ применения локальных обработчиков — в отличие от вычисляемого свойства, которое всегда актуально.

    Ассистент НЕ ДОЛЖЕН рассчитывать в таком месте на то, что локальный обработчик отработал: либо это делает APPLY, либо перед чтением вызывается System.executeLocalEvents[].

Правила: ограничения

  1. Когда выбор значения в одном свойстве должен быть ограничен значениями других свойств — соседние поля той же формы, текущий контекст, связанные объекты — ассистенту СЛЕДУЕТ в первую очередь рассматривать CONSTRAINT ... CHECKED BY <свойство>.

    CHECKED BY заставляет диалог изменения для указанного свойства автоматически фильтровать варианты, нарушающие ограничение, так что запрет применяется декларативно в момент выбора, а не постфактум.

    Фильтр доходит только до этих диалогов изменения. Механизм ввода, предлагающий значения как-то иначе, его не использует, и там нарушающее значение отвергается только при проверке самого ограничения.

  2. Откатываться к ручным фильтрам на форме или валидирующим действиям СЛЕДУЕТ, когда CHECKED BY не способен выразить ограничение (например, фильтр зависит от состояния UI, не смоделированного как свойство, или правило носит рекомендательный, а не обязательный характер), — а также когда ограничение ВЫРАЗИМО так, но значение выбирается другим механизмом, до которого фильтр CHECKED BY не доходит.

  3. Ассистенту НЕ СЛЕДУЕТ помещать в условие CONSTRAINT тяжелые группировки по большим таблицам (особенно вложенные нематериализованные): инкрементальная проверка при применении может развернуться в непрактично большой запрос, даже при заданных для свойств хинтах вычисления.

    Для таких дорогих проверок используйте событие WHEN: дешевое условие-детектор изменений, чтение тяжелых значений в LOCAL в обработчике, затем MESSAGE + CANCEL при нарушении.

Правила: сессии изменений (NEWSESSION, APPLY)

  1. Прежде чем вводить NEWSESSION, ассистент ОБЯЗАН решить, какое поведение сессии требуется. Ни один из вариантов ниже не работает внутри транзакции APPLY — в обработчике глобального события или в применяемом действии, — где сессия не создается вовсе: внутреннее действие откладывается и выполняется в текущей сессии, в той же транзакции. Ассистент НЕ ДОЛЖЕН рассчитывать там на независимую фиксацию.

    • изолированная независимая единица -> NEWSESSION
    • изолированная единица, которая также должна видеть отдельные локальные свойства верхней сессии -> NEWSESSION NESTED (...)
    • изолированная единица, которая должна видеть все локальные свойства верхней сессии -> NEWSESSION NESTED LOCAL
    • дочерний диалог или редактор, который должен работать с несохранёнными объектами верхней сессии и возвращать свои изменения в эту верхнюю сессию -> NESTEDSESSION
  2. Для действий, добавленных на формы, есть два основных паттерна:

    • паттерн readonly-формы: форма фактически только для просмотра, поэтому добавленные на неё действия СЛЕДУЕТ по умолчанию выполнять в новой сессии
    • паттерн редактируемой формы: форма имеет редактируемые свойства, поэтому любое добавленное на неё действие, использующее NEWSESSION, ДОЛЖНО либо: APPLY; IF canceled() THEN RETURN; перед NEWSESSION, либо быть полностью независимым от несохранённых изменений в этой форме
  3. Простой NEWSESSION — вариант по умолчанию для изолированной работы, которая не должна случайно применить ожидающие изменения формы вызывающей стороны.

    Типичные паттерны в исходниках:

    • readonly-списочные формы с PROPERTIES(...) NEWSESSION NEW, EDIT, DELETE
    • смена статусов или создание зависимых документов после предшествующего APPLY
    • внешние или интеграционные действия, изолирующие HTTP-вызовы и сохраняющие собственные результаты
    • небольшие немедленные обновления UI с NEWSESSION { APPLY { ... } }
  4. Если внутренняя логика зависит от локального состояния верхней сессии, такого как выделения, отметки или буферы импорта, ассистент ОБЯЗАН явно перенести это состояние через NESTED (...) или NESTED LOCAL в операторе либо объявить само свойство DATA LOCAL NESTED — такое переносится без перечисления в операторе. Под NEWSQL не работает ни то, ни другое: на собственном соединении не переносится ничего, поэтому ассистент НЕ ДОЛЖЕН сочетать NEWSQL с зависимостью от локального состояния верхней сессии.

  5. Успешный APPLY очищает сессию, а с ней по умолчанию и все LOCAL-свойства в ней: после возврата из такого APPLY обычный LOCAL снова пуст. APPLY, который не прошел или был отменен, оставляет сессию как была, вместе с локальными, — поэтому ассистент НЕ ДОЛЖЕН читать LOCAL после APPLY, чтобы отличить успех от неудачи; их различает canceled(). Внутри вложенной сессии очистки нет вовсе: изменения копируются в родительскую сессию, а вложенная остается стоять вместе со своими локальными.

    Вне вложенной сессии значение LOCAL переживает УСПЕШНЫЙ APPLY, когда выполнено ХОТЯ БЫ ОДНО из:

    • LOCAL объявлен как NESTED в момент объявления (LOCAL NESTED name = Type (); или name = DATA LOCAL NESTED Type (...);), ИЛИ
    • APPLY явно сохраняет его через APPLY NESTED (name1, ..., nameN) или APPLY NESTED LOCAL для всех локалов.

    Ассистент НЕ ДОЛЖЕН рассчитывать, что значение обычного LOCAL, вычисленное до УСПЕШНОГО APPLY, останется доступным после него. Сохраняют его два случая: вложенная сессия, где не очищается ничего, и применение, которое не прошло или было отменено, — оно оставляет сессию как была. Если накопленное значение должно жить дольше APPLY — например, буфер импорта, читаемый в пост-apply доработке — ассистент ОБЯЗАН либо объявить его с NESTED, либо перечислить его в APPLY NESTED (...) (или использовать APPLY NESTED LOCAL) в месте вызова.

  6. При использовании NEWSESSION NESTED (...) или NEWSESSION NESTED LOCAL ассистенту СЛЕДУЕТ сохранять те же вложенные локальные свойства на APPLY, если результат должен быть скопирован обратно в верхнюю сессию, например через APPLY NESTED (...) или APPLY NESTED LOCAL.

  7. Ассистент НЕ ДОЛЖЕН заменять NESTEDSESSION простым NEWSESSION для дочерних форм или диалогов, привязанных к родительскому объекту, который может быть ещё не сохранён в текущей сессии формы.

  8. Прежде чем открывать свежую NEWSESSION из действия, запущенного на форме редактирования, ассистенту СЛЕДУЕТ решить, нужно ли сначала сохранить текущие изменения формы.

    Распространённый паттерн: APPLY; IF canceled() THEN RETURN; NEWSESSION { ... }

    Этот паттерн используется перед сменой статусов, генерацией документов и другими изолированными последующими действиями.

  9. После APPLY ассистент ОБЯЗАН проверять canceled() только тогда, когда последующая логика зависит от того, удалось ли сохранение — для раннего выхода, пропуска побочного эффекта или отката отложенной работы.

    APPLY в интерактивном контексте сам показывает пользователю сообщение об ограничении. Ассистент НЕ ДОЛЖЕН добавлять IF canceled() THEN MESSAGE applyMessage() после APPLY в интерактивных действиях только для того, чтобы сообщить об ошибке — это дублирует сообщение, которое уже показала платформа. Явный вывод через applyMessage() или throwException(applyMessage()) нужен только для не интерактивных вызывающих сторон (API-эндпоинты, фоновые интеграции), где никакой диалог не показывается.

    Если APPLY не проходит из-за ограничения, изменения остаются несохранёнными в текущей сессии, и любой последующий APPLY в той же сессии также не пройдёт, пока проблемные данные не исправлены или изменения не отменены (например, через CANCEL).

  10. Ассистенту СЛЕДУЕТ держать блоки NEWSESSION небольшими и целевыми: изолировать одну единицу работы, применить её при необходимости и выйти.

    Ассистент НЕ ДОЛЖЕН вводить NEWSESSION лишь для сокрытия багов видимости сессии. Если изменения верхней сессии должны оставаться видимыми, требуется семантика вложенной сессии.

  11. Тело APPLY может выполниться не один раз. Транзакция применения МОЖЕТ быть автоматически повторена после update conflict, deadlock или таймаута — будет ли, зависит от сбоя и от лимита попыток, — а применяемое действие и синхронные глобальные обработчики находятся внутри того, что повтор выполняет заново.

    Поэтому они ОБЯЗАНЫ выдерживать повтор. Необратимый внешний побочный эффект — отправка письма, вызов HTTP API, печать, запись файла — НЕ ДОЛЖЕН выполняться там: его место после успешного применения, где canceled() говорит, состоялось ли оно.