Skip to content

Commit

Permalink
Merge pull request #1 from evantbyrne/v0.2.0
Browse files Browse the repository at this point in the history
V0.2.0
  • Loading branch information
evantbyrne authored Aug 21, 2023
2 parents bd7d070 + c5616c1 commit ba4ec54
Show file tree
Hide file tree
Showing 14 changed files with 1,109 additions and 92 deletions.
82 changes: 43 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ The retro Golang ORM. **R**etro **E**ntity **M**apper.
```go
type Accounts struct {
Group rem.NullForeignKey[Groups] `db:"group_id"`
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name"`
}

type Groups struct {
Accounts rem.OneToMany[Accounts] `related_column:"group_id"`
Id int64 `db:"id" primary_key:"true"`
Accounts rem.OneToMany[Accounts] `db:"group_id"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name" db_max_length:"100"`
}
```
Expand Down Expand Up @@ -72,15 +72,17 @@ import (
// Choose one:
"github.com/evantbyrne/rem/mysqldialect"
"github.com/evantbyrne/rem/pqdialect"
"github.com/evantbyrne/rem/sqlitedialect"

// Don't forget to import your database driver.
)
```

```go
// Choose one:
rem.SetDialect(mysqldialect.Dialect{})
rem.SetDialect(pqdialect.Dialect{})
rem.SetDialect(mysqldialect.MysqlDialect{})
rem.SetDialect(pqdialect.PqDialect{})
rem.SetDialect(sqlitedialect.SqliteDialect{})

// Then connect to your database as usual.
db, err := sql.Open("<driver>", "<connection string>")
Expand All @@ -97,7 +99,7 @@ Models are structs that define table schemas.

```go
type Accounts struct {
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name" db_max_length:"100"`
Junk string
}
Expand Down Expand Up @@ -145,7 +147,7 @@ type Migration0001Accounts struct{}
func (m Migration0001Accounts) Up(db *sql.DB) error {
// We embed the Accounts model to avoid colliding with the package-level Accounts model used for queries. You could also use `rem.Config` as demonstrated in the Models documentation section.
type Accounts struct {
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name" db_max_length:"100"`
}

Expand Down Expand Up @@ -185,24 +187,26 @@ REM determines column types based on Go field types. The following table shows t

**Note:** REM uses special Go types for nullable columns. Don't use pointers for model fields.

Go | MySQL | PostgreSQL
--- | --- | ---
`bool` | `BOOLEAN` | `BOOLEAN`
`[]byte` | - | -
`int8` | `TINYINT` | `SMALLINT`
`int16` | `SMALLINT` | `SMALLINT`
`int32` | `INTEGER` | `INTEGER`
`int64` | `BIGINT` | `BIGINT`
`float32` | `FLOAT` | -
`float64` | `DOUBLE` | `DOUBLE PRECISION`
`string` | `VARCHAR`,`TEXT`\[1\] | `VARCHAR`,`TEXT`\[1\]
`time.Time` | `DATETIME`\[2\] | `TIMESTAMP`\[3\]
Go | MySQL | PostgreSQL | SQLite
--- | --- | --- | ---
`bool` | `BOOLEAN` | `BOOLEAN` | `BOOLEAN`\[1\]
`[]byte` | - | - | -
`int8` | `TINYINT` | `SMALLINT` | `INTEGER`
`int16` | `SMALLINT` | `SMALLINT` | `INTEGER`
`int32` | `INTEGER` | `INTEGER` | `INTEGER`
`int64` | `BIGINT` | `BIGINT` | `INTEGER`
`float32` | `FLOAT` | - | `REAL`
`float64` | `DOUBLE` | `DOUBLE PRECISION` | `REAL`
`string` | `VARCHAR`,`TEXT`\[2\] | `VARCHAR`,`TEXT`\[2\] | `TEXT`
`time.Time` | `DATETIME`\[3\] | `TIMESTAMP`\[4\] | `DATETIME`

\[1\] The `VARCHAR` column type is used for `string` and `sql.NullString` fields when the `db_max_length` field tag is provided. Otherwise, `TEXT` is used.
\[1\] SQLite `BOOLEAN` behaves as an `INTEGER` internally. The SQLite driver should automatically convert `bool` field values to `0` or `1` when parameterized.

\[2\] Go's most popular MySQL driver requires adding the `parseTime=true` GET parameter to the connection string to properly scan into `time.Time` and `sql.NullTime` fields.
\[2\] The `VARCHAR` column type is used for `string` and `sql.NullString` fields when the `db_max_length` field tag is provided. Otherwise, `TEXT` is used.

\[3\] The PostgreSQL dialect defaults to `WITHOUT TIME ZONE` for time types. Add the `db_time_zone:"true"` field tag to use `WITH TIME ZONE` instead.
\[3\] Go's most popular MySQL driver requires adding the `parseTime=true` GET parameter to the connection string to properly scan into `time.Time` and `sql.NullTime` fields.

\[4\] The PostgreSQL dialect defaults to `WITHOUT TIME ZONE` for time types. Add the `db_time_zone:"true"` field tag to use `WITH TIME ZONE` instead.

Columns are not nullable by default. REM uses the standard `database/sql` package types to represent nullable columns.

Expand All @@ -217,17 +221,17 @@ Not Null | Nullable
`string` | `sql.NullString`
`time.Time` | `sql.NullTime`

Primary keys are specified with the `primary_key:"true"` field tag. All models must have a primary key. Integer fields that are primary keys will auto-increment.
Primary keys are specified with the `db_primary:"true"` field tag. All models must have a primary key. Integer fields that are primary keys will auto-increment.

```go
// An auto-incrementing primary key.
type A struct {
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
}

// VARCHAR primary key with no default value.
type B struct {
Guid string `db:"guid" db_max_length:"36" primary_key:"true"`
Guid string `db:"guid" db_max_length:"36" db_primary:"true"`
}
```

Expand Down Expand Up @@ -265,13 +269,13 @@ Custom column types can be set using the `db_type` field tag, which accpets any
```go
// An example of using PostgreSQL's JSONB type.
type A struct {
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Data []byte `db:"data" db_type:"JSONB NOT NULL"`
}

// db_type takes priority over all other field tags, including primary key typing.
type B struct {
Guid string `db:"guid" db_type:"CHAR(36) NOT NULL" primary_key:"true"`
Guid string `db:"guid" db_type:"CHAR(36) NOT NULL" db_primary:"true"`
}
```

Expand All @@ -281,17 +285,17 @@ Custom Go types may also be used for model fields, but they must implement the `

Foreign keys are specified with the `rem.ForeignKey[To]` and `rem.NullForeignKey[To]` field types. REM automatically matches the foreign key column type to the primary key of the target model.

On the other end of the relation, use `rem.OneToMany[To]`.
On the other side of the relation, use `rem.OneToMany[To]`. On both sides of the relation, the `db` field tag signifies the column on the `rem.ForeignKey[To]` side.

```go
type Groups struct {
Members rem.OneToMany[Members] `related_column:"group_id"`
Id int64 `db:"id" primary_key:"true"`
Members rem.OneToMany[Members] `db:"group_id"`
Id int64 `db:"id" db_primary:"true"`
}

type Members struct {
Group rem.ForeignKey[Groups] `db:"group_id"`
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
}
```

Expand Down Expand Up @@ -391,13 +395,13 @@ Regardless of which side of the relationship you start from or how many records
// Model definitions for Groups <->> Accounts relationship.
type Accounts struct {
Group rem.ForeignKey[Groups] `db:"group_id"`
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name" db_max_length:"100"`
}

type Groups struct {
Accounts rem.OneToMany[Accounts] `related_column:"group_id"`
Id int64 `db:"id" primary_key:"true"`
Accounts rem.OneToMany[Accounts] `db:"group_id"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name" db_max_length:"100"`
}
```
Expand Down Expand Up @@ -580,8 +584,8 @@ rows, err := rem.Use[Accounts]().

// Use a custom model.
type AccountsWithGroupName struct {
GroupName string `db:"group_name"`
Id string `db:"id" primary_key:"true"`
GroupName string `db:"group_name"`
Id string `db:"id" db_primary:"true"`
Name string `db:"name"`
}

Expand Down Expand Up @@ -677,9 +681,9 @@ The `TableColumnAdd` method adds a column to a table. A field must exist in the

```go
type Accounts struct {
Id int64 `db:"id" primary_key:"true"`
Name string `db:"name"`
IsAdmin bool `db:"is_admin"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name"`
IsAdmin bool `db:"is_admin"`
}

_, err := rem.Use[Accounts]().TableColumnAdd(db, "is_admin")
Expand Down
6 changes: 0 additions & 6 deletions filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,6 @@ func (filter FilterClause) rightString(dialect Dialect, args []interface{}) ([]i
case nil:
return args, "NULL", nil

case bool:
if right {
return args, "TRUE", nil
}
return args, "FALSE", nil

case []interface{}:
var sliceArgs strings.Builder
for j, arg := range right {
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/evantbyrne/rem

go 1.20
go 1.21

require golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1

Expand Down
2 changes: 1 addition & 1 deletion migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type Migration interface {
type MigrationLogs struct {
CreatedAt time.Time `db:"created_at"`
Direction string `db:"direction" db_max_length:"10"`
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
MigrationType string `db:"migration_type" db_max_length:"255"`
}

Expand Down
18 changes: 10 additions & 8 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func (model *Model[T]) ScanMap(data map[string]interface{}) (*T, error) {
for _, field := range model.Fields {
if strings.HasPrefix(field.Type.String(), "rem.OneToMany[") {
oneToMany := value.FieldByName(field.Name)
oneToMany.FieldByName("RelatedColumn").SetString(field.Tag.Get("related_column"))
oneToMany.FieldByName("RelatedColumn").SetString(field.Tag.Get("db"))
oneToMany.FieldByName("RowPk").Set(value.FieldByName(model.PrimaryField))
}
}
Expand Down Expand Up @@ -289,7 +289,7 @@ func (model *Model[T]) ToMap(row *T) (map[string]interface{}, error) {
field := value.FieldByName(fieldName)

// Skip zero valued primary keys.
if field.IsZero() && model.Fields[column].Tag.Get("primary_key") == "true" {
if field.IsZero() && model.Fields[column].Tag.Get("db_primary") == "true" {
continue
}

Expand Down Expand Up @@ -375,13 +375,15 @@ func Use[T any](configs ...Config) *Model[T] {

for _, field := range reflect.VisibleFields(modelType) {
if column, ok := field.Tag.Lookup("db"); ok {
fields[column] = field
if field.Tag.Get("primary_key") == "true" {
primaryColumn = column
primaryField = field.Name
if strings.HasPrefix(field.Type.String(), "rem.OneToMany[") {
fields[field.Name] = field
} else {
fields[column] = field
if field.Tag.Get("db_primary") == "true" {
primaryColumn = column
primaryField = field.Name
}
}
} else if strings.HasPrefix(field.Type.String(), "rem.OneToMany[") {
fields[field.Name] = field
}
}

Expand Down
20 changes: 10 additions & 10 deletions model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import (

func TestModelScanMap(t *testing.T) {
type testGroups struct {
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name" db_max_length:"100"`
}
type testAccounts struct {
EditedAt sql.NullTime `db:"edited_at"`
Group NullForeignKey[testGroups] `db:"group_id" db_on_delete:"SET NULL"`
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name"`
}
model := Use[testAccounts]()
Expand Down Expand Up @@ -83,14 +83,14 @@ func TestModelScanMap(t *testing.T) {
}

type testGroupsModelToMap struct {
Accounts OneToMany[testAccountsModelToMap] `related_column:"group_id"`
Id int64 `db:"id" primary_key:"true"`
Accounts OneToMany[testAccountsModelToMap] `db:"group_id"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name" db_max_length:"100"`
}
type testAccountsModelToMap struct {
EditedAt sql.NullTime `db:"edited_at"`
Group NullForeignKey[testGroupsModelToMap] `db:"group_id" db_on_delete:"SET NULL"`
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name"`
}

Expand Down Expand Up @@ -187,7 +187,7 @@ func TestRegister(t *testing.T) {
registeredModels = make(map[string]interface{})
}()
type testModel struct {
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name"`
}
m1 := Use[testModel]()
Expand All @@ -208,7 +208,7 @@ func TestRegister(t *testing.T) {
func TestScanToMap(t *testing.T) {
type testAccounts struct {
EditedAt sql.NullTime `db:"edited_at"`
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name"`
}
accounts := Use[testAccounts]()
Expand Down Expand Up @@ -261,13 +261,13 @@ func TestScanToMap(t *testing.T) {
func TestUse(t *testing.T) {
type testAccounts struct {
EditedAt sql.NullTime `db:"edited_at"`
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name"`
}
type testGroups struct {
Id int64 `db:"id" primary_key:"true"`
Id int64 `db:"id" db_primary:"true"`
Name string `db:"name" db_max_length:"100"`
Accounts OneToMany[testAccounts] `related_column:"group_id"`
Accounts OneToMany[testAccounts] `db:"group_id"`
}
groups := Use[testGroups]()
columns := maps.Keys(groups.Fields)
Expand Down
2 changes: 1 addition & 1 deletion mysqldialect/mysqldialect.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ func (dialect MysqlDialect) ColumnType(field reflect.StructField) (string, error
var columnPrimary string
var columnType string

if field.Tag.Get("primary_key") == "true" {
if field.Tag.Get("db_primary") == "true" {
columnPrimary = " PRIMARY KEY"

switch fieldInstance.(type) {
Expand Down
Loading

0 comments on commit ba4ec54

Please sign in to comment.