@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,127 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type {
|
|
3
|
+
GetOntologyCommitByIdempotencyKeyInput,
|
|
4
|
+
GetOntologyCommitByIdInput,
|
|
5
|
+
GetOntologyCommitByOriginInput,
|
|
6
|
+
ListOntologyCommitsInput,
|
|
7
|
+
ListOntologyCommitsResult,
|
|
8
|
+
OntologyCommitRecord,
|
|
9
|
+
OntologyCommitStorage,
|
|
10
|
+
} from "@sixb/core/storage"
|
|
11
|
+
import {
|
|
12
|
+
assertNonblank,
|
|
13
|
+
assertNonnegativeInteger,
|
|
14
|
+
commitRecord,
|
|
15
|
+
originWhere,
|
|
16
|
+
type SqliteOntologyCommitRow,
|
|
17
|
+
type SqliteRootOperation,
|
|
18
|
+
} from "./shared"
|
|
19
|
+
|
|
20
|
+
export class SqliteOntologyCommitStorage implements OntologyCommitStorage {
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly db: Database,
|
|
23
|
+
private readonly runRootOperation: SqliteRootOperation
|
|
24
|
+
) {}
|
|
25
|
+
|
|
26
|
+
async getByIdempotencyKey(
|
|
27
|
+
input: GetOntologyCommitByIdempotencyKeyInput
|
|
28
|
+
): Promise<OntologyCommitRecord | null> {
|
|
29
|
+
return this.runRootOperation(() => {
|
|
30
|
+
assertNonblank(input.projectId, "Ontology commit project id")
|
|
31
|
+
assertNonblank(input.idempotencyKey, "Ontology commit idempotency key")
|
|
32
|
+
return this.readOne(
|
|
33
|
+
"project_id = ? AND idempotency_key = ?",
|
|
34
|
+
input.projectId,
|
|
35
|
+
input.idempotencyKey
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async getById(input: GetOntologyCommitByIdInput): Promise<OntologyCommitRecord | null> {
|
|
41
|
+
return this.runRootOperation(() => {
|
|
42
|
+
assertNonblank(input.projectId, "Ontology commit project id")
|
|
43
|
+
assertNonblank(input.id, "Ontology commit id")
|
|
44
|
+
return this.readOne("project_id = ? AND id = ?", input.projectId, input.id)
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getByOrigin(input: GetOntologyCommitByOriginInput): Promise<OntologyCommitRecord | null> {
|
|
49
|
+
return this.runRootOperation(() => {
|
|
50
|
+
assertNonblank(input.projectId, "Ontology commit project id")
|
|
51
|
+
const origin = originWhere(input.origin)
|
|
52
|
+
assertNonblank(origin.runId, "Ontology commit origin run id")
|
|
53
|
+
if (origin.batchOrdinal !== null) {
|
|
54
|
+
assertNonnegativeInteger(origin.batchOrdinal, "Ontology telemetry commit batch ordinal")
|
|
55
|
+
}
|
|
56
|
+
return origin.batchOrdinal === null
|
|
57
|
+
? this.readOne(
|
|
58
|
+
"project_id = ? AND origin_kind = ? AND origin_run_id = ? AND origin_batch_ordinal IS NULL",
|
|
59
|
+
input.projectId,
|
|
60
|
+
origin.kind,
|
|
61
|
+
origin.runId
|
|
62
|
+
)
|
|
63
|
+
: this.readOne(
|
|
64
|
+
"project_id = ? AND origin_kind = ? AND origin_run_id = ? AND origin_batch_ordinal = ?",
|
|
65
|
+
input.projectId,
|
|
66
|
+
origin.kind,
|
|
67
|
+
origin.runId,
|
|
68
|
+
origin.batchOrdinal
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async list(input: ListOntologyCommitsInput): Promise<ListOntologyCommitsResult> {
|
|
74
|
+
return this.runRootOperation(() => {
|
|
75
|
+
assertNonblank(input.projectId, "Ontology commit project id")
|
|
76
|
+
if (input.run) assertNonblank(input.run.id, "Ontology commit run id")
|
|
77
|
+
const offset = input.offset ?? 0
|
|
78
|
+
const limit = input.limit ?? -1
|
|
79
|
+
assertNonnegativeInteger(offset, "Ontology commit list offset")
|
|
80
|
+
if (limit !== -1) assertNonnegativeInteger(limit, "Ontology commit list limit")
|
|
81
|
+
const filters = ["project_id = ?"]
|
|
82
|
+
const values: (string | number)[] = [input.projectId]
|
|
83
|
+
if (input.run?.kind === "action") {
|
|
84
|
+
filters.push("origin_kind = 'action' AND origin_run_id = ?")
|
|
85
|
+
values.push(input.run.id)
|
|
86
|
+
} else if (input.run?.kind === "projection") {
|
|
87
|
+
filters.push("origin_run_id = ? AND origin_kind IN ('projection', 'telemetry')")
|
|
88
|
+
values.push(input.run.id)
|
|
89
|
+
}
|
|
90
|
+
const where = filters.join(" AND ")
|
|
91
|
+
const totalRow = this.db
|
|
92
|
+
.query(`SELECT COUNT(*) AS total FROM ontology_commits WHERE ${where}`)
|
|
93
|
+
.get(...values) as { readonly total: number }
|
|
94
|
+
const direction = input.order === "desc" ? "DESC" : "ASC"
|
|
95
|
+
const order =
|
|
96
|
+
input.run?.kind === "projection"
|
|
97
|
+
? `CASE WHEN origin_kind = 'telemetry' THEN origin_batch_ordinal ELSE -1 END ${direction}, committed_at ${direction}, id ${direction}`
|
|
98
|
+
: `committed_at ${direction}, id ${direction}`
|
|
99
|
+
const rows = this.db
|
|
100
|
+
.query(
|
|
101
|
+
`
|
|
102
|
+
SELECT * FROM ontology_commits
|
|
103
|
+
WHERE ${where}
|
|
104
|
+
ORDER BY ${order}
|
|
105
|
+
LIMIT ? OFFSET ?
|
|
106
|
+
`
|
|
107
|
+
)
|
|
108
|
+
.all(...values, limit, offset) as SqliteOntologyCommitRow[]
|
|
109
|
+
const commits = rows.map(commitRecord)
|
|
110
|
+
return {
|
|
111
|
+
commits,
|
|
112
|
+
total: totalRow.total,
|
|
113
|
+
hasMore: offset + commits.length < totalRow.total,
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private readOne(
|
|
119
|
+
where: string,
|
|
120
|
+
...values: readonly (string | number)[]
|
|
121
|
+
): OntologyCommitRecord | null {
|
|
122
|
+
const row = this.db
|
|
123
|
+
.query(`SELECT * FROM ontology_commits WHERE ${where}`)
|
|
124
|
+
.get(...values) as SqliteOntologyCommitRow | null
|
|
125
|
+
return row ? commitRecord(row) : null
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type { OntologyStorage } from "@sixb/core/storage"
|
|
3
|
+
import { SqliteOntologyCommitStorage } from "./commits"
|
|
4
|
+
import type { SqliteOntologyTransactionContext } from "./materialization-session"
|
|
5
|
+
import { SqliteOntologyMaterializationStorage } from "./materializations"
|
|
6
|
+
import { SqliteOntologyOutboxStorage } from "./outbox"
|
|
7
|
+
import type { SqliteRootOperation } from "./shared"
|
|
8
|
+
import { SqliteOntologySourceStorage } from "./sources"
|
|
9
|
+
|
|
10
|
+
export class SqliteOntologyStorage implements OntologyStorage {
|
|
11
|
+
readonly commits: SqliteOntologyCommitStorage
|
|
12
|
+
readonly sources: SqliteOntologySourceStorage
|
|
13
|
+
readonly materializations: SqliteOntologyMaterializationStorage
|
|
14
|
+
readonly outbox: SqliteOntologyOutboxStorage
|
|
15
|
+
|
|
16
|
+
constructor(input: {
|
|
17
|
+
readonly db: Database
|
|
18
|
+
readonly runRootOperation: SqliteRootOperation
|
|
19
|
+
readonly transactionContext: SqliteOntologyTransactionContext | null
|
|
20
|
+
}) {
|
|
21
|
+
this.commits = new SqliteOntologyCommitStorage(input.db, input.runRootOperation)
|
|
22
|
+
this.sources = new SqliteOntologySourceStorage(input.db, input.runRootOperation)
|
|
23
|
+
this.materializations = new SqliteOntologyMaterializationStorage(
|
|
24
|
+
input.db,
|
|
25
|
+
input.transactionContext
|
|
26
|
+
)
|
|
27
|
+
this.outbox = new SqliteOntologyOutboxStorage(input.db, input.runRootOperation)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
deactivateSessions(): void {
|
|
31
|
+
this.materializations.deactivateSessions()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type { SqliteOntologyTransactionContext } from "./materialization-session"
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import {
|
|
3
|
+
linkRefKey,
|
|
4
|
+
MaterializationConflictError,
|
|
5
|
+
objectRefKey,
|
|
6
|
+
} from "@sixb/core/internal/materialization"
|
|
7
|
+
import {
|
|
8
|
+
correlateMaterializationChunk,
|
|
9
|
+
duplicateMaterializationWork as duplicateWork,
|
|
10
|
+
invalidCorrelation,
|
|
11
|
+
materializationPlanItems,
|
|
12
|
+
ProviderMaterializationSessionState,
|
|
13
|
+
type ProviderMaterializationTransactionLifecycle,
|
|
14
|
+
prepareMaterializationWork,
|
|
15
|
+
} from "@sixb/core/internal/ontology-storage-provider"
|
|
16
|
+
import type {
|
|
17
|
+
MaterializationEventWorkRecord,
|
|
18
|
+
MaterializationPlanChunk,
|
|
19
|
+
MaterializationPlanHeader,
|
|
20
|
+
MaterializationPlanWorkItem,
|
|
21
|
+
MaterializationSession,
|
|
22
|
+
MaterializationWorkPage,
|
|
23
|
+
MaterializationWorkRecord,
|
|
24
|
+
StageMaterializationWorkInput,
|
|
25
|
+
StreamMaterializationWorkInput,
|
|
26
|
+
} from "@sixb/core/storage"
|
|
27
|
+
import {
|
|
28
|
+
type ReplacementIdentity,
|
|
29
|
+
SQLITE_MATERIALIZATION_WORK_TABLE,
|
|
30
|
+
SQLITE_REPLACEMENT_WORK_TABLE,
|
|
31
|
+
} from "./materialization-state"
|
|
32
|
+
import { canonicalJson, isSqliteConstraintError, parseJson } from "./shared"
|
|
33
|
+
|
|
34
|
+
export interface SqliteOntologyTransactionContext {
|
|
35
|
+
readonly id: object
|
|
36
|
+
readonly materializations: ProviderMaterializationTransactionLifecycle
|
|
37
|
+
active: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class SqliteMaterializationSessionState extends ProviderMaterializationSessionState {}
|
|
41
|
+
|
|
42
|
+
export class SqliteMaterializationSessions {
|
|
43
|
+
private readonly sessions = new WeakMap<object, SqliteMaterializationSessionState>()
|
|
44
|
+
private readonly live = new Set<SqliteMaterializationSessionState>()
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
private readonly db: Database,
|
|
48
|
+
private readonly context: SqliteOntologyTransactionContext | null
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
create(header: MaterializationPlanHeader): SqliteMaterializationSessionState {
|
|
52
|
+
if (!this.context?.active) {
|
|
53
|
+
throw new MaterializationConflictError(
|
|
54
|
+
"effective-state",
|
|
55
|
+
"Materialization sessions require an active storage transaction."
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
this.ensureTables()
|
|
59
|
+
const session = new SqliteMaterializationSessionState(structuredClone(header), this.context.id)
|
|
60
|
+
this.sessions.set(session.providerToken, session)
|
|
61
|
+
this.live.add(session)
|
|
62
|
+
this.context.materializations.register(session.providerToken)
|
|
63
|
+
return session
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
require(session: MaterializationSession): SqliteMaterializationSessionState {
|
|
67
|
+
const value = this.sessions.get(session.providerToken)
|
|
68
|
+
if (
|
|
69
|
+
!value ||
|
|
70
|
+
!value.active ||
|
|
71
|
+
!this.context?.active ||
|
|
72
|
+
value.transactionId !== this.context.id
|
|
73
|
+
) {
|
|
74
|
+
throw new MaterializationConflictError(
|
|
75
|
+
"effective-state",
|
|
76
|
+
"Materialization session is inactive."
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
return value
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
release(session: SqliteMaterializationSessionState): void {
|
|
83
|
+
if (!session.active) return
|
|
84
|
+
this.deleteWork(session.id)
|
|
85
|
+
session.active = false
|
|
86
|
+
session.replacement = null
|
|
87
|
+
this.live.delete(session)
|
|
88
|
+
this.context?.materializations.complete(session.providerToken)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
deactivateAll(): void {
|
|
92
|
+
for (const session of this.live) this.release(session)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
stage(input: StageMaterializationWorkInput): void {
|
|
96
|
+
const session = this.require(input.session)
|
|
97
|
+
const records = prepareMaterializationWork(session, input)
|
|
98
|
+
const insert = this.db.query(
|
|
99
|
+
`
|
|
100
|
+
INSERT INTO ${SQLITE_MATERIALIZATION_WORK_TABLE} (
|
|
101
|
+
session_id, record_key, unique_key, kind, lane,
|
|
102
|
+
major_order, minor_order, sort_one, sort_two, payload
|
|
103
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, json(?))
|
|
104
|
+
`
|
|
105
|
+
)
|
|
106
|
+
try {
|
|
107
|
+
for (const { record, uniqueKey, columns } of records) {
|
|
108
|
+
insert.run(
|
|
109
|
+
session.id,
|
|
110
|
+
record.recordKey,
|
|
111
|
+
uniqueKey,
|
|
112
|
+
record.kind,
|
|
113
|
+
columns.lane,
|
|
114
|
+
columns.majorOrder,
|
|
115
|
+
columns.minorOrder,
|
|
116
|
+
columns.sortOne,
|
|
117
|
+
columns.sortTwo,
|
|
118
|
+
canonicalJson(record)
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (isSqliteConstraintError(error)) {
|
|
123
|
+
throw duplicateWork(input.records[0]?.recordKey ?? "unknown")
|
|
124
|
+
}
|
|
125
|
+
throw error
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async *stream(input: StreamMaterializationWorkInput): AsyncIterable<MaterializationWorkPage> {
|
|
130
|
+
const session = this.require(input.session)
|
|
131
|
+
const stream = session.workStreams[input.order]
|
|
132
|
+
if (stream.started) {
|
|
133
|
+
throw new MaterializationConflictError(
|
|
134
|
+
"effective-state",
|
|
135
|
+
`Materialization ${input.order} work may only be streamed once per session.`
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
stream.started = true
|
|
139
|
+
session.workSealed = true
|
|
140
|
+
let offset = 0
|
|
141
|
+
while (true) {
|
|
142
|
+
this.require(input.session)
|
|
143
|
+
const rows = this.db
|
|
144
|
+
.query(
|
|
145
|
+
`
|
|
146
|
+
SELECT payload
|
|
147
|
+
FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
148
|
+
WHERE session_id = ? AND lane = ?
|
|
149
|
+
ORDER BY major_order, minor_order, sort_one, sort_two, record_key
|
|
150
|
+
LIMIT ? OFFSET ?
|
|
151
|
+
`
|
|
152
|
+
)
|
|
153
|
+
.all(session.id, input.order, input.pageRows, offset) as { readonly payload: string }[]
|
|
154
|
+
if (rows.length === 0) break
|
|
155
|
+
const records = rows.map((row) =>
|
|
156
|
+
parseJson<MaterializationWorkRecord>(row.payload)
|
|
157
|
+
) as MaterializationWorkPage["records"]
|
|
158
|
+
offset += records.length
|
|
159
|
+
stream.emittedCount = offset
|
|
160
|
+
yield { records }
|
|
161
|
+
}
|
|
162
|
+
this.require(input.session)
|
|
163
|
+
stream.completed = true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
readObjectExistence(
|
|
167
|
+
session: SqliteMaterializationSessionState,
|
|
168
|
+
refs: readonly { readonly objectTypeId: string; readonly primaryId: string }[]
|
|
169
|
+
): readonly {
|
|
170
|
+
readonly ref: { readonly objectTypeId: string; readonly primaryId: string }
|
|
171
|
+
readonly exists: boolean
|
|
172
|
+
}[] {
|
|
173
|
+
const lookup = this.db.query(
|
|
174
|
+
`
|
|
175
|
+
SELECT payload FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
176
|
+
WHERE session_id = ? AND unique_key = ? AND kind = 'object-existence'
|
|
177
|
+
`
|
|
178
|
+
)
|
|
179
|
+
return refs.flatMap((ref) => {
|
|
180
|
+
const key = `object-existence:${JSON.stringify([ref.objectTypeId, ref.primaryId])}`
|
|
181
|
+
const row = lookup.get(session.id, key) as { readonly payload: string } | null
|
|
182
|
+
if (!row) return []
|
|
183
|
+
const record = parseJson<
|
|
184
|
+
Extract<MaterializationWorkRecord, { readonly kind: "object-existence" }>
|
|
185
|
+
>(row.payload)
|
|
186
|
+
return [{ ref: record.ref, exists: record.exists }]
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
assertChunkSequence(
|
|
191
|
+
session: SqliteMaterializationSessionState,
|
|
192
|
+
chunk: MaterializationPlanChunk
|
|
193
|
+
): void {
|
|
194
|
+
const items = materializationPlanItems(chunk)
|
|
195
|
+
const progress = correlateMaterializationChunk(
|
|
196
|
+
session,
|
|
197
|
+
chunk,
|
|
198
|
+
items,
|
|
199
|
+
this.planItems(session, session.appliedPlanCount, items.length),
|
|
200
|
+
this.eventRecords(session, session.appliedOutboxCount, chunk.outbox.length)
|
|
201
|
+
)
|
|
202
|
+
session.appliedPlanCount = progress.appliedPlanCount
|
|
203
|
+
session.appliedOutboxCount = progress.appliedOutboxCount
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
recordReplacementIdentities(
|
|
207
|
+
session: SqliteMaterializationSessionState,
|
|
208
|
+
identities: readonly ReplacementIdentity[]
|
|
209
|
+
): void {
|
|
210
|
+
const upsert = this.db.query(
|
|
211
|
+
`
|
|
212
|
+
INSERT INTO ${SQLITE_REPLACEMENT_WORK_TABLE} (
|
|
213
|
+
session_id, entity_kind, identity_key, diff_required
|
|
214
|
+
) VALUES (?, ?, ?, ?)
|
|
215
|
+
ON CONFLICT(session_id, entity_kind, identity_key) DO UPDATE SET
|
|
216
|
+
diff_required = MAX(diff_required, excluded.diff_required)
|
|
217
|
+
`
|
|
218
|
+
)
|
|
219
|
+
for (const identity of identities) {
|
|
220
|
+
upsert.run(
|
|
221
|
+
session.id,
|
|
222
|
+
identity.kind,
|
|
223
|
+
identity.kind === "object" ? objectRefKey(identity.ref) : linkRefKey(identity.ref),
|
|
224
|
+
identity.diffRequired ? 1 : 0
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
count(session: SqliteMaterializationSessionState, kind: string): number {
|
|
230
|
+
const row = this.db
|
|
231
|
+
.query(
|
|
232
|
+
`SELECT COUNT(*) AS count FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
233
|
+
WHERE session_id = ? AND kind = ?`
|
|
234
|
+
)
|
|
235
|
+
.get(session.id, kind) as { readonly count: number }
|
|
236
|
+
return row.count
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
laneCounts(session: SqliteMaterializationSessionState): {
|
|
240
|
+
readonly apply: number
|
|
241
|
+
readonly cardinality: number
|
|
242
|
+
readonly event: number
|
|
243
|
+
} {
|
|
244
|
+
const rows = this.db
|
|
245
|
+
.query(
|
|
246
|
+
`SELECT lane, COUNT(*) AS count FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
247
|
+
WHERE session_id = ? AND lane <> 'none'
|
|
248
|
+
GROUP BY lane`
|
|
249
|
+
)
|
|
250
|
+
.all(session.id) as {
|
|
251
|
+
readonly lane: "apply" | "cardinality" | "event"
|
|
252
|
+
readonly count: number
|
|
253
|
+
}[]
|
|
254
|
+
const counts = { apply: 0, cardinality: 0, event: 0 }
|
|
255
|
+
for (const row of rows) counts[row.lane] = row.count
|
|
256
|
+
return counts
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
records(session: SqliteMaterializationSessionState, kind?: string): MaterializationWorkRecord[] {
|
|
260
|
+
const rows = this.db
|
|
261
|
+
.query(
|
|
262
|
+
`SELECT payload FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
263
|
+
WHERE session_id = ? ${kind ? "AND kind = ?" : ""}`
|
|
264
|
+
)
|
|
265
|
+
.all(...(kind ? [session.id, kind] : [session.id])) as { readonly payload: string }[]
|
|
266
|
+
return rows.map((row) => parseJson<MaterializationWorkRecord>(row.payload))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
assertClassificationCoverage(session: SqliteMaterializationSessionState): void {
|
|
270
|
+
if (!session.replacement) return
|
|
271
|
+
const expectedObject = session.replacement.projectionKind === "object" ? 1 : 0
|
|
272
|
+
const mismatch = this.db
|
|
273
|
+
.query(
|
|
274
|
+
`
|
|
275
|
+
WITH expected AS (
|
|
276
|
+
SELECT entity_kind, identity_key
|
|
277
|
+
FROM ${SQLITE_REPLACEMENT_WORK_TABLE}
|
|
278
|
+
WHERE session_id = ? AND diff_required = 1
|
|
279
|
+
AND (entity_kind = 'link' OR ? = 1)
|
|
280
|
+
), actual AS (
|
|
281
|
+
SELECT json_extract(payload, '$.entityKind') AS entity_kind,
|
|
282
|
+
json_extract(payload, '$.identityKey') AS identity_key
|
|
283
|
+
FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
284
|
+
WHERE session_id = ? AND kind = 'classification'
|
|
285
|
+
AND json_extract(payload, '$.entityKind') IN ('object', 'link')
|
|
286
|
+
), differences AS (
|
|
287
|
+
SELECT * FROM expected EXCEPT SELECT * FROM actual
|
|
288
|
+
UNION ALL
|
|
289
|
+
SELECT * FROM actual EXCEPT SELECT * FROM expected
|
|
290
|
+
)
|
|
291
|
+
SELECT 1 FROM differences LIMIT 1
|
|
292
|
+
`
|
|
293
|
+
)
|
|
294
|
+
.get(session.id, expectedObject, session.id)
|
|
295
|
+
const pointClassifications = this.db
|
|
296
|
+
.query(
|
|
297
|
+
`
|
|
298
|
+
SELECT 1 FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
299
|
+
WHERE session_id = ? AND kind = 'classification'
|
|
300
|
+
AND json_extract(payload, '$.entityKind') = 'point'
|
|
301
|
+
LIMIT 1
|
|
302
|
+
`
|
|
303
|
+
)
|
|
304
|
+
.get(session.id)
|
|
305
|
+
if (mismatch || pointClassifications) {
|
|
306
|
+
invalidCorrelation(
|
|
307
|
+
"Projection replacement classification coverage does not match its streamed state."
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private planItems(
|
|
313
|
+
session: SqliteMaterializationSessionState,
|
|
314
|
+
offset: number,
|
|
315
|
+
limit: number
|
|
316
|
+
): MaterializationPlanWorkItem[] {
|
|
317
|
+
if (limit === 0) return []
|
|
318
|
+
const rows = this.db
|
|
319
|
+
.query(
|
|
320
|
+
`
|
|
321
|
+
SELECT payload FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
322
|
+
WHERE session_id = ? AND lane = 'apply'
|
|
323
|
+
ORDER BY major_order, minor_order, sort_one, sort_two, record_key
|
|
324
|
+
LIMIT ? OFFSET ?
|
|
325
|
+
`
|
|
326
|
+
)
|
|
327
|
+
.all(session.id, limit, offset) as { readonly payload: string }[]
|
|
328
|
+
return rows.map((row) => {
|
|
329
|
+
const record = parseJson<Extract<MaterializationWorkRecord, { readonly kind: "plan" }>>(
|
|
330
|
+
row.payload
|
|
331
|
+
)
|
|
332
|
+
return record.item
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private eventRecords(
|
|
337
|
+
session: SqliteMaterializationSessionState,
|
|
338
|
+
offset: number,
|
|
339
|
+
limit: number
|
|
340
|
+
): MaterializationEventWorkRecord[] {
|
|
341
|
+
if (limit === 0) return []
|
|
342
|
+
const rows = this.db
|
|
343
|
+
.query(
|
|
344
|
+
`
|
|
345
|
+
SELECT payload FROM ${SQLITE_MATERIALIZATION_WORK_TABLE}
|
|
346
|
+
WHERE session_id = ? AND lane = 'event'
|
|
347
|
+
ORDER BY major_order, minor_order, sort_one, sort_two, record_key
|
|
348
|
+
LIMIT ? OFFSET ?
|
|
349
|
+
`
|
|
350
|
+
)
|
|
351
|
+
.all(session.id, limit, offset) as { readonly payload: string }[]
|
|
352
|
+
return rows.map((row) => parseJson<MaterializationEventWorkRecord>(row.payload))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private ensureTables(): void {
|
|
356
|
+
this.db.run(`
|
|
357
|
+
CREATE TEMP TABLE IF NOT EXISTS ${SQLITE_MATERIALIZATION_WORK_TABLE} (
|
|
358
|
+
session_id TEXT NOT NULL,
|
|
359
|
+
record_key TEXT NOT NULL,
|
|
360
|
+
unique_key TEXT NOT NULL,
|
|
361
|
+
kind TEXT NOT NULL,
|
|
362
|
+
lane TEXT NOT NULL,
|
|
363
|
+
major_order INTEGER NOT NULL,
|
|
364
|
+
minor_order INTEGER NOT NULL,
|
|
365
|
+
sort_one TEXT NOT NULL,
|
|
366
|
+
sort_two TEXT NOT NULL,
|
|
367
|
+
payload TEXT NOT NULL CHECK (json_valid(payload)),
|
|
368
|
+
PRIMARY KEY (session_id, record_key),
|
|
369
|
+
UNIQUE (session_id, unique_key)
|
|
370
|
+
);
|
|
371
|
+
CREATE INDEX IF NOT EXISTS idx_ontology_materialization_work_lane
|
|
372
|
+
ON ${SQLITE_MATERIALIZATION_WORK_TABLE}(
|
|
373
|
+
session_id, lane, major_order, minor_order, sort_one, sort_two, record_key
|
|
374
|
+
);
|
|
375
|
+
CREATE TEMP TABLE IF NOT EXISTS ${SQLITE_REPLACEMENT_WORK_TABLE} (
|
|
376
|
+
session_id TEXT NOT NULL,
|
|
377
|
+
entity_kind TEXT NOT NULL,
|
|
378
|
+
identity_key TEXT NOT NULL,
|
|
379
|
+
diff_required INTEGER NOT NULL CHECK (diff_required IN (0, 1)),
|
|
380
|
+
PRIMARY KEY (session_id, entity_kind, identity_key)
|
|
381
|
+
);
|
|
382
|
+
`)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private deleteWork(sessionId: string): void {
|
|
386
|
+
this.db
|
|
387
|
+
.query(`DELETE FROM ${SQLITE_MATERIALIZATION_WORK_TABLE} WHERE session_id = ?`)
|
|
388
|
+
.run(sessionId)
|
|
389
|
+
this.db
|
|
390
|
+
.query(`DELETE FROM ${SQLITE_REPLACEMENT_WORK_TABLE} WHERE session_id = ?`)
|
|
391
|
+
.run(sessionId)
|
|
392
|
+
}
|
|
393
|
+
}
|