@sixb/pg 0.1.7 → 0.1.8

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 (49) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +7548 -6387
  4. package/dist/index.js.map +16 -10
  5. package/dist/migrations.d.ts.map +1 -1
  6. package/dist/objects/index.d.ts +2 -0
  7. package/dist/objects/index.d.ts.map +1 -0
  8. package/dist/{pg-object-query-compiler.d.ts → objects/query-compiler.d.ts} +25 -4
  9. package/dist/objects/query-compiler.d.ts.map +1 -0
  10. package/dist/objects/read-scope.d.ts +17 -0
  11. package/dist/objects/read-scope.d.ts.map +1 -0
  12. package/dist/objects/reader.d.ts +50 -0
  13. package/dist/objects/reader.d.ts.map +1 -0
  14. package/dist/objects/rows.d.ts +47 -0
  15. package/dist/objects/rows.d.ts.map +1 -0
  16. package/dist/objects/selected-reader.d.ts +8 -0
  17. package/dist/objects/selected-reader.d.ts.map +1 -0
  18. package/dist/{pg-object-storage.d.ts → objects/storage.d.ts} +12 -20
  19. package/dist/objects/storage.d.ts.map +1 -0
  20. package/dist/pg-execution-storage.d.ts +3 -2
  21. package/dist/pg-execution-storage.d.ts.map +1 -1
  22. package/dist/share-grant-storage.d.ts +11 -0
  23. package/dist/share-grant-storage.d.ts.map +1 -0
  24. package/dist/share-session-storage.d.ts +11 -0
  25. package/dist/share-session-storage.d.ts.map +1 -0
  26. package/dist/storage-errors.d.ts +2 -0
  27. package/dist/storage-errors.d.ts.map +1 -1
  28. package/dist/transactions.d.ts +9 -2
  29. package/dist/transactions.d.ts.map +1 -1
  30. package/package.json +3 -3
  31. package/src/index.ts +16 -3
  32. package/src/migrations/035-share-grants.sql +57 -0
  33. package/src/migrations/036-share-sessions.sql +173 -0
  34. package/src/migrations.ts +4 -0
  35. package/src/objects/index.ts +1 -0
  36. package/src/{pg-object-query-compiler.ts → objects/query-compiler.ts} +185 -100
  37. package/src/objects/read-scope.ts +324 -0
  38. package/src/objects/reader.ts +604 -0
  39. package/src/objects/rows.ts +146 -0
  40. package/src/objects/selected-reader.ts +118 -0
  41. package/src/objects/storage.ts +305 -0
  42. package/src/pg-execution-storage.ts +21 -2
  43. package/src/share-grant-storage.ts +236 -0
  44. package/src/share-session-storage.ts +127 -0
  45. package/src/storage-errors.ts +6 -0
  46. package/src/transactions.ts +46 -5
  47. package/dist/pg-object-query-compiler.d.ts.map +0 -1
  48. package/dist/pg-object-storage.d.ts.map +0 -1
  49. package/src/pg-object-storage.ts +0 -734
@@ -0,0 +1,146 @@
1
+ import type {
2
+ ExpandedLinkValue,
3
+ ExpandedObjectRow,
4
+ ObjectLinkRow,
5
+ ObjectRow,
6
+ ObjectRowLinks,
7
+ } from "@sixb/core/storage"
8
+
9
+ export function rowToObject(row: ObjectDatabaseRow): ObjectRow {
10
+ return {
11
+ projectId: row.project_id,
12
+ objectTypeId: row.object_type_id,
13
+ primaryId: row.primary_id,
14
+ properties: row.properties as Record<string, unknown>,
15
+ createdAt: new Date(row.created_at),
16
+ updatedAt: new Date(row.updated_at),
17
+ version: row.version,
18
+ lastCommitId: row.last_commit_id,
19
+ }
20
+ }
21
+
22
+ // Map a query row, attaching `links` when an `expand` pushdown produced them. The
23
+ // base columns map as usual; `_expand` (a `jsonb_build_object`) is revived into
24
+ // the runtime link shape the core executor's fallback also produces.
25
+ export function queryRowToObject(row: ObjectQueryDatabaseRow): ObjectRow {
26
+ const base = rowToObject(row)
27
+ const links = reviveExpandedLinks(row._expand)
28
+ return links ? { ...base, links } : base
29
+ }
30
+
31
+ function reviveExpandedLinks(value: unknown): ObjectRowLinks | undefined {
32
+ if (!isPlainRecord(value)) return undefined
33
+ const links: ObjectRowLinks = {}
34
+ for (const [linkId, raw] of Object.entries(value)) {
35
+ links[linkId] = reviveExpandedLinkValue(raw)
36
+ }
37
+ return links
38
+ }
39
+
40
+ // A "one" expansion arrives as a single object or null; a "many" expansion as an
41
+ // array (already ordered and trimmed in the database).
42
+ function reviveExpandedLinkValue(value: unknown): ExpandedLinkValue {
43
+ if (value === null || value === undefined) return null
44
+ if (Array.isArray(value)) return value.map(reviveExpandedRow)
45
+ return reviveExpandedRow(value)
46
+ }
47
+
48
+ // Revive one hydrated neighbour from `compileExpansionChildJson`: JSONB timestamp
49
+ // strings back to `Date`, empty link properties dropped (matching the fallback),
50
+ // and nested links recursed.
51
+ function reviveExpandedRow(value: unknown): ExpandedObjectRow {
52
+ const row = isPlainRecord(value) ? value : {}
53
+ const expanded: ExpandedObjectRow = {
54
+ projectId: String(row.projectId),
55
+ objectTypeId: String(row.objectTypeId),
56
+ primaryId: String(row.primaryId),
57
+ properties: isPlainRecord(row.properties) ? row.properties : {},
58
+ createdAt: new Date(row.createdAt as string),
59
+ updatedAt: new Date(row.updatedAt as string),
60
+ version: Number(row.version),
61
+ lastCommitId: String(row.lastCommitId),
62
+ }
63
+ if (isPlainRecord(row.linkProperties) && Object.keys(row.linkProperties).length > 0) {
64
+ expanded.linkProperties = row.linkProperties
65
+ }
66
+ const links = reviveExpandedLinks(row.links)
67
+ if (links) expanded.links = links
68
+ return expanded
69
+ }
70
+
71
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
72
+ return typeof value === "object" && value !== null && !Array.isArray(value)
73
+ }
74
+
75
+ export function rowToLink(row: LinkDatabaseRow): ObjectLinkRow {
76
+ return {
77
+ projectId: row.project_id,
78
+ sourceTypeId: row.source_type_id,
79
+ sourceId: row.source_id,
80
+ linkId: row.link_id,
81
+ targetTypeId: row.target_type_id,
82
+ targetId: row.target_id,
83
+ ...(row.properties === null ? {} : { properties: row.properties as Record<string, unknown> }),
84
+ createdAt: new Date(row.created_at),
85
+ updatedAt: new Date(row.updated_at),
86
+ lastCommitId: row.last_commit_id,
87
+ }
88
+ }
89
+
90
+ export function linkIdentity(link: ObjectLinkRow): string {
91
+ return JSON.stringify([
92
+ link.sourceTypeId,
93
+ link.sourceId,
94
+ link.linkId,
95
+ link.targetTypeId,
96
+ link.targetId,
97
+ ])
98
+ }
99
+
100
+ export interface ObjectDatabaseRow {
101
+ project_id: string
102
+ object_type_id: string
103
+ primary_id: string
104
+ properties: unknown
105
+ created_at: Date | string
106
+ updated_at: Date | string
107
+ version: number
108
+ last_commit_id: string
109
+ }
110
+
111
+ export interface ObjectQueryDatabaseRow extends ObjectDatabaseRow {
112
+ _cursor_properties?: unknown
113
+ /** `jsonb_build_object(linkId, value, ...)` from an `expand` pushdown; absent otherwise. */
114
+ _expand?: unknown
115
+ }
116
+
117
+ export interface ObjectBatchDatabaseRow extends ObjectDatabaseRow {
118
+ _batch_index: number
119
+ }
120
+
121
+ export interface PropertyPermissionBatchDatabaseRow {
122
+ _batch_index: number
123
+ }
124
+
125
+ export interface FacetDatabaseRow {
126
+ value_type: string | null
127
+ value_text: string | null
128
+ count: string | number | bigint
129
+ }
130
+
131
+ export interface LinkDatabaseRow {
132
+ project_id: string
133
+ source_type_id: string
134
+ source_id: string
135
+ link_id: string
136
+ target_type_id: string
137
+ target_id: string
138
+ properties: unknown | null
139
+ created_at: Date | string
140
+ updated_at: Date | string
141
+ last_commit_id: string
142
+ }
143
+
144
+ export interface LinkBatchDatabaseRow extends LinkDatabaseRow {
145
+ _batch_index: number
146
+ }
@@ -0,0 +1,118 @@
1
+ import type {
2
+ CompiledSelectedObjectReadScope,
3
+ ObjectQueryCapabilities,
4
+ ObjectReadExecutionLimits,
5
+ ObjectReadStorage,
6
+ } from "@sixb/core/storage"
7
+ import {
8
+ assertObjectReaderProject,
9
+ assertObjectReadFacetCount,
10
+ assertObjectReadOutputWithinLimit,
11
+ ObjectReadLimitExceededError,
12
+ snapshotObjectReadExecutionLimits,
13
+ } from "@sixb/core/storage"
14
+ import type { SQLClient, SqlParameter } from "../pg-client"
15
+ import { type PgStoreClient, runPgRepeatableReadTransaction } from "../transactions"
16
+ import { compilePgSelectedObjectReadSource, type PgSelectedObjectReadSource } from "./read-scope"
17
+
18
+ import { assertLinkQueryLimit, PgObjectReader } from "./reader"
19
+
20
+ export function createPgSelectedReader(
21
+ sql: PgStoreClient,
22
+ params: {
23
+ projectId: string
24
+ scope: CompiledSelectedObjectReadScope
25
+ limits: ObjectReadExecutionLimits
26
+ },
27
+ queryCapabilities: () => ObjectQueryCapabilities
28
+ ): ObjectReadStorage {
29
+ const projectId = params.projectId
30
+ const limits = snapshotObjectReadExecutionLimits(params.limits)
31
+ const source = compilePgSelectedObjectReadSource(
32
+ projectId,
33
+ params.scope,
34
+ limits.maxTraversalFacts
35
+ )
36
+ const assertProject = (actualProjectId: string): void =>
37
+ assertObjectReaderProject(projectId, actualProjectId)
38
+ const read = <T>(run: (reader: PgObjectReader) => Promise<T>): Promise<T> =>
39
+ runPgRepeatableReadTransaction(sql, async (tx) => {
40
+ await assertTraversalBudget(tx, source, limits.maxTraversalFacts)
41
+ const value = await run(new PgObjectReader(tx, source))
42
+ assertObjectReadOutputWithinLimit(value, limits)
43
+ return value
44
+ })
45
+ const readMap = <TKey, TValue>(
46
+ run: (reader: PgObjectReader) => Promise<Map<TKey, TValue>>
47
+ ): Promise<Map<TKey, TValue>> =>
48
+ runPgRepeatableReadTransaction(sql, async (tx) => {
49
+ await assertTraversalBudget(tx, source, limits.maxTraversalFacts)
50
+ const value = await run(new PgObjectReader(tx, source))
51
+ assertObjectReadOutputWithinLimit([...value.entries()], limits)
52
+ return value
53
+ })
54
+
55
+ return Object.freeze({
56
+ queryCapabilities,
57
+ queryObjects: async (input) => {
58
+ assertProject(input.projectId)
59
+ return read((reader) => reader.queryObjects(input))
60
+ },
61
+ countObjects: async (input) => {
62
+ assertProject(input.projectId)
63
+ return read((reader) => reader.countObjects(input))
64
+ },
65
+ existsObjects: async (input) => {
66
+ assertProject(input.projectId)
67
+ return read((reader) => reader.existsObjects(input))
68
+ },
69
+ facetObjects: async (input) => {
70
+ assertProject(input.projectId)
71
+ assertObjectReadFacetCount(input.facets.length)
72
+ return read((reader) => reader.facetObjects(input))
73
+ },
74
+ getByPrimaryId: async (input) => {
75
+ assertProject(input.projectId)
76
+ return read((reader) => reader.getByPrimaryId(input))
77
+ },
78
+ selectsObjectProperties: async (input) => {
79
+ assertProject(input.projectId)
80
+ return read((reader) => reader.selectsObjectProperties(input))
81
+ },
82
+ listLinks: async (input) => {
83
+ assertProject(input.projectId)
84
+ return read((reader) => reader.listLinks(input))
85
+ },
86
+ getByPrimaryIdBatch: async (input) => {
87
+ assertProject(input.projectId)
88
+ return readMap((reader) => reader.getByPrimaryIdBatch(input))
89
+ },
90
+ listLinksBatch: async (input) => {
91
+ assertProject(input.projectId)
92
+ return readMap((reader) => reader.listLinksBatch(input))
93
+ },
94
+ queryLinks: async (input) => {
95
+ assertProject(input.projectId)
96
+ assertLinkQueryLimit(input.limit)
97
+ return read((reader) => reader.queryLinks(input))
98
+ },
99
+ list: async (input) => {
100
+ assertProject(input.projectId)
101
+ return read((reader) => reader.list(input))
102
+ },
103
+ } satisfies ObjectReadStorage)
104
+ }
105
+
106
+ async function assertTraversalBudget(
107
+ sql: SQLClient,
108
+ source: PgSelectedObjectReadSource,
109
+ maxTraversalFacts: number
110
+ ): Promise<void> {
111
+ const [row] = await sql.unsafe<{ total: string | number | bigint }[]>(
112
+ source.traversalProbe.sql,
113
+ source.traversalProbe.args as SqlParameter[]
114
+ )
115
+ if (BigInt(row?.total ?? 0) > BigInt(maxTraversalFacts)) {
116
+ throw new ObjectReadLimitExceededError("traversalFacts", maxTraversalFacts)
117
+ }
118
+ }
@@ -0,0 +1,305 @@
1
+ import type {
2
+ CompiledSelectedObjectReadScope,
3
+ CountObjectsInput,
4
+ CountObjectsResult,
5
+ ExistsObjectsInput,
6
+ ExistsObjectsResult,
7
+ FacetObjectsInput,
8
+ FacetObjectsResult,
9
+ LinkBatchKey,
10
+ LinkDirection,
11
+ ObjectBatchKey,
12
+ ObjectLinkRow,
13
+ ObjectQueryCapabilities,
14
+ ObjectReadExecutionLimits,
15
+ ObjectReadStorage,
16
+ ObjectRow,
17
+ ObjectStorage,
18
+ QueryObjectLinksInput,
19
+ QueryObjectLinksResult,
20
+ QueryObjectsInput,
21
+ QueryObjectsResult,
22
+ } from "@sixb/core/storage"
23
+ import type { SQLClient, SqlParameter } from "../pg-client"
24
+ import type { PgStoreClient } from "../transactions"
25
+ import { DEFAULT_OBJECT_QUERY_SOURCE } from "./query-compiler"
26
+ import { assertLinkQueryLimit, PgObjectReader } from "./reader"
27
+ import {
28
+ type LinkDatabaseRow,
29
+ linkIdentity,
30
+ type ObjectDatabaseRow,
31
+ rowToLink,
32
+ rowToObject,
33
+ } from "./rows"
34
+ import { createPgSelectedReader } from "./selected-reader"
35
+
36
+ const PG_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
37
+ queryObjects: true,
38
+ countObjects: true,
39
+ existsObjects: true,
40
+ facetObjects: true,
41
+ nodes: {
42
+ start: true,
43
+ refs: true,
44
+ filter: true,
45
+ text: true,
46
+ sort: true,
47
+ limit: true,
48
+ page: true,
49
+ traverse: true,
50
+ set: true,
51
+ project: true,
52
+ expand: true,
53
+ },
54
+ predicateOps: {
55
+ and: true,
56
+ or: true,
57
+ not: true,
58
+ eq: true,
59
+ neq: true,
60
+ lt: true,
61
+ lte: true,
62
+ gt: true,
63
+ gte: true,
64
+ in: true,
65
+ exists: true,
66
+ contains: true,
67
+ },
68
+ sortKinds: {
69
+ property: true,
70
+ },
71
+ traversalDirections: {
72
+ outgoing: true,
73
+ incoming: true,
74
+ },
75
+ setOps: {
76
+ union: true,
77
+ intersect: true,
78
+ subtract: true,
79
+ },
80
+ scalarOperations: {
81
+ string: { equality: true, ordering: true },
82
+ uuid: { equality: true, ordering: true },
83
+ boolean: { equality: true },
84
+ integer: { equality: true, ordering: true },
85
+ double: { equality: true, ordering: true },
86
+ decimal: { equality: true, ordering: true },
87
+ date: { equality: true, ordering: true },
88
+ timestamp: { equality: true, ordering: true },
89
+ },
90
+ limits: {
91
+ totalCount: true,
92
+ stablePageTokens: true,
93
+ },
94
+ notes: [
95
+ "PostgreSQL object query pushdown supports start/refs/filter/text/sort/limit/page/traverse/set/project/expand over JSONB properties and object links.",
96
+ "expand hydrates linked objects in-database (top-N per parent via LATERAL + jsonb_agg); core resolves each expansion's cardinality before pushdown, and a mixed/unresolved one stays on the fallback.",
97
+ "Exact decimal predicates, ordering, and keyset pagination use PostgreSQL numeric casts.",
98
+ "Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases.",
99
+ ],
100
+ }
101
+
102
+ /** PostgreSQL object provider; interactive reads share one source-aware reader. */
103
+ export class PgObjectStorage implements ObjectStorage {
104
+ private readonly reader: PgObjectReader
105
+
106
+ constructor(private readonly sql: PgStoreClient) {
107
+ this.reader = new PgObjectReader(sql, DEFAULT_OBJECT_QUERY_SOURCE)
108
+ }
109
+
110
+ queryCapabilities(): ObjectQueryCapabilities {
111
+ return PG_OBJECT_QUERY_CAPABILITIES
112
+ }
113
+
114
+ createSelectedReadScope(params: {
115
+ projectId: string
116
+ scope: CompiledSelectedObjectReadScope
117
+ limits: ObjectReadExecutionLimits
118
+ }): ObjectReadStorage {
119
+ return createPgSelectedReader(this.sql, params, () => this.queryCapabilities())
120
+ }
121
+
122
+ async queryObjects(params: QueryObjectsInput): Promise<QueryObjectsResult> {
123
+ return this.reader.queryObjects(params)
124
+ }
125
+
126
+ async countObjects(params: CountObjectsInput): Promise<CountObjectsResult> {
127
+ return this.reader.countObjects(params)
128
+ }
129
+
130
+ async existsObjects(params: ExistsObjectsInput): Promise<ExistsObjectsResult> {
131
+ return this.reader.existsObjects(params)
132
+ }
133
+
134
+ async facetObjects(params: FacetObjectsInput): Promise<FacetObjectsResult> {
135
+ return this.reader.facetObjects(params)
136
+ }
137
+
138
+ async getByPrimaryId(params: {
139
+ projectId: string
140
+ objectTypeId: string
141
+ primaryId: string
142
+ }): Promise<ObjectRow | null> {
143
+ return this.reader.getByPrimaryId(params)
144
+ }
145
+
146
+ async selectsObjectProperties(
147
+ params: Parameters<ObjectReadStorage["selectsObjectProperties"]>[0]
148
+ ): Promise<readonly boolean[]> {
149
+ return this.reader.selectsObjectProperties(params)
150
+ }
151
+
152
+ async listLinks(params: {
153
+ projectId: string
154
+ objectTypeId: string
155
+ objectId: string
156
+ linkId?: string
157
+ direction?: LinkDirection
158
+ }): Promise<readonly ObjectLinkRow[]> {
159
+ return this.reader.listLinks(params)
160
+ }
161
+
162
+ async getByPrimaryIdBatch(params: {
163
+ projectId: string
164
+ items: readonly { objectTypeId: string; primaryId: string }[]
165
+ }): Promise<Map<ObjectBatchKey, ObjectRow>> {
166
+ return this.reader.getByPrimaryIdBatch(params)
167
+ }
168
+
169
+ async listLinksBatch(params: {
170
+ projectId: string
171
+ direction?: LinkDirection
172
+ items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
173
+ }): Promise<Map<LinkBatchKey, ObjectLinkRow[]>> {
174
+ return this.reader.listLinksBatch(params)
175
+ }
176
+
177
+ async queryLinks(params: QueryObjectLinksInput): Promise<QueryObjectLinksResult> {
178
+ assertLinkQueryLimit(params.limit)
179
+ return this.reader.queryLinks(params)
180
+ }
181
+
182
+ async listIncidentLinksBatch(params: {
183
+ projectId: string
184
+ items: readonly { objectTypeId: string; objectId: string }[]
185
+ }): Promise<readonly ObjectLinkRow[]> {
186
+ if (params.items.length === 0) return []
187
+ const tuples = params.items.map((item) => [item.objectTypeId, item.objectId])
188
+
189
+ // Cover both link directions with two index-friendly equality joins (source-side, then
190
+ // target-side) rather than a single OR-join, which would defeat index usage. This is a constant
191
+ // number of round trips regardless of the requested object count. They are issued sequentially
192
+ // because callers may run on a serializable transaction's single connection. A link incident to
193
+ // two listed objects matches both halves and is de-duplicated below.
194
+ const sourceRows = await valuesJoin<LinkDatabaseRow>(
195
+ this.sql,
196
+ "SELECT l.* FROM links l",
197
+ ["source_type_id", "source_id"],
198
+ tuples,
199
+ "WHERE l.project_id = $1",
200
+ [params.projectId]
201
+ )
202
+ const targetRows = await valuesJoin<LinkDatabaseRow>(
203
+ this.sql,
204
+ "SELECT l.* FROM links l",
205
+ ["target_type_id", "target_id"],
206
+ tuples,
207
+ "WHERE l.project_id = $1",
208
+ [params.projectId]
209
+ )
210
+
211
+ const deduped = new Map<string, ObjectLinkRow>()
212
+ for (const row of [...sourceRows, ...targetRows]) {
213
+ const link = rowToLink(row)
214
+ deduped.set(linkIdentity(link), link)
215
+ }
216
+ return [...deduped.values()]
217
+ }
218
+
219
+ async listByPrimaryIdPage(params: {
220
+ projectId: string
221
+ objectTypeId: string
222
+ afterPrimaryId?: string
223
+ limit: number
224
+ }): Promise<{ objects: readonly ObjectRow[]; nextPrimaryId?: string }> {
225
+ assertReconciliationPageLimit(params.limit)
226
+ const rows = await this.sql<ObjectDatabaseRow[]>`
227
+ SELECT * FROM objects
228
+ WHERE project_id = ${params.projectId}
229
+ AND object_type_id = ${params.objectTypeId}
230
+ ${params.afterPrimaryId ? this.sql`AND primary_id > ${params.afterPrimaryId}` : this.sql``}
231
+ ORDER BY primary_id ASC
232
+ LIMIT ${params.limit + 1}
233
+ `
234
+ const hasMore = rows.length > params.limit
235
+ const objects = rows.slice(0, params.limit).map((row) => rowToObject(row))
236
+ const last = objects.at(-1)
237
+ return {
238
+ objects,
239
+ ...(hasMore && last ? { nextPrimaryId: last.primaryId } : {}),
240
+ }
241
+ }
242
+
243
+ async list(params: {
244
+ projectId: string
245
+ objectTypeId?: string | readonly string[]
246
+ primaryIdPrefix?: string
247
+ primaryIdSuffix?: string
248
+ updatedAfter?: Date
249
+ updatedBefore?: Date
250
+ createdAfter?: Date
251
+ createdBefore?: Date
252
+ limit?: number
253
+ offset?: number
254
+ orderBy?: "createdAt" | "updatedAt" | "primaryId"
255
+ order?: "asc" | "desc"
256
+ }): Promise<{ objects: readonly ObjectRow[]; hasMore: boolean; total: number }> {
257
+ return this.reader.list(params)
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Build a `SELECT ... JOIN (VALUES ...) AS t(...) ON ... WHERE ...` query
263
+ * with positional parameters. Bun SQL's tagged-template helpers don't
264
+ * support VALUES inside SELECT/JOIN, so we construct the query string
265
+ * manually while keeping all user data in the parameter array.
266
+ */
267
+ function valuesJoin<Row = unknown>(
268
+ sql: SQLClient,
269
+ select: string,
270
+ columns: string[],
271
+ tuples: unknown[][],
272
+ where: string,
273
+ whereParams: unknown[]
274
+ ): Promise<Row[]> {
275
+ const alias = "t"
276
+ const colWidth = columns.length
277
+
278
+ // $1 … $N are reserved for the WHERE params that come first.
279
+ const base = whereParams.length
280
+ const valuePlaceholders = tuples
281
+ .map((_, i) => {
282
+ const cols = columns.map((_, j) => `$${base + i * colWidth + j + 1}`)
283
+ return `(${cols.join(",")})`
284
+ })
285
+ .join(",")
286
+
287
+ const onClause = columns
288
+ .map((c) => {
289
+ // Infer the table alias from the SELECT clause (first word after SELECT ... FROM)
290
+ const srcAlias = select.match(/FROM\s+\w+\s+(\w+)/i)?.[1] ?? select.split(" ").pop()
291
+ return `${srcAlias}.${c} = ${alias}.${c}`
292
+ })
293
+ .join(" AND ")
294
+
295
+ const query = `${select} JOIN (VALUES ${valuePlaceholders}) AS ${alias}(${columns.join(",")}) ON ${onClause} ${where}`
296
+ const params = [...whereParams, ...tuples.flat()]
297
+
298
+ return sql.unsafe(query, params as SqlParameter[]) as unknown as Promise<Row[]>
299
+ }
300
+
301
+ function assertReconciliationPageLimit(limit: number): void {
302
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
303
+ throw new Error("Object reconciliation page limit must be a positive safe integer.")
304
+ }
305
+ }
@@ -5,7 +5,12 @@ import {
5
5
  normalizeExecutionRecord,
6
6
  validateExecutionRecordReferences,
7
7
  } from "@sixb/core/internal/execution-storage"
8
- import type { CreateExecutionInput, ExecutionRecord, ExecutionStorage } from "@sixb/core/storage"
8
+ import type {
9
+ CreateExecutionInput,
10
+ ExecutionRecord,
11
+ ExecutionStorage,
12
+ ShareSessionStorage,
13
+ } from "@sixb/core/storage"
9
14
  import { ExecutionStorageError } from "@sixb/core/storage"
10
15
  import type { PgAuthStorage } from "./auth-storage"
11
16
  import { isUniqueViolation } from "./storage-errors"
@@ -14,7 +19,8 @@ import type { PgStoreClient } from "./transactions"
14
19
  export class PgExecutionStorage implements ExecutionStorage {
15
20
  constructor(
16
21
  private readonly sql: PgStoreClient,
17
- private readonly auth: PgAuthStorage
22
+ private readonly auth: PgAuthStorage,
23
+ private readonly shareSessions: Pick<ShareSessionStorage, "getById">
18
24
  ) {}
19
25
 
20
26
  async create(input: CreateExecutionInput): Promise<ExecutionRecord> {
@@ -22,6 +28,7 @@ export class PgExecutionStorage implements ExecutionStorage {
22
28
  await validateExecutionRecordReferences(record, {
23
29
  auth: this.auth,
24
30
  getExecution: (params) => this.getById(params),
31
+ getShareSession: (params) => this.shareSessions.getById(params),
25
32
  })
26
33
  const row = executionRecordToStorageRow(record)
27
34
 
@@ -46,6 +53,9 @@ export class PgExecutionStorage implements ExecutionStorage {
46
53
  authority_primitive_kind,
47
54
  authority_primitive_id,
48
55
  authority_kernel_operation,
56
+ authority_delegation_kind,
57
+ authority_delegation_id,
58
+ authority_delegation_session_id,
49
59
  created_at
50
60
  ) VALUES (
51
61
  ${row.projectId},
@@ -66,6 +76,9 @@ export class PgExecutionStorage implements ExecutionStorage {
66
76
  ${row.authorityPrimitiveKind},
67
77
  ${row.authorityPrimitiveId},
68
78
  ${row.authorityKernelOperation},
79
+ ${row.authorityDelegationKind},
80
+ ${row.authorityDelegationId},
81
+ ${row.authorityDelegationSessionId},
69
82
  ${row.createdAt}
70
83
  )
71
84
  RETURNING *
@@ -120,6 +133,9 @@ function toStorageRow(row: PgExecutionRow): ExecutionStorageRow {
120
133
  authorityPrimitiveKind: row.authority_primitive_kind,
121
134
  authorityPrimitiveId: row.authority_primitive_id,
122
135
  authorityKernelOperation: row.authority_kernel_operation,
136
+ authorityDelegationKind: row.authority_delegation_kind,
137
+ authorityDelegationId: row.authority_delegation_id,
138
+ authorityDelegationSessionId: row.authority_delegation_session_id,
123
139
  createdAt: new Date(row.created_at),
124
140
  }
125
141
  }
@@ -143,5 +159,8 @@ interface PgExecutionRow {
143
159
  readonly authority_primitive_kind: ExecutionStorageRow["authorityPrimitiveKind"]
144
160
  readonly authority_primitive_id: string | null
145
161
  readonly authority_kernel_operation: ExecutionStorageRow["authorityKernelOperation"]
162
+ readonly authority_delegation_kind: ExecutionStorageRow["authorityDelegationKind"]
163
+ readonly authority_delegation_id: string | null
164
+ readonly authority_delegation_session_id: string | null
146
165
  readonly created_at: Date | string
147
166
  }