sqlite_lint: add support for FTS5 and virtual tables
Some checks failed
CI / build-docker (push) Successful in 5s
CI / build-docker-bootstrap (push) Has been skipped
CI / release-test (push) Failing after 6s

This commit is contained in:
wispem-wantex 2026-01-17 09:00:18 +09:00
parent d70cbc1913
commit 0371fb4144
11 changed files with 68 additions and 36 deletions

3
ops/compile.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/sh
go build -tags fts5 -o gas ./cmd

View File

@ -13,7 +13,8 @@ cd "$(dirname "${BASH_SOURCE[0]}")/.."
# Compile `gas`
gas="/tmp/gas"
go build -o $gas ./cmd
ops/compile.sh
mv gas $gas
test_project="/memory/test_gasproj"
if [[ -e $test_project ]]; then

View File

@ -13,7 +13,8 @@ cd "$(dirname "${BASH_SOURCE[0]}")/.."
# Compile `gas`
gas="/tmp/gas"
go build -o $gas ./cmd
ops/compile.sh
mv gas $gas
test_schema_dir="pkg/schema/lint/test_schemas"

View File

@ -76,7 +76,7 @@ func GenerateModelAST(table schema.Table) *ast.GenDecl {
return &ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{&ast.TypeSpec{
Name: ast.NewIdent(table.TypeName),
Name: ast.NewIdent(table.GoTypeName),
Type: &ast.StructType{Fields: &ast.FieldList{List: fields}},
}},
}
@ -171,7 +171,7 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
Op: token.NEQ,
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
},
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("fmt.Errorf"), Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"got %s with ID (%%d), so attempted update, but it doesn't exist\"", strings.ToLower(tbl.TypeName))}, &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}}}}}}}},
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("fmt.Errorf"), Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"got %s with ID (%%d), so attempted update, but it doesn't exist\"", strings.ToLower(tbl.GoTypeName))}, &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}}}}}}}},
},
},
},
@ -181,9 +181,9 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
funcDecl := &ast.FuncDecl{
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}},
Name: ast.NewIdent("Save" + tbl.TypeName),
Name: ast.NewIdent("Save" + tbl.GoTypeName),
Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: &ast.StarExpr{X: ast.NewIdent(tbl.TypeName)}}}},
Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: &ast.StarExpr{X: ast.NewIdent(tbl.GoTypeName)}}}},
Results: nil,
},
Body: funcBody,
@ -194,11 +194,11 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
// GenerateGetItemByIDFunc produces an AST for the `GetXyzByID()` function.
// E.g., a table with `table.TypeName = "foods"` will produce a "GetFoodByID()" function.
func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
funcName := "Get" + tbl.TypeName + "ByID"
funcName := "Get" + tbl.GoTypeName + "ByID"
recv := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}
arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("id")}, Type: ast.NewIdent(tbl.TypeIDName)}}}
result := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: ast.NewIdent(tbl.TypeName)}, {Names: []*ast.Ident{ast.NewIdent("err")}, Type: ast.NewIdent("error")}}}
result := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: ast.NewIdent(tbl.GoTypeName)}, {Names: []*ast.Ident{ast.NewIdent("err")}, Type: ast.NewIdent("error")}}}
// Use the xyzSQLFields constant in the select query
selectExpr := &ast.BinaryExpr{
@ -220,7 +220,7 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
},
&ast.IfStmt{
Cond: &ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")}, Args: []ast.Expr{ast.NewIdent("err"), &ast.SelectorExpr{X: ast.NewIdent("sql"), Sel: ast.NewIdent("ErrNoRows")}}},
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ReturnStmt{Results: []ast.Expr{&ast.CompositeLit{Type: ast.NewIdent(tbl.TypeName)}, ast.NewIdent("ErrNotInDB")}}}},
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ReturnStmt{Results: []ast.Expr{&ast.CompositeLit{Type: ast.NewIdent(tbl.GoTypeName)}, ast.NewIdent("ErrNotInDB")}}}},
},
&ast.ReturnStmt{},
},
@ -238,12 +238,12 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
// GenerateGetAllItemsFunc produces an AST for the `GetAllXyzs()` function.
// E.g., a table with `table.TypeName = "foods"` will produce a "GetAllFoods()" function.
func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
funcName := "GetAll" + inflection.Plural(tbl.TypeName)
funcName := "GetAll" + inflection.Plural(tbl.GoTypeName)
recv := &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")},
}}
result := &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: &ast.ArrayType{Elt: ast.NewIdent(tbl.TypeName)}},
{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: &ast.ArrayType{Elt: ast.NewIdent(tbl.GoTypeName)}},
}}
selectCall := &ast.CallExpr{
@ -291,9 +291,9 @@ func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
// GenerateDeleteItemFunc produces an AST for the `DeleteXyz()` function.
// E.g., a table with `table.TypeName = "foods"` will produce a "DeleteFood()" function.
func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
funcName := "Delete" + tbl.TypeName
funcName := "Delete" + tbl.GoTypeName
recv := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}
arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: ast.NewIdent(tbl.TypeName)}}}
arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: ast.NewIdent(tbl.GoTypeName)}}}
funcBody := &ast.BlockStmt{
List: []ast.Stmt{
@ -328,7 +328,7 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
Args: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("fmt.Errorf"),
Args: []ast.Expr{
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"tried to delete %s with ID (%%d) but it doesn't exist\"", strings.ToLower(tbl.TypeName))},
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"tried to delete %s with ID (%%d) but it doesn't exist\"", strings.ToLower(tbl.GoTypeName))},
&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
},
}},
@ -372,5 +372,5 @@ func GenerateSQLFieldsConst(tbl schema.Table) *ast.GenDecl {
// ---------------
func SQLFieldsConstIdent(tbl schema.Table) *ast.Ident {
return ast.NewIdent(strings.ToLower(tbl.TypeName) + "SQLFields")
return ast.NewIdent(strings.ToLower(tbl.GoTypeName) + "SQLFields")
}

View File

@ -90,7 +90,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
description2 := `"a big item"`
testCreateUpdateDelete := &ast.FuncDecl{
Name: ast.NewIdent("TestCreateUpdateDelete" + tbl.TypeName),
Name: ast.NewIdent("TestCreateUpdateDelete" + tbl.GoTypeName),
Type: &ast.FuncType{
Params: &ast.FieldList{
List: []*ast.Field{{
@ -106,7 +106,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
Lhs: []ast.Expr{testObj},
Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CompositeLit{
Type: ast.NewIdent(tbl.TypeName),
Type: ast.NewIdent(tbl.GoTypeName),
Elts: []ast.Expr{
&ast.KeyValueExpr{
Key: fieldName,
@ -118,7 +118,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
// TestDB.SaveItem(&item)
&ast.ExprStmt{X: &ast.CallExpr{
Fun: ast.NewIdent("TestDB.Save" + tbl.TypeName),
Fun: ast.NewIdent("TestDB.Save" + tbl.GoTypeName),
Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}},
}},
@ -135,7 +135,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
Rhs: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("Must"),
Args: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("TestDB.Get" + tbl.TypeName + "ByID"),
Fun: ast.NewIdent("TestDB.Get" + tbl.GoTypeName + "ByID"),
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
}},
}},
@ -160,7 +160,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
// TestDB.SaveItem(&item)
&ast.ExprStmt{X: &ast.CallExpr{
Fun: ast.NewIdent("TestDB.Save" + tbl.TypeName),
Fun: ast.NewIdent("TestDB.Save" + tbl.GoTypeName),
Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}},
}},
@ -171,7 +171,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
Rhs: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("Must"),
Args: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("TestDB.Get" + tbl.TypeName + "ByID"),
Fun: ast.NewIdent("TestDB.Get" + tbl.GoTypeName + "ByID"),
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
}},
}},
@ -189,7 +189,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
// TestDB.DeleteItem(item)
&ast.ExprStmt{X: &ast.CallExpr{
Fun: ast.NewIdent("TestDB.Delete" + tbl.TypeName),
Fun: ast.NewIdent("TestDB.Delete" + tbl.GoTypeName),
Args: []ast.Expr{testObj},
}},
@ -198,7 +198,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
Lhs: []ast.Expr{ast.NewIdent("_"), ast.NewIdent("err")},
Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("TestDB.Get" + tbl.TypeName + "ByID"),
Fun: ast.NewIdent("TestDB.Get" + tbl.GoTypeName + "ByID"),
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
}},
},
@ -217,7 +217,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
}
testGetAll := &ast.FuncDecl{
Name: ast.NewIdent("TestGetAll" + inflection.Plural(tbl.TypeName)),
Name: ast.NewIdent("TestGetAll" + inflection.Plural(tbl.GoTypeName)),
Type: &ast.FuncType{Params: &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("t")}, Type: &ast.StarExpr{X: ast.NewIdent("testing.T")}},
}}, Results: nil},
@ -229,7 +229,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
Rhs: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{
X: ast.NewIdent("TestDB"),
Sel: ast.NewIdent("GetAll" + inflection.Plural(tbl.TypeName)),
Sel: ast.NewIdent("GetAll" + inflection.Plural(tbl.GoTypeName)),
},
}},
},

View File

@ -23,7 +23,10 @@ var Checks = []Check{
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 tablename, tbl := range s.Tables {
if tbl.TableType != "table" {
continue
}
for _, column := range s.Tables[tablename].Columns {
if !column.IsNotNull && !column.IsForeignKey && !column.IsPrimaryKey {
ret = append(ret, CheckResult{
@ -43,7 +46,10 @@ var Checks = []Check{
"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 {
for tablename, tbl := range s.Tables {
if tbl.TableType != "table" {
continue
}
if !s.Tables[tablename].IsStrict {
ret = append(ret, CheckResult{
ErrorMsg: "Table should be marked \"strict\"",
@ -78,7 +84,10 @@ var Checks = []Check{
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 tablename, tbl := range s.Tables {
if tbl.TableType != "table" {
continue
}
for _, column := range s.Tables[tablename].Columns {
if column.IsPrimaryKey {
continue tableloop
@ -161,6 +170,9 @@ var Checks = []Check{
Execute: func(s schema.Schema) (ret []CheckResult) {
tbl_loop:
for tblName, tbl := range s.Tables {
if tbl.TableType != "table" {
continue
}
if tbl.IsWithoutRowid {
continue
}

View File

@ -5,6 +5,14 @@ create table stuff (
) strict;
create index index_stuff_amount on stuff (amount);
create virtual table stuff_fts using fts5(
data,
content='stuff',
content_rowid='rowid',
tokenize='trigram'
);
create table stuff2 (
weird_pk integer primary key,
label text not null unique,

View File

@ -38,10 +38,10 @@ func SchemaFromDB(db *sqlx.DB) Schema {
ret := Schema{Tables: map[string]Table{}, Indexes: map[string]Index{}}
var tables []Table
PanicIf(db.Select(&tables, `select name, is_strict, is_without_rowid from tables`))
PanicIf(db.Select(&tables, `select name, table_type, is_strict, is_without_rowid from tables`))
for _, tbl := range tables {
tbl.TypeName = TypenameFromTablename(tbl.TableName)
tbl.TypeIDName = tbl.TypeName + "ID"
tbl.GoTypeName = TypenameFromTablename(tbl.TableName)
tbl.TypeIDName = tbl.GoTypeName + "ID"
tbl.VarName = strings.ToLower(string(tbl.TableName[0]))
PanicIf(db.Select(&tbl.Columns, `select * from columns where table_name = ?`, tbl.TableName))

View File

@ -25,7 +25,7 @@ func TestParseSchema(t *testing.T) {
foods := schema.Tables["foods"]
assert.Equal(foods.TableName, "foods")
assert.Equal(foods.TypeName, "Food")
assert.Equal(foods.GoTypeName, "Food")
assert.Equal(foods.TypeIDName, "FoodID")
assert.Equal(foods.IsStrict, true)
assert.Len(foods.Columns, 20)

View File

@ -22,15 +22,22 @@ func (c Column) IsNullableForeignKey() bool {
// Table is a single SQLite table.
type Table struct {
TableName string `db:"name"`
TableName string `db:"name"`
// One of "table", "view", "shadow", or "virtual"
TableType string `db:"table_type"`
IsStrict bool `db:"is_strict"`
IsWithoutRowid bool `db:"is_without_rowid"`
Columns []Column
TypeIDName string
VarName string
TypeName string
// Default variable name for variables of this type to use when generating Go code
VarName string
// Name of corresponding model type to be generated
GoTypeName string
}
type Index struct {

View File

@ -1,7 +1,7 @@
create temporary view tables as
select l.schema,
l.name,
l.type,
l.type as table_type,
l.wr as is_without_rowid,
l.strict as is_strict
from sqlite_schema s