From 0bc8b81ea12c1a178b98db3df810939d199617d7 Mon Sep 17 00:00:00 2001 From: Wei Cao Date: Mon, 7 Sep 2026 13:11:25 +0800 Subject: [PATCH] fix: compare DuckLake metadata paths on the same absolute file The image entrypoint cds into DATA_PATH and leaves --datadir at ".". Skipping the metadata file compared "./ducklake.db" with the absolute MYDUCK_DUCKLAKE_METADATA_PATH, so attachCatalogs still opened ducklake.db and same-volume recreate failed. Identify the catalog file with os.SameFile after Stat, with Abs/EvalSymlinks fallback. Keep ordinary catalogs attached. Regression: datadir=. versus an absolute metadata path, and myduck.db still ATTACHes. --- catalog/provider.go | 30 +++++++++++++++++++++- catalog/provider_ducklake_test.go | 42 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/catalog/provider.go b/catalog/provider.go index f002d84..bc7b4a9 100644 --- a/catalog/provider.go +++ b/catalog/provider.go @@ -525,6 +525,31 @@ func (prov *DatabaseProvider) HasCatalog(name string) bool { } // attachCatalogs attaches all the databases in the data directory +func duckLakeSamePath(a, b string) bool { + a = strings.TrimSpace(a) + b = strings.TrimSpace(b) + if a == "" || b == "" { + return false + } + infoA, errA := os.Stat(a) + infoB, errB := os.Stat(b) + if errA == nil && errB == nil { + return os.SameFile(infoA, infoB) + } + absA, errA := filepath.Abs(a) + absB, errB := filepath.Abs(b) + if errA != nil || errB != nil { + return filepath.Clean(a) == filepath.Clean(b) + } + if ev, err := filepath.EvalSymlinks(absA); err == nil { + absA = ev + } + if ev, err := filepath.EvalSymlinks(absB); err == nil { + absB = ev + } + return absA == absB +} + func (prov *DatabaseProvider) duckLakeMetadataFile(name string) bool { if prov == nil || prov.duckLake == nil { return false @@ -533,7 +558,10 @@ func (prov *DatabaseProvider) duckLakeMetadataFile(name string) bool { if meta == "" { return false } - return filepath.Clean(filepath.Join(prov.dataDir, name)) == filepath.Clean(meta) + // The image entrypoint cds into DATA_PATH and leaves --datadir at ".". + // Compare the same absolute file so "./ducklake.db" matches an absolute + // MYDUCK_DUCKLAKE_METADATA_PATH. Ordinary catalogs in dataDir are not skipped. + return duckLakeSamePath(filepath.Join(prov.dataDir, name), meta) } func (prov *DatabaseProvider) attachCatalogs() error { diff --git a/catalog/provider_ducklake_test.go b/catalog/provider_ducklake_test.go index ad118c8..16abdee 100644 --- a/catalog/provider_ducklake_test.go +++ b/catalog/provider_ducklake_test.go @@ -166,6 +166,48 @@ func TestAttachCatalogSkipsDuckLakeMetadataFile(t *testing.T) { require.NoError(t, prov.AttachCatalog(info, false)) } +func TestAttachCatalogSkipsDuckLakeMetadataRelativeDataDir(t *testing.T) { + dir := t.TempDir() + meta := filepath.Join(dir, "ducklake.db") + require.NoError(t, os.WriteFile(meta, []byte("existing"), 0o600)) + userConnector, err := duckdb.NewConnector(filepath.Join(dir, "myduck.db"), nil) + require.NoError(t, err) + require.NoError(t, userConnector.Close()) + cwd, err := os.Getwd() + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, os.Chdir(cwd)) }) + require.NoError(t, os.Chdir(dir)) + prov := &DatabaseProvider{ + dataDir: ".", + duckLake: &duckLakeRuntime{config: configuration.DuckLakeConfig{ + MetadataPath: meta, + }}, + } + info, err := os.Stat("ducklake.db") + require.NoError(t, err) + require.True(t, prov.duckLakeMetadataFile(info.Name())) + require.False(t, prov.duckLakeMetadataFile("myduck.db")) + + connector, err := duckdb.NewConnector("", nil) + require.NoError(t, err) + db := stdsql.OpenDB(connector) + t.Cleanup(func() { + require.NoError(t, db.Close()) + require.NoError(t, connector.Close()) + }) + prov.storage = db + userDB, err := os.Stat("myduck.db") + require.NoError(t, err) + require.NoError(t, prov.AttachCatalog(userDB, false)) + require.NoError(t, prov.AttachCatalog(info, false)) + + var attached int + require.NoError(t, db.QueryRow("SELECT count(*) FROM duckdb_databases() WHERE database_name = 'myduck'").Scan(&attached)) + require.Equal(t, 1, attached) + require.NoError(t, db.QueryRow("SELECT count(*) FROM duckdb_databases() WHERE database_name = 'ducklake'").Scan(&attached)) + require.Equal(t, 0, attached) +} + func TestDuckLakeAttachStatNonExistErrorDoesNotCreate(t *testing.T) { orig := duckLakeStat t.Cleanup(func() { duckLakeStat = orig })