@vrplatform/kysely 1.3.45-stage.669 → 1.3.45-stage.710

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.
@@ -0,0 +1,306 @@
1
+ import { type Kysely, sql } from 'kysely';
2
+
3
+ // The served `core.operation.failure_code` projection must agree with the
4
+ // TypeScript classifier in `packages/repository/src/journal-freshness/load.ts`:
5
+ // a governing accounting lock wins over a retained failure (CUS-2936), and a
6
+ // retained unbalanced attempt is recognized by its message prefix as well as by
7
+ // the typed result code (CUS-2894).
8
+ const failureProjection = (ordering: 'lock-first' | 'failure-first') => sql`
9
+ create or replace function core.refresh_operation_state(
10
+ target_operation_id uuid
11
+ ) returns void
12
+ language plpgsql
13
+ as $$
14
+ declare
15
+ next_status core.operation_status;
16
+ next_started_at timestamptz;
17
+ next_finished_at timestamptz;
18
+ next_failure_code text;
19
+ next_failure_message text;
20
+ target_operation_type core.operation_type;
21
+ target_public_id_format core.operation_public_id_format;
22
+ target_action_id uuid;
23
+ begin
24
+ select
25
+ operation.type,
26
+ operation.public_id_format,
27
+ source.source_id
28
+ into
29
+ target_operation_type,
30
+ target_public_id_format,
31
+ target_action_id
32
+ from core.operation as operation
33
+ left join core.operation_source as source
34
+ on source.operation_id = operation.id
35
+ and source.source_type = 'audit-action'
36
+ where operation.id = target_operation_id;
37
+
38
+ select
39
+ case
40
+ when count(*) filter (where required) = 0 then 'queued'
41
+ when bool_and(status = 'completed') filter (where required)
42
+ then 'completed'
43
+ when bool_and(status in ('completed', 'failed')) filter (where required)
44
+ then 'failed'
45
+ when bool_or(status = 'running') filter (where required)
46
+ or bool_or(status in ('completed', 'failed')) filter (where required)
47
+ then 'running'
48
+ else 'queued'
49
+ end::core.operation_status,
50
+ min(started_at) filter (where required),
51
+ max(finished_at) filter (where required)
52
+ into next_status, next_started_at, next_finished_at
53
+ from core.operation_source
54
+ where operation_id = target_operation_id;
55
+
56
+ if next_status = 'failed' then
57
+ if target_operation_type = 'reservation-journal-refresh'
58
+ and target_action_id is not null then
59
+ select
60
+ case
61
+ ${sql.raw(
62
+ ordering === 'lock-first'
63
+ ? `when core.journal_refresh_all_outstanding_locked(
64
+ bool_and(
65
+ case
66
+ when invalidation.id is not null
67
+ and invalidation.applied_at is null
68
+ then effect.status = 'completed'
69
+ and attempt.status is not distinct from 'skipped_locked'
70
+ end
71
+ ),
72
+ bool_and(
73
+ case
74
+ when invalidation.id is not null
75
+ and invalidation.applied_at is null
76
+ then core.journal_reservation_refresh_locked(
77
+ invalidation.tenant_id,
78
+ invalidation.reservation_id
79
+ )
80
+ end
81
+ )
82
+ ) then 'journalLocked'
83
+ when bool_or(
84
+ invalidation.applied_at is null
85
+ and attempt.result_code = 'journal_configuration_invalid'
86
+ ) then 'journalConfigurationInvalid'
87
+ when bool_or(
88
+ invalidation.applied_at is null
89
+ and (
90
+ attempt.result_code = 'journal_unbalanced'
91
+ or attempt.error_message like 'Journal refresh produced unbalanced%'
92
+ )
93
+ ) then 'journalUnbalanced'`
94
+ : `when bool_or(
95
+ invalidation.applied_at is null
96
+ and attempt.result_code = 'journal_unbalanced'
97
+ ) then 'journalUnbalanced'
98
+ when bool_or(
99
+ invalidation.applied_at is null
100
+ and attempt.result_code = 'journal_configuration_invalid'
101
+ ) then 'journalConfigurationInvalid'
102
+ when bool_or(
103
+ invalidation.applied_at is null
104
+ and attempt.result_code in (
105
+ 'statement_period_locked',
106
+ 'skipped_locked'
107
+ )
108
+ ) then 'journalLocked'`
109
+ )}
110
+ else 'journalRefreshFailed'
111
+ end,
112
+ case
113
+ ${sql.raw(
114
+ ordering === 'lock-first'
115
+ ? `when core.journal_refresh_all_outstanding_locked(
116
+ bool_and(
117
+ case
118
+ when invalidation.id is not null
119
+ and invalidation.applied_at is null
120
+ then effect.status = 'completed'
121
+ and attempt.status is not distinct from 'skipped_locked'
122
+ end
123
+ ),
124
+ bool_and(
125
+ case
126
+ when invalidation.id is not null
127
+ and invalidation.applied_at is null
128
+ then core.journal_reservation_refresh_locked(
129
+ invalidation.tenant_id,
130
+ invalidation.reservation_id
131
+ )
132
+ end
133
+ )
134
+ ) then 'Journal refresh is blocked by accounting locks'
135
+ when bool_or(
136
+ invalidation.applied_at is null
137
+ and attempt.result_code = 'journal_configuration_invalid'
138
+ ) then 'Journal configuration is invalid'
139
+ when bool_or(
140
+ invalidation.applied_at is null
141
+ and (
142
+ attempt.result_code = 'journal_unbalanced'
143
+ or attempt.error_message like 'Journal refresh produced unbalanced%'
144
+ )
145
+ ) then 'Journal refresh produced unbalanced entries'`
146
+ : `when bool_or(
147
+ invalidation.applied_at is null
148
+ and attempt.result_code = 'journal_unbalanced'
149
+ ) then 'Journal refresh produced unbalanced entries'
150
+ when bool_or(
151
+ invalidation.applied_at is null
152
+ and attempt.result_code = 'journal_configuration_invalid'
153
+ ) then 'Journal configuration is invalid'
154
+ when bool_or(
155
+ invalidation.applied_at is null
156
+ and attempt.result_code in (
157
+ 'statement_period_locked',
158
+ 'skipped_locked'
159
+ )
160
+ ) then 'Journal refresh is blocked by accounting locks'`
161
+ )}
162
+ else 'Journal refresh failed'
163
+ end
164
+ into next_failure_code, next_failure_message
165
+ from audit.effect as effect
166
+ left join accounting.reservation_journal_invalidation as invalidation
167
+ on invalidation.tenant_id = effect.tenant_id
168
+ and invalidation.effect_id = effect.id
169
+ left join lateral (
170
+ select
171
+ effect_attempt.status,
172
+ effect_attempt.result_code,
173
+ effect_attempt.error_message
174
+ from audit.effect_attempt as effect_attempt
175
+ where effect_attempt.tenant_id = effect.tenant_id
176
+ and effect_attempt.effect_id = effect.id
177
+ order by effect_attempt.attempt_num desc
178
+ limit 1
179
+ ) as attempt on true
180
+ where effect.action_id = target_action_id
181
+ and effect.effect_type = 'REFRESH_RESERVATION_JOURNAL'
182
+ and effect.entity_type = 'reservation';
183
+ else
184
+ next_failure_code := 'operation_failed';
185
+ next_failure_message := 'Operation failed';
186
+ end if;
187
+ else
188
+ next_failure_code := null;
189
+ next_failure_message := null;
190
+ end if;
191
+
192
+ update core.operation
193
+ set
194
+ status = next_status,
195
+ started_at = coalesce(core.operation.started_at, next_started_at),
196
+ finished_at = case
197
+ when next_status in ('completed', 'failed')
198
+ then coalesce(next_finished_at, now())
199
+ else null
200
+ end,
201
+ failure_code = next_failure_code,
202
+ failure_message = next_failure_message,
203
+ updated_at = now(),
204
+ source_version = source_version + 1
205
+ where id = target_operation_id
206
+ and (
207
+ status is distinct from next_status
208
+ or started_at is distinct from coalesce(started_at, next_started_at)
209
+ or finished_at is distinct from case
210
+ when next_status in ('completed', 'failed')
211
+ then coalesce(next_finished_at, finished_at, now())
212
+ else null
213
+ end
214
+ or failure_code is distinct from next_failure_code
215
+ or failure_message is distinct from next_failure_message
216
+ );
217
+ end;
218
+ $$
219
+ `;
220
+
221
+ // The reservation lock driver from `isReservationRefreshLocked`: the refresh is
222
+ // locked when every active reservation entry sits in a published statement or
223
+ // in closed books.
224
+ const reservationRefreshLocked = sql`
225
+ create or replace function core.journal_reservation_refresh_locked(
226
+ target_tenant_id uuid,
227
+ target_reservation_id uuid
228
+ ) returns boolean
229
+ language sql
230
+ stable
231
+ as $$
232
+ select exists (
233
+ select 1
234
+ from accounting.journal_entry as entry
235
+ inner join public.tenant as tenant on tenant.id = entry.tenant_id
236
+ where entry.tenant_id = target_tenant_id
237
+ and entry.reservation_id = target_reservation_id
238
+ and entry.status = 'active'
239
+ and (entry.entity_type is null or entry.entity_type = 'reservation')
240
+ and (
241
+ entry.attached_to_owner_statement_id is not null
242
+ or entry.transfer_owner_statement_id is not null
243
+ or entry.txn_at < tenant.books_closed_at
244
+ )
245
+ )
246
+ $$
247
+ `;
248
+
249
+ // `bool_and` over no outstanding invalidation returns null; the classifier
250
+ // treats that as "not locked".
251
+ const allOutstandingLocked = sql`
252
+ create or replace function core.journal_refresh_all_outstanding_locked(
253
+ all_lock_skipped boolean,
254
+ all_reservations_locked boolean
255
+ ) returns boolean
256
+ language sql
257
+ immutable
258
+ as $$
259
+ select coalesce(all_lock_skipped, false) or coalesce(all_reservations_locked, false)
260
+ $$
261
+ `;
262
+
263
+ async function recomputeFailedJournalOperations(db: Kysely<unknown>) {
264
+ // Derived operation state only. Attempts, invalidations and journals are untouched.
265
+ await sql`
266
+ select core.refresh_operation_state(operation.id)
267
+ from core.operation as operation
268
+ where operation.type = 'reservation-journal-refresh'
269
+ and operation.status = 'failed'
270
+ and exists (
271
+ select 1 from core.operation_source as source
272
+ where source.operation_id = operation.id
273
+ and source.source_type = 'audit-action'
274
+ )
275
+ `.execute(db);
276
+ }
277
+
278
+ export async function up(db: Kysely<unknown>): Promise<void> {
279
+ await sql`
280
+ do $$
281
+ begin
282
+ if position(
283
+ 'and target_public_id_format = ''action'''
284
+ in pg_get_functiondef('core.refresh_operation_state(uuid)'::regprocedure)
285
+ ) > 0 then
286
+ raise exception 'Journal operation failure projection has an unexpected definition';
287
+ end if;
288
+ end;
289
+ $$
290
+ `.execute(db);
291
+ await reservationRefreshLocked.execute(db);
292
+ await allOutstandingLocked.execute(db);
293
+ await failureProjection('lock-first').execute(db);
294
+ await recomputeFailedJournalOperations(db);
295
+ }
296
+
297
+ export async function down(db: Kysely<unknown>): Promise<void> {
298
+ await failureProjection('failure-first').execute(db);
299
+ await recomputeFailedJournalOperations(db);
300
+ await sql`drop function if exists core.journal_refresh_all_outstanding_locked(boolean, boolean)`.execute(
301
+ db
302
+ );
303
+ await sql`drop function if exists core.journal_reservation_refresh_locked(uuid, uuid)`.execute(
304
+ db
305
+ );
306
+ }
@@ -0,0 +1,28 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { sql } from 'kysely';
3
+
4
+ // An inline reservation journal refresh has no effect row, so a refresh that
5
+ // completes without writing the reservation's postings had nowhere to record
6
+ // why. Storage only: nullable column, no default, no backfill, no rewrite.
7
+ export async function up(db: Kysely<unknown>): Promise<void> {
8
+ await sql`
9
+ create type accounting.reservation_journal_invalidation_failure as enum (
10
+ 'journal_configuration_invalid'
11
+ )
12
+ `.execute(db);
13
+ await sql`
14
+ alter table accounting.reservation_journal_invalidation
15
+ add column failure_code
16
+ accounting.reservation_journal_invalidation_failure
17
+ `.execute(db);
18
+ }
19
+
20
+ export async function down(db: Kysely<unknown>): Promise<void> {
21
+ await sql`
22
+ alter table accounting.reservation_journal_invalidation
23
+ drop column if exists failure_code
24
+ `.execute(db);
25
+ await sql`
26
+ drop type if exists accounting.reservation_journal_invalidation_failure
27
+ `.execute(db);
28
+ }
@@ -5,6 +5,7 @@
5
5
  import type { ColumnType } from "kysely";
6
6
  export type AccountingJournalEntryInactiveReason = "cancelledReservationLine" | "currencyMismatch" | "generalLedgerStartAt" | "historicalLedger" | "inactiveAccount" | "listingInactive" | "listingOwnershipPeriodNotFound" | "nonPostingAccount" | "reservationGeneralLedgerStatusInactive" | "transactionInactive" | "unbalancedGroupInactiveLeg" | "unknownInactiveReason";
7
7
  export type AccountingJournalEntryLedger = "historical" | "operating" | "trust";
8
+ export type AccountingReservationJournalInvalidationFailure = "journal_configuration_invalid";
8
9
  export type AccountingTransactionAttachmentStatus = "attached" | "deleting" | "ready" | "uploading";
9
10
  export type ArrayType<T> = ArrayTypeImpl<T> extends (infer U)[] ? U[] : ArrayTypeImpl<T>;
10
11
  export type ArrayTypeImpl<T> = T extends ColumnType<infer S, infer I, infer U> ? ColumnType<S[], I[], U[]> : T[];
@@ -418,6 +419,7 @@ export interface AccountingReservationJournalInvalidation {
418
419
  coveredByEffectId: string | null;
419
420
  createdAt: Generated<Timestamp>;
420
421
  effectId: string | null;
422
+ failureCode: AccountingReservationJournalInvalidationFailure | null;
421
423
  id: Generated<string>;
422
424
  reservationId: string;
423
425
  revision: Int8;