From 238361b066907060d1e3eed30cd563f4273500ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Dulai?= Date: Fri, 5 Jun 2026 15:22:41 +0000 Subject: [PATCH] Refactor parsing & select order by impl (#6) Reviewed-on: https://git.gdulai.com/gdulai/simpleorm/pulls/6 --- cache/cache.go | 2 +- exec/exec.go | 114 ++---------- exec/select.go | 153 ++++++++++++++++ orm.go | 42 +++-- parser/parser.go | 101 ----------- repository/repository.go | 8 +- schema/constraint.go | 25 ++- schema/parser.go | 103 +++++++++++ schema/table.go | 112 +++++++++--- test/exec_test.go | 281 +---------------------------- test/select_test.go | 379 +++++++++++++++++++++++++++++++++++++++ 11 files changed, 792 insertions(+), 528 deletions(-) create mode 100644 exec/select.go delete mode 100644 parser/parser.go create mode 100644 schema/parser.go create mode 100644 test/select_test.go diff --git a/cache/cache.go b/cache/cache.go index e6760d9..1dc77c4 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -25,7 +25,7 @@ func NewSchemaCache(tables []*schema.Table) *SchemaCache { func (o *SchemaCache) add(table *schema.Table) { o.mu.Lock() defer o.mu.Unlock() - o.data[table.Type.Name()] = table + o.data[table.Type().Name()] = table } func (o *SchemaCache) Get(typeName string) (*schema.Table, bool) { diff --git a/exec/exec.go b/exec/exec.go index a0c0d01..6caa9cb 100644 --- a/exec/exec.go +++ b/exec/exec.go @@ -157,100 +157,6 @@ type Exec[T any] interface { execute(conn *simpleorm.DBConnection, tx *sql.Tx) error } -type Select[T any] struct { - target schema.Table - whereStmt string - args []any - results []T - Limit int64 - Offset int64 -} - -func CreateSelect[T any](orm *simpleorm.ORM, whereStmt string, args ...any) (Select[T], error) { - table, ok := orm.Cache().Get(reflect.TypeFor[T]().Name()) - if !ok { - return Select[T]{}, errors.New("Failed to get table from schema cache") - } - - return Select[T]{target: *table, whereStmt: whereStmt, args: args, Limit: -1, Offset: -1}, nil -} - -func (s *Select[T]) Results() []T { - return s.results -} - -func (s *Select[T]) Execute(conn *simpleorm.DBConnection) error { - return s.execute(conn, nil) -} - -func (s *Select[T]) execute(conn *simpleorm.DBConnection, tx *sql.Tx) error { - // Reinit the results, new execution - s.results = []T{} - dml, err := s.target.GetSelectDML() - if err != nil { - return err - } - - if s.whereStmt != "" { - dml += " WHERE " + s.whereStmt - } - - var effectiveArgs []any = s.args - if s.Limit != -1 { - dml += " LIMIT ?" - effectiveArgs = append(effectiveArgs, s.Limit) - } - - if s.Offset != -1 { - dml += " OFFSET ?" - effectiveArgs = append(effectiveArgs, s.Offset) - } - - log.LogDebug("Preparing sql: %s, with args: %s", dml, effectiveArgs) - - var stmt *sql.Stmt - if tx != nil { - stmt, err = tx.Prepare(dml) - } else { - stmt, err = conn.Prepare(dml) - } - - if err != nil { - return err - } - defer stmt.Close() - - log.LogDebug("Executing statement: %s", stmt) - - var rows *sql.Rows - if len(effectiveArgs) == 0 { - rows, err = stmt.Query() - } else { - // Flattent args to make sure it can be parsed correctly - var flatArgs []any - for _, a := range effectiveArgs { - if s, ok := a.([]any); ok { - flatArgs = append(flatArgs, s...) - } else { - flatArgs = append(flatArgs, a) - } - } - rows, err = stmt.Query(flatArgs...) - - } - - if err != nil { - return err - } - - s.results, err = readRows[T](s.target, rows) - if err != nil { - return err - } - - return nil -} - type Count[T any] struct { target schema.Table whereStmt string @@ -514,9 +420,10 @@ func (d *Delete[T]) execute(conn *simpleorm.DBConnection, tx *sql.Tx) error { } func createSelectResultContainer(t schema.Table) []any { - vals := make([]any, t.Type.NumField()) + typ := t.Type() + vals := make([]any, typ.NumField()) for i := range vals { - switch t.Type.Field(i).Type.Kind().String() { + switch typ.Field(i).Type.Kind().String() { case "string": var fieldContainer string vals[i] = &fieldContainer @@ -532,13 +439,14 @@ func createSelectResultContainer(t schema.Table) []any { } func prepareParams(src any, t schema.Table) []any { + typ := t.Type() var params []any - for _, col := range t.Columns { + for _, col := range t.Columns() { _, ok := col.Modifiers["pk"] if ok && t.IsPkAuto() { continue } - field, ok := t.Type.FieldByName(col.FieldName) + field, ok := typ.FieldByName(col.FieldName) if !ok { continue } @@ -550,11 +458,12 @@ func prepareParams(src any, t schema.Table) []any { } func getPk(src any, t schema.Table) ([]any, error) { + typ := t.Type() var values []any - for _, constraint := range t.Constraints { + for _, constraint := range t.Constraints() { if constraint.Type == "pk" { for _, col := range constraint.Columns { - field, ok := t.Type.FieldByName(col.FieldName) + field, ok := typ.FieldByName(col.FieldName) if !ok { continue @@ -582,10 +491,11 @@ func readRows[T any](table schema.Table, rows *sql.Rows) ([]T, error) { return nil, err } - targetType := table.Type + targetType := table.Type() + cols := table.Columns() parsedResult := reflect.New(targetType) for i, fieldVal := range rowContainer { - col := table.Columns[i] + col := cols[i] targetField := parsedResult.Elem().Field(i) rawValue := reflect.Indirect(reflect.ValueOf(fieldVal)) diff --git a/exec/select.go b/exec/select.go new file mode 100644 index 0000000..d4dc374 --- /dev/null +++ b/exec/select.go @@ -0,0 +1,153 @@ +package exec + +import ( + "database/sql" + "errors" + "reflect" + + "git.gdulai.com/gdulai/simpleorm" + "git.gdulai.com/gdulai/simpleorm/schema" + log "gitlab.com/gdulai/simpleloglvl" +) + +// Select execution which is translated to a select sql query. +type Select[T any] struct { + target schema.Table + whereStmt string + args []any + limit int64 + offset int64 + ordering []schema.OrderBy + results []T +} + +// Creates the select query builder +func CreateSelect[T any](orm *simpleorm.ORM) (Select[T], error) { + table, ok := orm.Cache().Get(reflect.TypeFor[T]().Name()) + if !ok { + return Select[T]{}, errors.New("Failed to get table from schema cache") + } + + return Select[T]{target: *table, limit: -1, offset: -1}, nil +} + +// Sets the where part of the select query and the arguments +// Returns the pointer of the Select instance +func (s *Select[T]) Where(whereStmt string, args ...any) *Select[T] { + s.whereStmt = whereStmt + s.args = args + return s +} + +// Sets the limit part of the select query +// Returns the pointer of the Select instance +func (s *Select[T]) Limit(limit int64) *Select[T] { + s.limit = limit + return s +} + +// Sets the offset part of the select query +// Returns the pointer of the Select instance +func (s *Select[T]) Offset(offset int64) *Select[T] { + s.offset = offset + return s +} + +// Sets the order by part of the select query +// Returns the pointer of the Select instance +func (s *Select[T]) OrderBy(ordering ...schema.OrderBy) *Select[T] { + s.ordering = ordering + return s +} + +// Returns the result of the select +func (s *Select[T]) Results() []T { + return s.results +} + +// Executes the select query based on the Select exec instance +// Returns an error if theres any +func (s *Select[T]) Execute(conn *simpleorm.DBConnection) error { + return s.execute(conn, nil) +} + +// Executes the select query based on the Select exec instance +// If tx is given, the transaction is used instead of the conn +// Returns an error if theres any +func (s *Select[T]) execute(conn *simpleorm.DBConnection, tx *sql.Tx) error { + // Reinit the results, new execution + s.results = []T{} + dml, err := s.target.GetSelectDML() + if err != nil { + return err + } + + if s.whereStmt != "" { + dml += " WHERE " + s.whereStmt + } + + var effectiveArgs []any = s.args + if s.limit != -1 { + dml += " LIMIT ?" + effectiveArgs = append(effectiveArgs, s.limit) + } + + if s.offset != -1 { + dml += " OFFSET ?" + effectiveArgs = append(effectiveArgs, s.offset) + } + + if len(s.ordering) > 0 { + orderBy, err := s.target.GetOrderByDML(s.ordering...) + if err != nil { + log.LogError("Failed to create ORDER BY part: %s", err) + } else { + dml += orderBy + } + } + + log.LogInfo("Preparing sql: %s", dml) + log.LogDebug("Preparing sql: %s, with args: %s", dml, effectiveArgs) + + var stmt *sql.Stmt + if tx != nil { + stmt, err = tx.Prepare(dml) + } else { + stmt, err = conn.Prepare(dml) + } + + if err != nil { + return err + } + defer stmt.Close() + + log.LogDebug("Executing statement: %s", stmt) + + var rows *sql.Rows + if len(effectiveArgs) == 0 { + rows, err = stmt.Query() + } else { + // Flattent args to make sure it can be parsed correctly + var flatArgs []any + for _, a := range effectiveArgs { + if s, ok := a.([]any); ok { + flatArgs = append(flatArgs, s...) + } else { + flatArgs = append(flatArgs, a) + } + } + rows, err = stmt.Query(flatArgs...) + + } + + if err != nil { + return err + } + + s.results, err = readRows[T](s.target, rows) + if err != nil { + return err + } + + return nil +} diff --git a/orm.go b/orm.go index ab127f5..ebc66c8 100644 --- a/orm.go +++ b/orm.go @@ -7,7 +7,6 @@ import ( "strings" "git.gdulai.com/gdulai/simpleorm/cache" - "git.gdulai.com/gdulai/simpleorm/parser" "git.gdulai.com/gdulai/simpleorm/schema" log "gitlab.com/gdulai/simpleloglvl" @@ -21,28 +20,39 @@ type ORM struct { // Inits the ORM library. // Param objs is an array which should be an array of the types which describe the tables. func NewORM(objs ...any) *ORM { - var parsers []*parser.Parser - var tables []*schema.Table + typeParsers := make(map[string]*schema.Parser) + + log.LogDebug("[ORM] Parsing entities to tables...") + log.LogDebug("[ORM] Step 1: Parsing table columns") for _, obj := range objs { - log.LogDebug("[ORM] Mapping type for: %s", reflect.TypeOf(obj).Name()) - parser := parser.NewParser(obj) - parsers = append(parsers, parser) - tables = append(tables, parser.ParseColumns()) + typ := reflect.TypeOf(obj) + log.LogDebug("[ORM] Mapping type for: %s", typ.Name()) + + parser := schema.NewParser(obj) + parser.ParseColumns() + + typeParsers[typ.Name()] = parser } - log.LogDebug("[ORM] Tables initiated, creating cache.") + log.LogDebug("[ORM] Step 1: Finished parsing columns!") + log.LogDebug("[ORM] Step 2: Parsing constraints...") + + for _, parser := range typeParsers { + parser.ParseConstraints(typeParsers) + } + + log.LogDebug("[ORM] Step 2: Finished parsing contraints!") + log.LogDebug("[ORM] Step 3: Creating and caching schema...") + + var tables []*schema.Table + for _, parser := range typeParsers { + tables = append(tables, parser.ParseTable()) + } // Create cache with the initialized tables cache := cache.NewSchemaCache(tables) - log.LogDebug("[ORM] Cache created.") - - // Finish the parsing with the constraints and add the to the tables - for _, parser := range parsers { - log.LogDebug("[ORM] Parsing constraing for: %s", parser.Table.Name) - parser.ParseConstraints(cache) - log.LogDebug("[ORM] Parsed constraints for: %s", parser.Table.Name) - } + log.LogDebug("[ORM] Step 3: Cache created.") return &ORM{cache: cache} } diff --git a/parser/parser.go b/parser/parser.go deleted file mode 100644 index 7d8b15e..0000000 --- a/parser/parser.go +++ /dev/null @@ -1,101 +0,0 @@ -package parser - -import ( - "reflect" - "strings" - - cache "git.gdulai.com/gdulai/simpleorm/cache" - "git.gdulai.com/gdulai/simpleorm/schema" - "git.gdulai.com/gdulai/simpleorm/util" - log "gitlab.com/gdulai/simpleloglvl" -) - -type Parser struct { - typ reflect.Type - Table *schema.Table -} - -func NewParser[T any](obj T) *Parser { - objType := reflect.TypeOf(obj) - return &Parser{typ: objType} -} - -// This is step 1 of the parsing, it creates the table instance and -func (p *Parser) ParseColumns() *schema.Table { - table := schema.Table{Name: util.CamelToSnake(p.typ.Name()), Type: p.typ} - - var columns []schema.Column - for field := range p.typ.Fields() { - field := field - col := schema.NewColumn(util.CamelToSnake((field.Name)), field.Name, determineType(field.Type), field.Tag) - columns = append(columns, col) - } - - table.Columns = columns - p.Table = &table - return p.Table -} - -func (p *Parser) ParseConstraints(cache *cache.SchemaCache) { - pkConstraint := schema.Constraint{Name: "pk_" + strings.ToLower(p.Table.Name), Type: "pk"} - fkConstraints := make(map[string]schema.Constraint) - for _, col := range p.Table.Columns { - _, ok := col.Modifiers["pk"] - if ok { - pkConstraint.Columns = append(pkConstraint.Columns, col) - continue - } - - fkMod, ok := col.Modifiers["fk"] - if !ok { - continue - } - - fkModParts := strings.Split(fkMod, ".") - refTable, ok := cache.Get(fkModParts[0]) - if !ok { - log.LogError("[ORM] Table %s not found in OrmCache!", fkModParts[0]) - return - } - - fkId, ok := col.Modifiers["fk_id"] - if fkId == "" { - fkId = "fk_" + strings.ToLower(refTable.Name) - } - - fkConstraint, ok := fkConstraints[fkId] - if !ok { - fkConstraint = schema.Constraint{Name: fkId, Type: "fk", RefTable: refTable} - } - - fkConstraint.Columns = append(fkConstraint.Columns, col) - - refField := fkModParts[1] - for _, refC := range refTable.Columns { - if refC.FieldName == refField { - fkConstraint.RefColumns = append(fkConstraint.RefColumns, refC) - } - } - - fkConstraints[fkId] = fkConstraint - } - p.Table.Constraints = append(p.Table.Constraints, pkConstraint) - for _, fkConstraint := range fkConstraints { - p.Table.Constraints = append(p.Table.Constraints, fkConstraint) - } - -} - -func determineType(typ reflect.Type) string { - typStr := typ.String() - switch typStr { - case "string": - return "TEXT" - case "int", "bool": - return "INTEGER" - case "time.Time", "int64": - return "BIGINT" - } - - return "VARCHAR(255)" -} diff --git a/repository/repository.go b/repository/repository.go index f14a08b..04763a7 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -23,7 +23,7 @@ func NewRepository[T HasPK](conn *simpleorm.DBConnection, orm *simpleorm.ORM) *R } func (r *Repository[T]) SelectAll() []*T { - selectExec, err := exec.CreateSelect[T](r.orm, "") + selectExec, err := exec.CreateSelect[T](r.orm) if err != nil { log.LogError("Failed to create select execution: %s", err) return []*T{} @@ -53,7 +53,7 @@ func (r *Repository[T]) SelectByPk(pks ...any) *T { var whereStmtBuilder strings.Builder - for _, constr := range table.Constraints { + for _, constr := range table.Constraints() { if constr.Type != "pk" { continue } @@ -68,12 +68,14 @@ func (r *Repository[T]) SelectByPk(pks ...any) *T { } - selectExec, err := exec.CreateSelect[T](r.orm, whereStmtBuilder.String(), pks) + selectExec, err := exec.CreateSelect[T](r.orm) if err != nil { log.LogError("Failed to create select execution: %s", err) return nil } + selectExec.Where(whereStmtBuilder.String(), pks) + err = selectExec.Execute(r.conn) if err != nil { log.LogError("Failed to load entity: %s", err) diff --git a/schema/constraint.go b/schema/constraint.go index aafd939..58ecf9e 100644 --- a/schema/constraint.go +++ b/schema/constraint.go @@ -3,14 +3,16 @@ package schema import ( "errors" "strings" + + "git.gdulai.com/gdulai/simpleorm/util" ) type Constraint struct { - Name string - Type string - Columns []Column - RefTable *Table - RefColumns []Column + Name string + Type string + Columns []Column + RefTypeName string + RefColumns []Column } func (c *Constraint) GetDDL() (string, error) { @@ -26,7 +28,9 @@ func (c *Constraint) GetDDL() (string, error) { func (c *Constraint) getPkDDL() string { var ddl strings.Builder - ddl.WriteString("CONSTRAINT " + c.Name + " PRIMARY KEY(") + ddl.WriteString("CONSTRAINT ") + ddl.WriteString(c.Name) + ddl.WriteString(" PRIMARY KEY(") for i, col := range c.Columns { if i != 0 { ddl.WriteString(", ") @@ -39,7 +43,9 @@ func (c *Constraint) getPkDDL() string { func (c *Constraint) getFkDDL() string { var ddl strings.Builder - ddl.WriteString("CONSTRAINT " + c.Name + " FOREIGN KEY(") + ddl.WriteString("CONSTRAINT ") + ddl.WriteString(c.Name) + ddl.WriteString(" FOREIGN KEY(") for i, col := range c.Columns { if i != 0 { @@ -47,7 +53,10 @@ func (c *Constraint) getFkDDL() string { } ddl.WriteString(col.Name) } - ddl.WriteString(") REFERENCES " + c.RefTable.Name + "(") + + ddl.WriteString(") REFERENCES ") + ddl.WriteString(util.CamelToSnake(c.RefTypeName)) + ddl.WriteString("(") for i, col := range c.RefColumns { if i != 0 { diff --git a/schema/parser.go b/schema/parser.go new file mode 100644 index 0000000..d261ae5 --- /dev/null +++ b/schema/parser.go @@ -0,0 +1,103 @@ +package schema + +import ( + "reflect" + "strings" + + "git.gdulai.com/gdulai/simpleorm/util" + log "gitlab.com/gdulai/simpleloglvl" +) + +type Parser struct { + typ reflect.Type + columns []Column + constraints []Constraint +} + +func NewParser[T any](obj T) *Parser { + objType := reflect.TypeOf(obj) + return &Parser{typ: objType} +} + +// Step 1 of the parsing, it creates the table instance and +func (p *Parser) ParseColumns() { + var columns []Column + for field := range p.typ.Fields() { + field := field + col := NewColumn(util.CamelToSnake((field.Name)), field.Name, determineType(field.Type), field.Tag) + columns = append(columns, col) + } + p.columns = columns +} + +// Step 2 of the parsing, it creates the constrains with the table references +func (p *Parser) ParseConstraints(tableParsers map[string]*Parser) { + pkConstraint := Constraint{Name: "pk_" + strings.ToLower(util.CamelToSnake((p.typ.Name()))), Type: "pk"} + fkConstraints := make(map[string]Constraint) + for _, col := range p.columns { + _, ok := col.Modifiers["pk"] + if ok { + pkConstraint.Columns = append(pkConstraint.Columns, col) + continue + } + + fkMod, ok := col.Modifiers["fk"] + if !ok { + continue + } + + fkModParts := strings.Split(fkMod, ".") + refTypeName := fkModParts[0] + refTableName := util.CamelToSnake(refTypeName) + parser, ok := tableParsers[refTypeName] + if !ok { + log.LogError("[ORM] Reference table not found", refTypeName) + } + + fkId, ok := col.Modifiers["fk_id"] + if fkId == "" { + fkId = "fk_" + strings.ToLower(refTableName) + } + + fkConstraint, ok := fkConstraints[fkId] + if !ok { + fkConstraint = Constraint{Name: fkId, Type: "fk", RefTypeName: refTypeName} + } + + fkConstraint.Columns = append(fkConstraint.Columns, col) + + refField := fkModParts[1] + for _, refC := range parser.columns { + if refC.FieldName == refField { + fkConstraint.RefColumns = append(fkConstraint.RefColumns, refC) + } + } + + fkConstraints[fkId] = fkConstraint + } + + p.constraints = append(p.constraints, pkConstraint) + for _, fkConstraint := range fkConstraints { + p.constraints = append(p.constraints, fkConstraint) + } +} + +// Step 3 of the paring, create the schema.Table instance +// Returns the schema.Table pointer +func (p Parser) ParseTable() *Table { + return &Table{name: util.CamelToSnake(p.typ.Name()), typ: p.typ, columns: p.columns, constraints: p.constraints} +} + +func determineType(typ reflect.Type) string { + typStr := typ.String() + switch typStr { + case "string": + return "TEXT" + case "int", "bool": + return "INTEGER" + case "time.Time", "int64": + return "BIGINT" + } + + return "VARCHAR(255)" +} diff --git a/schema/table.go b/schema/table.go index f16cf4b..4534155 100644 --- a/schema/table.go +++ b/schema/table.go @@ -1,23 +1,47 @@ package schema import ( + "errors" "reflect" "strings" "git.gdulai.com/gdulai/simpleorm/util" ) +type OrderBy struct { + Field string + Direction string +} + type Table struct { - Name string - Type reflect.Type - Columns []Column - Constraints []Constraint + name string + typ reflect.Type + columns []Column + constraints []Constraint +} + +func (t Table) Name() string { + return t.name +} + +func (t Table) Type() reflect.Type { + return t.typ +} + +func (t Table) Columns() []Column { + return t.columns +} + +func (t Table) Constraints() []Constraint { + return t.constraints } func (t Table) GetDDL() (string, error) { var ddl strings.Builder - ddl.WriteString("CREATE TABLE IF NOT EXISTS " + t.Name + " (") - for i, col := range t.Columns { + ddl.WriteString("CREATE TABLE IF NOT EXISTS ") + ddl.WriteString(t.name) + ddl.WriteString(" (") + for i, col := range t.columns { if i != 0 { ddl.WriteString(", ") } @@ -27,12 +51,13 @@ func (t Table) GetDDL() (string, error) { } ddl.WriteString(colDdl) } - for _, constr := range t.Constraints { + for _, constr := range t.constraints { constrDdl, err := constr.GetDDL() if err != nil { return "", err } - ddl.WriteString(", " + constrDdl) + ddl.WriteString(", ") + ddl.WriteString(constrDdl) } ddl.WriteString(");") @@ -43,7 +68,7 @@ func (t Table) GetSelectDML() (string, error) { var dml strings.Builder dml.WriteString("SELECT ") - for i, col := range t.Columns { + for i, col := range t.columns { if i != 0 { dml.WriteString(", ") } @@ -54,21 +79,24 @@ func (t Table) GetSelectDML() (string, error) { dml.WriteString(colDml) } - dml.WriteString(" FROM " + t.Name) + dml.WriteString(" FROM ") + dml.WriteString(t.name) return dml.String(), nil } func (t Table) GetCountDML() string { - return "SELECT COUNT(*) FROM " + t.Name + return "SELECT COUNT(*) FROM " + t.name } func (t Table) GetInsertDML(count int) (string, error) { var dml strings.Builder - dml.WriteString("INSERT INTO " + util.CamelToSnake(t.Type.Name()) + " (") + dml.WriteString("INSERT INTO ") + dml.WriteString(util.CamelToSnake(t.typ.Name())) + dml.WriteString(" (") - columnsLen := len(t.Columns) + columnsLen := len(t.columns) effectiveColumnsLen := 0 - for i, col := range t.Columns { + for i, col := range t.columns { _, ok := col.Modifiers["pk"] if ok && t.IsPkAuto() { continue @@ -111,12 +139,14 @@ func (t Table) GetInsertDML(count int) (string, error) { func (t Table) GetUpdateDML() (string, error) { var dml strings.Builder - dml.WriteString("UPDATE " + util.CamelToSnake(t.Type.Name()) + " SET ") + dml.WriteString("UPDATE ") + dml.WriteString(util.CamelToSnake(t.typ.Name())) + dml.WriteString(" SET ") - colLen := len(t.Columns) + colLen := len(t.columns) var pkColumns []string // Colum names - for i, col := range t.Columns { + for i, col := range t.columns { modifiers := col.Modifiers _, ok := modifiers["pk"] @@ -133,9 +163,11 @@ func (t Table) GetUpdateDML() (string, error) { } if i != colLen-1 { - dml.WriteString(colDml + ", ") + dml.WriteString(colDml) + dml.WriteString(", ") } else { - dml.WriteString(colDml + " ") + dml.WriteString(colDml) + dml.WriteString(" ") } } @@ -145,7 +177,8 @@ func (t Table) GetUpdateDML() (string, error) { if i != 0 { dml.WriteString(" AND ") } - dml.WriteString(pkCol + " = ?") + dml.WriteString(pkCol) + dml.WriteString(" = ?") } return dml.String(), nil @@ -153,17 +186,21 @@ func (t Table) GetUpdateDML() (string, error) { func (t Table) GetDeleteDML(count int) (string, error) { var dml strings.Builder - dml.WriteString("DELETE FROM " + t.Name + " WHERE ") + dml.WriteString("DELETE FROM ") + dml.WriteString(t.name) + dml.WriteString(" WHERE ") - for _, col := range t.Columns { + for _, col := range t.columns { modifiers := col.Modifiers _, ok := modifiers["pk"] if ok { if count > 1 { - dml.WriteString(col.Name + " IN (") + dml.WriteString(col.Name) + dml.WriteString(" IN (") } else { - dml.WriteString(col.Name + " = ") + dml.WriteString(col.Name) + dml.WriteString(" = ") } } } @@ -182,8 +219,33 @@ func (t Table) GetDeleteDML(count int) (string, error) { return dml.String(), nil } +func (t Table) GetOrderByDML(ordering ...OrderBy) (string, error) { + var dml strings.Builder + dml.WriteString(" ORDER BY") + for _, orderBy := range ordering { + col, err := t.getColumnByField(orderBy.Field) + if err != nil { + return "", err + } + dml.WriteString(" ") + dml.WriteString(col.Name) + dml.WriteString(" ") + dml.WriteString(orderBy.Direction) + } + return dml.String(), nil +} + +func (t Table) getColumnByField(fieldName string) (Column, error) { + for _, col := range t.columns { + if col.FieldName == fieldName { + return col, nil + } + } + return Column{}, errors.New("No column for field " + fieldName + " in table " + t.name) +} + func (t Table) IsPkAuto() bool { - for _, constr := range t.Constraints { + for _, constr := range t.constraints { if constr.Type != "pk" { continue } diff --git a/test/exec_test.go b/test/exec_test.go index 8892190..b56f645 100644 --- a/test/exec_test.go +++ b/test/exec_test.go @@ -9,36 +9,6 @@ import ( log "gitlab.com/gdulai/simpleloglvl" ) -func TestSelectEmpty(t *testing.T) { - // GIVEN - orm, conn := testSetup() - defer cleanUp("test.db", conn) - - // WHEN - selectExec, err := exec.CreateSelect[Test](orm, "") - - // THEN - if err != nil { - log.LogError("Failed to create select. %s", err) - t.Fail() - return - } - - err = selectExec.Execute(conn) - if err != nil { - log.LogError("Select failure. %s", err) - t.Fail() - return - } - - res := selectExec.Results() - if len(res) > 0 { - log.LogError("Expected empty result.") - t.Fail() - return - } -} - func TestInsert(t *testing.T) { // GIVEN orm, conn := testSetup() @@ -133,193 +103,6 @@ func TestInsertMultiple(t *testing.T) { } -func TestInsertAndSelectSingle(t *testing.T) { - // GIVEN - orm, conn := testSetup() - defer cleanUp("test.db", conn) - - testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} - - // WHEN - insertExec, err := exec.NewInsert(orm, testObj) - if err != nil { - log.LogError("TestInsertAndSelectSingle setup failed: %s", err) - t.Fail() - return - } - - selectExec, err := exec.CreateSelect[Test](orm, "") - if err != nil { - log.LogError("TestInsertAndSelectSingle setup failed: %s", err) - t.Fail() - return - } - - insertErr := insertExec.Execute(conn) - selectErr := selectExec.Execute(conn) - - // THEN - if insertErr != nil { - log.LogError("TestInsertAndSelectSingle insert failed: %s", err) - t.Fail() - return - } - - if selectErr != nil { - log.LogError("TestInsertAndSelectSingle failure. %s", err) - t.Fail() - return - } - - insertRes := insertExec.Results() - if len(insertRes) != 1 { - log.LogError("TestInsertAndSelectSingle test failed. Expected: 1, Actual: %s", len(insertRes)) - t.Fail() - return - } - - selectRes := selectExec.Results() - if len(selectRes) != 1 { - log.LogError("TestInsertAndSelectSingle test failed. Expected: 1, Actual: %s", len(selectRes)) - t.Fail() - return - } - - singleRes := insertRes[0] - if singleRes.ID != 1 || singleRes.Int64Field != 54 || singleRes.IntField != 12 || singleRes.StringField != "asdf" { - log.LogError("TestInsertAndSelectSingle invalid result. Expected: %s, Actual: %s", testObj, singleRes) - t.Fail() - } - - singleRes = selectRes[0] - if singleRes.ID != 1 || singleRes.Int64Field != 54 || singleRes.IntField != 12 || singleRes.StringField != "asdf" { - log.LogError("TestInsertAndSelectSingle invalid result. Expected: %s, Actual: %s", testObj, singleRes) - t.Fail() - } - -} - -func TestInsertSelectWithCompositePk(t *testing.T) { - // GIVEN - orm, conn := testSetup() - defer cleanUp("test.db", conn) - - testObj := TestWithCompositePk{ID: 12, Name: "Test"} - - // WHEN - insertExec, err := exec.NewInsert(orm, testObj) - if err != nil { - log.LogError("TestInsertSelectWithCompositePk setup failed: %s", err) - t.Fail() - return - } - - selectExec, err := exec.CreateSelect[TestWithCompositePk](orm, "") - if err != nil { - log.LogError("TestInsertSelectWithCompositePk setup failed: %s", err) - t.Fail() - return - } - - err = insertExec.Execute(conn) - if err != nil { - log.LogError("TestInsertSelectWithCompositePk insert failed: %s", err) - t.Fail() - return - } - - err = selectExec.Execute(conn) - - // THEN - if err != nil { - log.LogError("TestInsertSelectWithCompositePk failure. %s", err) - t.Fail() - return - } - - res := selectExec.Results() - if len(res) != 1 { - log.LogError("TestInsertSelectWithCompositePk test failed. Expected: 1, Actual: %s", len(res)) - t.Fail() - return - } - - singleRes := res[0] - if singleRes.ID != 12 || singleRes.Name != "Test" { - log.LogError("TestInsertSelectWithCompositePk invalid result. Expected: %s, Actual: %s", testObj, singleRes) - t.Fail() - } - -} - -func TestInsertMultipleAndSelectWithParam(t *testing.T) { - // GIVEN - orm, conn := testSetup() - defer cleanUp("test.db", conn) - - // WHEN - testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} - testObjSecond := Test{Int64Field: 12, IntField: 54, StringField: "fdsa"} - - insertExec, err := exec.NewInsert(orm, testObj) - if err != nil { - log.LogError("TestInsertMultipleAndSelectWithParam setup failed: %s", err) - t.Fail() - return - } - - insertSecond, err := exec.NewInsert(orm, testObjSecond) - if err != nil { - log.LogError("TestInsertMultipleAndSelectWithParam setup failed: %s", err) - t.Fail() - return - } - - selectExec, err := exec.CreateSelect[Test](orm, "string_field = ?", "fdsa") - if err != nil { - log.LogError("TestInsertMultipleAndSelectWithParam setup failed: %s", err) - t.Fail() - return - } - - err = insertExec.Execute(conn) - if err != nil { - log.LogError("TestInsertMultipleAndSelectWithParam first insert failed: %s", err) - t.Fail() - return - } - - err = insertSecond.Execute(conn) - if err != nil { - log.LogError("TestInsertMultipleAndSelectWithParam second insert failed: %s", err) - t.Fail() - return - } - - err = selectExec.Execute(conn) - - // THEN - if err != nil { - log.LogError("TestInsertMultipleAndSelectWithParam failure. %s", err) - t.Fail() - return - } - - res := selectExec.Results() - if len(res) != 1 { - log.LogError(" TestInsertMultipleAndSelectWithParam test failed. Expected: 1, Actual: %s", len(res)) - t.Fail() - return - } - - singleRes := res[0] - if singleRes.ID != 2 || singleRes.Int64Field != 12 || singleRes.IntField != 54 || singleRes.StringField != "fdsa" { - log.LogError("TestInsertMultipleAndSelectWithParam invalid result. Expected: %s, Actual: %s", testObjSecond, singleRes) - t.Fail() - return - } -} - func TestInsertAndUpdate(t *testing.T) { // GIVEN orm, conn := testSetup() @@ -335,13 +118,15 @@ func TestInsertAndUpdate(t *testing.T) { return } - selectExec, err := exec.CreateSelect[Test](orm, "string_field = ?", "asdf") + selectExec, err := exec.CreateSelect[Test](orm) if err != nil { log.LogError("TestInsertMultipleAndSelectWithParam setup failed: %s", err) t.Fail() return } + selectExec.Where("string_field = ?", "asdf") + err = insertExec.Execute(conn) if err != nil { log.LogError("TestInsertAndUpdate insert failed: %s", err) @@ -422,7 +207,7 @@ func TestInsertAndDelete(t *testing.T) { return } - selectExec, err := exec.CreateSelect[Test](orm, "") + selectExec, err := exec.CreateSelect[Test](orm) if err != nil { log.LogError("TestInsertAndDelete setup failed: %s", err) t.Fail() @@ -478,7 +263,7 @@ func TestBoolInsertAndSelectSingle(t *testing.T) { return } - selectExec, err := exec.CreateSelect[TestWithBool](orm, "") + selectExec, err := exec.CreateSelect[TestWithBool](orm) if err != nil { log.LogError("TestBoolInsertAndSelectSingle setup failed: %s", err) t.Fail() @@ -532,7 +317,7 @@ func TestTimeInsertAndSelectSingle(t *testing.T) { return } - selectExec, err := exec.CreateSelect[TestWithTime](orm, "") + selectExec, err := exec.CreateSelect[TestWithTime](orm) if err != nil { log.LogError("TestTimeInsertAndSelectSingle setup failed: %s", err) t.Fail() @@ -638,13 +423,15 @@ func TestTransactionRollback(t *testing.T) { return } - selectExec, err := exec.CreateSelect[Test](orm, "id = ?", insertExec.Results()[0].ID) + selectExec, err := exec.CreateSelect[Test](orm) if err != nil { log.LogError("TestTransactionRollback WHEN failed: %s", err) t.Fail() return } + selectExec.Where("id = ?", insertExec.Results()[0].ID) + transErr = transaction.Execute(&selectExec) if transErr != nil { log.LogError("TestTransactionRollback WHEN failed: %s", err) @@ -683,56 +470,6 @@ func TestTransactionRollback(t *testing.T) { } } -func TestSelectWithLimitAndOffset(t *testing.T) { - // GIVEN - orm, conn := testSetup() - defer cleanUp("test.db", conn) - - testEntites := []Test{} - - for i := 0; i < 1000; i++ { - testEntites = append(testEntites, Test{Int64Field: -1, IntField: i, StringField: "Entity " + strconv.Itoa(i)}) - } - - insertExec, err := exec.NewInsert[Test](orm, testEntites...) - if err != nil { - log.LogError("TestSelectWithLimitAndOffset setup failed: %s", err) - t.Fail() - return - } - - err = insertExec.Execute(conn) - if err != nil { - log.LogError("TestSelectWithLimitAndOffset setup failed: %s", err) - t.Fail() - return - } - // WHEN - - selectExec, err := exec.CreateSelect[Test](orm, "") - if err != nil { - log.LogError("TestSelectWithLimitAndOffset failed: %s", err) - t.Fail() - return - } - selectExec.Limit = 100 - selectExec.Offset = 0 - - err = selectExec.Execute(conn) - if err != nil { - log.LogError("TestSelectWithLimitAndOffset failed: %s", err) - t.Fail() - return - } - - // THEN - - if len(selectExec.Results()) != 100 { - log.LogError("TestSelectWithLimitAndOffset expected result size 100, actual: %s", len(selectExec.Results())) - t.Fail() - } -} - func TestCount(t *testing.T) { // GIVEN orm, conn := testSetup() diff --git a/test/select_test.go b/test/select_test.go new file mode 100644 index 0000000..858bcac --- /dev/null +++ b/test/select_test.go @@ -0,0 +1,379 @@ +package simpleorm_test + +import ( + "strconv" + "testing" + + simpleorm "git.gdulai.com/gdulai/simpleorm" + "git.gdulai.com/gdulai/simpleorm/exec" + "git.gdulai.com/gdulai/simpleorm/schema" + log "gitlab.com/gdulai/simpleloglvl" +) + +func TestSelectEmpty(t *testing.T) { + // GIVEN + orm, conn := testSetup() + defer cleanUp("test.db", conn) + + // WHEN + selectExec, err := exec.CreateSelect[Test](orm) + + // THEN + if err != nil { + log.LogError("Failed to create select. %s", err) + t.Fail() + return + } + + err = selectExec.Execute(conn) + if err != nil { + log.LogError("Select failure. %s", err) + t.Fail() + return + } + + res := selectExec.Results() + if len(res) > 0 { + log.LogError("Expected empty result.") + t.Fail() + return + } +} + +func TestSelectSingle(t *testing.T) { + // GIVEN + orm, conn := testSetup() + defer cleanUp("test.db", conn) + + testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + + // WHEN + insertExec, err := exec.NewInsert(orm, testObj) + if err != nil { + log.LogError("TestInsertAndSelectSingle setup failed: %s", err) + t.Fail() + return + } + + selectExec, err := exec.CreateSelect[Test](orm) + if err != nil { + log.LogError("TestInsertAndSelectSingle setup failed: %s", err) + t.Fail() + return + } + + insertErr := insertExec.Execute(conn) + selectErr := selectExec.Execute(conn) + + // THEN + if insertErr != nil { + log.LogError("TestInsertAndSelectSingle insert failed: %s", err) + t.Fail() + return + } + + if selectErr != nil { + log.LogError("TestInsertAndSelectSingle failure. %s", err) + t.Fail() + return + } + + insertRes := insertExec.Results() + if len(insertRes) != 1 { + log.LogError("TestInsertAndSelectSingle test failed. Expected: 1, Actual: %s", len(insertRes)) + t.Fail() + return + } + + selectRes := selectExec.Results() + if len(selectRes) != 1 { + log.LogError("TestInsertAndSelectSingle test failed. Expected: 1, Actual: %s", len(selectRes)) + t.Fail() + return + } + + singleRes := insertRes[0] + if singleRes.ID != 1 || singleRes.Int64Field != 54 || singleRes.IntField != 12 || singleRes.StringField != "asdf" { + log.LogError("TestInsertAndSelectSingle invalid result. Expected: %s, Actual: %s", testObj, singleRes) + t.Fail() + } + + singleRes = selectRes[0] + if singleRes.ID != 1 || singleRes.Int64Field != 54 || singleRes.IntField != 12 || singleRes.StringField != "asdf" { + log.LogError("TestInsertAndSelectSingle invalid result. Expected: %s, Actual: %s", testObj, singleRes) + t.Fail() + } + +} + +func TestSelectWithCompositePk(t *testing.T) { + // GIVEN + orm, conn := testSetup() + defer cleanUp("test.db", conn) + + testObj := TestWithCompositePk{ID: 12, Name: "Test"} + + // WHEN + insertExec, err := exec.NewInsert(orm, testObj) + if err != nil { + log.LogError("TestInsertSelectWithCompositePk setup failed: %s", err) + t.Fail() + return + } + + selectExec, err := exec.CreateSelect[TestWithCompositePk](orm) + if err != nil { + log.LogError("TestInsertSelectWithCompositePk setup failed: %s", err) + t.Fail() + return + } + + err = insertExec.Execute(conn) + if err != nil { + log.LogError("TestInsertSelectWithCompositePk insert failed: %s", err) + t.Fail() + return + } + + err = selectExec.Execute(conn) + + // THEN + if err != nil { + log.LogError("TestInsertSelectWithCompositePk failure. %s", err) + t.Fail() + return + } + + res := selectExec.Results() + if len(res) != 1 { + log.LogError("TestInsertSelectWithCompositePk test failed. Expected: 1, Actual: %s", len(res)) + t.Fail() + return + } + + singleRes := res[0] + if singleRes.ID != 12 || singleRes.Name != "Test" { + log.LogError("TestInsertSelectWithCompositePk invalid result. Expected: %s, Actual: %s", testObj, singleRes) + t.Fail() + } + +} + +func TestSelectWithParam(t *testing.T) { + // GIVEN + orm, conn := testSetup() + defer cleanUp("test.db", conn) + + // WHEN + testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + testObjSecond := Test{Int64Field: 12, IntField: 54, StringField: "fdsa"} + + insertExec, err := exec.NewInsert(orm, testObj) + if err != nil { + log.LogError("TestInsertMultipleAndSelectWithParam setup failed: %s", err) + t.Fail() + return + } + + insertSecond, err := exec.NewInsert(orm, testObjSecond) + if err != nil { + log.LogError("TestInsertMultipleAndSelectWithParam setup failed: %s", err) + t.Fail() + return + } + + selectExec, err := exec.CreateSelect[Test](orm) + if err != nil { + log.LogError("TestInsertMultipleAndSelectWithParam setup failed: %s", err) + t.Fail() + return + } + + selectExec.Where("string_field = ?", "fdsa") + + err = insertExec.Execute(conn) + if err != nil { + log.LogError("TestInsertMultipleAndSelectWithParam first insert failed: %s", err) + t.Fail() + return + } + + err = insertSecond.Execute(conn) + if err != nil { + log.LogError("TestInsertMultipleAndSelectWithParam second insert failed: %s", err) + t.Fail() + return + } + + err = selectExec.Execute(conn) + + // THEN + if err != nil { + log.LogError("TestInsertMultipleAndSelectWithParam failure. %s", err) + t.Fail() + return + } + + res := selectExec.Results() + if len(res) != 1 { + log.LogError(" TestInsertMultipleAndSelectWithParam test failed. Expected: 1, Actual: %s", len(res)) + t.Fail() + return + } + + singleRes := res[0] + if singleRes.ID != 2 || singleRes.Int64Field != 12 || singleRes.IntField != 54 || singleRes.StringField != "fdsa" { + log.LogError("TestInsertMultipleAndSelectWithParam invalid result. Expected: %s, Actual: %s", testObjSecond, singleRes) + t.Fail() + return + } +} + +func TestSelectWithLimitAndOffset(t *testing.T) { + // GIVEN + orm, conn := testSetup() + defer cleanUp("test.db", conn) + + testEntites := []Test{} + + for i := 0; i < 1000; i++ { + testEntites = append(testEntites, Test{Int64Field: -1, IntField: i, StringField: "Entity " + strconv.Itoa(i)}) + } + + insertExec, err := exec.NewInsert[Test](orm, testEntites...) + if err != nil { + log.LogError("TestSelectWithLimitAndOffset setup failed: %s", err) + t.Fail() + return + } + + err = insertExec.Execute(conn) + if err != nil { + log.LogError("TestSelectWithLimitAndOffset setup failed: %s", err) + t.Fail() + return + } + // WHEN + + selectExec, err := exec.CreateSelect[Test](orm) + if err != nil { + log.LogError("TestSelectWithLimitAndOffset failed: %s", err) + t.Fail() + return + } + selectExec.Limit(100).Offset(0) + + err = selectExec.Execute(conn) + if err != nil { + log.LogError("TestSelectWithLimitAndOffset failed: %s", err) + t.Fail() + return + } + + // THEN + + if len(selectExec.Results()) != 100 { + log.LogError("TestSelectWithLimitAndOffset expected result size 100, actual: %s", len(selectExec.Results())) + t.Fail() + } +} + +func TestSelectOrderByDesc(t *testing.T) { + // GIVEN + orm, conn := testSetup() + defer cleanUp("test.db", conn) + + err := orderBySetup(conn, orm) + if err != nil { + log.LogError("TestSelectOrderByAsc setup failed: %s", err) + t.Fail() + return + } + + // WHEN + selectExec, err := exec.CreateSelect[Test](orm) + if err != nil { + log.LogError("TestSelectOrderByDesc failed: %s", err) + t.Fail() + return + } + selectExec.OrderBy(schema.OrderBy{Field: "StringField", Direction: "DESC"}) + + err = selectExec.Execute(conn) + if err != nil { + log.LogError("TestSelectOrderByDesc failed: %s", err) + t.Fail() + return + } + + // THEN + var resultStr string + for _, obj := range selectExec.Results() { + resultStr += obj.StringField + } + + if resultStr != "EDCBA" { + log.LogError("TestSelectOrderByDesc failed, epxected: EDCBA actual: %s", resultStr) + t.Fail() + } +} + +func TestSelectOrderByAsc(t *testing.T) { + // GIVEN + orm, conn := testSetup() + defer cleanUp("test.db", conn) + + err := orderBySetup(conn, orm) + if err != nil { + log.LogError("TestSelectOrderByAsc setup failed: %s", err) + t.Fail() + return + } + + // WHEN + selectExec, err := exec.CreateSelect[Test](orm) + if err != nil { + log.LogError("TestSelectOrderByAsc failed: %s", err) + t.Fail() + return + } + selectExec.OrderBy(schema.OrderBy{Field: "StringField", Direction: "ASC"}) + + err = selectExec.Execute(conn) + if err != nil { + log.LogError("TestSelectOrderByAsc failed: %s", err) + t.Fail() + return + } + + // THEN + var resultStr string + for _, obj := range selectExec.Results() { + resultStr += obj.StringField + } + + if resultStr != "ABCDE" { + log.LogError("TestSelectOrderByAsc failed, epxected: ABCDE actual: %s", resultStr) + t.Fail() + } +} + +func orderBySetup(conn *simpleorm.DBConnection, orm *simpleorm.ORM) error { + insertExec, err := exec.NewInsert[Test](orm, + Test{Int64Field: -1, IntField: 1, StringField: "C"}, + Test{Int64Field: -1, IntField: 1, StringField: "D"}, + Test{Int64Field: -1, IntField: 1, StringField: "E"}, + Test{Int64Field: -1, IntField: 1, StringField: "A"}, + Test{Int64Field: -1, IntField: 1, StringField: "B"}, + ) + if err != nil { + return err + } + + err = insertExec.Execute(conn) + if err != nil { + return err + } + + return nil +}