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