laskoviymishka commented on code in PR #3260:
URL: https://github.com/apache/iceberg-rust/pull/3260#discussion_r4148496335


##########
crates/iceberg/src/scan/mod.rs:
##########
@@ -282,7 +317,15 @@ impl<'a> TableScanBuilder<'a> {
 
     /// Build the table scan.
     pub fn build(self) -> Result<TableScan> {
-        let snapshot = match self.snapshot_id {
+        let snapshot_id = match self.snapshot_selection {
+            Some(SnapshotSelection::SnapshotId(id)) => Some(id),
+            Some(SnapshotSelection::AsOfTime(timestamp_ms)) => 
Some(snapshot_id_as_of_time(

Review Comment:
   When `as_of_time` resolves to a since-GC'd snapshot, `build()` fails with 
the bare `Snapshot with id {X} not found` from the id path — the timestamp that 
produced X is gone, so a caller can't tell an expired time-travel target from a 
stale `snapshot_id`. I'd wrap the not-found case in this branch to carry the 
timestamp, e.g. `Snapshot ... resolved from timestamp {timestamp_ms} ms has 
expired`. Non-blocking.



##########
crates/iceberg/src/util/snapshot.rs:
##########
@@ -76,10 +78,57 @@ pub fn ancestors_between(
     })
 }
 
+/// Resolve the snapshot ID from the main-history entry with the greatest 
timestamp
+/// at or before `timestamp_ms` (milliseconds since the Unix epoch), taking 
the first
+/// entry on ties.
+///
+/// This matches Java's ordering. PyIceberg takes the last qualifying log 
entry,
+/// so ties or out-of-order timestamps can produce different results.
+///
+/// Returns [`ErrorKind::DataInvalid`] if no matching history exists.
+/// The returned snapshot may have expired, so
+/// [`snapshot_by_id`](crate::spec::TableMetadata::snapshot_by_id) can still 
return `None`.
+///
+/// ```
+/// # use iceberg::TableIdent;
+/// # use iceberg::io::FileIO;
+/// # use iceberg::table::StaticTable;
+/// use iceberg::util::snapshot::snapshot_id_as_of_time;
+/// # #[tokio::main]
+/// # async fn main() -> iceberg::Result<()> {
+/// # let location = concat!(env!("CARGO_MANIFEST_DIR"), 
"/testdata/example_table_metadata_v2.json");
+/// # let ident = TableIdent::from_strs(["ns", "t"])?;
+/// # let table = StaticTable::from_metadata_file(location, ident, 
FileIO::new_with_fs()).await?;
+/// let metadata = table.metadata();
+/// // The snapshot log has entries at 1515100955770 and 1555100955770.
+/// let snapshot_id = snapshot_id_as_of_time(&metadata, 1_555_100_955_770)?;
+/// assert_eq!(snapshot_id, 3055729675574597004);
+/// assert!(snapshot_id_as_of_time(&metadata, 1_515_100_955_769).is_err());
+/// # Ok(())
+/// # }
+/// ```
+pub fn snapshot_id_as_of_time(table_metadata: &TableMetadataRef, timestamp_ms: 
i64) -> Result<i64> {
+    table_metadata
+        .history()
+        .iter()
+        .enumerate()
+        .filter(|(_, entry)| entry.timestamp_ms() <= timestamp_ms)
+        // Keep the first entry on ties, matching Java's timestamp selection.
+        .max_by_key(|(idx, entry)| (entry.timestamp_ms(), Reverse(*idx)))

Review Comment:
   Still the one place the tie-break leans on the comment rather than the shape 
— `Reverse(*idx)` inside `max_by_key` is two hops to read as "first entry 
wins." Not gating, but if you're in here anyway, an explicit comparator says it 
directly:
   
   ```rust
   .max_by(|(idx_a, a), (idx_b, b)| {
       a.timestamp_ms()
           .cmp(&b.timestamp_ms())
           .then(idx_b.cmp(idx_a)) // earlier index wins on ties
   })
   ```



##########
crates/iceberg/src/util/snapshot.rs:
##########
@@ -76,10 +78,57 @@ pub fn ancestors_between(
     })
 }
 
+/// Resolve the snapshot ID from the main-history entry with the greatest 
timestamp
+/// at or before `timestamp_ms` (milliseconds since the Unix epoch), taking 
the first
+/// entry on ties.
+///
+/// This matches Java's ordering. PyIceberg takes the last qualifying log 
entry,

Review Comment:
   This still reads as a flat "matches Java's ordering" — the one spot it 
doesn't is `i64::MIN`, where Java's `Long.MIN_VALUE` seed plus strict `>` 
matches nothing but we return the entry. The test comment already notes this; 
I'd lift a sentence of it up into the docstring so API consumers see the 
sentinel edge. Same wording point from last round, still not gating.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to