diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..0b16f0f --- /dev/null +++ b/cache.go @@ -0,0 +1,45 @@ +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.TypeName] = table +} + +func (o *OrmCache) Get(tableName string) (*Table, bool) { + o.mu.RLock() + defer o.mu.RUnlock() + table, ok := o.data[tableName] + 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/column.go b/column.go new file mode 100644 index 0000000..3cff771 --- /dev/null +++ b/column.go @@ -0,0 +1,65 @@ +package simpleorm + +import ( + "reflect" + "strings" +) + +type Column struct { + Name string + FieldName string + Type string + Modifiers map[string]string +} + +func NewColumn(name string, typ string, fieldName string, tag reflect.StructTag) Column { + return Column{name, typ, fieldName, determineModifiers(tag)} +} + +func determineModifiers(tag reflect.StructTag) map[string]string { + modifiers := make(map[string]string) + sqlTag := tag.Get("sql") + if sqlTag == "" { + return modifiers + } + + rawModifiers := strings.SplitSeq(sqlTag, ";") + for rawModifier := range rawModifiers { + if strings.Contains(rawModifier, "=") { + keyValue := strings.Split(rawModifier, "=") + modifiers[keyValue[0]] = keyValue[1] + } else { + modifiers[rawModifier] = "" + } + } + return modifiers +} + +func (c *Column) ToDDL() string { + var ddlBuilder strings.Builder + + ddlBuilder.WriteString(c.Name + " " + c.Type) + + inlineMods := c.inlineModifiers() + for _, mod := range inlineMods { + ddlBuilder.WriteString(" " + mod) + } + + return ddlBuilder.String() +} + +func (c *Column) inlineModifiers() []string { + var mods []string + for k, _ := range c.Modifiers { + switch k { + case "nn": + mods = append(mods, "NOT NULL") + } + } + return mods +} + +func (c *Column) IsPK() bool { + _, ok := c.Modifiers["pk"] + return ok +} diff --git a/constraint.go b/constraint.go new file mode 100644 index 0000000..1bcea67 --- /dev/null +++ b/constraint.go @@ -0,0 +1,59 @@ +package simpleorm + +import "strings" + +type Constraint struct { + Name string + Type string + Columns []Column + RefTable *Table + RefColumns []Column +} + +func (c *Constraint) ToDDL() string { + switch c.Type { + case "pk": + return c.toPkDDL() + case "fk": + return c.toFkDDL() + } + + return "" +} + +func (c *Constraint) toPkDDL() string { + var ddl strings.Builder + ddl.WriteString("CONSTRAINT " + c.Name + " PRIMARY KEY(") + for i, col := range c.Columns { + if i != 0 { + ddl.WriteString(", ") + } + ddl.WriteString(col.Name) + } + ddl.WriteString(")") + return ddl.String() +} + +func (c *Constraint) toFkDDL() string { + var ddl strings.Builder + ddl.WriteString("CONSTRAINT " + c.Name + " FOREIGN KEY(") + + for i, col := range c.Columns { + if i != 0 { + ddl.WriteString(", ") + } + ddl.WriteString(col.Name) + } + ddl.WriteString(") REFERENCES " + c.RefTable.Name + "(") + + for i, col := range c.RefColumns { + if i != 0 { + ddl.WriteString(", ") + } + ddl.WriteString(col.Name) + } + + ddl.WriteString(")") + + return ddl.String() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..50ffbdd --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module simpleorm + +go 1.26.2 + +require gitlab.com/gdulai/simpleloglvl v0.0.0-20260418080844-d5cca4888d97 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..256fbf7 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +gitlab.com/gdulai/simpleloglvl v0.0.0-20260418080844-d5cca4888d97 h1:f6UvrrilTIgQ2jWMJEEObZwAQQPykoWY2ju3Bq2qaGA= +gitlab.com/gdulai/simpleloglvl v0.0.0-20260418080844-d5cca4888d97/go.mod h1:H7XPunUrSyAvPa9nx8UbKnThEQJDmj3mdt+9ZtqDth4= diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..dd922e7 --- /dev/null +++ b/main_test.go @@ -0,0 +1,16 @@ +package simpleorm_test + +import ( + "os" + "testing" + + log "gitlab.com/gdulai/simpleloglvl" +) + +func TestMain(m *testing.M) { + log.SetupLogs("Debug") + + code := m.Run() + + os.Exit(code) +} diff --git a/orm.go b/orm.go new file mode 100644 index 0000000..abea45d --- /dev/null +++ b/orm.go @@ -0,0 +1,59 @@ +// Package simpleorm provies a simple, basic ORM functionalities for making +// interaction with relational databses easier. +package simpleorm + +import ( + "reflect" + "strings" + + log "gitlab.com/gdulai/simpleloglvl" +) + +// Type to access the ORM functionalities in a structured manner- +type ORM struct { + cache *OrmCache +} + +// 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 + for _, obj := range objs { + log.LogDebug("[ORM] Mapping type for: %s", reflect.TypeOf(obj).Name()) + parser := NewParser(obj) + parsers = append(parsers, parser) + tables = append(tables, parser.ParseColumns()) + } + + log.LogDebug("[ORM] Tables initiated, creating cache.") + + // Create cache with the initialized tables + cache := NewOrmCache(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) + } + + return &ORM{cache: cache} +} + +func (orm *ORM) CreateDDL() string { + var ddl strings.Builder + + tables := orm.cache.GetAll() + + for i, table := range tables { + if i != 0 { + ddl.WriteString("\n") + } + ddl.WriteString(table.ToDDL()) + } + + return ddl.String() +} diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..c74cbc8 --- /dev/null +++ b/parser.go @@ -0,0 +1,114 @@ +package simpleorm + +import ( + "reflect" + "strings" + "unicode" + + log "gitlab.com/gdulai/simpleloglvl" +) + +type Parser struct { + typ reflect.Type + Table *Table +} + +func NewParser(obj any) *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() *Table { + table := Table{Name: camelToSnake(p.typ.Name()), TypeName: p.typ.Name()} + + var columns []Column + for field := range p.typ.Fields() { + field := field + col := NewColumn(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 *OrmCache) { + pkConstraint := Constraint{Name: "pk_" + strings.ToLower(p.Table.Name), Type: "pk"} + fkConstraints := make(map[string]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 = 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": + return "INTEGER" + case "time.Time", "int64": + return "BIGINT" + } + + 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/parser_test.go b/parser_test.go new file mode 100644 index 0000000..33a0526 --- /dev/null +++ b/parser_test.go @@ -0,0 +1,117 @@ +package simpleorm_test + +import ( + "simpleorm" + "strconv" + "testing" + + log "gitlab.com/gdulai/simpleloglvl" +) + +type Test struct { + ID int `sql:"pk"` + Int64Field int64 `sql:"nn"` + IntField int `sql:"nn"` + StringField string `sql:"nn"` +} + +func TestParseSimple(t *testing.T) { + log.LogInfo("Test start...") + // GIVEN + orm := simpleorm.NewORM(Test{}) + // WHEN + ddl := orm.CreateDDL() + // THEN + 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) + log.LogError("\nExpected size: %s\nActual size: %s", strconv.Itoa(len(expectedDdl)), strconv.Itoa(len(ddl))) + t.Fail() + } + log.LogInfo("Test finished!") +} + +type TestWithFk struct { + ID int `sql:"pk"` + TestID int `sql:"nn;fk=Test.ID"` +} + +func TestParseFk(t *testing.T) { + log.LogInfo("Test start...") + // GIVEN + orm := simpleorm.NewORM(Test{}, TestWithFk{}) + // WHEN + ddl := orm.CreateDDL() + // THEN + 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));" + if ddl != expectedDdl { + log.LogError("Incorrect DDL.\nExpected\n%s\nActual\n%s", expectedDdl, ddl) + log.LogError("\nExpected size: %s\nActual size: %s", strconv.Itoa(len(expectedDdl)), strconv.Itoa(len(ddl))) + t.Fail() + } + log.LogInfo("Test finished!") +} + +type TestWithFkAndFkId struct { + ID int `sql:"pk"` + TestID int `sql:"nn;fk=Test.ID;fk_id=custom_fk"` +} + +func TestParseFkAndFkId(t *testing.T) { + // GIVEN + orm := simpleorm.NewORM(Test{}, TestWithFkAndFkId{}) + // WHEN + ddl := orm.CreateDDL() + // THEN + 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));" + if ddl != expectedDdl { + log.LogError("Incorrect DDL.\nExpected\n%s\nActual\n%s", expectedDdl, ddl) + log.LogError("\nExpected size: %s\nActual size: %s", strconv.Itoa(len(expectedDdl)), strconv.Itoa(len(ddl))) + t.Fail() + } +} + +type TestWithCompositePk struct { + ID int `sql:"nn;pk"` + Name string `sql:"nn;pk"` +} + +func TestParseCompositePk(t *testing.T) { + // GIVEN + orm := simpleorm.NewORM(TestWithCompositePk{}) + // WHEN + ddl := orm.CreateDDL() + // THEN + 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) + log.LogError("\nExpected size: %s\nActual size: %s", strconv.Itoa(len(expectedDdl)), strconv.Itoa(len(ddl))) + t.Fail() + } +} + +type TestWithCompositeFk struct { + ID int `sql:"nn;pk"` + CompositeFkId int `sql:"nn;fk=TestWithCompositePk.ID;"` + CompositeFkName string `sql:"nn;fk=TestWithCompositePk.Name;"` +} + +func TestParseCompositeFk(t *testing.T) { + // GIVEN + orm := simpleorm.NewORM(TestWithCompositePk{}, TestWithCompositeFk{}) + // WHEN + ddl := orm.CreateDDL() + // THEN + 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));" + if ddl != expectedDdl { + log.LogError("Incorrect DDL.\nExpected\n%s\nActual\n%s", expectedDdl, ddl) + log.LogError("\nExpected size: %s\nActual size: %s", strconv.Itoa(len(expectedDdl)), strconv.Itoa(len(ddl))) + t.Fail() + } +} diff --git a/table.go b/table.go new file mode 100644 index 0000000..617786d --- /dev/null +++ b/table.go @@ -0,0 +1,29 @@ +package simpleorm + +import ( + "strings" +) + +type Table struct { + Name string + TypeName string + 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() +}