lint

package
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 20, 2026 License: Apache-2.0 Imports: 21 Imported by: 2

README

Linter Framework

The lint package provides a framework for static analysis of MySQL schema definitions and DDL statements. It enables validation and best-practice enforcement beyond the runtime checks provided by the check package.

Architecture

All built-in linters are automatically registered and enabled when the lint package is imported. The framework uses a flat package structure:

  • Core framework files: lint.go, linter.go, registry.go, violation.go
  • Linter implementations: lint_*.go (e.g., lint_invisible_index.go)

Quick Start

Using the Linter Framework
import (
    "github.com/block/spirit/pkg/lint"
)

// All built-in linters are automatically registered!
violations, err := lint.RunLinters(tables, stmts, lint.Config{})
if err != nil {
    // Handle configuration errors
}

// Check for errors
if lint.HasErrors(violations) {
    // Handle errors
}

// Filter violations by linter
flagViolations := lint.FilterByLinter(violations, "has_foreign_key")
Creating a Custom Linter

Custom linters can be

  1. added directly to the lint package (in new files with the lint_ prefix, for consistency)
  2. added to your own package and registered by blank import that relies on the init() function
  3. added to your own code and registered explicitly using lint.Register()
// lint_my_custom.go
package lint

import (
    "github.com/block/spirit/pkg/statement"
)

// Register your linter in init()
func init() {
    Register(&MyCustomLinter{})
}

// MyCustomLinter checks custom rules
type MyCustomLinter struct{}

func (l *MyCustomLinter) Name() string        { return "my_custom" }
func (l *MyCustomLinter) Description() string { return "Checks naming conventions" }
func (l *MyCustomLinter) String() string      { return l.Name() }

func (l *MyCustomLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) []Violation {
    var violations []Violation
    
    for _, ct := range existingTables {
        // Check table properties
        if /* condition */ {
            violations = append(violations, Violation{
                Linter:   l,
                Severity: SeverityWarning,
                Message:  "Table name issue",
                Location: &Location{
                    Table: ct.GetTableName(),
                },
            })
        }
    }
    
    return violations
}
Configuring Linters
Enabling/Disabling Linters
// Disable specific linters
violations, err := lint.RunLinters(tables, stmts, lint.Config{
    Enabled: map[string]bool{
        "invisible_index_before_drop": false,
        "primary_key":                 true,
    },
})
if err != nil {
    // Handle configuration errors
}
Configurable Linters

Some linters support additional configuration options via the Settings field. Linters that implement the ConfigurableLinter interface accept settings as map[string]string:

violations, err := lint.RunLinters(tables, stmts, lint.Config{
    Settings: map[string]map[string]string{
        "invisible_index_before_drop": {
            "raiseError": "false",  // Demote violations from errors to warnings (default is "true")
        },
    },
})
if err != nil {
    // Handle configuration errors (e.g., invalid settings)
}

Each configurable linter defines its own settings keys and values. See the individual linter documentation below for available options.

Core Types

Severity Levels
  • ERROR: Will cause actual problems (data loss, inconsistency, MySQL limitations)
  • WARNING: Best practice violations, potential issues
  • INFO: Suggestions, style preferences
Violation
type Violation struct {
    Linter     Linter              // The linter that produced this violation
    Severity   Severity            // ERROR, WARNING, or INFO
    Message    string              // Human-readable message
    Location   *Location           // Where the violation occurred
    Suggestion *string             // Optional fix suggestion
    Context    map[string]any      // Additional context
}
Location
type Location struct {
    Table      string   // Table name
    Column     *string  // Column name (if applicable)
    Index      *string  // Index name (if applicable)
    Constraint *string  // Constraint name (if applicable)
}

API Functions

Registration
  • Register(l Linter) - Register a linter (call from init())
  • Enable(name string) - Enable a linter by name
  • Disable(name string) - Disable a linter by name
  • List() - Get all registered linter names
  • Get(name string) - Get a linter by name
Execution
  • RunLinters(createTables, alterStatements, config) ([]Violation, error) - Run all enabled linters, returns violations and any configuration errors
  • HasErrors(violations) - Check if any violations are errors
  • HasWarnings(violations) - Check if any violations are warnings
  • FilterByLinter(violations, name) - Filter by linter name

Built-in Linters

The lint package includes 17 built-in linters covering schema design, data types, and safety best practices.

allow_charset

Severity: Warning
Configurable: Yes
Checks: CREATE TABLE, ALTER TABLE (ADD/MODIFY/CHANGE COLUMN)

Restricts which character sets are allowed for tables and columns. Helps enforce consistent encoding across your database.

Configuration Options:

  • charsets (string): Comma-separated list of allowed character sets. Default: "utf8mb4,binary". The binary charset covers JSON, BLOB, and spatial columns, which MySQL reports as having the binary character set.

Examples:

-- ❌ Violation (latin1 not allowed by default)
CREATE TABLE users (
  id INT PRIMARY KEY,
  name VARCHAR(100) CHARACTER SET latin1
) CHARACTER SET latin1;

-- ✅ Correct
CREATE TABLE users (
  id INT PRIMARY KEY,
  name VARCHAR(100) CHARACTER SET utf8mb4
) CHARACTER SET utf8mb4;

-- ❌ Violation in ALTER TABLE
ALTER TABLE users ADD COLUMN legacy VARCHAR(100) CHARACTER SET latin1;

Configuration Example:

violations, err := lint.RunLinters(tables, stmts, lint.Config{
    Settings: map[string]map[string]string{
        "allow_charset": {
            "charsets": "utf8mb4,utf8mb3",  // Allow multiple charsets
        },
    },
})

allow_engine

Severity: Warning
Configurable: Yes
Checks: CREATE TABLE, ALTER TABLE ENGINE

Restricts which storage engines are allowed. Helps ensure consistent engine usage and avoid problematic engines like MyISAM.

Configuration Options:

  • allowed_engines (string): Comma-separated list of allowed engines. Default: "innodb".

Examples:

-- ❌ Violation (MyISAM not allowed by default)
CREATE TABLE users (
  id INT PRIMARY KEY
) ENGINE=MyISAM;

-- ✅ Correct
CREATE TABLE users (
  id INT PRIMARY KEY
) ENGINE=InnoDB;

-- ❌ Violation in ALTER TABLE
ALTER TABLE users ENGINE=MyISAM;

Configuration Example:

violations, err := lint.RunLinters(tables, stmts, lint.Config{
    Settings: map[string]map[string]string{
        "allow_engine": {
            "allowed_engines": "innodb,rocksdb",  // Allow multiple engines
        },
    },
})

auto_inc_capacity

Severity: Error
Configurable: Yes
Checks: CREATE TABLE

Ensures that AUTO_INCREMENT values are not within a dangerous percentage of the column type's maximum capacity. Approaching the capacity ceiling will eventually cause INSERTs to fail.

Configuration Options:

  • threshold (string): Percentage threshold (1-100). Default: "85".

Examples:

-- ❌ Violation (90% of INT UNSIGNED capacity)
CREATE TABLE users (
  id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT
) AUTO_INCREMENT=4000000000;

-- ✅ Correct (low value)
CREATE TABLE users (
  id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT
) AUTO_INCREMENT=100;

Configuration Example:

violations, err := lint.RunLinters(tables, stmts, lint.Config{
    Settings: map[string]map[string]string{
        "auto_inc_capacity": {
            "threshold": "90",  // Warn at 90% capacity
        },
    },
})

datetime_index_position

Severity: Warning
Configurable: No
Checks: CREATE TABLE, ALTER TABLE (ADD INDEX, ADD CONSTRAINT)

Detects composite indexes where a DATETIME, TIMESTAMP, or DATE column appears in any non-last position. Date/time columns are overwhelmingly queried with range predicates (>, >=, <, <=, BETWEEN), and once the optimizer hits a range predicate on a column inside a composite index, the columns that follow can no longer be used for sorted access.

This is a heuristic with no visibility into the actual query workload, so it always emits Warnings. Suppress or ignore the warning when the column is known to be queried with equality predicates only, or when the trailing columns exist to make this a covering index.

Examples:

-- ❌ Violation: range scans on updated_at make `attempt` unusable for sorted access
CREATE TABLE jobs (
  id INT PRIMARY KEY,
  updated_at DATETIME NOT NULL,
  attempt INT NOT NULL,
  KEY updated_at_attempt (updated_at, attempt)
);

-- ✅ Correct: equality column first, range column last
CREATE TABLE jobs (
  id INT PRIMARY KEY,
  updated_at DATETIME NOT NULL,
  attempt INT NOT NULL,
  KEY attempt_updated (attempt, updated_at)
);

-- ❌ Violation in ALTER TABLE
ALTER TABLE jobs ADD INDEX bad_idx (updated_at, attempt);

FULLTEXT and SPATIAL indexes are excluded (they don't behave like B-tree indexes for ranges). Single-column indexes are not flagged.


has_foreign_key

Severity: Warning
Configurable: No
Checks: CREATE TABLE, ALTER TABLE (ADD CONSTRAINT)

Detects foreign key constraints, which can cause performance issues and operational complexity in large-scale systems.

Examples:

-- ❌ Violation (foreign key detected)
CREATE TABLE orders (
  id INT PRIMARY KEY,
  user_id INT,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

-- ✅ Correct (no foreign key)
CREATE TABLE orders (
  id INT PRIMARY KEY,
  user_id INT,
  INDEX idx_user_id (user_id)
);

-- ❌ Violation in ALTER TABLE
ALTER TABLE orders ADD CONSTRAINT fk_user 
  FOREIGN KEY (user_id) REFERENCES users(id);

has_float

Severity: Warning
Configurable: No
Checks: CREATE TABLE, ALTER TABLE (ADD/MODIFY/CHANGE COLUMN)

Detects FLOAT and DOUBLE columns, which can have precision issues. Recommends DECIMAL for exact numeric values.

Examples:

-- ❌ Violation (FLOAT has precision issues)
CREATE TABLE products (
  id INT PRIMARY KEY,
  price FLOAT
);

-- ✅ Correct (DECIMAL for exact values)
CREATE TABLE products (
  id INT PRIMARY KEY,
  price DECIMAL(10,2)
);

-- ❌ Violation in ALTER TABLE
ALTER TABLE products ADD COLUMN discount DOUBLE;

invisible_index_before_drop

Severity: Error (default), Warning (configurable)
Configurable: Yes
Checks: ALTER TABLE (DROP INDEX)

Requires indexes to be made invisible before dropping them as a safety measure. This ensures the index isn't needed before permanently removing it.

Configuration Options:

  • raiseError (string): Set to "false" to demote violations to warnings instead of errors. Default: "true".

Examples:

-- ❌ Violation
ALTER TABLE users DROP INDEX idx_email;

-- ✅ Correct
ALTER TABLE users ALTER INDEX idx_email INVISIBLE;
-- Wait and monitor performance
ALTER TABLE users DROP INDEX idx_email;

Configuration Example:

violations, err := lint.RunLinters(tables, stmts, lint.Config{
    Settings: map[string]map[string]string{
        "invisible_index_before_drop": {
            "raiseError": "false",  // Demote violations to warnings (default is "true")
        },
    },
})

multiple_alter_table

Severity: Info
Configurable: No
Checks: ALTER TABLE

Detects multiple ALTER TABLE statements on the same table that could be combined into one for better performance and fewer table rebuilds. This is an optimization suggestion, not a correctness issue.

Examples:

-- ❌ Violation (multiple ALTERs on same table)
ALTER TABLE users ADD COLUMN age INT;
ALTER TABLE users ADD INDEX idx_age (age);

-- ✅ Better (combined into one)
ALTER TABLE users 
  ADD COLUMN age INT,
  ADD INDEX idx_age (age);

name_case

Severity: Warning
Configurable: No
Checks: CREATE TABLE, ALTER TABLE (RENAME)

Ensures that table names are all lowercase to avoid case-sensitivity issues across different operating systems.

Examples:

-- ❌ Violation (mixed case)
CREATE TABLE UserAccounts (
  id INT PRIMARY KEY
);

-- ✅ Correct (lowercase)
CREATE TABLE user_accounts (
  id INT PRIMARY KEY
);

-- ❌ Violation in ALTER TABLE
ALTER TABLE users RENAME TO UserAccounts;

primary_key

Severity: Warning for existing tables, Error for new tables (CREATE TABLE in changes)
Configurable: Yes
Checks: CREATE TABLE

Ensures primary keys are defined and use appropriate data types (BIGINT UNSIGNED, BINARY, or VARBINARY by default). Severity is scoped by source: existing tables produce Warning so legacy schemas don't block unrelated ALTERs, while new CREATE TABLE statements in the incoming changes produce Error.

Configuration Options:

  • allowedTypes (string): Comma-separated list of allowed types. Default: "BIGINT,BINARY,VARBINARY".
  • Supported types: BINARY, VARBINARY, BIGINT, CHAR, VARCHAR, BIT, DECIMAL, ENUM, SET, TINYINT, SMALLINT, MEDIUMINT, INT, TIME, TIMESTAMP, YEAR, DATE, DATETIME

Examples:

-- ❌ Error in a new CREATE TABLE / ⚠️ Warning if the table already exists (no primary key)
CREATE TABLE users (
  id INT,
  name VARCHAR(100)
);

-- ❌ Error in a new CREATE TABLE / ⚠️ Warning if the table already exists (INT not allowed by default)
CREATE TABLE users (
  id INT PRIMARY KEY
);

-- ✅ Correct
CREATE TABLE users (
  id BIGINT UNSIGNED PRIMARY KEY
);

Configuration Example:

violations, err := lint.RunLinters(tables, stmts, lint.Config{
    Settings: map[string]map[string]string{
        "primary_key": {
            "allowedTypes": "BIGINT,INT,VARCHAR",  // Allow additional types
        },
    },
})

type_pedantic

Severity: Warning (same-name rule), Error (inferred FK rule) — both configurable
Configurable: Yes
Checks: CREATE TABLE, ALTER TABLE (ADD/MODIFY/CHANGE/DROP COLUMN, ADD/DROP INDEX)

Cross-table column type consistency checks. Unlike most linters in this package, type_pedantic looks at the entire schema rather than a single table — it needs the full set of existingTables to spot inconsistencies. The linter operates on a post-state view of the schema: existing tables with pending CREATE TABLE and ALTER TABLE changes applied, so it's useful both for whole-schema audits (spirit lint --source-dir) and for ALTER-driven migration flows. Two rules are bundled:

Rule 1 — Same-name columns must match types. Columns sharing a name across tables (e.g. customer_id in both orders and returns) should use the same MySQL type, including signedness and width. When one type clearly dominates the schema, the minority occurrences are flagged against that majority. When the top counts are tied (e.g. 2 BIGINT vs 2 INT), every occurrence is flagged as "inconsistent" with the conflicting types listed — the linter doesn't silently pick a winner by alphabet. By default id is excluded, since id is intentionally typed differently across unrelated tables in many schemas — use the primary_key linter for PK type enforcement. By default Rule 1 also fires only when at least one table in the column-name group indexes the column (see requireIndexed below for the rationale).

Rule 2 — Inferred foreign keys must match the referenced id type. A column named {table}_id is treated as an implicit foreign key to {table}.id. The linter tries the literal base name and English pluralizations: base+s, base+es for sibilant- and o-stems (so address_id → addresses, process_id → processes, bus_id → buses, tomato_id → tomatoes), and base[:-1]+ies for y-stems (so category_id → categories). Mismatches default to an Error because JOINs across mismatched types force implicit casts and prevent index use. requireIndexed does not gate this rule — the referenced id is always indexed. The suggestion advises growing the smaller side rather than shrinking the larger, since the underlying problem is often an undersized PK on the target.

Configuration Options:

  • checkSameName (string "true"/"false"): Enable Rule 1. Default: "true".
  • checkInferredFK (string "true"/"false"): Enable Rule 2. Default: "true".
  • requireIndexed (string "true"/"false"): Restrict Rule 1 to column-name groups where at least one occurrence is indexed (any position in any index, including inline column-level PRIMARY KEY/UNIQUE). Default: "true". Why: the real cost of a type mismatch is on JOINs and lookups, where mismatched types force implicit casts and prevent index use. On unindexed columns the schema isn't paying that cost in the first place, so flagging them is mostly false positives — incidental name collisions on scalars like status, name, or value that happen to share a name across unrelated tables.
  • ignoreColumns (string): Comma-separated column names (case-insensitive) excluded from both rules. Default: "id".
  • fkSeverity (string "error"/"warning"/"info"): Severity for Rule 2 violations. Default: "error".
  • sameNameSeverity (string "error"/"warning"/"info"): Severity for Rule 1 violations. Default: "warning".

Examples:

-- Rule 1: same-name mismatch
CREATE TABLE orders   (id BIGINT UNSIGNED PRIMARY KEY, customer_id BIGINT UNSIGNED);
CREATE TABLE invoices (id BIGINT UNSIGNED PRIMARY KEY, customer_id BIGINT UNSIGNED);
CREATE TABLE returns  (id BIGINT UNSIGNED PRIMARY KEY, customer_id INT);
-- ⚠️ returns.customer_id (INT) doesn't match the majority type (BIGINT UNSIGNED)

-- Rule 2: inferred FK mismatch
CREATE TABLE customers (id BIGINT UNSIGNED PRIMARY KEY);
CREATE TABLE orders    (id BIGINT UNSIGNED PRIMARY KEY, customer_id INT UNSIGNED);
-- ❌ orders.customer_id (INT UNSIGNED) doesn't match customers.id (BIGINT UNSIGNED)

Configuration Example:

violations, err := lint.RunLinters(tables, stmts, lint.Config{
    Settings: map[string]map[string]string{
        "type_pedantic": {
            "ignoreColumns":   "id,created_at,updated_at",
            "fkSeverity":      "warning", // downgrade FK rule to a warning
            "requireIndexed":  "false",   // also lint unindexed columns (noisier)
            "checkSameName":   "false",   // disable Rule 1 entirely
        },
    },
})

reserved_words

Severity: Warning
Configurable: No
Checks: CREATE TABLE, ALTER TABLE (ADD/MODIFY/CHANGE COLUMN, RENAME)

Checks for usage of MySQL reserved words in table and column names. Using reserved words as identifiers can cause syntax errors and requires backtick quoting. The linter uses a static list of 259 reserved words from MySQL 9.5.0.

Examples:

-- ❌ Violation (SELECT is reserved)
CREATE TABLE `select` (
  id INT PRIMARY KEY
);

-- ❌ Violation (ORDER is reserved)
CREATE TABLE `order` (
  id INT PRIMARY KEY,
  `where` VARCHAR(100)  -- WHERE is also reserved
);

-- ✅ Correct (non-reserved words)
CREATE TABLE orders (
  id INT PRIMARY KEY,
  location VARCHAR(100)
);

-- ❌ Violation in ALTER TABLE
ALTER TABLE users ADD COLUMN `select` VARCHAR(100);
ALTER TABLE users RENAME TO `table`;

Note: While MySQL allows reserved words as identifiers when quoted with backticks, it's better practice to avoid them entirely to prevent confusion and potential issues. The reserved words list is sourced directly from MySQL 9.5.0 and includes all keywords that cannot be used as unquoted identifiers.


unsafe

Severity: Warning
Configurable: Yes
Checks: ALTER TABLE, DROP TABLE, TRUNCATE TABLE, DROP DATABASE

Detects unsafe operations that can cause data loss or service disruption, such as DROP COLUMN, DROP TABLE, TRUNCATE, and DROP DATABASE.

Configuration Options:

  • allowUnsafe (string): Set to "true" to disable this linter. Default: "false".

Examples:

-- ❌ Violation (drops data)
ALTER TABLE users DROP COLUMN email;

-- ❌ Violation (drops table)
DROP TABLE users;

-- ❌ Violation (truncates data)
TRUNCATE TABLE users;

-- ✅ Safe operations
ALTER TABLE users ADD COLUMN email VARCHAR(255);
ALTER TABLE users ADD INDEX idx_email (email);

Configuration Example:

violations, err := lint.RunLinters(tables, stmts, lint.Config{
    Settings: map[string]map[string]string{
        "unsafe": {
            "allowUnsafe": "true",  // Disable unsafe checks (not recommended)
        },
    },
})

zero_date

Severity: Warning
Configurable: No
Checks: CREATE TABLE, ALTER TABLE (ADD/MODIFY/CHANGE COLUMN)

Checks for columns with zero-date default values (0000-00-00 or 0000-00-00 00:00:00) and NOT NULL date columns without defaults, which can cause issues with strict SQL modes.

Examples:

-- ❌ Violation (zero-date default)
CREATE TABLE users (
  id INT PRIMARY KEY,
  created_at DATETIME DEFAULT '0000-00-00 00:00:00'
);

-- ❌ Violation (NOT NULL without default)
CREATE TABLE users (
  id INT PRIMARY KEY,
  created_at DATETIME NOT NULL
);

-- ✅ Correct
CREATE TABLE users (
  id INT PRIMARY KEY,
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- ❌ Violation in ALTER TABLE
ALTER TABLE users ADD COLUMN legacy_date DATETIME DEFAULT '0000-00-00 00:00:00';
has_timestamp

Severity: Warning for existing tables, Error for new tables (CREATE TABLE in changes) and for ALTER TABLE statements that add/modify TIMESTAMP columns
Configurable: No
Checks: CREATE TABLE, ALTER TABLE (ADD/MODIFY/CHANGE COLUMN)

Detects TIMESTAMP columns, which have problematic behavior in MySQL (automatic initialization, timezone conversion, limited range to 2038). Recommends using DATETIME instead.

redundant_indexes

Severity: Warning
Configurable: No
Checks: CREATE TABLE

Detects redundant indexes where one index is a prefix of another (e.g., INDEX(a) is redundant if INDEX(a, b) exists). Also detects duplicate indexes with the same column list.

rename_column

Severity: Error
Configurable: No
Checks: ALTER TABLE

Detects column renames via RENAME COLUMN or CHANGE COLUMN. Column renames cannot be done atomically across application pods and break ORMs that generate column names at compile time. Recommends using ADD COLUMN + DROP COLUMN instead.


Linter Summary Table

Linter Configurable CREATE TABLE ALTER TABLE Severity
allow_charset Warning
allow_engine Warning
auto_inc_capacity Error
datetime_index_position Warning
has_foreign_key Warning
has_float Warning
has_timestamp Warning (existing) / Error (new)
invisible_index_before_drop Error (default), Warning (configurable)
multiple_alter_table Info
name_case Warning
primary_key Warning (existing) / Error (new)
redundant_indexes Warning
rename_column Error
reserved_words Warning
type_pedantic Warning / Error
unsafe Warning
zero_date Warning

Example Linters

The example package provides demonstration linters for learning purposes:

TableNameLengthLinter

Checks that table names don't exceed MySQL's 64 character limit.

DuplicateColumnLinter

Detects duplicate column definitions in CREATE TABLE statements.

See pkg/lint/example/ for reference implementations.

Contributing

When adding new linters to the lint package:

  1. Create a new file with the lint_ prefix (e.g., lint_my_rule.go)
  2. Implement the Linter interface with all required methods
  3. Register in init() function to enable automatic registration
  4. Add comprehensive tests in a corresponding lint_my_rule_test.go file
  5. Document the linter with clear comments and examples
  6. Choose appropriate severity levels:
    • SeverityError for violations that will cause actual problems
    • SeverityWarning for best practice violations
    • SeverityInfo for suggestions and style preferences
  7. Provide helpful messages with actionable suggestions when possible
  8. Update this README with documentation for the new linter
File Naming Convention
  • Linter implementation: lint_<name>.go
  • Linter tests: lint_<name>_test.go
Example Structure
pkg/lint/
├── lint.go                      # Core API
├── linter.go                    # Interface definition
├── registry.go                  # Registration system
├── violation.go                 # Violation types
├── lint_invisible_index.go      # Built-in linter
├── lint_multiple_alter.go       # Built-in linter
├── lint_primary_key_type.go     # Built-in linter
├── lint_my_new_rule.go          # Your new linter
└── lint_my_new_rule_test.go     # Your tests

Documentation

Overview

Package lint provides a framework for static analysis of MySQL schema definitions and DDL statements. It enables validation and best-practice enforcement beyond the runtime checks provided by the check package.

The linter framework operates on parsed CREATE TABLE statements rather than live database connections.

Basic Usage

Linters are registered via init() functions and executed via RunLinters():

package naming

func init() {
    lint.Register(TableNameLinter{})
}

// Later, run all linters:
violations := lint.RunLinters(tables, stmts, config)

Creating a Linter

To create a custom linter, implement the Linter interface:

type MyLinter struct{}

func (l *MyLinter) Name() string { return "my_linter" }
func (l *MyLinter) Category() string { return "custom" }
func (l *MyLinter) Description() string { return "My custom linter" }
func (l *MyLinter) Lint(createTables []*statement.CreateTable, statements []*statement.AbstractStatement) []lint.Violation {
    // Perform linting logic
    return violations
}

Configuration

Linters can be enabled/disabled via the Config.Enabled map:

config := lint.Config{
    Enabled: map[string]bool{
        "table_name": true,
        "column_name": false,
    },
}

Configurable linters can implement the ConfigurableLinter interface to accept custom settings via Config.Settings. Settings must be provided as map[string]string:

config := lint.Config{
    Settings: map[string]map[string]string{
        "my_linter": {
            "option1": "value1",
            "option2": "value2",
        },
    },
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AlterTableTypeToString

func AlterTableTypeToString(tp ast.AlterTableType) string

AlterTableTypeToString converts an AlterTableType constant to a human-readable string

func ConfigBool

func ConfigBool(value string, key string) (bool, error)

ConfigBool parses a boolean configuration value from a string. It accepts "true" or "false" (case-insensitive) and returns an error for invalid values. The key parameter is used in error messages to provide context.

func CreateTableStatements

func CreateTableStatements(statements ...any) iter.Seq[*statement.CreateTable]

CreateTableStatements returns an iterator over all CREATE TABLE statements, combining those from the existing schema state and those included in incoming changes.

func Disable

func Disable(names ...string) error

Disable disables specific linters by name. Returns an error if the linter is not found.

func Enable

func Enable(names ...string) error

Enable enables specific linters by name. Returns an error if the linter is not found.

func HasErrors

func HasErrors(violations []Violation) bool

HasErrors returns true if any violations have ERROR severity.

func HasWarnings

func HasWarnings(violations []Violation) bool

HasWarnings returns true if any violations have WARNING severity.

func List

func List() []string

List returns the names of all registered linters in sorted order.

func LoadSchemaFromDSN added in v0.11.0

func LoadSchemaFromDSN(ctx context.Context, dsn string) ([]*statement.CreateTable, error)

LoadSchemaFromDSN connects to a MySQL server and retrieves all CREATE TABLE statements from the connected database, parsed into structured CreateTable objects.

func LoadSchemaFromDir added in v0.11.0

func LoadSchemaFromDir(dir string) ([]*statement.CreateTable, error)

LoadSchemaFromDir reads all .sql files from a directory and parses them as CREATE TABLE statements. Each file should contain exactly one CREATE TABLE statement.

func PostState added in v0.14.0

func PostState(existing []*statement.CreateTable, changes []*statement.AbstractStatement) []*statement.CreateTable

PostState returns a deterministic post-state view of the schema: the existing tables augmented with CREATE TABLE statements from changes, and existing tables patched with ADD/DROP/MODIFY/CHANGE COLUMN and ADD/DROP INDEX (and DROP PRIMARY KEY) specs from ALTER TABLE statements. Other ALTER specs are ignored.

The result is sorted by table name. The input tables are not mutated.

Linters that need to evaluate the schema as it will exist *after* a set of pending changes (rather than as it exists today) should iterate this slice rather than existingTables directly — otherwise they will produce false positives for ALTER statements that fix legacy issues.

func PreStateColumns added in v0.14.0

func PreStateColumns(existing []*statement.CreateTable) map[string]map[string]*statement.Column

PreStateColumns returns a lookup of (lowercased table name) → (lowercased column name) → column from the existing tables. Linters use this to distinguish columns that pre-existed (severity Warning) from columns added or modified by changes (severity Error).

func Register

func Register(l Linter)

Register registers a linter with the global registry. This should be called from init() functions in linter implementations. Linters are enabled by default when registered.

func Reset

func Reset()

Reset clears all registered linters. This is primarily useful for testing.

func Stringer

func Stringer(l Linter) string

Stringer returns a string representation of the linter This is a helper function used by linters' String() methods.

Types

type AllowCharset

type AllowCharset struct {
	// contains filtered or unexported fields
}

func (*AllowCharset) Configure

func (l *AllowCharset) Configure(config map[string]string) error

func (*AllowCharset) DefaultConfig

func (l *AllowCharset) DefaultConfig() map[string]string

func (*AllowCharset) Description

func (l *AllowCharset) Description() string

func (*AllowCharset) Lint

func (l *AllowCharset) Lint(createTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

Lint walks the post-state of the schema. The message form preserves the pre/post-state distinction that the old ALTER pass made explicit:

  • "New column …" when the column did not exist in the pre-state.
  • "Column … modified to use …" when MODIFY/CHANGE COLUMN retypes an existing column.
  • "Column … has unsupported character set" for pre-existing untouched columns and columns inside a new CREATE TABLE.

func (*AllowCharset) Name

func (l *AllowCharset) Name() string

func (*AllowCharset) String

func (l *AllowCharset) String() string

type AllowEngine

type AllowEngine struct {
	// contains filtered or unexported fields
}

func (*AllowEngine) Configure

func (l *AllowEngine) Configure(config map[string]string) error

func (*AllowEngine) DefaultConfig

func (l *AllowEngine) DefaultConfig() map[string]string

func (*AllowEngine) Description

func (l *AllowEngine) Description() string

func (*AllowEngine) Lint

func (l *AllowEngine) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

Lint walks the post-state of the schema so an ALTER TABLE ENGINE=... that migrates *to* an allowed engine no longer false-positives against the legacy engine, and one that changes the engine to a disallowed value is reported against the table's final shape.

func (*AllowEngine) Name

func (l *AllowEngine) Name() string

func (*AllowEngine) String

func (l *AllowEngine) String() string

type AutoIncCapacityLinter

type AutoIncCapacityLinter struct {
	// contains filtered or unexported fields
}

func (*AutoIncCapacityLinter) Configure

func (l *AutoIncCapacityLinter) Configure(config map[string]string) error

func (*AutoIncCapacityLinter) DefaultConfig

func (l *AutoIncCapacityLinter) DefaultConfig() map[string]string

func (*AutoIncCapacityLinter) Description

func (l *AutoIncCapacityLinter) Description() string

func (*AutoIncCapacityLinter) Lint

func (l *AutoIncCapacityLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

Lint walks the post-state of the schema so an ALTER that raises AUTO_INCREMENT or widens the column type is linted against the table's final shape rather than the pre-ALTER snapshot.

func (*AutoIncCapacityLinter) Name

func (l *AutoIncCapacityLinter) Name() string

func (*AutoIncCapacityLinter) String

func (l *AutoIncCapacityLinter) String() string

type Config

type Config struct {
	// Enabled maps linter names to whether they are enabled
	// If a linter is not in this map, it uses its default enabled state
	Enabled map[string]bool

	// Settings maps linter names to their configuration as map[string]string
	// Each linter's settings are provided as key-value string pairs
	Settings map[string]map[string]string

	// LintOnlyChanges indicates whether to lint only the changes
	// or all of the existing schema plus the changes.
	LintOnlyChanges bool

	// IgnoreTables can be used to discard violations for specific tables
	IgnoreTables map[string]bool
}

Config holds linter configuration

func (*Config) IsEnabled

func (c *Config) IsEnabled(linterName string) bool

IsEnabled checks the config as well as the registry to see if a given linter is enabled in either place. If the linter doesn't exist, false is returned because a non-existent linter can't be enabled.

type ConfigurableLinter

type ConfigurableLinter interface {
	Linter

	// Configure applies configuration to the linter
	// Configuration is provided as a map of string keys to string values
	Configure(config map[string]string) error

	// DefaultConfig returns the default configuration for this linter
	DefaultConfig() map[string]string
}

ConfigurableLinter is an optional interface for linters that support configuration

type DatetimeIndexPositionLinter added in v0.14.0

type DatetimeIndexPositionLinter struct{}

func (*DatetimeIndexPositionLinter) Description added in v0.14.0

func (l *DatetimeIndexPositionLinter) Description() string

func (*DatetimeIndexPositionLinter) Lint added in v0.14.0

func (l *DatetimeIndexPositionLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

Lint operates on a post-state view of the schema. For each composite index (>=2 columns) on each table, it flags any DATETIME, TIMESTAMP, or DATE column that appears in a non-last position.

Rationale: date/time columns are overwhelmingly queried with range predicates (>, >=, <, <=, BETWEEN). Once the MySQL optimizer hits a range predicate on a column inside a composite index, the columns that follow can no longer be used for sorted index access — they're only available for index condition pushdown filtering. So a composite index that places a date/time column anywhere but last is usually carrying dead weight in its trailing columns.

This is a heuristic with no visibility into the actual query workload, so it always emits SeverityWarning. FULLTEXT and SPATIAL indexes are skipped — they don't behave like B-tree indexes for range scans.

func (*DatetimeIndexPositionLinter) Name added in v0.14.0

func (*DatetimeIndexPositionLinter) String added in v0.14.0

func (l *DatetimeIndexPositionLinter) String() string

type DiffCmd added in v0.11.0

type DiffCmd struct {
	// Source of existing schema (exactly one required)
	SourceDSN string `help:"MySQL DSN for existing schema" xor:"source" required:"" env:"MYSQL_DSN"`
	SourceDir string `help:"Directory of CREATE TABLE .sql files for existing schema" xor:"source" required:"" type:"existingdir"`

	// Target / proposed changes (exactly one required)
	TargetDSN   string   `help:"MySQL DSN for target schema (will diff against source)" xor:"target" required:""`
	TargetDir   string   `help:"Directory of CREATE TABLE .sql files for target state" xor:"target" required:"" type:"existingdir"`
	TargetAlter []string `help:"ALTER TABLE statement(s) to apply" short:"a" xor:"target" required:""`

	// Filtering
	IgnoreTables string `help:"Regex pattern of table names to ignore" default:""`
}

DiffCmd is the Kong CLI struct for the diff command. It diffs two schemas, shows the generated DDL, and lints the changes.

func (*DiffCmd) Run added in v0.11.0

func (cmd *DiffCmd) Run() error

Run executes the diff command. It is called by Kong. The output is valid SQL: lint violations appear as SQL comments at the top, followed by the DDL statements. This allows the output to be piped directly into mysql.

type HasFKLinter

type HasFKLinter struct{}

func (*HasFKLinter) Description

func (l *HasFKLinter) Description() string

func (*HasFKLinter) Lint

func (l *HasFKLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

Lint walks the post-state of the schema, so an ALTER DROP FOREIGN KEY that fixes a legacy FK does not produce a false positive, and an ALTER ADD FOREIGN KEY surfaces the violation against the table's final shape.

func (*HasFKLinter) Name

func (l *HasFKLinter) Name() string

func (*HasFKLinter) String

func (l *HasFKLinter) String() string

type HasFloatLinter

type HasFloatLinter struct{}

func (*HasFloatLinter) Description

func (l *HasFloatLinter) Description() string

func (*HasFloatLinter) Lint

func (l *HasFloatLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

Lint operates on a post-state view of the schema, so a column being converted away from FLOAT/DOUBLE in an ALTER doesn't generate a false positive. The violation message uses three forms to preserve actionability:

  • "New column …" when the column did not exist in the pre-state (ADD COLUMN, or a column in a CREATE TABLE).
  • "Column … modified to use …" when the column existed pre-state and is being retyped by MODIFY / CHANGE COLUMN.
  • "Column … uses …" for pre-existing untouched columns.

func (*HasFloatLinter) Name

func (l *HasFloatLinter) Name() string

func (*HasFloatLinter) String

func (l *HasFloatLinter) String() string

type HasTimestampLinter added in v0.13.0

type HasTimestampLinter struct{}

func (*HasTimestampLinter) Description added in v0.13.0

func (l *HasTimestampLinter) Description() string

func (*HasTimestampLinter) Lint added in v0.13.0

func (l *HasTimestampLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

Lint operates on a post-state view of the schema. For each TIMESTAMP column in the post-state, the severity is:

  • Warning, if the column existed in the pre-state with TIMESTAMP type and no incoming change touches it (legacy schema — don't boil the ocean).
  • Error, if the column is newly added in this changeset, or modified to TIMESTAMP, or appears in a CREATE TABLE in the changeset.

Columns that were TIMESTAMP in the pre-state but are being dropped or converted to a different type in the changeset don't appear in the post-state and therefore produce no violation — which is exactly the desired behavior for migrations that fix legacy TIMESTAMP columns.

func (*HasTimestampLinter) Name added in v0.13.0

func (l *HasTimestampLinter) Name() string

func (*HasTimestampLinter) String added in v0.13.0

func (l *HasTimestampLinter) String() string

type InvisibleIndexBeforeDropLinter

type InvisibleIndexBeforeDropLinter struct {
	// contains filtered or unexported fields
}

InvisibleIndexBeforeDropLinter checks that indexes are made invisible before dropping. This is a safety practice to ensure the index is not needed before permanently removing it.

func (*InvisibleIndexBeforeDropLinter) Configure

func (l *InvisibleIndexBeforeDropLinter) Configure(config map[string]string) error

func (*InvisibleIndexBeforeDropLinter) DefaultConfig

func (l *InvisibleIndexBeforeDropLinter) DefaultConfig() map[string]string

func (*InvisibleIndexBeforeDropLinter) Description

func (l *InvisibleIndexBeforeDropLinter) Description() string

func (*InvisibleIndexBeforeDropLinter) Lint

func (*InvisibleIndexBeforeDropLinter) Name

func (*InvisibleIndexBeforeDropLinter) String

type LintCmd added in v0.11.0

type LintCmd struct {
	// Source of existing schema (exactly one required)
	SourceDSN string `help:"MySQL DSN for existing schema" xor:"source" required:"" env:"MYSQL_DSN"`
	SourceDir string `help:"Directory of CREATE TABLE .sql files for existing schema" xor:"source" required:"" type:"existingdir"`

	// Filtering
	IgnoreTables string `help:"Regex pattern of table names to ignore" default:""`
}

LintCmd is the Kong CLI struct for the lint command. It lints an entire schema without requiring a target.

func (*LintCmd) Run added in v0.11.0

func (cmd *LintCmd) Run() error

Run executes the lint command. It is called by Kong.

type Linter

type Linter interface {
	// Name returns the unique name of this linter
	Name() string

	// Description returns a human-readable description of what this linter checks
	Description() string

	// Lint performs the actual linting and returns any violations found.
	// Linters can use either or both of the parameters as needed.
	Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

	// String returns a string representation of the linter
	String() string
}

Linter is the interface that all linters must implement

func Get

func Get(name string) (Linter, error)

Get returns a linter by name. Returns an error if the linter is not found.

type Location

type Location struct {
	// Table is the name of the table where the violation occurred
	Table string

	// Column is the name of the column (if applicable)
	Column *string

	// Index is the name of the index (if applicable)
	Index *string

	// Constraint is the name of the constraint (if applicable)
	Constraint *string
}

Location provides information about where a violation occurred

func (*Location) String

func (l *Location) String() string

type MultipleAlterTableLinter

type MultipleAlterTableLinter struct{}

MultipleAlterTableLinter checks for multiple ALTER TABLE statements affecting the same table. Multiple ALTER TABLE statements on the same table should be combined into a single statement for better performance, fewer table rebuilds, and decreased danger of bad intermediate state.

func (*MultipleAlterTableLinter) Description

func (l *MultipleAlterTableLinter) Description() string

func (*MultipleAlterTableLinter) Lint

func (*MultipleAlterTableLinter) Name

func (l *MultipleAlterTableLinter) Name() string

func (*MultipleAlterTableLinter) String

func (l *MultipleAlterTableLinter) String() string

type NameCaseLinter

type NameCaseLinter struct{}

func (*NameCaseLinter) Description

func (l *NameCaseLinter) Description() string

func (*NameCaseLinter) Lint

func (l *NameCaseLinter) Lint(createTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

Lint walks the post-state of the schema so an ALTER RENAME that fixes a non-lowercase name does not produce a false positive, and a rename that introduces a non-lowercase name surfaces against the final table name.

func (*NameCaseLinter) Name

func (*NameCaseLinter) Name() string

func (*NameCaseLinter) String

func (l *NameCaseLinter) String() string

type Plan added in v0.11.3

type Plan struct {
	// Changes is the ordered list of DDL changes with per-statement lint results.
	Changes []PlannedChange
}

Plan holds the complete result of a declarative-to-imperative diff with linting.

func PlanChanges added in v0.11.3

func PlanChanges(current, desired []table.TableSchema, diffOpts *statement.DiffOptions, lintConfig *Config) (*Plan, error)

PlanChanges computes the imperative DDL statements needed to transform the current schema into the desired schema, and lints each statement against the current schema. This combines statement.DeclarativeToImperative with RunLinters into a single call, returning per-statement lint results.

LintOnlyChanges is always set to true regardless of the provided lintConfig, since PlanChanges only produces lint results for tables that have changes.

Parameters:

  • current: the current table schemas (CREATE TABLE DDL)
  • desired: the desired table schemas (CREATE TABLE DDL)
  • diffOpts: options for the diff (nil uses defaults)
  • lintConfig: configuration for linting (nil uses defaults; LintOnlyChanges is always overridden to true)

func (*Plan) HasChanges added in v0.11.3

func (p *Plan) HasChanges() bool

HasChanges returns true if the plan contains any DDL statements.

func (*Plan) HasErrors added in v0.11.3

func (p *Plan) HasErrors() bool

HasErrors returns true if any change has lint errors.

func (*Plan) HasInfos added in v0.11.3

func (p *Plan) HasInfos() bool

HasInfos returns true if any change has lint infos.

func (*Plan) HasWarnings added in v0.11.3

func (p *Plan) HasWarnings() bool

HasWarnings returns true if any change has lint warnings.

func (*Plan) Statements added in v0.11.3

func (p *Plan) Statements() []string

Statements returns just the DDL strings from all changes.

type PlannedChange added in v0.11.3

type PlannedChange struct {
	// Statement is the semicolon-terminated DDL (ALTER, CREATE, or DROP TABLE).
	Statement string

	// TableName is the table affected by this change.
	TableName string

	// Violations contains lint violations for this table.
	// Linters operate at the table level. When a diff produces multiple
	// statements for the same table, violations are attached only to the
	// last statement to avoid duplication.
	Violations []Violation
}

PlannedChange represents a single DDL statement produced by declarative schema diffing, along with any lint violations associated with it.

func (*PlannedChange) Errors added in v0.11.3

func (c *PlannedChange) Errors() []Violation

Errors returns lint violations with ERROR severity.

func (*PlannedChange) Infos added in v0.11.3

func (c *PlannedChange) Infos() []Violation

Infos returns lint violations with INFO severity.

func (*PlannedChange) Warnings added in v0.11.3

func (c *PlannedChange) Warnings() []Violation

Warnings returns lint violations with WARNING severity.

type PrimaryKeyLinter

type PrimaryKeyLinter struct {
	// contains filtered or unexported fields
}

PrimaryKeyLinter checks that primary keys are defined and use appropriate data types. Primary keys should be BIGINT (preferably UNSIGNED) or BINARY/VARBINARY, but the linter can be configured to allow other types. Severity is scoped by source: existing tables produce SeverityWarning (don't block ALTERs on legacy schemas), while CREATE TABLE statements in the incoming changes produce SeverityError (enforce standards on new tables).

func (*PrimaryKeyLinter) Configure

func (l *PrimaryKeyLinter) Configure(config map[string]string) error

func (*PrimaryKeyLinter) DefaultConfig

func (l *PrimaryKeyLinter) DefaultConfig() map[string]string

func (*PrimaryKeyLinter) Description

func (l *PrimaryKeyLinter) Description() string

func (*PrimaryKeyLinter) Lint

func (l *PrimaryKeyLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

func (*PrimaryKeyLinter) Name

func (l *PrimaryKeyLinter) Name() string

func (*PrimaryKeyLinter) String

func (l *PrimaryKeyLinter) String() string

type RedundantIndexLinter

type RedundantIndexLinter struct{}

RedundantIndexLinter checks for redundant indexes in tables. An index is considered redundant if:

  1. Its columns are a prefix of the PRIMARY KEY columns
  2. Another index's column list starts with this index's columns (prefix match)
  3. Another index has exactly the same columns in the same order (duplicate)
  4. The index ends with PRIMARY KEY columns (suffix redundancy) — InnoDB auto-appends PK columns to secondary indexes, so spelling them out is wasteful.
  5. The index starts with the full PRIMARY KEY (prefix redundancy) — point and equality lookups by PK are already served by the clustered index, so the leading PK columns add no capability that a non-PK-leading variant of the index wouldn't already provide.

The linter evaluates the *post-state* of the schema (existing tables with CREATE TABLE / ALTER TABLE changes applied) so warnings fire when a redundant index is being introduced, and stay silent when it is being removed — matching the post-state convention established for other linters in #840.

func (*RedundantIndexLinter) Description

func (l *RedundantIndexLinter) Description() string

func (*RedundantIndexLinter) Lint

func (l *RedundantIndexLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) []Violation

func (*RedundantIndexLinter) Name

func (l *RedundantIndexLinter) Name() string

func (*RedundantIndexLinter) String

func (l *RedundantIndexLinter) String() string

type RenameColumnLinter added in v0.11.0

type RenameColumnLinter struct{}

RenameColumnLinter detects column renames in ALTER TABLE statements. This is specifically for imperative mode (user-supplied ALTER statements). There are many reasons why renames are problematic:

  1. Spirit does not support them (unless they are INSTANT)
  2. For applications with many pods, atomically changing a column name is basically impossible. It only works if the column is not used, or all column references are via SELECT * with ordinal reference.
  3. Some ORMs like jOOQ generate column names at compile time. If there is a rename, it will break the application until code is recompiled.
  4. They are also not supported by declarative workflows, which is what we should all be moving to.

The recommended solution is to use ADD COLUMN+later DROP COLUMN instead of RENAME. This is the only safe way.

func (*RenameColumnLinter) Description added in v0.11.0

func (l *RenameColumnLinter) Description() string

func (*RenameColumnLinter) Lint added in v0.11.0

func (l *RenameColumnLinter) Lint(_ []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

func (*RenameColumnLinter) Name added in v0.11.0

func (l *RenameColumnLinter) Name() string

func (*RenameColumnLinter) String added in v0.11.0

func (l *RenameColumnLinter) String() string

type ReservedWordsLinter

type ReservedWordsLinter struct{}

ReservedWordsLinter checks for usage of MySQL reserved words in table and column names. Using reserved words as identifiers can cause syntax errors and requires quoting.

func (*ReservedWordsLinter) Description

func (l *ReservedWordsLinter) Description() string

func (*ReservedWordsLinter) Lint

func (l *ReservedWordsLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

Lint walks the post-state of the schema so warnings reflect the final table and column names. ALTERs that DROP, RENAME, or MODIFY away a reserved-word identifier no longer produce false positives, and ALTERs that introduce a reserved-word identifier (ADD COLUMN, RENAME TABLE, etc.) are reported against the final shape.

func (*ReservedWordsLinter) Name

func (l *ReservedWordsLinter) Name() string

func (*ReservedWordsLinter) String

func (l *ReservedWordsLinter) String() string

type Severity

type Severity int

Severity represents the severity level of a linting violation

const (
	// SeverityInfo indicates a suggestion or style preference
	// This is the default value if no explicit Severity is given
	SeverityInfo Severity = iota

	// SeverityWarning indicates a best practice violation or potential issue
	SeverityWarning

	// SeverityError indicates a violation that will cause actual problems
	// (syntax errors, MySQL limitations, etc.)
	SeverityError
)

func (Severity) String added in v0.10.1

func (s Severity) String() string

type TypePedanticLinter added in v0.14.0

type TypePedanticLinter struct {
	// contains filtered or unexported fields
}

TypePedanticLinter enforces type consistency across tables in the same schema.

Rule 1 (same_name): Columns sharing a name across tables should share a type. Rule 2 (inferred_fk): Columns named like {table}_id are inferred to reference {table}.id and should match its type — JOINs across mismatched types force implicit casts and prevent index use.

Both rules operate on a synthesized post-state view: existing tables with pending CREATE TABLE / ALTER TABLE changes applied. This makes the linter useful both for whole-schema audits and for ALTER-driven migration flows.

func (*TypePedanticLinter) Configure added in v0.14.0

func (l *TypePedanticLinter) Configure(config map[string]string) error

func (*TypePedanticLinter) DefaultConfig added in v0.14.0

func (l *TypePedanticLinter) DefaultConfig() map[string]string

func (*TypePedanticLinter) Description added in v0.14.0

func (l *TypePedanticLinter) Description() string

func (*TypePedanticLinter) Lint added in v0.14.0

func (l *TypePedanticLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

func (*TypePedanticLinter) Name added in v0.14.0

func (l *TypePedanticLinter) Name() string

func (*TypePedanticLinter) String added in v0.14.0

func (l *TypePedanticLinter) String() string

type UnsafeLinter

type UnsafeLinter struct {
	// contains filtered or unexported fields
}

func (*UnsafeLinter) Configure

func (l *UnsafeLinter) Configure(config map[string]string) error

func (*UnsafeLinter) DefaultConfig

func (l *UnsafeLinter) DefaultConfig() map[string]string

func (*UnsafeLinter) Description

func (l *UnsafeLinter) Description() string

func (*UnsafeLinter) Lint

func (l *UnsafeLinter) Lint(_ []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

func (*UnsafeLinter) Name

func (l *UnsafeLinter) Name() string

func (*UnsafeLinter) String

func (l *UnsafeLinter) String() string

type Violation

type Violation struct {
	// Linter is the linter that produced this violation
	Linter Linter

	// Severity is the severity level of the violation
	Severity Severity

	// Message is a human-readable description of the violation
	Message string

	// Location provides information about where the violation occurred
	Location *Location

	// Suggestion is an optional suggestion for fixing the violation
	Suggestion *string

	// Context provides additional context-specific information
	Context map[string]any
}

Violation represents a linting violation found during analysis

func FilterByLinter

func FilterByLinter(violations []Violation, linterName string) []Violation

FilterByLinter returns only violations from the specified linter.

func RunLinters

func RunLinters(existingSchema []*statement.CreateTable, changes []*statement.AbstractStatement, config Config) ([]Violation, error)

RunLinters runs all enabled linters and returns any violations found. Linters are executed in an undefined order.

A linter is executed if:

  • It is enabled by default (set during Register), AND
  • It is not explicitly disabled in config.Enabled

OR:

  • It is explicitly enabled in config.Enabled

If a linter implements ConfigurableLinter and has settings in config.Settings, those settings are applied before running the linter.

func (Violation) String

func (v Violation) String() string

type ZeroDateLinter

type ZeroDateLinter struct{}

func (*ZeroDateLinter) Description

func (l *ZeroDateLinter) Description() string

func (*ZeroDateLinter) Lint

func (l *ZeroDateLinter) Lint(existingTables []*statement.CreateTable, changes []*statement.AbstractStatement) (violations []Violation)

Lint operates on a post-state view of the schema so that ALTERs which fix zero-date columns don't produce false positives on the pre-state.

func (*ZeroDateLinter) Name

func (l *ZeroDateLinter) Name() string

func (*ZeroDateLinter) String

func (l *ZeroDateLinter) String() string

Directories

Path Synopsis
Package example provides example linters to demonstrate the linter framework.
Package example provides example linters to demonstrate the linter framework.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL