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


##########
view/metadata.go:
##########
@@ -518,16 +518,24 @@ func NewMetadataWithUUID(version *Version, sc 
*iceberg.Schema, location string,
                viewUUID = uuid.New()
        }
 
+       var inputProps iceberg.Properties
        formatVersion := DefaultViewFormatVersion
        if props != nil {
-               verStr, ok := props["format-version"]
+               inputProps = maps.Clone(props)
+               verStr, ok := inputProps[table.PropertyFormatVersion]
                if ok {
                        var err error
                        if formatVersion, err = strconv.Atoi(verStr); err != 
nil {
-                               formatVersion = DefaultViewFormatVersion
+                               return nil, fmt.Errorf("%w: %s", 
iceberg.ErrInvalidFormatVersion, verStr)
                        }
-                       delete(props, "format-version")
+                       delete(inputProps, table.PropertyFormatVersion)
                }
+       } else {
+               inputProps = props
+       }
+
+       if formatVersion != DefaultViewFormatVersion {

Review Comment:
   I'd make this a range check rather than an equality check — `if 
formatVersion > SupportedViewFormatVersion`. Right now it happens to be 
equivalent since both constants are 1, but once view V2 lands and they bump, 
this rejects a valid `"1"` and every other still-supported version below the 
max. The table path deliberately accepts anything in `[1, supported]` and lets 
the builder do range validation; I'd mirror that here so the two constructors 
don't drift. wdyt?



##########
view/metadata.go:
##########
@@ -518,16 +518,24 @@ func NewMetadataWithUUID(version *Version, sc 
*iceberg.Schema, location string,
                viewUUID = uuid.New()
        }
 
+       var inputProps iceberg.Properties
        formatVersion := DefaultViewFormatVersion
        if props != nil {
-               verStr, ok := props["format-version"]
+               inputProps = maps.Clone(props)
+               verStr, ok := inputProps[table.PropertyFormatVersion]
                if ok {
                        var err error
                        if formatVersion, err = strconv.Atoi(verStr); err != 
nil {
-                               formatVersion = DefaultViewFormatVersion
+                               return nil, fmt.Errorf("%w: %s", 
iceberg.ErrInvalidFormatVersion, verStr)

Review Comment:
   Heads up that the view package already has its own 
`ErrInvalidViewMetadataFormatVersion`, used by `validateViewMetadata` on the 
deserialization path. Wrapping `iceberg.ErrInvalidFormatVersion` here means the 
same package now has two sentinels for the same kind of failure, so anyone 
doing `errors.Is(err, view.ErrInvalidViewMetadataFormatVersion)` will miss 
constructor errors. I'd lean toward reusing the existing view sentinel here for 
consistency — thoughts?



##########
table/metadata.go:
##########
@@ -2205,16 +2205,20 @@ func NewMetadataWithUUID(sc *iceberg.Schema, partitions 
*iceberg.PartitionSpec,
        if tableUuid == uuid.Nil {
                tableUuid = uuid.New()
        }
+       var inputProps iceberg.Properties
        var err error
        formatVersion := DefaultFormatVersion
        if props != nil {
-               verStr, ok := props[PropertyFormatVersion]
+               inputProps = maps.Clone(props)
+               verStr, ok := inputProps[PropertyFormatVersion]
                if ok {
                        if formatVersion, err = strconv.Atoi(verStr); err != 
nil {
-                               formatVersion = DefaultFormatVersion
+                               return nil, fmt.Errorf("%w: %s", 
iceberg.ErrInvalidFormatVersion, verStr)
                        }
-                       delete(props, PropertyFormatVersion)
+                       delete(inputProps, PropertyFormatVersion)
                }
+       } else {

Review Comment:
   This `else` is a no-op — `inputProps` is already the nil zero value and we 
only get here when `props == nil`, so it assigns nil to nil. I'd drop the whole 
branch and just do `inputProps := maps.Clone(props)` up top (`maps.Clone(nil)` 
returns nil), gating the key lookup on `props != nil`. Same dead `else` over in 
`view/metadata.go` around line 534.



##########
table/metadata.go:
##########
@@ -2205,16 +2205,20 @@ func NewMetadataWithUUID(sc *iceberg.Schema, partitions 
*iceberg.PartitionSpec,
        if tableUuid == uuid.Nil {
                tableUuid = uuid.New()
        }
+       var inputProps iceberg.Properties
        var err error
        formatVersion := DefaultFormatVersion
        if props != nil {
-               verStr, ok := props[PropertyFormatVersion]
+               inputProps = maps.Clone(props)
+               verStr, ok := inputProps[PropertyFormatVersion]
                if ok {
                        if formatVersion, err = strconv.Atoi(verStr); err != 
nil {
-                               formatVersion = DefaultFormatVersion
+                               return nil, fmt.Errorf("%w: %s", 
iceberg.ErrInvalidFormatVersion, verStr)

Review Comment:
   This rejects non-numeric strings but a numeric-but-out-of-range value like 
`"0"` or `"99"` still sails past here and only gets caught later by 
`NewMetadataBuilder`. Both wrap the same sentinel so `errors.Is` is fine, but 
the error origin and message differ depending on the failure mode. Is that 
intentional? If we want the constructor to be the single validation point I'd 
add a range check right after the `Atoi`.



##########
table/metadata_internal_test.go:
##########
@@ -780,6 +780,22 @@ func TestNewMetadataWithExplicitV1Format(t *testing.T) {
        assert.Truef(t, expected.Equals(actual), "expected: %s\ngot: %s", 
expected, actual)
 }
 
+func TestNewMetadataRejectInvalidFormatVersion(t *testing.T) {
+       props := iceberg.Properties{
+               "format-version": "banana",

Review Comment:
   This only exercises the non-numeric case. The numeric out-of-range path 
(`"0"`, `"99"`) goes through a different branch — the builder rather than this 
constructor — so it's worth a case here too, especially if we tighten the 
constructor validation. The view test is already table-driven; might be worth 
matching that shape so both add the extra case in one spot.



##########
view/metadata_test.go:
##########
@@ -88,6 +89,46 @@ func TestNewMetadata(t *testing.T) {
        assert.Equal(t, []VersionLogEntry{{TimestampMS: 1000, VersionID: 1}}, 
md.VersionLog())
 }
 
+func TestNewMetadataRejectInvalidFormatVersion(t *testing.T) {
+       tests := []struct {
+               name         string
+               formatVer    string
+               versionValue string

Review Comment:
   `versionValue` is always equal to `formatVer` in both cases — I'd drop it 
and just assert against `tc.formatVer`. Separately, this file mixes the new 
`table.PropertyFormatVersion` constant with the raw `"format-version"` literal 
the other ~30 cases use; I'd pick one (raw literal avoids the `table` import 
entirely) so the file isn't half-and-half.



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