@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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +57 -0
  3. package/dist/action-run-storage.d.ts +29 -0
  4. package/dist/action-run-storage.d.ts.map +1 -0
  5. package/dist/agents/agent-storage.d.ts +25 -0
  6. package/dist/agents/agent-storage.d.ts.map +1 -0
  7. package/dist/agents/index.d.ts +6 -0
  8. package/dist/agents/index.d.ts.map +1 -0
  9. package/dist/agents/messages.d.ts +14 -0
  10. package/dist/agents/messages.d.ts.map +1 -0
  11. package/dist/agents/rows.d.ts +77 -0
  12. package/dist/agents/rows.d.ts.map +1 -0
  13. package/dist/agents/runs.d.ts +26 -0
  14. package/dist/agents/runs.d.ts.map +1 -0
  15. package/dist/agents/threads.d.ts +14 -0
  16. package/dist/agents/threads.d.ts.map +1 -0
  17. package/dist/auth-storage/access-tokens.d.ts +33 -0
  18. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  19. package/dist/auth-storage/auth-storage.d.ts +46 -0
  20. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  21. package/dist/auth-storage/group-memberships.d.ts +21 -0
  22. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  23. package/dist/auth-storage/identities.d.ts +17 -0
  24. package/dist/auth-storage/identities.d.ts.map +1 -0
  25. package/dist/auth-storage/index.d.ts +3 -0
  26. package/dist/auth-storage/index.d.ts.map +1 -0
  27. package/dist/auth-storage/invitations.d.ts +28 -0
  28. package/dist/auth-storage/invitations.d.ts.map +1 -0
  29. package/dist/auth-storage/magic-links.d.ts +28 -0
  30. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  31. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  32. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  33. package/dist/auth-storage/rows.d.ts +134 -0
  34. package/dist/auth-storage/rows.d.ts.map +1 -0
  35. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  36. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  37. package/dist/auth-storage/service-accounts.d.ts +14 -0
  38. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  39. package/dist/auth-storage/sessions.d.ts +55 -0
  40. package/dist/auth-storage/sessions.d.ts.map +1 -0
  41. package/dist/auth-storage/shared.d.ts +131 -0
  42. package/dist/auth-storage/shared.d.ts.map +1 -0
  43. package/dist/auth-storage/users.d.ts +19 -0
  44. package/dist/auth-storage/users.d.ts.map +1 -0
  45. package/dist/index.d.ts +71 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +10204 -0
  48. package/dist/index.js.map +54 -0
  49. package/dist/latest-run-query.d.ts +22 -0
  50. package/dist/latest-run-query.d.ts.map +1 -0
  51. package/dist/migrations.d.ts +18 -0
  52. package/dist/migrations.d.ts.map +1 -0
  53. package/dist/object-query-compiler.d.ts +44 -0
  54. package/dist/object-query-compiler.d.ts.map +1 -0
  55. package/dist/object-storage.d.ts +93 -0
  56. package/dist/object-storage.d.ts.map +1 -0
  57. package/dist/ontology-storage/commits.d.ts +14 -0
  58. package/dist/ontology-storage/commits.d.ts.map +1 -0
  59. package/dist/ontology-storage/index.d.ts +22 -0
  60. package/dist/ontology-storage/index.d.ts.map +1 -0
  61. package/dist/ontology-storage/materialization-session.d.ts +49 -0
  62. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  63. package/dist/ontology-storage/materialization-state.d.ts +63 -0
  64. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  65. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  66. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  67. package/dist/ontology-storage/materializations.d.ts +32 -0
  68. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  69. package/dist/ontology-storage/outbox.d.ts +14 -0
  70. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  71. package/dist/ontology-storage/shared.d.ts +91 -0
  72. package/dist/ontology-storage/shared.d.ts.map +1 -0
  73. package/dist/ontology-storage/sources.d.ts +26 -0
  74. package/dist/ontology-storage/sources.d.ts.map +1 -0
  75. package/dist/pipeline-run-storage.d.ts +26 -0
  76. package/dist/pipeline-run-storage.d.ts.map +1 -0
  77. package/dist/projection-run-storage.d.ts +32 -0
  78. package/dist/projection-run-storage.d.ts.map +1 -0
  79. package/dist/rules-storage.d.ts +33 -0
  80. package/dist/rules-storage.d.ts.map +1 -0
  81. package/dist/run-list-query.d.ts +27 -0
  82. package/dist/run-list-query.d.ts.map +1 -0
  83. package/dist/storage-errors.d.ts +2 -0
  84. package/dist/storage-errors.d.ts.map +1 -0
  85. package/dist/sync-run-storage.d.ts +23 -0
  86. package/dist/sync-run-storage.d.ts.map +1 -0
  87. package/dist/timeseries-storage.d.ts +41 -0
  88. package/dist/timeseries-storage.d.ts.map +1 -0
  89. package/dist/transactions.d.ts +15 -0
  90. package/dist/transactions.d.ts.map +1 -0
  91. package/dist/tsconfig.json +5 -0
  92. package/dist/validation.d.ts +5 -0
  93. package/dist/validation.d.ts.map +1 -0
  94. package/dist/webhook-delivery-storage.d.ts +27 -0
  95. package/dist/webhook-delivery-storage.d.ts.map +1 -0
  96. package/dist/webhook-run-storage.d.ts +22 -0
  97. package/dist/webhook-run-storage.d.ts.map +1 -0
  98. package/dist/workflow-intervention-storage.d.ts +26 -0
  99. package/dist/workflow-intervention-storage.d.ts.map +1 -0
  100. package/dist/workflow-run-storage.d.ts +63 -0
  101. package/dist/workflow-run-storage.d.ts.map +1 -0
  102. package/package.json +58 -0
  103. package/src/action-run-storage.ts +761 -0
  104. package/src/agents/agent-storage.ts +46 -0
  105. package/src/agents/index.ts +5 -0
  106. package/src/agents/messages.ts +162 -0
  107. package/src/agents/rows.ts +205 -0
  108. package/src/agents/runs.ts +389 -0
  109. package/src/agents/threads.ts +133 -0
  110. package/src/auth-storage/access-tokens.ts +289 -0
  111. package/src/auth-storage/auth-storage.ts +784 -0
  112. package/src/auth-storage/group-memberships.ts +52 -0
  113. package/src/auth-storage/identities.ts +98 -0
  114. package/src/auth-storage/index.ts +2 -0
  115. package/src/auth-storage/invitations.ts +258 -0
  116. package/src/auth-storage/magic-links.ts +149 -0
  117. package/src/auth-storage/oidc-attempts.ts +109 -0
  118. package/src/auth-storage/rows.ts +338 -0
  119. package/src/auth-storage/service-account-group-memberships.ts +161 -0
  120. package/src/auth-storage/service-accounts.ts +168 -0
  121. package/src/auth-storage/sessions.ts +230 -0
  122. package/src/auth-storage/shared.ts +707 -0
  123. package/src/auth-storage/users.ts +226 -0
  124. package/src/index.ts +292 -0
  125. package/src/latest-run-query.ts +49 -0
  126. package/src/migrations/001-initial-schema.sql +1068 -0
  127. package/src/migrations.ts +245 -0
  128. package/src/object-query-compiler.ts +1198 -0
  129. package/src/object-storage.ts +605 -0
  130. package/src/ontology-storage/commits.ts +127 -0
  131. package/src/ontology-storage/index.ts +35 -0
  132. package/src/ontology-storage/materialization-session.ts +393 -0
  133. package/src/ontology-storage/materialization-state.ts +901 -0
  134. package/src/ontology-storage/materialization-writer.ts +394 -0
  135. package/src/ontology-storage/materializations.ts +838 -0
  136. package/src/ontology-storage/outbox.ts +237 -0
  137. package/src/ontology-storage/shared.ts +269 -0
  138. package/src/ontology-storage/sources.ts +653 -0
  139. package/src/pipeline-run-storage.ts +506 -0
  140. package/src/projection-run-storage.ts +592 -0
  141. package/src/rules-storage.ts +375 -0
  142. package/src/run-list-query.ts +98 -0
  143. package/src/sql.d.ts +4 -0
  144. package/src/storage-errors.ts +3 -0
  145. package/src/sync-run-storage.ts +299 -0
  146. package/src/timeseries-storage.ts +212 -0
  147. package/src/transactions.ts +96 -0
  148. package/src/validation.ts +30 -0
  149. package/src/webhook-delivery-storage.ts +272 -0
  150. package/src/webhook-run-storage.ts +240 -0
  151. package/src/workflow-intervention-storage.ts +398 -0
  152. 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
+ }