@sixb/sqlite 0.1.2 → 0.1.4
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/agent-storage.d.ts +5 -3
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/agents/checkpoints.d.ts +18 -0
- package/dist/agents/checkpoints.d.ts.map +1 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/messages.d.ts +2 -1
- package/dist/agents/messages.d.ts.map +1 -1
- package/dist/agents/rows.d.ts +18 -1
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/ai-cost-storage.d.ts +15 -0
- package/dist/ai-cost-storage.d.ts.map +1 -0
- package/dist/ai-usage-storage.d.ts +2 -1
- package/dist/ai-usage-storage.d.ts.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +979 -74
- package/dist/index.js.map +15 -13
- package/dist/migrations.d.ts.map +1 -1
- package/dist/object-storage.d.ts +4 -3
- package/dist/object-storage.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/agent-storage.ts +6 -3
- package/src/agents/checkpoints.ts +205 -0
- package/src/agents/index.ts +1 -0
- package/src/agents/messages.ts +59 -0
- package/src/agents/rows.ts +44 -0
- package/src/ai-cost-storage.ts +744 -0
- package/src/ai-usage-storage.ts +19 -0
- package/src/index.ts +8 -2
- package/src/migrations/026-ai-cost-accounting.sql +57 -0
- package/src/migrations/027-agent-context-checkpoints.sql +28 -0
- package/src/migrations/028-object-override-edit-times.sql +3 -0
- package/src/migrations.ts +10 -0
- package/src/object-query-compiler.ts +50 -0
- package/src/object-storage.ts +138 -34
- package/src/ontology-storage/materialization-state.ts +2 -0
- package/src/ontology-storage/materialization-writer.ts +6 -3
package/src/ai-usage-storage.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
AiUsageStorageError,
|
|
8
8
|
aggregateAiModelCallUsage,
|
|
9
9
|
assertAiUsageExecutionId,
|
|
10
|
+
type GetLatestAiModelCallForExecutionInput,
|
|
10
11
|
normalizeAiModelCallRecord,
|
|
11
12
|
type RecordAiModelCallInput,
|
|
12
13
|
type RecordAiModelCallResult,
|
|
@@ -84,6 +85,24 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
|
|
|
84
85
|
return summary!
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
async getLatestForExecution(
|
|
89
|
+
input: GetLatestAiModelCallForExecutionInput
|
|
90
|
+
): Promise<AiModelCallUsageRecord | null> {
|
|
91
|
+
assertNonBlankProjectId(input.projectId)
|
|
92
|
+
assertAiUsageExecutionId(input.executionId)
|
|
93
|
+
const row = this.connection.db
|
|
94
|
+
.query(
|
|
95
|
+
`
|
|
96
|
+
SELECT * FROM ai_model_call_usage
|
|
97
|
+
WHERE project_id = ? AND execution_id = ?
|
|
98
|
+
ORDER BY occurred_at DESC, id DESC
|
|
99
|
+
LIMIT 1
|
|
100
|
+
`
|
|
101
|
+
)
|
|
102
|
+
.get(input.projectId, input.executionId) as AiUsageRow | null
|
|
103
|
+
return row ? this.rowToRecord(row) : null
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
async summarizeExecutions(
|
|
88
107
|
input: SummarizeAiUsageExecutionsInput
|
|
89
108
|
): Promise<readonly AiUsageExecutionSummary[]> {
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
createWorkflowRunOperationScope,
|
|
20
20
|
} from "@sixb/core/internal/storage-operation-scope"
|
|
21
21
|
import {
|
|
22
|
+
type AiCostStorage,
|
|
22
23
|
type AiUsageStorage,
|
|
23
24
|
createTransactionStorageProxy,
|
|
24
25
|
StorageTransactionError,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
} from "@sixb/core/storage"
|
|
27
28
|
import { SqliteActionRunStorage } from "./action-run-storage"
|
|
28
29
|
import { SqliteAgentStorage } from "./agents"
|
|
30
|
+
import { SqliteAiCostStorage } from "./ai-cost-storage"
|
|
29
31
|
import { SqliteAiUsageStorage } from "./ai-usage-storage"
|
|
30
32
|
import { SqliteAuthStorage } from "./auth-storage"
|
|
31
33
|
import { SqliteConnectorConnectionStorage } from "./connector-connection-storage"
|
|
@@ -61,8 +63,8 @@ export interface SqliteStorageOptions {
|
|
|
61
63
|
/**
|
|
62
64
|
* SQLite storage provider for Sixb.
|
|
63
65
|
*
|
|
64
|
-
* Bundles object, timeseries, auth, execution, AI
|
|
65
|
-
* workflow run, webhook run, and connector connection storage backed by SQLite.
|
|
66
|
+
* Bundles object, timeseries, auth, execution, AI accounting, sync run, pipeline run, projection
|
|
67
|
+
* run, workflow run, webhook run, and connector connection storage backed by SQLite.
|
|
66
68
|
*
|
|
67
69
|
* Usage:
|
|
68
70
|
* ```ts
|
|
@@ -81,6 +83,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
81
83
|
readonly executions: SqliteExecutionStorage
|
|
82
84
|
readonly agents: SqliteAgentStorage
|
|
83
85
|
readonly aiUsage: AiUsageStorage
|
|
86
|
+
readonly aiCosts: AiCostStorage
|
|
84
87
|
readonly actionRuns: SqliteActionRunStorage
|
|
85
88
|
readonly pipelineRuns: SqlitePipelineRunStorage
|
|
86
89
|
readonly syncRuns: SqliteSyncRunStorage
|
|
@@ -143,6 +146,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
143
146
|
this.executions = createOperationScopedFacade(stores.executions, scope)
|
|
144
147
|
this.agents = createAgentOperationScope(stores.agents, scope)
|
|
145
148
|
this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope)
|
|
149
|
+
this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope)
|
|
146
150
|
this.actionRuns = createOperationScopedFacade(stores.actionRuns, scope)
|
|
147
151
|
this.pipelineRuns = createOperationScopedFacade(stores.pipelineRuns, scope)
|
|
148
152
|
this.timeseries = createOperationScopedFacade(readTimeseries, readScope)
|
|
@@ -298,6 +302,7 @@ function createSqliteStores(
|
|
|
298
302
|
executions,
|
|
299
303
|
agents: new SqliteAgentStorage({ connection, executions }),
|
|
300
304
|
aiUsage: new SqliteAiUsageStorage({ connection }),
|
|
305
|
+
aiCosts: new SqliteAiCostStorage(connection),
|
|
301
306
|
actionRuns: new SqliteActionRunStorage({ connection, executions }),
|
|
302
307
|
pipelineRuns: new SqlitePipelineRunStorage({ connection, executions }),
|
|
303
308
|
timeseries: new SqliteTimeseriesStorage({ connection }),
|
|
@@ -318,6 +323,7 @@ interface SqliteStoreSet {
|
|
|
318
323
|
readonly executions: SqliteExecutionStorage
|
|
319
324
|
readonly agents: SqliteAgentStorage
|
|
320
325
|
readonly aiUsage: SqliteAiUsageStorage
|
|
326
|
+
readonly aiCosts: SqliteAiCostStorage
|
|
321
327
|
readonly actionRuns: SqliteActionRunStorage
|
|
322
328
|
readonly pipelineRuns: SqlitePipelineRunStorage
|
|
323
329
|
readonly syncRuns: SqliteSyncRunStorage
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
CREATE TABLE ai_model_call_valuations (
|
|
2
|
+
project_id TEXT NOT NULL CHECK (length(trim(project_id)) > 0),
|
|
3
|
+
usage_record_id TEXT NOT NULL CHECK (length(trim(usage_record_id)) > 0),
|
|
4
|
+
status TEXT NOT NULL CHECK (status IN ('rated', 'unpriceable')),
|
|
5
|
+
provider_id TEXT CHECK (provider_id IS NULL OR length(trim(provider_id)) > 0),
|
|
6
|
+
model_id TEXT CHECK (model_id IS NULL OR length(trim(model_id)) > 0),
|
|
7
|
+
currency TEXT CHECK (
|
|
8
|
+
currency IS NULL OR (length(currency) = 3 AND currency GLOB '[A-Z][A-Z][A-Z]')
|
|
9
|
+
),
|
|
10
|
+
amount_nanos INTEGER CHECK (
|
|
11
|
+
amount_nanos IS NULL OR (typeof(amount_nanos) = 'integer' AND amount_nanos >= 0)
|
|
12
|
+
),
|
|
13
|
+
reason TEXT CHECK (
|
|
14
|
+
reason IS NULL OR reason IN (
|
|
15
|
+
'missingBillingIdentity',
|
|
16
|
+
'missingCatalogEntry',
|
|
17
|
+
'missingUsageMeter',
|
|
18
|
+
'unsupportedPricingDimension',
|
|
19
|
+
'invalidUsageForFormula'
|
|
20
|
+
)
|
|
21
|
+
),
|
|
22
|
+
details TEXT NOT NULL CHECK (json_valid(details) AND json_type(details) = 'object'),
|
|
23
|
+
rated_at TEXT NOT NULL CHECK (length(trim(rated_at)) > 0),
|
|
24
|
+
PRIMARY KEY (project_id, usage_record_id),
|
|
25
|
+
FOREIGN KEY (project_id, usage_record_id)
|
|
26
|
+
REFERENCES ai_model_call_usage(project_id, id)
|
|
27
|
+
ON DELETE RESTRICT,
|
|
28
|
+
CHECK (
|
|
29
|
+
(
|
|
30
|
+
status = 'rated'
|
|
31
|
+
AND provider_id IS NOT NULL
|
|
32
|
+
AND model_id IS NOT NULL
|
|
33
|
+
AND currency IS NOT NULL
|
|
34
|
+
AND amount_nanos IS NOT NULL
|
|
35
|
+
AND reason IS NULL
|
|
36
|
+
)
|
|
37
|
+
OR
|
|
38
|
+
(
|
|
39
|
+
status = 'unpriceable'
|
|
40
|
+
AND currency IS NULL
|
|
41
|
+
AND amount_nanos IS NULL
|
|
42
|
+
AND reason IS NOT NULL
|
|
43
|
+
AND (
|
|
44
|
+
(reason = 'missingBillingIdentity' AND provider_id IS NULL AND model_id IS NULL)
|
|
45
|
+
OR
|
|
46
|
+
(reason <> 'missingBillingIdentity' AND provider_id IS NOT NULL AND model_id IS NOT NULL)
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE INDEX idx_ai_model_call_usage_provider_model_time
|
|
53
|
+
ON ai_model_call_usage (project_id, provider_id, requested_model_id, occurred_at, id);
|
|
54
|
+
CREATE INDEX idx_ai_model_call_usage_model_time
|
|
55
|
+
ON ai_model_call_usage (project_id, requested_model_id, occurred_at, id);
|
|
56
|
+
CREATE INDEX idx_ai_model_call_valuations_summary
|
|
57
|
+
ON ai_model_call_valuations (project_id, status, currency, usage_record_id);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
CREATE TABLE agent_context_checkpoints (
|
|
2
|
+
project_id TEXT NOT NULL,
|
|
3
|
+
id TEXT NOT NULL,
|
|
4
|
+
thread_id TEXT NOT NULL,
|
|
5
|
+
created_by_run_id TEXT NOT NULL,
|
|
6
|
+
previous_checkpoint_id TEXT,
|
|
7
|
+
reason TEXT NOT NULL CHECK (reason IN ('threshold', 'overflow')),
|
|
8
|
+
summary TEXT NOT NULL CHECK (length(trim(summary)) > 0),
|
|
9
|
+
summary_format_version INTEGER NOT NULL CHECK (summary_format_version = 1),
|
|
10
|
+
summarized_through_seq INTEGER NOT NULL CHECK (summarized_through_seq >= 1),
|
|
11
|
+
observed_head_seq INTEGER NOT NULL,
|
|
12
|
+
estimated_input_tokens_before INTEGER NOT NULL CHECK (estimated_input_tokens_before >= 0),
|
|
13
|
+
estimated_input_tokens_after INTEGER NOT NULL CHECK (estimated_input_tokens_after >= 0),
|
|
14
|
+
summary_model_id TEXT NOT NULL CHECK (length(trim(summary_model_id)) > 0),
|
|
15
|
+
created_at TEXT NOT NULL,
|
|
16
|
+
PRIMARY KEY (project_id, id),
|
|
17
|
+
UNIQUE (project_id, created_by_run_id),
|
|
18
|
+
FOREIGN KEY (project_id, thread_id)
|
|
19
|
+
REFERENCES agent_threads(project_id, id) ON DELETE CASCADE,
|
|
20
|
+
FOREIGN KEY (project_id, created_by_run_id)
|
|
21
|
+
REFERENCES agent_runs(project_id, id) ON DELETE RESTRICT,
|
|
22
|
+
FOREIGN KEY (project_id, previous_checkpoint_id)
|
|
23
|
+
REFERENCES agent_context_checkpoints(project_id, id) ON DELETE RESTRICT,
|
|
24
|
+
CHECK (summarized_through_seq < observed_head_seq)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE INDEX idx_agent_context_checkpoints_thread_latest
|
|
28
|
+
ON agent_context_checkpoints(project_id, thread_id, summarized_through_seq DESC, created_at DESC, id DESC);
|
package/src/migrations.ts
CHANGED
|
@@ -69,6 +69,13 @@ import ontologyCommitExecutionsSql from "./migrations/024-ontology-commit-execut
|
|
|
69
69
|
import connectorConnectionsSql from "./migrations/025-connector-connections.sql" with {
|
|
70
70
|
type: "text",
|
|
71
71
|
}
|
|
72
|
+
import aiCostAccountingSql from "./migrations/026-ai-cost-accounting.sql" with { type: "text" }
|
|
73
|
+
import agentContextCheckpointsSql from "./migrations/027-agent-context-checkpoints.sql" with {
|
|
74
|
+
type: "text",
|
|
75
|
+
}
|
|
76
|
+
import objectOverrideEditTimesSql from "./migrations/028-object-override-edit-times.sql" with {
|
|
77
|
+
type: "text",
|
|
78
|
+
}
|
|
72
79
|
|
|
73
80
|
const MIGRATIONS_TABLE_SQL = `
|
|
74
81
|
CREATE TABLE IF NOT EXISTS sixb_migrations (
|
|
@@ -149,6 +156,9 @@ export const sqliteStorageMigrations = defineMigrations({
|
|
|
149
156
|
{ checksum: checksum(ontologyCommitExecutionsSql) }
|
|
150
157
|
),
|
|
151
158
|
sqliteSql("025-connector-connections", connectorConnectionsSql),
|
|
159
|
+
sqliteSql("026-ai-cost-accounting", aiCostAccountingSql),
|
|
160
|
+
sqliteSql("027-agent-context-checkpoints", agentContextCheckpointsSql),
|
|
161
|
+
sqliteSql("028-object-override-edit-times", objectOverrideEditTimesSql),
|
|
152
162
|
],
|
|
153
163
|
})
|
|
154
164
|
|
|
@@ -98,6 +98,8 @@ function compileObjectQueryInternal(
|
|
|
98
98
|
switch (query.kind) {
|
|
99
99
|
case "start":
|
|
100
100
|
return compileStart(projectId, query)
|
|
101
|
+
case "refs":
|
|
102
|
+
return compileRefs(projectId, query)
|
|
101
103
|
case "filter":
|
|
102
104
|
return compileFilter(projectId, query.input, query.predicate)
|
|
103
105
|
case "sort":
|
|
@@ -162,6 +164,54 @@ function compileStart(
|
|
|
162
164
|
}
|
|
163
165
|
}
|
|
164
166
|
|
|
167
|
+
function compileRefs(
|
|
168
|
+
projectId: string,
|
|
169
|
+
query: Extract<ObjectQuery, { kind: "refs" }>
|
|
170
|
+
): CompiledObjectQuery {
|
|
171
|
+
if (query.refs.length === 0) {
|
|
172
|
+
throw new Error("[Sixb] SQLite object storage requires at least one ref")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const order = compileOrder(identityOrderFields())
|
|
176
|
+
const selectedOrder = compileOrder(identityOrderFields(), "selected")
|
|
177
|
+
const refsJson = JSON.stringify(query.refs)
|
|
178
|
+
const requested = `
|
|
179
|
+
SELECT DISTINCT
|
|
180
|
+
json_extract(ref.value, '$.objectTypeId') AS object_type_id,
|
|
181
|
+
json_extract(ref.value, '$.primaryId') AS primary_id
|
|
182
|
+
FROM json_each(?) AS ref
|
|
183
|
+
`
|
|
184
|
+
const sql = `
|
|
185
|
+
WITH requested AS (${requested})
|
|
186
|
+
SELECT selected.*, selected.properties AS _cursor_properties
|
|
187
|
+
FROM requested
|
|
188
|
+
JOIN objects AS selected
|
|
189
|
+
ON selected.project_id = ?
|
|
190
|
+
AND selected.object_type_id = requested.object_type_id
|
|
191
|
+
AND selected.primary_id = requested.primary_id
|
|
192
|
+
ORDER BY ${selectedOrder.sql}
|
|
193
|
+
`
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
sql,
|
|
197
|
+
args: [refsJson, projectId, ...selectedOrder.args],
|
|
198
|
+
totalSql: `
|
|
199
|
+
WITH requested AS (${requested})
|
|
200
|
+
SELECT COUNT(*) AS total
|
|
201
|
+
FROM requested
|
|
202
|
+
JOIN objects AS selected
|
|
203
|
+
ON selected.project_id = ?
|
|
204
|
+
AND selected.object_type_id = requested.object_type_id
|
|
205
|
+
AND selected.primary_id = requested.primary_id
|
|
206
|
+
`,
|
|
207
|
+
totalArgs: [refsJson, projectId],
|
|
208
|
+
order,
|
|
209
|
+
hasMore: () => false,
|
|
210
|
+
trimRows: identityRows,
|
|
211
|
+
nextPageToken: () => undefined,
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
165
215
|
function compileFilter(
|
|
166
216
|
projectId: string,
|
|
167
217
|
inputQuery: ObjectQuery,
|
package/src/object-storage.ts
CHANGED
|
@@ -9,16 +9,21 @@ import type {
|
|
|
9
9
|
ExpandedObjectRow,
|
|
10
10
|
FacetObjectsInput,
|
|
11
11
|
FacetObjectsResult,
|
|
12
|
+
LinkBatchKey,
|
|
12
13
|
LinkDirection,
|
|
14
|
+
ObjectBatchKey,
|
|
13
15
|
ObjectFacetRequest,
|
|
14
16
|
ObjectLinkRow,
|
|
15
17
|
ObjectQueryCapabilities,
|
|
16
18
|
ObjectRow,
|
|
17
19
|
ObjectRowLinks,
|
|
18
20
|
ObjectStorage,
|
|
21
|
+
QueryObjectLinksInput,
|
|
22
|
+
QueryObjectLinksResult,
|
|
19
23
|
QueryObjectsInput,
|
|
20
24
|
QueryObjectsResult,
|
|
21
25
|
} from "@sixb/core/storage"
|
|
26
|
+
import { linkBatchKey, objectBatchKey } from "@sixb/core/storage"
|
|
22
27
|
import { installFreshSqliteSchema } from "./migrations"
|
|
23
28
|
import { type CompiledObjectQuery, compileObjectQuery } from "./object-query-compiler"
|
|
24
29
|
import {
|
|
@@ -41,6 +46,7 @@ const SQLITE_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
|
|
|
41
46
|
facetObjects: true,
|
|
42
47
|
nodes: {
|
|
43
48
|
start: true,
|
|
49
|
+
refs: true,
|
|
44
50
|
filter: true,
|
|
45
51
|
text: true,
|
|
46
52
|
sort: true,
|
|
@@ -92,7 +98,7 @@ const SQLITE_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
|
|
|
92
98
|
stablePageTokens: true,
|
|
93
99
|
},
|
|
94
100
|
notes: [
|
|
95
|
-
"SQLite object query pushdown supports start/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
|
|
101
|
+
"SQLite object query pushdown supports start/refs/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
|
|
96
102
|
"expand hydrates linked objects in-database (top-N per parent via row_number() + json_group_array); core resolves each expansion's cardinality before pushdown, and a mixed/unresolved one stays on the fallback.",
|
|
97
103
|
"Ordered decimal predicates and sorting use the bounded core fallback because SQLite has no native exact decimal type; canonical decimal equality remains pushdown-safe.",
|
|
98
104
|
"Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases.",
|
|
@@ -219,22 +225,24 @@ export class SqliteObjectStorage implements ObjectStorage {
|
|
|
219
225
|
async getByPrimaryIdBatch(params: {
|
|
220
226
|
projectId: string
|
|
221
227
|
items: readonly { objectTypeId: string; primaryId: string }[]
|
|
222
|
-
}): Promise<Map<
|
|
223
|
-
const result = new Map<
|
|
228
|
+
}): Promise<Map<ObjectBatchKey, ObjectRow>> {
|
|
229
|
+
const result = new Map<ObjectBatchKey, ObjectRow>()
|
|
224
230
|
if (params.items.length === 0) return result
|
|
225
231
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
232
|
+
const rows = this.db
|
|
233
|
+
.query(
|
|
234
|
+
`
|
|
235
|
+
SELECT object.*
|
|
236
|
+
FROM json_each(?) AS requested
|
|
237
|
+
JOIN objects AS object
|
|
238
|
+
ON object.project_id = ?
|
|
239
|
+
AND object.object_type_id = json_extract(requested.value, '$.objectTypeId')
|
|
240
|
+
AND object.primary_id = json_extract(requested.value, '$.primaryId')
|
|
241
|
+
`
|
|
242
|
+
)
|
|
243
|
+
.all(JSON.stringify(params.items), params.projectId) as DatabaseRow[]
|
|
244
|
+
for (const row of rows) {
|
|
245
|
+
result.set(objectBatchKey(row.object_type_id, row.primary_id), this.rowToObject(row))
|
|
238
246
|
}
|
|
239
247
|
return result
|
|
240
248
|
}
|
|
@@ -242,8 +250,8 @@ export class SqliteObjectStorage implements ObjectStorage {
|
|
|
242
250
|
async listLinksBatch(params: {
|
|
243
251
|
projectId: string
|
|
244
252
|
items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
|
|
245
|
-
}): Promise<Map<
|
|
246
|
-
const result = new Map<
|
|
253
|
+
}): Promise<Map<LinkBatchKey, ObjectLinkRow[]>> {
|
|
254
|
+
const result = new Map<LinkBatchKey, ObjectLinkRow[]>()
|
|
247
255
|
if (params.items.length === 0) return result
|
|
248
256
|
|
|
249
257
|
const stmt = this.db.query(
|
|
@@ -258,7 +266,7 @@ export class SqliteObjectStorage implements ObjectStorage {
|
|
|
258
266
|
) as LinkDatabaseRow[]
|
|
259
267
|
if (rows.length > 0) {
|
|
260
268
|
result.set(
|
|
261
|
-
|
|
269
|
+
linkBatchKey(item.objectTypeId, item.objectId, item.linkId),
|
|
262
270
|
rows.map((r) => this.rowToLink(r))
|
|
263
271
|
)
|
|
264
272
|
}
|
|
@@ -266,26 +274,116 @@ export class SqliteObjectStorage implements ObjectStorage {
|
|
|
266
274
|
return result
|
|
267
275
|
}
|
|
268
276
|
|
|
277
|
+
async queryLinks(params: QueryObjectLinksInput): Promise<QueryObjectLinksResult> {
|
|
278
|
+
assertLinkQueryLimit(params.limit)
|
|
279
|
+
if (params.objectRefs.length === 0 || params.endpointObjectTypeIds?.length === 0) {
|
|
280
|
+
return { links: [], hasMore: false }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const sourceJoin = `
|
|
284
|
+
SELECT link.*
|
|
285
|
+
FROM links AS link
|
|
286
|
+
JOIN requested
|
|
287
|
+
ON requested.object_type_id = link.source_type_id
|
|
288
|
+
AND requested.object_id = link.source_id
|
|
289
|
+
WHERE link.project_id = ?
|
|
290
|
+
`
|
|
291
|
+
const targetJoin = `
|
|
292
|
+
SELECT link.*
|
|
293
|
+
FROM links AS link
|
|
294
|
+
JOIN requested
|
|
295
|
+
ON requested.object_type_id = link.target_type_id
|
|
296
|
+
AND requested.object_id = link.target_id
|
|
297
|
+
WHERE link.project_id = ?
|
|
298
|
+
`
|
|
299
|
+
const incidentSql =
|
|
300
|
+
params.direction === "outgoing"
|
|
301
|
+
? sourceJoin
|
|
302
|
+
: params.direction === "incoming"
|
|
303
|
+
? targetJoin
|
|
304
|
+
: `${sourceJoin} UNION ${targetJoin}`
|
|
305
|
+
const args: (string | number)[] = [JSON.stringify(params.objectRefs), params.projectId]
|
|
306
|
+
if (params.direction === "both") args.push(params.projectId)
|
|
307
|
+
|
|
308
|
+
const predicates: string[] = []
|
|
309
|
+
if (params.linkId !== undefined) {
|
|
310
|
+
predicates.push("link_id = ?")
|
|
311
|
+
args.push(params.linkId)
|
|
312
|
+
}
|
|
313
|
+
if (params.endpointObjectTypeIds !== undefined) {
|
|
314
|
+
const allowedTypes = JSON.stringify([...new Set(params.endpointObjectTypeIds)])
|
|
315
|
+
predicates.push(
|
|
316
|
+
"source_type_id IN (SELECT value FROM json_each(?))",
|
|
317
|
+
"target_type_id IN (SELECT value FROM json_each(?))"
|
|
318
|
+
)
|
|
319
|
+
args.push(allowedTypes, allowedTypes)
|
|
320
|
+
}
|
|
321
|
+
if (params.after) {
|
|
322
|
+
predicates.push(
|
|
323
|
+
"(source_type_id, source_id, link_id, target_type_id, target_id) > (?, ?, ?, ?, ?)"
|
|
324
|
+
)
|
|
325
|
+
args.push(...params.after)
|
|
326
|
+
}
|
|
327
|
+
args.push(params.limit + 1)
|
|
328
|
+
|
|
329
|
+
const whereSql = predicates.length > 0 ? `WHERE ${predicates.join(" AND ")}` : ""
|
|
330
|
+
const rows = this.db
|
|
331
|
+
.query(
|
|
332
|
+
`
|
|
333
|
+
WITH requested AS (
|
|
334
|
+
SELECT DISTINCT
|
|
335
|
+
json_extract(value, '$.objectTypeId') AS object_type_id,
|
|
336
|
+
json_extract(value, '$.primaryId') AS object_id
|
|
337
|
+
FROM json_each(?)
|
|
338
|
+
), incident AS (${incidentSql})
|
|
339
|
+
SELECT *
|
|
340
|
+
FROM incident
|
|
341
|
+
${whereSql}
|
|
342
|
+
ORDER BY source_type_id, source_id, link_id, target_type_id, target_id
|
|
343
|
+
LIMIT ?
|
|
344
|
+
`
|
|
345
|
+
)
|
|
346
|
+
.all(...args) as LinkDatabaseRow[]
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
links: rows.slice(0, params.limit).map((row) => this.rowToLink(row)),
|
|
350
|
+
hasMore: rows.length > params.limit,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
269
354
|
async listIncidentLinksBatch(params: {
|
|
270
355
|
projectId: string
|
|
271
356
|
items: readonly { objectTypeId: string; objectId: string }[]
|
|
272
357
|
}): Promise<readonly ObjectLinkRow[]> {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
358
|
+
if (params.items.length === 0) return []
|
|
359
|
+
|
|
360
|
+
const rows = this.db
|
|
361
|
+
.query(
|
|
362
|
+
`
|
|
363
|
+
WITH requested AS (
|
|
364
|
+
SELECT
|
|
365
|
+
json_extract(value, '$.objectTypeId') AS object_type_id,
|
|
366
|
+
json_extract(value, '$.objectId') AS object_id
|
|
367
|
+
FROM json_each(?)
|
|
368
|
+
)
|
|
369
|
+
SELECT link.*
|
|
370
|
+
FROM links AS link
|
|
371
|
+
JOIN requested
|
|
372
|
+
ON requested.object_type_id = link.source_type_id
|
|
373
|
+
AND requested.object_id = link.source_id
|
|
374
|
+
WHERE link.project_id = ?
|
|
375
|
+
UNION
|
|
376
|
+
SELECT link.*
|
|
377
|
+
FROM links AS link
|
|
378
|
+
JOIN requested
|
|
379
|
+
ON requested.object_type_id = link.target_type_id
|
|
380
|
+
AND requested.object_id = link.target_id
|
|
381
|
+
WHERE link.project_id = ?
|
|
382
|
+
`
|
|
383
|
+
)
|
|
384
|
+
.all(JSON.stringify(params.items), params.projectId, params.projectId) as LinkDatabaseRow[]
|
|
385
|
+
|
|
386
|
+
return rows.map((row) => this.rowToLink(row))
|
|
289
387
|
}
|
|
290
388
|
|
|
291
389
|
async listByPrimaryIdPage(params: {
|
|
@@ -457,6 +555,12 @@ function assertReconciliationPageLimit(limit: number): void {
|
|
|
457
555
|
}
|
|
458
556
|
}
|
|
459
557
|
|
|
558
|
+
function assertLinkQueryLimit(limit: number): void {
|
|
559
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
560
|
+
throw new Error("Object link query limit must be a positive safe integer.")
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
460
564
|
function readTotal(db: Database, compiled: CompiledObjectQuery): number {
|
|
461
565
|
const row = db.query(compiled.totalSql).get(...compiled.totalArgs) as { total: number }
|
|
462
566
|
return row.total
|
|
@@ -86,6 +86,7 @@ interface LinkScopeRow extends EffectiveLinkRow {
|
|
|
86
86
|
interface ObjectOverrideRow extends SqliteStoredOverrideRow {
|
|
87
87
|
readonly object_type_id: string
|
|
88
88
|
readonly primary_id: string
|
|
89
|
+
readonly edited_at: string
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
interface LinkOverrideRow extends SqliteStoredOverrideRow {
|
|
@@ -1028,6 +1029,7 @@ function storedObjectOverride(row: ObjectOverrideRow): StoredObjectOverride {
|
|
|
1028
1029
|
return {
|
|
1029
1030
|
ref,
|
|
1030
1031
|
value: parseJson<StoredObjectOverride["value"]>(row.value),
|
|
1032
|
+
editedAt: parseJson<Readonly<Record<string, string>>>(row.edited_at),
|
|
1031
1033
|
lastCommitId: row.last_commit_id,
|
|
1032
1034
|
updatedAt: row.updated_at,
|
|
1033
1035
|
}
|
|
@@ -41,14 +41,16 @@ export class SqliteMaterializationWriter {
|
|
|
41
41
|
this.db
|
|
42
42
|
.query(
|
|
43
43
|
`INSERT INTO ontology_object_overrides (
|
|
44
|
-
project_id, object_type_id, primary_id, value,
|
|
45
|
-
|
|
44
|
+
project_id, object_type_id, primary_id, value, edited_at,
|
|
45
|
+
last_commit_id, updated_at
|
|
46
|
+
) VALUES (?, ?, ?, json(?), json(?), ?, ?)`
|
|
46
47
|
)
|
|
47
48
|
.run(
|
|
48
49
|
projectId,
|
|
49
50
|
item.ref.objectTypeId,
|
|
50
51
|
item.ref.primaryId,
|
|
51
52
|
canonicalJson(item.value),
|
|
53
|
+
canonicalJson(item.editedAt),
|
|
52
54
|
item.lastCommitId,
|
|
53
55
|
item.updatedAt
|
|
54
56
|
)
|
|
@@ -64,12 +66,13 @@ export class SqliteMaterializationWriter {
|
|
|
64
66
|
this.db
|
|
65
67
|
.query(
|
|
66
68
|
`UPDATE ontology_object_overrides
|
|
67
|
-
SET value = json(?), last_commit_id = ?, updated_at = ?
|
|
69
|
+
SET value = json(?), edited_at = json(?), last_commit_id = ?, updated_at = ?
|
|
68
70
|
WHERE project_id = ? AND object_type_id = ? AND primary_id = ?
|
|
69
71
|
AND last_commit_id = ?`
|
|
70
72
|
)
|
|
71
73
|
.run(
|
|
72
74
|
canonicalJson(item.value),
|
|
75
|
+
canonicalJson(item.editedAt),
|
|
73
76
|
item.lastCommitId,
|
|
74
77
|
item.updatedAt,
|
|
75
78
|
projectId,
|