Initial source commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
module simpleorm
|
||||
|
||||
go 1.26.2
|
||||
|
||||
require gitlab.com/gdulai/simpleloglvl v0.0.0-20260418080844-d5cca4888d97 // indirect
|
||||
@@ -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=
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user