mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(local): bind legacy adoption provenance
This commit is contained in:
@@ -44,6 +44,11 @@ import {
|
||||
type LocalSqliteReadinessEvidence,
|
||||
} from '../readiness/readiness';
|
||||
import { LocalSqliteSecurityAuthorityStore } from '../security/securityAuthorityStore';
|
||||
import {
|
||||
LegacyAdoptionPublicationDigest,
|
||||
legacyAdoptionTaskProvenanceDigest,
|
||||
legacyAdoptionTriggerProvenanceDigest,
|
||||
} from './legacyAdoptionProvenance';
|
||||
|
||||
export const MAX_LOCAL_LEGACY_ADOPTION_TASKS = 100_000;
|
||||
export const MAX_LOCAL_LEGACY_ADOPTION_TRIGGERS = 500_000;
|
||||
@@ -202,6 +207,86 @@ function deterministicMutationId(
|
||||
)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
}
|
||||
|
||||
function assertStoredProvenance(
|
||||
client: DatabaseSync,
|
||||
adoption: Readonly<LocalLegacyAdoptionRecord>,
|
||||
): void {
|
||||
const rows = client
|
||||
.prepare(
|
||||
`SELECT task."row_ordinal" AS "rowOrdinal",
|
||||
task."source_digest" AS "sourceDigest",
|
||||
task."task_content_digest" AS "taskContentDigest",
|
||||
task."trigger_count" AS "triggerCount",
|
||||
task."item_digest" AS "taskItemDigest",
|
||||
trigger."trigger_ordinal" AS "triggerOrdinal",
|
||||
trigger."trigger_content_digest" AS "triggerContentDigest",
|
||||
trigger."item_digest" AS "triggerItemDigest"
|
||||
FROM "QingLong3LegacyAdoptionTasks" AS task
|
||||
LEFT JOIN "QingLong3LegacyAdoptionTriggers" AS trigger
|
||||
ON trigger."adoption_mutation_id" = task."adoption_mutation_id"
|
||||
AND trigger."row_ordinal" = task."row_ordinal"
|
||||
WHERE task."adoption_mutation_id" = ?
|
||||
ORDER BY task."row_ordinal" ASC, trigger."trigger_ordinal" ASC`,
|
||||
)
|
||||
.iterate(adoption.mutationId) as Iterable<Row>;
|
||||
const publication = new LegacyAdoptionPublicationDigest(
|
||||
adoption.mutationId,
|
||||
);
|
||||
let taskCount = 0;
|
||||
let triggerCount = 0;
|
||||
let currentRowOrdinal = 0;
|
||||
let expectedTriggerCount = 0;
|
||||
let currentTriggerCount = 0;
|
||||
for (const row of rows) {
|
||||
const rowOrdinal = integer(row, 'rowOrdinal');
|
||||
if (rowOrdinal !== currentRowOrdinal) {
|
||||
if (
|
||||
currentRowOrdinal !== 0 &&
|
||||
currentTriggerCount !== expectedTriggerCount
|
||||
) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
if (rowOrdinal <= currentRowOrdinal) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
currentRowOrdinal = rowOrdinal;
|
||||
taskCount += 1;
|
||||
currentTriggerCount = 0;
|
||||
expectedTriggerCount = integer(row, 'triggerCount');
|
||||
publication.appendTask({
|
||||
rowOrdinal,
|
||||
sourceDigest: text(row, 'sourceDigest'),
|
||||
taskContentDigest: text(row, 'taskContentDigest'),
|
||||
itemDigest: text(row, 'taskItemDigest'),
|
||||
});
|
||||
}
|
||||
if (row.triggerOrdinal === null) {
|
||||
if (expectedTriggerCount !== 0) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const triggerOrdinal = integer(row, 'triggerOrdinal');
|
||||
currentTriggerCount += 1;
|
||||
triggerCount += 1;
|
||||
if (triggerOrdinal !== currentTriggerCount) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
publication.appendTrigger({
|
||||
triggerContentDigest: text(row, 'triggerContentDigest'),
|
||||
itemDigest: text(row, 'triggerItemDigest'),
|
||||
});
|
||||
}
|
||||
if (
|
||||
(currentRowOrdinal !== 0 && currentTriggerCount !== expectedTriggerCount) ||
|
||||
taskCount !== adoption.adoptedTaskCount ||
|
||||
triggerCount !== adoption.adoptedTriggerCount ||
|
||||
publication.digest() !== adoption.publicationDigest
|
||||
) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
}
|
||||
|
||||
function exactReplay(
|
||||
existing: LocalLegacyAdoptionRecord,
|
||||
command: PublishLocalLegacyAdoptionCommand,
|
||||
@@ -344,6 +429,7 @@ export class LocalSqliteLegacyAdoptionPublisher {
|
||||
if (!exactReplay(existing, input)) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
assertStoredProvenance(client, existing);
|
||||
await input.confirmExternalAuthority();
|
||||
client.exec('COMMIT');
|
||||
began = false;
|
||||
@@ -389,9 +475,9 @@ export class LocalSqliteLegacyAdoptionPublisher {
|
||||
const taskRegistry = createBuiltInTaskSpecSemanticRegistry();
|
||||
const triggerRegistry = createBuiltInTriggerSpecSemanticRegistry();
|
||||
const dispatch = new LocalSqliteDispatchDefinitionStore(client);
|
||||
const publication = createHash('sha256')
|
||||
.update('qinglong3.legacy-adoption-publication.v1\0')
|
||||
.update(input.mutationId);
|
||||
const publication = new LegacyAdoptionPublicationDigest(
|
||||
input.mutationId,
|
||||
);
|
||||
let adoptedTaskCount = 0;
|
||||
let adoptedTriggerCount = 0;
|
||||
let previousRowOrdinal = 0;
|
||||
@@ -488,13 +574,46 @@ export class LocalSqliteLegacyAdoptionPublisher {
|
||||
compileLocalCommandTaskDefinition(task, taskRegistry),
|
||||
);
|
||||
}
|
||||
publication
|
||||
.update('\0task\0')
|
||||
.update(String(candidate.rowOrdinal))
|
||||
.update('\0')
|
||||
.update(candidate.sourceDigest)
|
||||
.update('\0')
|
||||
.update(task.contentDigest);
|
||||
const taskProvenance = Object.freeze({
|
||||
adoptionMutationId: input.mutationId,
|
||||
rowOrdinal: candidate.rowOrdinal,
|
||||
projectId: task.projectId,
|
||||
sourceDigest: candidate.sourceDigest,
|
||||
taskId: task.taskId,
|
||||
taskRevision: task.revision,
|
||||
taskMutationId: task.mutationId,
|
||||
taskContentDigest: task.contentDigest,
|
||||
triggerCount: candidate.triggers.length,
|
||||
});
|
||||
const taskItemDigest =
|
||||
legacyAdoptionTaskProvenanceDigest(taskProvenance);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LegacyAdoptionTasks" (
|
||||
"adoption_mutation_id", "row_ordinal", "project_id",
|
||||
"source_digest", "task_id", "task_revision",
|
||||
"task_mutation_id", "task_content_digest",
|
||||
"trigger_count", "item_digest"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
taskProvenance.adoptionMutationId,
|
||||
taskProvenance.rowOrdinal,
|
||||
taskProvenance.projectId,
|
||||
taskProvenance.sourceDigest,
|
||||
taskProvenance.taskId,
|
||||
taskProvenance.taskRevision,
|
||||
taskProvenance.taskMutationId,
|
||||
taskProvenance.taskContentDigest,
|
||||
taskProvenance.triggerCount,
|
||||
taskItemDigest,
|
||||
);
|
||||
publication.appendTask({
|
||||
rowOrdinal: candidate.rowOrdinal,
|
||||
sourceDigest: candidate.sourceDigest,
|
||||
taskContentDigest: task.contentDigest,
|
||||
itemDigest: taskItemDigest,
|
||||
});
|
||||
|
||||
for (const [
|
||||
triggerIndex,
|
||||
@@ -605,13 +724,54 @@ export class LocalSqliteLegacyAdoptionPublisher {
|
||||
trigger.revision,
|
||||
trigger.updatedAtMs,
|
||||
);
|
||||
publication.update('\0trigger\0').update(trigger.contentDigest);
|
||||
const triggerOrdinal = triggerIndex + 1;
|
||||
const triggerProvenance = Object.freeze({
|
||||
adoptionMutationId: input.mutationId,
|
||||
rowOrdinal: candidate.rowOrdinal,
|
||||
triggerOrdinal,
|
||||
projectId: trigger.projectId,
|
||||
taskId: trigger.taskId,
|
||||
taskRevision: trigger.taskRevision,
|
||||
triggerId: trigger.triggerId,
|
||||
triggerRevision: trigger.revision,
|
||||
triggerMutationId: trigger.mutationId,
|
||||
triggerContentDigest: trigger.contentDigest,
|
||||
});
|
||||
const triggerItemDigest =
|
||||
legacyAdoptionTriggerProvenanceDigest(triggerProvenance);
|
||||
client
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3LegacyAdoptionTriggers" (
|
||||
"adoption_mutation_id", "row_ordinal",
|
||||
"trigger_ordinal", "project_id", "task_id",
|
||||
"task_revision", "trigger_id", "trigger_revision",
|
||||
"trigger_mutation_id", "trigger_content_digest",
|
||||
"item_digest"
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
)
|
||||
.run(
|
||||
triggerProvenance.adoptionMutationId,
|
||||
triggerProvenance.rowOrdinal,
|
||||
triggerProvenance.triggerOrdinal,
|
||||
triggerProvenance.projectId,
|
||||
triggerProvenance.taskId,
|
||||
triggerProvenance.taskRevision,
|
||||
triggerProvenance.triggerId,
|
||||
triggerProvenance.triggerRevision,
|
||||
triggerProvenance.triggerMutationId,
|
||||
triggerProvenance.triggerContentDigest,
|
||||
triggerItemDigest,
|
||||
);
|
||||
publication.appendTrigger({
|
||||
triggerContentDigest: trigger.contentDigest,
|
||||
itemDigest: triggerItemDigest,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (adoptedTaskCount + input.skippedCount !== input.rowCount) {
|
||||
throw new LocalLegacyAdoptionConflictError();
|
||||
}
|
||||
const publicationDigest = publication.digest('hex');
|
||||
const publicationDigest = publication.digest();
|
||||
insertAudit(client, audit);
|
||||
client
|
||||
.prepare(
|
||||
@@ -650,6 +810,7 @@ export class LocalSqliteLegacyAdoptionPublisher {
|
||||
)
|
||||
.get(input.mutationId) as Row,
|
||||
);
|
||||
assertStoredProvenance(client, stored);
|
||||
await input.confirmExternalAuthority();
|
||||
client.exec('COMMIT');
|
||||
began = false;
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { createHash, type Hash } from 'node:crypto';
|
||||
|
||||
export interface LegacyAdoptionTaskProvenancePayload {
|
||||
readonly adoptionMutationId: string;
|
||||
readonly rowOrdinal: number;
|
||||
readonly projectId: string;
|
||||
readonly sourceDigest: string;
|
||||
readonly taskId: string;
|
||||
readonly taskRevision: number;
|
||||
readonly taskMutationId: string;
|
||||
readonly taskContentDigest: string;
|
||||
readonly triggerCount: number;
|
||||
}
|
||||
|
||||
export interface LegacyAdoptionTriggerProvenancePayload {
|
||||
readonly adoptionMutationId: string;
|
||||
readonly rowOrdinal: number;
|
||||
readonly triggerOrdinal: number;
|
||||
readonly projectId: string;
|
||||
readonly taskId: string;
|
||||
readonly taskRevision: number;
|
||||
readonly triggerId: string;
|
||||
readonly triggerRevision: number;
|
||||
readonly triggerMutationId: string;
|
||||
readonly triggerContentDigest: string;
|
||||
}
|
||||
|
||||
function itemDigest(
|
||||
domain: 'task' | 'trigger',
|
||||
payload: object,
|
||||
): string {
|
||||
return createHash('sha256')
|
||||
.update(`qinglong3.legacy-adoption-${domain}-provenance.v1\0`)
|
||||
.update(JSON.stringify(payload))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function legacyAdoptionTaskProvenanceDigest(
|
||||
payload: Readonly<LegacyAdoptionTaskProvenancePayload>,
|
||||
): string {
|
||||
return itemDigest('task', payload);
|
||||
}
|
||||
|
||||
export function legacyAdoptionTriggerProvenanceDigest(
|
||||
payload: Readonly<LegacyAdoptionTriggerProvenancePayload>,
|
||||
): string {
|
||||
return itemDigest('trigger', payload);
|
||||
}
|
||||
|
||||
export class LegacyAdoptionPublicationDigest {
|
||||
readonly #hash: Hash;
|
||||
#sealed = false;
|
||||
|
||||
constructor(mutationId: string) {
|
||||
this.#hash = createHash('sha256')
|
||||
.update('qinglong3.legacy-adoption-publication.v2\0')
|
||||
.update(mutationId);
|
||||
}
|
||||
|
||||
appendTask(input: {
|
||||
readonly rowOrdinal: number;
|
||||
readonly sourceDigest: string;
|
||||
readonly taskContentDigest: string;
|
||||
readonly itemDigest: string;
|
||||
}): void {
|
||||
if (this.#sealed) throw new TypeError('Publication digest is sealed');
|
||||
this.#hash
|
||||
.update('\0task\0')
|
||||
.update(String(input.rowOrdinal))
|
||||
.update('\0')
|
||||
.update(input.sourceDigest)
|
||||
.update('\0')
|
||||
.update(input.taskContentDigest)
|
||||
.update('\0')
|
||||
.update(input.itemDigest);
|
||||
}
|
||||
|
||||
appendTrigger(input: {
|
||||
readonly triggerContentDigest: string;
|
||||
readonly itemDigest: string;
|
||||
}): void {
|
||||
if (this.#sealed) throw new TypeError('Publication digest is sealed');
|
||||
this.#hash
|
||||
.update('\0trigger\0')
|
||||
.update(input.triggerContentDigest)
|
||||
.update('\0')
|
||||
.update(input.itemDigest);
|
||||
}
|
||||
|
||||
digest(): string {
|
||||
if (this.#sealed) throw new TypeError('Publication digest is sealed');
|
||||
this.#sealed = true;
|
||||
return this.#hash.digest('hex');
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,8 @@ import { local0097PluginPackageSecretBindingTransitionReceiptsMigration } from '
|
||||
import { local0098CapabilityV49Migration } from '../migrations/0098-capability-v49';
|
||||
import { local0099LegacyDataDirectoryAdoptionsMigration } from '../migrations/0099-legacy-data-directory-adoptions';
|
||||
import { local0100CapabilityV50Migration } from '../migrations/0100-capability-v50';
|
||||
import { local0101LegacyAdoptionProvenanceMigration } from '../migrations/0101-legacy-adoption-provenance';
|
||||
import { local0102CapabilityV51Migration } from '../migrations/0102-capability-v51';
|
||||
import type { LocalSqliteMigrationContext } from '../migrations/sqlMigration';
|
||||
import {
|
||||
LOCAL_SQLITE_MIGRATION_STREAM_ID,
|
||||
@@ -232,6 +234,8 @@ export const localSqliteMigrationDefinition: MigrationStreamDefinition<LocalSqli
|
||||
local0098CapabilityV49Migration,
|
||||
local0099LegacyDataDirectoryAdoptionsMigration,
|
||||
local0100CapabilityV50Migration,
|
||||
local0101LegacyAdoptionProvenanceMigration,
|
||||
local0102CapabilityV51Migration,
|
||||
]),
|
||||
});
|
||||
|
||||
|
||||
@@ -512,5 +512,15 @@ export const localSqliteMigrationManifest: MigrationStreamManifest =
|
||||
checksum:
|
||||
'ea4ef39fe237d8db032da89c8f79dfda631b7201d1e5ac8c743b67e75aad5b07',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0101-legacy-adoption-provenance',
|
||||
checksum:
|
||||
'9c58cf7bc11d26307bfc2c7cc4a587a51f08f498d0b5e1c0c97d9357175d1ed9',
|
||||
}),
|
||||
Object.freeze({
|
||||
id: '0102-capability-v51',
|
||||
checksum:
|
||||
'539f9d60b41b7cfebb203888804329241c178b1ee0105d14949750856260ac1f',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const local0101LegacyAdoptionProvenanceMigration =
|
||||
defineLocalSqliteMigration({
|
||||
id: '0101-legacy-adoption-provenance',
|
||||
statements: [
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_legacy_adoptions_mutation_project_uidx"
|
||||
ON "QingLong3LegacyAdoptions" ("mutation_id", "project_id")
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3LegacyAdoptionTasks" (
|
||||
"adoption_mutation_id" TEXT NOT NULL,
|
||||
"row_ordinal" INTEGER NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"source_digest" TEXT NOT NULL,
|
||||
"task_id" TEXT NOT NULL,
|
||||
"task_revision" INTEGER NOT NULL,
|
||||
"task_mutation_id" TEXT NOT NULL,
|
||||
"task_content_digest" TEXT NOT NULL,
|
||||
"trigger_count" INTEGER NOT NULL,
|
||||
"item_digest" TEXT NOT NULL,
|
||||
PRIMARY KEY ("adoption_mutation_id", "row_ordinal"),
|
||||
CONSTRAINT ql3_legacy_adoption_task_parent_fk
|
||||
FOREIGN KEY ("adoption_mutation_id", "project_id")
|
||||
REFERENCES "QingLong3LegacyAdoptions" ("mutation_id", "project_id")
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT
|
||||
DEFERRABLE INITIALLY DEFERRED,
|
||||
CONSTRAINT ql3_legacy_adoption_task_revision_fk
|
||||
FOREIGN KEY ("project_id", "task_id", "task_revision")
|
||||
REFERENCES "QingLong3TaskDefinitionRevisions" (
|
||||
"project_id", "task_id", "revision"
|
||||
)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_legacy_adoption_task_mutation_fk
|
||||
FOREIGN KEY ("task_mutation_id")
|
||||
REFERENCES "QingLong3TaskDefinitionRevisions" ("mutation_id")
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_legacy_adoption_task_identity_check CHECK (
|
||||
"row_ordinal" BETWEEN 1 AND 100000 AND
|
||||
length("project_id") BETWEEN 1 AND 128 AND
|
||||
length("task_id") BETWEEN 1 AND 128 AND
|
||||
"task_revision" = 1 AND
|
||||
length("task_mutation_id") = 36 AND
|
||||
replace("task_mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*' AND
|
||||
"trigger_count" BETWEEN 0 AND 500000
|
||||
),
|
||||
CONSTRAINT ql3_legacy_adoption_task_digest_check CHECK (
|
||||
length("source_digest") = 64 AND
|
||||
"source_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||
length("task_content_digest") = 64 AND
|
||||
"task_content_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||
length("item_digest") = 64 AND
|
||||
"item_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_legacy_adoption_tasks_project_task_uidx"
|
||||
ON "QingLong3LegacyAdoptionTasks" ("project_id", "task_id")
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_legacy_adoption_tasks_mutation_uidx"
|
||||
ON "QingLong3LegacyAdoptionTasks" ("task_mutation_id")
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_legacy_adoption_tasks_item_uidx"
|
||||
ON "QingLong3LegacyAdoptionTasks" ("item_digest")
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_legacy_adoption_tasks_identity_uidx"
|
||||
ON "QingLong3LegacyAdoptionTasks" (
|
||||
"adoption_mutation_id", "row_ordinal", "project_id", "task_id",
|
||||
"task_revision"
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE TABLE "QingLong3LegacyAdoptionTriggers" (
|
||||
"adoption_mutation_id" TEXT NOT NULL,
|
||||
"row_ordinal" INTEGER NOT NULL,
|
||||
"trigger_ordinal" INTEGER NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
"task_id" TEXT NOT NULL,
|
||||
"task_revision" INTEGER NOT NULL,
|
||||
"trigger_id" TEXT NOT NULL,
|
||||
"trigger_revision" INTEGER NOT NULL,
|
||||
"trigger_mutation_id" TEXT NOT NULL,
|
||||
"trigger_content_digest" TEXT NOT NULL,
|
||||
"item_digest" TEXT NOT NULL,
|
||||
PRIMARY KEY (
|
||||
"adoption_mutation_id", "row_ordinal", "trigger_ordinal"
|
||||
),
|
||||
CONSTRAINT ql3_legacy_adoption_trigger_parent_fk
|
||||
FOREIGN KEY (
|
||||
"adoption_mutation_id", "row_ordinal", "project_id", "task_id",
|
||||
"task_revision"
|
||||
)
|
||||
REFERENCES "QingLong3LegacyAdoptionTasks" (
|
||||
"adoption_mutation_id", "row_ordinal", "project_id", "task_id",
|
||||
"task_revision"
|
||||
)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_legacy_adoption_trigger_revision_fk
|
||||
FOREIGN KEY ("project_id", "trigger_id", "trigger_revision")
|
||||
REFERENCES "QingLong3TriggerRevisions" (
|
||||
"project_id", "trigger_id", "revision"
|
||||
)
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_legacy_adoption_trigger_mutation_fk
|
||||
FOREIGN KEY ("trigger_mutation_id")
|
||||
REFERENCES "QingLong3TriggerRevisions" ("mutation_id")
|
||||
ON DELETE RESTRICT ON UPDATE RESTRICT,
|
||||
CONSTRAINT ql3_legacy_adoption_trigger_identity_check CHECK (
|
||||
"row_ordinal" BETWEEN 1 AND 100000 AND
|
||||
"trigger_ordinal" BETWEEN 1 AND 500000 AND
|
||||
length("project_id") BETWEEN 1 AND 128 AND
|
||||
length("task_id") BETWEEN 1 AND 128 AND
|
||||
"task_revision" = 1 AND
|
||||
length("trigger_id") BETWEEN 1 AND 128 AND
|
||||
"trigger_revision" = 1 AND
|
||||
length("trigger_mutation_id") = 36 AND
|
||||
replace("trigger_mutation_id", '-', '') NOT GLOB '*[^0-9a-f]*'
|
||||
),
|
||||
CONSTRAINT ql3_legacy_adoption_trigger_digest_check CHECK (
|
||||
length("trigger_content_digest") = 64 AND
|
||||
"trigger_content_digest" NOT GLOB '*[^0-9a-f]*' AND
|
||||
length("item_digest") = 64 AND
|
||||
"item_digest" NOT GLOB '*[^0-9a-f]*'
|
||||
)
|
||||
)
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_legacy_adoption_triggers_project_trigger_uidx"
|
||||
ON "QingLong3LegacyAdoptionTriggers" ("project_id", "trigger_id")
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_legacy_adoption_triggers_mutation_uidx"
|
||||
ON "QingLong3LegacyAdoptionTriggers" ("trigger_mutation_id")
|
||||
`,
|
||||
`
|
||||
CREATE UNIQUE INDEX "ql3_legacy_adoption_triggers_item_uidx"
|
||||
ON "QingLong3LegacyAdoptionTriggers" ("item_digest")
|
||||
`,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CAPABILITIES_V50 } from './0100-capability-v50';
|
||||
import { defineLocalSqliteMigration } from './sqlMigration';
|
||||
|
||||
export const CAPABILITIES_V51 = CAPABILITIES_V50.replace(
|
||||
'"legacy_adoption_ledger":1,',
|
||||
'"legacy_adoption_ledger":1,"legacy_adoption_provenance":1,',
|
||||
);
|
||||
|
||||
export const local0102CapabilityV51Migration = defineLocalSqliteMigration({
|
||||
id: '0102-capability-v51',
|
||||
statements: [
|
||||
`UPDATE "QingLong3SchemaCapabilities" SET contract_version = 51, migration_id = '0101-legacy-adoption-provenance', capabilities = '${CAPABILITIES_V51}', updated_at_ms = CAST(unixepoch('subsec') * 1000 AS INTEGER) WHERE contract_name = 'local-control-core' AND contract_version = 50 AND migration_id = '0099-legacy-data-directory-adoptions' AND capabilities = '${CAPABILITIES_V50}'`,
|
||||
],
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '../run/stepRunSchemaContract';
|
||||
|
||||
export const LOCAL_SQLITE_CONTRACT_NAME = 'local-control-core';
|
||||
export const LOCAL_SQLITE_CONTRACT_VERSION = 50;
|
||||
export const LOCAL_SQLITE_CONTRACT_VERSION = 51;
|
||||
|
||||
const LEGACY_DATA_DIRECTORY_ADOPTION_TRIGGERS = Object.freeze([
|
||||
Object.freeze({
|
||||
@@ -1395,9 +1395,50 @@ const REQUIRED_SCHEMA = Object.freeze({
|
||||
]),
|
||||
indexes: Object.freeze([
|
||||
'ql3_legacy_adoptions_decision_uidx',
|
||||
'ql3_legacy_adoptions_mutation_project_uidx',
|
||||
'ql3_legacy_adoptions_project_time_idx',
|
||||
]),
|
||||
}),
|
||||
QingLong3LegacyAdoptionTasks: Object.freeze({
|
||||
columns: Object.freeze([
|
||||
'adoption_mutation_id',
|
||||
'row_ordinal',
|
||||
'project_id',
|
||||
'source_digest',
|
||||
'task_id',
|
||||
'task_revision',
|
||||
'task_mutation_id',
|
||||
'task_content_digest',
|
||||
'trigger_count',
|
||||
'item_digest',
|
||||
]),
|
||||
indexes: Object.freeze([
|
||||
'ql3_legacy_adoption_tasks_project_task_uidx',
|
||||
'ql3_legacy_adoption_tasks_mutation_uidx',
|
||||
'ql3_legacy_adoption_tasks_item_uidx',
|
||||
'ql3_legacy_adoption_tasks_identity_uidx',
|
||||
]),
|
||||
}),
|
||||
QingLong3LegacyAdoptionTriggers: Object.freeze({
|
||||
columns: Object.freeze([
|
||||
'adoption_mutation_id',
|
||||
'row_ordinal',
|
||||
'trigger_ordinal',
|
||||
'project_id',
|
||||
'task_id',
|
||||
'task_revision',
|
||||
'trigger_id',
|
||||
'trigger_revision',
|
||||
'trigger_mutation_id',
|
||||
'trigger_content_digest',
|
||||
'item_digest',
|
||||
]),
|
||||
indexes: Object.freeze([
|
||||
'ql3_legacy_adoption_triggers_project_trigger_uidx',
|
||||
'ql3_legacy_adoption_triggers_mutation_uidx',
|
||||
'ql3_legacy_adoption_triggers_item_uidx',
|
||||
]),
|
||||
}),
|
||||
QingLong3LegacyDataDirectoryAdoptions: Object.freeze({
|
||||
columns: Object.freeze([
|
||||
'mutation_id',
|
||||
@@ -2658,10 +2699,10 @@ export async function auditLocalSqliteReadiness(
|
||||
!capability ||
|
||||
capability.contract_name !== LOCAL_SQLITE_CONTRACT_NAME ||
|
||||
capability.contract_version !== LOCAL_SQLITE_CONTRACT_VERSION ||
|
||||
capability.migration_id !== '0099-legacy-data-directory-adoptions' ||
|
||||
capability.migration_id !== '0101-legacy-adoption-provenance' ||
|
||||
typeof capability.capabilities !== 'string' ||
|
||||
capability.capabilities !==
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"legacy_data_directory_adoption":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_binding_transition_receipt":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
|
||||
'{"run_core":1,"run_retry_policy":1,"completion_receipt_journal":1,"local_dispatch_plan":1,"local_secret_envelope":1,"local_project_policy":1,"local_project_administration":1,"local_security_audit":1,"local_security_audit_compaction":1,"local_secret_authorized_mutation":1,"local_identity":1,"local_api_credential":1,"local_identity_provisioning":1,"local_identity_credential_administration":1,"local_owner_bootstrap":1,"local_owner_delivery_acknowledgement":1,"api_credential_pepper_binding":1,"local_owner_pepper_catalog":1,"local_owner_credential_recovery":1,"local_owner_pepper_reference_inspection":1,"local_owner_pepper_material_gc":1,"local_owner_delivery_acknowledgement_gc":1,"task_definition":1,"local_execution_revision_digest":1,"trigger_definition":1,"legacy_adoption_ledger":1,"legacy_adoption_provenance":1,"legacy_data_directory_adoption":1,"local_scheduler_admission":1,"plugin_package_install":1,"approved_action":1,"plugin_package_admission":1,"approved_action_execution":1,"plugin_package_proposal":1,"plugin_package_materialized_revision":1,"plugin_package_secret_binding":1,"plugin_package_secret_binding_transition":1,"plugin_package_secret_binding_transition_receipt":1,"plugin_package_secret_materialization":1,"plugin_package_task_reconciliation":1,"project_tool_definition_snapshot":1,"step_run":1,"tool_execution_evidence":1,"tool_execution_start_barrier":1,"tool_invocation_artifact":1,"tool_execution_artifact_binding":1,"tool_execution_completion":1,"tool_execution_failure_completion":1,"tool_result_key_catalog":1,"tool_result_rekey":1,"plugin_package_quarantine":1,"plugin_package_lifecycle":1,"plugin_package_automation_publication":1,"plugin_package_automation_security_withdrawal":1,"plugin_package_workflow_admission":1,"plugin_package_workflow_run_list":1,"run_attempt_log_retention":1,"plugin_package_workflow_task_attempt_admission":1}' ||
|
||||
typeof capability.updated_at_ms !== 'number' ||
|
||||
!Number.isSafeInteger(capability.updated_at_ms) ||
|
||||
capability.updated_at_ms < 0
|
||||
|
||||
@@ -3320,6 +3320,10 @@ export const legacyAdoptions = sqliteTable(
|
||||
),
|
||||
check('ql3_legacy_adoptions_created_check', sql`${table.createdAtMs} >= 0`),
|
||||
uniqueIndex('ql3_legacy_adoptions_decision_uidx').on(table.decisionId),
|
||||
uniqueIndex('ql3_legacy_adoptions_mutation_project_uidx').on(
|
||||
table.mutationId,
|
||||
table.projectId,
|
||||
),
|
||||
index('ql3_legacy_adoptions_project_time_idx').on(
|
||||
table.projectId,
|
||||
sql`${table.createdAtMs} desc`,
|
||||
@@ -3328,6 +3332,146 @@ export const legacyAdoptions = sqliteTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const legacyAdoptionTasks = sqliteTable(
|
||||
'QingLong3LegacyAdoptionTasks',
|
||||
{
|
||||
adoptionMutationId: text('adoption_mutation_id').notNull(),
|
||||
rowOrdinal: integer('row_ordinal').notNull(),
|
||||
projectId: text('project_id').notNull(),
|
||||
sourceDigest: text('source_digest').notNull(),
|
||||
taskId: text('task_id').notNull(),
|
||||
taskRevision: integer('task_revision').notNull(),
|
||||
taskMutationId: text('task_mutation_id').notNull(),
|
||||
taskContentDigest: text('task_content_digest').notNull(),
|
||||
triggerCount: integer('trigger_count').notNull(),
|
||||
itemDigest: text('item_digest').notNull(),
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({ columns: [table.adoptionMutationId, table.rowOrdinal] }),
|
||||
foreignKey({
|
||||
columns: [table.adoptionMutationId, table.projectId],
|
||||
foreignColumns: [legacyAdoptions.mutationId, legacyAdoptions.projectId],
|
||||
})
|
||||
.onDelete('restrict')
|
||||
.onUpdate('restrict'),
|
||||
foreignKey({
|
||||
columns: [table.projectId, table.taskId, table.taskRevision],
|
||||
foreignColumns: [
|
||||
taskDefinitionRevisions.projectId,
|
||||
taskDefinitionRevisions.taskId,
|
||||
taskDefinitionRevisions.revision,
|
||||
],
|
||||
})
|
||||
.onDelete('restrict')
|
||||
.onUpdate('restrict'),
|
||||
foreignKey({
|
||||
columns: [table.taskMutationId],
|
||||
foreignColumns: [taskDefinitionRevisions.mutationId],
|
||||
})
|
||||
.onDelete('restrict')
|
||||
.onUpdate('restrict'),
|
||||
check(
|
||||
'ql3_legacy_adoption_task_identity_check',
|
||||
sql`${table.rowOrdinal} between 1 and 100000 and length(${table.projectId}) between 1 and 128 and length(${table.taskId}) between 1 and 128 and ${table.taskRevision} = 1 and length(${table.taskMutationId}) = 36 and replace(${table.taskMutationId}, '-', '') not glob '*[^0-9a-f]*' and ${table.triggerCount} between 0 and 500000`,
|
||||
),
|
||||
check(
|
||||
'ql3_legacy_adoption_task_digest_check',
|
||||
sql`length(${table.sourceDigest}) = 64 and ${table.sourceDigest} not glob '*[^0-9a-f]*' and length(${table.taskContentDigest}) = 64 and ${table.taskContentDigest} not glob '*[^0-9a-f]*' and length(${table.itemDigest}) = 64 and ${table.itemDigest} not glob '*[^0-9a-f]*'`,
|
||||
),
|
||||
uniqueIndex('ql3_legacy_adoption_tasks_project_task_uidx').on(
|
||||
table.projectId,
|
||||
table.taskId,
|
||||
),
|
||||
uniqueIndex('ql3_legacy_adoption_tasks_mutation_uidx').on(
|
||||
table.taskMutationId,
|
||||
),
|
||||
uniqueIndex('ql3_legacy_adoption_tasks_item_uidx').on(table.itemDigest),
|
||||
uniqueIndex('ql3_legacy_adoption_tasks_identity_uidx').on(
|
||||
table.adoptionMutationId,
|
||||
table.rowOrdinal,
|
||||
table.projectId,
|
||||
table.taskId,
|
||||
table.taskRevision,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
export const legacyAdoptionTriggers = sqliteTable(
|
||||
'QingLong3LegacyAdoptionTriggers',
|
||||
{
|
||||
adoptionMutationId: text('adoption_mutation_id').notNull(),
|
||||
rowOrdinal: integer('row_ordinal').notNull(),
|
||||
triggerOrdinal: integer('trigger_ordinal').notNull(),
|
||||
projectId: text('project_id').notNull(),
|
||||
taskId: text('task_id').notNull(),
|
||||
taskRevision: integer('task_revision').notNull(),
|
||||
triggerId: text('trigger_id').notNull(),
|
||||
triggerRevision: integer('trigger_revision').notNull(),
|
||||
triggerMutationId: text('trigger_mutation_id').notNull(),
|
||||
triggerContentDigest: text('trigger_content_digest').notNull(),
|
||||
itemDigest: text('item_digest').notNull(),
|
||||
},
|
||||
(table) => [
|
||||
primaryKey({
|
||||
columns: [
|
||||
table.adoptionMutationId,
|
||||
table.rowOrdinal,
|
||||
table.triggerOrdinal,
|
||||
],
|
||||
}),
|
||||
foreignKey({
|
||||
columns: [
|
||||
table.adoptionMutationId,
|
||||
table.rowOrdinal,
|
||||
table.projectId,
|
||||
table.taskId,
|
||||
table.taskRevision,
|
||||
],
|
||||
foreignColumns: [
|
||||
legacyAdoptionTasks.adoptionMutationId,
|
||||
legacyAdoptionTasks.rowOrdinal,
|
||||
legacyAdoptionTasks.projectId,
|
||||
legacyAdoptionTasks.taskId,
|
||||
legacyAdoptionTasks.taskRevision,
|
||||
],
|
||||
})
|
||||
.onDelete('restrict')
|
||||
.onUpdate('restrict'),
|
||||
foreignKey({
|
||||
columns: [table.projectId, table.triggerId, table.triggerRevision],
|
||||
foreignColumns: [
|
||||
triggerRevisions.projectId,
|
||||
triggerRevisions.triggerId,
|
||||
triggerRevisions.revision,
|
||||
],
|
||||
})
|
||||
.onDelete('restrict')
|
||||
.onUpdate('restrict'),
|
||||
foreignKey({
|
||||
columns: [table.triggerMutationId],
|
||||
foreignColumns: [triggerRevisions.mutationId],
|
||||
})
|
||||
.onDelete('restrict')
|
||||
.onUpdate('restrict'),
|
||||
check(
|
||||
'ql3_legacy_adoption_trigger_identity_check',
|
||||
sql`${table.rowOrdinal} between 1 and 100000 and ${table.triggerOrdinal} between 1 and 500000 and length(${table.projectId}) between 1 and 128 and length(${table.taskId}) between 1 and 128 and ${table.taskRevision} = 1 and length(${table.triggerId}) between 1 and 128 and ${table.triggerRevision} = 1 and length(${table.triggerMutationId}) = 36 and replace(${table.triggerMutationId}, '-', '') not glob '*[^0-9a-f]*'`,
|
||||
),
|
||||
check(
|
||||
'ql3_legacy_adoption_trigger_digest_check',
|
||||
sql`length(${table.triggerContentDigest}) = 64 and ${table.triggerContentDigest} not glob '*[^0-9a-f]*' and length(${table.itemDigest}) = 64 and ${table.itemDigest} not glob '*[^0-9a-f]*'`,
|
||||
),
|
||||
uniqueIndex('ql3_legacy_adoption_triggers_project_trigger_uidx').on(
|
||||
table.projectId,
|
||||
table.triggerId,
|
||||
),
|
||||
uniqueIndex('ql3_legacy_adoption_triggers_mutation_uidx').on(
|
||||
table.triggerMutationId,
|
||||
),
|
||||
uniqueIndex('ql3_legacy_adoption_triggers_item_uidx').on(table.itemDigest),
|
||||
],
|
||||
);
|
||||
|
||||
export const legacyDataDirectoryAdoptions = sqliteTable(
|
||||
'QingLong3LegacyDataDirectoryAdoptions',
|
||||
{
|
||||
@@ -5180,6 +5324,8 @@ export const localSqliteSchema = Object.freeze({
|
||||
toolExecutionResultRekeyHeads,
|
||||
toolResultKeyRetirementReceipts,
|
||||
legacyAdoptions,
|
||||
legacyAdoptionTasks,
|
||||
legacyAdoptionTriggers,
|
||||
legacyDataDirectoryAdoptions,
|
||||
legacyDataDirectoryAdoptionSecrets,
|
||||
localIdentitySubjects,
|
||||
|
||||
Reference in New Issue
Block a user