laskoviymishka commented on code in PR #1326:
URL: https://github.com/apache/iceberg-go/pull/1326#discussion_r3491574443
##########
catalog/hadoop/hadoop.go:
##########
@@ -509,10 +550,13 @@ func (c *Catalog) CommitTable(ctx context.Context, ident
table.Identifier, reqs
return nil, "", fmt.Errorf("hadoop catalog: failed to create
metadata directory: %w", err)
}
- newMetaPath := c.metadataFilePath(ident, newVersion)
- tempPath := filepath.Join(metaDir, uuid.New().String()+".metadata.json")
-
compression := updated.Properties().Get(table.MetadataCompressionKey,
table.MetadataCompressionDefault)
+ newMetaPath, err := c.metadataFilePathForCompression(ident, newVersion,
compression)
Review Comment:
The conflict guard below only stats the codec-specific path for
`newVersion`, so cross-codec races slip through. If writer A plans
`v2.gz.metadata.json` and writer B plans `v2.metadata.json` concurrently,
neither `Stat` sees the other and both commit — two different files both
claiming version 2, breaking the monotonic chain.
Java's `getMetadataFile` iterates every codec variant, so its conflict
detection is codec-agnostic by construction.
I'd make the guard probe all variants: call `metadataVersionLocation(ident,
newVersion)` and fail the commit if it finds any file for that version,
regardless of suffix. That also covers the migration case where someone
switches codecs between commits.
##########
catalog/hadoop/hadoop.go:
##########
@@ -184,8 +186,20 @@ func (c *Catalog) metadataDir(ident table.Identifier)
string {
return filepath.Join(c.tableToPath(ident), "metadata")
}
-func (c *Catalog) metadataFilePath(ident table.Identifier, version int) string
{
- return filepath.Join(c.metadataDir(ident),
fmt.Sprintf("v%d.metadata.json", version))
+func (c *Catalog) metadataFilePathForCompression(ident table.Identifier,
version int, compression string) (string, error) {
+ var suffix string
+ switch compression {
+ case table.MetadataCompressionCodecNone:
+ suffix = ".metadata.json"
+ case table.MetadataCompressionCodecGzip:
+ suffix = ".gz.metadata.json"
+ case table.MetadataCompressionCodecZstd:
+ suffix = ".zstd.metadata.json"
Review Comment:
I think the zstd branch here is a cross-client trap. `zstd` isn't a value
Java's `TableMetadataParser.Codec` understands — its enum is only `NONE("")`
and `GZIP(".gz")`, and `HadoopTableOperations.getMetadataFile` only ever probes
`Codec.values()`, so a `vN.zstd.metadata.json` is never even looked for.
PyIceberg falls back to `NOOP_COMPRESSOR` for any non-`.gz` suffix and reads
the compressed bytes as garbage, and iceberg-rust rejects zstd outright in
`parse_metadata_file_compression`.
So a Hadoop-catalog table we write with zstd is invisible-or-corrupt to
every other Iceberg client, and the Hadoop catalog is precisely the
shared-filesystem case where that interop matters most.
I'd gate zstd at this layer for the Hadoop catalog — return the same
"unsupported codec" error you already produce for `brotli`, so the failure is
loud at write time rather than a silent unreadable file. If the intent is
genuinely a go-to-go-only extension, that needs a prominent warning in the PR
description and CHANGES.md and a clear note in the code that this is
non-portable. wdyt?
##########
catalog/hadoop/hadoop.go:
##########
@@ -330,16 +355,30 @@ func (c *Catalog) findVersion(ident table.Identifier)
(int, error) {
return nil
})
if err != nil {
- return 0, fmt.Errorf("hadoop catalog: cannot read metadata
directory for %s: %w",
+ return "", 0, fmt.Errorf("hadoop catalog: cannot read metadata
directory for %s: %w",
strings.Join(ident, "."), catalog.ErrNoSuchTable)
}
if maxVer == 0 {
- return 0, fmt.Errorf("hadoop catalog: no metadata files found
for table %s: %w",
+ return "", 0, fmt.Errorf("hadoop catalog: no metadata files
found for table %s: %w",
+ strings.Join(ident, "."), catalog.ErrNoSuchTable)
+ }
+
+ metaPath, ok := c.metadataVersionLocation(ident, maxVer)
Review Comment:
The `WalkDir` above already saw the exact filename for `maxVer` (it matched
`versionPattern` to compute the version), but we throw the name away and only
keep the integer — then re-probe here with the fixed plain→gz→zstd order.
That re-probe is where stale data wins: if both `v5.metadata.json` (old) and
`v5.gz.metadata.json` (new) exist, plain is probed first and silently shadows
the compressed current version, so the catalog serves stale uncompressed
metadata even though the authoritative version is the gzip one. A compression
migration would be quietly defeated.
I'd capture the matched filename in the `WalkDir` callback (it's the `path`
param) when `v > maxVer`, and thread that straight into `scanForward` so we
never re-probe a file we just saw. That removes the shadowing and drops the
extra `Stat`s per lookup on the slow path.
##########
catalog/hadoop/hadoop_test.go:
##########
@@ -392,6 +416,33 @@ func (s *HadoopCatalogTestSuite)
TestFindVersionMixedGzipAndPlain() {
s.Equal(3, ver)
}
+func (s *HadoopCatalogTestSuite) TestFindVersionZstdOnlyWithHint() {
+ ident := []string{"ns", "tbl"}
+ dir := s.cat.metadataDir(ident)
+ s.Require().NoError(os.MkdirAll(dir, 0o755))
+ s.Require().NoError(os.WriteFile(filepath.Join(dir,
"v1.zstd.metadata.json"), nil, 0o644))
+ s.Require().NoError(os.WriteFile(filepath.Join(dir,
"v2.zstd.metadata.json"), nil, 0o644))
+ s.cat.writeVersionHint(ident, 1)
+
+ ver, err := s.cat.findVersion(ident)
+ s.Require().NoError(err)
+ s.Equal(2, ver)
Review Comment:
`TestFindVersionZstdOnlyWithHint` and `TestFindVersionMixedZstdAndPlain`
only assert the version number, never the returned path — so the part of the
fix that actually matters (that `findMetadataLocation` returns the
codec-suffixed path, not a reconstructed plain one) isn't directly exercised
here. The empty `nil` files are fine since these call only `Stat`, but the path
is the thing the LoadTable fix hinges on.
I'd add a direct `findMetadataLocation` assertion that the returned path
ends in `.zstd.metadata.json` for the zstd-only case and `.gz.metadata.json`
for a gzip-only one. That's also the natural place to add the shadowing case
from the comment on `findMetadataLocation` — write both `v3.metadata.json` and
`v3.gz.metadata.json` and assert which path wins, so the resolution is pinned
by a test.
##########
catalog/hadoop/hadoop_test.go:
##########
@@ -129,12 +129,24 @@ func (s *HadoopCatalogTestSuite) TestMetadataDir() {
s.Equal(filepath.Join(s.warehouse, "ns", "tbl", "metadata"), path)
}
-func (s *HadoopCatalogTestSuite) TestMetadataFilePath() {
- path := s.cat.metadataFilePath([]string{"ns", "tbl"}, 1)
+func (s *HadoopCatalogTestSuite) TestMetadataFilePathForCompression() {
+ ident := []string{"ns", "tbl"}
+
+ path, err := s.cat.metadataFilePathForCompression(ident, 1,
table.MetadataCompressionCodecNone)
+ s.Require().NoError(err)
s.Equal(filepath.Join(s.warehouse, "ns", "tbl", "metadata",
"v1.metadata.json"), path)
- path = s.cat.metadataFilePath([]string{"ns", "tbl"}, 42)
- s.Equal(filepath.Join(s.warehouse, "ns", "tbl", "metadata",
"v42.metadata.json"), path)
+ path, err = s.cat.metadataFilePathForCompression(ident, 1,
table.MetadataCompressionCodecGzip)
+ s.Require().NoError(err)
+ s.Equal(filepath.Join(s.warehouse, "ns", "tbl", "metadata",
"v1.gz.metadata.json"), path)
+
+ path, err = s.cat.metadataFilePathForCompression(ident, 1,
table.MetadataCompressionCodecZstd)
+ s.Require().NoError(err)
+ s.Equal(filepath.Join(s.warehouse, "ns", "tbl", "metadata",
"v1.zstd.metadata.json"), path)
Review Comment:
The old `TestMetadataFilePath` covered version 1 and version 42; the rewrite
only keeps version 1 across the three codecs. The multi-digit case is the
normal path for any table past v9, and `fmt.Sprintf("v%d%s", ...)` is exactly
where a width/format slip would hide.
I'd carry a `version=42` assertion over for at least one codec while we're
here.
##########
catalog/hadoop/hadoop.go:
##########
@@ -270,36 +284,47 @@ func (c *Catalog) writeVersionHint(ident
table.Identifier, version int) {
}
}
-// metadataVersionExists checks whether a metadata file for the given version
-// exists in either plain or gzip-compressed form.
-func (c *Catalog) metadataVersionExists(ident table.Identifier, version int)
bool {
+// metadataVersionLocation returns the path for the given metadata version in
+// plain, gzip-compressed, or zstd-compressed form.
+func (c *Catalog) metadataVersionLocation(ident table.Identifier, version int)
(string, bool) {
dir := c.metadataDir(ident)
- plain := filepath.Join(dir, fmt.Sprintf("v%d.metadata.json", version))
-
- if _, err := c.filesystem.Stat(plain); err == nil {
- return true
+ for _, path := range []string{
+ filepath.Join(dir, fmt.Sprintf("v%d.metadata.json", version)),
+ filepath.Join(dir, fmt.Sprintf("v%d.gz.metadata.json",
version)),
+ filepath.Join(dir, fmt.Sprintf("v%d.zstd.metadata.json",
version)),
+ } {
Review Comment:
Worth a one-line comment that any non-`ErrNotExist` `Stat` error (permission
denied, transient FS error) is treated as "not found" here. It's the same
behavior as the old `metadataVersionExists`, so not a regression, but on a slow
filesystem a transient error could make a real version look absent — calling it
out keeps the next reader from assuming it's only an existence check.
##########
catalog/hadoop/hadoop.go:
##########
@@ -51,15 +51,17 @@ func init() {
var _ catalog.Catalog = (*Catalog)(nil)
// versionPattern matches Hadoop catalog metadata filenames:
-// v1.metadata.json, v42.metadata.json, v1.gz.metadata.json, etc.
-var versionPattern = regexp.MustCompile(`^v([0-9]+)(?:\.gz)?\.metadata\.json$`)
+// v1.metadata.json, v42.metadata.json, v1.gz.metadata.json,
+// v1.zstd.metadata.json, etc.
+var versionPattern =
regexp.MustCompile(`^v([0-9]+)(?:\.(?:gz|zstd))?\.metadata\.json$`)
// uuidMetadataPattern matches UUID-style metadata filenames produced by
-// Java/PyIceberg catalogs: 00000-<uuid>.metadata.json or
-// 00000-<uuid>.gz.metadata.json. The sequence is a 5-digit zero-padded
+// Java/PyIceberg catalogs: 00000-<uuid>.metadata.json,
+// 00000-<uuid>.gz.metadata.json, or 00000-<uuid>.zstd.metadata.json.
+// The sequence is a 5-digit zero-padded
// number and the UUID is in canonical 8-4-4-4-12 hex format.
var uuidMetadataPattern = regexp.MustCompile(
-
`^[0-9]{5}-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(?:\.gz)?\.metadata\.json$`,
+
`^[0-9]{5}-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(?:\.(?:gz|zstd))?\.metadata\.json$`,
Review Comment:
Two small things here. The `// Java/PyIceberg catalogs` comment above is now
inaccurate for the zstd alternative — neither Java nor PyIceberg ever emits
`00000-<uuid>.zstd.metadata.json`, so I'd note the zstd part is an
iceberg-go-only extension rather than implying parity.
And recognizing UUID-style zstd here (used by `isTableDir`/`ListTables`)
while `findMetadataLocation`'s `WalkDir` still keys off `versionPattern` only
creates a confusing split: a user could create a table in Python, see it in
`ListTables`, then get `ErrNoSuchTable` on `LoadTable`. That load gap is
pre-existing, but I'd at least not widen the recognition surface without a `//
recognized but not loadable` note. The new zstd UUID branch also has no test
backing it.
##########
catalog/hadoop/hadoop_test.go:
##########
@@ -990,6 +1041,50 @@ func (s *HadoopCatalogTestSuite) TestCreateTableAndLoad()
{
s.Equal(len(created.Schema().Fields()), len(loaded.Schema().Fields()))
}
+func (s *HadoopCatalogTestSuite) TestCreateTableGzipMetadata() {
Review Comment:
These four new integration tests (`TestCreateTableGzipMetadata`,
`TestCreateTableZstdMetadata`, `TestCommitTableGzipMetadata`,
`TestCommitTableZstdMetadata`) repeat the same `os.Mkdir` + props map +
`CreateTable` + `FileExists` boilerplate, where the existing suite leans on a
`createTestTable` helper.
I'd fold the four into a `createTestTableWithCompression` helper or
table-driven subtests to match the rest of the suite, and while you're adding
zstd coverage, a `TestIsTableDirTrueUUIDMetadataZstd` to mirror the existing
UUID+gzip case would catch a `ztsd`-style typo in `uuidMetadataPattern`. Low
priority next to the blockers above.
--
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]