@sixb/sqlite 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/action-run-storage.d.ts.map +1 -1
- 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/ai-cost-storage.d.ts.map +1 -1
- package/dist/ai-usage-storage.d.ts.map +1 -1
- package/dist/execution-storage.d.ts +3 -2
- package/dist/execution-storage.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1567 -430
- package/dist/index.js.map +25 -19
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/index.d.ts +3 -0
- package/dist/objects/index.d.ts.map +1 -0
- package/dist/{object-query-compiler.d.ts → objects/query-compiler.d.ts} +16 -1
- 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 +51 -0
- package/dist/objects/reader.d.ts.map +1 -0
- package/dist/objects/rows.d.ts +33 -0
- package/dist/objects/rows.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +9 -0
- package/dist/objects/selected-reader.d.ts.map +1 -0
- package/dist/{object-storage.d.ts → objects/storage.d.ts} +12 -9
- package/dist/objects/storage.d.ts.map +1 -0
- package/dist/pipeline-run-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts +20 -0
- package/dist/share-grant-storage.d.ts.map +1 -0
- package/dist/share-session-storage.d.ts +19 -0
- package/dist/share-session-storage.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +1 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/dist/sync-run-storage.d.ts.map +1 -1
- package/dist/transactions.d.ts +8 -0
- package/dist/transactions.d.ts.map +1 -1
- package/dist/workflow-run-storage.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/action-run-storage.ts +6 -7
- package/src/agents/rows.ts +0 -2
- package/src/agents/runs.ts +3 -7
- package/src/ai-cost-groups.ts +3 -0
- package/src/ai-cost-storage.ts +11 -2
- package/src/ai-usage-storage.ts +1 -2
- package/src/execution-storage.ts +28 -7
- package/src/index.ts +22 -5
- package/src/migrations/035-share-grants.sql +66 -0
- package/src/migrations/036-share-sessions.sql +226 -0
- package/src/migrations/037-execution-requester-groups.sql +24 -0
- package/src/migrations.ts +9 -0
- package/src/objects/index.ts +2 -0
- package/src/{object-query-compiler.ts → objects/query-compiler.ts} +104 -54
- package/src/objects/read-scope.ts +316 -0
- package/src/objects/reader.ts +526 -0
- package/src/objects/rows.ts +123 -0
- package/src/objects/selected-reader.ts +119 -0
- package/src/objects/storage.ts +296 -0
- package/src/pipeline-run-storage.ts +2 -5
- package/src/share-grant-storage.ts +252 -0
- package/src/share-session-storage.ts +189 -0
- package/src/storage-errors.ts +4 -0
- package/src/sync-run-storage.ts +2 -5
- package/src/transactions.ts +32 -0
- package/src/workflow-run-storage.ts +1 -6
- package/dist/object-query-compiler.d.ts.map +0 -1
- package/dist/object-storage.d.ts.map +0 -1
- package/src/object-storage.ts +0 -709
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type {
|
|
3
|
+
CompiledSelectedObjectReadScope,
|
|
4
|
+
ObjectQueryCapabilities,
|
|
5
|
+
ObjectReadExecutionLimits,
|
|
6
|
+
ObjectReadStorage,
|
|
7
|
+
} from "@sixb/core/storage"
|
|
8
|
+
import {
|
|
9
|
+
assertObjectReaderProject,
|
|
10
|
+
assertObjectReadFacetCount,
|
|
11
|
+
assertObjectReadOutputWithinLimit,
|
|
12
|
+
ObjectReadLimitExceededError,
|
|
13
|
+
snapshotObjectReadExecutionLimits,
|
|
14
|
+
} from "@sixb/core/storage"
|
|
15
|
+
import { runDeferredReadTransaction } from "../transactions"
|
|
16
|
+
import {
|
|
17
|
+
compileSqliteSelectedObjectReadSource,
|
|
18
|
+
type SqliteSelectedObjectReadSource,
|
|
19
|
+
} from "./read-scope"
|
|
20
|
+
import { assertLinkQueryLimit, SqliteObjectReader } from "./reader"
|
|
21
|
+
|
|
22
|
+
/** Keep the traversal probe and every terminal statement in the same synchronous snapshot. */
|
|
23
|
+
export function createSqliteSelectedReader(
|
|
24
|
+
db: Database,
|
|
25
|
+
params: {
|
|
26
|
+
projectId: string
|
|
27
|
+
scope: CompiledSelectedObjectReadScope
|
|
28
|
+
limits: ObjectReadExecutionLimits
|
|
29
|
+
},
|
|
30
|
+
queryCapabilities: () => ObjectQueryCapabilities
|
|
31
|
+
): ObjectReadStorage {
|
|
32
|
+
const projectId = params.projectId
|
|
33
|
+
const limits = snapshotObjectReadExecutionLimits(params.limits)
|
|
34
|
+
const source = compileSqliteSelectedObjectReadSource(
|
|
35
|
+
projectId,
|
|
36
|
+
params.scope,
|
|
37
|
+
limits.maxTraversalFacts
|
|
38
|
+
)
|
|
39
|
+
const reader = new SqliteObjectReader(db, source)
|
|
40
|
+
const assertProject = (actualProjectId: string): void =>
|
|
41
|
+
assertObjectReaderProject(projectId, actualProjectId)
|
|
42
|
+
const read = <T>(run: () => T): T =>
|
|
43
|
+
runDeferredReadTransaction(db, () => {
|
|
44
|
+
assertTraversalBudget(db, source, limits.maxTraversalFacts)
|
|
45
|
+
const value = run()
|
|
46
|
+
assertObjectReadOutputWithinLimit(value, limits)
|
|
47
|
+
return value
|
|
48
|
+
})
|
|
49
|
+
const readMap = <TKey, TValue>(run: () => Map<TKey, TValue>): Map<TKey, TValue> =>
|
|
50
|
+
runDeferredReadTransaction(db, () => {
|
|
51
|
+
assertTraversalBudget(db, source, limits.maxTraversalFacts)
|
|
52
|
+
const value = run()
|
|
53
|
+
assertObjectReadOutputWithinLimit([...value.entries()], limits)
|
|
54
|
+
return value
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
queryCapabilities,
|
|
59
|
+
queryObjects: async (input) => {
|
|
60
|
+
assertProject(input.projectId)
|
|
61
|
+
return read(() => reader.queryObjects(input))
|
|
62
|
+
},
|
|
63
|
+
countObjects: async (input) => {
|
|
64
|
+
assertProject(input.projectId)
|
|
65
|
+
return read(() => reader.countObjects(input))
|
|
66
|
+
},
|
|
67
|
+
existsObjects: async (input) => {
|
|
68
|
+
assertProject(input.projectId)
|
|
69
|
+
return read(() => reader.existsObjects(input))
|
|
70
|
+
},
|
|
71
|
+
facetObjects: async (input) => {
|
|
72
|
+
assertProject(input.projectId)
|
|
73
|
+
assertObjectReadFacetCount(input.facets.length)
|
|
74
|
+
return read(() => reader.facetObjects(input))
|
|
75
|
+
},
|
|
76
|
+
getByPrimaryId: async (input) => {
|
|
77
|
+
assertProject(input.projectId)
|
|
78
|
+
return read(() => reader.getByPrimaryId(input))
|
|
79
|
+
},
|
|
80
|
+
selectsObjectProperties: async (input) => {
|
|
81
|
+
assertProject(input.projectId)
|
|
82
|
+
return read(() => reader.selectsObjectProperties(input))
|
|
83
|
+
},
|
|
84
|
+
listLinks: async (input) => {
|
|
85
|
+
assertProject(input.projectId)
|
|
86
|
+
return read(() => reader.listLinks(input))
|
|
87
|
+
},
|
|
88
|
+
getByPrimaryIdBatch: async (input) => {
|
|
89
|
+
assertProject(input.projectId)
|
|
90
|
+
return readMap(() => reader.getByPrimaryIdBatch(input))
|
|
91
|
+
},
|
|
92
|
+
listLinksBatch: async (input) => {
|
|
93
|
+
assertProject(input.projectId)
|
|
94
|
+
return readMap(() => reader.listLinksBatch(input))
|
|
95
|
+
},
|
|
96
|
+
queryLinks: async (input) => {
|
|
97
|
+
assertProject(input.projectId)
|
|
98
|
+
assertLinkQueryLimit(input.limit)
|
|
99
|
+
return read(() => reader.queryLinks(input))
|
|
100
|
+
},
|
|
101
|
+
list: async (input) => {
|
|
102
|
+
assertProject(input.projectId)
|
|
103
|
+
return read(() => reader.list(input))
|
|
104
|
+
},
|
|
105
|
+
} satisfies ObjectReadStorage)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function assertTraversalBudget(
|
|
109
|
+
db: Database,
|
|
110
|
+
source: SqliteSelectedObjectReadSource,
|
|
111
|
+
maxTraversalFacts: number
|
|
112
|
+
): void {
|
|
113
|
+
const row = db.query(source.traversalProbe.sql).get(...source.traversalProbe.args) as {
|
|
114
|
+
total: number | bigint
|
|
115
|
+
}
|
|
116
|
+
if (BigInt(row.total) > BigInt(maxTraversalFacts)) {
|
|
117
|
+
throw new ObjectReadLimitExceededError("traversalFacts", maxTraversalFacts)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type {
|
|
3
|
+
CompiledSelectedObjectReadScope,
|
|
4
|
+
CountObjectsInput,
|
|
5
|
+
CountObjectsResult,
|
|
6
|
+
ExistsObjectsInput,
|
|
7
|
+
ExistsObjectsResult,
|
|
8
|
+
FacetObjectsInput,
|
|
9
|
+
FacetObjectsResult,
|
|
10
|
+
LinkBatchKey,
|
|
11
|
+
LinkDirection,
|
|
12
|
+
ObjectBatchKey,
|
|
13
|
+
ObjectLinkRow,
|
|
14
|
+
ObjectQueryCapabilities,
|
|
15
|
+
ObjectReadExecutionLimits,
|
|
16
|
+
ObjectReadStorage,
|
|
17
|
+
ObjectRow,
|
|
18
|
+
ObjectStorage,
|
|
19
|
+
QueryObjectLinksInput,
|
|
20
|
+
QueryObjectLinksResult,
|
|
21
|
+
QueryObjectsInput,
|
|
22
|
+
QueryObjectsResult,
|
|
23
|
+
} from "@sixb/core/storage"
|
|
24
|
+
import { installFreshSqliteSchema } from "../migrations"
|
|
25
|
+
import {
|
|
26
|
+
closeSqliteStoreConnection,
|
|
27
|
+
openSqliteStoreConnection,
|
|
28
|
+
type SqliteStoreConnection,
|
|
29
|
+
} from "../transactions"
|
|
30
|
+
import { DEFAULT_OBJECT_QUERY_SOURCE } from "./query-compiler"
|
|
31
|
+
import { assertLinkQueryLimit, SqliteObjectReader } from "./reader"
|
|
32
|
+
import { type DatabaseRow, type LinkDatabaseRow, rowToLink, rowToObject } from "./rows"
|
|
33
|
+
import { createSqliteSelectedReader } from "./selected-reader"
|
|
34
|
+
|
|
35
|
+
export interface SqliteObjectStorageOptions {
|
|
36
|
+
/** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
|
|
37
|
+
path?: string
|
|
38
|
+
/** Internal shared connection used by bundled SqliteStorage. */
|
|
39
|
+
connection?: SqliteStoreConnection
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SQLITE_OBJECT_QUERY_CAPABILITIES: ObjectQueryCapabilities = {
|
|
43
|
+
queryObjects: true,
|
|
44
|
+
countObjects: true,
|
|
45
|
+
existsObjects: true,
|
|
46
|
+
facetObjects: true,
|
|
47
|
+
nodes: {
|
|
48
|
+
start: true,
|
|
49
|
+
refs: true,
|
|
50
|
+
filter: true,
|
|
51
|
+
text: true,
|
|
52
|
+
sort: true,
|
|
53
|
+
limit: true,
|
|
54
|
+
page: true,
|
|
55
|
+
traverse: true,
|
|
56
|
+
set: true,
|
|
57
|
+
project: true,
|
|
58
|
+
expand: true,
|
|
59
|
+
},
|
|
60
|
+
predicateOps: {
|
|
61
|
+
and: true,
|
|
62
|
+
or: true,
|
|
63
|
+
not: true,
|
|
64
|
+
eq: true,
|
|
65
|
+
neq: true,
|
|
66
|
+
lt: true,
|
|
67
|
+
lte: true,
|
|
68
|
+
gt: true,
|
|
69
|
+
gte: true,
|
|
70
|
+
in: true,
|
|
71
|
+
exists: true,
|
|
72
|
+
contains: true,
|
|
73
|
+
},
|
|
74
|
+
sortKinds: {
|
|
75
|
+
property: true,
|
|
76
|
+
},
|
|
77
|
+
traversalDirections: {
|
|
78
|
+
outgoing: true,
|
|
79
|
+
incoming: true,
|
|
80
|
+
},
|
|
81
|
+
setOps: {
|
|
82
|
+
union: true,
|
|
83
|
+
intersect: true,
|
|
84
|
+
subtract: true,
|
|
85
|
+
},
|
|
86
|
+
scalarOperations: {
|
|
87
|
+
string: { equality: true, ordering: true },
|
|
88
|
+
uuid: { equality: true, ordering: true },
|
|
89
|
+
boolean: { equality: true },
|
|
90
|
+
integer: { equality: true, ordering: true },
|
|
91
|
+
double: { equality: true, ordering: true },
|
|
92
|
+
decimal: { equality: true },
|
|
93
|
+
date: { equality: true, ordering: true },
|
|
94
|
+
timestamp: { equality: true, ordering: true },
|
|
95
|
+
},
|
|
96
|
+
limits: {
|
|
97
|
+
totalCount: true,
|
|
98
|
+
stablePageTokens: true,
|
|
99
|
+
},
|
|
100
|
+
notes: [
|
|
101
|
+
"SQLite object query pushdown supports start/refs/filter/text/sort/limit/page/traverse/set/project/expand over JSON properties and object links.",
|
|
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.",
|
|
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.",
|
|
104
|
+
"Relevance sorting, vector search, and unresolved start.includeSubtypes remain planner fallback or rejection cases.",
|
|
105
|
+
],
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* SQLite-based ObjectStorage implementation.
|
|
110
|
+
*
|
|
111
|
+
* Stores object projections and links. V1 object-query IR pushdown covers the
|
|
112
|
+
* scalar JSON-property and link-traversal subset declared by queryCapabilities().
|
|
113
|
+
*/
|
|
114
|
+
export class SqliteObjectStorage implements ObjectStorage {
|
|
115
|
+
private readonly connection: SqliteStoreConnection
|
|
116
|
+
private readonly db: Database
|
|
117
|
+
private readonly reader: SqliteObjectReader
|
|
118
|
+
|
|
119
|
+
constructor(options: SqliteObjectStorageOptions = {}) {
|
|
120
|
+
this.connection = openSqliteStoreConnection(options)
|
|
121
|
+
this.db = this.connection.db
|
|
122
|
+
this.reader = new SqliteObjectReader(this.db, DEFAULT_OBJECT_QUERY_SOURCE)
|
|
123
|
+
|
|
124
|
+
if (this.connection.installFreshSchema) {
|
|
125
|
+
installFreshSqliteSchema(this.db)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
queryCapabilities(): ObjectQueryCapabilities {
|
|
130
|
+
return SQLITE_OBJECT_QUERY_CAPABILITIES
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
createSelectedReadScope(params: {
|
|
134
|
+
projectId: string
|
|
135
|
+
scope: CompiledSelectedObjectReadScope
|
|
136
|
+
limits: ObjectReadExecutionLimits
|
|
137
|
+
}): ObjectReadStorage {
|
|
138
|
+
return createSqliteSelectedReader(this.db, params, () => this.queryCapabilities())
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async queryObjects(params: QueryObjectsInput): Promise<QueryObjectsResult> {
|
|
142
|
+
return this.reader.queryObjects(params)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async countObjects(params: CountObjectsInput): Promise<CountObjectsResult> {
|
|
146
|
+
return this.reader.countObjects(params)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async existsObjects(params: ExistsObjectsInput): Promise<ExistsObjectsResult> {
|
|
150
|
+
return this.reader.existsObjects(params)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async facetObjects(params: FacetObjectsInput): Promise<FacetObjectsResult> {
|
|
154
|
+
return this.reader.facetObjects(params)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async getByPrimaryId(params: {
|
|
158
|
+
projectId: string
|
|
159
|
+
objectTypeId: string
|
|
160
|
+
primaryId: string
|
|
161
|
+
}): Promise<ObjectRow | null> {
|
|
162
|
+
return this.reader.getByPrimaryId(params)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async selectsObjectProperties(
|
|
166
|
+
params: Parameters<ObjectReadStorage["selectsObjectProperties"]>[0]
|
|
167
|
+
): Promise<readonly boolean[]> {
|
|
168
|
+
return this.reader.selectsObjectProperties(params)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async listLinks(params: {
|
|
172
|
+
projectId: string
|
|
173
|
+
objectTypeId: string
|
|
174
|
+
objectId: string
|
|
175
|
+
linkId?: string
|
|
176
|
+
direction?: LinkDirection
|
|
177
|
+
}): Promise<readonly ObjectLinkRow[]> {
|
|
178
|
+
return this.reader.listLinks(params)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async getByPrimaryIdBatch(params: {
|
|
182
|
+
projectId: string
|
|
183
|
+
items: readonly { objectTypeId: string; primaryId: string }[]
|
|
184
|
+
}): Promise<Map<ObjectBatchKey, ObjectRow>> {
|
|
185
|
+
return this.reader.getByPrimaryIdBatch(params)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async listLinksBatch(params: {
|
|
189
|
+
projectId: string
|
|
190
|
+
direction?: LinkDirection
|
|
191
|
+
items: readonly { objectTypeId: string; objectId: string; linkId: string }[]
|
|
192
|
+
}): Promise<Map<LinkBatchKey, ObjectLinkRow[]>> {
|
|
193
|
+
return this.reader.listLinksBatch(params)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async queryLinks(params: QueryObjectLinksInput): Promise<QueryObjectLinksResult> {
|
|
197
|
+
assertLinkQueryLimit(params.limit)
|
|
198
|
+
return this.reader.queryLinks(params)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async listIncidentLinksBatch(params: {
|
|
202
|
+
projectId: string
|
|
203
|
+
items: readonly { objectTypeId: string; objectId: string }[]
|
|
204
|
+
}): Promise<readonly ObjectLinkRow[]> {
|
|
205
|
+
if (params.items.length === 0) return []
|
|
206
|
+
|
|
207
|
+
const rows = this.db
|
|
208
|
+
.query(
|
|
209
|
+
`
|
|
210
|
+
WITH requested AS (
|
|
211
|
+
SELECT
|
|
212
|
+
json_extract(value, '$.objectTypeId') AS object_type_id,
|
|
213
|
+
json_extract(value, '$.objectId') AS object_id
|
|
214
|
+
FROM json_each(?)
|
|
215
|
+
)
|
|
216
|
+
SELECT link.*
|
|
217
|
+
FROM links AS link
|
|
218
|
+
JOIN requested
|
|
219
|
+
ON requested.object_type_id = link.source_type_id
|
|
220
|
+
AND requested.object_id = link.source_id
|
|
221
|
+
WHERE link.project_id = ?
|
|
222
|
+
UNION
|
|
223
|
+
SELECT link.*
|
|
224
|
+
FROM links AS link
|
|
225
|
+
JOIN requested
|
|
226
|
+
ON requested.object_type_id = link.target_type_id
|
|
227
|
+
AND requested.object_id = link.target_id
|
|
228
|
+
WHERE link.project_id = ?
|
|
229
|
+
`
|
|
230
|
+
)
|
|
231
|
+
.all(JSON.stringify(params.items), params.projectId, params.projectId) as LinkDatabaseRow[]
|
|
232
|
+
|
|
233
|
+
return rows.map((row) => rowToLink(row))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async listByPrimaryIdPage(params: {
|
|
237
|
+
projectId: string
|
|
238
|
+
objectTypeId: string
|
|
239
|
+
afterPrimaryId?: string
|
|
240
|
+
limit: number
|
|
241
|
+
}): Promise<{ objects: readonly ObjectRow[]; nextPrimaryId?: string }> {
|
|
242
|
+
assertReconciliationPageLimit(params.limit)
|
|
243
|
+
const rows = this.db
|
|
244
|
+
.query(
|
|
245
|
+
`
|
|
246
|
+
SELECT * FROM objects
|
|
247
|
+
WHERE project_id = ? AND object_type_id = ?
|
|
248
|
+
AND (? IS NULL OR primary_id > ?)
|
|
249
|
+
ORDER BY primary_id ASC
|
|
250
|
+
LIMIT ?
|
|
251
|
+
`
|
|
252
|
+
)
|
|
253
|
+
.all(
|
|
254
|
+
params.projectId,
|
|
255
|
+
params.objectTypeId,
|
|
256
|
+
params.afterPrimaryId ?? null,
|
|
257
|
+
params.afterPrimaryId ?? null,
|
|
258
|
+
params.limit + 1
|
|
259
|
+
) as DatabaseRow[]
|
|
260
|
+
const hasMore = rows.length > params.limit
|
|
261
|
+
const objects = rows.slice(0, params.limit).map((row) => rowToObject(row))
|
|
262
|
+
const last = objects.at(-1)
|
|
263
|
+
return {
|
|
264
|
+
objects,
|
|
265
|
+
...(hasMore && last ? { nextPrimaryId: last.primaryId } : {}),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async list(params: {
|
|
270
|
+
projectId: string
|
|
271
|
+
objectTypeId?: string | readonly string[]
|
|
272
|
+
primaryIdPrefix?: string
|
|
273
|
+
primaryIdSuffix?: string
|
|
274
|
+
updatedAfter?: Date
|
|
275
|
+
updatedBefore?: Date
|
|
276
|
+
createdAfter?: Date
|
|
277
|
+
createdBefore?: Date
|
|
278
|
+
limit?: number
|
|
279
|
+
offset?: number
|
|
280
|
+
orderBy?: "createdAt" | "updatedAt" | "primaryId"
|
|
281
|
+
order?: "asc" | "desc"
|
|
282
|
+
}): Promise<{ objects: readonly ObjectRow[]; hasMore: boolean; total: number }> {
|
|
283
|
+
return this.reader.list(params)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** Close the connection only when this provider owns it. */
|
|
287
|
+
close(): void {
|
|
288
|
+
closeSqliteStoreConnection(this.connection)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function assertReconciliationPageLimit(limit: number): void {
|
|
293
|
+
if (!Number.isSafeInteger(limit) || limit <= 0) {
|
|
294
|
+
throw new Error("Object reconciliation page limit must be a positive safe integer.")
|
|
295
|
+
}
|
|
296
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
import { parseSixbFailure, serializeSixbFailure } from "@sixb/core/internal/errors"
|
|
3
3
|
import { assertPipelineRunExecution } from "@sixb/core/internal/pipeline-run-storage-provider"
|
|
4
|
+
import { canRequeuePipelineRunAfterEnqueueFailure } from "@sixb/core/internal/storage"
|
|
4
5
|
import type { DatasetVersionRef } from "@sixb/core/lake-storage"
|
|
5
6
|
import type {
|
|
6
7
|
ExecutionStorage,
|
|
@@ -19,11 +20,7 @@ import type {
|
|
|
19
20
|
StartPipelineRunInput,
|
|
20
21
|
StartPipelineStepRunInput,
|
|
21
22
|
} from "@sixb/core/storage"
|
|
22
|
-
import {
|
|
23
|
-
canRequeuePipelineRunAfterEnqueueFailure,
|
|
24
|
-
PIPELINE_RUN_FAILURE_CODES,
|
|
25
|
-
PipelineRunError,
|
|
26
|
-
} from "@sixb/core/storage"
|
|
23
|
+
import { PIPELINE_RUN_FAILURE_CODES, PipelineRunError } from "@sixb/core/storage"
|
|
27
24
|
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
28
25
|
import { installFreshSqliteSchema } from "./migrations"
|
|
29
26
|
import {
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import {
|
|
3
|
+
normalizeGetShareGrantByIdInput,
|
|
4
|
+
normalizeListShareGrantsInput,
|
|
5
|
+
normalizeRevokeShareGrantInput,
|
|
6
|
+
normalizeShareGrantCreate,
|
|
7
|
+
parseShareGrantRecord,
|
|
8
|
+
} from "@sixb/core/internal/share-grant-storage-provider"
|
|
9
|
+
import type {
|
|
10
|
+
CreateShareGrantInput,
|
|
11
|
+
GetShareGrantByIdInput,
|
|
12
|
+
ListShareGrantsInput,
|
|
13
|
+
ListShareGrantsResult,
|
|
14
|
+
RevokeShareGrantInput,
|
|
15
|
+
ShareGrantRecord,
|
|
16
|
+
ShareGrantStorage,
|
|
17
|
+
} from "@sixb/core/storage"
|
|
18
|
+
import { ShareGrantStorageError } from "@sixb/core/storage"
|
|
19
|
+
import { installFreshSqliteSchema } from "./migrations"
|
|
20
|
+
import { isUniqueConstraintError } from "./storage-errors"
|
|
21
|
+
import {
|
|
22
|
+
closeSqliteStoreConnection,
|
|
23
|
+
openSqliteStoreConnection,
|
|
24
|
+
type SqliteStoreConnection,
|
|
25
|
+
} from "./transactions"
|
|
26
|
+
|
|
27
|
+
export interface SqliteShareGrantStorageOptions {
|
|
28
|
+
readonly path?: string
|
|
29
|
+
readonly connection?: SqliteStoreConnection
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** SQLite persistence for issued Share grants. Secret handling remains in Core. */
|
|
33
|
+
export class SqliteShareGrantStorage implements ShareGrantStorage {
|
|
34
|
+
private readonly connection: SqliteStoreConnection
|
|
35
|
+
private readonly db: Database
|
|
36
|
+
|
|
37
|
+
constructor(options: SqliteShareGrantStorageOptions = {}) {
|
|
38
|
+
this.connection = openSqliteStoreConnection(options)
|
|
39
|
+
this.db = this.connection.db
|
|
40
|
+
if (this.connection.installFreshSchema) installFreshSqliteSchema(this.db)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async create(input: CreateShareGrantInput): Promise<ShareGrantRecord> {
|
|
44
|
+
const record = normalizeShareGrantCreate(input)
|
|
45
|
+
try {
|
|
46
|
+
this.db
|
|
47
|
+
.query(
|
|
48
|
+
`
|
|
49
|
+
INSERT INTO share_grants (
|
|
50
|
+
project_id, id, definition_id, target_object_type_id, target_primary_id,
|
|
51
|
+
issued_by_type, issued_by_id, authority_version, authority_snapshot,
|
|
52
|
+
authority_digest, token_hash, destination_path, created_at, expires_at
|
|
53
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
54
|
+
`
|
|
55
|
+
)
|
|
56
|
+
.run(
|
|
57
|
+
record.projectId,
|
|
58
|
+
record.id,
|
|
59
|
+
record.definitionId,
|
|
60
|
+
record.target.objectTypeId,
|
|
61
|
+
record.target.primaryId,
|
|
62
|
+
record.issuedBy.type,
|
|
63
|
+
record.issuedBy.id,
|
|
64
|
+
record.authoritySnapshot.version,
|
|
65
|
+
JSON.stringify(record.authoritySnapshot),
|
|
66
|
+
record.authorityDigest,
|
|
67
|
+
record.tokenHash,
|
|
68
|
+
record.destinationPath,
|
|
69
|
+
record.createdAt.toISOString(),
|
|
70
|
+
record.expiresAt.toISOString()
|
|
71
|
+
)
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (isUniqueConstraintError(error)) {
|
|
74
|
+
throw new ShareGrantStorageError(
|
|
75
|
+
"duplicate",
|
|
76
|
+
`[SixbSqlite] Share grant '${record.id}' conflicts with an existing record.`,
|
|
77
|
+
{ cause: error }
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
throw error
|
|
81
|
+
}
|
|
82
|
+
return this.require(record.projectId, record.id, "create")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async getById(input: GetShareGrantByIdInput): Promise<ShareGrantRecord | null> {
|
|
86
|
+
const normalized = normalizeGetShareGrantByIdInput(input)
|
|
87
|
+
return this.getRow(normalized.projectId, normalized.id)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async list(input: ListShareGrantsInput): Promise<ListShareGrantsResult> {
|
|
91
|
+
const normalized = normalizeListShareGrantsInput(input)
|
|
92
|
+
const where = ["project_id = ?"]
|
|
93
|
+
const args: (string | number)[] = [normalized.projectId]
|
|
94
|
+
if (normalized.definitionId !== undefined) {
|
|
95
|
+
where.push("definition_id = ?")
|
|
96
|
+
args.push(normalized.definitionId)
|
|
97
|
+
}
|
|
98
|
+
if (normalized.target !== undefined) {
|
|
99
|
+
where.push("target_object_type_id = ?", "target_primary_id = ?")
|
|
100
|
+
args.push(normalized.target.objectTypeId, normalized.target.primaryId)
|
|
101
|
+
}
|
|
102
|
+
if (!normalized.includeRevoked) where.push("revoked_at IS NULL")
|
|
103
|
+
if (!normalized.includeExpired) {
|
|
104
|
+
where.push("expires_at > ?")
|
|
105
|
+
args.push(normalized.now.toISOString())
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const predicate = where.join(" AND ")
|
|
109
|
+
const totalRow = this.db
|
|
110
|
+
.query(`SELECT COUNT(*) AS total FROM share_grants WHERE ${predicate}`)
|
|
111
|
+
.get(...args) as { readonly total: number }
|
|
112
|
+
const rows = this.db
|
|
113
|
+
.query(
|
|
114
|
+
`
|
|
115
|
+
SELECT * FROM share_grants
|
|
116
|
+
WHERE ${predicate}
|
|
117
|
+
ORDER BY created_at DESC, id DESC
|
|
118
|
+
LIMIT ? OFFSET ?
|
|
119
|
+
`
|
|
120
|
+
)
|
|
121
|
+
.all(...args, normalized.limit, normalized.offset) as SqliteShareGrantRow[]
|
|
122
|
+
const grants = rows.map(rowToRecord)
|
|
123
|
+
return {
|
|
124
|
+
grants,
|
|
125
|
+
total: totalRow.total,
|
|
126
|
+
hasMore: normalized.offset + grants.length < totalRow.total,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async revoke(input: RevokeShareGrantInput): Promise<ShareGrantRecord | null> {
|
|
131
|
+
const normalized = normalizeRevokeShareGrantInput(input)
|
|
132
|
+
const current = this.getRow(normalized.projectId, normalized.id)
|
|
133
|
+
if (!current || current.revokedAt) return current
|
|
134
|
+
|
|
135
|
+
const revocation = normalizeRevokeShareGrantInput(normalized, current.createdAt)
|
|
136
|
+
this.db
|
|
137
|
+
.query(
|
|
138
|
+
`
|
|
139
|
+
UPDATE share_grants
|
|
140
|
+
SET revoked_at = ?, revoked_by_type = ?, revoked_by_id = ?
|
|
141
|
+
WHERE project_id = ? AND id = ? AND revoked_at IS NULL
|
|
142
|
+
`
|
|
143
|
+
)
|
|
144
|
+
.run(
|
|
145
|
+
revocation.revokedAt.toISOString(),
|
|
146
|
+
revocation.revokedBy.type,
|
|
147
|
+
revocation.revokedBy.id,
|
|
148
|
+
revocation.projectId,
|
|
149
|
+
revocation.id
|
|
150
|
+
)
|
|
151
|
+
return this.require(revocation.projectId, revocation.id, "revoke")
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
close(): void {
|
|
155
|
+
closeSqliteStoreConnection(this.connection)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private getRow(projectId: string, id: string): ShareGrantRecord | null {
|
|
159
|
+
const row = this.db
|
|
160
|
+
.query("SELECT * FROM share_grants WHERE project_id = ? AND id = ?")
|
|
161
|
+
.get(projectId, id) as SqliteShareGrantRow | null
|
|
162
|
+
return row ? rowToRecord(row) : null
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private require(projectId: string, id: string, operation: string): ShareGrantRecord {
|
|
166
|
+
const record = this.getRow(projectId, id)
|
|
167
|
+
if (!record) {
|
|
168
|
+
throw new ShareGrantStorageError(
|
|
169
|
+
"invalid_record",
|
|
170
|
+
`[SixbSqlite] Share grant '${id}' disappeared after ${operation}.`
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
return record
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface SqliteShareGrantRow {
|
|
178
|
+
readonly project_id: unknown
|
|
179
|
+
readonly id: unknown
|
|
180
|
+
readonly definition_id: unknown
|
|
181
|
+
readonly target_object_type_id: unknown
|
|
182
|
+
readonly target_primary_id: unknown
|
|
183
|
+
readonly issued_by_type: unknown
|
|
184
|
+
readonly issued_by_id: unknown
|
|
185
|
+
readonly authority_version: unknown
|
|
186
|
+
readonly authority_snapshot: unknown
|
|
187
|
+
readonly authority_digest: unknown
|
|
188
|
+
readonly token_hash: unknown
|
|
189
|
+
readonly destination_path: unknown
|
|
190
|
+
readonly created_at: unknown
|
|
191
|
+
readonly expires_at: unknown
|
|
192
|
+
readonly revoked_at: unknown
|
|
193
|
+
readonly revoked_by_type: unknown
|
|
194
|
+
readonly revoked_by_id: unknown
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function rowToRecord(row: SqliteShareGrantRow): ShareGrantRecord {
|
|
198
|
+
const authoritySnapshot = parseStoredJson(row.authority_snapshot, "authority snapshot")
|
|
199
|
+
if (
|
|
200
|
+
typeof row.authority_version !== "number" ||
|
|
201
|
+
!isRecord(authoritySnapshot) ||
|
|
202
|
+
authoritySnapshot.version !== row.authority_version
|
|
203
|
+
) {
|
|
204
|
+
throw invalidRecord("Share authority version does not match its snapshot.")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const hasRevokedAt = row.revoked_at !== null
|
|
208
|
+
const hasRevokedBy = row.revoked_by_type !== null || row.revoked_by_id !== null
|
|
209
|
+
if (hasRevokedAt !== hasRevokedBy) {
|
|
210
|
+
throw invalidRecord("Share revocation time and actor must be stored together.")
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return parseShareGrantRecord({
|
|
214
|
+
projectId: row.project_id,
|
|
215
|
+
id: row.id,
|
|
216
|
+
definitionId: row.definition_id,
|
|
217
|
+
target: {
|
|
218
|
+
objectTypeId: row.target_object_type_id,
|
|
219
|
+
primaryId: row.target_primary_id,
|
|
220
|
+
},
|
|
221
|
+
issuedBy: { type: row.issued_by_type, id: row.issued_by_id },
|
|
222
|
+
authoritySnapshot,
|
|
223
|
+
authorityDigest: row.authority_digest,
|
|
224
|
+
tokenHash: row.token_hash,
|
|
225
|
+
destinationPath: row.destination_path,
|
|
226
|
+
createdAt: row.created_at,
|
|
227
|
+
expiresAt: row.expires_at,
|
|
228
|
+
...(hasRevokedAt ? { revokedAt: row.revoked_at } : {}),
|
|
229
|
+
...(hasRevokedBy ? { revokedBy: { type: row.revoked_by_type, id: row.revoked_by_id } } : {}),
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseStoredJson(value: unknown, field: string): unknown {
|
|
234
|
+
if (typeof value !== "string") throw invalidRecord(`Share ${field} must be stored as JSON text.`)
|
|
235
|
+
try {
|
|
236
|
+
return JSON.parse(value)
|
|
237
|
+
} catch (cause) {
|
|
238
|
+
throw new ShareGrantStorageError(
|
|
239
|
+
"invalid_record",
|
|
240
|
+
`[SixbSqlite] Share ${field} contains invalid JSON.`,
|
|
241
|
+
{ cause }
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
247
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function invalidRecord(message: string): ShareGrantStorageError {
|
|
251
|
+
return new ShareGrantStorageError("invalid_record", `[SixbSqlite] ${message}`)
|
|
252
|
+
}
|