From 34f8393ff8810bd7aa0c5d7d24f01b74c5660b61 Mon Sep 17 00:00:00 2001 From: Ludwig Lehnert Date: Mon, 23 Sep 2024 16:57:40 +0200 Subject: [PATCH] next steps: extended schema.go and created duckdb.go --- README.md | 2 +- database.go | 30 +++++ duckdb.go | 198 +++++++++++++++++++++++++++++ go.mod | 23 ++++ go.sum | 49 ++++++++ ldb.go | 4 +- ldb_test.go | 63 ++++++++++ migration.go | 1 + schema.go | 342 +++++++++++++++++++++++++++++++++++++++------------ 9 files changed, 629 insertions(+), 83 deletions(-) create mode 100644 database.go create mode 100644 duckdb.go create mode 100644 go.sum create mode 100644 ldb_test.go diff --git a/README.md b/README.md index bff6ecb..f306488 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # LDB (lehnert-db) Framework -SQLite3 DBMS wrapper as-a-library inspired by [pocketbase](https://pocketbase.io/). +[DuckDB](https://duckdb.org/) DBMS wrapper as-a-library inspired by [pocketbase](https://pocketbase.io/). ## Key Features diff --git a/database.go b/database.go new file mode 100644 index 0000000..dcdae9d --- /dev/null +++ b/database.go @@ -0,0 +1,30 @@ +package ldb + +type DatabaseAdapter interface { + Close() error + Begin() (DatabaseTransaction, error) +} + +type DatabaseTransaction interface { + // perform commit; implementation may be omitted for NoSQL datbases + Commit() error + // perform rollback; implementation may be omitted for NoSQL databases + Rollback() error + + SaveCollection(collection Collection) error + DropCollection(collection Collection) error + + SaveView(view View) error + DropView(view View) error + + // checks if the migration with the given name has already been performed + MigrationExists(migrationName string) (bool, error) + // saves the given migration name to the migration history + FinishMigration(migrationName string) error + + // GetCollection(name string, fields map[string]FieldType) ([]any, error) + // GetRecord(collection string, fields map[string]FieldType, id string) (any, error) + // CreateRecord(collection string, fields map[string]FieldType, data map[string]any) (string, error) + // UpdateRecord(collection string, fields map[string]FieldType, id string, data map[string]any) error + // DeleteRecord(collection string, fields map[string]FieldType, id string) error +} diff --git a/duckdb.go b/duckdb.go new file mode 100644 index 0000000..58399c9 --- /dev/null +++ b/duckdb.go @@ -0,0 +1,198 @@ +package ldb + +import ( + "database/sql" + "fmt" + "strings" + + _ "github.com/marcboeker/go-duckdb" + "github.com/samber/lo" +) + +var _ DatabaseAdapter = DuckDBAdapter{} +var _ DatabaseTransaction = DuckDBTransaction{} + +type DuckDBAdapter struct { + db *sql.DB +} + +func OpenDuckDBAdapter(databaseFilePath string) (*DuckDBAdapter, error) { + db, err := sql.Open("duckdb", databaseFilePath) + if err != nil { + return nil, err + } + + return &DuckDBAdapter{db}, nil +} + +func (s DuckDBAdapter) Close() error { + return s.db.Close() +} + +func (s DuckDBAdapter) Begin() (DatabaseTransaction, error) { + tx, err := s.db.Begin() + if err != nil { + return nil, err + } + + return DatabaseTransaction(DuckDBTransaction{tx}), nil +} + +type DuckDBTransaction struct { + tx *sql.Tx +} + +// Commit implements DatabaseTransaction. +func (s DuckDBTransaction) Commit() error { + return s.tx.Commit() +} + +// Rollback implements DatabaseTransaction. +func (s DuckDBTransaction) Rollback() error { + return s.tx.Rollback() +} + +// SaveCollection implements DatabaseTransaction. +func (s DuckDBTransaction) SaveCollection(collection Collection) error { + // create collection if not exists + if collection.original == nil { + columns := []string{} + for _, field := range collection.Schema.Fields { + columns = append(columns, columnSQL(field.Name, field.Schema.Type)) + } + + sql := fmt.Sprintf("CREATE TABLE %s (%s)", collection.Name, strings.Join(columns, ", ")) + + _, err := s.tx.Exec(sql) + return err + } + + // rename collection if neccessary + if collection.original.Name != collection.Name { + sql := fmt.Sprintf("ALTER TABLE %s RENAME TO %s", collection.original.Name, collection.Name) + _, err := s.tx.Exec(sql) + if err != nil { + + return err + } + } + + createFields := lo.Filter(collection.Schema.Fields, func(field *Field, i int) bool { + return field.original == nil + }) + + renameFields := lo.Filter(collection.Schema.Fields, func(field *Field, i int) bool { + return field.original.original.Name != field.Name + }) + + removeFields := []*Field{} + if collection.original != nil { + removeFields = lo.Filter(collection.original.Schema.Fields, func(origField *Field, i int) bool { + _, found := lo.Find(collection.Schema.Fields, func(field *Field) bool { + return field.original != nil && field.original.Name == origField.Name + }) + + return !found + }) + } + + for _, field := range removeFields { + sql := fmt.Sprintf("ALTER TABLE %s DROP COLUMN %s", collection.Name, field.Name) + if _, err := s.tx.Exec(sql); err != nil { + return err + } + } + + for _, field := range renameFields { + sql := fmt.Sprintf("ALTER TABLE %s RENAME COLUMN %s TO %s", collection.Name, field.original.Name, field.Name) + if _, err := s.tx.Exec(sql); err != nil { + return err + } + } + + for _, field := range createFields { + sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", collection.Name, columnSQL(field.Name, field.Schema.Type)) + if _, err := s.tx.Exec(sql); err != nil { + return err + } + } + + return nil +} + +// DropCollection implements DatabaseTransaction. +func (s DuckDBTransaction) DropCollection(collection Collection) error { + panic("unimplemented") +} + +// SaveView implements DatabaseTransaction. +func (s DuckDBTransaction) SaveView(view View) error { + panic("unimplemented") +} + +// DropView implements DatabaseTransaction. +func (s DuckDBTransaction) DropView(view View) error { + panic("unimplemented") +} + +// MigrationExists implements DatabaseTransaction. +func (s DuckDBTransaction) MigrationExists(migrationName string) (bool, error) { + panic("unimplemented") +} + +// FinishMigration implements DatabaseTransaction. +func (s DuckDBTransaction) FinishMigration(migrationName string) error { + panic("unimplemented") +} + +func withNullConstraint(sql string, nullable bool) string { + if nullable { + return sql + " NULL" + } + + return sql + " NOT NULL" +} + +func columnSQL(column string, fieldType FieldType) string { + switch ft := fieldType.(type) { + case FieldTypeBool: + return withNullConstraint(column+" BOOL", ft.Nullable) + + case FieldTypeDateTime: + return withNullConstraint(column+" TIMESTAMP", ft.Nullable) + + case FieldTypeEnum: + return withNullConstraint(column+" TEXT", ft.Nullable) + + case FieldTypeFloat: + return withNullConstraint(column+" REAL", ft.Nullable) + + case FieldTypeId: + sql := withNullConstraint(column+" TEXT", ft.Nullable || ft.PrimaryKey) + + if ft.PrimaryKey { + sql += " PRIMARY KEY" + } + + return sql + + case FieldTypeInt: + return withNullConstraint(column+" BIGINT", ft.Nullable) + + case FieldTypeSingleRelation: + sql := withNullConstraint(column+" TEXT", ft.Nullable) + sql += " REFERENCES " + ft.Collection + "(id)" + + if ft.CascadeDelete { + sql += " ON DELETE CASCADE" + } + + return sql + + case FieldTypeText: + return withNullConstraint(column+" TEXT", ft.Nullable) + + default: + panic("SQLiteAdapter: unexpected fieldType") + } +} diff --git a/go.mod b/go.mod index 9e51f37..9702976 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,26 @@ module lehnert.dev/ldb go 1.22.7 + +require ( + github.com/marcboeker/go-duckdb v1.8.0 + github.com/samber/lo v1.47.0 +) + +require ( + github.com/apache/arrow/go/v17 v17.0.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/flatbuffers v24.3.25+incompatible // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.22.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3f791e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,49 @@ +github.com/apache/arrow/go/v17 v17.0.0 h1:RRR2bdqKcdbss9Gxy2NS/hK8i4LDMh23L6BbkN5+F54= +github.com/apache/arrow/go/v17 v17.0.0/go.mod h1:jR7QHkODl15PfYyjM2nU+yTLScZ/qfj7OSUZmJ8putc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= +github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/marcboeker/go-duckdb v1.8.0 h1:iOWv1wTL0JIMqpyns6hCf5XJJI4fY6lmJNk+itx5RRo= +github.com/marcboeker/go-duckdb v1.8.0/go.mod h1:2oV8BZv88S16TKGKM+Lwd0g7DX84x0jMxjTInThC8Is= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= +gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ldb.go b/ldb.go index fc8e3dd..66e75cb 100644 --- a/ldb.go +++ b/ldb.go @@ -1,7 +1,5 @@ package ldb -import "fmt" - type App struct { Migrations map[string]*Migration DatabaseService *DatabaseService @@ -14,7 +12,7 @@ type Migration struct { } type DatabaseService interface { - CreateCollection(name string, schema CollectionSchema) error + CreateCollection(schema CollectionSchema) error DropCollection(name string) error } diff --git a/ldb_test.go b/ldb_test.go new file mode 100644 index 0000000..f874a8a --- /dev/null +++ b/ldb_test.go @@ -0,0 +1,63 @@ +package ldb_test + +import ( + "testing" + + "lehnert.dev/ldb" +) + +func TestSQLite(t *testing.T) { + adapter, err := ldb.OpenDuckDBAdapter("/tmp/test.db") + if err != nil { + t.Fatal(err) + } + + tx, err := adapter.Begin() + if err != nil { + t.Fatal(err) + } + + if err := tx.SaveCollection(ldb.Collection{ + Name: "test0", + Schema: &ldb.CollectionSchema{ + Fields: []*ldb.Field{ + { + Name: "id", + Schema: &ldb.FieldSchema{ + Type: ldb.FieldTypeId{ + PrimaryKey: true, + }, + }, + }, + }, + }, + }); err != nil { + t.Fatal(err) + } + + if err := tx.SaveCollection(ldb.Collection{ + Name: "test1", + Schema: &ldb.CollectionSchema{ + Fields: []*ldb.Field{ + {Name: "bool", Schema: &ldb.FieldSchema{Type: ldb.FieldTypeBool{}}}, + {Name: "datetime", Schema: &ldb.FieldSchema{Type: ldb.FieldTypeDateTime{}}}, + {Name: "enum", Schema: &ldb.FieldSchema{Type: ldb.FieldTypeEnum{EnumValues: []string{"a", "b", "c"}}}}, + {Name: "float", Schema: &ldb.FieldSchema{Type: ldb.FieldTypeFloat{}}}, + {Name: "id", Schema: &ldb.FieldSchema{Type: ldb.FieldTypeId{PrimaryKey: true}}}, + {Name: "int", Schema: &ldb.FieldSchema{Type: ldb.FieldTypeInt{}}}, + {Name: "singleRelation", Schema: &ldb.FieldSchema{Type: ldb.FieldTypeSingleRelation{Collection: "test0"}}}, + {Name: "text", Schema: &ldb.FieldSchema{Type: ldb.FieldTypeText{}}}, + }, + }, + }); err != nil { + t.Fatal(err) + } + + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + + if err := adapter.Close(); err != nil { + t.Fatal(err) + } +} diff --git a/migration.go b/migration.go index e69de29..2211a30 100644 --- a/migration.go +++ b/migration.go @@ -0,0 +1 @@ +package ldb diff --git a/schema.go b/schema.go index 86b4f29..600068b 100644 --- a/schema.go +++ b/schema.go @@ -8,18 +8,108 @@ import ( "time" ) +type Forwardable interface { + Forward() +} + +type Clonable[T any] interface { + Clone() T +} + +// ensure interface implementation +var _ Forwardable = (*Collection)(nil) +var _ Forwardable = (*Field)(nil) +var _ Clonable[*Collection] = Collection{} +var _ Clonable[*CollectionSchema] = CollectionSchema{} +var _ Clonable[*Field] = Field{} +var _ Clonable[*FieldSchema] = FieldSchema{} +var _ Clonable[FieldType] = (FieldType)(nil) +var _ FieldType = FieldTypeId{} +var _ FieldType = FieldTypeText{} +var _ FieldType = FieldTypeInt{} +var _ FieldType = FieldTypeFloat{} +var _ FieldType = FieldTypeBool{} +var _ FieldType = FieldTypeDateTime{} +var _ FieldType = FieldTypeEnum{} +var _ FieldType = FieldTypeSingleRelation{} + +type Collection struct { + // collection data on last migration; useful for detecting schema changes + original *Collection + + Name string + Schema *CollectionSchema +} + +func (c *Collection) Forward() { + c.original = c.Clone() + + for _, field := range c.Schema.Fields { + field.Forward() + } +} + +func (c Collection) Clone() *Collection { + cloned := Collection{} + cloned.Name = c.Name + cloned.Schema = c.Schema.Clone() + return &cloned +} + type CollectionSchema struct { + Fields []*Field + ViewFilter func() bool + AllowCreate func() bool + AllowUpdate func() bool + AllowDelete func() bool +} + +func (s CollectionSchema) Clone() *CollectionSchema { + cloned := s + + clonedFields := make([]*Field, len(s.Fields)) + for i, field := range s.Fields { + clonedFields[i] = field.Clone() + } + + return &cloned +} + +type Field struct { + // field data on last migration; useful for detecting schema changes + original *Field + Name string - Fields []*SchemaField + Schema *FieldSchema +} + +func (f *Field) Forward() { + f.original = f.Clone() +} + +func (f Field) Clone() *Field { + cloned := Field{} + cloned.Name = f.Name + cloned.Schema = f.Schema.Clone() + return &cloned +} + +type FieldSchema struct { + Type FieldType } -type SchemaField struct { - Name string - Type SchemaFieldType +func (s FieldSchema) Clone() *FieldSchema { + cloned := FieldSchema{} + cloned.Type = s.Type.Clone() + return &cloned } -type SchemaFieldType interface { - GetName() string +type FieldType interface { + Clone() FieldType + + // validates if the specified value suits the field type; + // returns the value either in original or in encoded/decoded/recoded form; + // returns a comprehensive error message if the value is not suitable ValidateValue(value any) (any, error) } @@ -31,21 +121,55 @@ func validateNullable(nullable bool, value any) error { return nil } -type TextFieldType struct { - Nullable bool - DefaultValue *string - MaxLength *int - MinLength *int - Pattern *string +type FieldTypeId struct { + Nullable bool + PrimaryKey bool + CreateDefaultValue func() string +} + +func (ft FieldTypeId) Clone() FieldType { + return FieldType(ft) } -func (fieldType TextFieldType) ValidateValue(value any) (any, error) { +func (fieldType FieldTypeId) ValidateValue(value any) (any, error) { + if err := validateNullable(fieldType.Nullable || fieldType.PrimaryKey, value); err != nil { + return nil, err + } + + if value == nil { + return nil, nil + } + + if err := ValidateId(value); err != nil { + return nil, err + } + + return value, nil +} + +type FieldTypeText struct { + Nullable bool + CreateDefaultValue func() string + CreateMaxLength func() int + CreateMinLength func() int + CreatePattern func() string +} + +func (ft FieldTypeText) Clone() FieldType { + return FieldType(ft) +} + +func (fieldType FieldTypeText) ValidateValue(value any) (any, error) { if err := validateNullable(fieldType.Nullable, value); err != nil { return nil, err } if value == nil { - return fieldType.DefaultValue, nil + if fieldType.CreateDefaultValue != nil { + return fieldType.CreateDefaultValue(), nil + } + + return nil, nil } str, ok := value.(string) @@ -53,37 +177,50 @@ func (fieldType TextFieldType) ValidateValue(value any) (any, error) { return nil, fmt.Errorf("invalid value, expected string") } - if fieldType.MaxLength != nil && len(str) > *fieldType.MaxLength { - return nil, fmt.Errorf("value too long, max length is %v", *fieldType.MaxLength) + if fieldType.CreateMinLength != nil { + if minLength := fieldType.CreateMinLength(); len(str) < minLength { + return nil, fmt.Errorf("value too short, min length is %v", minLength) + } } - if fieldType.MinLength != nil && len(str) < *fieldType.MaxLength { - return nil, fmt.Errorf("value too short, min length is %v", *fieldType.MinLength) + if fieldType.CreateMaxLength != nil { + if maxLength := fieldType.CreateMaxLength(); len(str) > maxLength { + return nil, fmt.Errorf("value too long, max length is %v", maxLength) + } } - if fieldType.Pattern != nil { - if _, err := regexp.MatchString(*fieldType.Pattern, str); err != nil { - return nil, fmt.Errorf("value does not match pattern, pattern is %v", *fieldType.Pattern) + if fieldType.CreatePattern != nil { + pattern := fieldType.CreatePattern() + if _, err := regexp.MatchString(pattern, str); err != nil { + return nil, fmt.Errorf("value does not match pattern, pattern is %v", pattern) } } - return &str, nil + return str, nil } -type IntFieldType struct { - Nullable bool - DefaultValue *int64 - MinValue *int64 - MaxValue *int64 +type FieldTypeInt struct { + Nullable bool + CreateDefaultValue func() int64 + CreateMinValue func() int64 + CreateMaxValue func() int64 } -func (fieldType IntFieldType) ValidateValue(value any) (any, error) { +func (ft FieldTypeInt) Clone() FieldType { + return FieldType(ft) +} + +func (fieldType FieldTypeInt) ValidateValue(value any) (any, error) { if err := validateNullable(fieldType.Nullable, value); err != nil { return nil, err } if value == nil { - return fieldType.DefaultValue, nil + if fieldType.CreateDefaultValue != nil { + return fieldType.CreateDefaultValue(), nil + } + + return nil, nil } i, ok := value.(int64) @@ -91,31 +228,43 @@ func (fieldType IntFieldType) ValidateValue(value any) (any, error) { return nil, fmt.Errorf("invalid value, expected integer") } - if fieldType.MinValue != nil && i < *fieldType.MinValue { - return nil, fmt.Errorf("value too small, min value is %v", *fieldType.MinValue) + if fieldType.CreateMinValue != nil { + if minValue := fieldType.CreateMinValue(); i < minValue { + return nil, fmt.Errorf("value too small, min value is %v", minValue) + } } - if fieldType.MaxValue != nil && i > *fieldType.MaxValue { - return nil, fmt.Errorf("value too big, max value is %v", *fieldType.MaxValue) + if fieldType.CreateMaxValue != nil { + if maxValue := fieldType.CreateMaxValue(); i > maxValue { + return nil, fmt.Errorf("value too big, max value is %v", maxValue) + } } - return &i, nil + return i, nil } -type FloatFieldType struct { - Nullable bool - DefaultValue *float64 - MinValue *float64 - MaxValue *float64 +type FieldTypeFloat struct { + Nullable bool + CreateDefaultValue func() float64 + CreateMinValue func() float64 + CreateMaxValue func() float64 +} + +func (ft FieldTypeFloat) Clone() FieldType { + return FieldType(ft) } -func (fieldType FloatFieldType) ValidateValue(value any) (any, error) { +func (fieldType FieldTypeFloat) ValidateValue(value any) (any, error) { if err := validateNullable(fieldType.Nullable, value); err != nil { return nil, err } if value == nil { - return fieldType.DefaultValue, nil + if fieldType.CreateDefaultValue != nil { + return fieldType.CreateDefaultValue(), nil + } + + return nil, nil } f, ok := value.(float64) @@ -123,29 +272,41 @@ func (fieldType FloatFieldType) ValidateValue(value any) (any, error) { return nil, fmt.Errorf("invalid value, expected float") } - if fieldType.MinValue != nil && f < *fieldType.MinValue { - return nil, fmt.Errorf("value too small, min value is %v", *fieldType.MinValue) + if fieldType.CreateMinValue != nil { + if minValue := fieldType.CreateMinValue(); f < minValue { + return nil, fmt.Errorf("value too small, min value is %v", minValue) + } } - if fieldType.MaxValue != nil && f > *fieldType.MaxValue { - return nil, fmt.Errorf("value too big, max value is %v", *fieldType.MaxValue) + if fieldType.CreateMaxValue != nil { + if maxValue := fieldType.CreateMaxValue(); f > maxValue { + return nil, fmt.Errorf("value too big, max value is %v", maxValue) + } } - return &f, nil + return f, nil +} + +type FieldTypeBool struct { + Nullable bool + CreateDefaultValue func() bool } -type BoolFieldType struct { - Nullable bool - DefaultValue *bool +func (ft FieldTypeBool) Clone() FieldType { + return FieldType(ft) } -func (fieldType BoolFieldType) ValidateValue(value any) (any, error) { +func (fieldType FieldTypeBool) ValidateValue(value any) (any, error) { if err := validateNullable(fieldType.Nullable, value); err != nil { return nil, err } if value == nil { - return fieldType.DefaultValue, nil + if fieldType.CreateDefaultValue != nil { + return fieldType.CreateDefaultValue(), nil + } + + return nil, nil } b, ok := value.(bool) @@ -153,25 +314,28 @@ func (fieldType BoolFieldType) ValidateValue(value any) (any, error) { return nil, fmt.Errorf("invalid value, expected bool") } - return &b, nil + return b, nil } -type DateTimeFieldType struct { +type FieldTypeDateTime struct { Nullable bool CreateDefaultValue func() time.Time CreateMinValue func() time.Time CreateMaxValue func() time.Time } -func (fieldType DateTimeFieldType) ValidateValue(value any) (any, error) { +func (ft FieldTypeDateTime) Clone() FieldType { + return FieldType(ft) +} + +func (fieldType FieldTypeDateTime) ValidateValue(value any) (any, error) { if err := validateNullable(fieldType.Nullable, value); err != nil { return nil, err } if value == nil { if fieldType.CreateDefaultValue != nil { - defaultValue := fieldType.CreateDefaultValue() - return &defaultValue, nil + return fieldType.CreateDefaultValue(), nil } return nil, nil @@ -204,18 +368,29 @@ func (fieldType DateTimeFieldType) ValidateValue(value any) (any, error) { } } - return &d, nil + return d, nil +} + +type FieldTypeEnum struct { + Nullable bool + EnumValues []string + CreateDefaultValue func() string } -type EnumFieldType struct { - Nullable bool - DefaultValue *string - EnumValues []string +func (ft FieldTypeEnum) Clone() FieldType { + values := ft.EnumValues + ft.EnumValues = make([]string, len(values)) + copy(ft.EnumValues, values) + return FieldType(ft) } -func (fieldType EnumFieldType) ValidateValue(value any) (any, error) { - if fieldType.DefaultValue != nil && !slices.Contains(fieldType.EnumValues, *fieldType.DefaultValue) { - return nil, fmt.Errorf("configuration error, invalid default value") +func (fieldType FieldTypeEnum) ValidateValue(value any) (any, error) { + var defaultValue string = "" + if fieldType.CreateDefaultValue != nil { + defaultValue = fieldType.CreateDefaultValue() + if !slices.Contains(fieldType.EnumValues, defaultValue) { + return nil, fmt.Errorf("configuration error, invalid default value") + } } if err := validateNullable(fieldType.Nullable, value); err != nil { @@ -223,7 +398,11 @@ func (fieldType EnumFieldType) ValidateValue(value any) (any, error) { } if value == nil { - return fieldType.DefaultValue, nil + if len(defaultValue) > 0 { + return defaultValue, nil + } + + return nil, nil } str, ok := value.(string) @@ -234,23 +413,28 @@ func (fieldType EnumFieldType) ValidateValue(value any) (any, error) { return str, nil } -type SingleRelationFieldType struct { - Nullable bool - Collection string +type FieldTypeSingleRelation struct { + Nullable bool + Collection string + CascadeDelete bool } -func (fieldType SingleRelationFieldType) ValidateValue(value any) (any, error) { - if err := validateNullable(fieldType.Nullable, value); err != nil { - return nil, err - } +func (ft FieldTypeSingleRelation) Clone() FieldType { + return FieldType(ft) +} - if value == nil { - return nil, nil - } +func (fieldType FieldTypeSingleRelation) ValidateValue(value any) (any, error) { + idType := FieldTypeId{Nullable: fieldType.Nullable} + return idType.ValidateValue(value) +} - if err := ValidateId(value); err != nil { - return nil, err - } +type View struct { + // collection name on last migration; empty for newly created collections; + // useful for detecting when a collection has been renamed + originalName string - return value, nil + Name string + Schema ViewSchema } + +type ViewSchema struct{}