# Inspector Feature Implementation Plan ## Overview Add a model inspection feature that validates database schemas against configurable rules. The inspector will read any supported format, apply validation rules from a YAML config, and output a report in markdown or JSON format. ## Architecture ### Core Components 1. **CLI Command** (`cmd/relspec/inspect.go`) - New subcommand: `relspec inspect` - Flags: - `--from` (required): Input format (dbml, pgsql, json, etc.) - `--from-path`: File path for file-based formats - `--from-conn`: Connection string for database formats - `--rules` (optional): Path to rules YAML file (default: `.relspec-rules.yaml`) - `--output-format`: Report format (markdown, json) (default: markdown) - `--output`: Output file path (default: stdout) - `--schema`: Schema name filter (optional) 2. **Inspector Package** (`pkg/inspector/`) - `inspector.go`: Main inspector logic - `rules.go`: Rule definitions and configuration - `validators.go`: Individual validation rule implementations - `report.go`: Report generation (markdown, JSON) - `config.go`: YAML config loading and parsing ### Data Flow ``` Input Format → Reader → Database Model → Inspector → Validation Results → Report Formatter → Output ``` ## Rules Configuration Structure ### YAML Schema (`rules.yaml`) ```yaml version: "1.0" rules: # Primary Key Rules primary_key_naming: enabled: enforce|warn|off pattern: "^id_" # regex pattern message: "Primary key columns must start with 'id_'" primary_key_datatype: enabled: enforce|warn|off allowed_types: ["bigserial", "bigint", "int", "serial", "integer"] message: "Primary keys must use approved integer types" primary_key_auto_increment: enabled: enforce|warn|off require_auto_increment: true|false message: "Primary keys without auto-increment detected" # Foreign Key Rules foreign_key_column_naming: enabled: enforce|warn|off pattern: "^rid_" message: "Foreign key columns must start with 'rid_'" foreign_key_constraint_naming: enabled: enforce|warn|off pattern: "^fk_" message: "Foreign key constraint names must start with 'fk_'" foreign_key_index: enabled: enforce|warn|off require_index: true message: "Foreign keys should have indexes" # Naming Convention Rules table_naming_case: enabled: enforce|warn|off case: "lowercase" # lowercase, uppercase, snake_case, camelCase pattern: "^[a-z][a-z0-9_]*$" message: "Table names must be lowercase with underscores" column_naming_case: enabled: enforce|warn|off case: "lowercase" pattern: "^[a-z][a-z0-9_]*$" message: "Column names must be lowercase with underscores" # Length Rules table_name_length: enabled: enforce|warn|off max_length: 64 message: "Table name exceeds maximum length" column_name_length: enabled: enforce|warn|off max_length: 64 message: "Column name exceeds maximum length" # Reserved Keywords reserved_keywords: enabled: enforce|warn|off check_tables: true check_columns: true message: "Using reserved SQL keywords" # Schema Integrity Rules missing_primary_key: enabled: enforce|warn|off message: "Table missing primary key" orphaned_foreign_key: enabled: enforce|warn|off message: "Foreign key references non-existent table" circular_dependency: enabled: enforce|warn|off message: "Circular foreign key dependency detected" ``` ### Rule Levels - **enforce**: Violations are errors (exit code 1) - **warn**: Violations are warnings (exit code 0) - **off**: Rule disabled ## Implementation Details ### 1. Inspector Core (`pkg/inspector/inspector.go`) ```go type Inspector struct { config *Config db *models.Database } type ValidationResult struct { RuleName string Level string // "error" or "warning" Message string Location string // e.g., "schema.table.column" Context map[string]interface{} Passed bool } type InspectorReport struct { Summary ReportSummary Violations []ValidationResult GeneratedAt time.Time Database string SourceFormat string } type ReportSummary struct { TotalRules int RulesChecked int RulesSkipped int ErrorCount int WarningCount int PassedCount int } func NewInspector(db *models.Database, config *Config) *Inspector func (i *Inspector) Inspect() (*InspectorReport, error) func (i *Inspector) validateDatabase() []ValidationResult func (i *Inspector) validateSchema(schema *models.Schema) []ValidationResult func (i *Inspector) validateTable(table *models.Table) []ValidationResult ``` ### 2. Rule Definitions (`pkg/inspector/rules.go`) ```go type Config struct { Version string Rules map[string]Rule } type Rule struct { Enabled string // "enforce", "warn", "off" Message string Pattern string AllowedTypes []string MaxLength int Case string RequireIndex bool CheckTables bool CheckColumns bool // ... rule-specific fields } type RuleValidator interface { Name() string Validate(db *models.Database, rule Rule) []ValidationResult } func LoadConfig(path string) (*Config, error) func GetDefaultConfig() *Config ``` **Configuration Loading Behavior:** - If `--rules` flag is provided but file not found: Use default configuration (don't error) - If file exists but is invalid YAML: Return error - Default configuration has sensible rules enabled at "warn" level - Users can override by creating their own `.relspec-rules.yaml` file ### 3. Validators (`pkg/inspector/validators.go`) Each validator implements rule logic: ```go // Primary Key Validators func validatePrimaryKeyNaming(db *models.Database, rule Rule) []ValidationResult func validatePrimaryKeyDatatype(db *models.Database, rule Rule) []ValidationResult func validatePrimaryKeyAutoIncrement(db *models.Database, rule Rule) []ValidationResult // Foreign Key Validators func validateForeignKeyColumnNaming(db *models.Database, rule Rule) []ValidationResult func validateForeignKeyConstraintNaming(db *models.Database, rule Rule) []ValidationResult func validateForeignKeyIndex(db *models.Database, rule Rule) []ValidationResult // Naming Convention Validators func validateTableNamingCase(db *models.Database, rule Rule) []ValidationResult func validateColumnNamingCase(db *models.Database, rule Rule) []ValidationResult // Length Validators func validateTableNameLength(db *models.Database, rule Rule) []ValidationResult func validateColumnNameLength(db *models.Database, rule Rule) []ValidationResult // Reserved Keywords Validator func validateReservedKeywords(db *models.Database, rule Rule) []ValidationResult // Integrity Validators func validateMissingPrimaryKey(db *models.Database, rule Rule) []ValidationResult func validateOrphanedForeignKey(db *models.Database, rule Rule) []ValidationResult func validateCircularDependency(db *models.Database, rule Rule) []ValidationResult // Registry of all validators var validators = map[string]RuleValidator{ "primary_key_naming": primaryKeyNamingValidator{}, // ... } ``` ### 4. Report Formatting (`pkg/inspector/report.go`) ```go type ReportFormatter interface { Format(report *InspectorReport) (string, error) } type MarkdownFormatter struct { UseColors bool // ANSI colors for terminal output } type JSONFormatter struct{} func (f *MarkdownFormatter) Format(report *InspectorReport) (string, error) func (f *JSONFormatter) Format(report *InspectorReport) (string, error) // Helper to detect if output is a TTY (terminal) func isTerminal(w io.Writer) bool ``` **Output Behavior:** - Markdown format will use ANSI color codes when outputting to a terminal (TTY) - When piped or redirected to a file, plain markdown without colors - Colors: Red for errors, Yellow for warnings, Green for passed checks **Markdown Format Example:** ```markdown # RelSpec Inspector Report **Database:** my_database **Source Format:** pgsql **Generated:** 2025-12-31 10:30:45 ## Summary - Rules Checked: 12 - Errors: 3 - Warnings: 5 - Passed: 4 ## Violations ### Errors (3) #### primary_key_naming **Table:** users, **Column:** user_id Primary key columns must start with 'id_' #### table_name_length **Table:** user_authentication_sessions_with_metadata Table name exceeds maximum length (64 characters) ### Warnings (5) #### foreign_key_index **Table:** orders, **Column:** customer_id Foreign keys should have indexes ... ``` **JSON Format Example:** ```json { "summary": { "total_rules": 12, "rules_checked": 12, "error_count": 3, "warning_count": 5, "passed_count": 4 }, "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", "current_name": "user_id", "expected_pattern": "^id_" }, "passed": false } ], "generated_at": "2025-12-31T10:30:45Z", "database": "my_database", "source_format": "pgsql" } ``` ### 5. CLI Command (`cmd/relspec/inspect.go`) ```go var inspectCmd = &cobra.Command{ Use: "inspect", Short: "Inspect and validate database schemas against rules", Long: `Read database schemas from various formats and validate against configurable rules.`, RunE: runInspect, } func init() { inspectCmd.Flags().String("from", "", "Input format (dbml, pgsql, json, etc.)") inspectCmd.Flags().String("from-path", "", "Input file path") inspectCmd.Flags().String("from-conn", "", "Database connection string") inspectCmd.Flags().String("rules", ".relspec-rules.yaml", "Rules configuration file") inspectCmd.Flags().String("output-format", "markdown", "Output format (markdown, json)") inspectCmd.Flags().String("output", "", "Output file (default: stdout)") inspectCmd.Flags().String("schema", "", "Filter by schema name") inspectCmd.MarkFlagRequired("from") } func runInspect(cmd *cobra.Command, args []string) error { // 1. Parse flags // 2. Create reader (reuse pattern from convert.go) // 3. Read database // 4. Load rules config (use defaults if file not found) // 5. Create inspector // 6. Run inspection // 7. Detect if output is terminal (for color support) // 8. Format report (with/without ANSI colors) // 9. Write output // 10. Exit with appropriate code (0 if no errors, 1 if errors) } ``` ## Implementation Phases ### Phase 1: Core Infrastructure 1. Create `pkg/inspector/` package structure 2. Implement `Config` and YAML loading 3. Implement `Inspector` core with basic validation framework 4. Create CLI command skeleton ### Phase 2: Basic Validators 1. Implement naming convention validators - Primary key naming - Foreign key column naming - Foreign key constraint naming - Table/column case validation 2. Implement length validators 3. Implement reserved keywords validator (leverage `pkg/pgsql/keywords.go`) ### Phase 3: Advanced Validators 1. Implement datatype validators 2. Implement integrity validators (missing PK, orphaned FK, circular deps) 3. Implement foreign key index validator ### Phase 4: Reporting 1. Implement `InspectorReport` structure 2. Implement markdown formatter 3. Implement JSON formatter 4. Add summary statistics ### Phase 5: CLI Integration 1. Wire up CLI command with flags 2. Integrate reader factory (from convert.go pattern) 3. Add output file handling 4. Add exit code logic 5. Add progress reporting ### Phase 6: Testing & Documentation 1. Unit tests for validators 2. Integration tests with sample schemas 3. Test with all reader formats 4. Update README with inspector documentation 5. Create example rules configuration file ## Files to Create 1. `pkg/inspector/inspector.go` - Core inspector logic 2. `pkg/inspector/rules.go` - Rule definitions and config loading 3. `pkg/inspector/validators.go` - Validation implementations 4. `pkg/inspector/report.go` - Report formatting 5. `pkg/inspector/config.go` - Config utilities 6. `cmd/relspec/inspect.go` - CLI command 7. `.relspec-rules.yaml.example` - Example configuration 8. `pkg/inspector/inspector_test.go` - Tests ## Files to Modify 1. `cmd/relspec/root.go` - Register inspect command 2. `README.md` - Add inspector documentation (if requested) ## Example Usage ```bash # Inspect a PostgreSQL database with default rules relspec inspect --from pgsql --from-conn "postgresql://localhost/mydb" # Inspect a DBML file with custom rules relspec inspect --from dbml --from-path schema.dbml --rules my-rules.yaml # Output JSON report to file relspec inspect --from json --from-path db.json --output-format json --output report.json # Inspect specific schema only relspec inspect --from pgsql --from-conn "..." --schema public # Use custom rules location relspec inspect --from dbml --from-path schema.dbml --rules /path/to/rules.yaml ``` ## Exit Codes - 0: Success (no errors, only warnings or all passed) - 1: Validation errors found (rules with level="enforce" failed) - 2: Runtime error (invalid config, reader error, etc.) ## Dependencies - Existing: `pkg/models`, `pkg/readers`, `pkg/pgsql/keywords.go` - New: `gopkg.in/yaml.v3` for YAML parsing (may already be in go.mod) ## Design Decisions ### Confirmed Choices (from user) 1. **Example config file**: Create `.relspec-rules.yaml.example` in repository root with documented examples 2. **Missing rules file**: Use sensible built-in defaults (don't error), all rules at "warn" level by default 3. **Terminal output**: ANSI colors (red/yellow/green) when outputting to terminal, plain markdown when piped/redirected 4. **Foreign key naming**: Separate configurable rules for both FK column names and FK constraint names ### Architecture Rationale 1. **Why YAML for config?**: Human-readable, supports comments, standard for config files 2. **Why three levels (enforce/warn/off)?**: Flexibility for gradual adoption, different contexts 3. **Why markdown + JSON?**: Markdown for human review, JSON for tooling integration 4. **Why pkg/inspector?**: Follows existing package structure, separates concerns 5. **Reuse readers**: Leverage existing reader infrastructure, supports all formats automatically 6. **Exit codes**: Follow standard conventions (0=success, 1=validation fail, 2=error) ## Future Enhancements (Not in Scope) - Auto-fix mode (automatically rename columns, etc.) - Custom rule plugins - HTML report format - Rule templates for different databases - CI/CD integration examples - Performance metrics in report