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


##########
table/transaction.go:
##########
@@ -142,6 +153,17 @@ func (t *Transaction) apply(updates []Update, reqs 
[]Requirement) error {
        return nil
 }
 
+// requirementSemanticKey assumes Requirement JSON marshaling is canonical and

Review Comment:
   More of a design question than a blocker: this comment names a load-bearing 
invariant — every `Requirement` marshals to canonical, deterministic JSON — 
that nothing enforces. It holds today because every requirement is flat scalars 
in declaration order, but `Requirement` is exported, and a future field that's 
a map (non-deterministic order) or one tagged `json:"-"` (two distinct 
requirements collapsing to one key) would break the key silently.
   
   Two ways to think about it. Keep `json.Marshal` but guard the invariant with 
a test that round-trips each concrete requirement type twice and asserts equal 
keys — cheap, and it turns the comment into something enforced. Or give 
`Requirement` a typed `SemanticKey()` (e.g. `"assert-ref-snapshot-id:main:10"`) 
so the equivalence contract is explicit rather than a side effect of marshaling 
— more code, but it's the more honest abstraction and it'd sidestep the 
same-ref question above too. Which direction feels right to you?



##########
table/transaction.go:
##########
@@ -114,12 +114,23 @@ func (t *Transaction) apply(updates []Update, reqs 
[]Requirement) error {
 
        existing := map[string]struct{}{}
        for _, r := range t.reqs {
-               existing[r.GetType()] = struct{}{}
+               key, err := requirementSemanticKey(r)
+               if err != nil {
+                       return err
+               }
+
+               existing[key] = struct{}{}
        }
 
        for _, r := range reqs {
-               if _, ok := existing[r.GetType()]; !ok {
+               key, err := requirementSemanticKey(r)
+               if err != nil {
+                       return err
+               }
+
+               if _, ok := existing[key]; !ok {

Review Comment:
   Now that the key is the full payload, I'd like to think through one case 
with you. If `apply` runs more than once on the same `Transaction` for the same 
ref at different snapshot IDs — say an append on an empty table emits 
`AssertRefSnapshotID("main", nil)`, then a later append emits 
`AssertRefSnapshotID("main", &firstID)` — those serialize to different JSON 
(`"snapshot-id":null` vs `:<id>`), so both survive dedupe and both go to the 
catalog. The base state only matches one of them, so I'd expect the other to 
409. The old type-only key collapsed them into one.
   
   Is that path reachable in practice, or does `apply` only ever see one 
assertion per ref per transaction? If it's reachable, the design question is 
what the key should really be: full payload preserves the genuinely-distinct 
refs you're fixing, but it also preserves same-ref/different-id pairs that we 
probably don't want both of. Java threads this with a per-ref `changedRefs` set 
— first assertion per ref wins. One option here is to key `assertRefSnapshotID` 
on type + ref and fall back to payload for the rest. How are you thinking about 
it?



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