package lint import ( "git.offline-twitter.com/offline-labs/gas-stack/pkg/schema" ) type Check struct { Name string Explanation string Execute func(schema.Schema) []CheckResult } // CheckResult represents a row in the query result with error message, table name, and column name. type CheckResult struct { ErrorMsg string `db:"error_msg"` TableName string `db:"table_name"` ColumnName string `db:"column_name"` } var Checks = []Check{ { Name: "require_not_null", Explanation: "All columns should be marked as `not null` unless they are foreign keys. (Primary keys are\n" + "automatically not-null, and don't need to be specified.)", Execute: func(s schema.Schema) (ret []CheckResult) { for tablename := range s.Tables { for _, column := range s.Tables[tablename].Columns { if !column.IsNotNull && !column.IsForeignKey && !column.IsPrimaryKey { ret = append(ret, CheckResult{ ErrorMsg: "Column should be \"not null\"", TableName: tablename, ColumnName: column.Name, }) } } } return }, }, { Name: "require_strict", Explanation: "All tables should be marked as `strict` (must specify column types; types must be int,\n" + "integer, real, text, blob or any). This disallows all 'date' and 'time' column types.\n" + "See more: https://www.sqlite.org/stricttables.html", Execute: func(s schema.Schema) (ret []CheckResult) { for tablename := range s.Tables { if !s.Tables[tablename].IsStrict { ret = append(ret, CheckResult{ ErrorMsg: "Table should be marked \"strict\"", TableName: tablename, ColumnName: "", }) } } return }, }, { Name: "forbid_int_type", Explanation: "All columns should use `integer` type instead of `int`.", Execute: func(s schema.Schema) (ret []CheckResult) { for tablename := range s.Tables { for _, column := range s.Tables[tablename].Columns { if column.Type == "int" { ret = append(ret, CheckResult{ ErrorMsg: "Column should use \"integer\" type instead of \"int\"", TableName: tablename, ColumnName: column.Name, }) } } } return }, }, { Name: "require_explicit_primary_key", Explanation: "All tables must have a primary key. If it's rowid, it has to be named explicitly.", Execute: func(s schema.Schema) (ret []CheckResult) { tableloop: for tablename := range s.Tables { for _, column := range s.Tables[tablename].Columns { if column.IsPrimaryKey { continue tableloop } } ret = append(ret, CheckResult{ ErrorMsg: "Table should declare an explicit primary key", TableName: tablename, ColumnName: "", }) } return }, }, { Name: "require_indexes_for_foreign_keys", Explanation: "Columns referenced by foreign keys must have indexes.", Execute: func(s schema.Schema) (ret []CheckResult) { for tablename := range s.Tables { fk_loop: for _, column := range s.Tables[tablename].Columns { if !column.IsForeignKey { continue } // Check if target column is a primary key for _, target_col := range s.Tables[column.ForeignKeyTargetTable].Columns { if target_col.Name == column.ForeignKeyTargetColumn && target_col.IsPrimaryKey { continue fk_loop } } // Check if target column is at the beginning of any index for _, idx := range s.Indexes { if idx.TableName != column.ForeignKeyTargetTable { continue } for _, idx_col_name := range idx.Columns { if column.ForeignKeyTargetColumn == idx_col_name { continue fk_loop } break // Only look at 1st column } } ret = append(ret, CheckResult{ ErrorMsg: "Foreign keys should point to indexed columns", TableName: tablename, ColumnName: column.Name, }) } } return }, }, }