@sixb/pg 0.1.7 → 0.1.9
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/dist/agents/rows.d.ts +0 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7596 -6416
- package/dist/index.js.map +25 -19
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/index.d.ts +2 -0
- package/dist/objects/index.d.ts.map +1 -0
- package/dist/{pg-object-query-compiler.d.ts → objects/query-compiler.d.ts} +25 -4
- package/dist/objects/query-compiler.d.ts.map +1 -0
- package/dist/objects/read-scope.d.ts +17 -0
- package/dist/objects/read-scope.d.ts.map +1 -0
- package/dist/objects/reader.d.ts +50 -0
- package/dist/objects/reader.d.ts.map +1 -0
- package/dist/objects/rows.d.ts +47 -0
- package/dist/objects/rows.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +8 -0
- package/dist/objects/selected-reader.d.ts.map +1 -0
- package/dist/{pg-object-storage.d.ts → objects/storage.d.ts} +12 -20
- package/dist/objects/storage.d.ts.map +1 -0
- package/dist/pg-action-run-storage.d.ts.map +1 -1
- package/dist/pg-ai-cost-storage.d.ts.map +1 -1
- package/dist/pg-ai-usage-storage.d.ts.map +1 -1
- package/dist/pg-execution-storage.d.ts +3 -2
- package/dist/pg-execution-storage.d.ts.map +1 -1
- package/dist/pg-pipeline-run-storage.d.ts.map +1 -1
- package/dist/pg-sync-run-storage.d.ts.map +1 -1
- package/dist/pg-workflow-run-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +11 -0
- package/dist/share-grant-storage.d.ts.map +1 -0
- package/dist/share-session-storage.d.ts +11 -0
- package/dist/share-session-storage.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/dist/transactions.d.ts +9 -2
- package/dist/transactions.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/agents/rows.ts +0 -5
- package/src/agents/runs.ts +1 -6
- package/src/ai-cost-groups.ts +3 -0
- package/src/index.ts +20 -5
- package/src/migrations/035-share-grants.sql +57 -0
- package/src/migrations/036-share-sessions.sql +173 -0
- package/src/migrations/037-execution-requester-groups.sql +21 -0
- package/src/migrations.ts +9 -0
- package/src/objects/index.ts +1 -0
- package/src/{pg-object-query-compiler.ts → objects/query-compiler.ts} +185 -100
- package/src/objects/read-scope.ts +324 -0
- package/src/objects/reader.ts +604 -0
- package/src/objects/rows.ts +146 -0
- package/src/objects/selected-reader.ts +118 -0
- package/src/objects/storage.ts +305 -0
- package/src/pg-action-run-storage.ts +6 -7
- package/src/pg-ai-cost-storage.ts +11 -2
- package/src/pg-ai-usage-storage.ts +1 -2
- package/src/pg-execution-storage.ts +27 -6
- package/src/pg-pipeline-run-storage.ts +2 -5
- package/src/pg-sync-run-storage.ts +2 -5
- package/src/pg-workflow-run-storage.ts +0 -5
- package/src/share-grant-storage.ts +236 -0
- package/src/share-session-storage.ts +127 -0
- package/src/storage-errors.ts +6 -0
- package/src/transactions.ts +46 -5
- package/dist/pg-object-query-compiler.d.ts.map +0 -1
- package/dist/pg-object-storage.d.ts.map +0 -1
- 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
|
+
}
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
serializeActionRunFailure,
|
|
5
5
|
} from "@sixb/core/internal/action-run-storage"
|
|
6
6
|
import { assertActionRunExecution } from "@sixb/core/internal/action-run-storage-provider"
|
|
7
|
+
import {
|
|
8
|
+
actionRunPhaseRecordsEqual,
|
|
9
|
+
canRequeueActionRunAfterEnqueueFailure,
|
|
10
|
+
finishActionRunPhase,
|
|
11
|
+
} from "@sixb/core/internal/storage"
|
|
7
12
|
import type {
|
|
8
13
|
ActionRunEffectsRecord,
|
|
9
14
|
ActionRunFailure,
|
|
@@ -23,13 +28,7 @@ import type {
|
|
|
23
28
|
RecordActionWritebackInput,
|
|
24
29
|
StartActionRunInput,
|
|
25
30
|
} from "@sixb/core/storage"
|
|
26
|
-
import {
|
|
27
|
-
ActionRunError,
|
|
28
|
-
actionRunPhaseRecordsEqual,
|
|
29
|
-
canRequeueActionRunAfterEnqueueFailure,
|
|
30
|
-
finishActionRunPhase,
|
|
31
|
-
isTerminalActionRun,
|
|
32
|
-
} from "@sixb/core/storage"
|
|
31
|
+
import { ActionRunError, isTerminalActionRun } from "@sixb/core/storage"
|
|
33
32
|
import type { SQLClient, SqlParameter } from "./pg-client"
|
|
34
33
|
import { isUniqueViolation } from "./storage-errors"
|
|
35
34
|
import { type PgStoreClient, runPgTransaction } from "./transactions"
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
aiModelCallCostDetails,
|
|
5
5
|
aiModelCallCostMatchesUsage,
|
|
6
6
|
buildAiAccountingOverviewFromFragments,
|
|
7
|
+
directModelCallAttribution,
|
|
7
8
|
normalizeAiAccountingQuery,
|
|
8
9
|
normalizeAiModelCallAccountingQuery,
|
|
9
10
|
normalizeAiModelCallCostRecord,
|
|
@@ -117,11 +118,14 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
117
118
|
ELSE NULL
|
|
118
119
|
END AS attribution_agent_kind,
|
|
119
120
|
workflow_node.node_id AS attribution_agent_step_id,
|
|
120
|
-
workflow_node.workflow_id
|
|
121
|
+
COALESCE(workflow_node.workflow_id,
|
|
122
|
+
CASE WHEN execution.executor_kind = 'workflow' THEN execution.authority_primitive_id END
|
|
123
|
+
) AS attribution_workflow_id,
|
|
121
124
|
cost.status AS cost_status,
|
|
122
125
|
cost.currency AS cost_currency,
|
|
123
126
|
cost.amount_nanos AS cost_amount_nanos
|
|
124
127
|
FROM ai_model_call_usage AS usage
|
|
128
|
+
JOIN executions AS execution ON execution.project_id = usage.project_id AND execution.id = usage.execution_id
|
|
125
129
|
LEFT JOIN agent_runs AS direct_agent
|
|
126
130
|
ON direct_agent.project_id = usage.project_id
|
|
127
131
|
AND direct_agent.execution_id = usage.execution_id
|
|
@@ -251,6 +255,7 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
251
255
|
workflow_agent.node_run_id AS attribution_node_run_id,
|
|
252
256
|
workflow_node.workflow_id AS attribution_workflow_id,
|
|
253
257
|
workflow_node.workflow_run_id AS attribution_workflow_run_id,
|
|
258
|
+
execution.executor_kind, execution.executor_id, execution.authority_primitive_id AS primitive_id,
|
|
254
259
|
COALESCE((
|
|
255
260
|
SELECT jsonb_agg(groups.group_id ORDER BY groups.group_id)
|
|
256
261
|
FROM ai_model_call_usage_groups AS groups
|
|
@@ -265,6 +270,7 @@ export class PgAiCostStorage implements AiCostStorage {
|
|
|
265
270
|
cost.details AS cost_details,
|
|
266
271
|
cost.rated_at AS cost_rated_at
|
|
267
272
|
FROM ai_model_call_usage AS usage
|
|
273
|
+
JOIN executions AS execution ON execution.project_id = usage.project_id AND execution.id = usage.execution_id
|
|
268
274
|
LEFT JOIN agent_runs AS direct_agent
|
|
269
275
|
ON direct_agent.project_id = usage.project_id
|
|
270
276
|
AND direct_agent.execution_id = usage.execution_id
|
|
@@ -342,6 +348,9 @@ interface UsageRow {
|
|
|
342
348
|
}
|
|
343
349
|
|
|
344
350
|
interface JoinedRow extends UsageRow {
|
|
351
|
+
readonly executor_kind: string | null
|
|
352
|
+
readonly executor_id: string | null
|
|
353
|
+
readonly primitive_id: string | null
|
|
345
354
|
readonly attribution_kind: "agent" | "subagent" | "workflowAgent" | null
|
|
346
355
|
readonly attribution_agent_step_id: string | null
|
|
347
356
|
readonly attribution_agent_run_id: string | null
|
|
@@ -491,7 +500,7 @@ function accountingItemFromRow(row: JoinedRow): AiAccountingRecordSetItem {
|
|
|
491
500
|
const cost = row.cost_status === null ? undefined : costFromJoinedRow(row)
|
|
492
501
|
const attribution =
|
|
493
502
|
row.attribution_kind === null
|
|
494
|
-
?
|
|
503
|
+
? directModelCallAttribution(row.executor_kind, row.executor_id, row.primitive_id)
|
|
495
504
|
: row.attribution_kind === "agent"
|
|
496
505
|
? {
|
|
497
506
|
kind: "agent" as const,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertAiUsageExecutionId, normalizeAiModelCallRecord } from "@sixb/core/internal/storage"
|
|
1
2
|
import { isModelReasoning, normalizeModelProviderIds } from "@sixb/core/models"
|
|
2
3
|
import type { ReadonlyJsonObject } from "@sixb/core/storage"
|
|
3
4
|
import {
|
|
@@ -7,9 +8,7 @@ import {
|
|
|
7
8
|
type AiUsageStorage,
|
|
8
9
|
AiUsageStorageError,
|
|
9
10
|
aggregateAiModelCallUsage,
|
|
10
|
-
assertAiUsageExecutionId,
|
|
11
11
|
type GetLatestAiModelCallForExecutionInput,
|
|
12
|
-
normalizeAiModelCallRecord,
|
|
13
12
|
type RecordAiModelCallInput,
|
|
14
13
|
type RecordAiModelCallResult,
|
|
15
14
|
type SummarizeAiUsageExecutionInput,
|