486 lines
13 KiB
Markdown
486 lines
13 KiB
Markdown
# RelSpec Inspector
|
|
|
|
> Database Schema Validation and Linting Tool
|
|
|
|
The RelSpec Inspector validates database schemas against configurable rules, helping you maintain consistency, enforce naming conventions, and catch common schema design issues across your database models.
|
|
|
|
## Overview
|
|
|
|
The Inspector reads database schemas from any supported RelSpec format and validates them against a set of configurable rules. It generates detailed reports highlighting violations, warnings, and passed checks.
|
|
|
|
## Features
|
|
|
|
- **Flexible Rule Configuration**: YAML-based rules with three severity levels (enforce, warn, off)
|
|
- **Generic Validators**: Reusable regex-based validators for custom naming conventions
|
|
- **Multiple Input Formats**: Works with all RelSpec readers (PostgreSQL, DBML, JSON, GORM, Bun, etc.)
|
|
- **Multiple Output Formats**: Markdown with ANSI colors for terminals, JSON for tooling integration
|
|
- **Smart Defaults**: Works out-of-the-box with sensible default rules
|
|
- **Terminal-Aware**: Automatic color support detection for improved readability
|
|
- **Exit Codes**: Proper exit codes for CI/CD integration
|
|
|
|
[Todo List of Features](./TODO.md)
|
|
|
|
## Quick Start
|
|
|
|
### Basic Usage
|
|
|
|
```bash
|
|
# Inspect a PostgreSQL database with default rules
|
|
relspec inspect --from pgsql --from-conn "postgres://user:pass@localhost/mydb"
|
|
|
|
# Inspect a DBML file
|
|
relspec inspect --from dbml --from-path schema.dbml
|
|
|
|
# Inspect with custom rules
|
|
relspec inspect --from json --from-path db.json --rules my-rules.yaml
|
|
|
|
# Output JSON report to file
|
|
relspec inspect --from pgsql --from-conn "..." \
|
|
--output-format json --output report.json
|
|
|
|
# Inspect specific schema only
|
|
relspec inspect --from pgsql --from-conn "..." --schema public
|
|
```
|
|
|
|
### Configuration
|
|
|
|
Create a `.relspec-rules.yaml` file to customize validation rules. If the file doesn't exist, the inspector uses sensible defaults.
|
|
|
|
```yaml
|
|
version: "1.0"
|
|
|
|
rules:
|
|
# Primary key columns must start with "id_"
|
|
primary_key_naming:
|
|
enabled: enforce # enforce|warn|off
|
|
function: primary_key_naming
|
|
pattern: "^id_"
|
|
message: "Primary key columns must start with 'id_'"
|
|
|
|
# Foreign key columns must start with "rid_"
|
|
foreign_key_column_naming:
|
|
enabled: warn
|
|
function: foreign_key_column_naming
|
|
pattern: "^rid_"
|
|
message: "Foreign key columns should start with 'rid_'"
|
|
|
|
# Table names must be lowercase snake_case
|
|
table_naming_case:
|
|
enabled: warn
|
|
function: table_regexpr # Generic regex validator
|
|
pattern: "^[a-z][a-z0-9_]*$"
|
|
message: "Table names should be lowercase with underscores"
|
|
|
|
# Ensure all tables have primary keys
|
|
missing_primary_key:
|
|
enabled: warn
|
|
function: have_primary_key
|
|
message: "Table is missing a primary key"
|
|
```
|
|
|
|
## Built-in Validation Rules
|
|
|
|
### Primary Key Rules
|
|
|
|
| Rule | Function | Description |
|
|
|------|----------|-------------|
|
|
| `primary_key_naming` | `primary_key_naming` | Validate PK column names against regex pattern |
|
|
| `primary_key_datatype` | `primary_key_datatype` | Enforce approved PK data types (bigint, serial, etc.) |
|
|
| `primary_key_auto_increment` | `primary_key_auto_increment` | Check if PKs have auto-increment enabled |
|
|
|
|
### Foreign Key Rules
|
|
|
|
| Rule | Function | Description |
|
|
|------|----------|-------------|
|
|
| `foreign_key_column_naming` | `foreign_key_column_naming` | Validate FK column names against regex pattern |
|
|
| `foreign_key_constraint_naming` | `foreign_key_constraint_naming` | Validate FK constraint names against regex pattern |
|
|
| `foreign_key_index` | `foreign_key_index` | Ensure FK columns have indexes for performance |
|
|
|
|
### Naming Convention Rules
|
|
|
|
| Rule | Function | Description |
|
|
|------|----------|-------------|
|
|
| `table_naming_case` | `table_regexpr` | Generic regex validator for table names |
|
|
| `column_naming_case` | `column_regexpr` | Generic regex validator for column names |
|
|
|
|
### Length Rules
|
|
|
|
| Rule | Function | Description |
|
|
|------|----------|-------------|
|
|
| `table_name_length` | `table_name_length` | Limit table name length (default: 64 chars) |
|
|
| `column_name_length` | `column_name_length` | Limit column name length (default: 64 chars) |
|
|
|
|
### Reserved Keywords
|
|
|
|
| Rule | Function | Description |
|
|
|------|----------|-------------|
|
|
| `reserved_keywords` | `reserved_words` | Detect use of SQL reserved keywords as identifiers |
|
|
|
|
### Schema Integrity Rules
|
|
|
|
| Rule | Function | Description |
|
|
|------|----------|-------------|
|
|
| `missing_primary_key` | `have_primary_key` | Ensure tables have primary keys |
|
|
| `orphaned_foreign_key` | `orphaned_foreign_key` | Detect FKs referencing non-existent tables |
|
|
| `circular_dependency` | `circular_dependency` | Detect circular FK dependencies |
|
|
|
|
## Rule Configuration
|
|
|
|
### Severity Levels
|
|
|
|
Rules support three severity levels:
|
|
|
|
- **`enforce`**: Violations are errors (exit code 1)
|
|
- **`warn`**: Violations are warnings (exit code 0)
|
|
- **`off`**: Rule is disabled
|
|
|
|
### Rule Structure
|
|
|
|
```yaml
|
|
rule_name:
|
|
enabled: enforce|warn|off
|
|
function: validator_function_name
|
|
message: "Custom message shown on violation"
|
|
# Rule-specific parameters
|
|
pattern: "^regex_pattern$" # For pattern-based validators
|
|
allowed_types: [type1, type2] # For type validators
|
|
max_length: 64 # For length validators
|
|
check_tables: true # For keyword validator
|
|
check_columns: true # For keyword validator
|
|
require_index: true # For FK index validator
|
|
```
|
|
|
|
## Generic Validators
|
|
|
|
The inspector provides generic validator functions that can be reused for custom rules:
|
|
|
|
### `table_regexpr`
|
|
|
|
Generic regex validator for table names. Create custom table naming rules:
|
|
|
|
```yaml
|
|
# Example: Ensure table names don't contain numbers
|
|
table_no_numbers:
|
|
enabled: warn
|
|
function: table_regexpr
|
|
pattern: "^[a-z_]+$"
|
|
message: "Table names should not contain numbers"
|
|
|
|
# Example: Tables must start with "tbl_"
|
|
table_prefix:
|
|
enabled: enforce
|
|
function: table_regexpr
|
|
pattern: "^tbl_[a-z][a-z0-9_]*$"
|
|
message: "Table names must start with 'tbl_'"
|
|
```
|
|
|
|
### `column_regexpr`
|
|
|
|
Generic regex validator for column names. Create custom column naming rules:
|
|
|
|
```yaml
|
|
# Example: Audit columns must end with "_audit"
|
|
audit_column_suffix:
|
|
enabled: enforce
|
|
function: column_regexpr
|
|
pattern: ".*_audit$"
|
|
message: "Audit columns must end with '_audit'"
|
|
|
|
# Example: Timestamp columns must end with "_at"
|
|
timestamp_suffix:
|
|
enabled: warn
|
|
function: column_regexpr
|
|
pattern: ".*(created|updated|deleted)_at$"
|
|
message: "Timestamp columns should end with '_at'"
|
|
```
|
|
|
|
## Output Formats
|
|
|
|
### Markdown (Default)
|
|
|
|
Human-readable markdown report with ANSI colors when outputting to a terminal:
|
|
|
|
```
|
|
# RelSpec Inspector Report
|
|
|
|
**Database:** my_database
|
|
**Source Format:** pgsql
|
|
**Generated:** 2025-12-31T10:30:45Z
|
|
|
|
## Summary
|
|
- Rules Checked: 13
|
|
- Errors: 2
|
|
- Warnings: 5
|
|
- Passed: 120
|
|
|
|
## Violations
|
|
|
|
### Errors (2)
|
|
|
|
#### primary_key_naming
|
|
**Location:** public.users.user_id
|
|
**Message:** Primary key columns must start with 'id_'
|
|
**Details:** expected_pattern=^id_
|
|
|
|
### Warnings (5)
|
|
|
|
#### foreign_key_index
|
|
**Location:** public.orders.customer_id
|
|
**Message:** Foreign key columns should have indexes
|
|
**Details:** has_index=false
|
|
```
|
|
|
|
### JSON
|
|
|
|
Structured JSON output for tooling integration:
|
|
|
|
```json
|
|
{
|
|
"summary": {
|
|
"total_rules": 13,
|
|
"rules_checked": 13,
|
|
"error_count": 2,
|
|
"warning_count": 5,
|
|
"passed_count": 120
|
|
},
|
|
"violations": [
|
|
{
|
|
"rule_name": "primary_key_naming",
|
|
"level": "error",
|
|
"message": "Primary key columns must start with 'id_'",
|
|
"location": "public.users.user_id",
|
|
"context": {
|
|
"schema": "public",
|
|
"table": "users",
|
|
"column": "user_id",
|
|
"expected_pattern": "^id_"
|
|
},
|
|
"passed": false
|
|
}
|
|
],
|
|
"generated_at": "2025-12-31T10:30:45Z",
|
|
"database": "my_database",
|
|
"source_format": "pgsql"
|
|
}
|
|
```
|
|
|
|
## CLI Reference
|
|
|
|
### Flags
|
|
|
|
| Flag | Type | Description |
|
|
|------|------|-------------|
|
|
| `--from` | string | **Required**. Source format (dbml, pgsql, json, yaml, gorm, etc.) |
|
|
| `--from-path` | string | Source file path (for file-based formats) |
|
|
| `--from-conn` | string | Connection string (for database formats) |
|
|
| `--rules` | string | Path to rules YAML file (default: `.relspec-rules.yaml`) |
|
|
| `--output-format` | string | Output format: `markdown` or `json` (default: `markdown`) |
|
|
| `--output` | string | Output file path (default: stdout) |
|
|
| `--schema` | string | Filter to specific schema by name |
|
|
|
|
### Exit Codes
|
|
|
|
| Code | Meaning |
|
|
|------|---------|
|
|
| 0 | Success (no errors, only warnings or all passed) |
|
|
| 1 | Validation errors found (rules with `enabled: enforce` failed) |
|
|
| 2 | Runtime error (invalid config, reader error, etc.) |
|
|
|
|
## CI/CD Integration
|
|
|
|
### GitHub Actions Example
|
|
|
|
```yaml
|
|
name: Schema Validation
|
|
|
|
on: [pull_request]
|
|
|
|
jobs:
|
|
validate:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v3
|
|
|
|
- name: Install RelSpec
|
|
run: go install git.warky.dev/wdevs/relspecgo/cmd/relspec@latest
|
|
|
|
- name: Validate Schema
|
|
run: |
|
|
relspec inspect \
|
|
--from dbml \
|
|
--from-path schema.dbml \
|
|
--rules .relspec-rules.yaml \
|
|
--output-format json \
|
|
--output validation-report.json
|
|
|
|
- name: Upload Report
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: validation-report
|
|
path: validation-report.json
|
|
```
|
|
|
|
### Pre-commit Hook Example
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# .git/hooks/pre-commit
|
|
|
|
echo "Running schema validation..."
|
|
|
|
relspec inspect \
|
|
--from dbml \
|
|
--from-path schema.dbml \
|
|
--rules .relspec-rules.yaml
|
|
|
|
exit $?
|
|
```
|
|
|
|
## Example Configuration File
|
|
|
|
See [`.relspec-rules.yaml.example`](../../.relspec-rules.yaml.example) for a fully documented example configuration with all available rules and customization options.
|
|
|
|
## Common Use Cases
|
|
|
|
### Enforce Naming Standards
|
|
|
|
```yaml
|
|
# Ensure consistent naming across your schema
|
|
table_naming_case:
|
|
enabled: enforce
|
|
function: table_regexpr
|
|
pattern: "^[a-z][a-z0-9_]*$"
|
|
message: "Tables must use snake_case"
|
|
|
|
column_naming_case:
|
|
enabled: enforce
|
|
function: column_regexpr
|
|
pattern: "^[a-z][a-z0-9_]*$"
|
|
message: "Columns must use snake_case"
|
|
|
|
primary_key_naming:
|
|
enabled: enforce
|
|
function: primary_key_naming
|
|
pattern: "^id$"
|
|
message: "Primary key must be named 'id'"
|
|
|
|
foreign_key_column_naming:
|
|
enabled: enforce
|
|
function: foreign_key_column_naming
|
|
pattern: "^[a-z]+_id$"
|
|
message: "Foreign keys must end with '_id'"
|
|
```
|
|
|
|
### Performance Best Practices
|
|
|
|
```yaml
|
|
# Ensure optimal database performance
|
|
foreign_key_index:
|
|
enabled: enforce
|
|
function: foreign_key_index
|
|
require_index: true
|
|
message: "Foreign keys must have indexes"
|
|
|
|
primary_key_datatype:
|
|
enabled: enforce
|
|
function: primary_key_datatype
|
|
allowed_types: [bigserial, bigint]
|
|
message: "Use bigserial or bigint for primary keys"
|
|
```
|
|
|
|
### Schema Integrity
|
|
|
|
```yaml
|
|
# Prevent common schema issues
|
|
missing_primary_key:
|
|
enabled: enforce
|
|
function: have_primary_key
|
|
message: "All tables must have a primary key"
|
|
|
|
orphaned_foreign_key:
|
|
enabled: enforce
|
|
function: orphaned_foreign_key
|
|
message: "Foreign keys must reference existing tables"
|
|
|
|
circular_dependency:
|
|
enabled: warn
|
|
function: circular_dependency
|
|
message: "Circular dependencies detected"
|
|
```
|
|
|
|
### Avoid Reserved Keywords
|
|
|
|
```yaml
|
|
reserved_keywords:
|
|
enabled: warn
|
|
function: reserved_words
|
|
check_tables: true
|
|
check_columns: true
|
|
message: "Avoid using SQL reserved keywords"
|
|
```
|
|
|
|
## Programmatic Usage
|
|
|
|
You can use the inspector programmatically in your Go code:
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/inspector"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
)
|
|
|
|
func main() {
|
|
// Load your database model
|
|
db := &models.Database{
|
|
Name: "my_database",
|
|
Schemas: []*models.Schema{
|
|
// ... your schema
|
|
},
|
|
}
|
|
|
|
// Load rules configuration
|
|
config, err := inspector.LoadConfig(".relspec-rules.yaml")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Create and run inspector
|
|
insp := inspector.NewInspector(db, config)
|
|
report, err := insp.Inspect()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Generate report
|
|
formatter := inspector.NewMarkdownFormatter(os.Stdout)
|
|
output, err := formatter.Format(report)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
fmt.Println(output)
|
|
|
|
// Check for errors
|
|
if report.HasErrors() {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Contributing
|
|
|
|
Contributions are welcome! To add a new validator:
|
|
|
|
1. Add the validator function to `validators.go`
|
|
2. Register it in `inspector.go` `getValidator()` function
|
|
3. Add default configuration to `rules.go` `GetDefaultConfig()`
|
|
4. Update this README with the new rule documentation
|
|
|
|
## License
|
|
|
|
Apache License 2.0 - See [LICENSE](../../LICENSE) for details.
|