Skip to content

feat: bitemporal holdings store + as_of query (#1463) - #1474

Merged
stranske merged 4 commits into
mainfrom
cursor/issue-1463-bitemporal-holdings
Jul 24, 2026
Merged

feat: bitemporal holdings store + as_of query (#1463)#1474
stranske merged 4 commits into
mainfrom
cursor/issue-1463-bitemporal-holdings

Conversation

@stranske

@stranske stranske commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Closes #1463

Summary

  • Add append-only holdings versions (knowledge_time, superseded_at, version, content_hash) with Alembic 012 + schema.sql + v_current_holdings
  • Change ingest/edgar write paths to supersede prior rows instead of delete/mutate; identical re-ingest is a content-hash no-op
  • Add etl/point_in_time.py (holdings_as_of, current_holdings) for point-in-time reconstruction

Validation

  • PYTHONPATH=. python3 -m pytest tests/test_bitemporal_holdings.py -q
  • PYTHONPATH=. python3 -m pytest tests/test_ingest_flow.py::test_replace_holdings_rows_uses_postgres_transaction -q

Notes

Foundation for backtest (#1464) and attribution (#1465). Large core-table migration — recommend owner review before merge.

Summary by CodeRabbit

  • New Features
    • Added bitemporal holdings support with versioning and supersession metadata, including a “current holdings” view for non-superseded records.
    • Introduced point-in-time helpers to query holdings as-of a cutoff, including schema-aware handling.
    • Improved holdings re-ingestion to be idempotent when content hasn’t changed, avoiding unnecessary version churn.
  • Bug Fixes
    • Prevented unintended holdings churn by skipping updates when re-ingested holdings match the current stored content.
  • Tests
    • Added acceptance tests covering as-of reconstruction, current holdings filtering, and idempotent re-ingestion.

Preserve holdings history across re-ingest and amendments so point-in-time
reconstructions and backtest foundations stay safe (closes #1463).

Co-authored-by: Cursor <cursoragent@cursor.com>
@stranske stranske added autofix Triggers autofix on PR agents:keepalive Enables keepalive automation for PR agent:cursor Route PR to Cursor agent runner labels Jul 24, 2026
@cursor

cursor Bot commented Jul 24, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@stranske
stranske temporarily deployed to agent-standard July 24, 2026 13:58 — with GitHub Actions Inactive
@stranske stranske added the agent:retry Add to trigger agent retry after rate limit or pause label Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c08f3d3-84c3-4bbb-97be-3263fc5a8971

📥 Commits

Reviewing files that changed from the base of the PR and between 6a423c8 and e934ff7.

📒 Files selected for processing (3)
  • alembic/versions/012_bitemporal_holdings.py
  • etl/point_in_time.py
  • tests/test_bitemporal_holdings.py

📝 Walkthrough

Walkthrough

The PR adds bitemporal columns and indexes to holdings, changes ingestion to append versions and supersede prior rows, adds current and as-of query helpers, and introduces acceptance tests for historical visibility, idempotency, no-look-ahead, and current-row behavior.

Changes

Bitemporal Holdings

Layer / File(s) Summary
Temporal schema and current view
alembic/versions/012_bitemporal_holdings.py, schema.sql
Holdings gain content hashes, knowledge timestamps, supersession timestamps, versions, supporting indexes, and a current-row view.
Append-only holdings ingestion
etl/ingest_flow.py, etl/edgar_flow.py
Ingestion detects bitemporal columns, skips identical re-ingests, supersedes active rows, and inserts new versioned holdings while retaining legacy behavior otherwise.
Current and as-of retrieval
etl/point_in_time.py
New helpers return non-superseded holdings and select authoritative holdings visible at a specified knowledge time.
Bitemporal behavior validation
tests/test_bitemporal_holdings.py
Acceptance tests cover amendments, no-look-ahead, idempotency, current holdings, and the loss of history under deletion-based updates.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IngestFlow
  participant HoldingsTable
  participant FilingsTable
  participant PointInTime
  IngestFlow->>HoldingsTable: compare content hash
  IngestFlow->>FilingsTable: resolve filing periods
  IngestFlow->>HoldingsTable: supersede active rows and insert version
  PointInTime->>FilingsTable: select latest filings before cutoff
  PointInTime->>HoldingsTable: apply temporal visibility filters
  HoldingsTable-->>PointInTime: return historical holdings
Loading

Possibly related PRs

Suggested labels: agent:codex, verify:compare

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: bitemporal holdings storage plus as-of query support.
Linked Issues check ✅ Passed The PR appears to implement the linked foundation work: temporal columns, append-only versioning, no-op reingestion, as-of queries, and a current-view helper.
Out of Scope Changes check ✅ Passed The changes stay focused on the bitemporal holdings foundation, migration, query layer, and supporting tests with no obvious unrelated additions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/issue-1463-bitemporal-holdings

Comment @coderabbitai help to get the list of available commands.

@stranske
stranske temporarily deployed to agent-standard July 24, 2026 13:59 — with GitHub Actions Inactive
@stranske

stranske commented Jul 24, 2026

Copy link
Copy Markdown
Owner Author

Runner dispatch state for autofix on PR #1474. Do not edit.

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Autofix updated these files:

  • alembic/versions/012_bitemporal_holdings.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@alembic/versions/012_bitemporal_holdings.py`:
- Around line 23-44: Update the migration’s knowledge_time handling in the
holdings table alteration: add it as nullable initially without applying the
server default to existing rows, run the existing created_at-based backfill,
then alter knowledge_time to nullable=False while retaining CURRENT_TIMESTAMP as
the server default for future inserts. Preserve the existing superseded_at and
version changes.
- Around line 58-67: Update the v_current_holdings view creation in the
migration to use PostgreSQL-compatible CREATE OR REPLACE VIEW syntax instead of
CREATE VIEW IF NOT EXISTS, matching the existing schema.sql convention while
preserving the current SELECT and filtering logic.

In `@etl/point_in_time.py`:
- Around line 95-147: Update holdings_as_of’s filing-selection logic around
filing_rows and latest_by_period to apply knowledge-time visibility before
choosing the latest filing for each period. Exclude filings ingested after as_of
when has_knowledge is available, using the existing knowledge-time column and
parameter conventions, while preserving the filed_date cutoff and fallback
behavior for schemas without knowledge metadata.

In `@tests/test_bitemporal_holdings.py`:
- Around line 58-136: The test
test_holdings_as_of_preserves_versions_and_no_lookahead must cover an amendment
whose filed_date is on or before the as-of timestamp while its knowledge_time is
later. Add or adjust the amendment scenario so holdings_as_of queried before
that knowledge_time returns the prior filing version, then retain assertions
that the amended version appears after its knowledge_time.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 93bd8c83-82b0-43d2-bac7-82a9fa6cc5f4

📥 Commits

Reviewing files that changed from the base of the PR and between 705e427 and 34c4f14.

📒 Files selected for processing (6)
  • alembic/versions/012_bitemporal_holdings.py
  • etl/edgar_flow.py
  • etl/ingest_flow.py
  • etl/point_in_time.py
  • schema.sql
  • tests/test_bitemporal_holdings.py

Comment thread alembic/versions/012_bitemporal_holdings.py Outdated
Comment thread alembic/versions/012_bitemporal_holdings.py Outdated
Comment thread etl/point_in_time.py
Comment thread tests/test_bitemporal_holdings.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
etl/point_in_time.py (1)

39-61: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep holdings identifier handling consistent across both retrieval helpers.

The code advertises support for either holding_id or id, but both query paths hard-code h.holding_id, causing runtime failures on id-only schemas.

  • etl/point_in_time.py#L39-L61: derive the ordering column from the detected schema and use it in both current_holdings SQL branches.
  • etl/point_in_time.py#L139-L145: apply the same detected ordering column in holdings_as_of, or reject schemas without holding_id consistently.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@etl/point_in_time.py` around lines 39 - 61, Make holdings ordering
schema-aware in both current_holdings (etl/point_in_time.py:39-61) and
holdings_as_of (etl/point_in_time.py:139-145): derive the ordering column as
holding_id when available, otherwise id, and use it in each SQL ORDER BY. Keep
identifier detection consistent across both helpers, or reject id-only schemas
consistently if holdings_as_of cannot support them.
♻️ Duplicate comments (2)
alembic/versions/012_bitemporal_holdings.py (2)

36-40: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

The knowledge_time backfill is still unreachable.

Because the column is added as NOT NULL with a CURRENT_TIMESTAMP default, existing rows already have non-null values; this WHERE knowledge_time IS NULL matches nothing. Historical rows therefore lose their original created_at knowledge time. Add the column nullable, backfill, then enforce NOT NULL and the default.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic/versions/012_bitemporal_holdings.py` around lines 36 - 40, Update the
holdings migration around the knowledge_time column creation and backfill: add
knowledge_time as nullable without the default, execute the existing
created_at-based backfill for all historical rows, then enforce the NOT NULL
constraint and CURRENT_TIMESTAMP default afterward.

54-59: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Use a PostgreSQL-compatible view creation path.

CREATE VIEW IF NOT EXISTS is invalid PostgreSQL syntax, so this migration fails on PostgreSQL despite the SQLite/PostgreSQL compatibility requirement. Use a dialect branch (CREATE OR REPLACE VIEW for PostgreSQL and CREATE VIEW IF NOT EXISTS for SQLite), or another tested portable strategy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic/versions/012_bitemporal_holdings.py` around lines 54 - 59, Update the
v_current_holdings creation in the migration upgrade path to use a
PostgreSQL-compatible dialect branch: CREATE OR REPLACE VIEW for PostgreSQL and
CREATE VIEW IF NOT EXISTS for SQLite. Preserve the existing view definition and
ensure the selected SQL executes correctly for each supported dialect.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@etl/point_in_time.py`:
- Around line 39-61: Make holdings ordering schema-aware in both
current_holdings (etl/point_in_time.py:39-61) and holdings_as_of
(etl/point_in_time.py:139-145): derive the ordering column as holding_id when
available, otherwise id, and use it in each SQL ORDER BY. Keep identifier
detection consistent across both helpers, or reject id-only schemas consistently
if holdings_as_of cannot support them.

---

Duplicate comments:
In `@alembic/versions/012_bitemporal_holdings.py`:
- Around line 36-40: Update the holdings migration around the knowledge_time
column creation and backfill: add knowledge_time as nullable without the
default, execute the existing created_at-based backfill for all historical rows,
then enforce the NOT NULL constraint and CURRENT_TIMESTAMP default afterward.
- Around line 54-59: Update the v_current_holdings creation in the migration
upgrade path to use a PostgreSQL-compatible dialect branch: CREATE OR REPLACE
VIEW for PostgreSQL and CREATE VIEW IF NOT EXISTS for SQLite. Preserve the
existing view definition and ensure the selected SQL executes correctly for each
supported dialect.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8668147c-71c3-4bdb-899a-7d5131ac7d63

📥 Commits

Reviewing files that changed from the base of the PR and between 34c4f14 and 6a423c8.

📒 Files selected for processing (3)
  • alembic/versions/012_bitemporal_holdings.py
  • etl/point_in_time.py
  • tests/test_bitemporal_holdings.py

@stranske

Copy link
Copy Markdown
Owner Author

Addressed the current CodeRabbit findings in fe1cc98. The migration now backfills knowledge_time before enforcing its default/NOT NULL constraint and uses a PostgreSQL-safe view statement; as-of filing selection excludes rows learned after the query instant; both helpers support id-only holdings tables; and the amended-filing test now exercises filed-before/learned-after visibility. Validation: pytest tests/test_bitemporal_holdings.py tests/test_config_migration_consistency.py -q (9 passed), ruff check, and an id-only SQLite smoke test.

@stranske
stranske merged commit 36bb103 into main Jul 24, 2026
34 checks passed
@stranske
stranske deleted the cursor/issue-1463-bitemporal-holdings branch July 24, 2026 15:29
@stranske stranske added the verify:compare Runs verifier comparison mode after merge label Jul 24, 2026
@stranske
stranske temporarily deployed to agent-standard July 24, 2026 15:30 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Workflow state fingerprint for Agents Verifier. Do not edit.

@stranske stranske added verify:compare Runs verifier comparison mode after merge and removed verify:compare Runs verifier comparison mode after merge labels Jul 24, 2026
@stranske
stranske temporarily deployed to agent-standard July 24, 2026 15:38 — with GitHub Actions Inactive
@stranske

Copy link
Copy Markdown
Owner Author

Closer verifier-disposition audit: verifier run 30105537454 returned CONCERNS solely because it sampled ci.yml before its Gate completed. Current post-merge Gate is fully green (Validate inputs, lint-format, ruff, mypy, Python 3.12/3.13, logs summary, gate-summary); no review threads remain. origin/main is content-identical to the validated PR head e934ff7. Re-ran the exact acceptance commands on that merged content: PYTHONPATH=. /opt/anaconda3/bin/python3.12 -m pytest tests/test_bitemporal_holdings.py -q (4 passed) and PYTHONPATH=. /opt/anaconda3/bin/python3.12 -m pytest tests/test_ingest_flow.py::test_replace_holdings_rows_uses_postgres_transaction -q (1 passed). Reapplying verify:compare produced run 30106184689, which skipped unchanged state; the original report should therefore be treated as a CI-timing false positive, not an implementation gap. Remaining decision: accept this verified local/current-main evidence or authorize a verifier fingerprint reset/re-run.

@stranske

Copy link
Copy Markdown
Owner Author

Verifier reset executed by the closer lane: manual dispatch run 30160098574 ran the full compare path on this merged PR with green post-merge CI (it did not skip on unchanged state, because a workflow_dispatch changes the state fingerprint). It still returned CONCERNS, but the artifact shows the reason is unverified: compare mode requires two cross-family verifier judges; available families: none — the LLM evaluation never ran. The CI-timing explanation for run 30105537454 is therefore superseded: no verifier judge is configured for this repo at all, so no verify:compare run can produce a PASS here regardless of code quality. Full disposition and the exact owner decision required are recorded on source #1463.

@github-actions

Copy link
Copy Markdown
Contributor

Provider Comparison Report

Provider Summary

Provider Model Verdict Confidence Summary
openai gpt-5.4 PASS 83% The merged PR appears to satisfy the documented acceptance criteria for bitemporal holdings. It adds the required schema/migration pieces (knowledge_time, superseded_at, version, `content_has...
anthropic claude-opus-4-6 PASS 88% The PR implements all major requirements for bitemporal holdings: Alembic 012 migration with temporal columns, v_current_holdings view, append-only write paths in both ingest_flow.py and edgar_flow...
📋 Full Provider Details (click to expand)

openai

  • Model: gpt-5.4
  • Verdict: PASS
  • Confidence: 83%
  • Scores:
    • Correctness: 8.0/10
    • Completeness: 9.0/10
    • Quality: 8.0/10
    • Testing: 8.0/10
    • Risks: 7.0/10
  • Summary: The merged PR appears to satisfy the documented acceptance criteria for bitemporal holdings. It adds the required schema/migration pieces (knowledge_time, superseded_at, version, content_hash, plus v_current_holdings), introduces a point-in-time query layer in etl/point_in_time.py, and updates ingest/write paths to support append-only versioning instead of destructive mutation. The new tests/test_bitemporal_holdings.py directly covers the key behavioral requirements: retaining prior versions, reconstructing holdings as of a knowledge timestamp, and preventing look-ahead. The targeted transaction-path acceptance test remains part of the stated criteria and, given the successful post-merge test suite and no contradictory code evidence, this change set is complete enough for a PASS. Main residual risk is maintainability around cross-dialect view creation and migration parity, not a clear acceptance failure.
  • Concerns:
    • The migration creates the current-holdings view with dialect-specific SQL (CREATE OR REPLACE VIEW for Postgres vs CREATE VIEW IF NOT EXISTS elsewhere). That is workable but slightly fragile for long-term schema parity if the view definition ever changes on non-Postgres engines.
    • Because the review material includes a truncated diff, some low-level implementation details in etl/ingest_flow.py and etl/edgar_flow.py cannot be line-by-line revalidated here; verdict is based on the described changes, file list, and added tests.

anthropic

  • Model: claude-opus-4-6
  • Verdict: PASS
  • Confidence: 88%
  • Scores:
    • Correctness: 9.0/10
    • Completeness: 8.0/10
    • Quality: 8.0/10
    • Testing: 8.0/10
    • Risks: 7.0/10
  • Summary: The PR implements all major requirements for bitemporal holdings: Alembic 012 migration with temporal columns, v_current_holdings view, append-only write paths in both ingest_flow.py and edgar_flow.py with content-hash no-op, and a point_in_time.py query layer. CI passed all jobs including the specific test commands listed in acceptance criteria. The diff truncation prevents full verification of implementation details, but the structural changes align with all stated tasks. The migration, schema changes, and new files are well-organized. Minor concern about the deliberate-break test criterion not being verifiable from the available diff.
  • Concerns:
    • PR diff is truncated, preventing full review of point_in_time.py query logic, the complete _replace_holdings_rows implementation, and the full test suite — assessment relies partly on CI success signals
    • Migration backfills knowledge_time from created_at with COALESCE to CURRENT_TIMESTAMP; if created_at is widely NULL, many rows get current timestamp rather than historically accurate knowledge times
    • Cannot verify from truncated diff whether the 'deliberate-break' acceptance criterion (reverting append-only to mutate makes as-of test fail) is explicitly tested

Agreement

  • Verdict: PASS (all providers)
  • Correctness: scores within 1 point (avg 8.5/10, range 8.0-9.0)
  • Completeness: scores within 1 point (avg 8.5/10, range 8.0-9.0)
  • Quality: scores within 1 point (avg 8.0/10, range 8.0-8.0)
  • Testing: scores within 1 point (avg 8.0/10, range 8.0-8.0)
  • Risks: scores within 1 point (avg 7.0/10, range 7.0-7.0)

Disagreement

No major disagreements detected.

Unique Insights

  • openai: The migration creates the current-holdings view with dialect-specific SQL (CREATE OR REPLACE VIEW for Postgres vs CREATE VIEW IF NOT EXISTS elsewhere). That is workable but slightly fragile for long-term schema parity if the view definition ever changes on non-Postgres engines.; Because the review material includes a truncated diff, some low-level implementation details in etl/ingest_flow.py and etl/edgar_flow.py cannot be line-by-line revalidated here; verdict is based on the described changes, file list, and added tests.
  • anthropic: PR diff is truncated, preventing full review of point_in_time.py query logic, the complete _replace_holdings_rows implementation, and the full test suite — assessment relies partly on CI success signals; Migration backfills knowledge_time from created_at with COALESCE to CURRENT_TIMESTAMP; if created_at is widely NULL, many rows get current timestamp rather than historically accurate knowledge times; Cannot verify from truncated diff whether the 'deliberate-break' acceptance criterion (reverting append-only to mutate makes as-of test fail) is explicitly tested

🔍 LangSmith Traces

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:cursor Route PR to Cursor agent runner agent:retry Add to trigger agent retry after rate limit or pause agents:keepalive Enables keepalive automation for PR autofix:patch autofix Triggers autofix on PR verify:compare Runs verifier comparison mode after merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Audit] Full bitemporal holdings store + as_of() query layer (foundation)

1 participant