Skip to content

Commit ec9a3b4

Browse files
authored
feat: add maintenance history endpoint (#1318)
* feat: read check evidence through the control API People with view.evidence on a check's model can read the sample rows stored with one of its results. Columns are the rows' keys in sorted order, the order the warehouse stores them in, and a row without a key holds null there. Every read is audited, and a read whose audit cannot be written discloses nothing. The SQL and evidence reads now share one authorize, read and audit path. * feat: read maintenance history through the control API People with view on a target can read its recorded model moves, every step of their old-name views, and janitor cleanups, newest moves first. Filtering by model returns only moves into that model and their views, using new indexes on the destination model and the move's event. Check and maintenance reads now live in their own warehouse-history domain, apart from metadata ingestion. * test: keep the worker contract's scheduler lease alive The worker contract held one 30 second scheduler lease without renewing it, so on a slow runner its second claim came after expiry and was fenced. Renew it in the background, as a running scheduler does.
1 parent bb756d6 commit ec9a3b4

30 files changed

Lines changed: 921 additions & 292 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
{
2+
"migrations": [
3+
{
4+
"event_id": "move-orders-1",
5+
"destination_model": "orders",
6+
"origin_model": "orders_v1",
7+
"origin_name": "orders_v1",
8+
"decision": "migrate",
9+
"created_at": "2026-02-01T04:05:06Z"
10+
},
11+
{
12+
"event_id": "move-orders-2",
13+
"destination_model": "orders",
14+
"origin_model": null,
15+
"origin_name": "orders_🧾",
16+
"decision": "renamed",
17+
"discovery": "automatic",
18+
"created_at": "2026-02-03T04:05:06Z"
19+
},
20+
{
21+
"event_id": "move-customers-1",
22+
"destination_model": "customers",
23+
"origin_model": "clients",
24+
"origin_name": "clients",
25+
"decision": "superseded_replace",
26+
"created_at": "2026-02-02T04:05:06Z"
27+
}
28+
],
29+
"old_name_views": [
30+
{
31+
"event_id": "view-orders-1",
32+
"migration_event_id": "move-orders-2",
33+
"destination_model": "orders",
34+
"event_type": "required",
35+
"created_at": "2026-02-03T04:05:06Z"
36+
},
37+
{
38+
"event_id": "view-orders-2",
39+
"migration_event_id": "move-orders-2",
40+
"destination_model": "orders",
41+
"event_type": "view_created",
42+
"created_at": "2026-02-03T04:05:07Z",
43+
"column_aliases": "{\"order_id\":\"legacy_order_id\"}"
44+
},
45+
{
46+
"event_id": "view-customers-1",
47+
"migration_event_id": "move-customers-1",
48+
"destination_model": "customers",
49+
"event_type": "required",
50+
"created_at": "2026-02-02T04:05:06Z"
51+
},
52+
{
53+
"event_id": "view-orders-old-1",
54+
"migration_event_id": "move-orders-1",
55+
"destination_model": "orders",
56+
"event_type": "required",
57+
"created_at": "2026-02-01T04:05:06Z"
58+
}
59+
],
60+
"janitor_events": [
61+
{
62+
"event_id": "cleanup-1",
63+
"event_type": "archive",
64+
"occurred_at": "2026-02-01T05:05:06Z"
65+
},
66+
{
67+
"event_id": "cleanup-2",
68+
"event_type": "delete",
69+
"occurred_at": "2026-02-04T05:05:06Z"
70+
}
71+
],
72+
"preview_old_name_views": [
73+
{
74+
"event_id": "view-preview-1",
75+
"migration_event_id": "move-orders-2",
76+
"destination_model": "orders",
77+
"event_type": "view_dropped",
78+
"created_at": "2026-02-05T04:05:06Z"
79+
}
80+
]
81+
}

‎clients/typescript/src/schema.d.ts‎

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,22 @@ export interface paths {
322322
patch?: never;
323323
trace?: never;
324324
};
325+
"/api/control/locations/{location_id}/targets/{target}/maintenance": {
326+
parameters: {
327+
query?: never;
328+
header?: never;
329+
path?: never;
330+
cookie?: never;
331+
};
332+
get: operations["readPersonMaintenance"];
333+
put?: never;
334+
post?: never;
335+
delete?: never;
336+
options?: never;
337+
head?: never;
338+
patch?: never;
339+
trace?: never;
340+
};
325341
"/api/control/locations/{location_id}/targets/{target}/node-status": {
326342
parameters: {
327343
query?: never;
@@ -1121,6 +1137,14 @@ export interface components {
11211137
/** Format: uuid */
11221138
session_id: string;
11231139
};
1140+
/** @description Recorded moves, their old-name view steps and janitor cleanups, newest moves first. */
1141+
MaintenanceHistory: {
1142+
/** @description Empty when the history is for one model; cleanups are not tied to models. */
1143+
janitor_events: components["schemas"]["WarehouseJanitorRecord"][];
1144+
migrations: components["schemas"]["WarehouseMigrationRecord"][];
1145+
/** @description Every step recorded for the listed moves, oldest first; same-time steps are not in step order. */
1146+
old_name_views: components["schemas"]["WarehouseOldNameRecord"][];
1147+
};
11241148
/** @description One bounded page of one state table from the registered reader of a published generation. */
11251149
MetadataBatch: {
11261150
/** Format: int64 */
@@ -1731,12 +1755,81 @@ export interface components {
17311755
* @enum {string}
17321756
*/
17331757
WarehouseHistoryKind: "fingerprints" | "source_freshness" | "audit_results" | "janitor_events" | "migrations" | "old_name_views" | "microbatches" | "catalog" | "statistics";
1758+
/** @description Warehouse cleanup facts do not authorize control-plane deletion or worker recovery. */
1759+
WarehouseJanitorRecord: {
1760+
archive_name: string;
1761+
archive_qualified_name: string;
1762+
/** Format: date-time */
1763+
archived_at: string;
1764+
event_id: string;
1765+
event_type: string;
1766+
/** Format: date-time */
1767+
occurred_at: string;
1768+
original_name?: string | null;
1769+
original_qualified_name?: string | null;
1770+
relation_database?: string | null;
1771+
relation_schema: string;
1772+
relation_type?: string | null;
1773+
run_id: string;
1774+
/** Format: int64 */
1775+
schema_version: number;
1776+
state_database?: string | null;
1777+
state_schema: string;
1778+
};
1779+
/** @description Model-move history is evidence, not authority to migrate a warehouse relation. */
1780+
WarehouseMigrationRecord: {
1781+
/** Format: date-time */
1782+
created_at: string;
1783+
decision: string;
1784+
destination_database?: string | null;
1785+
destination_model: string;
1786+
destination_name: string;
1787+
destination_schema: string;
1788+
discovery: string;
1789+
event_id: string;
1790+
origin_database?: string | null;
1791+
origin_model?: string | null;
1792+
origin_name: string;
1793+
origin_schema?: string | null;
1794+
origin_version_hash: string;
1795+
run_id: string;
1796+
state_database?: string | null;
1797+
state_schema: string;
1798+
target_name?: string | null;
1799+
};
17341800
WarehouseMirrorCursor: {
17351801
/** Format: uuid */
17361802
event_id: string;
17371803
/** Format: date-time */
17381804
observed_at: string;
17391805
};
1806+
/** @description Preserve nullable compatibility-view facts without projecting live warehouse existence. */
1807+
WarehouseOldNameRecord: {
1808+
archive_name?: string | null;
1809+
column_aliases?: string | null;
1810+
/** Format: date-time */
1811+
created_at: string;
1812+
destination_model: string;
1813+
drop_reason?: string | null;
1814+
event_id: string;
1815+
event_type: string;
1816+
/** Format: date-time */
1817+
expires_at?: string | null;
1818+
grants_copied?: string | null;
1819+
migration_event_id: string;
1820+
new_database?: string | null;
1821+
new_name: string;
1822+
new_schema: string;
1823+
old_database?: string | null;
1824+
old_name: string;
1825+
old_schema?: string | null;
1826+
run_id: string;
1827+
state_database?: string | null;
1828+
state_schema: string;
1829+
target_name?: string | null;
1830+
view_retention?: string | null;
1831+
view_sql?: string | null;
1832+
};
17401833
/** @description Physical namespace of SQLBuild's source-freshness state table, never a source query. */
17411834
WarehouseStateLocation: {
17421835
database?: string | null;
@@ -2547,6 +2640,48 @@ export interface operations {
25472640
};
25482641
};
25492642
};
2643+
readPersonMaintenance: {
2644+
parameters: {
2645+
query?: {
2646+
/** @description Only moves into this model, and no janitor events */
2647+
node?: string;
2648+
/** @description Maximum moves and janitor events each; defaults to 100, capped at 1000 */
2649+
limit?: number;
2650+
};
2651+
header?: never;
2652+
path: {
2653+
location_id: string;
2654+
target: string;
2655+
};
2656+
cookie?: never;
2657+
};
2658+
requestBody?: never;
2659+
responses: {
2660+
200: {
2661+
headers: {
2662+
[name: string]: unknown;
2663+
};
2664+
content: {
2665+
"application/json": components["schemas"]["MaintenanceHistory"];
2666+
};
2667+
};
2668+
/** @description Session rejected */
2669+
401: {
2670+
headers: {
2671+
[name: string]: unknown;
2672+
};
2673+
content?: never;
2674+
};
2675+
403: {
2676+
headers: {
2677+
[name: string]: unknown;
2678+
};
2679+
content: {
2680+
"application/json": components["schemas"]["ApiErrorBody"];
2681+
};
2682+
};
2683+
};
2684+
};
25502685
readPersonNodeStatus: {
25512686
parameters: {
25522687
query: {

‎crates/sqlbuild-orchestration/src/api/_helpers/authentication/password_sessions.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ pub(crate) fn person_routes() -> Router<ControlStore> {
125125
"/api/control/locations/{location_id}/targets/{target}/checks/{binding_key}/results/{event_id}/evidence",
126126
get(crate::api::_helpers::resources::person_checks::evidence),
127127
)
128+
.route(
129+
"/api/control/locations/{location_id}/targets/{target}/maintenance",
130+
get(crate::api::_helpers::resources::person_maintenance::read),
131+
)
128132
.layer(DefaultBodyLimit::max(PASSWORD_BODY_BYTES))
129133
}
130134

‎crates/sqlbuild-orchestration/src/api/_helpers/resources/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub(crate) mod location_health;
77
pub(crate) mod location_work;
88
pub(crate) mod person_checks;
99
pub(crate) mod person_location_work;
10+
pub(crate) mod person_maintenance;
1011
pub(crate) mod person_node_sql;
1112
pub(crate) mod person_operations;
1213
pub(crate) mod router;

‎crates/sqlbuild-orchestration/src/api/_helpers/resources/person_checks.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ use crate::api::errors::ApiError;
88
use crate::api::models::{CheckResultPath, ReadQuery};
99
use crate::credentials::models::{AuditContext, AuthenticatedPerson, DefaultRole};
1010
use crate::store::models::ControlStore;
11-
use crate::warehouse_metadata::main::read_person_check::read_person_check;
12-
use crate::warehouse_metadata::main::read_person_check_evidence::read_person_check_evidence;
13-
use crate::warehouse_metadata::main::read_person_check_sql::read_person_check_sql;
14-
use crate::warehouse_metadata::main::read_person_checks::read_person_checks;
15-
use crate::warehouse_metadata::models::{CheckEvidence, CheckResult, CheckSql};
11+
use crate::warehouse_history::main::read_person_check::read_person_check;
12+
use crate::warehouse_history::main::read_person_check_evidence::read_person_check_evidence;
13+
use crate::warehouse_history::main::read_person_check_sql::read_person_check_sql;
14+
use crate::warehouse_history::main::read_person_checks::read_person_checks;
15+
use crate::warehouse_history::models::{CheckEvidence, CheckResult, CheckSql};
1616

1717
#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/checks", operation_id = "listPersonChecks",
1818
params(("location_id" = Uuid, Path), ("target" = String, Path)),
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//! Maintenance history reads use live person grants over the whole target.
2+
3+
use axum::extract::{Path, Query, State};
4+
use axum::{Extension, Json};
5+
use uuid::Uuid;
6+
7+
use crate::api::errors::ApiError;
8+
use crate::credentials::models::{AuditContext, AuthenticatedPerson, DefaultRole};
9+
use crate::store::models::ControlStore;
10+
use crate::warehouse_history::main::read_person_maintenance::read_person_maintenance;
11+
use crate::warehouse_history::models::{MaintenanceHistory, MaintenanceQuery};
12+
13+
#[utoipa::path(get, path = "/api/control/locations/{location_id}/targets/{target}/maintenance", operation_id = "readPersonMaintenance",
14+
params(("location_id" = Uuid, Path), ("target" = String, Path),
15+
("node" = Option<String>, Query, description = "Only moves into this model, and no janitor events"),
16+
("limit" = Option<u32>, Query, description = "Maximum moves and janitor events each; defaults to 100, capped at 1000")),
17+
responses((status = 200, body = MaintenanceHistory), (status = 401, description = "Session rejected"),
18+
(status = 403, body = crate::api::models::ApiErrorBody)), security(("session" = []), ("cliSession" = [])))]
19+
pub(crate) async fn read(
20+
(State(store), Extension(context)): (State<ControlStore>, Extension<AuditContext>),
21+
Extension(default_role): Extension<DefaultRole>,
22+
(Path((location_id, target)), Query(options)): (Path<(Uuid, String)>, Query<MaintenanceQuery>),
23+
Extension(AuthenticatedPerson(principal, _)): Extension<AuthenticatedPerson>,
24+
) -> Result<Json<MaintenanceHistory>, ApiError> {
25+
Ok(Json(
26+
read_person_maintenance(
27+
&store,
28+
(&context, principal),
29+
(location_id, &target, &options),
30+
default_role,
31+
)
32+
.await?,
33+
))
34+
}

‎crates/sqlbuild-orchestration/src/api/models.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use crate::api::_helpers::authentication::password_sessions;
66
use crate::api::_helpers::authentication::{cli_login, oidc};
77
use crate::api::_helpers::resources::{
88
administrators, audit, changes, location_health, location_work, person_checks,
9-
person_location_work, person_node_sql, person_operations, run_reads, runs, snapshots,
10-
trigger_reads, warehouse_metadata,
9+
person_location_work, person_maintenance, person_node_sql, person_operations, run_reads, runs,
10+
snapshots, trigger_reads, warehouse_metadata,
1111
};
1212

1313
#[derive(Debug, utoipa::OpenApi)]
@@ -28,6 +28,7 @@ use crate::api::_helpers::resources::{
2828
person_checks::history,
2929
person_checks::sql,
3030
person_checks::evidence,
31+
person_maintenance::read,
3132
person_location_work::request,
3233
person_location_work::result,
3334
person_node_sql::read,

‎crates/sqlbuild-orchestration/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub mod scheduler;
1212
pub mod sensors;
1313
pub mod snapshots;
1414
pub mod store;
15+
pub mod warehouse_history;
1516
pub mod warehouse_metadata;
1617
pub mod webhooks;
1718
pub mod worker;

‎crates/sqlbuild-orchestration/src/store/schema.sql‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ CREATE TABLE sqb_warehouse_migrations (
429429
scan_id {uuid}
430430
);
431431
CREATE INDEX sqb_migrations_namespace ON sqb_warehouse_migrations (location_id,target,state_database,state_schema,observed_at,event_id);
432+
CREATE INDEX sqb_migrations_destination ON sqb_warehouse_migrations (location_id,target,destination_model,observed_at,event_id);
432433
CREATE TABLE sqb_warehouse_old_name_views (
433434
event_id {uuid} PRIMARY KEY,
434435
location_id {uuid} NOT NULL REFERENCES sqb_locations(location_id),
@@ -444,6 +445,7 @@ CREATE TABLE sqb_warehouse_old_name_views (
444445
scan_id {uuid}
445446
);
446447
CREATE INDEX sqb_old_name_views_namespace ON sqb_warehouse_old_name_views (location_id,target,state_database,state_schema,observed_at,event_id);
448+
CREATE INDEX sqb_old_name_views_migration ON sqb_warehouse_old_name_views (location_id,target,migration_event_id,observed_at,event_id);
447449
CREATE TABLE sqb_warehouse_microbatches (
448450
event_id {uuid} PRIMARY KEY,
449451
location_id {uuid} NOT NULL REFERENCES sqb_locations(location_id),

crates/sqlbuild-orchestration/src/warehouse_metadata/_helpers/checks.rs renamed to crates/sqlbuild-orchestration/src/warehouse_history/_helpers/checks.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ use crate::credentials::models::{Action, AuditContext, DefaultRole, PermissionSc
1212
use crate::store::errors::ControlError;
1313
use crate::store::main::{commit::commit, query::query, query_records::query_records};
1414
use crate::store::models::{ControlTransaction, Parameter};
15-
use crate::warehouse_metadata::models::{CheckEvidence, WarehouseAuditRecord};
15+
use crate::warehouse_history::models::CheckEvidence;
16+
use crate::warehouse_metadata::models::WarehouseAuditRecord;
1617

1718
#[derive(Deserialize)]
1819
pub(crate) struct AuditRow {

0 commit comments

Comments
 (0)