Fix/dbml commented refs duplicate indexes #31
@@ -123,6 +123,7 @@ rules:
|
|||||||
| `missing_primary_key` | `have_primary_key` | Ensure tables have primary keys |
|
| `missing_primary_key` | `have_primary_key` | Ensure tables have primary keys |
|
||||||
| `orphaned_foreign_key` | `orphaned_foreign_key` | Detect FKs referencing non-existent tables |
|
| `orphaned_foreign_key` | `orphaned_foreign_key` | Detect FKs referencing non-existent tables |
|
||||||
| `circular_dependency` | `circular_dependency` | Detect circular FK dependencies |
|
| `circular_dependency` | `circular_dependency` | Detect circular FK dependencies |
|
||||||
|
| `duplicate_index_name` | `duplicate_index_name` | Index / PK / unique names must be unique per schema (default: `enforce`) |
|
||||||
|
|
||||||
## Rule Configuration
|
## Rule Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ func getValidator(functionName string) (validatorFunc, bool) {
|
|||||||
"have_primary_key": validateMissingPrimaryKey,
|
"have_primary_key": validateMissingPrimaryKey,
|
||||||
"orphaned_foreign_key": validateOrphanedForeignKey,
|
"orphaned_foreign_key": validateOrphanedForeignKey,
|
||||||
"circular_dependency": validateCircularDependency,
|
"circular_dependency": validateCircularDependency,
|
||||||
|
"duplicate_index_name": validateDuplicateIndexName,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn, exists := validators[functionName]
|
fn, exists := validators[functionName]
|
||||||
|
|||||||
@@ -154,6 +154,11 @@ func GetDefaultConfig() *Config {
|
|||||||
Function: "circular_dependency",
|
Function: "circular_dependency",
|
||||||
Message: "Circular foreign key dependency detected",
|
Message: "Circular foreign key dependency detected",
|
||||||
},
|
},
|
||||||
|
"duplicate_index_name": {
|
||||||
|
Enabled: "enforce",
|
||||||
|
Function: "duplicate_index_name",
|
||||||
|
Message: "Index name is reused within the schema; PostgreSQL skips the duplicate CREATE INDEX IF NOT EXISTS",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func TestGetDefaultConfig(t *testing.T) {
|
|||||||
"missing_primary_key",
|
"missing_primary_key",
|
||||||
"orphaned_foreign_key",
|
"orphaned_foreign_key",
|
||||||
"circular_dependency",
|
"circular_dependency",
|
||||||
|
"duplicate_index_name",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ruleName := range expectedRules {
|
for _, ruleName := range expectedRules {
|
||||||
|
|||||||
@@ -643,3 +643,64 @@ func contains(slice []string, value string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// validateDuplicateIndexName checks that index names are unique per schema.
|
||||||
|
// PostgreSQL keeps indexes in the schema-wide relation namespace, so a name
|
||||||
|
// reused on another table makes CREATE INDEX IF NOT EXISTS silently skip it.
|
||||||
|
// Primary key and unique constraints create backing indexes and share that
|
||||||
|
// namespace too. An index and a constraint with the same name on the same
|
||||||
|
// table describe one object and are not reported.
|
||||||
|
func validateDuplicateIndexName(db *models.Database, rule Rule, ruleName string) []ValidationResult {
|
||||||
|
results := []ValidationResult{}
|
||||||
|
|
||||||
|
for _, schema := range db.Schemas {
|
||||||
|
// lowercased name -> "table" entries, one per distinct object
|
||||||
|
owners := make(map[string][]string)
|
||||||
|
display := make(map[string]string)
|
||||||
|
order := []string{}
|
||||||
|
|
||||||
|
add := func(name, table string, sameTableMerges bool) {
|
||||||
|
key := strings.ToLower(name)
|
||||||
|
if _, seen := owners[key]; !seen {
|
||||||
|
order = append(order, key)
|
||||||
|
display[key] = name
|
||||||
|
}
|
||||||
|
if sameTableMerges && contains(owners[key], table) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
owners[key] = append(owners[key], table)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range schema.Tables {
|
||||||
|
for _, key := range sortedKeys(table.Indexes) {
|
||||||
|
if name := table.Indexes[key].Name; name != "" {
|
||||||
|
add(name, table.Name, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range sortConstraints(table.Constraints) {
|
||||||
|
if c.Name == "" || (c.Type != models.PrimaryKeyConstraint && c.Type != models.UniqueConstraint) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
add(c.Name, table.Name, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range order {
|
||||||
|
tables := owners[key]
|
||||||
|
results = append(results, createResult(
|
||||||
|
ruleName,
|
||||||
|
len(tables) == 1,
|
||||||
|
rule.Message,
|
||||||
|
formatLocation(schema.Name, display[key], "")+" on "+strings.Join(tables, ", "),
|
||||||
|
map[string]interface{}{
|
||||||
|
"schema": schema.Name,
|
||||||
|
"index": display[key],
|
||||||
|
"tables": tables,
|
||||||
|
"occurrences": len(tables),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|||||||
@@ -835,3 +835,84 @@ func TestFormatLocation(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateDuplicateIndexName(t *testing.T) {
|
||||||
|
db := &models.Database{
|
||||||
|
Name: "testdb",
|
||||||
|
Schemas: []*models.Schema{
|
||||||
|
{
|
||||||
|
Name: "entity",
|
||||||
|
Tables: []*models.Table{
|
||||||
|
{
|
||||||
|
Name: "actor_phone",
|
||||||
|
Indexes: map[string]*models.Index{
|
||||||
|
"idx_actor": {Name: "idx_actor", Columns: []string{"rid_actor"}},
|
||||||
|
"uk_phone": {Name: "uk_phone", Columns: []string{"phone"}, Unique: true},
|
||||||
|
},
|
||||||
|
Constraints: map[string]*models.Constraint{
|
||||||
|
// Same name as the index on the same table: one object.
|
||||||
|
"uk_phone": {Name: "uk_phone", Type: models.UniqueConstraint, Columns: []string{"phone"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "actor_email",
|
||||||
|
Indexes: map[string]*models.Index{
|
||||||
|
"idx_actor": {Name: "idx_actor", Columns: []string{"rid_actor"}},
|
||||||
|
},
|
||||||
|
Constraints: map[string]*models.Constraint{
|
||||||
|
"UK_Phone": {Name: "UK_Phone", Type: models.UniqueConstraint, Columns: []string{"email"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "actor_address",
|
||||||
|
Indexes: map[string]*models.Index{
|
||||||
|
"idx_actor": {Name: "idx_actor", Columns: []string{"rid_actor"}},
|
||||||
|
"idx_actor#2": {Name: "idx_actor", Columns: []string{"rid_actor", "kind"}},
|
||||||
|
"idx_address": {Name: "idx_address", Columns: []string{"line1"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Same names in another schema do not collide.
|
||||||
|
Name: "org",
|
||||||
|
Tables: []*models.Table{
|
||||||
|
{Name: "api_provider", Indexes: map[string]*models.Index{"idx_actor": {Name: "idx_actor"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
results := validateDuplicateIndexName(db, Rule{Message: "dup"}, "duplicate_index_name")
|
||||||
|
|
||||||
|
got := map[string]bool{}
|
||||||
|
occ := map[string]int{}
|
||||||
|
for _, r := range results {
|
||||||
|
key := r.Context["schema"].(string) + "." + r.Context["index"].(string)
|
||||||
|
got[key] = r.Passed
|
||||||
|
occ[key] = r.Context["occurrences"].(int)
|
||||||
|
}
|
||||||
|
|
||||||
|
want := map[string]struct {
|
||||||
|
passed bool
|
||||||
|
occ int
|
||||||
|
}{
|
||||||
|
"entity.idx_actor": {false, 4},
|
||||||
|
"entity.uk_phone": {false, 2},
|
||||||
|
"entity.idx_address": {true, 1},
|
||||||
|
"org.idx_actor": {true, 1},
|
||||||
|
}
|
||||||
|
if len(got) != len(want) {
|
||||||
|
t.Fatalf("got %d results %v, want %d", len(got), got, len(want))
|
||||||
|
}
|
||||||
|
for k, w := range want {
|
||||||
|
p, ok := got[k]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("missing result for %s", k)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p != w.passed || occ[k] != w.occ {
|
||||||
|
t.Errorf("%s: passed=%v occurrences=%d, want passed=%v occurrences=%d", k, p, occ[k], w.passed, w.occ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user