laskoviymishka commented on code in PR #1370:
URL: https://github.com/apache/iceberg-go/pull/1370#discussion_r3539727946


##########
table/transaction.go:
##########
@@ -153,9 +153,25 @@ func (t *Transaction) apply(updates []Update, reqs 
[]Requirement) error {
        return nil
 }
 
-// requirementSemanticKey assumes Requirement JSON marshaling is canonical and
-// deterministic for every requirement type that participates in dedupe.
+// requirementSemanticKey computes the dedupe key for a requirement.
+//
+// For most requirement types the key is the canonical JSON marshaling, which
+// keeps semantically distinct requirements (e.g. assertions on different refs)
+// from collapsing into one another.
+//
+// assert-ref-snapshot-id is special-cased to key by requirement type + ref 
name
+// only, deliberately ignoring the asserted snapshot id. Within a single
+// transaction the builder mutates its own ref state across operations (e.g. 
the
+// first append asserts main == nil, a later append asserts main == 
snapshot-1),
+// which would otherwise produce multiple, mutually contradictory base-state
+// assertions for the same ref against the pre-transaction metadata. Keying by
+// ref name keeps only the first assertion for each ref while still letting
+// assertions for different refs survive dedupe.
 func requirementSemanticKey(r Requirement) (string, error) {
+       if ref, ok := r.(*assertRefSnapshotID); ok {
+               return fmt.Sprintf("%s\x00%s", reqAssertRefSnapshotID, 
ref.Ref), nil

Review Comment:
   Tiny one: this can just be `reqAssertRefSnapshotID + "\x00" + ref.Ref` — 
perfsprint won't love a `Sprintf` that's really string concatenation, and it's 
a hair faster.
   
   While we're here, a half-line noting `\x00` is a safe separator because 
Iceberg ref names can't contain NUL would save the next person a double-take.



##########
table/transaction.go:
##########
@@ -153,9 +153,25 @@ func (t *Transaction) apply(updates []Update, reqs 
[]Requirement) error {
        return nil
 }
 
-// requirementSemanticKey assumes Requirement JSON marshaling is canonical and
-// deterministic for every requirement type that participates in dedupe.
+// requirementSemanticKey computes the dedupe key for a requirement.
+//
+// For most requirement types the key is the canonical JSON marshaling, which
+// keeps semantically distinct requirements (e.g. assertions on different refs)
+// from collapsing into one another.
+//
+// assert-ref-snapshot-id is special-cased to key by requirement type + ref 
name
+// only, deliberately ignoring the asserted snapshot id. Within a single
+// transaction the builder mutates its own ref state across operations (e.g. 
the
+// first append asserts main == nil, a later append asserts main == 
snapshot-1),
+// which would otherwise produce multiple, mutually contradictory base-state
+// assertions for the same ref against the pre-transaction metadata. Keying by
+// ref name keeps only the first assertion for each ref while still letting
+// assertions for different refs survive dedupe.
 func requirementSemanticKey(r Requirement) (string, error) {
+       if ref, ok := r.(*assertRefSnapshotID); ok {

Review Comment:
   The whole first-wins-is-base-state property only holds because `commit()` 
captures the builder's ref state before `apply()` applies that op's updates — 
so the first assertion we see for a ref is the pre-transaction one. That's true 
today, but it's an implicit ordering contract, not something the types enforce.
   
   I'd add a line here (and near the `commit()` call site in 
`snapshot_producers.go`) spelling out that dedupe keeps the first assertion 
*because* it's the base-state one, and that this breaks silently — wrong 
precondition to the catalog, no error — if a future refactor ever advances the 
builder before `commit()` runs. Cheap insurance. wdyt?



##########
table/transaction_internal_test.go:
##########
@@ -60,6 +60,116 @@ func 
TestTransactionApplyDedupesEquivalentRequirementsWithinAndAcrossCalls(t *te
        requireContainsRefSnapshotRequirement(t, txn.reqs, MainBranch, 
&mainSnapshotID)
 }
 
+// TestTransactionApplyDedupesSameRefAssertionsNewTable covers two appends in a
+// single new-table transaction: the first asserts main must not exist, and the
+// second (after the builder has created main) asserts main == the new 
snapshot.
+// Only the first main == nil assertion, which reflects the pre-transaction 
base
+// state, must be retained.
+func TestTransactionApplyDedupesSameRefAssertionsNewTable(t *testing.T) {
+       txn, _ := createTestTransactionWithMemIO(t, *iceberg.UnpartitionedSpec)
+
+       // First append: main does not exist yet in the pre-transaction 
metadata.
+       err := txn.apply(nil, []Requirement{AssertRefSnapshotID(MainBranch, 
nil)})
+       require.NoError(t, err)
+       require.Len(t, txn.reqs, 1)
+       requireContainsRefSnapshotRequirement(t, txn.reqs, MainBranch, nil)
+
+       // Simulate the first append creating main -> 10 in the transaction 
builder.
+       require.NoError(t, txn.meta.AddSnapshot(&Snapshot{
+               SnapshotID:     10,
+               SequenceNumber: 1,
+               ManifestList:   
"mem://default/table-location/metadata/manifest-10.avro",
+               Summary:        &Summary{Operation: OpAppend},
+               TimestampMs:    time.Now().UnixMilli(),
+       }))
+       require.NoError(t, txn.meta.SetSnapshotRef(MainBranch, 10, BranchRef))
+
+       // Second append asserts main == 10; it must dedupe against the first
+       // assertion for main rather than adding a contradictory base-state 
check.
+       newHead := int64(10)
+       err = txn.apply(nil, []Requirement{AssertRefSnapshotID(MainBranch, 
&newHead)})

Review Comment:
   Optional: every one of these uses two separate `apply()` calls, so the 
cross-call dedupe path is well covered but the within-a-single-`apply()` case — 
same ref twice in one reqs slice, different snapshot ids — isn't. The loop 
handles it today, but a future "build a set then append" refactor would pass 
all these tests while quietly breaking the within-call case. A quick variant of 
this test with both assertions in one `apply()` would close that.



-- 
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