diff --git a/server/tables/pg_database.go b/server/tables/pg_database.go index a8e905fc85..c17bef9c1a 100644 --- a/server/tables/pg_database.go +++ b/server/tables/pg_database.go @@ -34,6 +34,7 @@ type PgDatabase struct { var _ sql.DatabaseSchema = &PgDatabase{} var _ sql.SchemaDatabase = &PgDatabase{} var _ sql.SchemaObjectNameValidator = &PgDatabase{} +var _ sql.IndexNameGenerator = &PgDatabase{} // PgReadOnlyDatabase is the read-only variant of PgDatabase, used for revision databases // such as "postgres/main" returned by sqle.DoltDatabaseProvider for detached-HEAD sessions. @@ -259,6 +260,41 @@ func (d *PgDatabase) ValidateNewTableName(ctx *sql.Context, newTableName string, return true, fmt.Errorf(`relation "%s" already exists`, newTableName) } +// GenerateIndexName implements the sql.IndexNameGenerator interface with PostgreSQL-compatible naming conventions: +// - UNIQUE indexes: _[_col2...]_key +// - All other indexes:
_[_col2...]_idx +// +// Collisions are resolved by appending a numeric suffix (1, 2, …) to the base name. +// The collision check uses doesRelationExist so that any schema-level relation — table, +// view, sequence, or another index — blocks a candidate name, matching PostgreSQL's +// behavior where all relations in a schema share one namespace. +func (d *PgDatabase) GenerateIndexName(ctx *sql.Context, tableName string, idxDef sql.IndexDef, _ sql.Table) (string, error) { + colPart := strings.Join(idxDef.ColumnNames(), "_") + suffix := "_idx" + if idxDef.IsUnique() { + suffix = "_key" + } + base := tableName + "_" + colPart + suffix + + exists, _, err := d.doesRelationExist(ctx, base) + if err != nil { + return "", err + } + if !exists { + return base, nil + } + for i := 1; ; i++ { + candidate := fmt.Sprintf("%s%d", base, i) + exists, _, err = d.doesRelationExist(ctx, candidate) + if err != nil { + return "", err + } + if !exists { + return candidate, nil + } + } +} + // doesRelationExist tests if a relation with the specified |name| exists in this database. If any relation with that // name exists, this function returns true for |exists|, the relation type (e.g. index, view, table, sequence) for // |relationType|. If any problems are encountered looking up a relation, an error is returned in |err|. diff --git a/testing/go/dolt_tables_test.go b/testing/go/dolt_tables_test.go index 2cd2f2f219..ea24c03340 100755 --- a/testing/go/dolt_tables_test.go +++ b/testing/go/dolt_tables_test.go @@ -858,15 +858,15 @@ func TestUserSpaceDoltTables(t *testing.T) { { Query: `SELECT violation_type, pk, col1, violation_info FROM dolt_constraint_violations_test`, Expected: []sql.Row{ - {"unique index", 1, 1, `{"Columns": ["col1"], "Name": "col1"}`}, - {"unique index", 2, 1, `{"Columns": ["col1"], "Name": "col1"}`}, + {"unique index", 1, 1, `{"Columns":["col1"],"Name":"test_col1_key"}`}, + {"unique index", 2, 1, `{"Columns":["col1"],"Name":"test_col1_key"}`}, }, }, { Query: `SELECT violation_type, pk, col1, violation_info FROM public.dolt_constraint_violations_test`, Expected: []sql.Row{ - {"unique index", 1, 1, `{"Columns": ["col1"], "Name": "col1"}`}, - {"unique index", 2, 1, `{"Columns": ["col1"], "Name": "col1"}`}, + {"unique index", 1, 1, `{"Columns":["col1"],"Name":"test_col1_key"}`}, + {"unique index", 2, 1, `{"Columns":["col1"],"Name":"test_col1_key"}`}, }, }, { diff --git a/testing/go/enginetest/doltgres_engine_test.go b/testing/go/enginetest/doltgres_engine_test.go index 69efc42bef..dba83ede89 100755 --- a/testing/go/enginetest/doltgres_engine_test.go +++ b/testing/go/enginetest/doltgres_engine_test.go @@ -1342,6 +1342,7 @@ func TestDoltMergeArtifacts(t *testing.T) { "schema conflicts return an error when autocommit is enabled", // problems detecting autocommit for business logic "Multiple foreign key violations for a given row not supported", // foreign keys "divergent type change causes schema conflict", // alter table + "merge error lists all constraint violations when table has multiple violations", // index names differ under PG naming }) denginetest.RunDoltMergeArtifacts(t, h) } diff --git a/testing/go/foreign_keys_test.go b/testing/go/foreign_keys_test.go index 9904cebb6b..a55a17a4e6 100755 --- a/testing/go/foreign_keys_test.go +++ b/testing/go/foreign_keys_test.go @@ -1157,7 +1157,7 @@ func TestForeignKeys(t *testing.T) { { Query: "select violation_type, c, d, violation_info from dolt_constraint_violations_child order by 1", Expected: []sql.Row{ - {"foreign key", 3, 3, "{\"Columns\":[\"d\"],\"ForeignKey\":\"fk\",\"Index\":\"fk\",\"OnDelete\":\"RESTRICT\",\"OnUpdate\":\"RESTRICT\",\"ReferencedColumns\":[\"b\"],\"ReferencedIndex\":\"b\",\"ReferencedTable\":\"parent\",\"Table\":\"child\"}"}, + {"foreign key", 3, 3, "{\"Columns\":[\"d\"],\"ForeignKey\":\"fk\",\"Index\":\"fk\",\"OnDelete\":\"RESTRICT\",\"OnUpdate\":\"RESTRICT\",\"ReferencedColumns\":[\"b\"],\"ReferencedIndex\":\"parent_b_key\",\"ReferencedTable\":\"parent\",\"Table\":\"child\"}"}, }, }, }, diff --git a/testing/go/functions_test.go b/testing/go/functions_test.go index ef20bc0f10..59f6cebe5c 100644 --- a/testing/go/functions_test.go +++ b/testing/go/functions_test.go @@ -1896,7 +1896,7 @@ func TestSchemaVisibilityInquiryFunctions(t *testing.T) { {1539973141, "test_seq", "testschema"}, {1952237395, "test_table", "testschema"}, {3508950454, "test_table_pkey", "testschema"}, - {521883837, "v1", "testschema"}, + {2590613415, "test_table_v1_key", "testschema"}, }, }, { diff --git a/testing/go/index_test.go b/testing/go/index_test.go index 6b7aac5681..e5e7c48064 100644 --- a/testing/go/index_test.go +++ b/testing/go/index_test.go @@ -1629,5 +1629,56 @@ func TestBasicIndexing(t *testing.T) { }, }, }, + { + Name: "index naming: unnamed index uses table_col_idx convention", + SetUpScript: []string{ + "CREATE TABLE t1 (pk INT PRIMARY KEY, a INT, b INT);", + "CREATE INDEX ON t1 (a);", + "CREATE TABLE t2 (pk INT PRIMARY KEY, a INT);", + "CREATE UNIQUE INDEX ON t2 (a);", + "CREATE TABLE t3 (pk INT PRIMARY KEY, a INT UNIQUE);", + "CREATE TABLE t4 (pk INT PRIMARY KEY, a INT, b INT);", + "CREATE INDEX ON t4 (a, b);", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT indexname FROM pg_indexes WHERE tablename = 't1' AND indexname <> 't1_pkey' ORDER BY indexname;", + Expected: []sql.Row{{"t1_a_idx"}}, + }, + { + Query: "SELECT indexname FROM pg_indexes WHERE tablename = 't2' AND indexname <> 't2_pkey' ORDER BY indexname;", + Expected: []sql.Row{{"t2_a_key"}}, + }, + { + Query: "SELECT indexname FROM pg_indexes WHERE tablename = 't3' AND indexname <> 't3_pkey' ORDER BY indexname;", + Expected: []sql.Row{{"t3_a_key"}}, + }, + { + Query: "SELECT indexname FROM pg_indexes WHERE tablename = 't4' AND indexname <> 't4_pkey' ORDER BY indexname;", + Expected: []sql.Row{{"t4_a_b_idx"}}, + }, + }, + }, + { + Name: "index naming: collision appends numeric suffix", + SetUpScript: []string{ + "CREATE TABLE t5 (pk INT PRIMARY KEY, a INT);", + "CREATE INDEX t5_a_idx ON t5 (a);", + "CREATE INDEX ON t5 (a);", + "CREATE TABLE t6_a_idx (pk INT PRIMARY KEY);", + "CREATE TABLE t6 (pk INT PRIMARY KEY, a INT);", + "CREATE INDEX ON t6 (a);", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT indexname FROM pg_indexes WHERE tablename = 't5' AND indexname NOT IN ('t5_pkey', 't5_a_idx') ORDER BY indexname;", + Expected: []sql.Row{{"t5_a_idx1"}}, + }, + { + Query: "SELECT indexname FROM pg_indexes WHERE tablename = 't6' AND indexname <> 't6_pkey' ORDER BY indexname;", + Expected: []sql.Row{{"t6_a_idx1"}}, + }, + }, + }, }) } diff --git a/testing/go/pgcatalog_test.go b/testing/go/pgcatalog_test.go index 9bd0260b14..0ffb518b45 100644 --- a/testing/go/pgcatalog_test.go +++ b/testing/go/pgcatalog_test.go @@ -560,8 +560,8 @@ func TestPgClass(t *testing.T) { Expected: []sql.Row{ {"testing"}, {"testing_pkey"}, + {"testing_v1_key"}, {"testview"}, - {"v1"}, }, }, { @@ -633,7 +633,7 @@ JOIN pg_class t ON t.oid = i.indrelid JOIN pg_class ix ON ix.oid = i.indexrelid JOIN pg_namespace n ON t.relnamespace = n.oid JOIN pg_am AS am ON ix.relam = am.oid WHERE t.relname = 'foo' AND n.nspname = 'public';`, - Expected: []sql.Row{{"foo_pkey", "BTREE"}, {"b", "BTREE"}, {"b_2", "BTREE"}}, // TODO: should follow Postgres index naming convention: "foo_pkey", "foo_b_idx", "foo_b_a_idx" + Expected: []sql.Row{{"foo_pkey", "BTREE"}, {"foo_b_a_idx", "BTREE"}, {"foo_b_idx", "BTREE"}}, }, }, }, @@ -707,7 +707,7 @@ func TestPgConstraint(t *testing.T) { Expected: []sql.Row{ {1719906648, "testing2_pktesting_fkey", 2200, "f", "f", "f", "t", 2694106299, 0, 1719906648, 0, 2147906242, "a", "a", "s", "t", 0, "t", "{2}", "{1}", nil, nil, nil, nil, nil, nil}, {2068729390, "testing2_pkey", 2200, "p", "f", "f", "t", 2694106299, 0, 2068729390, 0, 0, "", "", "", "t", 0, "t", "{1}", nil, nil, nil, nil, nil, nil, nil}, - {3050361446, "v1", 2200, "u", "f", "f", "t", 2147906242, 0, 3050361446, 0, 0, "", "", "", "t", 0, "t", "{2}", nil, nil, nil, nil, nil, nil, nil}, + {2652383090, "testing_v1_key", 2200, "u", "f", "f", "t", 2147906242, 0, 2652383090, 0, 0, "", "", "", "t", 0, "t", "{2}", nil, nil, nil, nil, nil, nil, nil}, {3259318326, "v1_check", 2200, "c", "f", "f", "t", 2694106299, 0, 0, 0, 0, "", "", "", "t", 0, "t", nil, nil, nil, nil, nil, nil, nil, nil}, {3757635986, "testing_pkey", 2200, "p", "f", "f", "t", 2147906242, 0, 3757635986, 0, 0, "", "", "", "t", 0, "t", "{1}", nil, nil, nil, nil, nil, nil, nil}, }, @@ -726,7 +726,7 @@ func TestPgConstraint(t *testing.T) { {"testing2_pkey"}, {"testing2_pktesting_fkey"}, {"testing_pkey"}, - {"v1"}, + {"testing_v1_key"}, {"v1_check"}, }, }, @@ -793,6 +793,7 @@ func TestPgConstraintIndexes(t *testing.T) { {2068729390, "testing2_pkey"}, {1719906648, "testing2_pktesting_fkey"}, {3757635986, "testing_pkey"}, + {2652383090, "testing_v1_key"}, }, }, }, @@ -863,7 +864,7 @@ func TestPgConstraintIndexes(t *testing.T) { Query: "SELECT conname FROM pg_catalog.pg_constraint WHERE conrelid = 3645786842 AND contypid = 0 ORDER BY conname;", Expected: []sql.Row{ {"test_table1_pkey"}, - {"val1"}, // TODO: postgres names this "test_table1_val1_key" + {"test_table1_val1_key"}, {"val2_check"}, }, }, @@ -884,19 +885,19 @@ func TestPgConstraintIndexes(t *testing.T) { Query: "SELECT conname FROM pg_catalog.pg_constraint WHERE conrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'test_table1') AND contypid = 0 ORDER BY conname;", Expected: []sql.Row{ {"test_table1_pkey"}, - {"val1"}, // TODO: postgres names this "test_table1_val1_key" + {"test_table1_val1_key"}, {"val2_check"}, }, }, { Query: "SELECT conname FROM pg_catalog.pg_constraint WHERE conrelid IN (SELECT oid FROM pg_catalog.pg_class WHERE relname IN ('test_table1', 'test_table2')) AND contypid = 0 ORDER BY conname;", Expected: []sql.Row{ - {"name"}, // should be test_table2_name_key {"name_check"}, {"test_table1_pkey"}, + {"test_table1_val1_key"}, {"test_table2_fk_col_fkey"}, + {"test_table2_name_key"}, {"test_table2_pkey"}, - {"val1"}, // should be test_table1_val1_key {"val2_check"}, }, }, @@ -950,13 +951,13 @@ func TestPgConstraintIndexes(t *testing.T) { { Query: "SELECT conname FROM pg_catalog.pg_constraint WHERE conrelid >= (SELECT MIN(oid) FROM pg_catalog.pg_class WHERE relname LIKE 'test_%') AND conrelid <= (SELECT MAX(oid) FROM pg_catalog.pg_class WHERE relname LIKE 'test_%') AND contypid = 0 ORDER BY conname;", Expected: []sql.Row{ - {"name"}, // should be test_table2_name_key {"name_check"}, {"test_table1_pkey"}, + {"test_table1_val1_key"}, {"test_table2_fk_col_fkey"}, + {"test_table2_name_key"}, {"test_table2_pkey"}, {"test_table3_pkey"}, - {"val1"}, // should be test_table1_val1_key {"val2_check"}, }, }, @@ -978,7 +979,7 @@ func TestPgConstraintIndexes(t *testing.T) { Query: "SELECT conname FROM pg_catalog.pg_constraint WHERE conrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname = 'test_table1') ORDER BY conname;", Expected: []sql.Row{ {"test_table1_pkey"}, - {"val1"}, // should be test_table1_val1_key + {"test_table1_val1_key"}, {"val2_check"}, }, }, @@ -986,7 +987,7 @@ func TestPgConstraintIndexes(t *testing.T) { Query: "SELECT conname FROM pg_catalog.pg_constraint WHERE conrelid = 3645786842 ORDER BY conname;", Expected: []sql.Row{ {"test_table1_pkey"}, - {"val1"}, // should be test_table1_val1_key + {"test_table1_val1_key"}, {"val2_check"}, }, }, @@ -1007,7 +1008,9 @@ func TestPgConstraintIndexes(t *testing.T) { Query: "SELECT conname FROM pg_catalog.pg_constraint WHERE (conname LIKE '%_pkey' OR conname LIKE '%_key') AND connamespace = 2200 ORDER BY conname;", Expected: []sql.Row{ {"test_table1_pkey"}, + {"test_table1_val1_key"}, {"test_table2_fk_col_fkey"}, + {"test_table2_name_key"}, {"test_table2_pkey"}, {"test_table3_pkey"}, }, @@ -1547,7 +1550,7 @@ func TestPgIndex(t *testing.T) { ORDER BY 1;`, Expected: []sql.Row{ {1067629180, 3120782595, 1, 0, "t", "f", "t", "f", "f", "f", "t", "f", "t", "t", "f", "1", "", "", "0", nil, nil}, - {1322775662, 3120782595, 1, 0, "t", "f", "f", "f", "f", "f", "t", "f", "t", "t", "f", "2", "", "", "0", nil, nil}, + {2070175302, 3120782595, 1, 0, "t", "f", "f", "f", "f", "f", "t", "f", "t", "t", "f", "2", "", "", "0", nil, nil}, {3185790121, 1784425749, 2, 0, "t", "f", "t", "f", "f", "f", "t", "f", "t", "t", "f", "1 2", "", "", "0", nil, nil}, }, }, @@ -1565,7 +1568,7 @@ func TestPgIndex(t *testing.T) { "JOIN pg_namespace n ON c.relnamespace = n.oid " + "WHERE n.nspname = 'testschema' and left(c.relname, 5) <> 'dolt_' " + "ORDER BY 1;", - Expected: []sql.Row{{1067629180}, {1322775662}, {3185790121}}, + Expected: []sql.Row{{1067629180}, {2070175302}, {3185790121}}, }, { Query: "SELECT i.indexrelid, i.indrelid, c.relname, t.relname FROM pg_catalog.pg_index i " + @@ -1575,7 +1578,7 @@ func TestPgIndex(t *testing.T) { "WHERE n.nspname = 'testschema' and left(c.relname, 5) <> 'dolt_'", Expected: []sql.Row{ {1067629180, 3120782595, "testing_pkey", "testing"}, - {1322775662, 3120782595, "v1", "testing"}, + {2070175302, 3120782595, "testing_v1_key", "testing"}, {3185790121, 1784425749, "testing2_pkey", "testing2"}, }, }, @@ -1604,7 +1607,7 @@ func TestPgIndexes(t *testing.T) { Query: `SELECT * FROM "pg_catalog"."pg_indexes" where schemaname = 'testschema';`, Expected: []sql.Row{ {"testschema", "testing", "testing_pkey", "", "CREATE UNIQUE INDEX testing_pkey ON testschema.testing USING btree (pk)"}, - {"testschema", "testing", "v1", "", "CREATE UNIQUE INDEX v1 ON testschema.testing USING btree (v1)"}, + {"testschema", "testing", "testing_v1_key", "", "CREATE UNIQUE INDEX testing_v1_key ON testschema.testing USING btree (v1)"}, {"testschema", "testing2", "testing2_pkey", "", "CREATE UNIQUE INDEX testing2_pkey ON testschema.testing2 USING btree (pk, v1)"}, {"testschema", "testing2", "my_index", "", "CREATE INDEX my_index ON testschema.testing2 USING btree (v1)"}, }, @@ -1619,7 +1622,7 @@ func TestPgIndexes(t *testing.T) { }, { // Different cases but non-quoted, so it works Query: "SELECT indexname FROM PG_catalog.pg_INDEXES where schemaname='testschema' ORDER BY indexname;", - Expected: []sql.Row{{"my_index"}, {"testing2_pkey"}, {"testing_pkey"}, {"v1"}}, + Expected: []sql.Row{{"my_index"}, {"testing2_pkey"}, {"testing_pkey"}, {"testing_v1_key"}}, }, }, }, @@ -5232,8 +5235,8 @@ func TestPgIndexIndexes(t *testing.T) { Query: `SELECT * FROM pg_catalog.pg_index i WHERE i.indrelid = 1496157034 order by 1`, Expected: []sql.Row{ - {3674955271, 1496157034, 1, 0, "f", "f", "f", "f", "f", "f", "t", "f", "t", "t", "f", "2", "", "", "0", nil, nil}, {3992679530, 1496157034, 1, 0, "t", "f", "t", "f", "f", "f", "t", "f", "t", "t", "f", "1", "", "", "0", nil, nil}, + {4052612617, 1496157034, 1, 0, "f", "f", "f", "f", "f", "f", "t", "f", "t", "t", "f", "2", "", "", "0", nil, nil}, }, }, { @@ -5242,7 +5245,7 @@ WHERE i.indrelid = 1496157034 order by 1`, join pg_class c2 on i.indexrelid = c2.oid WHERE c.relname = 't2' order by 1,2`, Expected: []sql.Row{ - {"t2", "d"}, + {"t2", "t2_d_idx"}, {"t2", "t2_pkey"}, }, }, @@ -5834,7 +5837,7 @@ FROM pg_catalog.pg_index WHERE pg_catalog.pg_index.indrelid IN (select oid from pg_class where relname='t2') AND NOT pg_catalog.pg_index.indisprimary ORDER BY pg_catalog.pg_index.indrelid, cls_idx.relname`, Expected: []sql.Row{ - {1496157034, "b", "f", "f", "0", nil, "btree", nil, 0, "f", "{b}", "{f}"}, + {1496157034, "t2_b_idx", "f", "f", "0", nil, "btree", nil, 0, "f", "{b}", "{f}"}, }, }, },