Skip to content

Commit ba3050a

Browse files
committed
Fix issue with exporting identifiers across multiple databases
1 parent 30cd68c commit ba3050a

4 files changed

Lines changed: 128 additions & 23 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ The export creates a **zip file** containing the following files:
106106
- **`crdb_internal.transaction_statistics.csv`** - Transaction execution stats
107107
- **`crdb_internal.transaction_contention_events.csv`** - Lock contention events
108108
- **`crdb_internal.gossip_nodes.csv`** - Node information and topology
109-
- **`crdb_internal.table_indexes.csv`** - Table and index descriptor IDs
109+
- **`crdb_internal.table_indexes.csv`** - Table and index descriptor IDs across all databases
110110

111111
*Statistics files only include data within the specified time range*
112112

pkg/export/exporter.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ var exportTables = []Table{
8282
Table{Database: "crdb_internal", Name: "transaction_statistics", TimeColumn: "aggregated_ts"},
8383
Table{Database: "crdb_internal", Name: "transaction_contention_events", TimeColumn: "collection_ts"},
8484
Table{Database: "crdb_internal", Name: "gossip_nodes", TimeColumn: ""},
85-
Table{Database: "crdb_internal", Name: "table_indexes", TimeColumn: ""},
85+
Table{Database: "", Name: "crdb_internal.table_indexes", TimeColumn: ""}, // Use "" to query across all databases
8686
}
8787

8888
// NewExporter creates a new Exporter instance with the given configuration.
@@ -155,7 +155,7 @@ func (exporter *Exporter) Close() error {
155155
// - Cluster metadata (version, ID, name, organization, settings)
156156
// - Database schemas (CREATE statements for all user databases)
157157
// - Zone configurations
158-
// - Statistics tables (statement_statistics, transaction_statistics, transaction_contention_events, gossip_nodes, table_indexes)
158+
// - Statistics tables (statement_statistics, transaction_statistics, transaction_contention_events, gossip_nodes, table_indexes across all databases)
159159
//
160160
// The statistics tables are filtered by the TimeRange specified in Config.
161161
// All exported data is written to the OutputFile specified in Config.
@@ -435,7 +435,13 @@ func (exporter *Exporter) userDatabases() ([]string, error) {
435435
}
436436

437437
func (exporter *Exporter) exportTable(ctx context.Context, dir string, table Table, aggregationInterval time.Duration) error {
438-
filename := fmt.Sprintf("%s.%s.csv", table.Database, table.Name)
438+
// Create filename - if database is empty, just use table name
439+
var filename string
440+
if table.Database == "" {
441+
filename = fmt.Sprintf("%s.csv", table.Name)
442+
} else {
443+
filename = fmt.Sprintf("%s.%s.csv", table.Database, table.Name)
444+
}
439445
dataFile := filepath.Join(dir, filename)
440446

441447
// Create output file
@@ -451,10 +457,16 @@ func (exporter *Exporter) exportTable(ctx context.Context, dir string, table Tab
451457
}(file)
452458

453459
// Get column names
454-
rows, err := exporter.Db.Query(ctx,
455-
fmt.Sprintf("SELECT * FROM %s.%s LIMIT 0", pgx.Identifier{table.Database}.Sanitize(),
460+
// Construct table reference - handle empty database for cross-database queries
461+
var tableRef string
462+
if table.Database == "" {
463+
// Empty database means query across all databases using "" prefix
464+
tableRef = fmt.Sprintf(`"".%s`, table.Name)
465+
} else {
466+
tableRef = fmt.Sprintf("%s.%s", pgx.Identifier{table.Database}.Sanitize(), pgx.Identifier{table.Name}.Sanitize())
467+
}
456468

457-
pgx.Identifier{table.Name}.Sanitize()))
469+
rows, err := exporter.Db.Query(ctx, fmt.Sprintf("SELECT * FROM %s LIMIT 0", tableRef))
458470
if err != nil {
459471
return err
460472
}
@@ -483,8 +495,8 @@ func (exporter *Exporter) exportTable(ctx context.Context, dir string, table Tab
483495
)
484496
}
485497
copyQuery := fmt.Sprintf(
486-
"COPY (SELECT * FROM %s.%s %s) TO STDOUT WITH CSV",
487-
pgx.Identifier{table.Database}.Sanitize(), pgx.Identifier{table.Name}.Sanitize(), where)
498+
"COPY (SELECT * FROM %s %s) TO STDOUT WITH CSV",
499+
tableRef, where)
488500
logrus.Info(copyQuery)
489501
_, err = exporter.Db.PgConn().CopyTo(ctx, file, copyQuery)
490502
if err != nil {

pkg/export/exporter_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,8 @@ func TestExportTables(t *testing.T) {
176176
}
177177

178178
for i, table := range exportTables {
179-
if table.Database == "" {
180-
t.Errorf("exportTables[%d].Database should not be empty", i)
181-
}
179+
// Database can be empty for cross-database queries (e.g., "".crdb_internal.table_indexes)
180+
// but Name must always be present
182181
if table.Name == "" {
183182
t.Errorf("exportTables[%d].Name should not be empty", i)
184183
}

pkg/export/integration_test.go

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ package export
66
import (
77
"archive/zip"
88
"context"
9+
"encoding/csv"
910
"encoding/json"
1011
"fmt"
12+
"io"
1113
"os"
1214
"path/filepath"
15+
"strings"
1316
"testing"
1417
"time"
1518

@@ -95,17 +98,35 @@ func seedTestData(t *testing.T, conn *pgx.Conn) {
9598
ctx := context.Background()
9699

97100
queries := []string{
98-
// Create a test database
99-
"CREATE DATABASE IF NOT EXISTS testdb",
100-
// Create a test table
101-
"CREATE TABLE IF NOT EXISTS testdb.test_table (id INT PRIMARY KEY, name STRING)",
101+
// Create multiple test databases to verify cross-database table_indexes export
102+
"CREATE DATABASE IF NOT EXISTS testdb1",
103+
"CREATE DATABASE IF NOT EXISTS testdb2",
104+
"CREATE DATABASE IF NOT EXISTS testdb3",
105+
106+
// Create tables in testdb1
107+
"CREATE TABLE IF NOT EXISTS testdb1.users (id INT PRIMARY KEY, username STRING)",
108+
"CREATE TABLE IF NOT EXISTS testdb1.orders (id INT PRIMARY KEY, user_id INT, total DECIMAL)",
109+
110+
// Create tables in testdb2
111+
"CREATE TABLE IF NOT EXISTS testdb2.products (id INT PRIMARY KEY, name STRING, price DECIMAL)",
112+
"CREATE TABLE IF NOT EXISTS testdb2.inventory (product_id INT PRIMARY KEY, quantity INT)",
113+
114+
// Create tables in testdb3
115+
"CREATE TABLE IF NOT EXISTS testdb3.logs (id INT PRIMARY KEY, message STRING, created_at TIMESTAMP)",
116+
102117
// Insert some data
103-
"INSERT INTO testdb.test_table VALUES (1, 'test1'), (2, 'test2')",
118+
"INSERT INTO testdb1.users VALUES (1, 'alice'), (2, 'bob')",
119+
"INSERT INTO testdb2.products VALUES (1, 'widget', 9.99), (2, 'gadget', 19.99)",
120+
"INSERT INTO testdb3.logs VALUES (1, 'test log', now())",
121+
104122
// Run some queries to generate statistics
105-
"SELECT * FROM testdb.test_table WHERE id = 1",
106-
"SELECT * FROM testdb.test_table WHERE name = 'test2'",
107-
// Create a zone configuration
108-
"ALTER TABLE testdb.test_table CONFIGURE ZONE USING num_replicas = 1",
123+
"SELECT * FROM testdb1.users WHERE id = 1",
124+
"SELECT * FROM testdb2.products WHERE price > 10",
125+
"SELECT * FROM testdb3.logs LIMIT 1",
126+
127+
// Create zone configurations
128+
"ALTER TABLE testdb1.users CONFIGURE ZONE USING num_replicas = 1",
129+
"ALTER TABLE testdb2.products CONFIGURE ZONE USING num_replicas = 1",
109130
}
110131

111132
for _, query := range queries {
@@ -116,7 +137,7 @@ func seedTestData(t *testing.T, conn *pgx.Conn) {
116137
// Wait a bit for statistics to be collected
117138
time.Sleep(2 * time.Second)
118139

119-
t.Log("Test data seeded successfully")
140+
t.Log("Test data seeded successfully with multiple databases")
120141
}
121142

122143
// validateExport checks that the export file contains all expected content
@@ -142,7 +163,9 @@ func validateExport(t *testing.T, zipPath string, version string) {
142163
"crdb_internal.gossip_nodes.csv": false,
143164
"crdb_internal.table_indexes.csv": false,
144165
"zone_configurations.txt": false,
145-
"testdb.schema.txt": false, // Our test database
166+
"testdb1.schema.txt": false, // Our test databases
167+
"testdb2.schema.txt": false,
168+
"testdb3.schema.txt": false,
146169
}
147170

148171
// Check all files in zip
@@ -162,6 +185,11 @@ func validateExport(t *testing.T, zipPath string, version string) {
162185
if file.Name == "metadata.json" {
163186
validateMetadataFile(t, file, version)
164187
}
188+
189+
// Validate table_indexes contains data from multiple databases
190+
if file.Name == "crdb_internal.table_indexes.csv" {
191+
validateTableIndexesFile(t, file, version)
192+
}
165193
}
166194

167195
// Ensure all expected files were found
@@ -207,3 +235,69 @@ func validateMetadataFile(t *testing.T, file *zip.File, version string) {
207235

208236
t.Logf(" Cluster version from metadata: %s", metadata.ClusterVersion)
209237
}
238+
239+
// validateTableIndexesFile ensures table_indexes CSV contains data from multiple databases
240+
func validateTableIndexesFile(t *testing.T, file *zip.File, version string) {
241+
rc, err := file.Open()
242+
require.NoError(t, err, "Should be able to open table_indexes CSV")
243+
defer rc.Close()
244+
245+
// Parse CSV
246+
reader := csv.NewReader(rc)
247+
248+
// Read header
249+
header, err := reader.Read()
250+
require.NoError(t, err, "Should be able to read CSV header")
251+
252+
// Find the descriptor_name column index (contains database.schema.table)
253+
descriptorNameIdx := -1
254+
for i, col := range header {
255+
if col == "descriptor_name" {
256+
descriptorNameIdx = i
257+
break
258+
}
259+
}
260+
require.NotEqual(t, -1, descriptorNameIdx, "CSV should have descriptor_name column")
261+
262+
// Track which databases we've seen
263+
databasesSeen := make(map[string]bool)
264+
265+
// Read all rows and extract database names
266+
for {
267+
record, err := reader.Read()
268+
if err == io.EOF {
269+
break
270+
}
271+
require.NoError(t, err, "Should be able to read CSV row")
272+
273+
if len(record) > descriptorNameIdx {
274+
descriptorName := record[descriptorNameIdx]
275+
// descriptor_name format is typically "database.schema.table" or "database.public.table"
276+
parts := strings.Split(descriptorName, ".")
277+
if len(parts) >= 1 {
278+
database := parts[0]
279+
// Track non-system databases
280+
if database != "system" && database != "postgres" && database != "" {
281+
databasesSeen[database] = true
282+
}
283+
}
284+
}
285+
}
286+
287+
// Verify we have entries from our test databases
288+
expectedDatabases := []string{"testdb1", "testdb2", "testdb3"}
289+
foundCount := 0
290+
for _, db := range expectedDatabases {
291+
if databasesSeen[db] {
292+
foundCount++
293+
t.Logf(" ✓ Found table indexes for database: %s", db)
294+
}
295+
}
296+
297+
// We should see at least 2 of our test databases to prove cross-database querying works
298+
require.GreaterOrEqual(t, foundCount, 2,
299+
"table_indexes CSV should contain entries from multiple test databases (found %d, expected at least 2)",
300+
foundCount)
301+
302+
t.Logf(" ✓ table_indexes contains data from %d databases (version %s)", len(databasesSeen), version)
303+
}

0 commit comments

Comments
 (0)