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