letopis 1.0.1 → 1.2.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.
Files changed (60) hide show
  1. package/AGENT-CHEATSHEET.en.md +117 -10
  2. package/AGENT-CHEATSHEET.md +110 -8
  3. package/CHANGELOG.md +739 -0
  4. package/README.en.md +873 -68
  5. package/README.md +830 -66
  6. package/dist/admin.d.ts +24 -4
  7. package/dist/admin.js +39 -9
  8. package/dist/cache.d.ts +33 -9
  9. package/dist/cache.js +81 -32
  10. package/dist/chain.d.ts +29 -7
  11. package/dist/chain.js +61 -12
  12. package/dist/cli.js +59 -7
  13. package/dist/errors.js +4 -2
  14. package/dist/import.d.ts +6 -0
  15. package/dist/import.js +196 -175
  16. package/dist/index.d.ts +27 -5
  17. package/dist/index.js +191 -29
  18. package/dist/internal.d.ts +16 -0
  19. package/dist/internal.js +14 -0
  20. package/dist/load.d.ts +63 -0
  21. package/dist/load.js +331 -44
  22. package/dist/mcp.d.ts +28 -0
  23. package/dist/mcp.js +810 -0
  24. package/dist/model.d.ts +34 -0
  25. package/dist/model.js +17 -0
  26. package/dist/project.d.ts +309 -0
  27. package/dist/project.js +1097 -0
  28. package/dist/registry.d.ts +17 -0
  29. package/dist/registry.js +13 -0
  30. package/dist/signals.d.ts +73 -0
  31. package/dist/signals.js +169 -0
  32. package/dist/sql.d.ts +1 -1
  33. package/dist/sql.js +26 -7
  34. package/dist/tx.d.ts +9 -7
  35. package/dist/tx.js +15 -5
  36. package/dist/typed.d.ts +21 -6
  37. package/dist/types.d.ts +77 -4
  38. package/dist/types.js +8 -3
  39. package/dist/up.d.ts +16 -2
  40. package/dist/up.js +34 -4
  41. package/dist/uuid.js +19 -8
  42. package/dist/watch.d.ts +23 -2
  43. package/dist/watch.js +44 -7
  44. package/dist/write.d.ts +11 -2
  45. package/dist/write.js +57 -10
  46. package/llms.txt +9 -2
  47. package/package.json +13 -2
  48. package/sql/10-core.sql +13 -2
  49. package/sql/15-errors.sql +4 -1
  50. package/sql/40-class.sql +57 -2
  51. package/sql/60-write.sql +611 -82
  52. package/sql/70-read.sql +33 -8
  53. package/sql/75-history.sql +358 -0
  54. package/sql/80-auth.sql +7 -7
  55. package/sql/88-derived.sql +1154 -0
  56. package/sql/90-time.sql +134 -21
  57. package/sql/95-seed.system.sql +24 -6
  58. package/sql/99-revision.sql +1 -1
  59. package/sql/README.md +5 -1
  60. package/sql/migrate/0006-changed-paths.sql +3 -0
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import postgres from 'postgres';
11
11
  import { LetopisError, fromDbError, removed } from './errors.js';
12
12
  import { Registry } from './registry.js';
13
13
  import { AclPasses, ExplicitTx, Notifier, poolExecutor, runTx } from './tx.js';
14
+ import { checkReason } from './write.js';
15
+ import { registerDb } from './internal.js';
14
16
  import { makeAuth } from './auth.js';
15
17
  import { makeAcl } from './acl.js';
16
18
  import { idOf as idOfClass } from './uuid.js';
@@ -19,7 +21,9 @@ import { makeBatch, startEntity, startStep } from './chain.js';
19
21
  import { DDL_REVISION } from './types.js';
20
22
  import { ACL_CLASSES, ResultCache } from './cache.js';
21
23
  import { Subscription } from './watch.js';
22
- import { describe as describeModel, maintain as runMaintain, rekeyClass as runRekeyClass, reset as runReset, scheduleMaintain, trimHistory as runTrim, verify as runVerify, } from './admin.js';
24
+ import { Projector } from './project.js';
25
+ import { SIGNAL_MODES, Signals, bucket, throttle } from './signals.js';
26
+ import { describe as describeModel, maintain as runMaintain, rebuildAggregates as runRebuildAggregates, rekeyClass as runRekeyClass, reset as runReset, scheduleMaintain, trimHistory as runTrim, verify as runVerify, } from './admin.js';
23
27
  export { DDL_REVISION } from './types.js';
24
28
  export { uuidv5, v5Name, v5Id, idOf } from './uuid.js';
25
29
  export { LetopisError, ValidationError, fromDbError } from './errors.js';
@@ -64,6 +68,7 @@ function makeDb(sh, scope, exec, owner, tx) {
64
68
  const txc = scope.txc;
65
69
  const chainCtx = {
66
70
  reg: scope.registry(), schema: sh.schema, exec, tenant: scope.tenant, owner, onQuery: sh.onQuery, slowMs: sh.slowMs,
71
+ reason: txc.reason ?? '', txReason: txc.reason ?? '',
67
72
  ...(sh.cache ? { cache: { store: sh.cache, who: () => ({ token: txc.token, account: txc.account, tenant: scope.tenant() }) } } : {}),
68
73
  };
69
74
  // реестр корня меняется после auth.switch — цепочки берут действующий
@@ -116,7 +121,10 @@ function makeDb(sh, scope, exec, owner, tx) {
116
121
  const classes = o.classes ? [...new Set(o.classes.flatMap((c) => reg.family(c).map((x) => x.name)))] : null;
117
122
  return new Subscription({
118
123
  S: sh.S, exec: poolExecutor(txc), classes,
119
- ...(sh.listen ? { onSignal: (fn) => { sh.signals.add(fn); return () => void sh.signals.delete(fn); } } : {}),
124
+ ...(sh.listen ? {
125
+ onSignal: (fn) => { sh.signals.add(fn); return () => void sh.signals.delete(fn); },
126
+ tenant: () => (sh.allTenants ? null : sh.sig.tenantToken(scope.tenant())),
127
+ } : {}),
120
128
  }, o);
121
129
  },
122
130
  cache: {
@@ -141,29 +149,48 @@ function makeDb(sh, scope, exec, owner, tx) {
141
149
  const registry = await sh.registryFor(tenant, scoped);
142
150
  return makeDb(sh, { txc: scoped, tenant: () => tenant, registry: () => registry, root: false }, poolExecutor(scoped), nextOwner);
143
151
  },
144
- begin: async () => {
152
+ begin: async (o) => {
145
153
  if (tx)
146
154
  throw new LetopisError('invalid_query', 'вложенных транзакций нет: db.begin() внутри db.begin()');
147
- const t = await ExplicitTx.open(txc);
148
- return makeDb(sh, scope, t, owner, t);
155
+ // причина блока ставится в начале транзакции (setupSql) и действует на все её операции
156
+ const btx = o?.reason !== undefined ? { ...txc, reason: checkReason(o.reason, 'db.begin()') } : txc;
157
+ const t = await ExplicitTx.open(btx);
158
+ return makeDb(sh, btx === txc ? scope : { ...scope, txc: btx }, t, owner, t);
149
159
  },
150
160
  load: async (input, o) => {
151
161
  if (tx)
152
162
  throw new LetopisError('invalid_query', 'load() — своей транзакцией, не внутри db.begin()');
153
163
  return load({ sql: sh.sql, schema: sh.schema, tenant: scope.tenant(), admin: sh.admin, ownerRole: sh.ownerRole }, input, o);
154
164
  },
155
- batch: (name) => {
165
+ projector: (name, o) => {
166
+ if (tx)
167
+ throw new LetopisError('invalid_query', 'projector() — от корня подключения, а не внутри db.begin()');
168
+ if (!sh.admin)
169
+ throw new LetopisError('admin_required', 'проектору нужно администраторское подключение (connect({ allowBypassRls: true }) роли, которая может стать владельцем схемы)');
170
+ return new Projector({
171
+ sql: sh.sql, schema: sh.schema, S: sh.S, ownerRole: sh.ownerRole,
172
+ ...(sh.listen ? { onSignal: (fn) => { sh.signals.add(fn); return () => void sh.signals.delete(fn); } } : {}),
173
+ }, name, o);
174
+ },
175
+ rebuildAggregates: async (cls) => {
176
+ if (tx)
177
+ throw new LetopisError('invalid_query', 'rebuildAggregates() — своей транзакцией, не внутри db.begin()');
178
+ if (!sh.admin)
179
+ throw new LetopisError('admin_required', 'ремонту агрегатов нужно администраторское подключение (connect({ allowBypassRls: true }) роли, которая может стать владельцем схемы)');
180
+ return runRebuildAggregates({ sql: sh.sql, S: sh.S, ownerRole: sh.ownerRole }, cls);
181
+ },
182
+ batch: (name, o) => {
156
183
  let q = batches.get(name);
157
184
  if (!q)
158
185
  batches.set(name, (q = []));
159
- return makeBatch(chainCtx, q);
186
+ return makeBatch(o?.reason !== undefined ? { ...chainCtx, reason: checkReason(o.reason, 'db.batch()') } : chainCtx, q);
160
187
  },
161
188
  lock: async (...keys) => (tx ? tx.lock(...keys) : noTx('lock()')),
162
189
  commit: async (tr) => (tr ? tr.commit() : tx ? tx.commit() : noTx('commit()')),
163
190
  rollback: async (tr) => (tr ? tr.rollback() : tx ? tx.rollback() : noTx('rollback()')),
164
191
  close: () => (tx ? tx.rollback() : sh.close()),
165
192
  };
166
- return new Proxy(core, {
193
+ const facade = new Proxy(core, {
167
194
  get(t, prop, r) {
168
195
  if (typeof prop === 'symbol' || prop in t)
169
196
  return Reflect.get(t, prop, r);
@@ -183,6 +210,9 @@ function makeDb(sh, scope, exec, owner, tx) {
183
210
  return startStep(chainCtx, prop);
184
211
  },
185
212
  });
213
+ // модули пакета (letopis/mcp) узнают обход RLS по самому подключению, а не со слов вызывающего (internal.ts)
214
+ registerDb(facade, { allTenants: sh.allTenants, explicit: Boolean(tx) });
215
+ return facade;
186
216
  }
187
217
  export async function connect(opts) {
188
218
  if (!SCHEMA_NAME.test(opts.schema))
@@ -227,10 +257,20 @@ export async function connect(opts) {
227
257
  throw new LetopisError('invalid_data', `connect: схемы ${opts.schema} нет — установите её: up({ schema })`);
228
258
  if (rev < DDL_REVISION)
229
259
  console.warn(`letopis: движок схемы ${opts.schema} ревизии ${rev}, библиотека ждёт ${DDL_REVISION} — обновите: up({ schema: '${opts.schema}', upgrade: true })`);
260
+ // режим сигналов установки (план 1.1, §2.2); схема, накатанная до него (нет signal_mode()), — class
261
+ const [sm] = await sql.unsafe(`select ${S}.signal_mode() as m`).catch((e) => {
262
+ if (e.code === '42883')
263
+ return [{ m: 'class' }];
264
+ throw fromDbError(e);
265
+ });
266
+ const mode = SIGNAL_MODES.includes(sm?.m) ? sm.m : 'class';
267
+ const sig = new Signals(mode);
230
268
  const notifier = new Notifier(sql, opts.schema, opts.notifyIntervalMs ?? 100);
231
269
  const passes = new AclPasses(S);
232
- const listen = opts.listen !== false;
233
- const cache = opts.cache === false ? null : new ResultCache(opts.cache ?? {});
270
+ // режим none: сигналов нет — без LISTEN, кэш только по сроку (ttlOnly), подписка опрашивает журнал
271
+ const listen = opts.listen !== false && mode !== 'none';
272
+ // подключение в обход RLS видит строки всех арендаторов: его ответы сбрасывает сигнал из любого
273
+ const cache = opts.cache === false ? null : new ResultCache(opts.cache ?? {}, sig, Boolean(bypass));
234
274
  const signals = new Set();
235
275
  // продление сессии (§2.9): отдельным вызовом не чаще раза в минуту; ошибка (реплика,
236
276
  // транзакции только для чтения) не мешает работе
@@ -241,60 +281,182 @@ export async function connect(opts) {
241
281
  refreshedAt = Date.now();
242
282
  void runTx({ sql, token, notifier: null }, (q) => q.unsafe(`select ${S}.auth_refresh()`), { retries: 0 }).catch(() => { });
243
283
  };
244
- const ctx = { sql, token, notifier, passes, after: touch, ...(cache ? { onChanged: (names, raw) => cache.changed(names, raw) } : {}) };
284
+ // в режиме none нотификатор не отправляет: letopis.changed своей транзакции сбрасывает только свой кэш
285
+ const ctx = { sql, token, notifier: mode === 'none' ? null : notifier, passes, after: touch,
286
+ ...(cache ? { onChanged: (names, raw) => cache.changed(names, raw) } : {}) };
245
287
  const root = { tenant: null, registry: new Registry() };
246
- const loadClasses = (txc) => runTx(txc, (q) => q.unsafe(`select tenant, rev, data from ${S}.entity where class = 'Class' and tenant in (${S}.ctx_tenant(), '${SYSTEM_ID}')`), { read: true });
288
+ // классы арендатора tenant под контекстом txc; null — контекст уже не в этом арендаторе (сессия кончилась,
289
+ // пользователь выбыл из арендатора): тогда прежний реестр остаётся, а не пустеет (доработки после точки Б)
290
+ const loadClasses = (txc, tenant) => runTx(txc, async (q) => {
291
+ const [t] = await q.unsafe(`select ${S}.ctx_tenant() as t`);
292
+ if (String(t?.t ?? '').toLowerCase() !== tenant.toLowerCase())
293
+ return null;
294
+ return q.unsafe(`select tenant, rev, data from ${S}.entity where class = 'Class' and tenant in (${S}.ctx_tenant(), '${SYSTEM_ID}')`);
295
+ }, { read: true });
296
+ // реестры db.as() — общие на арендатора; txc — контекст последнего успешного as() этого арендатора
247
297
  const others = new Map();
248
- const refresh = async () => {
298
+ const loading = new Map();
299
+ // токены классов (режим opaque) — по именам всех реестров подключения
300
+ const tokens = () => sig.setClasses([root.registry, ...[...others.values()].map((o) => o.registry)].flatMap((r) => r.all().map((c) => c.name)));
301
+ // only — токены арендаторов из сигналов Class (режимы tenant и opaque): реестры db.as() только этих арендаторов
302
+ const refresh = async (only) => {
249
303
  const rows = await runTx(ctx, async (q) => {
250
304
  const [t] = await q.unsafe(`select ${S}.ctx_tenant() as t`);
251
305
  root.tenant = t?.t ?? null;
252
306
  return q.unsafe(`select tenant, rev, data from ${S}.entity where class = 'Class' and tenant in (${S}.ctx_tenant(), '${SYSTEM_ID}')`);
253
307
  });
254
308
  root.registry.load(rows);
255
- for (const o of others.values())
256
- o.registry.load(await loadClasses(o.txc).catch(() => []));
309
+ for (const [tenant, o] of others) {
310
+ if (only && !only.has(sig.tenantToken(tenant) ?? ''))
311
+ continue;
312
+ const loaded = await loadClasses(o.txc, tenant).catch(() => null);
313
+ if (loaded)
314
+ o.registry.load(loaded);
315
+ }
316
+ tokens();
257
317
  };
258
318
  const registryFor = async (tenant, txc) => {
259
319
  if (tenant === root.tenant)
260
320
  return root.registry;
261
321
  const known = others.get(tenant);
262
- if (known)
322
+ if (known) {
323
+ known.txc = txc;
263
324
  return known.registry;
264
- const registry = new Registry();
265
- registry.load(await loadClasses(txc));
266
- others.set(tenant, { registry, txc });
267
- return registry;
325
+ }
326
+ // одновременные as() нового арендатора получают один реестр
327
+ let pending = loading.get(tenant);
328
+ if (!pending) {
329
+ pending = (async () => {
330
+ const registry = new Registry();
331
+ registry.load(((await loadClasses(txc, tenant)) ?? []));
332
+ others.set(tenant, { registry, txc });
333
+ tokens();
334
+ return registry;
335
+ })().finally(() => loading.delete(tenant));
336
+ loading.set(tenant, pending);
337
+ }
338
+ return pending;
268
339
  };
340
+ // ключ сигналов (режимы tenant и opaque) — сессии с правом letopis.signals; без права процесс
341
+ // работает без ключа: арендатора (а в opaque — и класса) в сигнале не различает
342
+ const fetchKey = async () => {
343
+ if (mode !== 'tenant' && mode !== 'opaque')
344
+ return;
345
+ const key = await runTx(ctx, (q) => q.unsafe(`select ${S}.signal_key() as k`), { read: true }).then((r) => r[0]?.k ?? null, (e) => {
346
+ if (e instanceof LetopisError && (e.code === 'acl_denied' || e.code === 'no_session'))
347
+ return null;
348
+ throw e;
349
+ });
350
+ // ключ сменился (reset({ all })): ответы с токенами старого ключа сигналы больше не узнают
351
+ if (sig.setKey(key))
352
+ cache?.clear();
353
+ };
354
+ await fetchKey();
269
355
  await refresh();
270
- // описания классов меняются — реестр перечитывается по сигналу; строки прав — пропуска заново;
271
- // кэш результатов и подписки — по именам классов; (пере)подключение слушателя — кэш целиком
272
- const onSignal = (cls) => {
273
- if (ACL_CLASSES.has(cls) || cls === '*')
356
+ // ключ, реестр и пропуска прав — заново по *, незнакомому токену, нагрузке не по режиму, а у процесса
357
+ // без ключа в режиме opaque — по любому сигналу; не чаще раза в 5 с: заливка канала (в том числе
358
+ // поддельными сигналами) не становится потоком перечитываний
359
+ const resync = throttle(() => {
360
+ passes.clear();
361
+ void fetchKey().then(() => refresh()).catch(() => { });
362
+ }, 5000);
363
+ // сигнал Class — реестр сразу, как в 1.0: правка описания доходит без задержки; пока идёт перечитывание, новые
364
+ // сигналы сливаются в одно следующее. Три перечитывания подряд — сразу, дальше не чаще раза в 5 с: заливка
365
+ // канала, в том числе поддельными Class, не становится потоком перечитываний (план 1.1, §7.7 №32; ревью №1
366
+ // точки А)
367
+ let reloading = false;
368
+ let again = false;
369
+ // какие реестры db.as() перечитать: токены арендаторов из сигналов или все (сигнал без арендатора)
370
+ let reloadAll = false;
371
+ const reloadTokens = new Set();
372
+ const startReload = bucket(() => {
373
+ reloading = true;
374
+ const only = reloadAll ? null : new Set(reloadTokens);
375
+ reloadAll = false;
376
+ reloadTokens.clear();
377
+ void refresh(only).catch(() => { }).finally(() => {
378
+ reloading = false;
379
+ if (again) {
380
+ again = false;
381
+ reload(undefined);
382
+ }
383
+ });
384
+ }, 3, 5000);
385
+ // tenant: null — все реестры, строка — токен арендатора сигнала, undefined — то, что уже накоплено
386
+ const reload = (tenant) => {
387
+ if (tenant === null)
388
+ reloadAll = true;
389
+ else if (tenant !== undefined)
390
+ reloadTokens.add(tenant);
391
+ if (reloading) {
392
+ again = true;
393
+ return;
394
+ }
395
+ startReload();
396
+ };
397
+ // строки прав — пропуска заново; кэш результатов и подписки — по классу и арендатору сигнала;
398
+ // (пере)подключение слушателя — кэш целиком, после обрыва — и ключ с реестром (сигналы потеряны)
399
+ const onSignal = (payload) => {
400
+ const s = sig.parse(payload);
401
+ if (s.kind === 'class' && ACL_CLASSES.has(s.cls))
274
402
  passes.clear();
275
- if (cls === 'Class' || cls === '*')
276
- void refresh().catch(() => { });
277
- cache?.signal(cls);
403
+ if (s.kind === 'class' && s.cls === 'Class')
404
+ reload(s.tenant);
405
+ else if (s.kind === 'all' || s.kind === 'opaque' || s.kind === 'unknown' || s.kind === 'foreign')
406
+ resync();
407
+ cache?.signal(s);
278
408
  for (const fn of signals)
279
- fn(cls);
409
+ fn(s);
280
410
  };
411
+ let listened = false;
281
412
  const listener = listen
282
413
  ? await sql.listen(opts.schema, onSignal, () => {
414
+ if (listened)
415
+ resync();
416
+ listened = true;
283
417
  cache?.listen();
284
418
  for (const fn of signals)
285
- fn('*');
419
+ fn({ kind: 'all' });
286
420
  })
287
421
  : null;
422
+ // режим none: о новых и изменённых классах сигнала нет — отпечаток классов раз в 5 с
423
+ let stamper = null;
424
+ if (mode === 'none') {
425
+ const stampOf = (txc) => runTx(txc, (q) => q.unsafe(`select ${S}.class_stamp() as s`), { read: true }).then((r) => String(r[0]?.s ?? ''));
426
+ const stamp = async () => [await stampOf(ctx), ...(await Promise.all([...others.values()].map((o) => stampOf(o.txc).catch(() => ''))))].join('|');
427
+ let last = await stamp();
428
+ let busy = false;
429
+ stamper = setInterval(() => {
430
+ if (busy)
431
+ return;
432
+ busy = true;
433
+ void stamp().then(async (cur) => {
434
+ if (cur === last)
435
+ return;
436
+ last = cur;
437
+ await refresh();
438
+ }).catch(() => { }).finally(() => {
439
+ busy = false;
440
+ });
441
+ }, 5000);
442
+ stamper.unref?.();
443
+ }
288
444
  let stopMaintain = null;
289
445
  const shared = {
290
446
  sql, schema: opts.schema, S, notifier, passes, root, registryFor, refresh, onQuery: opts.onQuery, slowMs: opts.slowMs,
291
447
  admin: Boolean(bypass && (role.rolsuper || role.can_set)),
292
448
  ownerRole: owner,
293
449
  cache,
450
+ sig,
451
+ allTenants: Boolean(bypass),
294
452
  signals,
295
453
  listen,
296
454
  async close() {
297
455
  stopMaintain?.();
456
+ resync.stop();
457
+ startReload.stop();
458
+ if (stamper)
459
+ clearInterval(stamper);
298
460
  await notifier.close();
299
461
  await listener?.unlisten();
300
462
  if (own)
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Внутренний канал между модулями пакета (план 1.1, этап 5): свойства подключения, которых нет в публичном API.
3
+ * Модуля нет в exports package.json — снаружи пакета его не импортировать; letopis/mcp узнаёт по нему, что
4
+ * фасад db обходит RLS, не полагаясь на то, что сообщает о себе сам вызывающий.
5
+ */
6
+ /** Что фасад db (корень, db.as(), db.begin()) сообщает модулям пакета. */
7
+ export interface DbInternals {
8
+ /** Подключение в обход RLS (суперпользователь, BYPASSRLS, член роли-владельца): видит строки всех арендаторов. */
9
+ readonly allTenants: boolean;
10
+ /** Фасад явной транзакции db.begin(). */
11
+ readonly explicit: boolean;
12
+ }
13
+ /** Запомнить фасад (зовёт makeDb для каждого фасада, который отдаёт наружу). */
14
+ export declare function registerDb(db: object, info: DbInternals): void;
15
+ /** Свойства фасада; undefined — объект не фасад connect() этого пакета. */
16
+ export declare function internalsOf(db: unknown): DbInternals | undefined;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Внутренний канал между модулями пакета (план 1.1, этап 5): свойства подключения, которых нет в публичном API.
3
+ * Модуля нет в exports package.json — снаружи пакета его не импортировать; letopis/mcp узнаёт по нему, что
4
+ * фасад db обходит RLS, не полагаясь на то, что сообщает о себе сам вызывающий.
5
+ */
6
+ const facades = new WeakMap();
7
+ /** Запомнить фасад (зовёт makeDb для каждого фасада, который отдаёт наружу). */
8
+ export function registerDb(db, info) {
9
+ facades.set(db, info);
10
+ }
11
+ /** Свойства фасада; undefined — объект не фасад connect() этого пакета. */
12
+ export function internalsOf(db) {
13
+ return db !== null && (typeof db === 'object' || typeof db === 'function') ? facades.get(db) : undefined;
14
+ }
package/dist/load.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type postgres from 'postgres';
2
2
  import { type Issue } from './errors.js';
3
+ import { Registry } from './registry.js';
3
4
  type Sql = postgres.Sql<Record<string, unknown>>;
4
5
  type Raw = Record<string, unknown>;
5
6
  export interface LoadOptions {
@@ -27,6 +28,12 @@ export interface LoadOptions {
27
28
  * existing: 'update' — прежние) значения строк; цели ссылок ему видны; владелец — по ownerDefault.
28
29
  */
29
30
  as?: string;
31
+ /**
32
+ * Первая заливка (план 1.1, §2.3): только в пустую схему (кроме строк системных классов — иначе
33
+ * not_empty), одной транзакцией; на время загрузки схема закрыта, индексы и триггеры сняты и
34
+ * перестраиваются в конце. Без commitEvery, existing (кроме 'error') и { as }.
35
+ */
36
+ initial?: boolean;
30
37
  }
31
38
  export interface LoadReport {
32
39
  /** Строк во входе. */
@@ -65,6 +72,50 @@ export interface LoadEnv {
65
72
  /** Подключение может стать владельцем (set role) — иначе admin_required. */
66
73
  admin: boolean;
67
74
  ownerRole: string;
75
+ /** Сессия первой заливки (initialSession): загрузка идёт на её соединении и в её транзакции. */
76
+ session?: InitialState;
77
+ }
78
+ /** Внутренние настройки первой заливки — для разбивки замера (bench/initial-load.mjs); в публичные типы не входят. */
79
+ export interface InitialTuning {
80
+ /** COPY … FREEZE (по умолчанию да). */
81
+ freeze?: boolean;
82
+ /** Оставить триггеры (BEFORE ROW делает COPY построчным). */
83
+ keepTriggers?: boolean;
84
+ /** Оставить первичный ключ и уникальные ограничения. */
85
+ keepKeys?: boolean;
86
+ /** Оставить индексы GIN. */
87
+ keepGin?: boolean;
88
+ }
89
+ /** Проверки загрузки, отложенные до конца первой заливки: по перестроенным индексам. */
90
+ interface Pending {
91
+ reg: Registry;
92
+ tenant: string;
93
+ external: Map<string, {
94
+ line: number;
95
+ role: string;
96
+ allowed: string[];
97
+ }>;
98
+ owners: Map<string, number>;
99
+ /** verify: true — id записанных строк. */
100
+ verify: string[] | null;
101
+ }
102
+ /** Состояние сессии первой заливки. */
103
+ export interface InitialState {
104
+ conn: Sql;
105
+ /** id строк системных классов и объектов, уже записанных сессией: первичного ключа на время загрузки нет. */
106
+ ids: Set<string>;
107
+ pending: Pending[];
108
+ /** Арендатор → классы записанных строк: сигнал в конце сессии. */
109
+ touched: Map<string, Set<string>>;
110
+ freeze: boolean;
111
+ }
112
+ /** Первая заливка для нескольких загрузок (импорт): соединение и транзакция сессии. */
113
+ export interface InitialSession {
114
+ /** Соединение сессии (роль владельца): свои операторы импорта — в той же транзакции. */
115
+ readonly sql: Sql;
116
+ load(input: LoadInput, opts?: LoadOptions): Promise<LoadReport>;
117
+ /** id уже есть в схеме: строка системного класса или объект, записанный этой сессией. */
118
+ taken(id: string): boolean;
68
119
  }
69
120
  /**
70
121
  * Загрузить строки (§2.6). Одна транзакция (или порции commitEvery): описания классов под
@@ -73,4 +124,16 @@ export interface LoadEnv {
73
124
  * называет строки и нарушения.
74
125
  */
75
126
  export declare function load(env: LoadEnv, input: LoadInput, opts?: LoadOptions): Promise<LoadReport>;
127
+ /**
128
+ * Первая заливка (план 1.1, §2.3): загрузки fn — одной транзакцией в пустую схему. Блокировка таблиц до
129
+ * проверки; в схеме только строки системных классов и записи reset (иначе not_empty); они (с seq и tx
130
+ * журнала) и определения ограничений, индексов и триггеров из каталога (с режимом включения tgenabled:
131
+ * pg_get_triggerdef его не передаёт) сохраняются в памяти; TRUNCATE; триггеры, ограничения и индексы
132
+ * снимаются — BEFORE ROW делает COPY построчным одним своим наличием, триггеры после оператора копят
133
+ * переходные таблицы; системные строки возвращаются (seq журнала — generated always: overriding system
134
+ * value); загрузки пишут COPY FREEZE. В конце — ограничения (дубликат найдёт сортировка), индексы,
135
+ * триггеры с прежним режимом, отложенные проверки загрузок, сигнал, счётчик прав и ANALYZE. Любая ошибка
136
+ * откатывает всё: DDL в PostgreSQL транзакционен.
137
+ */
138
+ export declare function initialSession<T>(env: LoadEnv, fn: (s: InitialSession) => Promise<T>, tune?: InitialTuning): Promise<T>;
76
139
  export {};