This commit is contained in:
		
							parent
							
								
									a4de16a1dd
								
							
						
					
					
						commit
						8b422ada11
					
				
							
								
								
									
										136
									
								
								pkg/schema/lint/checks.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								pkg/schema/lint/checks.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,136 @@ | ||||
| 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 | ||||
| 		}, | ||||
| 	}, | ||||
| } | ||||
							
								
								
									
										66
									
								
								pkg/schema/lint/checks_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								pkg/schema/lint/checks_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| package lint_test | ||||
| 
 | ||||
| import ( | ||||
| 	"slices" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	_ "github.com/mattn/go-sqlite3" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| 
 | ||||
| 	"git.offline-twitter.com/offline-labs/gas-stack/pkg/schema" | ||||
| 	"git.offline-twitter.com/offline-labs/gas-stack/pkg/schema/lint" | ||||
| ) | ||||
| 
 | ||||
| func TestFailureCases(t *testing.T) { | ||||
| 	test_cases := []struct { | ||||
| 		sqlFile          string | ||||
| 		expectedFailures []string | ||||
| 	}{ | ||||
| 		{"test_schemas/failure-has-foreign-key-no-index.sql", []string{"require_indexes_for_foreign_keys"}}, | ||||
| 		{"test_schemas/failure-has-ints.sql", []string{"forbid_int_type"}}, | ||||
| 		{"test_schemas/failure-has-nulls.sql", []string{"require_not_null"}}, | ||||
| 		{"test_schemas/failure-no-strict.sql", []string{"require_strict"}}, | ||||
| 		{"test_schemas/failure-total.sql", []string{ | ||||
| 			"require_not_null", | ||||
| 			"require_explicit_primary_key", | ||||
| 			"forbid_int_type", | ||||
| 			"require_strict", | ||||
| 			"require_indexes_for_foreign_keys", | ||||
| 		}}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, test_case := range test_cases { | ||||
| 		t.Run(test_case.sqlFile, func(t *testing.T) { | ||||
| 			schema, err := schema.SchemaFromSQLFile(test_case.sqlFile) | ||||
| 			require.NoError(t, err) | ||||
| 
 | ||||
| 			for _, check := range lint.Checks { | ||||
| 				results := check.Execute(schema) | ||||
| 
 | ||||
| 				is_failure := len(results) > 0 | ||||
| 				is_failure_expected := slices.Contains(test_case.expectedFailures, check.Name) | ||||
| 
 | ||||
| 				if is_failure != is_failure_expected { | ||||
| 					if is_failure_expected { | ||||
| 						t.Errorf("Expected check '%s' to fail, but it passed: %s", check.Name, test_case.sqlFile) | ||||
| 					} else { | ||||
| 						t.Errorf("Expected check '%s' to pass, but it failed: %s (%q.%q)", check.Name, test_case.sqlFile, results[0].TableName, results[0].ColumnName) | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestSuccessCase(t *testing.T) { | ||||
| 	file := "test_schemas/success.sql" | ||||
| 	schema, err := schema.SchemaFromSQLFile(file) | ||||
| 	require.NoError(t, err) | ||||
| 
 | ||||
| 	for _, check := range lint.Checks { | ||||
| 		results := check.Execute(schema) | ||||
| 		for _, r := range results { | ||||
| 			t.Errorf("Unexpected error in file %q: %#v", file, r) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,12 @@ | ||||
| create table stuff ( | ||||
|     rowid integer primary key, | ||||
|     data text not null, | ||||
|     amount integer not null | ||||
| ) strict; | ||||
| 
 | ||||
| create table stuff2 ( | ||||
|     weird_pk integer primary key, | ||||
|     label text not null unique, | ||||
|     stuff_id integer references stuff(rowid), | ||||
|     alternative_stuff_id integer references stuff(amount) | ||||
| ) strict; | ||||
							
								
								
									
										13
									
								
								pkg/schema/lint/test_schemas/failure-has-ints.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								pkg/schema/lint/test_schemas/failure-has-ints.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| create table stuff ( | ||||
|     rowid integer primary key, | ||||
|     data text not null, | ||||
|     amount integer not null | ||||
| ) strict; | ||||
| create index index_stuff_amount on stuff (amount); | ||||
| 
 | ||||
| create table stuff2 ( | ||||
|     weird_pk integer primary key, | ||||
|     label text not null unique, | ||||
|     stuff_id int references stuff(rowid), | ||||
|     alternative_stuff_id integer references stuff(amount) | ||||
| ) strict; | ||||
							
								
								
									
										13
									
								
								pkg/schema/lint/test_schemas/failure-has-nulls.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								pkg/schema/lint/test_schemas/failure-has-nulls.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| create table stuff ( | ||||
|     rowid integer primary key, | ||||
|     data text not null, | ||||
|     amount integer | ||||
| ) strict; | ||||
| create index index_stuff_amount on stuff (amount); | ||||
| 
 | ||||
| create table stuff2 ( | ||||
|     weird_pk integer primary key, | ||||
|     label text not null unique, | ||||
|     stuff_id integer references stuff(rowid), | ||||
|     alternative_stuff_id integer references stuff(amount) | ||||
| ) strict; | ||||
							
								
								
									
										13
									
								
								pkg/schema/lint/test_schemas/failure-no-strict.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								pkg/schema/lint/test_schemas/failure-no-strict.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| create table stuff ( | ||||
|     rowid integer primary key, | ||||
|     data text not null, | ||||
|     amount integer not null | ||||
| ) strict; | ||||
| create index index_stuff_amount on stuff (amount); | ||||
| 
 | ||||
| create table stuff2 ( | ||||
|     weird_pk integer primary key, | ||||
|     label text not null unique, | ||||
|     stuff_id integer references stuff(rowid), | ||||
|     alternative_stuff_id integer references stuff(amount) | ||||
| ); | ||||
							
								
								
									
										28
									
								
								pkg/schema/lint/test_schemas/failure-total.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								pkg/schema/lint/test_schemas/failure-total.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| PRAGMA foreign_keys = on; | ||||
| 
 | ||||
| 
 | ||||
| create table implicit_rowid ( | ||||
|     a integer | ||||
| ); | ||||
| 
 | ||||
| create table explicit_rowid_not_pk ( | ||||
| 	rowid integer | ||||
| ); | ||||
| 
 | ||||
| create table explicit_rowid ( | ||||
| 	rowid integer primary key | ||||
| ); | ||||
| 
 | ||||
| create table without_rowid ( | ||||
| 	a integer primary key | ||||
| ) without rowid; | ||||
| 
 | ||||
| create table multi_column ( | ||||
| 	a int, | ||||
| 	b integer, | ||||
| 	primary key(a, b) | ||||
| ); | ||||
| 
 | ||||
| create table foreign_key_missing_index ( | ||||
| 	a references implicit_rowid(a) | ||||
| ); | ||||
							
								
								
									
										18
									
								
								pkg/schema/lint/test_schemas/success.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								pkg/schema/lint/test_schemas/success.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| create table stuff ( | ||||
|     rowid integer primary key, | ||||
|     data text not null, | ||||
|     amount integer not null | ||||
| ) strict; | ||||
| create index index_stuff_amount on stuff (amount); | ||||
| 
 | ||||
| create table stuff2 ( | ||||
|     weird_pk integer primary key, | ||||
|     label text not null unique, | ||||
|     stuff_id integer references stuff(rowid), | ||||
|     alternative_stuff_id integer references stuff(amount) | ||||
| ) strict; | ||||
| 
 | ||||
| create table stuff3 ( | ||||
|     weird_pk3 integer primary key, | ||||
|     stuff2_id integer not null references stuff2(weird_pk) | ||||
| ) strict; | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user