Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions internal/consistency/mtree/merkle.go
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,7 @@ func (m *MerkleTreeTask) BuildMtree() (err error) {
logger.Info("Getting row estimates from all nodes...")
var maxRows int64
var refNode map[string]any
successfulEstimates := 0
for _, nodeInfo := range m.ClusterNodes {
pool, err := auth.GetSizedClusterNodeConnection(nodeInfo, auth.ConnectionOptions{PoolSize: poolSize})
if err != nil {
Expand All @@ -1517,16 +1518,30 @@ func (m *MerkleTreeTask) BuildMtree() (err error) {
logger.Warn("Warning: Could not get row estimate from node %s: %v", nodeInfo["Name"], err)
continue
}
successfulEstimates++

if count > maxRows {
// Seed refNode on the first successful estimate so an all-empty table
// (every count == 0) still resolves a reference node instead of being
// mistaken for a total estimate failure below.
if refNode == nil || count > maxRows {
maxRows = count
refNode = nodeInfo
}
}

if refNode == nil {
if successfulEstimates == 0 {
for _, p := range pools {
p.Close()
}
return fmt.Errorf("could not determine a reference node; failed to get row estimates from all nodes")
}

if maxRows == 0 {
for _, p := range pools {
p.Close()
}
return fmt.Errorf("table %s has 0 rows on all nodes; add data before building a Merkle tree", m.QualifiedTableName)
}
logger.Info("Using node %s as the reference for defining block ranges.", refNode["Name"])

if len(blockRanges) == 0 {
Expand Down
59 changes: 59 additions & 0 deletions tests/integration/mtree_empty_table_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// ///////////////////////////////////////////////////////////////////////////
//
// # ACE - Active Consistency Engine
//
// Copyright (C) 2023 - 2026, pgEdge (https://www.pgedge.com/)
//
// This software is released under the PostgreSQL License:
// https://opensource.org/license/postgresql
//
// ///////////////////////////////////////////////////////////////////////////

package integration

import (
"context"
"fmt"
"testing"

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require"
)

// mtree build on a table that is empty on every node must fail with a clear,
// actionable "0 rows on all nodes" message -- not the internal "could not
// determine a reference node" error that leaves the user unsure whether the
// table is missing, the nodes are unreachable, or something is broken.
func TestBuildMtreeFailsClearlyOnEmptyTable(t *testing.T) {
ctx := context.Background()
tableName := "mtree_empty_table_test"
qualifiedTable := fmt.Sprintf("%s.%s", testSchema, tableName)
safeTable := pgx.Identifier{testSchema, tableName}.Sanitize()
nodes := []string{serviceN1, serviceN2}

// Create the table on both nodes but insert no rows.
for _, pool := range []*pgxpool.Pool{pgCluster.Node1Pool, pgCluster.Node2Pool} {
_, err := pool.Exec(ctx, "CREATE TABLE IF NOT EXISTS "+safeTable+" (id INT PRIMARY KEY, payload TEXT)") // nosemgrep
require.NoError(t, err)
}
t.Cleanup(func() {
for _, pool := range []*pgxpool.Pool{pgCluster.Node1Pool, pgCluster.Node2Pool} {
_, _ = pool.Exec(ctx, "DROP TABLE IF EXISTS "+safeTable) // nosemgrep
}
})

task := newTestMerkleTreeTask(t, qualifiedTable, nodes)
require.NoError(t, task.RunChecks(false))
require.NoError(t, task.MtreeInit())
t.Cleanup(func() { _ = task.MtreeTeardown() })

err := task.BuildMtree()
require.Error(t, err, "build on an empty table must fail")
require.Contains(t, err.Error(), "has 0 rows on all nodes",
"error must name the real cause: the table is empty")
require.Contains(t, err.Error(), qualifiedTable,
"error must name the offending table")
require.NotContains(t, err.Error(), "could not determine a reference node",
"the internal reference-node error must no longer leak for empty tables")
}
Loading