Initial source commit

This commit is contained in:
2026-05-03 19:23:27 +02:00
parent b27b9add45
commit 106e5c3dc5
10 changed files with 511 additions and 0 deletions
+45
View File
@@ -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
}
+65
View File
@@ -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
}
+59
View File
@@ -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()
}
+5
View File
@@ -0,0 +1,5 @@
module simpleorm
go 1.26.2
require gitlab.com/gdulai/simpleloglvl v0.0.0-20260418080844-d5cca4888d97 // indirect
+2
View File
@@ -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=
+16
View File
@@ -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)
}
+59
View File
@@ -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()
}
+114
View File
@@ -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()
}
+117
View File
@@ -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()
}
}
+29
View File
@@ -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()
}