package schema import ( "sort" "strings" "git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils" ) // 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"` PrimaryKeyRank uint `db:"primary_key_rank"` 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 } func (c Column) IsNonCodeTableForeignKey() bool { return c.IsForeignKey && strings.HasSuffix(c.Name, "_id") } func (c Column) GoFieldName() string { if c.Name == "rowid" { return "ID" } if c.IsNonCodeTableForeignKey() { return textutils.SnakeToCamel(strings.TrimSuffix(c.Name, "_id")) + "ID" } return textutils.SnakeToCamel(c.Name) } // GoVarName returns the name of a local variable for this column, e.g., when used as a function parameter. func (c Column) GoVarName() string { if c.Name == "rowid" { return strings.ToLower(c.TableName)[0:1] + "ID" // TODO: Or should it just be "id"?? } // For foreign keys, use first letter of the target type and "ID". "UserID" => "uID" if c.IsNonCodeTableForeignKey() { return strings.ToLower(c.ForeignKeyTargetTable)[0:1] + "ID" } // Otherwise, just lowercase the field name fieldname := c.GoFieldName() return strings.ToLower(fieldname)[0:1] + fieldname[1:] } func (c Column) GoTypeName() string { if c.IsNonCodeTableForeignKey() { return TypenameFromTablename(c.ForeignKeyTargetTable) + "ID" } switch c.Type { case "integer", "int": if strings.HasPrefix(c.Name, "is_") || strings.HasPrefix(c.Name, "has_") { return "bool" } else if strings.HasSuffix(c.Name, "_at") { return "Timestamp" } return "int" case "text": return "string" case "real": return "float32" case "blob": return "[]byte" default: panic("Unrecognized sqlite column type: " + c.Type) } } // Table is a single SQLite table. type Table struct { 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 // 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 } // PrimaryKeyColumns returns the ordered list of columns in this table's primary key. // This can be useful for "without rowid" tables with composite primary keys. // // TODO: needs test func (t Table) PrimaryKeyColumns() []Column { pks := make([]Column, 0) for _, c := range t.Columns { if c.IsPrimaryKey { pks = append(pks, c) } } sort.Slice(pks, func(i, j int) bool { return pks[i].PrimaryKeyRank < pks[j].PrimaryKeyRank }) return pks } func (t Table) GetColumnByName(name string) Column { for _, c := range t.Columns { if c.Name == name { return c } } panic("no such column: " + name) } func (t Table) HasAutoTimestamps() (hasCreatedAt bool, hasUpdatedAt bool) { for _, c := range t.Columns { if c.Name == "created_at" && c.Type == "integer" { hasCreatedAt = true } if c.Name == "updated_at" && c.Type == "integer" { hasUpdatedAt = true } } return } type Index struct { Name string `db:"index_name"` TableName string `db:"table_name"` Columns []string IsUnique bool `db:"is_unique"` // TODO: `where ...` for partial indexes // TODO: identify columns that are expressions } type Schema struct { Tables map[string]Table Indexes map[string]Index }