diff --git a/cache.go b/cache.go deleted file mode 100644 index 205cd04..0000000 --- a/cache.go +++ /dev/null @@ -1,45 +0,0 @@ -package simpleorm - -import ( - "sync" -) - -type OrmCache struct { - mu sync.RWMutex - data map[string]*Table -} - -func NewOrmCache(tables []*Table) *OrmCache { - cache := &OrmCache{ - data: make(map[string]*Table), - } - for _, table := range tables { - cache.add(table) - } - - return cache -} - -func (o *OrmCache) add(table *Table) { - o.mu.Lock() - defer o.mu.Unlock() - o.data[table.Type.Name()] = table -} - -func (o *OrmCache) Get(typeName string) (*Table, bool) { - o.mu.RLock() - defer o.mu.RUnlock() - table, ok := o.data[typeName] - return table, ok -} - -func (o *OrmCache) GetAll() []*Table { - o.mu.RLock() - defer o.mu.RUnlock() - var result []*Table - - for _, v := range o.data { - result = append(result, v) - } - return result -} diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..e6760d9 --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,47 @@ +package cache + +import ( + "sync" + + "git.gdulai.com/gdulai/simpleorm/schema" +) + +type SchemaCache struct { + mu sync.RWMutex + data map[string]*schema.Table +} + +func NewSchemaCache(tables []*schema.Table) *SchemaCache { + cache := &SchemaCache{ + data: make(map[string]*schema.Table), + } + for _, table := range tables { + cache.add(table) + } + + return cache +} + +func (o *SchemaCache) add(table *schema.Table) { + o.mu.Lock() + defer o.mu.Unlock() + o.data[table.Type.Name()] = table +} + +func (o *SchemaCache) Get(typeName string) (*schema.Table, bool) { + o.mu.RLock() + defer o.mu.RUnlock() + table, ok := o.data[typeName] + return table, ok +} + +func (o *SchemaCache) GetAll() []*schema.Table { + o.mu.RLock() + defer o.mu.RUnlock() + var result []*schema.Table + + for _, v := range o.data { + result = append(result, v) + } + return result +} diff --git a/dbconnection.go b/dbconnection.go index c88c0e3..0db9357 100644 --- a/dbconnection.go +++ b/dbconnection.go @@ -22,6 +22,20 @@ func OpenConnection(driver string, dsn string) *DBConnection { return &DBConnection{db: db, State: 1} } +func (c *DBConnection) Exec(sql string) (sql.Result, error) { + // Force a real DB interaction + result, err := c.db.Exec(sql) + if err != nil { + log.LogFatalError("DB execution failed: %", err) + return nil, err + } + return result, nil +} + +func (c *DBConnection) Prepare(sql string) (*sql.Stmt, error) { + return c.db.Prepare(sql) +} + func (c *DBConnection) Close() (bool, error) { err := c.db.Close() if err != nil { diff --git a/exec/exec.go b/exec/exec.go new file mode 100644 index 0000000..56ac02d --- /dev/null +++ b/exec/exec.go @@ -0,0 +1,326 @@ +package exec + +import ( + "database/sql" + "errors" + "reflect" + "time" + + "git.gdulai.com/gdulai/simpleorm" + "git.gdulai.com/gdulai/simpleorm/schema" + log "gitlab.com/gdulai/simpleloglvl" +) + +func ExecuteDDL(conn *simpleorm.DBConnection, orm *simpleorm.ORM) { + schema, err := orm.CreateSchema() + if err != nil { + log.LogError("Failed to run DDL: %s", schema) + return + } + + log.LogInfo("Executing DDL:\n%s", schema) + + if _, err := conn.Exec(schema); err != nil { + log.LogFatalError("%", err) + } +} + +type Exec[T any] interface { + Execute(conn *simpleorm.DBConnection) ([]T, error) +} + +type Select[T any] struct { + target schema.Table + whereStmt string + args []any +} + +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}, nil +} + +func (s Select[T]) Execute(conn *simpleorm.DBConnection) ([]T, error) { + dml, err := s.target.GetSelectDML() + if err != nil { + return []T{}, err + } + + if s.whereStmt != "" { + dml += " WHERE " + s.whereStmt + } + + log.LogDebug("Preparing sql: %s, with args: %s", dml, s.args) + + stmt, err := conn.Prepare(dml) + if err != nil { + return nil, err + } + defer stmt.Close() + + log.LogDebug("Executing statement: %s", stmt) + + var rows *sql.Rows + if len(s.args) == 0 { + rows, err = stmt.Query() + } else { + // Flattent args to make sure it can be parsed correctly + var flatArgs []any + for _, a := range s.args { + if s, ok := a.([]any); ok { + flatArgs = append(flatArgs, s...) + } else { + flatArgs = append(flatArgs, a) + } + } + rows, err = stmt.Query(flatArgs...) + + } + + if err != nil { + return nil, err + } + + rowContainer := createSelectResultContainer(s.target) + + var results []T + + for rows.Next() { + err = rows.Scan(rowContainer...) + if err != nil { + return nil, err + } + + targetType := s.target.Type + parsedResult := reflect.New(targetType) + for i, fieldVal := range rowContainer { + parsedResult.Elem().Field(i).Set(reflect.Indirect(reflect.ValueOf(fieldVal))) + } + parsedObj := reflect.Indirect(parsedResult).Interface().(T) + results = append(results, parsedObj) + } + + return results, nil +} + +type Insert[T any] struct { + target schema.Table + toInsert []T +} + +func NewInsert[T any](orm *simpleorm.ORM, toInsert ...T) (Insert[T], error) { + table, ok := orm.Cache().Get(reflect.TypeFor[T]().Name()) + if !ok { + return Insert[T]{}, errors.New("Failed to get table from schema cache") + } + + return Insert[T]{target: *table, toInsert: toInsert}, nil +} + +func (ins Insert[T]) Execute(conn *simpleorm.DBConnection) ([]T, error) { + dml, err := ins.target.GetInsertDML(len(ins.toInsert)) + if err != nil { + return []T{}, err + } + + var params []any + for i := range len(ins.toInsert) { + actualParams := prepareParams(ins.toInsert[i], ins.target) + if len(params) == 0 { + params = make([]any, len(ins.toInsert)*len(actualParams)) + } + for j := range actualParams { + params[(i*len(actualParams))+j] = actualParams[j] + } + } + + log.LogInfo("%s [%s]", dml, params) + + stmt, err := conn.Prepare(dml) + if err != nil { + return []T{}, err + } + defer stmt.Close() + + result, err := stmt.Exec(params...) + if err != nil { + } else { + rowsAffected, _ := result.RowsAffected() + log.LogInfo("Inserted %s row", rowsAffected) + lastInsertId, _ := result.LastInsertId() + log.LogInfo("Last ID: %s", lastInsertId) + } + return []T{}, nil +} + +type Update[T any] struct { + target schema.Table + toUpdate T +} + +func NewUpdate[T any](orm *simpleorm.ORM, toUpdate T) (Update[T], error) { + table, ok := orm.Cache().Get(reflect.TypeFor[T]().Name()) + if !ok { + return Update[T]{}, errors.New("Failed to get table from schema cache") + } + + return Update[T]{target: *table, toUpdate: toUpdate}, nil +} + +func (u Update[T]) Execute(conn *simpleorm.DBConnection) ([]T, error) { + dml, err := u.target.GetUpdateDML() + if err != nil { + return []T{}, err + } + + params := prepareParams(u.toUpdate, u.target) + + pkCols, err := getPk(u.toUpdate, u.target) + if err != nil { + return []T{}, err + } + // Put the pks back at the end + for _, pk := range pkCols { + params = append(params, pk) + } + + log.LogInfo("%s [%s]", dml, params) + + stmt, err := conn.Prepare(dml) + if err != nil { + return []T{}, err + } + defer stmt.Close() + + result, err := stmt.Exec(params...) + if err != nil { + return []T{}, err + } else { + rowsAffected, _ := result.RowsAffected() + log.LogInfo("Updated %s row", rowsAffected) + } + return []T{u.toUpdate}, nil +} + +type Delete[T any] struct { + target schema.Table + toDelete []T +} + +func NewDelete[T any](orm *simpleorm.ORM, toDelete []T) (Delete[T], error) { + table, ok := orm.Cache().Get(reflect.TypeFor[T]().Name()) + if !ok { + return Delete[T]{}, errors.New("Failed to get table from schema cache") + } + + return Delete[T]{target: *table, toDelete: toDelete}, nil +} + +func (d Delete[T]) Execute(conn *simpleorm.DBConnection) ([]T, error) { + count := len(d.toDelete) + dml, err := d.target.GetDeleteDML(count) + if err != nil { + return []T{}, err + } + + var params []any + + for _, del := range d.toDelete { + pkCols, err := getPk(del, d.target) + if err != nil { + return []T{}, err + } + + for _, pk := range pkCols { + params = append(params, pk) + } + } + + log.LogInfo("%s [%s]", dml, params) + + stmt, err := conn.Prepare(dml) + if err != nil { + return []T{}, err + } + defer stmt.Close() + + result, err := stmt.Exec(params...) + if err != nil { + return []T{}, err + } else { + rowsAffected, _ := result.RowsAffected() + log.LogInfo("Deleted %s row", rowsAffected) + } + + return d.toDelete, nil + +} + +func createSelectResultContainer(t schema.Table) []any { + vals := make([]any, t.Type.NumField()) + for i := range vals { + switch t.Type.Field(i).Type.Kind().String() { + case "string": + var fieldContainer string + vals[i] = &fieldContainer + case "int": + var fieldContainer int + vals[i] = &fieldContainer + case "int64", "time.Time": + var fieldContainer int64 + vals[i] = &fieldContainer + } + } + return vals +} + +func prepareParams(src any, t schema.Table) []any { + var params []any + for _, col := range t.Columns { + _, ok := col.Modifiers["pk"] + if ok { + continue + } + field, ok := t.Type.FieldByName(col.FieldName) + if !ok { + continue + } + fieldValue := reflect.ValueOf(src).FieldByIndex(field.Index) + typStr := field.Type.String() + + switch typStr { + case "time.Time": + time := fieldValue.Interface().(time.Time) + params = append(params, time.UnixMilli()) + default: + params = append(params, fieldValue.Interface()) + } + } + return params +} + +func getPk(src any, t schema.Table) ([]any, error) { + var values []any + for _, constraint := range t.Constraints { + if constraint.Type == "pk" { + for _, col := range constraint.Columns { + field, ok := t.Type.FieldByName(col.FieldName) + if !ok { + continue + + } + fieldValue := reflect.ValueOf(src).FieldByIndex(field.Index) + values = append(values, fieldValue.Interface()) + } + } + } + if len(values) == 0 { + return nil, errors.New("Could not determine pk column!") + } + + return values, nil +} diff --git a/exec_test.go b/exec_test.go new file mode 100644 index 0000000..de5a969 --- /dev/null +++ b/exec_test.go @@ -0,0 +1,321 @@ +package simpleorm_test + +import ( + "os" + "testing" + + simpleorm "git.gdulai.com/gdulai/simpleorm" + "git.gdulai.com/gdulai/simpleorm/exec" + log "gitlab.com/gdulai/simpleloglvl" +) + +func cleanUp(dbFile string, conn *simpleorm.DBConnection) { + conn.Close() + os.Remove(dbFile) +} + +func TestSelectEmpty(t *testing.T) { + // GIVEN + conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + defer cleanUp("test.db", conn) + + orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, + TestWithCompositePk{}, TestWithCompositeFk{}) + + exec.ExecuteDDL(conn, orm) + + // WHEN + selectExec, err := exec.CreateSelect[Test](orm, "") + + // THEN + + if err != nil { + log.LogError("Failed to create select. %s", err) + t.Fail() + return + } + + res, err := selectExec.Execute(conn) + + if err != nil { + log.LogError("Select failure. %s", err) + t.Fail() + return + } + if len(res) > 0 { + log.LogError("Expected empty result.") + t.Fail() + return + } +} + +func TestInsertAndSelectSingle(t *testing.T) { + // GIVEN + conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + defer cleanUp("test.db", conn) + orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, + TestWithCompositePk{}, TestWithCompositeFk{}) + + exec.ExecuteDDL(conn, orm) + + testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + + 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 + } + + // WHEN + _, err = insertExec.Execute(conn) + if err != nil { + log.LogError("TestInsertAndSelectSingle insert failed: %s", err) + t.Fail() + return + } + + res, err := selectExec.Execute(conn) + + // THEN + if err != nil { + log.LogError("TestInsertAndSelectSingle failure. %s", err) + t.Fail() + return + } + if len(res) != 1 { + log.LogError("TestInsertAndSelectSingle test failed. Expected: 1, Actual: %s", len(res)) + t.Fail() + return + } + + singleRes := res[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 TestInsertMultipleAndSelectWithParam(t *testing.T) { + // GIVEN + conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + defer cleanUp("test.db", conn) + + orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, + TestWithCompositePk{}, TestWithCompositeFk{}) + + exec.ExecuteDDL(conn, orm) + + 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 + } + + // WHEN + + _, 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 + } + + res, err := selectExec.Execute(conn) + + // THEN + if err != nil { + log.LogError("TestInsertMultipleAndSelectWithParam failure. %s", err) + t.Fail() + return + } + 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 + conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + defer cleanUp("test.db", conn) + orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, + TestWithCompositePk{}, TestWithCompositeFk{}) + + exec.ExecuteDDL(conn, orm) + + testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + + insertExec, err := exec.NewInsert(orm, testObj) + if err != nil { + log.LogError("TestInsertAndUpdate setup failed: %s", err) + t.Fail() + return + } + + selectExec, err := exec.CreateSelect[Test](orm, "string_field = ?", "asdf") + if err != nil { + log.LogError("TestInsertMultipleAndSelectWithParam setup failed: %s", err) + t.Fail() + return + } + + // WHEN + + _, err = insertExec.Execute(conn) + if err != nil { + log.LogError("TestInsertAndUpdate insert failed: %s", err) + t.Fail() + return + } + + res, err := selectExec.Execute(conn) + if err != nil { + log.LogError("TestInsertAndUpdate re-select failed: %s", err) + t.Fail() + return + } + + testObj = res[0] + + testObj.IntField = 42069 + testObj.Int64Field = 69420 + + updateExec, err := exec.NewUpdate(orm, testObj) + if err != nil { + log.LogError("TestInsertAndUpdate setup failed: %s", err) + t.Fail() + return + } + + _, err = updateExec.Execute(conn) + res, selectErr := selectExec.Execute(conn) + + // THEN + if err != nil { + log.LogError("TestInsertAndUpdate update failure. %s", err) + t.Fail() + return + } + + if selectErr != nil { + log.LogError("TestInsertAndUpdate select failure. %s", err) + t.Fail() + return + } + + if len(res) != 1 { + log.LogError("TestInsertAndUpdate test failed. Expected: 1, Actual: %s", len(res)) + t.Fail() + return + } + + singleRes := res[0] + if singleRes.ID != 1 || singleRes.Int64Field != 69420 || singleRes.IntField != 42069 || singleRes.StringField != "asdf" { + log.LogError("TestInsertAndUpdate invalid result. Expected: %s, Actual: %s", testObj, singleRes) + t.Fail() + } + +} + +func TestInsertAndDelete(t *testing.T) { + // GIVEN + conn := simpleorm.OpenConnection("sqlite3", "file:test.db") + defer cleanUp("test.db", conn) + orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, + TestWithCompositePk{}, TestWithCompositeFk{}) + + exec.ExecuteDDL(conn, orm) + + testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} + + insertExec, err := exec.NewInsert(orm, testObj) + if err != nil { + log.LogError("TestInsertAndDelete setup failed: %s", err) + t.Fail() + return + } + + _, err = insertExec.Execute(conn) + if err != nil { + log.LogError("TestInsertAndDelete setup failed: %s", err) + t.Fail() + return + } + + selectExec, err := exec.CreateSelect[Test](orm, "") + if err != nil { + log.LogError("TestInsertAndDelete setup failed: %s", err) + t.Fail() + return + } + + selectRes, err := selectExec.Execute(conn) + + if err != nil { + log.LogError("TestInsertAndDelete setup failed. %s", err) + t.Fail() + return + } + + deleteExec, err := exec.NewDelete[Test](orm, selectRes) + + // WHEN + _, err = deleteExec.Execute(conn) + + // THEN + if err != nil { + log.LogError("TestInsertAndDelete delete failure. %s", err) + t.Fail() + return + } + + selectRes, err = selectExec.Execute(conn) + if len(selectRes) != 0 { + log.LogError("TestInsertAndDelete test failed. Expected: 0, Actual: %s", len(selectRes)) + t.Fail() + return + } + +} diff --git a/orm.go b/orm.go index 1d4ae59..ab127f5 100644 --- a/orm.go +++ b/orm.go @@ -6,22 +6,26 @@ import ( "reflect" "strings" + "git.gdulai.com/gdulai/simpleorm/cache" + "git.gdulai.com/gdulai/simpleorm/parser" + "git.gdulai.com/gdulai/simpleorm/schema" + log "gitlab.com/gdulai/simpleloglvl" ) // Type to access the ORM functionalities in a structured manner- type ORM struct { - cache *OrmCache + cache *cache.SchemaCache } // 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 - var tables []*Table + var parsers []*parser.Parser + var tables []*schema.Table for _, obj := range objs { log.LogDebug("[ORM] Mapping type for: %s", reflect.TypeOf(obj).Name()) - parser := NewParser(obj) + parser := parser.NewParser(obj) parsers = append(parsers, parser) tables = append(tables, parser.ParseColumns()) } @@ -29,7 +33,7 @@ func NewORM(objs ...any) *ORM { log.LogDebug("[ORM] Tables initiated, creating cache.") // Create cache with the initialized tables - cache := NewOrmCache(tables) + cache := cache.NewSchemaCache(tables) log.LogDebug("[ORM] Cache created.") @@ -44,7 +48,7 @@ func NewORM(objs ...any) *ORM { } // Builds the DDL and returns it as a string -func (orm ORM) CreateDDL() string { +func (orm *ORM) CreateSchema() (string, error) { var ddl strings.Builder tables := orm.cache.GetAll() @@ -53,8 +57,15 @@ func (orm ORM) CreateDDL() string { if i != 0 { ddl.WriteString("\n") } - ddl.WriteString(table.ToDDL()) + tableDdl, err := table.GetDDL() + if err != nil { + return "", err + } + ddl.WriteString(tableDdl) } + return ddl.String(), nil +} - return ddl.String() +func (orm *ORM) Cache() *cache.SchemaCache { + return orm.cache } diff --git a/ormexec.go b/ormexec.go deleted file mode 100644 index ca725f7..0000000 --- a/ormexec.go +++ /dev/null @@ -1,191 +0,0 @@ -package simpleorm - -import ( - "database/sql" - "errors" - "reflect" - - log "gitlab.com/gdulai/simpleloglvl" -) - -type ORMExec[T any] struct { - orm *ORM - conn *DBConnection -} - -func CreateExecution[T any](orm *ORM, conn *DBConnection) ORMExec[T] { - return ORMExec[T]{orm: orm, conn: conn} -} - -func InitDB(conn *DBConnection, orm *ORM) { - // Force a real DB interaction - if _, err := conn.db.Exec(orm.CreateDDL()); err != nil { - log.LogFatalError("%", err) - } -} - -func (ormExec ORMExec[T]) Select(desc T, whereStmt string, args ...any) ([]T, error) { - lookup := reflect.TypeOf(desc).Name() - table, ok := ormExec.orm.cache.Get(lookup) - if !ok { - return []T{}, errors.New("Failed to get descriptor from cache! " + lookup) - } - sqlStr := table.ToSelectDML() - if whereStmt != "" { - sqlStr += " WHERE " + whereStmt - } - log.LogDebug("Preparing sql: %s, with args: %s", sqlStr, args) - - stmt, err := ormExec.conn.db.Prepare(sqlStr) - if err != nil { - return nil, err - } - defer stmt.Close() - - log.LogDebug("Executing statement: %s", stmt) - - var rows *sql.Rows - if len(args) == 0 { - rows, err = stmt.Query() - } else { - // Flattent args to make sure it can be parsed correctly - var flatArgs []any - for _, a := range args { - if s, ok := a.([]any); ok { - flatArgs = append(flatArgs, s...) - } else { - flatArgs = append(flatArgs, a) - } - } - rows, err = stmt.Query(flatArgs...) - - } - - if err != nil { - return nil, err - } - - rowContainer := table.createSelectResultContainer() - - var results []T - - for rows.Next() { - err = rows.Scan(rowContainer...) - if err != nil { - return nil, err - } - - targetType := table.Type - parsedResult := reflect.New(targetType) - for i, fieldVal := range rowContainer { - parsedResult.Elem().Field(i).Set(reflect.Indirect(reflect.ValueOf(fieldVal))) - } - parsedObj := reflect.Indirect(parsedResult).Interface().(T) - results = append(results, parsedObj) - } - - return results, nil -} - -func (ormExec ORMExec[T]) Insert(src ...T) error { - typeName := reflect.TypeOf(src[0]).Name() - table, ok := ormExec.orm.cache.Get(typeName) - if !ok { - return errors.New("Could not find table for type " + typeName) - } - - sql := table.ToInsertDML(len(src)) - var params []any - for i := range len(src) { - actualParams := table.prepareParams(src[i]) - if len(params) == 0 { - params = make([]any, len(src)*len(actualParams)) - } - for j := range actualParams { - params[(i*len(actualParams))+j] = actualParams[j] - } - } - - log.LogInfo("%s [%s]", sql, params) - - stmt, err := ormExec.conn.db.Prepare(sql) - if err != nil { - return err - } - defer stmt.Close() - - result, err := stmt.Exec(params...) - if err != nil { - return err - } else { - rowsAffected, _ := result.RowsAffected() - log.LogInfo("Inserted %s row", rowsAffected) - } - return nil -} - -func (ormExec ORMExec[T]) Update(src T) error { - typeName := reflect.TypeOf(src).Name() - table, ok := ormExec.orm.cache.Get(reflect.TypeOf(src).Name()) - if !ok { - return errors.New("Could not find table for type " + typeName) - } - - sql := table.ToUpdateDML(src) - params := table.prepareParams(src) - - pk, err := table.getPk(src) - if err != nil { - return err - } - // Put the pk back at the end - params = append(params, pk) - - log.LogInfo("%s [%s]", sql, params) - - stmt, err := ormExec.conn.db.Prepare(sql) - if err != nil { - return err - } - defer stmt.Close() - - result, err := stmt.Exec(params...) - if err != nil { - return err - } else { - rowsAffected, _ := result.RowsAffected() - log.LogInfo("Updated %s row", rowsAffected) - } - return nil -} - -func (ormExec ORMExec[T]) Delete(src T) error { - typeName := reflect.TypeOf(src).Name() - table, ok := ormExec.orm.cache.Get(reflect.TypeOf(src).Name()) - if !ok { - return errors.New("Could not find table for type " + typeName) - } - - sql := table.ToDeleteDML(src) - pk, err := table.getPk(src) - if err != nil { - return err - } - - log.LogInfo("%s [%s]", sql, pk) - - stmt, err := ormExec.conn.db.Prepare(sql) - if err != nil { - return err - } - defer stmt.Close() - - result, err := stmt.Exec(pk) - if err != nil { - return err - } else { - rowsAffected, _ := result.RowsAffected() - log.LogInfo("Deleted %s row", rowsAffected) - } - return nil -} diff --git a/ormexec_test.go b/ormexec_test.go deleted file mode 100644 index ed0c929..0000000 --- a/ormexec_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package simpleorm_test - -import ( - "os" - "testing" - - simpleorm "git.gdulai.com/gdulai/simpleorm" - log "gitlab.com/gdulai/simpleloglvl" -) - -func TestSelectEmpty(t *testing.T) { - // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") - defer cleanUp("test.db", conn) - - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - simpleorm.InitDB(conn, orm) - - exec := simpleorm.CreateExecution[Test](orm, conn) - // WHEN - res, err := exec.Select(Test{}, "") - // THEN - if err != nil { - log.LogError("Select failure. %s", err) - t.Fail() - } - if len(res) > 0 { - log.LogError("Expected empty result.") - t.Fail() - } -} - -func TestInsertAndSelectSingle(t *testing.T) { - // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") - defer cleanUp("test.db", conn) - - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - simpleorm.InitDB(conn, orm) - - exec := simpleorm.CreateExecution[Test](orm, conn) - - testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} - err := exec.Insert(testObj) - if err != nil { - log.LogError("Insert failure. %s", err) - t.Fail() - } - // WHEN - res, err := exec.Select(Test{}, "") - // THEN - if err != nil { - log.LogError("Select failure. %s", err) - t.Fail() - } - if len(res) != 1 { - log.LogError("Selec test failed. Expected: 1, Actual: %s", len(res)) - t.Fail() - } - - singleRes := res[0] - if singleRes.ID != 1 || singleRes.Int64Field != 54 || singleRes.IntField != 12 || singleRes.StringField != "asdf" { - log.LogError("Invalid result. Expected: %s, Actual: %s", testObj, singleRes) - t.Fail() - } - -} - -func TestInsertMultipleAndSelectWithParam(t *testing.T) { - // GIVEN - conn := simpleorm.OpenConnection("sqlite3", "file:test.db") - defer cleanUp("test.db", conn) - - orm := simpleorm.NewORM(Test{}, TestWithFk{}, TestWithFkAndFkId{}, - TestWithCompositePk{}, TestWithCompositeFk{}) - - simpleorm.InitDB(conn, orm) - - exec := simpleorm.CreateExecution[Test](orm, conn) - - testObj := Test{Int64Field: 54, IntField: 12, StringField: "asdf"} - testObjSecond := Test{Int64Field: 12, IntField: 54, StringField: "fdsa"} - err := exec.Insert(testObj, testObjSecond) - if err != nil { - log.LogError("Insert failure. %s", err) - t.Fail() - } - // WHEN - res, err := exec.Select(Test{}, "string_field = ?", "fdsa") - // THEN - if err != nil { - log.LogError("Select failure. %s", err) - t.Fail() - } - if len(res) != 1 { - log.LogError("Select test failed. Expected: 1, Actual: %s", len(res)) - t.Fail() - } - - singleRes := res[0] - if singleRes.ID != 2 || singleRes.Int64Field != 12 || singleRes.IntField != 54 || singleRes.StringField != "fdsa" { - log.LogError("Invalid result. Expected: %s, Actual: %s", testObjSecond, singleRes) - t.Fail() - } -} - -func cleanUp(dbFile string, conn *simpleorm.DBConnection) { - conn.Close() - os.Remove(dbFile) -} diff --git a/parser.go b/parser/parser.go similarity index 65% rename from parser.go rename to parser/parser.go index 72cf0c8..5d335b4 100644 --- a/parser.go +++ b/parser/parser.go @@ -1,16 +1,18 @@ -package simpleorm +package parser import ( "reflect" "strings" - "unicode" + 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 *Table + Table *schema.Table } func NewParser[T any](obj T) *Parser { @@ -19,13 +21,13 @@ func NewParser[T any](obj T) *Parser { } // This is step 1 of the parsing, it creates the table instance and -func (p *Parser) ParseColumns() *Table { - table := Table{Name: camelToSnake(p.typ.Name()), Type: p.typ} +func (p *Parser) ParseColumns() *schema.Table { + table := schema.Table{Name: util.CamelToSnake(p.typ.Name()), Type: p.typ} - var columns []Column + var columns []schema.Column for field := range p.typ.Fields() { field := field - col := NewColumn(camelToSnake((field.Name)), field.Name, determineType(field.Type), field.Tag) + col := schema.NewColumn(util.CamelToSnake((field.Name)), field.Name, determineType(field.Type), field.Tag) columns = append(columns, col) } @@ -34,9 +36,9 @@ func (p *Parser) ParseColumns() *Table { return p.Table } -func (p *Parser) ParseConstraints(cache *OrmCache) { - pkConstraint := Constraint{Name: "pk_" + strings.ToLower(p.Table.Name), Type: "pk"} - fkConstraints := make(map[string]Constraint) +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 { @@ -63,7 +65,7 @@ func (p *Parser) ParseConstraints(cache *OrmCache) { fkConstraint, ok := fkConstraints[fkId] if !ok { - fkConstraint = Constraint{Name: fkId, Type: "fk", RefTable: refTable} + fkConstraint = schema.Constraint{Name: fkId, Type: "fk", RefTable: refTable} } fkConstraint.Columns = append(fkConstraint.Columns, col) @@ -97,18 +99,3 @@ func determineType(typ reflect.Type) string { return "VARCHAR(255)" } - -func camelToSnake(str string) string { - var snake strings.Builder - isPrevUpper := true - for _, c := range str { - if unicode.IsUpper(c) && !isPrevUpper { - snake.WriteString("_" + string(unicode.ToUpper(c))) - isPrevUpper = true - } else { - snake.WriteString(string(unicode.ToUpper(c))) - isPrevUpper = unicode.IsUpper(c) - } - } - return snake.String() -} diff --git a/column.go b/schema/column.go similarity index 73% rename from column.go rename to schema/column.go index 3cff771..61ae9ac 100644 --- a/column.go +++ b/schema/column.go @@ -1,6 +1,7 @@ -package simpleorm +package schema import ( + "errors" "reflect" "strings" ) @@ -35,7 +36,7 @@ func determineModifiers(tag reflect.StructTag) map[string]string { return modifiers } -func (c *Column) ToDDL() string { +func (c *Column) GetDDL() (string, error) { var ddlBuilder strings.Builder ddlBuilder.WriteString(c.Name + " " + c.Type) @@ -45,7 +46,7 @@ func (c *Column) ToDDL() string { ddlBuilder.WriteString(" " + mod) } - return ddlBuilder.String() + return ddlBuilder.String(), nil } func (c *Column) inlineModifiers() []string { @@ -59,6 +60,22 @@ func (c *Column) inlineModifiers() []string { return mods } +func (c Column) GetSelectDML() (string, error) { + return c.Name, nil +} + +func (c Column) GetInsertDML() (string, error) { + return c.Name, nil +} + +func (c Column) GetUpdateDML() (string, error) { + return c.Name + " = ?", nil +} + +func (Column) GetDeleteDML() (string, error) { + return "", errors.New("No delete DML for columns") +} + func (c *Column) IsPK() bool { _, ok := c.Modifiers["pk"] return ok diff --git a/constraint.go b/schema/constraint.go similarity index 53% rename from constraint.go rename to schema/constraint.go index 1bcea67..aafd939 100644 --- a/constraint.go +++ b/schema/constraint.go @@ -1,6 +1,9 @@ -package simpleorm +package schema -import "strings" +import ( + "errors" + "strings" +) type Constraint struct { Name string @@ -10,18 +13,18 @@ type Constraint struct { RefColumns []Column } -func (c *Constraint) ToDDL() string { +func (c *Constraint) GetDDL() (string, error) { switch c.Type { case "pk": - return c.toPkDDL() + return c.getPkDDL(), nil case "fk": - return c.toFkDDL() + return c.getFkDDL(), nil } - return "" + return "", errors.New("Unimplemented constraing: " + c.Type) } -func (c *Constraint) toPkDDL() string { +func (c *Constraint) getPkDDL() string { var ddl strings.Builder ddl.WriteString("CONSTRAINT " + c.Name + " PRIMARY KEY(") for i, col := range c.Columns { @@ -34,7 +37,7 @@ func (c *Constraint) toPkDDL() string { return ddl.String() } -func (c *Constraint) toFkDDL() string { +func (c *Constraint) getFkDDL() string { var ddl strings.Builder ddl.WriteString("CONSTRAINT " + c.Name + " FOREIGN KEY(") @@ -57,3 +60,19 @@ func (c *Constraint) toFkDDL() string { return ddl.String() } + +func (Constraint) GetSelectDML() (string, error) { + return "", errors.New("No DML for constraints") +} + +func (Constraint) GetInsertDML() (string, error) { + return "", errors.New("No DML for constraints") +} + +func (Constraint) GetUpdateDML() (string, error) { + return "", errors.New("No DML for constraints") +} + +func (Constraint) GetDeleteDML() (string, error) { + return "", errors.New("No DML for constraints") +} diff --git a/schema/schema.go b/schema/schema.go new file mode 100644 index 0000000..e698665 --- /dev/null +++ b/schema/schema.go @@ -0,0 +1,11 @@ +package schema + +// Interface that descibes a DB schema descripotor part. +// eg.: Table, Column, Constraints etc. +type SchemaDescriptor interface { + getDDL() (string, error) + GetSelectDML() (string, error) + GetInsertDML(count int) (string, error) + GetUpdateDML() (string, error) + GetDeleteDML(count int) (string, error) +} diff --git a/schema/table.go b/schema/table.go new file mode 100644 index 0000000..76bd660 --- /dev/null +++ b/schema/table.go @@ -0,0 +1,176 @@ +package schema + +import ( + "reflect" + "strings" + + "git.gdulai.com/gdulai/simpleorm/util" +) + +type Table struct { + Name string + Type reflect.Type + Columns []Column + Constraints []Constraint +} + +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 { + if i != 0 { + ddl.WriteString(", ") + } + colDdl, err := col.GetDDL() + if err != nil { + return "", err + } + ddl.WriteString(colDdl) + } + for _, constr := range t.Constraints { + constrDdl, err := constr.GetDDL() + if err != nil { + return "", err + } + ddl.WriteString(", " + constrDdl) + } + + ddl.WriteString(");") + return ddl.String(), nil +} + +func (t Table) GetSelectDML() (string, error) { + var dml strings.Builder + dml.WriteString("SELECT ") + + for i, col := range t.Columns { + if i != 0 { + dml.WriteString(", ") + } + colDml, err := col.GetSelectDML() + if err != nil { + return "", err + } + dml.WriteString(colDml) + } + + dml.WriteString(" FROM " + t.Name) + return dml.String(), nil +} + +func (t Table) GetInsertDML(count int) (string, error) { + var dml strings.Builder + dml.WriteString("INSERT INTO " + util.CamelToSnake(t.Type.Name()) + " (") + + columnsLen := len(t.Columns) + effectiveColumnsLen := 0 + for i, col := range t.Columns { + _, ok := col.Modifiers["pk"] + if ok { + continue + } + + colDml, err := col.GetInsertDML() + if err != nil { + return "", err + } + + dml.WriteString(colDml) + if i != columnsLen-1 { + dml.WriteString(", ") + } + effectiveColumnsLen++ + } + + dml.WriteString(") VALUES") + + for i := range count { + if i != 0 { + dml.WriteString(", ") + } + for j := range effectiveColumnsLen { + if j == 0 { + dml.WriteString("(") + } + if j != effectiveColumnsLen-1 { + dml.WriteString("?, ") + } else { + dml.WriteString("?)") + } + } + } + + return dml.String(), nil +} + +func (t Table) GetUpdateDML() (string, error) { + var dml strings.Builder + dml.WriteString("UPDATE " + util.CamelToSnake(t.Type.Name()) + " SET ") + + colLen := len(t.Columns) + var pkColumns []string + // Colum names + for i, col := range t.Columns { + modifiers := col.Modifiers + + _, ok := modifiers["pk"] + if ok { + pkColumns = append(pkColumns, col.Name) + continue + } + + colDml, err := col.GetUpdateDML() + if err != nil { + return "", err + } + + if i != colLen-1 { + dml.WriteString(colDml + ", ") + } else { + dml.WriteString(colDml + " ") + } + + } + + dml.WriteString("WHERE ") + + for i, pkCol := range pkColumns { + if i != 0 { + dml.WriteString(" AND ") + } + dml.WriteString(pkCol + " = ?") + } + + return dml.String(), nil +} + +func (t Table) GetDeleteDML(count int) (string, error) { + var dml strings.Builder + dml.WriteString("DELETE FROM " + t.Name + " WHERE ") + + for _, col := range t.Columns { + modifiers := col.Modifiers + + _, ok := modifiers["pk"] + if ok { + if count > 1 { + dml.WriteString(col.Name + " IN (") + } else { + dml.WriteString(col.Name + " = ") + } + } + } + + for i := range count { + if i != 0 { + dml.WriteString(", ") + } + dml.WriteString("?") + } + + if count > 1 { + dml.WriteString(")") + } + + return dml.String(), nil +} diff --git a/orm_test.go b/schema_test.go similarity index 86% rename from orm_test.go rename to schema_test.go index fe350be..1b6ab35 100644 --- a/orm_test.go +++ b/schema_test.go @@ -13,8 +13,13 @@ func TestParseSimple(t *testing.T) { // GIVEN orm := simpleorm.NewORM(Test{}) // WHEN - ddl := orm.CreateDDL() + ddl, err := orm.CreateSchema() // THEN + if err != nil { + log.LogError("Failed to parse schema! %s", err) + t.Fail() + } + expectedDdl := "CREATE TABLE IF NOT EXISTS TEST (ID INTEGER, INT64_FIELD BIGINT NOT NULL, INT_FIELD INTEGER NOT NULL, STRING_FIELD TEXT NOT NULL, CONSTRAINT pk_test PRIMARY KEY(ID));" if ddl != expectedDdl { log.LogError("Incorrect DDL.\nExpected\n%s\nActual\n%s", expectedDdl, ddl) @@ -29,8 +34,13 @@ func TestParseFk(t *testing.T) { // GIVEN orm := simpleorm.NewORM(Test{}, TestWithFk{}) // WHEN - ddl := orm.CreateDDL() + ddl, err := orm.CreateSchema() // THEN + if err != nil { + log.LogError("Failed to parse schema! %s", err) + t.Fail() + } + expectedDdl := "CREATE TABLE IF NOT EXISTS TEST (ID INTEGER, INT64_FIELD BIGINT NOT NULL, INT_FIELD INTEGER NOT NULL, STRING_FIELD TEXT NOT NULL, CONSTRAINT pk_test PRIMARY KEY(ID));\n" + "CREATE TABLE IF NOT EXISTS TEST_WITH_FK (ID INTEGER, TEST_ID INTEGER NOT NULL, CONSTRAINT pk_test_with_fk PRIMARY KEY(ID), CONSTRAINT fk_test FOREIGN KEY(TEST_ID) REFERENCES TEST(ID));" @@ -46,8 +56,14 @@ func TestParseFkAndFkId(t *testing.T) { // GIVEN orm := simpleorm.NewORM(Test{}, TestWithFkAndFkId{}) // WHEN - ddl := orm.CreateDDL() + // WHEN + ddl, err := orm.CreateSchema() // THEN + if err != nil { + log.LogError("Failed to parse schema! %s", err) + t.Fail() + } + expectedDdl := "CREATE TABLE IF NOT EXISTS TEST (ID INTEGER, INT64_FIELD BIGINT NOT NULL, INT_FIELD INTEGER NOT NULL, STRING_FIELD TEXT NOT NULL, CONSTRAINT pk_test PRIMARY KEY(ID));\n" + "CREATE TABLE IF NOT EXISTS TEST_WITH_FK_AND_FK_ID (ID INTEGER, TEST_ID INTEGER NOT NULL, CONSTRAINT pk_test_with_fk_and_fk_id PRIMARY KEY(ID), CONSTRAINT custom_fk FOREIGN KEY(TEST_ID) REFERENCES TEST(ID));" @@ -62,8 +78,13 @@ func TestParseCompositePk(t *testing.T) { // GIVEN orm := simpleorm.NewORM(TestWithCompositePk{}) // WHEN - ddl := orm.CreateDDL() + ddl, err := orm.CreateSchema() // THEN + if err != nil { + log.LogError("Failed to parse schema! %s", err) + t.Fail() + } + expectedDdl := "CREATE TABLE IF NOT EXISTS TEST_WITH_COMPOSITE_PK (ID INTEGER NOT NULL, NAME TEXT NOT NULL, CONSTRAINT pk_test_with_composite_pk PRIMARY KEY(ID, NAME));" if ddl != expectedDdl { log.LogError("Incorrect DDL.\nExpected\n%s\nActual\n%s", expectedDdl, ddl) @@ -76,8 +97,13 @@ func TestParseCompositeFk(t *testing.T) { // GIVEN orm := simpleorm.NewORM(TestWithCompositePk{}, TestWithCompositeFk{}) // WHEN - ddl := orm.CreateDDL() + ddl, err := orm.CreateSchema() // THEN + if err != nil { + log.LogError("Failed to parse schema! %s", err) + t.Fail() + } + expectedDdl := "CREATE TABLE IF NOT EXISTS TEST_WITH_COMPOSITE_PK (ID INTEGER NOT NULL, NAME TEXT NOT NULL, CONSTRAINT pk_test_with_composite_pk PRIMARY KEY(ID, NAME));\n" + "CREATE TABLE IF NOT EXISTS TEST_WITH_COMPOSITE_FK (ID INTEGER NOT NULL, COMPOSITE_FK_ID INTEGER NOT NULL, COMPOSITE_FK_NAME TEXT NOT NULL, CONSTRAINT pk_test_with_composite_fk PRIMARY KEY(ID), CONSTRAINT fk_test_with_composite_pk FOREIGN KEY(COMPOSITE_FK_ID, COMPOSITE_FK_NAME) REFERENCES TEST_WITH_COMPOSITE_PK(ID, NAME));" diff --git a/table.go b/table.go deleted file mode 100644 index 3e36d65..0000000 --- a/table.go +++ /dev/null @@ -1,185 +0,0 @@ -package simpleorm - -import ( - "errors" - "reflect" - "strings" - "time" -) - -type Table struct { - Name string - Type reflect.Type - Columns []Column - Constraints []Constraint -} - -func (t Table) ToDDL() string { - var ddl strings.Builder - ddl.WriteString("CREATE TABLE IF NOT EXISTS " + t.Name + " (") - for i, col := range t.Columns { - if i != 0 { - ddl.WriteString(", ") - } - ddl.WriteString(col.ToDDL()) - } - for _, constr := range t.Constraints { - ddl.WriteString(", " + constr.ToDDL()) - } - - ddl.WriteString(");") - return ddl.String() -} - -func (t Table) ToSelectDML() string { - var dml strings.Builder - dml.WriteString("SELECT ") - - for i, col := range t.Columns { - if i != 0 { - dml.WriteString(", ") - } - dml.WriteString(col.Name) - } - - dml.WriteString(" FROM " + t.Name) - return dml.String() -} - -func (t Table) ToInsertDML(count int) string { - var dml strings.Builder - dml.WriteString("INSERT INTO " + camelToSnake(t.Type.Name()) + " (") - - columnsLen := len(t.Columns) - effectiveColumnsLen := 0 - for i, col := range t.Columns { - _, ok := col.Modifiers["pk"] - if ok { - continue - } - - dml.WriteString(col.Name) - if i != columnsLen-1 { - dml.WriteString(", ") - } - effectiveColumnsLen++ - } - - dml.WriteString(") VALUES") - - for i := range count { - if i != 0 { - dml.WriteString(", ") - } - for j := range effectiveColumnsLen { - if j == 0 { - dml.WriteString("(") - } - if j != effectiveColumnsLen-1 { - dml.WriteString("?, ") - } else { - dml.WriteString("?)") - } - } - } - - return dml.String() -} - -func (t Table) ToUpdateDML(src any) string { - var dml strings.Builder - dml.WriteString("UPDATE " + camelToSnake(t.Type.Name()) + " SET ") - - var pkCol string - // Colum names - for i := 0; i < t.Type.NumField(); i++ { - field := t.Type.Field(i) - modifiers := determineModifiers(field.Tag) - - _, ok := modifiers["pk"] - if ok { - pkCol = camelToSnake(field.Name) - continue - } - - if i != t.Type.NumField()-1 { - dml.WriteString(camelToSnake(field.Name) + " = ?, ") - } else { - dml.WriteString(camelToSnake(field.Name) + " = ? ") - } - - } - - dml.WriteString("WHERE " + pkCol + " = ?") - - return dml.String() -} - -func (t Table) ToDeleteDML(src any) string { - var dml strings.Builder - dml.WriteString("DELETE FROM " + camelToSnake(t.Type.Name()) + " WHERE ") - - for i := 0; i < t.Type.NumField(); i++ { - field := t.Type.Field(i) - modifiers := determineModifiers(field.Tag) - _, ok := modifiers["pk"] - if ok { - dml.WriteString(camelToSnake(field.Name) + " = ?") - return dml.String() - } - } - - return "" -} - -func (t Table) createSelectResultContainer() []any { - vals := make([]any, t.Type.NumField()) - for i := range vals { - switch t.Type.Field(i).Type.Kind().String() { - case "string": - var fieldContainer string - vals[i] = &fieldContainer - case "int": - var fieldContainer int - vals[i] = &fieldContainer - case "int64", "time.Time": - var fieldContainer int64 - vals[i] = &fieldContainer - } - } - return vals -} - -func (t Table) prepareParams(src any) []any { - var params []any - for i := 0; i < t.Type.NumField(); i++ { - field := t.Type.Field(i) - modifiers := determineModifiers(field.Tag) - _, ok := modifiers["pk"] - if ok { - continue - } - typStr := field.Type.String() - switch typStr { - case "time.Time": - time := reflect.ValueOf(src).Field(i).Interface().(time.Time) - params = append(params, time.UnixMilli()) - default: - params = append(params, reflect.ValueOf(src).Field(i).Interface()) - } - } - - return params -} - -func (t Table) getPk(src any) (any, error) { - for i := 0; i < t.Type.NumField(); i++ { - field := t.Type.Field(i) - modifiers := determineModifiers(field.Tag) - _, ok := modifiers["pk"] - if ok { - return reflect.ValueOf(src).Field(i).Interface(), nil - } - } - return nil, errors.New("Could not determine pk column!") -} diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..e927f72 --- /dev/null +++ b/util/util.go @@ -0,0 +1,21 @@ +package util + +import ( + "strings" + "unicode" +) + +func CamelToSnake(str string) string { + var snake strings.Builder + isPrevUpper := true + for _, c := range str { + if unicode.IsUpper(c) && !isPrevUpper { + snake.WriteString("_" + string(unicode.ToUpper(c))) + isPrevUpper = true + } else { + snake.WriteString(string(unicode.ToUpper(c))) + isPrevUpper = unicode.IsUpper(c) + } + } + return snake.String() +}