letopis 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-CHEATSHEET.en.md +14 -2
- package/AGENT-CHEATSHEET.md +14 -2
- package/CHANGELOG.md +186 -0
- package/README.en.md +134 -24
- package/README.md +124 -25
- package/dist/admin.d.ts +4 -0
- package/dist/admin.js +6 -0
- package/dist/cache.d.ts +33 -9
- package/dist/cache.js +81 -32
- package/dist/chain.d.ts +29 -7
- package/dist/chain.js +61 -12
- package/dist/cli.js +24 -7
- package/dist/import.d.ts +6 -0
- package/dist/import.js +196 -175
- package/dist/index.d.ts +11 -3
- package/dist/index.js +122 -18
- package/dist/load.d.ts +63 -0
- package/dist/load.js +293 -37
- package/dist/signals.d.ts +73 -0
- package/dist/signals.js +169 -0
- package/dist/sql.d.ts +1 -1
- package/dist/sql.js +17 -2
- package/dist/tx.d.ts +9 -7
- package/dist/tx.js +15 -5
- package/dist/typed.d.ts +21 -6
- package/dist/types.d.ts +76 -3
- package/dist/types.js +7 -2
- package/dist/up.d.ts +16 -2
- package/dist/up.js +34 -4
- package/dist/watch.d.ts +11 -2
- package/dist/watch.js +24 -6
- package/dist/write.d.ts +10 -1
- package/dist/write.js +32 -5
- package/llms.txt +4 -1
- package/package.json +2 -1
- package/sql/10-core.sql +8 -1
- package/sql/60-write.sql +100 -35
- package/sql/75-history.sql +358 -0
- package/sql/80-auth.sql +3 -3
- package/sql/90-time.sql +45 -9
- package/sql/95-seed.system.sql +12 -4
- package/sql/99-revision.sql +1 -1
- package/sql/README.md +1 -0
- package/sql/migrate/0006-changed-paths.sql +3 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
-- letopis 1.1 — история по журналу: diff и blame (план 1.1, §2.1, этап 1).
|
|
2
|
+
--
|
|
3
|
+
-- diff_versions и blame — с правами вызывающего (security invoker): журнал читается под политикой log_select,
|
|
4
|
+
-- поэтому чужие и скрытые версии в расчёт не попадают. Невидимая версия выглядит как пропуск номера
|
|
5
|
+
-- версии — такие результаты помечаются exact: false. Колонка log.changed приложению не выдана (ревью №1
|
|
6
|
+
-- точки А плана 1.1): пути версии описывают разницу с предыдущей версией, а та читателю может быть не видна;
|
|
7
|
+
-- blame получает их функцией владельца version_paths, только когда предыдущая версия видна. Строку журнала
|
|
8
|
+
-- целиком (l.*) приложение не читает — функции перечисляют колонки.
|
|
9
|
+
-- jsonb_changes — различия двух значений jsonb по путям;
|
|
10
|
+
-- row_changes — различия двух снимков строки по разделам;
|
|
11
|
+
-- change_paths — пути для колонки changed (пишут триггер правки и log_version);
|
|
12
|
+
-- row_leaves — листья снимка строки (то, что объясняет blame);
|
|
13
|
+
-- version_paths — пути из changed тех версий, у которых предыдущая версия видна вызывающему;
|
|
14
|
+
-- diff_versions — что изменилось между двумя версиями или моментами (jsonb в виде ответа diff());
|
|
15
|
+
-- blame — какая версия, кто, когда и почему последней изменила каждый лист (ответ blame()).
|
|
16
|
+
|
|
17
|
+
-- Различия значений по путям: объекты — по ключам рекурсивно, массивы и скаляры — целиком. Отсутствующий
|
|
18
|
+
-- ключ (SQL NULL) и JSON null различаются: added — ключа не было, removed — ключа не стало, changed —
|
|
19
|
+
-- значение другое. Добавленный или удалённый объект раскрывается до листьев; пустой объект — лист.
|
|
20
|
+
create or replace function @SCHEMA@.jsonb_changes(p_a jsonb, p_b jsonb, p_prefix text[] default '{}')
|
|
21
|
+
returns table (path text[], kind text, before jsonb, after jsonb)
|
|
22
|
+
language sql immutable parallel safe
|
|
23
|
+
begin atomic
|
|
24
|
+
with recursive w(path, a, b) as (
|
|
25
|
+
select p_prefix, p_a, p_b
|
|
26
|
+
union all
|
|
27
|
+
select w.path || k.key, w.a -> k.key, w.b -> k.key
|
|
28
|
+
from w cross join lateral (
|
|
29
|
+
select jsonb_object_keys(case when jsonb_typeof(w.a) = 'object' then w.a else '{}'::jsonb end) as key
|
|
30
|
+
union
|
|
31
|
+
select jsonb_object_keys(case when jsonb_typeof(w.b) = 'object' then w.b else '{}'::jsonb end)
|
|
32
|
+
) k
|
|
33
|
+
where (w.a is null or jsonb_typeof(w.a) = 'object') and (w.b is null or jsonb_typeof(w.b) = 'object')
|
|
34
|
+
and w.a is distinct from w.b
|
|
35
|
+
and not (w.a is null and w.b = '{}'::jsonb) and not (w.b is null and w.a = '{}'::jsonb)
|
|
36
|
+
)
|
|
37
|
+
select w.path, case when w.a is null then 'added' when w.b is null then 'removed' else 'changed' end, w.a, w.b
|
|
38
|
+
from w
|
|
39
|
+
where w.a is distinct from w.b
|
|
40
|
+
and not ((w.a is null or jsonb_typeof(w.a) = 'object') and (w.b is null or jsonb_typeof(w.b) = 'object')
|
|
41
|
+
and not (w.a is null and w.b = '{}'::jsonb) and not (w.b is null and w.a = '{}'::jsonb))
|
|
42
|
+
order by 1;
|
|
43
|
+
end;
|
|
44
|
+
|
|
45
|
+
-- Различия двух снимков строки по разделам: data — jsonb_changes с путём от 'data'; одиночный конец —
|
|
46
|
+
-- значение целиком; множественный конец и теги — множества: added и removed по каждому элементу (elem —
|
|
47
|
+
-- сам элемент); owner и class — значение. Снимок «до» пуст (объекта не было): всё added.
|
|
48
|
+
create or replace function @SCHEMA@.row_changes(a_data jsonb, a_links jsonb, a_tags text[], a_owner uuid, a_class text,
|
|
49
|
+
b_data jsonb, b_links jsonb, b_tags text[], b_owner uuid, b_class text)
|
|
50
|
+
returns table (path text[], kind text, before jsonb, after jsonb, elem text)
|
|
51
|
+
language sql immutable parallel safe
|
|
52
|
+
begin atomic
|
|
53
|
+
with r as (
|
|
54
|
+
select k.key, a_links -> k.key as a, b_links -> k.key as b
|
|
55
|
+
from (select jsonb_object_keys(coalesce(a_links, '{}'::jsonb)) as key
|
|
56
|
+
union select jsonb_object_keys(coalesce(b_links, '{}'::jsonb))) k
|
|
57
|
+
), m as (
|
|
58
|
+
-- изменившиеся множественные концы: только они раскладываются на элементы, и разностью множеств, а не
|
|
59
|
+
-- проверкой вхождения на каждый элемент — та шла квадратом длины, и правка любого поля объекта с тысячами
|
|
60
|
+
-- целей стоила секунды в триггере правки (ревью №1 точки А)
|
|
61
|
+
select r.key, coalesce(r.a, '[]'::jsonb) as a, coalesce(r.b, '[]'::jsonb) as b from r
|
|
62
|
+
where r.a is distinct from r.b
|
|
63
|
+
and (r.a is null or jsonb_typeof(r.a) = 'array') and (r.b is null or jsonb_typeof(r.b) = 'array')
|
|
64
|
+
)
|
|
65
|
+
select c.path, c.kind, c.before, c.after, null::text from @SCHEMA@.jsonb_changes(a_data, b_data, '{data}') c
|
|
66
|
+
union all
|
|
67
|
+
select array['links', r.key], case when r.a is null then 'added' when r.b is null then 'removed' else 'changed' end, r.a, r.b, null::text
|
|
68
|
+
from r
|
|
69
|
+
where r.a is distinct from r.b
|
|
70
|
+
and not ((r.a is null or jsonb_typeof(r.a) = 'array') and (r.b is null or jsonb_typeof(r.b) = 'array'))
|
|
71
|
+
union all
|
|
72
|
+
select array['links', m.key], 'added', null::jsonb, d.v, d.v #>> '{}'
|
|
73
|
+
from m cross join lateral (select jsonb_array_elements(m.b) except select jsonb_array_elements(m.a)) d(v)
|
|
74
|
+
union all
|
|
75
|
+
select array['links', m.key], 'removed', d.v, null::jsonb, d.v #>> '{}'
|
|
76
|
+
from m cross join lateral (select jsonb_array_elements(m.a) except select jsonb_array_elements(m.b)) d(v)
|
|
77
|
+
union all
|
|
78
|
+
select array['tags'], 'added', null::jsonb, to_jsonb(d.t), d.t
|
|
79
|
+
from (select unnest(coalesce(b_tags, '{}'::text[])) except select unnest(coalesce(a_tags, '{}'::text[]))) d(t)
|
|
80
|
+
union all
|
|
81
|
+
select array['tags'], 'removed', to_jsonb(d.t), null::jsonb, d.t
|
|
82
|
+
from (select unnest(coalesce(a_tags, '{}'::text[])) except select unnest(coalesce(b_tags, '{}'::text[]))) d(t)
|
|
83
|
+
union all
|
|
84
|
+
select array['owner'], case when a_owner is null then 'added' when b_owner is null then 'removed' else 'changed' end,
|
|
85
|
+
to_jsonb(a_owner), to_jsonb(b_owner), null::text
|
|
86
|
+
where a_owner is distinct from b_owner
|
|
87
|
+
union all
|
|
88
|
+
select array['class'], case when a_class is null then 'added' when b_class is null then 'removed' else 'changed' end,
|
|
89
|
+
to_jsonb(a_class), to_jsonb(b_class), null::text
|
|
90
|
+
where a_class is distinct from b_class;
|
|
91
|
+
end;
|
|
92
|
+
|
|
93
|
+
-- до ревью №1 точки А помощники принимали строку журнала целиком; приложению она теперь недоступна (changed)
|
|
94
|
+
drop function if exists @SCHEMA@.version_changes(@SCHEMA@.log, @SCHEMA@.log);
|
|
95
|
+
drop function if exists @SCHEMA@.version_leaves(@SCHEMA@.log);
|
|
96
|
+
|
|
97
|
+
-- Пути изменения для колонки log.changed (ревизия 6): пути row_changes, у элемента множества — с самим
|
|
98
|
+
-- элементом в конце (['links', роль, id], ['tags', тег]). Пишут триггер правки и log_version.
|
|
99
|
+
create or replace function @SCHEMA@.change_paths(a_data jsonb, a_links jsonb, a_tags text[], a_owner uuid, a_class text,
|
|
100
|
+
b_data jsonb, b_links jsonb, b_tags text[], b_owner uuid, b_class text)
|
|
101
|
+
returns jsonb
|
|
102
|
+
language sql immutable parallel safe
|
|
103
|
+
begin atomic
|
|
104
|
+
select coalesce(jsonb_agg(to_jsonb(c.path || case when c.elem is not null then array[c.elem] else '{}'::text[] end)
|
|
105
|
+
order by c.path, c.elem), '[]'::jsonb)
|
|
106
|
+
from @SCHEMA@.row_changes(a_data, a_links, a_tags, a_owner, a_class, b_data, b_links, b_tags, b_owner, b_class) c;
|
|
107
|
+
end;
|
|
108
|
+
|
|
109
|
+
-- Листья снимка строки для blame: скаляры, массивы и пустые объекты data; роль одиночного конца; каждый id
|
|
110
|
+
-- множественного конца (путь — роль и id); каждый тег; owner; class.
|
|
111
|
+
create or replace function @SCHEMA@.row_leaves(p_data jsonb, p_links jsonb, p_tags text[], p_owner uuid, p_class text)
|
|
112
|
+
returns setof text[]
|
|
113
|
+
language sql immutable parallel safe
|
|
114
|
+
begin atomic
|
|
115
|
+
with recursive w(path, v) as (
|
|
116
|
+
select '{data}'::text[], p_data
|
|
117
|
+
union all
|
|
118
|
+
select w.path || k.key, w.v -> k.key
|
|
119
|
+
from w cross join lateral jsonb_object_keys(case when jsonb_typeof(w.v) = 'object' then w.v else '{}'::jsonb end) k(key)
|
|
120
|
+
where jsonb_typeof(w.v) = 'object' and w.v <> '{}'::jsonb
|
|
121
|
+
)
|
|
122
|
+
select w.path from w where w.v is not null and not (jsonb_typeof(w.v) = 'object' and w.v <> '{}'::jsonb)
|
|
123
|
+
union all
|
|
124
|
+
select array['links', k.key] from jsonb_each(coalesce(p_links, '{}'::jsonb)) k(key, val) where jsonb_typeof(k.val) <> 'array'
|
|
125
|
+
union all
|
|
126
|
+
select array['links', k.key, e.v #>> '{}'] from jsonb_each(coalesce(p_links, '{}'::jsonb)) k(key, val)
|
|
127
|
+
cross join lateral jsonb_array_elements(case when jsonb_typeof(k.val) = 'array' then k.val else '[]'::jsonb end) e(v)
|
|
128
|
+
union all
|
|
129
|
+
select array['tags', t.t] from unnest(coalesce(p_tags, '{}'::text[])) t(t)
|
|
130
|
+
union all
|
|
131
|
+
select array['owner'] where p_owner is not null
|
|
132
|
+
union all
|
|
133
|
+
select array['class'] where p_class is not null;
|
|
134
|
+
end;
|
|
135
|
+
|
|
136
|
+
-- Пути из колонки changed для blame. Колонка приложению не выдана: пути версии описывают разницу с предыдущей
|
|
137
|
+
-- версией, а та читателю может быть не видна — условие ACL на теги, данные или концы скрывает одну версию и
|
|
138
|
+
-- пропускает следующую, и в путях видны снятые теги, цели и ключи скрытой (ревью №1 точки А). Пути отдаются
|
|
139
|
+
-- только для версии, у которой и она сама, и предыдущая версия того же id — настоящие версии, видимые
|
|
140
|
+
-- вызывающему (то же условие, что у политики log_select: row_visible с планом прав чтения). Такую разницу
|
|
141
|
+
-- вызывающий и так получит сравнением двух видимых ему строк.
|
|
142
|
+
create or replace function @SCHEMA@.version_paths(p_ids uuid[], p_revs int[])
|
|
143
|
+
returns table (id uuid, rev int, changed jsonb)
|
|
144
|
+
language plpgsql stable security definer
|
|
145
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
146
|
+
as $$
|
|
147
|
+
#variable_conflict use_column
|
|
148
|
+
declare
|
|
149
|
+
a jsonb := @SCHEMA@.acl_actor('READ');
|
|
150
|
+
c_tenant uuid := (a->>'tenant')::uuid;
|
|
151
|
+
c_account uuid := (a->>'account')::uuid;
|
|
152
|
+
c_plan jsonb := a->'plan';
|
|
153
|
+
begin
|
|
154
|
+
if c_account is null then
|
|
155
|
+
return;
|
|
156
|
+
end if;
|
|
157
|
+
return query
|
|
158
|
+
select n.id, n.rev, n.changed
|
|
159
|
+
from unnest(p_ids, p_revs) q(id, rev)
|
|
160
|
+
join @SCHEMA@.log n on n.id = q.id and n.rev = q.rev
|
|
161
|
+
join @SCHEMA@.log o on o.id = n.id and o.rev = n.rev - 1
|
|
162
|
+
where n.changed is not null
|
|
163
|
+
and n.op not in ('purge', 'trim', 'reset') and o.op not in ('purge', 'trim', 'reset')
|
|
164
|
+
and @SCHEMA@.row_visible(n.tenant, n.class, n.id, n.owner, n.tags, n.data, n.links, c_tenant, c_account, c_plan)
|
|
165
|
+
and @SCHEMA@.row_visible(o.tenant, o.class, o.id, o.owner, o.tags, o.data, o.links, c_tenant, c_account, c_plan);
|
|
166
|
+
end $$;
|
|
167
|
+
|
|
168
|
+
-- diff: что изменилось между двумя версиями объекта (ответ терминала diff()). «После» — номер версии
|
|
169
|
+
-- p_to, последняя версия на момент p_to_at или последняя вообще; «до» — номер p_from, последняя живая
|
|
170
|
+
-- версия на момент p_from_at (объекта не было — null, все пути added) или версия перед «после».
|
|
171
|
+
-- p_follow — через смену id (reclass, rekey) по колонке moved; надгробие переноса — не версия.
|
|
172
|
+
-- «После» — надгробие: to.deleted, изменения полей не перечисляются.
|
|
173
|
+
create or replace function @SCHEMA@.diff_versions(p_id uuid, p_from int default null, p_to int default null,
|
|
174
|
+
p_from_at timestamptz default null, p_to_at timestamptz default null, p_follow boolean default false)
|
|
175
|
+
returns jsonb
|
|
176
|
+
language plpgsql stable
|
|
177
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
178
|
+
-- JIT: оценка стоимости запроса по журналу завышена (jsonb_array_elements — 100 строк на вызов), и компиляция
|
|
179
|
+
-- JIT занимала бы больше секунды при исполнении в миллисекунды (замер этапа 1б)
|
|
180
|
+
set jit = off
|
|
181
|
+
as $$
|
|
182
|
+
declare
|
|
183
|
+
ids uuid[] := array[p_id];
|
|
184
|
+
t record;
|
|
185
|
+
f record;
|
|
186
|
+
has_f boolean;
|
|
187
|
+
begin
|
|
188
|
+
if p_follow then
|
|
189
|
+
with recursive g(id) as (
|
|
190
|
+
select p_id
|
|
191
|
+
union
|
|
192
|
+
select x.id from g cross join lateral (
|
|
193
|
+
select l.moved as id from @SCHEMA@.log l where l.id = g.id and l.moved is not null
|
|
194
|
+
union select l.id from @SCHEMA@.log l where l.moved = g.id) x)
|
|
195
|
+
select array_agg(g.id) into ids from g;
|
|
196
|
+
end if;
|
|
197
|
+
select l.id, l.rev, l.at, l.seq, l.op, l.class, l.data, l.links, l.tags, l.owner into t from @SCHEMA@.log l
|
|
198
|
+
where l.id = any (ids) and l.op not in ('purge', 'trim', 'reset') and not (p_follow and l.op = 'delete' and l.moved is not null)
|
|
199
|
+
and (p_to is null or (l.id = p_id and l.rev = p_to)) and (p_to_at is null or l.at <= p_to_at)
|
|
200
|
+
order by l.at desc, l.seq desc limit 1;
|
|
201
|
+
if t.id is null then
|
|
202
|
+
return jsonb_build_object('id', p_id, 'class', null, 'from', null, 'to', null, 'changes', '[]'::jsonb);
|
|
203
|
+
end if;
|
|
204
|
+
if p_from is not null then
|
|
205
|
+
select l.id, l.rev, l.at, l.op, l.class, l.data, l.links, l.tags, l.owner into f from @SCHEMA@.log l
|
|
206
|
+
where l.id = p_id and l.rev = p_from and l.op not in ('purge', 'trim', 'reset');
|
|
207
|
+
has_f := f.id is not null;
|
|
208
|
+
elsif p_from_at is not null then
|
|
209
|
+
select l.id, l.rev, l.at, l.op, l.class, l.data, l.links, l.tags, l.owner into f from @SCHEMA@.log l
|
|
210
|
+
where l.id = any (ids) and l.op not in ('purge', 'trim', 'reset') and not (p_follow and l.op = 'delete' and l.moved is not null)
|
|
211
|
+
and l.at <= p_from_at
|
|
212
|
+
order by l.at desc, l.seq desc limit 1;
|
|
213
|
+
-- срез на момент: удалённого объекта тогда не было
|
|
214
|
+
has_f := f.id is not null and f.op <> 'delete';
|
|
215
|
+
else
|
|
216
|
+
select l.id, l.rev, l.at, l.op, l.class, l.data, l.links, l.tags, l.owner into f from @SCHEMA@.log l
|
|
217
|
+
where l.id = any (ids) and l.op not in ('purge', 'trim', 'reset') and not (p_follow and l.op = 'delete' and l.moved is not null)
|
|
218
|
+
and (l.at, l.seq) < (t.at, t.seq)
|
|
219
|
+
order by l.at desc, l.seq desc limit 1;
|
|
220
|
+
has_f := f.id is not null;
|
|
221
|
+
end if;
|
|
222
|
+
return jsonb_build_object(
|
|
223
|
+
'id', p_id,
|
|
224
|
+
'class', t.class,
|
|
225
|
+
'from', case when not has_f then null
|
|
226
|
+
else jsonb_build_object('rev', f.rev, 'at', f.at)
|
|
227
|
+
|| case when f.id <> p_id then jsonb_build_object('id', f.id) else '{}'::jsonb end end,
|
|
228
|
+
'to', jsonb_build_object('rev', t.rev, 'at', t.at)
|
|
229
|
+
|| case when t.id <> p_id then jsonb_build_object('id', t.id) else '{}'::jsonb end
|
|
230
|
+
|| case when t.op = 'delete' then '{"deleted": true}'::jsonb else '{}'::jsonb end,
|
|
231
|
+
'changes', case when t.op = 'delete' then '[]'::jsonb else coalesce((
|
|
232
|
+
select jsonb_agg(jsonb_build_object('path', to_jsonb(c.path), 'kind', c.kind)
|
|
233
|
+
|| case when c.before is not null then jsonb_build_object('before', c.before) else '{}'::jsonb end
|
|
234
|
+
|| case when c.after is not null then jsonb_build_object('after', c.after) else '{}'::jsonb end
|
|
235
|
+
order by c.path, c.kind, c.before::text, c.after::text)
|
|
236
|
+
from @SCHEMA@.row_changes(case when has_f then f.data end, case when has_f then f.links end,
|
|
237
|
+
case when has_f then f.tags end, case when has_f then f.owner end, case when has_f then f.class end,
|
|
238
|
+
t.data, t.links, t.tags, t.owner, t.class) c), '[]'::jsonb) end);
|
|
239
|
+
end $$;
|
|
240
|
+
|
|
241
|
+
-- blame: для каждого листа версии (последней на момент p_at или текущей) — версия, которая последней его
|
|
242
|
+
-- изменила, с автором, агентом, видом операции и причиной (ответ терминала blame()). Просматривается не
|
|
243
|
+
-- больше p_limit последних версий, назад через смену id (первая версия нового id указывает на прежний
|
|
244
|
+
-- колонкой moved). Пропуск номера версии (прореживание, обрезка, невидимая версия) — выпавшая версия могла
|
|
245
|
+
-- изменить лист и вернуть значение, поэтому лист точен (exact), только если от текущей версии до найденной
|
|
246
|
+
-- пропусков нет; лист, не менявшийся в просмотренной истории, принадлежит самой старой просмотренной версии —
|
|
247
|
+
-- exact, только если это версия 1 и пропусков нет вовсе. complete — история просмотрена целиком и без пропусков.
|
|
248
|
+
create or replace function @SCHEMA@.blame(p_id uuid, p_at timestamptz default null, p_limit int default 1000)
|
|
249
|
+
returns jsonb
|
|
250
|
+
language plpgsql stable
|
|
251
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
252
|
+
-- JIT: оценка стоимости запроса по журналу завышена (jsonb_array_elements — 100 строк на вызов), и компиляция
|
|
253
|
+
-- JIT занимала бы больше секунды при исполнении в миллисекунды (замер этапа 1б)
|
|
254
|
+
set jit = off
|
|
255
|
+
as $$
|
|
256
|
+
declare
|
|
257
|
+
ids uuid[] := array[p_id];
|
|
258
|
+
t record;
|
|
259
|
+
prev uuid;
|
|
260
|
+
lim int := greatest(coalesce(p_limit, 1000), 1);
|
|
261
|
+
res jsonb;
|
|
262
|
+
begin
|
|
263
|
+
select l.id, l.rev, l.at, l.seq, l.op, l.class, l.data, l.links, l.tags, l.owner into t from @SCHEMA@.log l
|
|
264
|
+
where l.id = p_id and l.op not in ('purge', 'trim', 'reset') and (p_at is null or l.at <= p_at)
|
|
265
|
+
order by l.rev desc limit 1;
|
|
266
|
+
if t.id is null then return null; end if;
|
|
267
|
+
loop
|
|
268
|
+
select l.moved into prev from @SCHEMA@.log l
|
|
269
|
+
where l.id = ids[cardinality(ids)] and l.moved is not null and l.op in ('reclass', 'rekey') order by l.rev limit 1;
|
|
270
|
+
exit when prev is null or prev = any (ids);
|
|
271
|
+
ids := ids || prev;
|
|
272
|
+
end loop;
|
|
273
|
+
with v as (
|
|
274
|
+
-- соседняя более старая версия — окном (lead), а не соединением CTE с собой: соединение шло бы вложенным
|
|
275
|
+
-- циклом по квадрату числа версий
|
|
276
|
+
select l.id, l.rev, l.at, l.seq, l.op, l.author, l.agent, l.reason, l.data, l.links, l.tags, l.owner, l.class,
|
|
277
|
+
row_number() over o as n, lead(l.id) over o as o_id, lead(l.rev) over o as o_rev,
|
|
278
|
+
lead(l.data) over o as o_data, lead(l.links) over o as o_links, lead(l.tags) over o as o_tags,
|
|
279
|
+
lead(l.owner) over o as o_owner, lead(l.class) over o as o_class
|
|
280
|
+
from @SCHEMA@.log l
|
|
281
|
+
where l.id = any (ids) and l.op not in ('purge', 'trim', 'reset') and not (l.op = 'delete' and l.moved is not null)
|
|
282
|
+
and (l.at, l.seq) <= (t.at, t.seq)
|
|
283
|
+
window o as (order by l.at desc, l.seq desc)
|
|
284
|
+
order by l.at desc, l.seq desc limit lim + 1
|
|
285
|
+
), w as (
|
|
286
|
+
select * from v where v.n <= lim
|
|
287
|
+
), gaps as (
|
|
288
|
+
select w.n from w where w.o_id = w.id and w.rev - w.o_rev > 1
|
|
289
|
+
), stored as materialized (
|
|
290
|
+
-- пути колонки changed (ревизия 6) — только у пар одного id без пропуска номера: version_paths отдаёт их,
|
|
291
|
+
-- лишь когда и предыдущая версия видна вызывающему; после пропуска пути описывали бы разницу с выпавшей
|
|
292
|
+
-- версией, а не с соседней в окне (ревью №1 точки А)
|
|
293
|
+
select p.id, p.rev, p.changed from @SCHEMA@.version_paths(
|
|
294
|
+
array(select w.id from w where w.o_id = w.id and w.o_rev = w.rev - 1),
|
|
295
|
+
array(select w.rev from w where w.o_id = w.id and w.o_rev = w.rev - 1)) p
|
|
296
|
+
), ch as (
|
|
297
|
+
select w.n, array(select jsonb_array_elements_text(c.p)) as path
|
|
298
|
+
from w join stored s on s.id = w.id and s.rev = w.rev cross join lateral jsonb_array_elements(s.changed) c(p)
|
|
299
|
+
union all
|
|
300
|
+
-- остальные пары (пропуск номера, смена id, версия без путей — создание, загрузчик, импорт) — сравнение
|
|
301
|
+
-- соседних версий окна
|
|
302
|
+
select w.n, c.path || case when c.elem is not null then array[c.elem] else '{}'::text[] end
|
|
303
|
+
from w cross join lateral @SCHEMA@.row_changes(w.o_data, w.o_links, w.o_tags, w.o_owner, w.o_class,
|
|
304
|
+
w.data, w.links, w.tags, w.owner, w.class) c
|
|
305
|
+
where w.o_id is not null and not exists (select 1 from stored s where s.id = w.id and s.rev = w.rev)
|
|
306
|
+
), leaves as (
|
|
307
|
+
select x as path from @SCHEMA@.row_leaves(t.data, t.links, t.tags, t.owner, t.class) x
|
|
308
|
+
), hit as (
|
|
309
|
+
select lv.path, min(ch.n) as n
|
|
310
|
+
-- лист и путь изменения совпадают по префиксу в любую сторону: изменён сам лист, объект над ним или
|
|
311
|
+
-- лист внутри (пустой объект стал непустым); элемент множества — по пути с элементом
|
|
312
|
+
from leaves lv join ch on lv.path[1:cardinality(ch.path)] = ch.path or ch.path[1:cardinality(lv.path)] = lv.path
|
|
313
|
+
group by lv.path
|
|
314
|
+
), oldest as (
|
|
315
|
+
select w.n, w.rev from w order by w.n desc limit 1
|
|
316
|
+
), cut as (
|
|
317
|
+
select count(*) > lim as cut from v
|
|
318
|
+
), fields as (
|
|
319
|
+
select lv.path,
|
|
320
|
+
jsonb_build_object('path', to_jsonb(lv.path), 'rev', cv.rev, 'at', cv.at, 'author', cv.author, 'op', cv.op,
|
|
321
|
+
'exact', case when h.n is not null then not exists (select 1 from gaps g where g.n <= h.n)
|
|
322
|
+
else (select o.rev = 1 from oldest o) and not (select c.cut from cut c) and not exists (select 1 from gaps) end)
|
|
323
|
+
|| case when cv.id <> p_id then jsonb_build_object('id', cv.id) else '{}'::jsonb end
|
|
324
|
+
|| case when cv.agent is not null then jsonb_build_object('agent', cv.agent) else '{}'::jsonb end
|
|
325
|
+
|| case when cv.reason is not null then jsonb_build_object('reason', cv.reason) else '{}'::jsonb end as obj
|
|
326
|
+
from leaves lv
|
|
327
|
+
left join hit h on h.path = lv.path
|
|
328
|
+
join w cv on cv.n = coalesce(h.n, (select o.n from oldest o))
|
|
329
|
+
)
|
|
330
|
+
select jsonb_build_object('id', p_id, 'class', t.class, 'rev', t.rev,
|
|
331
|
+
'complete', (select o.rev = 1 from oldest o) and not (select c.cut from cut c) and not exists (select 1 from gaps),
|
|
332
|
+
'fields', coalesce((select jsonb_agg(f.obj order by f.path) from fields f), '[]'::jsonb))
|
|
333
|
+
|| case when t.op = 'delete' then '{"deleted": true}'::jsonb else '{}'::jsonb end
|
|
334
|
+
into res;
|
|
335
|
+
return res;
|
|
336
|
+
end $$;
|
|
337
|
+
|
|
338
|
+
do $$
|
|
339
|
+
declare f text;
|
|
340
|
+
begin
|
|
341
|
+
foreach f in array array[
|
|
342
|
+
'jsonb_changes(jsonb, jsonb, text[])',
|
|
343
|
+
'row_changes(jsonb, jsonb, text[], uuid, text, jsonb, jsonb, text[], uuid, text)',
|
|
344
|
+
'change_paths(jsonb, jsonb, text[], uuid, text, jsonb, jsonb, text[], uuid, text)',
|
|
345
|
+
'row_leaves(jsonb, jsonb, text[], uuid, text)', 'version_paths(uuid[], int[])',
|
|
346
|
+
'diff_versions(uuid, int, int, timestamptz, timestamptz, boolean)', 'blame(uuid, timestamptz, int)'] loop
|
|
347
|
+
execute format('alter function @SCHEMA@.%s owner to @OWNER@', f);
|
|
348
|
+
execute format('revoke all on function @SCHEMA@.%s from public', f);
|
|
349
|
+
end loop;
|
|
350
|
+
end $$;
|
|
351
|
+
-- терминалы diff() и blame() вызывают их от приложения; сырой SQL — так же, под RLS журнала (version_paths
|
|
352
|
+
-- проверяет видимость обеих версий сама)
|
|
353
|
+
grant execute on function @SCHEMA@.jsonb_changes(jsonb, jsonb, text[]),
|
|
354
|
+
@SCHEMA@.row_changes(jsonb, jsonb, text[], uuid, text, jsonb, jsonb, text[], uuid, text),
|
|
355
|
+
@SCHEMA@.change_paths(jsonb, jsonb, text[], uuid, text, jsonb, jsonb, text[], uuid, text),
|
|
356
|
+
@SCHEMA@.row_leaves(jsonb, jsonb, text[], uuid, text), @SCHEMA@.version_paths(uuid[], int[]),
|
|
357
|
+
@SCHEMA@.diff_versions(uuid, int, int, timestamptz, timestamptz, boolean),
|
|
358
|
+
@SCHEMA@.blame(uuid, timestamptz, int) to @APP@;
|
package/sql/80-auth.sql
CHANGED
|
@@ -165,7 +165,7 @@ begin
|
|
|
165
165
|
where s.account = p_account and s.kind = 'SESSION' and s.deleted_at is null and s.id is distinct from p_except
|
|
166
166
|
returning s.material)
|
|
167
167
|
select array_agg('session:' || left(encode(k.material, 'hex'), 16)) into sig from k;
|
|
168
|
-
perform @SCHEMA@.
|
|
168
|
+
perform @SCHEMA@.signal_raw(sig);
|
|
169
169
|
return coalesce(cardinality(sig), 0);
|
|
170
170
|
end $$;
|
|
171
171
|
|
|
@@ -185,7 +185,7 @@ begin
|
|
|
185
185
|
and (s.meta->>'cred' = p_cred::text or (not s.meta ? 'cred' and coalesce(s.meta->'methods', '[]'::jsonb) ? p_kind))
|
|
186
186
|
returning s.material)
|
|
187
187
|
select array_agg('session:' || left(encode(k.material, 'hex'), 16)) into sig from k;
|
|
188
|
-
perform @SCHEMA@.
|
|
188
|
+
perform @SCHEMA@.signal_raw(sig);
|
|
189
189
|
return coalesce(cardinality(sig), 0);
|
|
190
190
|
end $$;
|
|
191
191
|
|
|
@@ -721,7 +721,7 @@ begin
|
|
|
721
721
|
select * into c from @SCHEMA@.ctx();
|
|
722
722
|
perform @SCHEMA@.auth_session_required(c.account);
|
|
723
723
|
update @SCHEMA@.secret s set deleted_at = clock_timestamp() where s.id = c.session and s.deleted_at is null returning s.material into m;
|
|
724
|
-
perform @SCHEMA@.
|
|
724
|
+
perform @SCHEMA@.signal_raw(array['session:' || left(encode(m, 'hex'), 16)]);
|
|
725
725
|
return m is not null;
|
|
726
726
|
end $$;
|
|
727
727
|
|
package/sql/90-time.sql
CHANGED
|
@@ -119,6 +119,36 @@ begin
|
|
|
119
119
|
return (select s.expires_at from @SCHEMA@.secret s where s.id = c.session);
|
|
120
120
|
end $$;
|
|
121
121
|
|
|
122
|
+
-- ========================= сигналы (план 1.1, §2.2) =========================
|
|
123
|
+
|
|
124
|
+
-- Режим сигналов установки — константа up({ signals }): class, tenant, opaque, none.
|
|
125
|
+
create or replace function @SCHEMA@.signal_mode() returns text
|
|
126
|
+
language sql immutable parallel safe
|
|
127
|
+
return '@SIGNALS@';
|
|
128
|
+
|
|
129
|
+
-- Ключ сигналов (hex) для сессии с правом letopis.signals по правилам System (сид выдаёт его сервисам) или
|
|
130
|
+
-- администраторского подключения: по нему процесс узнаёт в нагрузке токены классов и арендаторов.
|
|
131
|
+
-- Отдаётся производный ключ HMAC(ключ строки KEY, 'letopis:signals'), а не сам ключ: им подписаны пропуска прав.
|
|
132
|
+
create or replace function @SCHEMA@.signal_key() returns text
|
|
133
|
+
language plpgsql stable security definer
|
|
134
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
135
|
+
as $$
|
|
136
|
+
begin
|
|
137
|
+
if not @SCHEMA@.letopis_allowed('letopis.signals') then
|
|
138
|
+
perform @SCHEMA@.lt_fail('acl_denied', 'нет права letopis.signals');
|
|
139
|
+
end if;
|
|
140
|
+
return (select encode(@PGCRYPTO@.hmac(convert_to('letopis:signals', 'UTF8'), s.material, 'sha256'), 'hex')
|
|
141
|
+
from @SCHEMA@.secret s where s.id = '@SYSTEM@'::uuid and s.kind = 'KEY');
|
|
142
|
+
end $$;
|
|
143
|
+
|
|
144
|
+
-- Отпечаток описаний классов, видимых сессии (свои и System): в режиме сигналов none реестр библиотеки
|
|
145
|
+
-- сверяет его раз в 5 секунд вместо сигнала Class. Под RLS — тот же набор строк, что читает реестр.
|
|
146
|
+
create or replace function @SCHEMA@.class_stamp() returns text
|
|
147
|
+
language sql stable
|
|
148
|
+
set search_path = pg_catalog, @SCHEMA@, pg_temp
|
|
149
|
+
return (select encode(sha256(convert_to(coalesce(string_agg(e.id::text || ':' || e.rev::text, ',' order by e.id), ''), 'UTF8')), 'hex')
|
|
150
|
+
from @SCHEMA@.entity e where e.class = 'Class' and e.tenant in (@SCHEMA@.ctx_tenant(), '@SYSTEM@'::uuid));
|
|
151
|
+
|
|
122
152
|
-- ========================= restore, purge, обрезка истории =========================
|
|
123
153
|
|
|
124
154
|
-- Последняя запись журнала id под блокировкой, если она видна актору в его арендаторе: параллельные
|
|
@@ -256,7 +286,7 @@ begin
|
|
|
256
286
|
if exists (select 1 from @SCHEMA@.entity e where e.id = p_id) then
|
|
257
287
|
perform @SCHEMA@.lt_fail('not_deleted', 'объект восстановлен во время purge');
|
|
258
288
|
end if;
|
|
259
|
-
perform @SCHEMA@.signal(array[l.class]);
|
|
289
|
+
perform @SCHEMA@.signal(array[l.class], array[l.tenant]);
|
|
260
290
|
return jsonb_build_object('id', p_id, 'class', l.class, 'versions', n, 'purged', true);
|
|
261
291
|
end $$;
|
|
262
292
|
|
|
@@ -330,7 +360,7 @@ begin
|
|
|
330
360
|
total := total + k - case when first.op = 'trim' then 1 else 0 end;
|
|
331
361
|
classes := classes || r.class;
|
|
332
362
|
end loop;
|
|
333
|
-
perform @SCHEMA@.signal(
|
|
363
|
+
perform @SCHEMA@.signal(array_agg(d.x), array_agg(c.tenant)) from (select distinct x from unnest(classes) x) d;
|
|
334
364
|
return total;
|
|
335
365
|
end $$;
|
|
336
366
|
|
|
@@ -566,7 +596,9 @@ begin
|
|
|
566
596
|
perform @SCHEMA@.tombstone(a);
|
|
567
597
|
perform @SCHEMA@.auth_kill(p_account, null);
|
|
568
598
|
delete from @SCHEMA@.secret s where s.account = p_account;
|
|
569
|
-
|
|
599
|
+
-- данные арендатора-цели — его парами; Account и member — классы прав, их сигнал сбрасывает всё
|
|
600
|
+
perform @SCHEMA@.signal(classes || array['Account', 'member'],
|
|
601
|
+
array(select p_account from unnest(classes)) || array['@SYSTEM@'::uuid, '@SYSTEM@'::uuid]);
|
|
570
602
|
perform @SCHEMA@.acl_bump();
|
|
571
603
|
return jsonb_build_object('account', p_account, 'rows', n_rows, 'versions', n_log, 'memberships', n_mem, 'owned', n_own);
|
|
572
604
|
end $$;
|
|
@@ -615,7 +647,7 @@ begin
|
|
|
615
647
|
with d as (delete from @SCHEMA@.secret s where s.kind = 'SESSION' and s.id is distinct from c.session returning 1)
|
|
616
648
|
select count(*)::int into n from d;
|
|
617
649
|
perform @SCHEMA@.reset_mark('@SYSTEM@'::uuid, p_level, c.author);
|
|
618
|
-
perform @SCHEMA@.
|
|
650
|
+
perform @SCHEMA@.signal_raw(array['session:*']);
|
|
619
651
|
return jsonb_build_object('level', p_level, 'sessions', n);
|
|
620
652
|
when 'tenant' then
|
|
621
653
|
t := coalesce(p_tenant, c.tenant);
|
|
@@ -635,7 +667,7 @@ begin
|
|
|
635
667
|
return jsonb_build_object('level', p_level, 'tenant', t, 'rows', n, 'versions', m, 'done', false);
|
|
636
668
|
end if;
|
|
637
669
|
perform @SCHEMA@.reset_mark(t, p_level, c.author);
|
|
638
|
-
perform @SCHEMA@.
|
|
670
|
+
perform @SCHEMA@.signal_raw(array['*']);
|
|
639
671
|
perform @SCHEMA@.acl_bump();
|
|
640
672
|
return jsonb_build_object('level', p_level, 'tenant', t, 'rows', n, 'versions', m, 'done', true);
|
|
641
673
|
when 'all' then
|
|
@@ -644,7 +676,7 @@ begin
|
|
|
644
676
|
perform @SCHEMA@.seed_system();
|
|
645
677
|
key := 'lts_' || encode(@PGCRYPTO@.gen_random_bytes(32), 'hex');
|
|
646
678
|
perform @SCHEMA@.bootstrap_service(key);
|
|
647
|
-
perform @SCHEMA@.
|
|
679
|
+
perform @SCHEMA@.signal_raw(array['*', 'session:*']);
|
|
648
680
|
return jsonb_build_object('level', p_level, 'serviceKey', key);
|
|
649
681
|
else
|
|
650
682
|
perform @SCHEMA@.lt_fail('invalid_data', format('уровень сброса %s: sessions, tenant или all', p_level));
|
|
@@ -917,7 +949,10 @@ begin
|
|
|
917
949
|
message = 'letopis: cursor_expired: курсор подписки старше яруса all — события могли проредиться, перечитайте текущее состояние';
|
|
918
950
|
end if;
|
|
919
951
|
end if;
|
|
920
|
-
|
|
952
|
+
-- колонки — явно: changed приложению не выдана (ревью №1 точки А плана 1.1), в ответе подписки она null
|
|
953
|
+
return query select l.seq, l.tx, l.id, l.rev, l.class, l.cv, l.tenant, l.owner, l.links, l.data, l.tags, l.at, l.author,
|
|
954
|
+
l.agent, l.op, l.reason, l.moved, l.prev_hash, l.hash, null::jsonb
|
|
955
|
+
from @SCHEMA@.log l
|
|
921
956
|
where (l.tx, l.seq) > (p_tx, p_seq) and l.tx < horizon and (p_classes is null or l.class = any (p_classes))
|
|
922
957
|
and (p_snap is null or not pg_visible_in_snapshot(l.tx, p_snap))
|
|
923
958
|
order by l.tx, l.seq limit p_limit;
|
|
@@ -940,7 +975,7 @@ declare f text;
|
|
|
940
975
|
begin
|
|
941
976
|
foreach f in array array[
|
|
942
977
|
'history_default()', 'history_policy(jsonb)', 'history_iv(text)', 'history_horizon(jsonb)',
|
|
943
|
-
'history_keep(jsonb, timestamp, timestamp, timestamp)', 'admin_role()', 'letopis_allowed(text)', 'session_expires()',
|
|
978
|
+
'history_keep(jsonb, timestamp, timestamp, timestamp)', 'admin_role()', 'letopis_allowed(text)', 'session_expires()', 'signal_mode()', 'signal_key()', 'class_stamp()',
|
|
944
979
|
'last_version_visible(uuid, uuid, jsonb)', 'restore(uuid)', 'purge(uuid, boolean)', 'trim_history(uuid[], text[], timestamptz)',
|
|
945
980
|
'rekey(uuid, jsonb, jsonb)', 'rekey_class(text, jsonb)', 'account_purge(uuid)', 'reset_mark(uuid, text, uuid)',
|
|
946
981
|
'reset(text, text, uuid, int)', 'thin_batch(timestamptz, int)', 'maintain(timestamptz, int)', 'verify(boolean)',
|
|
@@ -954,4 +989,5 @@ grant execute on function @SCHEMA@.restore(uuid), @SCHEMA@.purge(uuid, boolean),
|
|
|
954
989
|
@SCHEMA@.rekey(uuid, jsonb, jsonb), @SCHEMA@.rekey_class(text, jsonb), @SCHEMA@.account_purge(uuid),
|
|
955
990
|
@SCHEMA@.reset(text, text, uuid, int), @SCHEMA@.maintain(timestamptz, int), @SCHEMA@.verify(boolean),
|
|
956
991
|
@SCHEMA@.session_expires(), @SCHEMA@.watch_read(xid8, bigint, timestamptz, text[], int, pg_snapshot), @SCHEMA@.watch_head(),
|
|
957
|
-
@SCHEMA@.history_default(), @SCHEMA@.history_policy(jsonb), @SCHEMA@.history_iv(text)
|
|
992
|
+
@SCHEMA@.history_default(), @SCHEMA@.history_policy(jsonb), @SCHEMA@.history_iv(text),
|
|
993
|
+
@SCHEMA@.signal_mode(), @SCHEMA@.signal_key(), @SCHEMA@.class_stamp() to @APP@;
|
package/sql/95-seed.system.sql
CHANGED
|
@@ -69,12 +69,17 @@ declare
|
|
|
69
69
|
o_api uuid;
|
|
70
70
|
o_auth uuid;
|
|
71
71
|
o_letopis uuid;
|
|
72
|
+
o_signals uuid;
|
|
72
73
|
op text;
|
|
73
74
|
begin
|
|
74
|
-
-- Ключ подписи пропуска прав и счётчик изменений прав (50-acl.sql: acl_token, acl_bump)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
-- Ключ подписи пропуска прав и счётчик изменений прав (50-acl.sql: acl_token, acl_bump); от ключа же
|
|
76
|
+
-- выводится ключ сигналов. Режим сигналов (up({ signals })) хранится в meta этой строки: установщик
|
|
77
|
+
-- читает его перед накатом, поэтому повторный up() без опции режим не меняет, а reset({ all }) кладёт
|
|
78
|
+
-- его заново из константы установки.
|
|
79
|
+
insert into @SCHEMA@.secret as k (id, account, kind, material, meta)
|
|
80
|
+
values ('@SYSTEM@'::uuid, '@SYSTEM@'::uuid, 'KEY', @PGCRYPTO@.gen_random_bytes(32), '{"v": 0, "signals": "@SIGNALS@"}')
|
|
81
|
+
on conflict (id) do update set meta = k.meta || '{"signals": "@SIGNALS@"}'
|
|
82
|
+
where k.meta->>'signals' is distinct from '@SIGNALS@';
|
|
78
83
|
|
|
79
84
|
perform @SCHEMA@.sys_create('@SYSTEM@'::uuid, 'Account', '@SYSTEM@'::uuid, '{}',
|
|
80
85
|
'{"name": "System", "categories": ["System"], "enabled": true}');
|
|
@@ -124,8 +129,11 @@ begin
|
|
|
124
129
|
o_api := @SCHEMA@.sys_resource(sys, 'any:API', 'API', null);
|
|
125
130
|
o_auth := @SCHEMA@.sys_resource(sys, 'auth:API', 'API', '{"endpoint": "auth.*"}');
|
|
126
131
|
o_letopis := @SCHEMA@.sys_resource(sys, 'letopis:API', 'API', '{"endpoint": "letopis.*"}');
|
|
132
|
+
o_signals := @SCHEMA@.sys_resource(sys, 'signals:API', 'API', '{"endpoint": "letopis.signals"}');
|
|
127
133
|
perform @SCHEMA@.sys_rule(sys, g_any, o_api, 'deny', 0, '{"code": 403, "message": "Доступ запрещён по умолчанию"}');
|
|
128
134
|
perform @SCHEMA@.sys_rule(sys, g_svc, o_auth, 'allow', 100);
|
|
135
|
+
-- ключ сигналов (signal_key(), план 1.1): сервисы узнают токены классов и арендаторов в нагрузке
|
|
136
|
+
perform @SCHEMA@.sys_rule(sys, g_svc, o_signals, 'allow', 100);
|
|
129
137
|
perform @SCHEMA@.sys_rule(sys, g_admin, o_auth, 'allow', 100);
|
|
130
138
|
perform @SCHEMA@.sys_rule(sys, g_admin, o_letopis, 'allow', 100);
|
|
131
139
|
foreach op in array array['READ', 'WRITE', 'DELETE'] loop
|
package/sql/99-revision.sql
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
-- letopis 1.0 — штамп ревизии движка (план, §2.2, этап 1, п. 11): последний оператор наката.
|
|
2
2
|
-- Номер совпадает с DDL_REVISION в lib/src/types.ts (проверяет check-docs).
|
|
3
|
-
comment on schema @SCHEMA@ is 'letopis ddl_revision=
|
|
3
|
+
comment on schema @SCHEMA@ is 'letopis ddl_revision=6';
|
package/sql/README.md
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
| `@ALLOW_RESET@` | `true` или `false` — константа установки |
|
|
17
17
|
| `@HISTORY_DEFAULT@` | политика хранения по умолчанию `up({ history })` литералом JSON или `'null'` — константа установки |
|
|
18
18
|
| `@RESERVED_NAMES@` | методы цепочки — `RESERVED_CLASS_NAMES` из `lib/src/types.ts`, литералами через запятую |
|
|
19
|
+
| `@SIGNALS@` | режим сигналов `class`, `tenant`, `opaque` или `none` (план 1.1, §2.2): опция `up({ signals })`, иначе сохранённый в `meta.signals` строки ключа `secret`, иначе `class` — константа установки |
|
|
19
20
|
|
|
20
21
|
## Колонки снимка версии
|
|
21
22
|
|