From 55f0bb9dd1b5f1c8d07643232f82cab2f659066f Mon Sep 17 00:00:00 2001 From: Yvonne Jouffrault Date: Wed, 19 Aug 2026 14:22:54 -0400 Subject: [PATCH 1/7] docs(immutable-history): add a how-to for querying historical data Immutable History had a concept page but no task page, so the `at` parameter was described only as "timestamp parameters for historical queries" with no syntax, accepted formats, or limits anywhere in the published docs. Add `immutable-history/query-historical-data.mdx` covering the web interface, GraphQL, REST, and the Python SDK, plus comparing two arbitrary timestamps with DiffTree. Every fact is taken from the implementation rather than from the interface list on the overview. Make Immutable History a sidebar category with the overview as its hub, and add two concept subsections to the overview: how a temporal query differs from replaying a change log, and how branches share one immutable baseline. Co-Authored-By: Claude Opus 5 --- docs/docs/immutable-history/overview.mdx | 18 +- .../query-historical-data.mdx | 156 ++++++++++++++++++ docs/sidebars.ts | 7 +- 3 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 docs/docs/immutable-history/query-historical-data.mdx diff --git a/docs/docs/immutable-history/overview.mdx b/docs/docs/immutable-history/overview.mdx index 4d037d92dd1..5e18e0d57b8 100644 --- a/docs/docs/immutable-history/overview.mdx +++ b/docs/docs/immutable-history/overview.mdx @@ -48,6 +48,16 @@ This capability extends across all interfaces: - **REST API**: Temporal query support for retrieving historical data - **Python SDK**: Time-aware methods to access past states +For the syntax each one uses, the time formats they accept, and how far back a branch can be read, see [Query historical data](./query-historical-data.mdx). + +### Temporal queries and change logs + +A change log records the operations that were applied. Reading a past state from one means replaying those operations in order, and the result is only as complete as the log. Infrahub stores the resulting state at every point in time, so reading the past is a query against data that is already stored rather than a reconstruction from a record of what happened. + +### Multiple timelines at once + +Branches share the same immutable baseline and record only the values that diverge from it. Creating a branch stores a pointer to a base timestamp rather than a copy of the data, so a branch starts with the full history of its origin available to it and adds only what you change. This is what allows a query to name both a branch and a time: the two together identify one state out of every state the graph has recorded. + ### Attribute-level change tracking Unlike systems that capture entire object snapshots, Infrahub's immutable history operates at the attribute level. This approach offers several advantages: @@ -62,5 +72,9 @@ There is a timestamp associated with every change in the database. This timestam ## Related topics -- [Proposed Changes](../proposed-changes/overview.mdx) -- [Branches](../branches/overview.mdx) +- [Branches](../branches/overview.mdx) — how branches isolate changes, and what merging does to the timeline +- [Proposed Changes](../proposed-changes/overview.mdx) — review and validate a set of changes before they merge + +## In this section + +- [Query historical data](./query-historical-data.mdx) — Read the graph as it stood at any past time, from the web interface, GraphQL, REST, or the Python SDK diff --git a/docs/docs/immutable-history/query-historical-data.mdx b/docs/docs/immutable-history/query-historical-data.mdx new file mode 100644 index 00000000000..cb0d8717cd8 --- /dev/null +++ b/docs/docs/immutable-history/query-historical-data.mdx @@ -0,0 +1,156 @@ +--- +title: Query historical data +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Query historical data + +Every read in Infrahub happens at a point in time. By default that point is now. Add the `at` parameter to a read and Infrahub returns the data as it was at the time you name, on whichever branch you are querying. + +Use `at` when the current state cannot explain the present: a change was merged overnight and a device stopped forwarding traffic, an auditor asks which prefixes were assigned to a site on a given date, or you need to compare a configuration against the one it replaced. + +## Before you start + +Two things determine whether a first attempt returns data: the format of the time you pass, and how far back the branch can be read. + +### Time formats + +`at` accepts an absolute time or an offset from now. + +| Form | Example | Resolves to | +|---|---|---| +| ISO 8601 with a zone or offset | `2026-03-09T14:00:00Z`, `2026-03-09T15:00:00+01:00` | The instant you name | +| ISO 8601 without a zone | `2026-03-09T14:00:00` | The same wall-clock time, read as UTC | +| Date only | `2026-03-09` | 12:00 UTC on that day | +| Offset from now | `30s`, `45m`, `6h`, `2h30m` | That much time before now | + +A date on its own resolves to midday, not midnight. Write the time out when the distinction matters. + +Offsets are limited to seconds, minutes, and hours — `s`, `sec`, `second`, `seconds`, `m`, `min`, `minute`, `minutes`, `h`, `hour`, `hours` — and combine in one string. Days and weeks have no offset form, so `7d` returns a `TimestampFormatError`. For anything beyond a few hours back, pass an absolute time. + +### How far back you can read + +A branch cannot be read from before it existed. On the default branch and the global branch, the earliest readable time is that branch's own creation time. On any other branch, the boundary is the creation time of the branch it came from, because the history before the branch point belongs to the origin branch. + +Ask for something earlier and Infrahub rejects the query: + +```text +Requested time '2026-01-05T00:00:00Z' is before branch 'main' was created at '2026-02-01T09:14:22.481000Z'. +``` + +## Read data at a past time + +Pass `at` alongside the branch you are reading. The examples below all return the state of `ord1-edge1` at the same moment. + + + + +Select the time selector — the calendar and clock icon beside the branch selector — and choose a date and time in UTC. The control has no text label until you set a time, at which point the bar beside it reads **Current view time** with your selection. + +Every object you open while a time is set shows its state at that time. Select the **×** beside the displayed time to return to the present. + +The picker offers past times only. + +![The time selector, with a past time applied](../media/tutorial_2_historical.png) + + + + + +`at` is a query-string parameter on the GraphQL endpoint, so the query itself is unchanged: + +```graphql # Read a device at a past time +# Endpoint : http://localhost:8000/graphql/main?at=2026-03-09T14:00:00Z +query DeviceAtTime { + InfraDevice(name__value: "ord1-edge1") { + edges { + node { + name { value } + description { value } + status { value } + } + } + } +} +``` + +Because `at` is part of the endpoint rather than the query, the same stored query returns current or historical data depending on the URL you send it to. + + + + + +The REST API applies `at` to stored GraphQL queries, artifacts, and transformations rather than to ad-hoc object reads. To read objects at a past time over REST, save the query as a `CoreGraphQLQuery` object and run it by name: + +```bash +curl "http://localhost:8000/api/query/device-status?branch=main&at=2026-03-09T14:00:00Z" \ + -H "X-INFRAHUB-KEY: $INFRAHUB_API_TOKEN" +``` + +The same parameter works on `/api/artifact/{artifact_id}` and on the transformation endpoints, so you can render an artifact from the data as it was rather than from current data. + + + + + +`all()`, `get()`, `filters()`, and `execute_graphql()` each take an `at` argument: + +```python +from infrahub_sdk import InfrahubClientSync +from infrahub_sdk.timestamp import Timestamp + +client = InfrahubClientSync(address="http://localhost:8000") + +device = client.get( + kind="InfraDevice", + name__value="ord1-edge1", + at=Timestamp("2026-03-09T14:00:00Z"), +) + +print(device.description.value) +``` + +`Timestamp` accepts every form in the table above, so `Timestamp("6h")` reads six hours back. + + + + +## Compare two points in time + +To see what changed between two moments rather than the state at one of them, query `DiffTree` with `from_time` and `to_time`. Both take a timestamp, and the two times can be arbitrary — they do not have to correspond to a branch point or a proposed change. + +```graphql # What changed on main between two timestamps +# Endpoint : http://localhost:8000/graphql/main +query ChangesBetween { + DiffTree( + branch: "main" + from_time: "2026-03-09T00:00:00Z" + to_time: "2026-03-10T00:00:00Z" + ) { + num_added + num_updated + num_removed + nodes { + kind + label + status + attributes { + name + action + } + } + } +} +``` + +Omit `from_time` and the comparison starts at the branch's creation; omit `to_time` and it ends at the current time. Add `filters` to restrict the result by kind, namespace, or status, and `limit` and `offset` to page through a large result. For a count without the node detail, query `DiffTreeSummary` with the same arguments. + +Two things to expect from the result. The base of the comparison is always the default branch, so naming a feature branch compares that branch against the default branch across the window, while naming the default branch compares it against itself. And where no diff covering the requested window is available, `DiffTree` returns `null` rather than an empty result. + +## Related + +- [Immutable history](./overview.mdx) — how Infrahub stores every past state, and why a historical read is a query rather than a reconstruction +- [Branches](../branches/overview.mdx) — how branch creation and merging place the timestamps you query against +- [Proposed Changes](../proposed-changes/overview.mdx) — review a diff scoped to a branch, rather than to two times you choose diff --git a/docs/sidebars.ts b/docs/sidebars.ts index c81b46d7c1b..23f90c0d6fc 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -233,7 +233,12 @@ const sidebars: SidebarsConfig = { collapsed: false, link: { type: 'generated-index', slug: 'branches-and-change-control' }, items: [ - { type: 'doc', id: 'immutable-history/overview', label: 'Immutable History' }, + { + type: 'category', + label: 'Immutable History', + link: { type: 'doc', id: 'immutable-history/overview' }, // hub + items: ['immutable-history/query-historical-data'], + }, { type: 'category', label: 'Branches', From 4ffdf4a90e2985a612e5792d76a6410c94090b1c Mon Sep 17 00:00:00 2001 From: Yvonne Jouffrault Date: Wed, 19 Aug 2026 14:39:33 -0400 Subject: [PATCH 2/7] docs(immutable-history): rewrite both pages to explain, not just specify The first draft was mechanics-only. It stated the parameter, the formats and the limits accurately and never said what any of it is for, so a reader arriving without the context could not tell why the capability matters or when to reach for it. Rewrite the overview as a concept page: what Infrahub keeps and why that gap matters in ordinary work, how storing states rather than events makes a past state directly queryable, how that differs from a change log, and how branches act as parallel timelines over the same history. Rewrite the guide so each section leads with the situation it serves -- relative offsets while working backwards from a problem, absolute times for anything two people must agree on, and a reason for each interface rather than four equivalent code samples. The verified behaviour is unchanged; every fact now arrives with why it matters. Co-Authored-By: Claude Opus 5 --- docs/docs/immutable-history/overview.mdx | 116 ++++++++++-------- .../query-historical-data.mdx | 115 ++++++++++++----- 2 files changed, 151 insertions(+), 80 deletions(-) diff --git a/docs/docs/immutable-history/overview.mdx b/docs/docs/immutable-history/overview.mdx index 5e18e0d57b8..be2f7cac594 100644 --- a/docs/docs/immutable-history/overview.mdx +++ b/docs/docs/immutable-history/overview.mdx @@ -2,79 +2,95 @@ title: Immutable history --- -# Understanding immutable history in Infrahub +Infrahub keeps every state your infrastructure data has held. Query the graph as it was at any +point in the past — yesterday's routes, last week's VLAN definitions, the ACL a rollout replaced — +using the same queries you run against current data. -At its foundation, Infrahub implements data immutability—a principle where information in the database cannot be deleted or modified in place. Instead, every change creates a new version while preserving all previous states. This approach mirrors version control systems like Git, providing a robust history of all infrastructure changes. +## Why it matters -This architectural decision provides several critical benefits for infrastructure management: +Debugging a change means comparing what is true now with what was true before it. A data model +that stores only current values answers the first half and loses the second: once a rollout has +overwritten the configuration that worked, the comparison you need is gone. -- **Complete audit trail**: Every modification is permanently recorded with who, what, and when, providing full traceability -- **Time travel queries**: Access the exact state of your infrastructure at any point in history to understand past configurations -- **Risk-free rollbacks**: Return to any previous state without data loss when issues are detected -- **Compliance and forensics**: Meet regulatory requirements with immutable change history for audits -- **Parallel workflows**: Enable multiple teams to work on infrastructure changes simultaneously using branches -- **Change verification**: Review proposed changes before committing them to production environments +That gap turns up in ordinary work: -## Use cases +- A deployment breaks connectivity, and you need the exact configuration it replaced rather than + an approximation of it. +- Something has drifted from design intent, and the useful question is when it diverged. +- An audit asks which interfaces changed between two dates. +- A schema change is coming, and you want to know what it does to data that already exists. +- An automation behaved correctly in testing and incorrectly in production, and you want to run it + against the data as it stood at the time. -Immutable history in Infrahub supports several crucial infrastructure management scenarios: +Because Infrahub records the state of the graph at every point in time, each of these is a query +rather than an investigation. -- **Historical analysis**: View how your infrastructure looked at a specific point in time to troubleshoot issues or understand past decisions -- **Compliance auditing**: Extract all changes performed within a specific time frame for regulatory compliance -- **Change impact assessment**: Compare infrastructure states before and after significant changes -- **Security investigation**: Trace unauthorized or unexpected changes to their source -- **Knowledge preservation**: Understand why specific configuration decisions were made, even as team members change +## How Infrahub stores history -## Core concepts +Nothing in the database is modified in place. Updating an attribute adds a new value with its own +timestamp; the previous value stays where it is, still carrying the timestamps that say when it +was current. The same applies to relationships, so both the objects and the connections between +them are timestamped. -### Timestamps and commits +A graph recorded this way is a **temporal graph**: it holds not only how objects relate, but when +each of those relationships was valid. Naming a time in a query selects the values that were +current then, which is what makes "the topology before Tuesday's pipeline run" a question the +database can answer directly. -Every change in Infrahub is organized into commits, each with an immutable timestamp. These commits capture: +Two properties follow from storing history this way: -- The specific data that changed -- Who made the change -- When the change occurred +- **Change tracking is per attribute, not per object.** An update stores the value that changed + rather than a fresh copy of the whole object, and a diff can say which field moved rather than + only that the object was touched. +- **History cannot be rewritten.** Because values are added rather than replaced, a past state + cannot be edited after the fact, and the record of who changed what remains attached to it. -This approach ensures a complete and coherent historical record that can never be rewritten or deleted. +## Temporal history and change logs -### Temporal queries +A change log records events: what changed, when, and by whom. Reconstructing a past state from one +means finding an earlier snapshot and replaying the events on top of it, and the result is only as +complete as the log. -Infrahub's temporal query system allows you to retrieve data from any point in time. When you query the database, you're not just accessing current data—you're accessing a specific moment in the database's history. By default, queries return the latest state, but you can specify any timestamp to see exactly how your infrastructure looked at that moment. +Infrahub stores the resulting state at every point in time instead of the operations that produced +it. Reading the past is therefore a query against data that is already there, and it returns +objects in the same shape as a query against current data — the same fields, the same filters, the +same client code. -This capability extends across all interfaces: +Both are useful for different questions. Use the [activity +log](../deploy-manage/run-observe/activity-log) to see the sequence of operations someone +performed; query a past time to see the state those operations produced. -- **Web UI**: Time navigation controls in the interface -- **GraphQL API**: Timestamp parameters for historical queries -- **REST API**: Temporal query support for retrieving historical data -- **Python SDK**: Time-aware methods to access past states +## Branches are parallel timelines -For the syntax each one uses, the time formats they accept, and how far back a branch can be read, see [Query historical data](./query-historical-data.mdx). +A branch is a second timeline over the same history. Creating one stores a pointer to the moment +it diverged rather than a copy of the data, so the branch starts with the full history of its +origin available to it and records only the values you change on it. -### Temporal queries and change logs +That is why a query names both a branch and a time: together they identify one state out of every +state the graph has recorded. A branch created last week can be read as it was on Monday, and the +default branch can be read as it was before that branch merged into it. -A change log records the operations that were applied. Reading a past state from one means replaying those operations in order, and the result is only as complete as the log. Infrahub stores the resulting state at every point in time, so reading the past is a query against data that is already stored rather than a reconstruction from a record of what happened. +## Where you can use it -### Multiple timelines at once +| Surface | Access | +|---|---| +| Web interface | The time selector beside the branch selector; every object you open reflects the selected time | +| GraphQL API | `at` on the endpoint — see [Query historical data](./query-historical-data.mdx) | +| REST API | `at` on stored queries, artifacts, and transformations | +| Python SDK | `at` on `all()`, `get()`, `filters()`, and `execute_graphql()` | -Branches share the same immutable baseline and record only the values that diverge from it. Creating a branch stores a pointer to a base timestamp rather than a copy of the data, so a branch starts with the full history of its origin available to it and adds only what you change. This is what allows a query to name both a branch and a time: the two together identify one state out of every state the graph has recorded. - -### Attribute-level change tracking - -Unlike systems that capture entire object snapshots, Infrahub's immutable history operates at the attribute level. This approach offers several advantages: - -- **Storage efficiency**: Only changed values are stored, not entire object copies -- **Change clarity**: Easier identification of exactly what changed in each commit -- **Performance optimization**: Faster queries and better scalability with large datasets - -## Implementation details - -There is a timestamp associated with every change in the database. This timestamp is immutable and cannot be changed or deleted. Every query to the database can be associated with a timestamp, allowing you to see the state of the database at that specific point in time. +To compare two moments rather than read one, query `DiffTree` with a start and end time. A +[Proposed Change](../proposed-changes/overview.mdx) does the same comparison scoped to a branch, +which is the right tool when the two states you care about are a branch and its base. ## Related topics -- [Branches](../branches/overview.mdx) — how branches isolate changes, and what merging does to the timeline -- [Proposed Changes](../proposed-changes/overview.mdx) — review and validate a set of changes before they merge +- [Branches](../branches/overview.mdx) — how branches isolate changes, and what merging does to + the timeline +- [Proposed Changes](../proposed-changes/overview.mdx) — review and validate a set of changes + before they merge ## In this section -- [Query historical data](./query-historical-data.mdx) — Read the graph as it stood at any past time, from the web interface, GraphQL, REST, or the Python SDK +- [Query historical data](./query-historical-data.mdx) — Read the graph as it stood at a past + time from any interface, and compare two moments to see what changed diff --git a/docs/docs/immutable-history/query-historical-data.mdx b/docs/docs/immutable-history/query-historical-data.mdx index cb0d8717cd8..2931886d6a5 100644 --- a/docs/docs/immutable-history/query-historical-data.mdx +++ b/docs/docs/immutable-history/query-historical-data.mdx @@ -5,19 +5,25 @@ title: Query historical data import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -# Query historical data +Attach a time to any read and Infrahub returns the data as it stood then. The query is the one you +already use — same fields, same filters, same client code — with a point in time added to it. -Every read in Infrahub happens at a point in time. By default that point is now. Add the `at` parameter to a read and Infrahub returns the data as it was at the time you name, on whichever branch you are querying. +This is the tool for questions that current state cannot answer: which interfaces changed between +two dates, what the topology looked like before a pipeline ran, or what a device's configuration +was at the moment an alert fired. [Immutable history](./overview.mdx) covers why those states are +still available to query; this guide covers how to ask for them. -Use `at` when the current state cannot explain the present: a change was merged overnight and a device stopped forwarding traffic, an auditor asks which prefixes were assigned to a site on a given date, or you need to compare a configuration against the one it replaced. +Three things carry most of the work: choosing the point in time, knowing how far back the branch +you are querying can be read, and picking the interface that fits what you are doing. -## Before you start +## Choose a point in time -Two things determine whether a first attempt returns data: the format of the time you pass, and how far back the branch can be read. +Two kinds of question need two kinds of timestamp, and `at` accepts both. -### Time formats - -`at` accepts an absolute time or an offset from now. +While you are working backwards from a problem, the useful reference is now — "what did this look +like an hour ago" — and a relative offset says that without arithmetic. An audit question, a +post-incident writeup, or anything two people need to agree on requires a fixed moment that still +means the same thing next week, which is an absolute time. | Form | Example | Resolves to | |---|---|---| @@ -26,32 +32,53 @@ Two things determine whether a first attempt returns data: the format of the tim | Date only | `2026-03-09` | 12:00 UTC on that day | | Offset from now | `30s`, `45m`, `6h`, `2h30m` | That much time before now | -A date on its own resolves to midday, not midnight. Write the time out when the distinction matters. +Two limits are worth knowing before they surprise you. A date on its own resolves to midday rather +than midnight, so an audit question about "the 9th" is not the same request as `2026-03-09` — write +the time out when the boundary of a day matters. And offsets stop at hours: seconds, minutes, and +hours combine in one string, but there is no day or week form, so `7d` returns a +`TimestampFormatError`. For anything further back than a few hours, pass an absolute time. -Offsets are limited to seconds, minutes, and hours — `s`, `sec`, `second`, `seconds`, `m`, `min`, `minute`, `minutes`, `h`, `hour`, `hours` — and combine in one string. Days and weeks have no offset form, so `7d` returns a `TimestampFormatError`. For anything beyond a few hours back, pass an absolute time. +## How far back a branch can be read -### How far back you can read +A branch's history starts where the branch does. Creating a branch stores a pointer to the moment +it diverged rather than a copy of the data, so what a branch can be read from is the history it +inherited, plus the changes you have made on it since. -A branch cannot be read from before it existed. On the default branch and the global branch, the earliest readable time is that branch's own creation time. On any other branch, the boundary is the creation time of the branch it came from, because the history before the branch point belongs to the origin branch. +That inheritance is what puts the boundary where it is. On the default branch and the global +branch, the earliest readable time is that branch's own creation. On any other branch, the boundary +is the creation time of the branch it came from, because everything before the divergence point is +the origin branch's history and that is where the branch reads it from. -Ask for something earlier and Infrahub rejects the query: +Ask for anything earlier and Infrahub rejects the query rather than returning a partial answer: ```text Requested time '2026-01-05T00:00:00Z' is before branch 'main' was created at '2026-02-01T09:14:22.481000Z'. ``` +If you need history from before a branch existed, query the branch it came from. + ## Read data at a past time -Pass `at` alongside the branch you are reading. The examples below all return the state of `ord1-edge1` at the same moment. +Each interface suits a different moment. Use the web interface while you are still working out +what changed, when moving the time and re-reading objects is the fastest way to find it. Use +GraphQL for a specific question you can express as a query. Use the REST API when something +scheduled or external needs the answer. Use the Python SDK when you are comparing states in code +rather than reading them. + +The examples below all return the state of `ord1-edge1` at the same moment. -Select the time selector — the calendar and clock icon beside the branch selector — and choose a date and time in UTC. The control has no text label until you set a time, at which point the bar beside it reads **Current view time** with your selection. +Select the time selector — the calendar and clock icon beside the branch selector — and choose a +date and time in UTC. The control carries no text label until you set a time, at which point the +bar beside it reads **Current view time** with your selection. -Every object you open while a time is set shows its state at that time. Select the **×** beside the displayed time to return to the present. +The selected time then applies to everything you open, so you can move through related objects +without setting it again. Select the **×** beside the displayed time to return to the present. -The picker offers past times only. +Because the picker offers past times only, it is the one interface that cannot be pointed at a +future timestamp. ![The time selector, with a past time applied](../media/tutorial_2_historical.png) @@ -59,7 +86,7 @@ The picker offers past times only. -`at` is a query-string parameter on the GraphQL endpoint, so the query itself is unchanged: +`at` is a query-string parameter on the endpoint, so the query itself is unchanged: ```graphql # Read a device at a past time # Endpoint : http://localhost:8000/graphql/main?at=2026-03-09T14:00:00Z @@ -76,26 +103,34 @@ query DeviceAtTime { } ``` -Because `at` is part of the endpoint rather than the query, the same stored query returns current or historical data depending on the URL you send it to. +Keeping the time out of the query body means a query you have already written and saved works +against any point in time — the endpoint you send it to decides which one. That is what makes a +stored query reusable for both monitoring current state and answering a question about the past. -The REST API applies `at` to stored GraphQL queries, artifacts, and transformations rather than to ad-hoc object reads. To read objects at a past time over REST, save the query as a `CoreGraphQLQuery` object and run it by name: +The REST API applies `at` to stored GraphQL queries, artifacts, and transformations rather than to +ad-hoc object reads. To read objects at a past time over REST, save the query as a +`CoreGraphQLQuery` object and run it by name: ```bash curl "http://localhost:8000/api/query/device-status?branch=main&at=2026-03-09T14:00:00Z" \ -H "X-INFRAHUB-KEY: $INFRAHUB_API_TOKEN" ``` -The same parameter works on `/api/artifact/{artifact_id}` and on the transformation endpoints, so you can render an artifact from the data as it was rather than from current data. +The same parameter works on `/api/artifact/{artifact_id}` and on the transformation endpoints, +which renders an artifact from the data as it stood rather than from current data — useful for +producing the configuration a device was given at a particular time and comparing it with what is +on the device now. -`all()`, `get()`, `filters()`, and `execute_graphql()` each take an `at` argument: +`all()`, `get()`, `filters()`, and `execute_graphql()` each take an `at` argument, so a script can +read two moments and compare them: ```python from infrahub_sdk import InfrahubClientSync @@ -103,23 +138,29 @@ from infrahub_sdk.timestamp import Timestamp client = InfrahubClientSync(address="http://localhost:8000") -device = client.get( +before = client.get( kind="InfraDevice", name__value="ord1-edge1", at=Timestamp("2026-03-09T14:00:00Z"), ) +now = client.get(kind="InfraDevice", name__value="ord1-edge1") -print(device.description.value) +print(before.description.value, "->", now.description.value) ``` `Timestamp` accepts every form in the table above, so `Timestamp("6h")` reads six hours back. +Pointing a script at a past state is also how you test an automation against the data it ran on, +rather than against data that has moved on since. ## Compare two points in time -To see what changed between two moments rather than the state at one of them, query `DiffTree` with `from_time` and `to_time`. Both take a timestamp, and the two times can be arbitrary — they do not have to correspond to a branch point or a proposed change. +Reading one state answers "what was it then". To answer "what changed between these two moments", +query `DiffTree` with `from_time` and `to_time`. The two times are yours to choose — they do not +have to line up with a branch point or a proposed change, which is what makes this usable for a +window like "the four hours around the incident". ```graphql # What changed on main between two timestamps # Endpoint : http://localhost:8000/graphql/main @@ -145,12 +186,26 @@ query ChangesBetween { } ``` -Omit `from_time` and the comparison starts at the branch's creation; omit `to_time` and it ends at the current time. Add `filters` to restrict the result by kind, namespace, or status, and `limit` and `offset` to page through a large result. For a count without the node detail, query `DiffTreeSummary` with the same arguments. +Omit `from_time` and the comparison starts at the branch's creation; omit `to_time` and it ends at +the current time. Add `filters` to restrict the result by kind, namespace, or status, and `limit` +and `offset` to page through a large result. For counts without the node detail, query +`DiffTreeSummary` with the same arguments. + +Two things to expect from the result. The base of the comparison is always the default branch, so +naming a feature branch compares that branch against the default branch across the window, while +naming the default branch compares it against itself over time. And where no diff covering the +requested window is available, `DiffTree` returns `null` rather than an empty result — so treat a +null as "not calculated", not as "nothing changed". -Two things to expect from the result. The base of the comparison is always the default branch, so naming a feature branch compares that branch against the default branch across the window, while naming the default branch compares it against itself. And where no diff covering the requested window is available, `DiffTree` returns `null` rather than an empty result. +When the two states you want to compare are a branch and its base, a +[Proposed Change](../proposed-changes/overview.mdx) gives you the same comparison with review and +validation attached. ## Related -- [Immutable history](./overview.mdx) — how Infrahub stores every past state, and why a historical read is a query rather than a reconstruction -- [Branches](../branches/overview.mdx) — how branch creation and merging place the timestamps you query against -- [Proposed Changes](../proposed-changes/overview.mdx) — review a diff scoped to a branch, rather than to two times you choose +- [Immutable history](./overview.mdx) — how Infrahub stores every past state, and why reading one + is a query rather than a reconstruction +- [Branches](../branches/overview.mdx) — how branch creation and merging place the timestamps you + query against +- [Activity log](../deploy-manage/run-observe/activity-log.mdx) — the sequence of operations + someone performed, where a past state tells you what those operations produced From 5aaca81cd5f210f2f531d234f2e91f7a2f6f0ac2 Mon Sep 17 00:00:00 2001 From: Yvonne Jouffrault Date: Thu, 20 Aug 2026 09:58:53 -0400 Subject: [PATCH 3/7] docs(immutable-history): restructure both pages and apply review feedback Reorder the hub from architecture-first to capability-first: what the capability gives you and the situations it applies to, what you can do with it, how historical data differs from the Activity log, how history is preserved, how branches use it, and where to query it. Drop the "risk-free rollbacks" claim and the Implementation details section, which repeated Temporal queries. Restructure the spoke around the operating model rather than the parameter: branch and time select the state, how a historical query resolves, then reading, comparing, time formats, and history limits. Apply @ajtmccarty's review, each item verified against the implementation: - Deleting a branch removes its history and is not recoverable (DeleteBranchEdgesQuery), so the hub no longer implies history is always retained. - A branch's origin is always the default branch (Branch.origin_branch defaults to main), so the boundary and the remedy both name the default branch instead of "the branch it came from". - DiffTree retrieves a diff that has already been calculated; DiffUpdate calculates one for a period. This is what makes an arbitrary period return null on the first attempt. - A branch's Branch view shows and refreshes its diff without a Proposed Change. - Rebasing moves the timestamps of changes made on a branch up to the rebase time, so a pre-rebase change is no longer readable at its original timestamp (rebase_graph). Also correct the SDK argument types: at takes a Timestamp on all(), get(), and filters(), and additionally a string on execute_graphql(). There is no DateTime form -- a datetime resolves silently to now. Co-Authored-By: Claude Opus 5 --- docs/docs/immutable-history/overview.mdx | 132 +++++----- .../query-historical-data.mdx | 226 ++++++++++-------- 2 files changed, 185 insertions(+), 173 deletions(-) diff --git a/docs/docs/immutable-history/overview.mdx b/docs/docs/immutable-history/overview.mdx index be2f7cac594..d459091ede2 100644 --- a/docs/docs/immutable-history/overview.mdx +++ b/docs/docs/immutable-history/overview.mdx @@ -2,95 +2,89 @@ title: Immutable history --- -Infrahub keeps every state your infrastructure data has held. Query the graph as it was at any -point in the past — yesterday's routes, last week's VLAN definitions, the ACL a rollout replaced — -using the same queries you run against current data. +Infrahub preserves previous values and relationships as your infrastructure data changes. Earlier +versions remain available for queries, so you can inspect what existed at a specific time, compare +changes across a period, and trace how an object or relationship changed. -## Why it matters +This is useful for troubleshooting incidents, reviewing the effect of a change, answering audit +questions, and understanding how topology or dependencies evolved. When you specify a timestamp, +Infrahub returns the values and relationships that were valid at that time. -Debugging a change means comparing what is true now with what was true before it. A data model -that stores only current values answers the first half and loses the second: once a rollout has -overwritten the configuration that worked, the comparison you need is gone. +## What you can do with immutable history -That gap turns up in ordinary work: +- Query objects and relationships as they existed at a specific point in time. +- Compare two timestamps to identify which objects, attributes, or relationships changed. +- Trace each change to the account that made it and the time it happened. +- Preserve the history available before a branch was created while recording changes made on that + branch. +- Use previous infrastructure data for troubleshooting, audits, security investigations, and + post-incident analysis. -- A deployment breaks connectivity, and you need the exact configuration it replaced rather than - an approximation of it. -- Something has drifted from design intent, and the useful question is when it diverged. -- An audit asks which interfaces changed between two dates. -- A schema change is coming, and you want to know what it does to data that already exists. -- An automation behaved correctly in testing and incorrectly in production, and you want to run it - against the data as it stood at the time. +For example, you can answer questions such as: -Because Infrahub records the state of the graph at every point in time, each of these is a query -rather than an investigation. +- Which devices, interfaces, and relationships existed for this site during last night's incident? +- Which interface attributes changed between the last known-good timestamp and now? +- Which services and circuits were related before this topology change? -## How Infrahub stores history +## Historical data and the Activity log answer different questions -Nothing in the database is modified in place. Updating an attribute adds a new value with its own -timestamp; the previous value stays where it is, still carrying the timestamps that say when it -was current. The same applies to relationships, so both the objects and the connections between -them are timestamped. +The Activity log records operations: which objects were affected, when a change occurred, who made +it, and the sequence of actions. -A graph recorded this way is a **temporal graph**: it holds not only how objects relate, but when -each of those relationships was valid. Naming a time in a query selects the values that were -current then, which is what makes "the topology before Tuesday's pipeline run" a question the -database can answer directly. +Immutable history preserves the versions of data produced by those changes. If you need to know who +changed an interface and when, use the [Activity +log](../deploy-manage/run-observe/activity-log.mdx). If you need to know which interfaces, +addresses, and relationships existed at 14:00 during an incident, query the data for that timestamp. -Two properties follow from storing history this way: +Infrahub returns the relevant values and relationships for the time you specify, so you query that +data directly rather than reconstructing it from a backup and the changes recorded after it. -- **Change tracking is per attribute, not per object.** An update stores the value that changed - rather than a fresh copy of the whole object, and a diff can say which field moved rather than - only that the object was touched. -- **History cannot be rewritten.** Because values are added rather than replaced, a past state - cannot be edited after the fact, and the record of who changed what remains attached to it. +## How Infrahub preserves history -## Temporal history and change logs +Each change in Infrahub creates a new version instead of modifying the previous value in place. +Every version is associated with a timestamp, and earlier values remain available for queries that +specify an earlier time. -A change log records events: what changed, when, and by whom. Reconstructing a past state from one -means finding an earlier snapshot and replaying the events on top of it, and the result is only as -complete as the log. +History is tracked at the attribute level. When one field changes, Infrahub records the new value +for that field alone. This makes it possible to identify the specific attributes that changed +between two timestamps. -Infrahub stores the resulting state at every point in time instead of the operations that produced -it. Reading the past is therefore a query against data that is already there, and it returns -objects in the same shape as a query against current data — the same fields, the same filters, the -same client code. +Relationships are versioned as well. A query for a specific timestamp therefore returns both the +attribute values and the object relationships that were valid at that time. This is important for +topology and dependency questions where the connections between objects are part of the answer. -Both are useful for different questions. Use the [activity -log](../deploy-manage/run-observe/activity-log) to see the sequence of operations someone -performed; query a past time to see the state those operations produced. +A graph that stores validity times for both its values and its relationships is a **temporal +graph**. The period during which each value and connection applied is part of the stored data, which +is why you can answer a question about past topology with a query. -## Branches are parallel timelines +Deleting a branch is the exception. Infrahub removes the data and history recorded on that branch, +and that history is not recoverable. Versions on the default branch are unaffected. -A branch is a second timeline over the same history. Creating one stores a pointer to the moment -it diverged rather than a copy of the data, so the branch starts with the full history of its -origin available to it and records only the values you change on it. +## How branches use immutable history -That is why a query names both a branch and a time: together they identify one state out of every -state the graph has recorded. A branch created last week can be read as it was on Monday, and the -default branch can be read as it was before that branch merged into it. +When you create a branch, it starts from the data and history available on the default branch at its +branch point. Changes made on that branch create new versions there without changing the data on the +default branch. -## Where you can use it +You can therefore develop and review multiple changes independently while retaining the history +needed to compare and merge them. See [Branches](../branches/overview.mdx) for the branch creation, +diff, and merge workflow. -| Surface | Access | -|---|---| -| Web interface | The time selector beside the branch selector; every object you open reflects the selected time | -| GraphQL API | `at` on the endpoint — see [Query historical data](./query-historical-data.mdx) | -| REST API | `at` on stored queries, artifacts, and transformations | -| Python SDK | `at` on `all()`, `get()`, `filters()`, and `execute_graphql()` | +## Query data at a specific time -To compare two moments rather than read one, query `DiffTree` with a start and end time. A -[Proposed Change](../proposed-changes/overview.mdx) does the same comparison scoped to a branch, -which is the right tool when the two states you care about are a branch and its base. +By default, queries return the latest data on the selected branch. Set a time when you need the +values and relationships that were valid at an earlier timestamp. You can specify a time through the +web interface, GraphQL API, REST API, and Python SDK. -## Related topics +See [Query historical data](./query-historical-data.mdx) for the available interfaces, comparing two +timestamps, supported time formats, and branch history limits. -- [Branches](../branches/overview.mdx) — how branches isolate changes, and what merging does to - the timeline -- [Proposed Changes](../proposed-changes/overview.mdx) — review and validate a set of changes - before they merge +## Related -## In this section - -- [Query historical data](./query-historical-data.mdx) — Read the graph as it stood at a past - time from any interface, and compare two moments to see what changed +- [Query historical data](./query-historical-data.mdx) — read the graph at an earlier timestamp, + and compare two timestamps to see what changed +- [Branches](../branches/overview.mdx) — how branches diverge, share history, and merge +- [Proposed Changes](../proposed-changes/overview.mdx) — compare a branch with its base, with + review, validation, and checks +- [Activity log](../deploy-manage/run-observe/activity-log.mdx) — which operations occurred, when, + and by whom diff --git a/docs/docs/immutable-history/query-historical-data.mdx b/docs/docs/immutable-history/query-historical-data.mdx index 2931886d6a5..8d3c1947069 100644 --- a/docs/docs/immutable-history/query-historical-data.mdx +++ b/docs/docs/immutable-history/query-historical-data.mdx @@ -5,80 +5,64 @@ title: Query historical data import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -Attach a time to any read and Infrahub returns the data as it stood then. The query is the one you -already use — same fields, same filters, same client code — with a point in time added to it. +Infrahub lets you query infrastructure data at previous points in time and compare how it changed +between them. Use queries with a timestamp to investigate incidents, verify changes during a period, +answer audit questions, and inspect how objects and relationships differed at an earlier time. -This is the tool for questions that current state cannot answer: which interfaces changed between -two dates, what the topology looked like before a pipeline ran, or what a device's configuration -was at the moment an alert fired. [Immutable history](./overview.mdx) covers why those states are -still available to query; this guide covers how to ask for them. +To query a specific point, specify a branch and timestamp. Infrahub returns the values and +relationships that were valid at that time through the same interfaces you use for current data. To +compare changes, specify two timestamps. -Three things carry most of the work: choosing the point in time, knowing how far back the branch -you are querying can be read, and picking the interface that fits what you are doing. +## Specify a branch and a time -## Choose a point in time +Each query is evaluated for a branch and a point in time. Infrahub uses those two inputs to select +which versions of objects and relationships it returns. If you do not set either one, Infrahub +returns the current data from the default branch. -Two kinds of question need two kinds of timestamp, and `at` accepts both. - -While you are working backwards from a problem, the useful reference is now — "what did this look -like an hour ago" — and a relative offset says that without arithmetic. An audit question, a -post-incident writeup, or anything two people need to agree on requires a fixed moment that still -means the same thing next week, which is an absolute time. - -| Form | Example | Resolves to | +| State to read | Branch | Time | |---|---|---| -| ISO 8601 with a zone or offset | `2026-03-09T14:00:00Z`, `2026-03-09T15:00:00+01:00` | The instant you name | -| ISO 8601 without a zone | `2026-03-09T14:00:00` | The same wall-clock time, read as UTC | -| Date only | `2026-03-09` | 12:00 UTC on that day | -| Offset from now | `30s`, `45m`, `6h`, `2h30m` | That much time before now | - -Two limits are worth knowing before they surprise you. A date on its own resolves to midday rather -than midnight, so an audit question about "the 9th" is not the same request as `2026-03-09` — write -the time out when the boundary of a day matters. And offsets stop at hours: seconds, minutes, and -hours combine in one string, but there is no day or week form, so `7d` returns a -`TimestampFormatError`. For anything further back than a few hours, pass an absolute time. +| Production as it stands | default | now | +| Production during last night's incident | default | a time within the incident | +| The change someone is proposing | their branch | now | +| The default branch just before that change merged | default | a time before the merge | -## How far back a branch can be read +Queries for earlier data therefore use the same read interfaces as current queries. The difference +is that you specify the time to evaluate the data. -A branch's history starts where the branch does. Creating a branch stores a pointer to the moment -it diverged rather than a copy of the data, so what a branch can be read from is the history it -inherited, plus the changes you have made on it since. +## How historical queries work -That inheritance is what puts the boundary where it is. On the default branch and the global -branch, the earliest readable time is that branch's own creation. On any other branch, the boundary -is the creation time of the branch it came from, because everything before the divergence point is -the origin branch's history and that is where the branch reads it from. +When you add a timestamp to a query, Infrahub evaluates each requested object using the attribute +values and relationships that were valid at that time. You use the same query structure as you do +for current data; the timestamp changes which versions Infrahub returns. -Ask for anything earlier and Infrahub rejects the query rather than returning a partial answer: - -```text -Requested time '2026-01-05T00:00:00Z' is before branch 'main' was created at '2026-02-01T09:14:22.481000Z'. -``` +Infrahub can return those versions because changes create new attribute values and relationship +versions instead of replacing the previous ones. History is tracked per attribute and relationship, +so a comparison can identify the specific fields or connections that changed, including the previous +values. -If you need history from before a branch existed, query the branch it came from. +Later changes do not modify versions that were already recorded. See [Immutable +history](./overview.mdx) for more detail on how Infrahub preserves those versions and how immutable +history relates to branches and the Activity log. -## Read data at a past time +## Query data at a specific time -Each interface suits a different moment. Use the web interface while you are still working out -what changed, when moving the time and re-reading objects is the fastest way to find it. Use -GraphQL for a specific question you can express as a query. Use the REST API when something -scheduled or external needs the answer. Use the Python SDK when you are comparing states in code -rather than reading them. +Use a timestamp when you need the values and relationships that existed at a known point — for +example, during an incident or before a change. -The examples below all return the state of `ord1-edge1` at the same moment. +You can specify a time when viewing or querying Infrahub data through the web interface, GraphQL, +REST API, or Python SDK. Use the web interface for interactive investigation and the APIs or SDK +when you need a repeatable or programmatic query. Select the time selector — the calendar and clock icon beside the branch selector — and choose a -date and time in UTC. The control carries no text label until you set a time, at which point the -bar beside it reads **Current view time** with your selection. +date and time in UTC. Until you set a time, the selector displays only the icon; once you set one, +the bar beside it displays **Current view time** with your selection. -The selected time then applies to everything you open, so you can move through related objects -without setting it again. Select the **×** beside the displayed time to return to the present. - -Because the picker offers past times only, it is the one interface that cannot be pointed at a -future timestamp. +The selected time remains applied as you navigate, so objects and relationships are displayed using +the values valid at that timestamp. Select the × beside the displayed time to return to the current +time. ![The time selector, with a past time applied](../media/tutorial_2_historical.png) @@ -86,9 +70,12 @@ future timestamp. -`at` is a query-string parameter on the endpoint, so the query itself is unchanged: +Apply the historical time to the same GraphQL query you use for current data. Use this when you can +already state the data you need and want a repeatable query. + +`at` is a query-string parameter on the endpoint, so the query document itself is unchanged: -```graphql # Read a device at a past time +```graphql # Read a device as it existed at an earlier time # Endpoint : http://localhost:8000/graphql/main?at=2026-03-09T14:00:00Z query DeviceAtTime { InfraDevice(name__value: "ord1-edge1") { @@ -103,64 +90,49 @@ query DeviceAtTime { } ``` -Keeping the time out of the query body means a query you have already written and saved works -against any point in time — the endpoint you send it to decides which one. That is what makes a -stored query reusable for both monitoring current state and answering a question about the past. - -The REST API applies `at` to stored GraphQL queries, artifacts, and transformations rather than to -ad-hoc object reads. To read objects at a past time over REST, save the query as a -`CoreGraphQLQuery` object and run it by name: +Use the REST API when an external tool, scheduled process, or integration needs data for a specific +timestamp. `at` applies to stored GraphQL queries, artifacts, and Transformations rather than to +ad-hoc object reads, so save the query as a `CoreGraphQLQuery` and execute it by name: ```bash curl "http://localhost:8000/api/query/device-status?branch=main&at=2026-03-09T14:00:00Z" \ -H "X-INFRAHUB-KEY: $INFRAHUB_API_TOKEN" ``` -The same parameter works on `/api/artifact/{artifact_id}` and on the transformation endpoints, -which renders an artifact from the data as it stood rather than from current data — useful for -producing the configuration a device was given at a particular time and comparing it with what is -on the device now. +The same parameter applies to `/api/artifact/{artifact_id}` and the transformation endpoints, where +Infrahub renders the artifact from the data that was valid at that timestamp. -`all()`, `get()`, `filters()`, and `execute_graphql()` each take an `at` argument, so a script can -read two moments and compare them: +Pass the `at` argument to the SDK query methods when you need a node or set of nodes as they existed +at a specific time. `all()`, `get()`, and `filters()` take a `Timestamp`; `execute_graphql()` also +accepts a string. ```python -from infrahub_sdk import InfrahubClientSync from infrahub_sdk.timestamp import Timestamp -client = InfrahubClientSync(address="http://localhost:8000") - -before = client.get( +device = await client.get( kind="InfraDevice", name__value="ord1-edge1", at=Timestamp("2026-03-09T14:00:00Z"), ) -now = client.get(kind="InfraDevice", name__value="ord1-edge1") - -print(before.description.value, "->", now.description.value) ``` -`Timestamp` accepts every form in the table above, so `Timestamp("6h")` reads six hours back. -Pointing a script at a past state is also how you test an automation against the data it ran on, -rather than against data that has moved on since. - -## Compare two points in time +## Compare changes between two timestamps -Reading one state answers "what was it then". To answer "what changed between these two moments", -query `DiffTree` with `from_time` and `to_time`. The two times are yours to choose — they do not -have to line up with a branch point or a proposed change, which is what makes this usable for a -window like "the four hours around the incident". +Use a diff when you need to identify which objects or attributes changed between two timestamps. +`DiffTree` compares the data at `from_time` with the data at `to_time`. The timestamps do not need +to align with a branch point or Proposed Change, so you can compare any useful period, such as the +four hours around an incident. ```graphql # What changed on main between two timestamps # Endpoint : http://localhost:8000/graphql/main @@ -186,26 +158,72 @@ query ChangesBetween { } ``` -Omit `from_time` and the comparison starts at the branch's creation; omit `to_time` and it ends at -the current time. Add `filters` to restrict the result by kind, namespace, or status, and `limit` -and `offset` to page through a large result. For counts without the node detail, query -`DiffTreeSummary` with the same arguments. +Behavior to expect: + +- Omit `from_time` and the comparison starts at the branch's `branched_from` timestamp. +- Omit `to_time` and the comparison runs to the present. +- Use `filters` to narrow the result by kind, namespace, or status. +- Use `limit` and `offset` to page through results. +- Use `DiffTreeSummary` when you only need counts rather than the full set of changed nodes. +- The base of the comparison is the default branch. Name a feature branch to compare it with the + default branch across the requested period, or name the default branch to compare it with itself + over time. +- If no diff covering the requested period is available, the query returns `null` rather than an + empty result. Treat `null` as "not calculated," not "nothing changed." + +`DiffTree` retrieves a diff that has already been calculated. To calculate one for a period, send +the `DiffUpdate` mutation first with the `branch` and, when you need a period other than the +default, `from_time` and `to_time`. This is why a request for an arbitrary period can return `null` +on the first attempt. + +In the web interface, a branch's Branch view shows its diff and lets you refresh it, whether or not +a Proposed Change exists for that branch. When you are comparing a branch with its base and also +need review, validation, and checks, use a [Proposed Change](../proposed-changes/overview.mdx). + +## Choose an absolute timestamp or relative offset + +Use a relative offset when you are investigating from the current time. Use an absolute timestamp +when the query needs to resolve to the same time each time it runs — for example, for an audit +answer, post-incident report, or reproducible analysis. + +| Form | Example | Resolves to | +|---|---|---| +| ISO 8601 with a zone or offset | `2026-03-09T14:00:00Z` or `2026-03-09T15:00:00+01:00` | The specified instant | +| ISO 8601 without a zone | `2026-03-09T14:00:00` | The same wall-clock time, interpreted as UTC | +| Date only | `2026-03-09` | 12:00 UTC on that day | +| Offset from now | `30s`, `45m`, `6h`, `2h30m` | That interval before the current time | + +A date without a time resolves to midday rather than midnight, so include an explicit time when a +day boundary matters. Relative offsets support seconds, minutes, and hours, including combined +values such as `2h30m`. They do not support day or week units; use an absolute timestamp for those +intervals. + +## Understand branch history limits + +Each branch has an earliest timestamp you can query. If the requested time is earlier than the +history available to that branch, Infrahub rejects the query rather than returning partial data. + +When you create a branch, Infrahub records where it diverged rather than copying the entire dataset. +You can query the history available through the default branch, plus the changes recorded on the +branch after it diverged. + +On the default branch and the global branch, the earliest available time is that branch's creation. +On any other branch, the boundary is the creation time of the default branch. + +```text +Requested time '2026-01-05T00:00:00Z' is before branch 'main' was created at '2026-02-01T09:14:22.481000Z'. +``` -Two things to expect from the result. The base of the comparison is always the default branch, so -naming a feature branch compares that branch against the default branch across the window, while -naming the default branch compares it against itself over time. And where no diff covering the -requested window is available, `DiffTree` returns `null` rather than an empty result — so treat a -null as "not calculated", not as "nothing changed". +If you need data from an earlier timestamp than the branch allows, query the default branch instead. -When the two states you want to compare are a branch and its base, a -[Proposed Change](../proposed-changes/overview.mdx) gives you the same comparison with review and -validation attached. +Rebasing a branch moves the timestamps of changes made on it up to the rebase time, so a change +recorded on the branch before a rebase is no longer readable at its original timestamp. ## Related -- [Immutable history](./overview.mdx) — how Infrahub stores every past state, and why reading one - is a query rather than a reconstruction -- [Branches](../branches/overview.mdx) — how branch creation and merging place the timestamps you - query against -- [Activity log](../deploy-manage/run-observe/activity-log.mdx) — the sequence of operations - someone performed, where a past state tells you what those operations produced +- [Immutable history](./overview.mdx) — how Infrahub preserves previous values and relationships +- [Branches](../branches/overview.mdx) — how branches diverge, share history, and merge +- [Activity log](../deploy-manage/run-observe/activity-log.mdx) — which operations occurred, when, + and by whom +- [Proposed Changes](../proposed-changes/overview.mdx) — compare a branch with its base with review, + validation, and checks From fd1a3a093c1cd52d8e9de533d3e8bf88d1922c8d Mon Sep 17 00:00:00 2001 From: Yvonne Jouffrault Date: Thu, 20 Aug 2026 10:02:00 -0400 Subject: [PATCH 4/7] docs(immutable-history): accuracy audit against the implementation Document schema temporal support, which @ajtmccarty raised and the PR had left as an open question. It is real: when `at` precedes a branch's schema_changed_at, the GraphQL layer calls load_schema_from_db(at=...) and analyzes the query against the historical schema (backend/infrahub/graphql/app.py:228), so a query for an earlier time sees the attributes the schema defined then. Also from the audit: - A timestamp applies to reads only. A query document containing a mutation has `at` replaced with the current time (app.py:223). - The SDK example could not run as written -- it referenced `client` without creating one. Added the import and construction. - Ground the change-attribution bullet in objects/metadata.mdx, which documents created_by and updated_by, rather than asserting it. - "Infrahub lets you query" was the product-as-subject construction the voice rules exclude; the reader is the subject now. Co-Authored-By: Claude Opus 5 --- docs/docs/immutable-history/overview.mdx | 7 ++++++- .../immutable-history/query-historical-data.mdx | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/docs/docs/immutable-history/overview.mdx b/docs/docs/immutable-history/overview.mdx index d459091ede2..4291106c86b 100644 --- a/docs/docs/immutable-history/overview.mdx +++ b/docs/docs/immutable-history/overview.mdx @@ -14,7 +14,8 @@ Infrahub returns the values and relationships that were valid at that time. - Query objects and relationships as they existed at a specific point in time. - Compare two timestamps to identify which objects, attributes, or relationships changed. -- Trace each change to the account that made it and the time it happened. +- Trace a change to the account that made it and the time it happened, through [object + metadata](../objects/metadata.mdx). - Preserve the history available before a branch was created while recording changes made on that branch. - Use previous infrastructure data for troubleshooting, audits, security investigations, and @@ -53,6 +54,10 @@ Relationships are versioned as well. A query for a specific timestamp therefore attribute values and the object relationships that were valid at that time. This is important for topology and dependency questions where the connections between objects are part of the answer. +The schema is versioned the same way. If the schema changed after the timestamp you request, +Infrahub loads the schema as it was at that point, so the query sees the attributes and +relationships the schema defined then rather than the ones it defines now. + A graph that stores validity times for both its values and its relationships is a **temporal graph**. The period during which each value and connection applied is part of the stored data, which is why you can answer a question about past topology with a query. diff --git a/docs/docs/immutable-history/query-historical-data.mdx b/docs/docs/immutable-history/query-historical-data.mdx index 8d3c1947069..89c96bf58a0 100644 --- a/docs/docs/immutable-history/query-historical-data.mdx +++ b/docs/docs/immutable-history/query-historical-data.mdx @@ -5,9 +5,9 @@ title: Query historical data import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; -Infrahub lets you query infrastructure data at previous points in time and compare how it changed -between them. Use queries with a timestamp to investigate incidents, verify changes during a period, -answer audit questions, and inspect how objects and relationships differed at an earlier time. +You can query infrastructure data at previous points in time and compare how it changed between +them. Use queries with a timestamp to investigate incidents, verify changes during a period, answer +audit questions, and inspect how objects and relationships differed at an earlier time. To query a specific point, specify a branch and timestamp. Infrahub returns the values and relationships that were valid at that time through the same interfaces you use for current data. To @@ -40,6 +40,13 @@ versions instead of replacing the previous ones. History is tracked per attribut so a comparison can identify the specific fields or connections that changed, including the previous values. +The schema is evaluated for the same point in time. If the schema changed after the timestamp you +request, Infrahub loads the schema as it was then, so the query sees the attributes and relationships +that existed at that point rather than the current ones. + +A timestamp applies to reads only. If a query document contains a mutation, Infrahub ignores the +timestamp and applies the mutation at the current time. + Later changes do not modify versions that were already recorded. See [Immutable history](./overview.mdx) for more detail on how Infrahub preserves those versions and how immutable history relates to branches and the Activity log. @@ -115,8 +122,11 @@ at a specific time. `all()`, `get()`, and `filters()` take a `Timestamp`; `execu accepts a string. ```python +from infrahub_sdk import InfrahubClient from infrahub_sdk.timestamp import Timestamp +client = InfrahubClient(address="http://localhost:8000") + device = await client.get( kind="InfraDevice", name__value="ord1-edge1", From 1b0453434267e83b75ffce210f942eea731833d7 Mon Sep 17 00:00:00 2001 From: Yvonne Jouffrault Date: Thu, 20 Aug 2026 11:05:50 -0400 Subject: [PATCH 5/7] docs(immutable-history): anchor the immutability sentence in time "Later changes" gave no reference point for "later". The sentence now names what the change comes after: the moment Infrahub records the version. Co-Authored-By: Claude Opus 5 --- docs/docs/immutable-history/query-historical-data.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/immutable-history/query-historical-data.mdx b/docs/docs/immutable-history/query-historical-data.mdx index 89c96bf58a0..53c2927c303 100644 --- a/docs/docs/immutable-history/query-historical-data.mdx +++ b/docs/docs/immutable-history/query-historical-data.mdx @@ -47,7 +47,7 @@ that existed at that point rather than the current ones. A timestamp applies to reads only. If a query document contains a mutation, Infrahub ignores the timestamp and applies the mutation at the current time. -Later changes do not modify versions that were already recorded. See [Immutable +Once Infrahub records a version, a change made after it does not alter that version. See [Immutable history](./overview.mdx) for more detail on how Infrahub preserves those versions and how immutable history relates to branches and the Activity log. From a5f17f4d251f63af0b80cacaf9d12960d6d00802 Mon Sep 17 00:00:00 2001 From: Yvonne Jouffrault Date: Thu, 20 Aug 2026 12:04:51 -0400 Subject: [PATCH 6/7] docs(immutable-history): put the reader before Infrahub in the temporal-query sentence "Infrahub returns the relevant values and relationships for the time you specify" made Infrahub the subject of a transaction the reader starts, and "the data for the time you specify" was loose about what comes back. The sentence now runs reader action, then product behavior: you specify a timestamp, Infrahub returns the values and relationships that were valid then. Co-Authored-By: Claude Opus 5 --- docs/docs/immutable-history/overview.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/docs/immutable-history/overview.mdx b/docs/docs/immutable-history/overview.mdx index 4291106c86b..9671819a9ee 100644 --- a/docs/docs/immutable-history/overview.mdx +++ b/docs/docs/immutable-history/overview.mdx @@ -37,8 +37,9 @@ changed an interface and when, use the [Activity log](../deploy-manage/run-observe/activity-log.mdx). If you need to know which interfaces, addresses, and relationships existed at 14:00 during an incident, query the data for that timestamp. -Infrahub returns the relevant values and relationships for the time you specify, so you query that -data directly rather than reconstructing it from a backup and the changes recorded after it. +When you specify a timestamp, Infrahub returns the values and relationships that were valid then, so +you query that data directly rather than reconstructing it from a backup and the changes recorded +after it. ## How Infrahub preserves history From c41619ddc7a922c9385ee92b31dbfca05973f436 Mon Sep 17 00:00:00 2001 From: Yvonne Jouffrault Date: Thu, 20 Aug 2026 15:35:27 -0400 Subject: [PATCH 7/7] docs(immutable-history): make the reader the subject of the history limit "Each branch has an earliest timestamp you can query" made the branch the subject of a sentence about what the reader can do, and treated a derived boundary as a property the branch owns. The limit comes from comparing the requested time against the origin branch's creation time; the branch holds no such timestamp. Co-Authored-By: Claude Opus 5 --- docs/docs/immutable-history/query-historical-data.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/immutable-history/query-historical-data.mdx b/docs/docs/immutable-history/query-historical-data.mdx index 53c2927c303..91868f4d608 100644 --- a/docs/docs/immutable-history/query-historical-data.mdx +++ b/docs/docs/immutable-history/query-historical-data.mdx @@ -210,8 +210,8 @@ intervals. ## Understand branch history limits -Each branch has an earliest timestamp you can query. If the requested time is earlier than the -history available to that branch, Infrahub rejects the query rather than returning partial data. +You can query a branch back to the point where its history begins. If you request a time earlier +than that, Infrahub rejects the query rather than returning partial data. When you create a branch, Infrahub records where it diverged rather than copying the entire dataset. You can query the history available through the default branch, plus the changes recorded on the