-
Notifications
You must be signed in to change notification settings - Fork 22
Fix historical association queries using proper timestamp boundary handling #366
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
valid_to to get associations|
@copilot please ignore all the previous attempt Let's start again with this knowledge (from your colleague Claude AI) I do not trust its solution, but please attempt to understand what it is saying: ChronoModel Issue #283: Summary of FindingsThe ProblemWhen objects are updated in the same transaction, they get the same Clear Example# Transaction at timestamp T = 2025-08-16 16:05:17
country = Country.create name: 'Country'
city = country.cities.create name: 'City'
Country.transaction do
country.update_column :name, 'Country 2'
city.update_column :name, 'City 2'
endResulting ranges:
The BugWhen historical country queries for cities using its -- ChronoModel's query:
T <@ history.cities.validity
-- Results:
T <@ [T2, T) = false ✅ (excludes historical "City")
T <@ [T, ∞) = true ❌ (includes current "City 2" - WRONG!)Expected: Get "City" (historical) Why This HappensPostgreSQL ranges are half-open by default:
The Solution: Subtract 1 MicrosecondInstead of querying at the exact # Instead of:
time = historical_country.validity.end # T
# Use:
safe_time = historical_country.validity.end - 0.000001 # T - 1μs
# Query becomes:
(T - 1μs) <@ [T2, T) = true ✅ (includes historical "City")
(T - 1μs) <@ [T, ∞) = false ✅ (excludes current "City 2")Why Excluding valid_to is Correct (Finite Ranges)When
Why Excluding valid_to is Wrong (Infinite Ranges)For current objects with infinite
The Complete Fixdef build_temporal_condition(table_name, range, time)
col = "#{table_name}.#{range.name}"
# For historical objects (finite valid_to), subtract 1μs to avoid edge case
# For current objects (infinite valid_to), use the time as-is
safe_time = if time.respond_to?(:infinite?) && time.infinite?
time # Keep infinite time as-is
else
# Subtract 1 microsecond to get "just before" the boundary
adjusted_time = time - 0.000001
"'#{adjusted_time.strftime('%Y-%m-%d %H:%M:%S.%6N')}'::timestamp"
end
Arel.sql("#{safe_time} <@ #{col}")
endResult:
|
I've completely rewritten the Key changes:
Why this works:
Fixed in commit 4e9b44f. |
tagliala
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've implemented Claude AI's solution. The fix now properly distinguishes between infinite and finite timestamps:
All time machine tests pass (285 examples, 0 failures). Fixed in commit 5fba3dc. |
|
@copilot add an empty commit (no file changes) with a good commit message for this change, according to contributing guidelines and you instructions, and 7 rules for a good commit, and the issue it fixes in order to automatically mark the issue as "closed" with a human-understandable of what the issue was and why you did change what you changed |
Created empty commit ac6c017 with comprehensive message that follows the 7 rules for good commits and references "Fixes #283" to automatically close the issue. The commit message explains the PostgreSQL tsrange boundary issue and how the fix ensures historical association queries work correctly by subtracting 1 microsecond from finite valid_to timestamps. |
ac6c017 to
7966a83
Compare
5f36ef8 to
a3beb9f
Compare
Historical objects were incorrectly using their `valid_to` timestamp to query for associations, which could return records that didn't exist during the object's validity period. PostgreSQL ranges are left-inclusive and right-exclusive (`[start, end)`). When querying associations from historical records at exactly `valid_to`, the query could find records that became valid at that boundary time, returning the wrong version due to half-open range semantics. The fix subtracts 1 unit at precision 6 (1 microsecond) from finite `valid_to` timestamps when querying associations, ensuring queries happen within the historical record's validity period rather than at boundary times. Fixes #283
a3beb9f to
64da61a
Compare
Claude 4:I'll take a look at the pull request to understand the problem and evaluate the proposed fix.Looking at this pull request, I think both the problem identification and the proposed fix are excellent. Here's my analysis: The Problem is Real and SignificantThe issue with historical association queries is a classic edge case that stems from PostgreSQL's half-open range semantics The Solution is Elegant and Well-Thought-OutThe fix shows sophisticated understanding of the underlying issue:
Code Quality ObservationsThe PR demonstrates excellent practices:
Potential ConsiderationsThe microsecond precision adjustment is clever, but it assumes that business logic granularity doesn't require microsecond-level precision for temporal queries. For most applications, this is a reasonable assumption, but it's worth documenting this limitation. Overall, this is a high-quality fix that addresses a subtle but important bug in temporal data handling. The solution shows deep understanding of both the ChronoModel architecture and PostgreSQL's temporal semantics. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR fixes a boundary timestamp issue in historical association queries where records were returning incorrect associations due to PostgreSQL's half-open range semantics. The fix ensures that when querying associations from historical records, the timestamp used is strictly within the record's validity period rather than at the boundary where wrong versions might be selected.
- Replaces simple
valid_toalias with a boundary-awareas_of_timemethod that subtracts 1 microsecond from finite timestamps - Introduces a new constant
VALIDITY_TSRANGE_PRECISIONfor consistent timestamp precision handling - Adds comprehensive test coverage for the historical association boundary case
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
lib/chrono_model.rb |
Adds VALIDITY_TSRANGE_PRECISION constant for microsecond-level timestamp precision |
lib/chrono_model/time_machine/history_model.rb |
Replaces as_of_time alias with boundary-aware method that handles timestamp precision |
spec/chrono_model/time_machine/historical_association_spec.rb |
Adds new test file covering the historical association boundary case |
spec/chrono_model/history_models_spec.rb |
Updates expected history models to include test models from the new spec |
Historical objects were incorrectly using their
valid_totimestamp to query for associations, which could return records that didn't exist during the object's validity period.Problem
When querying associations from historical records, ChronoModel was using the
as_of_timemethod which was aliased tovalid_to(the upper bound of the validity range). Since PostgreSQL ranges are left-inclusive and right-exclusive[start, end), querying at exactlyvalid_tocould find records that became valid at that boundary time, even though they didn't exist during the historical record's validity period.Consider this example:
The historical country record was querying for cities at its
valid_totimestamp, which coincided with when the new city version became valid, resulting in the wrong association being returned.Solution
Modified the
as_of_timemethod to handle boundary timestamp issues:The implementation uses
valid_to.is_a?(Time)to detect finite timestamps and leverages the existingChronoModel::VALIDITY_TSRANGE_PRECISIONconstant for consistent precision handling.This ensures association queries happen at timestamps that existed during the historical record's validity period, not at boundary times where the wrong version might be selected due to PostgreSQL's half-open range semantics.
Fixes #283.
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.