Initial commit: create table, column and schemaparse
Some checks failed
CI / release-test (push) Failing after 4s

This commit is contained in:
wispem-wantex 2025-07-04 19:42:30 -07:00
commit 654585256c
5 changed files with 276 additions and 0 deletions

21
.gitea/workflows/CI.yaml Normal file
View 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 ./...

View 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
}

View 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
View 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

View 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
-- );