Initial commit: create table, column and schemaparse
Some checks failed
CI / release-test (push) Failing after 4s
Some checks failed
CI / release-test (push) Failing after 4s
This commit is contained in:
commit
654585256c
21
.gitea/workflows/CI.yaml
Normal file
21
.gitea/workflows/CI.yaml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-test:
|
||||||
|
container:
|
||||||
|
image: offline-twitter/go
|
||||||
|
volumes:
|
||||||
|
- woodpecker-gocache:/go-cache-volume
|
||||||
|
env:
|
||||||
|
GOPATH: /go-cache-volume
|
||||||
|
GOCACHE: /go-cache-volume/build-cache
|
||||||
|
steps:
|
||||||
|
- name: checkout
|
||||||
|
run: |
|
||||||
|
GOBIN=/usr/local/go/bin go install git.offline-twitter.com/offline-labs/gocheckout@0.0.1
|
||||||
|
gocheckout
|
||||||
|
- name: test
|
||||||
|
run: |
|
||||||
|
go test ./...
|
66
pkg/sqlgenerate/schema_parse.go
Normal file
66
pkg/sqlgenerate/schema_parse.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package sqlgenerate
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jinzhu/inflection"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitDB(sql_schema string) *sqlx.DB {
|
||||||
|
db := sqlx.MustOpen("sqlite3", ":memory:")
|
||||||
|
db.MustExec(sql_schema)
|
||||||
|
db.MustExec(`
|
||||||
|
create temporary view tables as
|
||||||
|
select l.schema, l.name, l.type, l.ncol, l.wr, l.strict
|
||||||
|
from sqlite_schema s
|
||||||
|
left join pragma_table_list l on s.name = l.name
|
||||||
|
where s.type = 'table';
|
||||||
|
|
||||||
|
create temporary view columns as
|
||||||
|
select tables.name as table_name,
|
||||||
|
table_info.name as column_name,
|
||||||
|
lower(table_info.type) as column_type,
|
||||||
|
"notnull",
|
||||||
|
dflt_value is not null as has_default_value,
|
||||||
|
ifnull(dflt_value, 0) dflt_value,
|
||||||
|
pk as is_primary_key,
|
||||||
|
fk."table" is not null as is_foreign_key,
|
||||||
|
ifnull(fk."table", '') as fk_target_table,
|
||||||
|
ifnull(fk."to", '') as fk_target_column
|
||||||
|
from tables
|
||||||
|
join pragma_table_info(tables.name) as table_info
|
||||||
|
left join pragma_foreign_key_list(tables.name) as fk on fk."from" = column_name;
|
||||||
|
`)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
// SchemaFromDB takes a DB connection, checks its schema metadata tables, and returns a Schema.
|
||||||
|
func SchemaFromDB(db *sqlx.DB) Schema {
|
||||||
|
return ParseSchema(db)
|
||||||
|
}
|
||||||
|
func ParseSchema(db *sqlx.DB) Schema {
|
||||||
|
ret := Schema{}
|
||||||
|
|
||||||
|
var table_list []string
|
||||||
|
err := db.Select(&table_list, `select name from tables`)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table_name := range table_list {
|
||||||
|
tbl := Table{TableName: table_name}
|
||||||
|
tbl.TypeName = snakeToCamel(inflection.Singular(table_name))
|
||||||
|
tbl.TypeIDName = tbl.TypeName + "ID"
|
||||||
|
tbl.VarName = strings.ToLower(string(table_name[0]))
|
||||||
|
|
||||||
|
err := db.Select(&tbl.Columns, `select * from columns where table_name = ?`, table_name)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
ret[tbl.TableName] = tbl
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
50
pkg/sqlgenerate/schema_parse_test.go
Normal file
50
pkg/sqlgenerate/schema_parse_test.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package sqlgenerate_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"gas_stack/pkg/sqlgenerate"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseSchema(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
schema_sql, err := os.ReadFile("test_schemas/food.sql")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
db := sqlgenerate.InitDB(string(schema_sql))
|
||||||
|
schema := sqlgenerate.ParseSchema(db)
|
||||||
|
expected_tbls := []string{"food_types", "foods", "units", "ingredients", "recipes", "iterations", "db_version"}
|
||||||
|
for _, tbl_name := range expected_tbls {
|
||||||
|
_, is_ok := schema[tbl_name]
|
||||||
|
assert.True(is_ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
foods := schema["foods"]
|
||||||
|
assert.Len(foods.Columns, 20)
|
||||||
|
assert.Equal(foods.Columns[0].Name, "rowid")
|
||||||
|
assert.Equal(foods.Columns[0].Type, "integer")
|
||||||
|
assert.Equal(foods.Columns[0].IsNotNull, false) // Explicit not-null
|
||||||
|
assert.Equal(foods.Columns[0].IsPrimaryKey, true)
|
||||||
|
assert.Equal(foods.Columns[0].IsForeignKey, false)
|
||||||
|
assert.Equal(foods.Columns[1].Name, "name")
|
||||||
|
assert.Equal(foods.Columns[1].Type, "text")
|
||||||
|
assert.Equal(foods.Columns[1].IsNotNull, true)
|
||||||
|
assert.Equal(foods.Columns[1].HasDefaultValue, false)
|
||||||
|
assert.Equal(foods.Columns[1].IsPrimaryKey, false)
|
||||||
|
assert.Equal(foods.Columns[16].Name, "mass")
|
||||||
|
assert.Equal(foods.Columns[16].Type, "real")
|
||||||
|
assert.Equal(foods.Columns[16].HasDefaultValue, true)
|
||||||
|
assert.Equal(foods.Columns[16].DefaultValue, "100")
|
||||||
|
|
||||||
|
ingredients := schema["ingredients"]
|
||||||
|
assert.Equal(ingredients.Columns[0].Name, "rowid")
|
||||||
|
assert.Equal(ingredients.Columns[0].IsPrimaryKey, true)
|
||||||
|
assert.Equal(ingredients.Columns[1].Name, "food_id")
|
||||||
|
assert.Equal(ingredients.Columns[1].IsForeignKey, true)
|
||||||
|
assert.Equal(ingredients.Columns[1].ForeignKeyTargetTable, "foods")
|
||||||
|
assert.Equal(ingredients.Columns[1].ForeignKeyTargetColumn, "rowid")
|
||||||
|
}
|
40
pkg/sqlgenerate/table.go
Normal file
40
pkg/sqlgenerate/table.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package sqlgenerate
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Column represents a single column in a table.
|
||||||
|
type Column struct {
|
||||||
|
TableName string `db:"table_name"`
|
||||||
|
Name string `db:"column_name"`
|
||||||
|
Type string `db:"column_type"`
|
||||||
|
IsNotNull bool `db:"notnull"`
|
||||||
|
HasDefaultValue bool `db:"has_default_value"`
|
||||||
|
DefaultValue string `db:"dflt_value"`
|
||||||
|
IsPrimaryKey bool `db:"is_primary_key"`
|
||||||
|
IsForeignKey bool `db:"is_foreign_key"`
|
||||||
|
ForeignKeyTargetTable string `db:"fk_target_table"`
|
||||||
|
ForeignKeyTargetColumn string `db:"fk_target_column"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNullableForeignKey is a helper function.
|
||||||
|
func (c Column) IsNullableForeignKey() bool {
|
||||||
|
return !c.IsNotNull && !c.IsPrimaryKey && c.IsForeignKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table is a single SQLite table.
|
||||||
|
type Table struct {
|
||||||
|
TableName string `db:"name"`
|
||||||
|
IsStrict bool `db:"strict"`
|
||||||
|
Columns []Column
|
||||||
|
|
||||||
|
TypeIDName string
|
||||||
|
VarName string
|
||||||
|
TypeName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schema is a container for a bunch of Tables, indexed by table name.
|
||||||
|
type Schema map[string]Table
|
99
pkg/sqlgenerate/test_schemas/food.sql
Normal file
99
pkg/sqlgenerate/test_schemas/food.sql
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
PRAGMA foreign_keys = on;
|
||||||
|
|
||||||
|
-- =======
|
||||||
|
-- DB meta
|
||||||
|
-- =======
|
||||||
|
|
||||||
|
create table db_version (
|
||||||
|
version integer primary key
|
||||||
|
) strict;
|
||||||
|
insert into db_version values(0);
|
||||||
|
|
||||||
|
create table food_types (rowid integer primary key,
|
||||||
|
name text not null unique check(length(name) != 0)
|
||||||
|
) strict;
|
||||||
|
insert into food_types (name) values
|
||||||
|
('grocery'),
|
||||||
|
('recipe'),
|
||||||
|
('daily log');
|
||||||
|
|
||||||
|
create table foods (rowid integer primary key,
|
||||||
|
name text not null check(length(name) != 0),
|
||||||
|
|
||||||
|
cals real not null,
|
||||||
|
carbs real not null,
|
||||||
|
protein real not null,
|
||||||
|
fat real not null,
|
||||||
|
|
||||||
|
sugar real not null,
|
||||||
|
alcohol real not null default 0,
|
||||||
|
water real not null default 0,
|
||||||
|
|
||||||
|
potassium real not null default 0,
|
||||||
|
sodium real not null default 0,
|
||||||
|
calcium real not null default 0,
|
||||||
|
magnesium real not null default 0,
|
||||||
|
phosphorus real not null default 0,
|
||||||
|
iron real not null default 0,
|
||||||
|
zinc real not null default 0,
|
||||||
|
|
||||||
|
mass real not null default 100,
|
||||||
|
price real not null default 0,
|
||||||
|
density real not null default 1,
|
||||||
|
cook_ratio real not null default 1
|
||||||
|
) strict;
|
||||||
|
|
||||||
|
|
||||||
|
create table units (rowid integer primary key,
|
||||||
|
name text not null unique check(length(name) != 0),
|
||||||
|
abbreviation text not null unique check(length(abbreviation) != 0)
|
||||||
|
-- is_metric integer not null check(is_metric in (0, 1))
|
||||||
|
) strict;
|
||||||
|
insert into units(rowid, name, abbreviation) values
|
||||||
|
-- Count
|
||||||
|
(1, 'count', 'ct'),
|
||||||
|
-- Mass
|
||||||
|
(2, 'grams', 'g'),
|
||||||
|
(3, 'pounds', 'lbs'),
|
||||||
|
(4, 'ounces', 'oz'),
|
||||||
|
-- Volume
|
||||||
|
(5, 'milliliters', 'mL'),
|
||||||
|
(6, 'cups', 'cups'),
|
||||||
|
(7, 'teaspoons', 'tsp'),
|
||||||
|
(8, 'tablespoons', 'tbsp'),
|
||||||
|
(9, 'fluid ounces', 'fl-oz');
|
||||||
|
|
||||||
|
|
||||||
|
create table ingredients (rowid integer primary key,
|
||||||
|
food_id integer references foods(rowid),
|
||||||
|
recipe_id integer references recipes(rowid),
|
||||||
|
|
||||||
|
quantity real not null default 1,
|
||||||
|
units integer not null default 0, -- Display purposes only
|
||||||
|
|
||||||
|
in_recipe_id integer references recipes(rowid) on delete cascade not null,
|
||||||
|
list_order integer not null,
|
||||||
|
is_hidden integer not null default false,
|
||||||
|
unique (in_recipe_id, list_order)
|
||||||
|
check((food_id is null) + (recipe_id is null) = 1) -- Exactly one should be active
|
||||||
|
) strict;
|
||||||
|
|
||||||
|
create table recipes (rowid integer primary key,
|
||||||
|
name text not null check(length(name) != 0),
|
||||||
|
blurb text not null,
|
||||||
|
instructions text not null,
|
||||||
|
|
||||||
|
computed_food_id integer references foods(rowid) not null
|
||||||
|
) strict;
|
||||||
|
|
||||||
|
create table iterations (rowid integer primary key,
|
||||||
|
original_recipe_id integer references recipes(rowid),
|
||||||
|
-- original_author integer not null, -- For azimuth integration
|
||||||
|
derived_recipe_id integer references recipes(rowid),
|
||||||
|
unique(derived_recipe_id)
|
||||||
|
) strict;
|
||||||
|
|
||||||
|
-- create table daily_logs (
|
||||||
|
-- date integer not null unique,
|
||||||
|
-- computed_food_id integer references foods(rowid) not null
|
||||||
|
-- );
|
Loading…
x
Reference in New Issue
Block a user