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