@sixb/sqlite 0.0.1
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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/action-run-storage.d.ts +29 -0
- package/dist/action-run-storage.d.ts.map +1 -0
- package/dist/agents/agent-storage.d.ts +25 -0
- package/dist/agents/agent-storage.d.ts.map +1 -0
- package/dist/agents/index.d.ts +6 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/messages.d.ts +14 -0
- package/dist/agents/messages.d.ts.map +1 -0
- package/dist/agents/rows.d.ts +77 -0
- package/dist/agents/rows.d.ts.map +1 -0
- package/dist/agents/runs.d.ts +26 -0
- package/dist/agents/runs.d.ts.map +1 -0
- package/dist/agents/threads.d.ts +14 -0
- package/dist/agents/threads.d.ts.map +1 -0
- package/dist/auth-storage/access-tokens.d.ts +33 -0
- package/dist/auth-storage/access-tokens.d.ts.map +1 -0
- package/dist/auth-storage/auth-storage.d.ts +46 -0
- package/dist/auth-storage/auth-storage.d.ts.map +1 -0
- package/dist/auth-storage/group-memberships.d.ts +21 -0
- package/dist/auth-storage/group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/identities.d.ts +17 -0
- package/dist/auth-storage/identities.d.ts.map +1 -0
- package/dist/auth-storage/index.d.ts +3 -0
- package/dist/auth-storage/index.d.ts.map +1 -0
- package/dist/auth-storage/invitations.d.ts +28 -0
- package/dist/auth-storage/invitations.d.ts.map +1 -0
- package/dist/auth-storage/magic-links.d.ts +28 -0
- package/dist/auth-storage/magic-links.d.ts.map +1 -0
- package/dist/auth-storage/oidc-attempts.d.ts +18 -0
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +134 -0
- package/dist/auth-storage/rows.d.ts.map +1 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
- package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
- package/dist/auth-storage/service-accounts.d.ts +14 -0
- package/dist/auth-storage/service-accounts.d.ts.map +1 -0
- package/dist/auth-storage/sessions.d.ts +55 -0
- package/dist/auth-storage/sessions.d.ts.map +1 -0
- package/dist/auth-storage/shared.d.ts +131 -0
- package/dist/auth-storage/shared.d.ts.map +1 -0
- package/dist/auth-storage/users.d.ts +19 -0
- package/dist/auth-storage/users.d.ts.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10204 -0
- package/dist/index.js.map +54 -0
- package/dist/latest-run-query.d.ts +22 -0
- package/dist/latest-run-query.d.ts.map +1 -0
- package/dist/migrations.d.ts +18 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/object-query-compiler.d.ts +44 -0
- package/dist/object-query-compiler.d.ts.map +1 -0
- package/dist/object-storage.d.ts +93 -0
- package/dist/object-storage.d.ts.map +1 -0
- package/dist/ontology-storage/commits.d.ts +14 -0
- package/dist/ontology-storage/commits.d.ts.map +1 -0
- package/dist/ontology-storage/index.d.ts +22 -0
- package/dist/ontology-storage/index.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-session.d.ts +49 -0
- package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-state.d.ts +63 -0
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
- package/dist/ontology-storage/materialization-writer.d.ts +15 -0
- package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
- package/dist/ontology-storage/materializations.d.ts +32 -0
- package/dist/ontology-storage/materializations.d.ts.map +1 -0
- package/dist/ontology-storage/outbox.d.ts +14 -0
- package/dist/ontology-storage/outbox.d.ts.map +1 -0
- package/dist/ontology-storage/shared.d.ts +91 -0
- package/dist/ontology-storage/shared.d.ts.map +1 -0
- package/dist/ontology-storage/sources.d.ts +26 -0
- package/dist/ontology-storage/sources.d.ts.map +1 -0
- package/dist/pipeline-run-storage.d.ts +26 -0
- package/dist/pipeline-run-storage.d.ts.map +1 -0
- package/dist/projection-run-storage.d.ts +32 -0
- package/dist/projection-run-storage.d.ts.map +1 -0
- package/dist/rules-storage.d.ts +33 -0
- package/dist/rules-storage.d.ts.map +1 -0
- package/dist/run-list-query.d.ts +27 -0
- package/dist/run-list-query.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +2 -0
- package/dist/storage-errors.d.ts.map +1 -0
- package/dist/sync-run-storage.d.ts +23 -0
- package/dist/sync-run-storage.d.ts.map +1 -0
- package/dist/timeseries-storage.d.ts +41 -0
- package/dist/timeseries-storage.d.ts.map +1 -0
- package/dist/transactions.d.ts +15 -0
- package/dist/transactions.d.ts.map +1 -0
- package/dist/tsconfig.json +5 -0
- package/dist/validation.d.ts +5 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/webhook-delivery-storage.d.ts +27 -0
- package/dist/webhook-delivery-storage.d.ts.map +1 -0
- package/dist/webhook-run-storage.d.ts +22 -0
- package/dist/webhook-run-storage.d.ts.map +1 -0
- package/dist/workflow-intervention-storage.d.ts +26 -0
- package/dist/workflow-intervention-storage.d.ts.map +1 -0
- package/dist/workflow-run-storage.d.ts +63 -0
- package/dist/workflow-run-storage.d.ts.map +1 -0
- package/package.json +58 -0
- package/src/action-run-storage.ts +761 -0
- package/src/agents/agent-storage.ts +46 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/messages.ts +162 -0
- package/src/agents/rows.ts +205 -0
- package/src/agents/runs.ts +389 -0
- package/src/agents/threads.ts +133 -0
- package/src/auth-storage/access-tokens.ts +289 -0
- package/src/auth-storage/auth-storage.ts +784 -0
- package/src/auth-storage/group-memberships.ts +52 -0
- package/src/auth-storage/identities.ts +98 -0
- package/src/auth-storage/index.ts +2 -0
- package/src/auth-storage/invitations.ts +258 -0
- package/src/auth-storage/magic-links.ts +149 -0
- package/src/auth-storage/oidc-attempts.ts +109 -0
- package/src/auth-storage/rows.ts +338 -0
- package/src/auth-storage/service-account-group-memberships.ts +161 -0
- package/src/auth-storage/service-accounts.ts +168 -0
- package/src/auth-storage/sessions.ts +230 -0
- package/src/auth-storage/shared.ts +707 -0
- package/src/auth-storage/users.ts +226 -0
- package/src/index.ts +292 -0
- package/src/latest-run-query.ts +49 -0
- package/src/migrations/001-initial-schema.sql +1068 -0
- package/src/migrations.ts +245 -0
- package/src/object-query-compiler.ts +1198 -0
- package/src/object-storage.ts +605 -0
- package/src/ontology-storage/commits.ts +127 -0
- package/src/ontology-storage/index.ts +35 -0
- package/src/ontology-storage/materialization-session.ts +393 -0
- package/src/ontology-storage/materialization-state.ts +901 -0
- package/src/ontology-storage/materialization-writer.ts +394 -0
- package/src/ontology-storage/materializations.ts +838 -0
- package/src/ontology-storage/outbox.ts +237 -0
- package/src/ontology-storage/shared.ts +269 -0
- package/src/ontology-storage/sources.ts +653 -0
- package/src/pipeline-run-storage.ts +506 -0
- package/src/projection-run-storage.ts +592 -0
- package/src/rules-storage.ts +375 -0
- package/src/run-list-query.ts +98 -0
- package/src/sql.d.ts +4 -0
- package/src/storage-errors.ts +3 -0
- package/src/sync-run-storage.ts +299 -0
- package/src/timeseries-storage.ts +212 -0
- package/src/transactions.ts +96 -0
- package/src/validation.ts +30 -0
- package/src/webhook-delivery-storage.ts +272 -0
- package/src/webhook-run-storage.ts +240 -0
- package/src/workflow-intervention-storage.ts +398 -0
- package/src/workflow-run-storage.ts +1148 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type { JsonValue } from "@sixb/core"
|
|
3
|
+
import type {
|
|
4
|
+
FinishSyncRunInput,
|
|
5
|
+
ListLatestSyncRunsInput,
|
|
6
|
+
ListLatestSyncRunsResult,
|
|
7
|
+
ListSyncRunsInput,
|
|
8
|
+
ListSyncRunsResult,
|
|
9
|
+
StartSyncRunInput,
|
|
10
|
+
SyncRunFailure,
|
|
11
|
+
SyncRunRecord,
|
|
12
|
+
SyncRunStorage,
|
|
13
|
+
} from "@sixb/core/storage"
|
|
14
|
+
import { SyncRunError } from "@sixb/core/storage"
|
|
15
|
+
import { queryLatestRunsByOwnerId } from "./latest-run-query"
|
|
16
|
+
import { installFreshSqliteSchema } from "./migrations"
|
|
17
|
+
import {
|
|
18
|
+
appendRunListFilters,
|
|
19
|
+
hasEmptyStatuses,
|
|
20
|
+
queryRunList,
|
|
21
|
+
type SqliteValue,
|
|
22
|
+
} from "./run-list-query"
|
|
23
|
+
import {
|
|
24
|
+
closeSqliteStoreConnection,
|
|
25
|
+
openSqliteStoreConnection,
|
|
26
|
+
type SqliteStoreConnection,
|
|
27
|
+
} from "./transactions"
|
|
28
|
+
|
|
29
|
+
export interface SqliteSyncRunStorageOptions {
|
|
30
|
+
/** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
|
|
31
|
+
path?: string
|
|
32
|
+
/** Internal shared connection used by bundled SqliteStorage. */
|
|
33
|
+
connection?: SqliteStoreConnection
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class SqliteSyncRunStorage implements SyncRunStorage {
|
|
37
|
+
private readonly connection: SqliteStoreConnection
|
|
38
|
+
private readonly db: Database
|
|
39
|
+
|
|
40
|
+
constructor(options: SqliteSyncRunStorageOptions = {}) {
|
|
41
|
+
this.connection = openSqliteStoreConnection(options)
|
|
42
|
+
this.db = this.connection.db
|
|
43
|
+
|
|
44
|
+
if (this.connection.installFreshSchema) {
|
|
45
|
+
installFreshSqliteSchema(this.db)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async start(input: StartSyncRunInput): Promise<SyncRunRecord> {
|
|
50
|
+
const startedAt = input.startedAt ?? new Date()
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
this.db
|
|
54
|
+
.query(
|
|
55
|
+
`
|
|
56
|
+
INSERT INTO sync_runs (
|
|
57
|
+
project_id,
|
|
58
|
+
id,
|
|
59
|
+
sync_id,
|
|
60
|
+
dataset_id,
|
|
61
|
+
mode,
|
|
62
|
+
status,
|
|
63
|
+
started_at,
|
|
64
|
+
expected_latest_version_id,
|
|
65
|
+
commit_message
|
|
66
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
67
|
+
`
|
|
68
|
+
)
|
|
69
|
+
.run(
|
|
70
|
+
input.projectId,
|
|
71
|
+
input.id,
|
|
72
|
+
input.syncId,
|
|
73
|
+
input.datasetId,
|
|
74
|
+
input.mode,
|
|
75
|
+
"running",
|
|
76
|
+
startedAt.toISOString(),
|
|
77
|
+
input.expectedLatestVersionId ?? null,
|
|
78
|
+
input.commitMessage ?? null
|
|
79
|
+
)
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (isUniqueConstraintError(error)) {
|
|
82
|
+
throw new SyncRunError(
|
|
83
|
+
`[SixbSqlite] Sync run '${input.id}' already exists for project '${input.projectId}'.`
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw error
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const record = await this.getById({ projectId: input.projectId, id: input.id })
|
|
91
|
+
if (!record) {
|
|
92
|
+
throw new SyncRunError(
|
|
93
|
+
`[SixbSqlite] Failed to load sync run '${input.id}' for project '${input.projectId}'.`
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return record
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async finish(input: FinishSyncRunInput): Promise<SyncRunRecord> {
|
|
101
|
+
return this.db.transaction(() => {
|
|
102
|
+
const existing = this.db
|
|
103
|
+
.query("SELECT * FROM sync_runs WHERE project_id = ? AND id = ?")
|
|
104
|
+
.get(input.projectId, input.id) as DatabaseRow | null
|
|
105
|
+
|
|
106
|
+
if (!existing) {
|
|
107
|
+
throw new SyncRunError(
|
|
108
|
+
`[SixbSqlite] Sync run '${input.id}' not found for project '${input.projectId}'.`
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (input.status === "succeeded") {
|
|
113
|
+
if (input.output && input.output.datasetId !== existing.dataset_id) {
|
|
114
|
+
throw new SyncRunError(
|
|
115
|
+
`[SixbSqlite] Sync run '${input.id}' output dataset '${input.output.datasetId}' does not match '${existing.dataset_id}'.`
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
if (!input.output && (existing.mode !== "append" || input.rowsRead !== 0)) {
|
|
119
|
+
throw new SyncRunError(
|
|
120
|
+
`[SixbSqlite] Sync run '${input.id}' may omit its output only for an empty append.`
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.db
|
|
126
|
+
.query(
|
|
127
|
+
`
|
|
128
|
+
UPDATE sync_runs
|
|
129
|
+
SET
|
|
130
|
+
status = ?,
|
|
131
|
+
finished_at = ?,
|
|
132
|
+
rows_read = ?,
|
|
133
|
+
output_version_id = ?,
|
|
134
|
+
error_name = ?,
|
|
135
|
+
error_message = ?,
|
|
136
|
+
checkpoint = ?
|
|
137
|
+
WHERE project_id = ? AND id = ?
|
|
138
|
+
`
|
|
139
|
+
)
|
|
140
|
+
.run(
|
|
141
|
+
input.status,
|
|
142
|
+
(input.finishedAt ?? new Date()).toISOString(),
|
|
143
|
+
input.status === "succeeded"
|
|
144
|
+
? input.rowsRead
|
|
145
|
+
: (input.rowsRead ?? existing.rows_read ?? null),
|
|
146
|
+
input.status === "succeeded" ? (input.output?.versionId ?? null) : null,
|
|
147
|
+
input.status === "succeeded" ? null : (input.error?.name ?? null),
|
|
148
|
+
input.status === "succeeded" ? null : (input.error?.message ?? null),
|
|
149
|
+
input.status === "succeeded" ? serializeCheckpoint(input.checkpoint) : null,
|
|
150
|
+
input.projectId,
|
|
151
|
+
input.id
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
const updated = this.db
|
|
155
|
+
.query("SELECT * FROM sync_runs WHERE project_id = ? AND id = ?")
|
|
156
|
+
.get(input.projectId, input.id) as DatabaseRow
|
|
157
|
+
|
|
158
|
+
return rowToSyncRunRecord(updated)
|
|
159
|
+
})()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async getById(params: { projectId: string; id: string }): Promise<SyncRunRecord | null> {
|
|
163
|
+
const row = this.db
|
|
164
|
+
.query("SELECT * FROM sync_runs WHERE project_id = ? AND id = ?")
|
|
165
|
+
.get(params.projectId, params.id) as DatabaseRow | null
|
|
166
|
+
|
|
167
|
+
return row ? rowToSyncRunRecord(row) : null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async list(input: ListSyncRunsInput): Promise<ListSyncRunsResult> {
|
|
171
|
+
if (hasEmptyStatuses(input) || input.syncIds?.length === 0) {
|
|
172
|
+
return {
|
|
173
|
+
runs: [],
|
|
174
|
+
hasMore: false,
|
|
175
|
+
total: 0,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const whereClauses = ["project_id = ?"]
|
|
180
|
+
const args: SqliteValue[] = [input.projectId]
|
|
181
|
+
|
|
182
|
+
if (input.syncId) {
|
|
183
|
+
whereClauses.push("sync_id = ?")
|
|
184
|
+
args.push(input.syncId)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (input.syncIds) {
|
|
188
|
+
whereClauses.push(`sync_id IN (${input.syncIds.map(() => "?").join(", ")})`)
|
|
189
|
+
args.push(...input.syncIds)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (input.datasetId) {
|
|
193
|
+
whereClauses.push("dataset_id = ?")
|
|
194
|
+
args.push(input.datasetId)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
appendRunListFilters(whereClauses, args, input)
|
|
198
|
+
|
|
199
|
+
const { rows, total, hasMore } = queryRunList<DatabaseRow>({
|
|
200
|
+
db: this.db,
|
|
201
|
+
tableName: "sync_runs",
|
|
202
|
+
whereClauses,
|
|
203
|
+
args,
|
|
204
|
+
order: input.order,
|
|
205
|
+
limit: input.limit,
|
|
206
|
+
offset: input.offset,
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
runs: rows.map(rowToSyncRunRecord),
|
|
211
|
+
hasMore,
|
|
212
|
+
total,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async listLatestBySyncIds(input: ListLatestSyncRunsInput): Promise<ListLatestSyncRunsResult> {
|
|
217
|
+
const rows = queryLatestRunsByOwnerId<DatabaseRow>({
|
|
218
|
+
db: this.db,
|
|
219
|
+
tableName: "sync_runs",
|
|
220
|
+
ownerColumn: "sync_id",
|
|
221
|
+
ownerIds: input.syncIds,
|
|
222
|
+
projectId: input.projectId,
|
|
223
|
+
ownerIdFor: (row) => row.sync_id,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
runs: rows.map(rowToSyncRunRecord),
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
close(): void {
|
|
232
|
+
closeSqliteStoreConnection(this.connection)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function serializeCheckpoint(checkpoint: JsonValue | undefined): string | null {
|
|
237
|
+
return checkpoint !== undefined ? JSON.stringify(checkpoint) : null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseCheckpoint(value: string | null | undefined): JsonValue | undefined {
|
|
241
|
+
return value != null ? (JSON.parse(value) as JsonValue) : undefined
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function toSyncRunFailure(row: DatabaseRow): SyncRunFailure | undefined {
|
|
245
|
+
if (!row.error_message) {
|
|
246
|
+
return undefined
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
name: row.error_name ?? undefined,
|
|
251
|
+
message: row.error_message,
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function rowToSyncRunRecord(row: DatabaseRow): SyncRunRecord {
|
|
256
|
+
return {
|
|
257
|
+
id: row.id,
|
|
258
|
+
projectId: row.project_id,
|
|
259
|
+
syncId: row.sync_id,
|
|
260
|
+
datasetId: row.dataset_id,
|
|
261
|
+
mode: row.mode,
|
|
262
|
+
status: row.status,
|
|
263
|
+
startedAt: new Date(row.started_at),
|
|
264
|
+
finishedAt: row.finished_at ? new Date(row.finished_at) : undefined,
|
|
265
|
+
rowsRead: row.rows_read ?? undefined,
|
|
266
|
+
output: row.output_version_id
|
|
267
|
+
? {
|
|
268
|
+
datasetId: row.dataset_id,
|
|
269
|
+
versionId: row.output_version_id,
|
|
270
|
+
}
|
|
271
|
+
: undefined,
|
|
272
|
+
expectedLatestVersionId: row.expected_latest_version_id ?? undefined,
|
|
273
|
+
commitMessage: row.commit_message ?? undefined,
|
|
274
|
+
error: toSyncRunFailure(row),
|
|
275
|
+
checkpoint: parseCheckpoint(row.checkpoint),
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isUniqueConstraintError(error: unknown): boolean {
|
|
280
|
+
return error instanceof Error && error.message.includes("UNIQUE constraint failed")
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
interface DatabaseRow {
|
|
284
|
+
project_id: string
|
|
285
|
+
id: string
|
|
286
|
+
sync_id: string
|
|
287
|
+
dataset_id: string
|
|
288
|
+
mode: SyncRunRecord["mode"]
|
|
289
|
+
status: SyncRunRecord["status"]
|
|
290
|
+
started_at: string
|
|
291
|
+
finished_at: string | null
|
|
292
|
+
rows_read: number | null
|
|
293
|
+
output_version_id: string | null
|
|
294
|
+
expected_latest_version_id: string | null
|
|
295
|
+
commit_message: string | null
|
|
296
|
+
error_name: string | null
|
|
297
|
+
error_message: string | null
|
|
298
|
+
checkpoint: string | null
|
|
299
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type {
|
|
3
|
+
TimeseriesHistoryBatchInput,
|
|
4
|
+
TimeseriesHistoryBatchResult,
|
|
5
|
+
TimeseriesPoint,
|
|
6
|
+
TimeseriesStorage,
|
|
7
|
+
} from "@sixb/core/storage"
|
|
8
|
+
import { installFreshSqliteSchema } from "./migrations"
|
|
9
|
+
import {
|
|
10
|
+
closeSqliteStoreConnection,
|
|
11
|
+
openSqliteStoreConnection,
|
|
12
|
+
type SqliteStoreConnection,
|
|
13
|
+
} from "./transactions"
|
|
14
|
+
|
|
15
|
+
export interface SqliteTimeseriesStorageOptions {
|
|
16
|
+
/** Path to SQLite database file. Defaults to ':memory:' for in-memory database. */
|
|
17
|
+
path?: string
|
|
18
|
+
/** Internal shared connection used by bundled SqliteStorage. */
|
|
19
|
+
connection?: SqliteStoreConnection
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* SQLite-based TimeseriesStorage implementation.
|
|
24
|
+
*
|
|
25
|
+
* Stores time-series data with efficient querying for history and latest values.
|
|
26
|
+
*/
|
|
27
|
+
export class SqliteTimeseriesStorage implements TimeseriesStorage {
|
|
28
|
+
private readonly connection: SqliteStoreConnection
|
|
29
|
+
private readonly db: Database
|
|
30
|
+
|
|
31
|
+
constructor(options: SqliteTimeseriesStorageOptions = {}) {
|
|
32
|
+
this.connection = openSqliteStoreConnection(options)
|
|
33
|
+
this.db = this.connection.db
|
|
34
|
+
|
|
35
|
+
if (this.connection.installFreshSchema) {
|
|
36
|
+
installFreshSqliteSchema(this.db)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getHistory(params: {
|
|
41
|
+
projectId: string
|
|
42
|
+
objectTypeId: string
|
|
43
|
+
objectId: string
|
|
44
|
+
propertyId: string
|
|
45
|
+
from?: Date
|
|
46
|
+
to?: Date
|
|
47
|
+
limit?: number
|
|
48
|
+
order?: "asc" | "desc"
|
|
49
|
+
}): Promise<readonly TimeseriesPoint[]> {
|
|
50
|
+
let query = `
|
|
51
|
+
SELECT * FROM timeseries
|
|
52
|
+
WHERE project_id = ? AND object_type_id = ? AND object_id = ? AND property_id = ?
|
|
53
|
+
`
|
|
54
|
+
const args: (string | number | null)[] = [
|
|
55
|
+
params.projectId,
|
|
56
|
+
params.objectTypeId,
|
|
57
|
+
params.objectId,
|
|
58
|
+
params.propertyId,
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
if (params.from) {
|
|
62
|
+
query += " AND at >= ?"
|
|
63
|
+
args.push(params.from.toISOString())
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (params.to) {
|
|
67
|
+
query += " AND at <= ?"
|
|
68
|
+
args.push(params.to.toISOString())
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
query += ` ORDER BY at ${params.order === "desc" ? "DESC" : "ASC"}`
|
|
72
|
+
|
|
73
|
+
if (params.limit !== undefined) {
|
|
74
|
+
query += " LIMIT ?"
|
|
75
|
+
args.push(params.limit)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const rows = this.db.query(query).all(...args) as DatabaseRow[]
|
|
79
|
+
|
|
80
|
+
return rows.map((row) => this.rowToPoint(row))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async getHistoryBatch(
|
|
84
|
+
input: TimeseriesHistoryBatchInput
|
|
85
|
+
): Promise<readonly TimeseriesHistoryBatchResult[]> {
|
|
86
|
+
if (input.series.length === 0) {
|
|
87
|
+
return []
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const order = input.order === "desc" ? "DESC" : "ASC"
|
|
91
|
+
const values = input.series.map(() => "(?, ?, ?, ?)").join(", ")
|
|
92
|
+
const args: (string | number | null)[] = []
|
|
93
|
+
|
|
94
|
+
input.series.forEach((series, index) => {
|
|
95
|
+
args.push(index, series.objectTypeId, series.objectId, series.propertyId)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
let query = `
|
|
99
|
+
WITH requested(series_index, object_type_id, object_id, property_id) AS (
|
|
100
|
+
VALUES ${values}
|
|
101
|
+
),
|
|
102
|
+
ranked AS (
|
|
103
|
+
SELECT
|
|
104
|
+
requested.series_index,
|
|
105
|
+
timeseries.*,
|
|
106
|
+
ROW_NUMBER() OVER (
|
|
107
|
+
PARTITION BY requested.series_index
|
|
108
|
+
ORDER BY timeseries.at ${order}
|
|
109
|
+
) AS series_row_number
|
|
110
|
+
FROM requested
|
|
111
|
+
JOIN timeseries
|
|
112
|
+
ON timeseries.project_id = ?
|
|
113
|
+
AND timeseries.object_type_id = requested.object_type_id
|
|
114
|
+
AND timeseries.object_id = requested.object_id
|
|
115
|
+
AND timeseries.property_id = requested.property_id
|
|
116
|
+
WHERE 1 = 1
|
|
117
|
+
`
|
|
118
|
+
args.push(input.projectId)
|
|
119
|
+
|
|
120
|
+
if (input.from) {
|
|
121
|
+
query += " AND timeseries.at >= ?"
|
|
122
|
+
args.push(input.from.toISOString())
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (input.to) {
|
|
126
|
+
query += " AND timeseries.at <= ?"
|
|
127
|
+
args.push(input.to.toISOString())
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
query += "\n )\n SELECT * FROM ranked"
|
|
131
|
+
|
|
132
|
+
if (input.limitPerSeries !== undefined) {
|
|
133
|
+
query += " WHERE series_row_number <= ?"
|
|
134
|
+
args.push(Math.max(0, input.limitPerSeries))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
query += ` ORDER BY series_index ASC, at ${order}`
|
|
138
|
+
|
|
139
|
+
const rows = this.db.query(query).all(...args) as (DatabaseRow & {
|
|
140
|
+
series_index: number
|
|
141
|
+
series_row_number: number
|
|
142
|
+
})[]
|
|
143
|
+
const results = input.series.map((series) => ({
|
|
144
|
+
...series,
|
|
145
|
+
points: [] as TimeseriesPoint[],
|
|
146
|
+
}))
|
|
147
|
+
|
|
148
|
+
for (const row of rows) {
|
|
149
|
+
const result = results[row.series_index]
|
|
150
|
+
if (result) {
|
|
151
|
+
result.points.push(this.rowToPoint(row))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return results
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getLatest(params: {
|
|
159
|
+
projectId: string
|
|
160
|
+
objectTypeId: string
|
|
161
|
+
objectId: string
|
|
162
|
+
propertyId: string
|
|
163
|
+
}): Promise<TimeseriesPoint | null> {
|
|
164
|
+
const row = this.db
|
|
165
|
+
.query(
|
|
166
|
+
`
|
|
167
|
+
SELECT * FROM timeseries
|
|
168
|
+
WHERE project_id = ? AND object_type_id = ? AND object_id = ? AND property_id = ?
|
|
169
|
+
ORDER BY at DESC
|
|
170
|
+
LIMIT 1
|
|
171
|
+
`
|
|
172
|
+
)
|
|
173
|
+
.get(
|
|
174
|
+
params.projectId,
|
|
175
|
+
params.objectTypeId,
|
|
176
|
+
params.objectId,
|
|
177
|
+
params.propertyId
|
|
178
|
+
) as DatabaseRow | null
|
|
179
|
+
|
|
180
|
+
return row ? this.rowToPoint(row) : null
|
|
181
|
+
}
|
|
182
|
+
private rowToPoint(row: DatabaseRow): TimeseriesPoint {
|
|
183
|
+
return {
|
|
184
|
+
projectId: row.project_id,
|
|
185
|
+
objectTypeId: row.object_type_id,
|
|
186
|
+
objectId: row.object_id,
|
|
187
|
+
propertyId: row.property_id,
|
|
188
|
+
value: JSON.parse(row.value),
|
|
189
|
+
unit: row.unit ?? undefined,
|
|
190
|
+
at: new Date(row.at),
|
|
191
|
+
lastCommitId: row.last_commit_id,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Close the database connection.
|
|
197
|
+
*/
|
|
198
|
+
close(): void {
|
|
199
|
+
closeSqliteStoreConnection(this.connection)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface DatabaseRow {
|
|
204
|
+
project_id: string
|
|
205
|
+
object_type_id: string
|
|
206
|
+
object_id: string
|
|
207
|
+
property_id: string
|
|
208
|
+
value: string
|
|
209
|
+
unit: string | null
|
|
210
|
+
at: string
|
|
211
|
+
last_commit_id: string
|
|
212
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { Database as SqliteDatabase } from "bun:sqlite"
|
|
3
|
+
import { mkdirSync } from "node:fs"
|
|
4
|
+
import { dirname } from "node:path"
|
|
5
|
+
|
|
6
|
+
export interface SqliteStoreConnection {
|
|
7
|
+
readonly db: Database
|
|
8
|
+
readonly ownsConnection: boolean
|
|
9
|
+
readonly installFreshSchema: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SqliteStoreConnectionOptions {
|
|
13
|
+
readonly path?: string
|
|
14
|
+
readonly connection?: SqliteStoreConnection
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function openSqliteStoreConnection(
|
|
18
|
+
options: SqliteStoreConnectionOptions = {}
|
|
19
|
+
): SqliteStoreConnection {
|
|
20
|
+
if (options.connection) {
|
|
21
|
+
return {
|
|
22
|
+
db: options.connection.db,
|
|
23
|
+
ownsConnection: false,
|
|
24
|
+
installFreshSchema: false,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const path = options.path ?? ":memory:"
|
|
29
|
+
if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true })
|
|
30
|
+
const db = new SqliteDatabase(path)
|
|
31
|
+
db.run("PRAGMA foreign_keys = ON")
|
|
32
|
+
return {
|
|
33
|
+
db,
|
|
34
|
+
ownsConnection: true,
|
|
35
|
+
installFreshSchema: path === ":memory:",
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function closeSqliteStoreConnection(connection: SqliteStoreConnection): void {
|
|
40
|
+
if (connection.ownsConnection) {
|
|
41
|
+
connection.db.close()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const activeImmediateTransactions = new WeakSet<Database>()
|
|
46
|
+
|
|
47
|
+
export function runImmediateTransaction<T>(db: Database, run: () => T): T {
|
|
48
|
+
if (activeImmediateTransactions.has(db)) {
|
|
49
|
+
return run()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
db.run("BEGIN IMMEDIATE")
|
|
53
|
+
activeImmediateTransactions.add(db)
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = run()
|
|
57
|
+
db.run("COMMIT")
|
|
58
|
+
return result
|
|
59
|
+
} catch (error) {
|
|
60
|
+
rollbackQuietly(db)
|
|
61
|
+
throw error
|
|
62
|
+
} finally {
|
|
63
|
+
activeImmediateTransactions.delete(db)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runImmediateTransactionAsync<T>(
|
|
68
|
+
db: Database,
|
|
69
|
+
run: () => Promise<T> | T
|
|
70
|
+
): Promise<T> {
|
|
71
|
+
if (activeImmediateTransactions.has(db)) {
|
|
72
|
+
return run()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
db.run("BEGIN IMMEDIATE")
|
|
76
|
+
activeImmediateTransactions.add(db)
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await run()
|
|
80
|
+
db.run("COMMIT")
|
|
81
|
+
return result
|
|
82
|
+
} catch (error) {
|
|
83
|
+
rollbackQuietly(db)
|
|
84
|
+
throw error
|
|
85
|
+
} finally {
|
|
86
|
+
activeImmediateTransactions.delete(db)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function rollbackQuietly(db: Database): void {
|
|
91
|
+
try {
|
|
92
|
+
db.run("ROLLBACK")
|
|
93
|
+
} catch {
|
|
94
|
+
// Preserve the original SQLite storage failure.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ProjectionRunError } from "@sixb/core/storage"
|
|
2
|
+
|
|
3
|
+
export function assertProjectionRunFieldNonEmpty(value: string, fieldName: string): void {
|
|
4
|
+
if (value.trim().length === 0) {
|
|
5
|
+
throw new ProjectionRunError(`[SixbSqlite] Projection run ${fieldName} must not be empty.`)
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function assertProjectionRunCounter(value: number, fieldName: string): void {
|
|
10
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
11
|
+
throw new ProjectionRunError(
|
|
12
|
+
`[SixbSqlite] Projection run ${fieldName} must be a non-negative integer.`
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function assertOptionalProjectionRunCounter(
|
|
18
|
+
value: number | undefined,
|
|
19
|
+
fieldName: string
|
|
20
|
+
): void {
|
|
21
|
+
if (value !== undefined) {
|
|
22
|
+
assertProjectionRunCounter(value, fieldName)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function assertProjectionRunListWindowValue(value: number, fieldName: string): void {
|
|
27
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
28
|
+
throw new ProjectionRunError(`[SixbSqlite] Projection run list ${fieldName} must be >= 0.`)
|
|
29
|
+
}
|
|
30
|
+
}
|