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 0d81d67..6caa9cb 100644 --- a/exec/exec.go +++ b/exec/exec.go @@ -157,11 +157,6 @@ type Exec[T any] interface { execute(conn *simpleorm.DBConnection, tx *sql.Tx) error } -type OrderBy struct { - Column string - Direction string -} - type Count[T any] struct { target schema.Table whereStmt string @@ -425,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 @@ -443,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 } @@ -461,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 @@ -493,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 index bb46f10..d36f979 100644 --- a/exec/select.go +++ b/exec/select.go @@ -17,7 +17,7 @@ type Select[T any] struct { args []any limit int64 offset int64 - ordering []OrderBy + ordering []schema.OrderBy results []T } @@ -55,7 +55,7 @@ func (s *Select[T]) Offset(offset int64) *Select[T] { // Sets the order by part of the select query // Returns the pointer of the Select instance -func (s *Select[T]) OrderBy(ordering ...OrderBy) *Select[T] { +func (s *Select[T]) OrderBy(ordering ...schema.OrderBy) *Select[T] { s.ordering = ordering return s } @@ -65,10 +65,15 @@ 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{} @@ -92,6 +97,15 @@ func (s *Select[T]) execute(conn *simpleorm.DBConnection, tx *sql.Tx) error { effectiveArgs = append(effectiveArgs, s.offset) } + if len(s.ordering) > 0 { + dml += " ORDER BY" + for _, orderBy := range s.ordering { + + dml += " " + orderBy.Field + " " + orderBy.Direction + } + } + + log.LogInfo("Preparing sql: %s", dml) log.LogDebug("Preparing sql: %s, with args: %s", dml, effectiveArgs) var stmt *sql.Stmt 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 6349e02..04763a7 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -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 } 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..ad4a539 100644 --- a/schema/table.go +++ b/schema/table.go @@ -7,17 +7,40 @@ import ( "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 +50,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 +67,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 +78,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 +138,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 +162,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 +176,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 +185,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(" = ") } } } @@ -183,7 +219,7 @@ func (t Table) GetDeleteDML(count int) (string, error) { } func (t Table) IsPkAuto() bool { - for _, constr := range t.Constraints { + for _, constr := range t.constraints { if constr.Type != "pk" { continue } diff --git a/test/select_test.go b/test/select_test.go index ce12c34..922b743 100644 --- a/test/select_test.go +++ b/test/select_test.go @@ -5,6 +5,7 @@ import ( "testing" "git.gdulai.com/gdulai/simpleorm/exec" + "git.gdulai.com/gdulai/simpleorm/schema" log "gitlab.com/gdulai/simpleloglvl" ) @@ -281,22 +282,22 @@ func TestSelectOrderBy(t *testing.T) { orm, conn := testSetup() defer cleanUp("test.db", conn) - testEntites := []Test{} + testA := Test{Int64Field: -1, IntField: 1, StringField: "A"} + testB := Test{Int64Field: -1, IntField: 1, StringField: "B"} + testC := Test{Int64Field: -1, IntField: 1, StringField: "C"} + testD := Test{Int64Field: -1, IntField: 1, StringField: "D"} + testE := Test{Int64Field: -1, IntField: 1, StringField: "E"} - 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...) + insertExec, err := exec.NewInsert[Test](orm, testA, testB, testC, testD, testE) if err != nil { - log.LogError("TestSelectWithLimitAndOffset setup failed: %s", err) + log.LogError("TestSelectOrderBy setup failed: %s", err) t.Fail() return } err = insertExec.Execute(conn) if err != nil { - log.LogError("TestSelectWithLimitAndOffset setup failed: %s", err) + log.LogError("TestSelectOrderBy setup failed: %s", err) t.Fail() return } @@ -304,23 +305,27 @@ func TestSelectOrderBy(t *testing.T) { selectExec, err := exec.CreateSelect[Test](orm) if err != nil { - log.LogError("TestSelectWithLimitAndOffset failed: %s", err) + log.LogError("TestSelectOrderBy failed: %s", err) t.Fail() return } - selectExec.OrderBy(exec.OrderBy{Column: "StringField", Direction: "ASC"}) + selectExec.OrderBy(schema.OrderBy{Field: "StringField", Direction: "DESC"}) err = selectExec.Execute(conn) if err != nil { - log.LogError("TestSelectWithLimitAndOffset failed: %s", err) + log.LogError("TestSelectOrderBy failed: %s", err) t.Fail() return } // THEN + var resultStr string + for _, obj := range selectExec.Results() { + resultStr += obj.StringField + } - if len(selectExec.Results()) != 100 { - log.LogError("TestSelectWithLimitAndOffset expected result size 100, actual: %s", len(selectExec.Results())) + if resultStr != "ABCDE" { + log.LogError("TestSelectOrderBy failed, epxected: ABCDE actual: %s", resultStr) t.Fail() } }