diff --git a/column.go b/column.go index bbc2831..341c7ff 100644 --- a/column.go +++ b/column.go @@ -8,17 +8,18 @@ import ( "github.com/aodin/sol/types" ) +// Tabular is the interface that all dialects of a SQL column must implement type Columnar interface { + // Two methods for neutral SQL element interfaces: + // (1) Require the interface to return the neutral implementation + // (2) Enumerate all the methods an implmentation would require + // Columnar and Tabular both use method (1) + // Name has been left as a legacy shortcut but may be removed Compiles Selectable - AddOperator(string) ColumnElem // TODO or Columnar? - Alias() string - As(string) Columnar - FullName() string - IsInvalid() bool Name() string - Table() Tabular - Type() types.Type + Column() ColumnElem + Table() *TableElem } // ColumnElem is a dialect neutral implementation of a SQL column @@ -43,16 +44,22 @@ func (col ColumnElem) Alias() string { return col.alias } -// As sets an alias for this ColumnElem -func (col ColumnElem) As(alias string) Columnar { +// As sets an alias and returns a copy of the ColumnElem +func (col ColumnElem) As(alias string) ColumnElem { col.alias = alias return col } +// Column returns a copy of its receiver Column. This method implements +// the Columnar interface. +func (col ColumnElem) Column() ColumnElem { + return col +} + // Columns returns the ColumnElem itself in a slice of ColumnElem. This // method implements the Selectable interface. -func (col ColumnElem) Columns() []Columnar { - return []Columnar{col} +func (col ColumnElem) Columns() []ColumnElem { + return []ColumnElem{col} } // Compile produces the dialect specific SQL and adds any parameters @@ -127,8 +134,7 @@ func (col ColumnElem) Modify(tabular Tabular) error { } // Table returns the column's Table -func (col ColumnElem) Table() Tabular { - // TODO should it return Tabular of *TableElem +func (col ColumnElem) Table() *TableElem { return col.table } diff --git a/columns.go b/columns.go index 3bfccfe..5f13915 100644 --- a/columns.go +++ b/columns.go @@ -7,8 +7,8 @@ import ( // Columns maps column to name to a ColumnElem. It also maintains an order // of columns. type Columns struct { - order []Columnar - c map[string]Columnar + order []ColumnElem + c map[string]ColumnElem } func (columns *Columns) add(column Columnar) error { @@ -21,18 +21,24 @@ func (columns *Columns) add(column Columnar) error { column.Name(), ) } - columns.order = append(columns.order, column) - columns.c[column.Name()] = column + columns.order = append(columns.order, column.Column()) + columns.c[column.Name()] = column.Column() return nil } // All returns all columns as ColumnElems in their default order -func (columns Columns) All() []Columnar { +func (columns Columns) All() []ColumnElem { return columns.order } -func (columns Columns) Get(name string) Columnar { - return columns.c[name] +// Get returns a ColumnElem - or an invalid ColumnElem if a column +// with the given name does not exist in Columns +func (columns Columns) Get(name string) ColumnElem { + col, ok := columns.c[name] + if !ok { + return InvalidColumn(name, nil) + } + return col } // Has returns true if there is a column with the given name in Columns diff --git a/foreignkey.go b/foreignkey.go index 0b184ba..524f222 100644 --- a/foreignkey.go +++ b/foreignkey.go @@ -26,7 +26,7 @@ var _ types.Type = FKElem{} // types.Type interface so it can be used in CREATE TABLE statements. type FKElem struct { name string - col Columnar + col ColumnElem datatype types.Type table *TableElem // the parent table of the key references *TableElem // the table the key references @@ -125,54 +125,40 @@ func (fk FKElem) OnUpdate(b fkAction) FKElem { // ForeignKey creates a FKElem from the given name and column/table. // If given a column, it must already have its table assigned. +// If given a table, it must have one and only one primary key func ForeignKey(name string, fk Selectable, datatypes ...types.Type) FKElem { - var col Columnar - var table *TableElem if fk == nil { log.Panic("sol: inline foreign key was given a nil Selectable") } - columns := fk.Columns() - // TODO use a switch with a fallthrough to fix repeated logic? - if len(columns) == 0 { - log.Panic( - "sol: inline foreign key Selectable must have at least one column", - ) - } else if len(columns) == 1 { - col = columns[0] - if col.Table() == nil || col.Table().Table() == nil { - log.Panic( - "sol: inline foreign key columns must have their table assigned before creation", - ) - } - table = col.Table().Table() - } else { - // Simply use the table of the first column - // TODO This is a strange decision that will error silently - // It also needs to call Table() twice to get the dialect neutral - // table implementation - if columns[0].Table() == nil || columns[0].Table().Table() == nil { - log.Panic( - "sol: inline foreign key columns must have their table assigned before creation", - ) - } - table = columns[0].Table().Table() - pk := table.PrimaryKey() + var col ColumnElem + switch t := fk.(type) { + case Columnar: + col = t.Column() + case Tabular: + // If the given Selectable was a Table - use its primary key + // TODO Can foreign keys reference multiple columns? + pk := t.Table().PrimaryKey() if len(pk) != 1 { log.Panic( - "sol: inline foreign key tables must have one and only one primary key column", + "sol: a table used directly in ForeignKey must have one and only one primary key column", ) } - col = table.C(pk[0]) + col = t.Table().C(pk[0]) + default: + log.Panicf("sol: unknown Selectable %T used in ForeignKey", t) } - if col.IsInvalid() { - log.Panic("sol: referenced column does not exist") + if col.Table() == nil { + log.Panic( + "sol: a column must have a table before being used in ForeignKey", + ) } // Allow an overriding datatype datatype := col.Type() if len(datatypes) > 0 { + // TODO what should happen if multiple datatypes are given? datatype = datatypes[0] } @@ -180,7 +166,7 @@ func ForeignKey(name string, fk Selectable, datatypes ...types.Type) FKElem { name: name, col: col, datatype: datatype, - references: table, + references: col.Table(), } } diff --git a/function.go b/function.go index e2fc53c..982b053 100644 --- a/function.go +++ b/function.go @@ -1,7 +1,7 @@ package sol func Function(name string, col Columnar) ColumnElem { - return col.AddOperator(name) + return col.Column().AddOperator(name) } func Avg(expression Columnar) ColumnElem { diff --git a/insert.go b/insert.go index 2746623..fe3223a 100644 --- a/insert.go +++ b/insert.go @@ -21,7 +21,7 @@ var ( type InsertStmt struct { Stmt table Tabular - columns []Columnar + columns []ColumnElem // TODO Columns args []interface{} fields fields } @@ -343,7 +343,8 @@ func isEmptyValue(v reflect.Value) bool { } // TODO error if there was no match? -func removeColumn(columns []Columnar, name string) []Columnar { +// TODO should be a method of ColumnMap +func removeColumn(columns []ColumnElem, name string) []ColumnElem { for i, col := range columns { if col.Name() == name { return append(columns[:i], columns[i+1:]...) @@ -373,7 +374,7 @@ func valuesMap(stmt InsertStmt, values Values) (fields, error) { // Insert creates an INSERT statement for the given columns. There must be at // least one column and all columns must belong to the same table. func Insert(selections ...Selectable) (stmt InsertStmt) { - var columns []Columnar + var columns []ColumnElem for _, selection := range selections { if selection == nil { stmt.AddMeta("sol: INSERT received a nil selectable - do the columns or tables you selected exist?") diff --git a/postgres/column.go b/postgres/column.go index 4e45b3f..5b2502b 100644 --- a/postgres/column.go +++ b/postgres/column.go @@ -20,10 +20,13 @@ const ( Difference = "-" ) +// ColumnElem is the postgres dialect's implementation of a SQL column type ColumnElem struct { sol.ColumnElem } +var _ sol.Columnar = ColumnElem{} + func (col ColumnElem) operator(op string, param interface{}) sol.BinaryClause { return sol.BinaryClause{ Pre: col, diff --git a/postgres/insert.go b/postgres/insert.go index 2335960..5926694 100644 --- a/postgres/insert.go +++ b/postgres/insert.go @@ -16,7 +16,7 @@ type InsertStmt struct { conflictTargets []string values sol.Values where sol.Clause - returning []sol.Columnar + returning []sol.ColumnElem // TODO ColumnMap } // String outputs the parameter-less INSERT ... RETURNING statement in the diff --git a/postgres/table.go b/postgres/table.go index 099764b..965ae1e 100644 --- a/postgres/table.go +++ b/postgres/table.go @@ -10,6 +10,8 @@ type TableElem struct { *sol.TableElem } +var _ sol.Tabular = &TableElem{} + // Column will return a postgres specific ColumnElem rather than a generic // ColumnElem func (table TableElem) Column(name string) ColumnElem { @@ -22,7 +24,7 @@ func (table TableElem) Column(name string) ColumnElem { } // TODO invalid column? Prevent the mixing of column types? } - return ColumnElem{ColumnElem: sol.InvalidColumn(name, table.TableElem)} + return ColumnElem{ColumnElem: sol.InvalidColumn(name, table)} } // C is an alias for Column diff --git a/select.go b/select.go index 8675d9e..b1c8249 100644 --- a/select.go +++ b/select.go @@ -10,20 +10,20 @@ import ( // Selectable is an interface that allows both tables and columns to be // selected. It is implemented by TableElem and ColumnElem. type Selectable interface { - Columns() []Columnar + Columns() []ColumnElem } // SelectStmt is the internal representation of an SQL SELECT statement. type SelectStmt struct { ConditionalStmt tables []Tabular - columns []Columnar + columns []ColumnElem // TODO Columns joins []JoinClause - groupBy []Columnar + groupBy []ColumnElem // TODO Columns having Clause orderBy []OrderedColumn isDistinct bool - distincts []Columnar + distincts []ColumnElem // TODO Columns limit int offset int } @@ -35,7 +35,7 @@ func (stmt SelectStmt) String() string { } // TODO where should this function live? Also used in postgres.InsertStmt -func CompileColumns(columns []Columnar) []string { +func CompileColumns(columns []ColumnElem) []string { names := make([]string, len(columns)) for i, col := range columns { // Ignore dialect, parameters and error? @@ -163,7 +163,10 @@ func (stmt SelectStmt) All() SelectStmt { // column are provided, the clause will be compiled as a DISTINCT ON. func (stmt SelectStmt) Distinct(columns ...Columnar) SelectStmt { stmt.isDistinct = true - stmt.distincts = columns + // TODO ColumnMap method + for _, column := range columns { + stmt.distincts = append(stmt.distincts, column.Column()) + } return stmt } @@ -228,7 +231,9 @@ func (stmt SelectStmt) Where(conditions ...Clause) SelectStmt { // is allowed per statement. Additional calls to GroupBy will overwrite the // existing GROUP BY clause. func (stmt SelectStmt) GroupBy(columns ...Columnar) SelectStmt { - stmt.groupBy = columns + for _, column := range columns { + stmt.groupBy = append(stmt.groupBy, column.Column()) + } return stmt } @@ -307,7 +312,7 @@ func SelectTable(table Tabular, selects ...Selectable) (stmt SelectStmt) { // Select generates a new SELECT statement from the given columns and tables. func Select(selections ...Selectable) (stmt SelectStmt) { - columns := make([]Columnar, 0) + columns := []ColumnElem{} for _, selection := range selections { if selection == nil { stmt.AddMeta("sol: received a nil selectable in Select()") diff --git a/table.go b/table.go index 93e2a37..2b4b189 100644 --- a/table.go +++ b/table.go @@ -7,15 +7,15 @@ import ( "github.com/aodin/sol/types" ) -// Tabular is the interface that all dialects of SQL table must implement +// Tabular is the interface that all dialects of a SQL table must implement type Tabular interface { + // Two methods for neutral SQL element interfaces: + // (1) Require the interface to return the neutral implementation + // (2) Enumerate all the methods an implmentation would require + // Columnar and Tabular both use method (1) + // Name has been left as a legacy shortcut but may be removed Selectable Name() string - // TODO There are two choices for neutral SQL element interfaces: - // 1. Require the interface to return the neutral implementation - // 2. Enumerate all the methods an implmentation would require - // For now, Tabular uses (1) and Columnar uses (2) - this should - // be standardized Table() *TableElem } @@ -53,7 +53,7 @@ func (table TableElem) C(name string) ColumnElem { } // Columns returns all the table columns in the original schema order -func (table TableElem) Columns() []Columnar { +func (table TableElem) Columns() []ColumnElem { return table.columns.order } @@ -132,7 +132,7 @@ func Table(name string, modifiers ...Modifier) *TableElem { } table := &TableElem{ name: name, - columns: Columns{c: make(map[string]Columnar)}, + columns: Columns{c: make(map[string]ColumnElem)}, // TODO ColumnMap } for _, modifier := range modifiers { if err := modifier.Modify(table); err != nil {