Skip to content

Commit 2d5330f

Browse files
committed
fix(indexes): use lib/pq array parser for column names with special characters
Replace string manipulation (Trim/Split) with pq.StringArray to properly handle PostgreSQL arrays containing column names with special characters. Previous implementation incorrectly split on commas inside quoted identifiers, breaking indexes with columns like "col,with,commas" or "Column Name". Changes: - Use pq.StringArray for scanning PostgreSQL text[] arrays - Remove manual string parsing that failed on edge cases - Add comprehensive test for special characters (spaces, commas, braces) Fixes #26
1 parent e92a53e commit 2d5330f

File tree

2 files changed

+99
-8
lines changed

2 files changed

+99
-8
lines changed

integration_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,100 @@ func TestIntegration_App_ListIndexes(t *testing.T) {
436436
assert.True(t, foundEmailIdx, "Should find email index")
437437
}
438438

439+
func TestIntegration_App_ListIndexes_SpecialCharacters(t *testing.T) {
440+
_, connectionString, cleanup := setupTestContainer(t)
441+
defer cleanup()
442+
443+
db, err := sql.Open("postgres", connectionString)
444+
require.NoError(t, err)
445+
defer db.Close()
446+
447+
ctx := context.Background()
448+
449+
// Create test schema
450+
testSchema := "test_special_chars"
451+
_, err = db.ExecContext(ctx, fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", testSchema))
452+
require.NoError(t, err)
453+
_, err = db.ExecContext(ctx, fmt.Sprintf("CREATE SCHEMA %s", testSchema))
454+
require.NoError(t, err)
455+
456+
// Create table with columns containing special characters
457+
_, err = db.ExecContext(ctx, fmt.Sprintf(`
458+
CREATE TABLE %s.test_table (
459+
id SERIAL PRIMARY KEY,
460+
"Column Name" VARCHAR(255),
461+
"col,with,commas" VARCHAR(255),
462+
"col{value}" VARCHAR(255),
463+
"col}data" VARCHAR(255),
464+
normal_col VARCHAR(255)
465+
)
466+
`, testSchema))
467+
require.NoError(t, err)
468+
469+
// Create indexes with special character column names
470+
_, err = db.ExecContext(ctx, fmt.Sprintf(`
471+
CREATE INDEX idx_quoted_name ON %s.test_table ("Column Name")
472+
`, testSchema))
473+
require.NoError(t, err)
474+
475+
_, err = db.ExecContext(ctx, fmt.Sprintf(`
476+
CREATE INDEX idx_with_commas ON %s.test_table ("col,with,commas", normal_col)
477+
`, testSchema))
478+
require.NoError(t, err)
479+
480+
_, err = db.ExecContext(ctx, fmt.Sprintf(`
481+
CREATE INDEX idx_braces ON %s.test_table ("col{value}", "col}data")
482+
`, testSchema))
483+
require.NoError(t, err)
484+
485+
// Test with app
486+
appInstance, err := app.New()
487+
require.NoError(t, err)
488+
defer appInstance.Disconnect()
489+
490+
err = appInstance.Connect(connectionString)
491+
require.NoError(t, err)
492+
493+
indexes, err := appInstance.ListIndexes(testSchema, "test_table")
494+
assert.NoError(t, err)
495+
assert.NotEmpty(t, indexes)
496+
497+
// Verify each index is parsed correctly
498+
indexMap := make(map[string]*app.IndexInfo)
499+
for _, idx := range indexes {
500+
indexMap[idx.Name] = idx
501+
}
502+
503+
// Check idx_quoted_name
504+
if idx, ok := indexMap["idx_quoted_name"]; ok {
505+
assert.Len(t, idx.Columns, 1, "idx_quoted_name should have 1 column")
506+
assert.Equal(t, "Column Name", idx.Columns[0], "Column name should be 'Column Name'")
507+
} else {
508+
t.Error("idx_quoted_name not found")
509+
}
510+
511+
// Check idx_with_commas - should have 2 columns, not split by commas inside quotes
512+
if idx, ok := indexMap["idx_with_commas"]; ok {
513+
assert.Len(t, idx.Columns, 2, "idx_with_commas should have exactly 2 columns")
514+
assert.Contains(t, idx.Columns, "col,with,commas", "Should contain column with commas")
515+
assert.Contains(t, idx.Columns, "normal_col", "Should contain normal_col")
516+
} else {
517+
t.Error("idx_with_commas not found")
518+
}
519+
520+
// Check idx_braces
521+
if idx, ok := indexMap["idx_braces"]; ok {
522+
assert.Len(t, idx.Columns, 2, "idx_braces should have 2 columns")
523+
assert.Contains(t, idx.Columns, "col{value}", "Should contain 'col{value}'")
524+
assert.Contains(t, idx.Columns, "col}data", "Should contain 'col}data'")
525+
} else {
526+
t.Error("idx_braces not found")
527+
}
528+
529+
// Cleanup
530+
_, _ = db.ExecContext(ctx, fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", testSchema))
531+
}
532+
439533
func TestIntegration_App_ExplainQuery(t *testing.T) {
440534
_, connectionString, cleanup := setupTestDatabase(t)
441535
defer cleanup()

internal/app/client.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"fmt"
88
"strings"
99

10-
_ "github.com/lib/pq"
10+
"github.com/lib/pq"
1111
)
1212

1313
// PostgreSQLClientImpl implements the PostgreSQLClient interface.
@@ -325,19 +325,16 @@ func (c *PostgreSQLClientImpl) ListIndexes(schema, table string) ([]*IndexInfo,
325325
var indexes []*IndexInfo
326326
for rows.Next() {
327327
var index IndexInfo
328-
var columnsStr string
328+
var columns pq.StringArray
329329
if err := rows.Scan(
330-
&index.Name, &index.Table, &columnsStr,
330+
&index.Name, &index.Table, &columns,
331331
&index.IsUnique, &index.IsPrimary, &index.IndexType,
332332
); err != nil {
333333
return nil, fmt.Errorf("failed to scan index row: %w", err)
334334
}
335335

336-
// Parse column array from PostgreSQL format
337-
columnsStr = strings.Trim(columnsStr, "{}")
338-
if columnsStr != "" {
339-
index.Columns = strings.Split(columnsStr, ",")
340-
}
336+
// Convert pq.StringArray to []string
337+
index.Columns = []string(columns)
341338

342339
indexes = append(indexes, &index)
343340
}

0 commit comments

Comments
 (0)