Add timeNowUTC and timeNowUTC/skipUpdates modifiers

pull/29/head
Vinícius Garcia 2022-10-09 21:49:21 -03:00
parent ae76cd5768
commit 57c0f4cade
7 changed files with 295 additions and 7 deletions

View File

@ -54,7 +54,7 @@ func startMySQLDB(dbName string) (databaseURL string, closer func()) {
}
hostAndPort := resource.GetHostPort("3306/tcp")
databaseUrl := fmt.Sprintf("root:mysql@(%s)/%s?timeout=30s", hostAndPort, dbName)
databaseUrl := fmt.Sprintf("root:mysql@(%s)/%s?timeout=30s&parseTime=true", hostAndPort, dbName)
fmt.Println("Connecting to mariadb on url: ", databaseUrl)

View File

@ -9,9 +9,11 @@ type AttrModifier struct {
// The following attributes will tell KSQL to
// leave this attribute out of insertions, updates,
// and queries respectively.
SkipOnInsert bool
//
// (the private ones are not implemented yet)
skipOnInsert bool
SkipOnUpdate bool
SkipOnQuery bool
skipOnQuery bool
// Implement these functions if you want to override the default Scan/Value behavior
// for the target attribute.

View File

@ -11,6 +11,8 @@ var modifiers sync.Map
func init() {
// These are the builtin modifiers
modifiers.Store("json", jsonModifier)
modifiers.Store("timeNowUTC", timeNowUTCModifier)
modifiers.Store("timeNowUTC/skipUpdates", timeNowUTCSkipUpdatesModifier)
}
// RegisterAttrModifier allow users to add custom modifiers on startup

View File

@ -0,0 +1,22 @@
package modifiers
import (
"context"
"time"
)
// This one is useful for updatedAt timestamps
var timeNowUTCModifier = AttrModifier{
Value: func(ctx context.Context, opInfo OpInfo, inputValue interface{}) (outputValue interface{}, _ error) {
return time.Now().UTC(), nil
},
}
// This one is useful for createdAt timestamps
var timeNowUTCSkipUpdatesModifier = AttrModifier{
SkipOnUpdate: true,
Value: func(ctx context.Context, opInfo OpInfo, inputValue interface{}) (outputValue interface{}, _ error) {
return time.Now().UTC(), nil
},
}

View File

@ -0,0 +1,12 @@
package tt
import (
"testing"
"time"
)
func ParseTime(t *testing.T, timestr string) time.Time {
parsedTime, err := time.Parse(time.RFC3339, timestr)
AssertNoErr(t, err)
return parsedTime
}

View File

@ -796,6 +796,12 @@ func buildUpdateQuery(
if err != nil {
return "", nil, err
}
for key := range recordMap {
if info.ByName(key).Modifier.SkipOnUpdate {
delete(recordMap, key)
}
}
numAttrs := len(recordMap)
args = make([]interface{}, numAttrs)
numNonIDArgs := numAttrs - len(idFieldNames)

View File

@ -7,6 +7,7 @@ import (
"fmt"
"io"
"testing"
"time"
"github.com/vingarcia/ksql/internal/modifiers"
tt "github.com/vingarcia/ksql/internal/testtools"
@ -71,6 +72,7 @@ func RunTestsForAdapter(
PatchTest(t, driver, connStr, newDBAdapter)
QueryChunksTest(t, driver, connStr, newDBAdapter)
TransactionTest(t, driver, connStr, newDBAdapter)
ModifiersTest(t, driver, connStr, newDBAdapter)
ScanRowsTest(t, driver, connStr, newDBAdapter)
})
}
@ -2516,6 +2518,240 @@ func TransactionTest(
})
}
func ModifiersTest(
t *testing.T,
driver string,
connStr string,
newDBAdapter func(t *testing.T) (DBAdapter, io.Closer),
) {
ctx := context.Background()
t.Run("Modifiers", func(t *testing.T) {
err := createTables(driver, connStr)
if err != nil {
t.Fatal("could not create test table!, reason:", err.Error())
}
t.Run("timeNowUTC modifier", func(t *testing.T) {
t.Run("should be set to time.Now().UTC() on insertion", func(t *testing.T) {
db, closer := newDBAdapter(t)
defer closer.Close()
c := newTestDB(db, driver)
type tsUser struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
UpdatedAt time.Time `ksql:"updated_at,timeNowUTC"`
}
u := tsUser{
Name: "Letícia",
}
err := c.Insert(ctx, usersTable, &u)
tt.AssertNoErr(t, err)
tt.AssertNotEqual(t, u.ID, 0)
var untaggedUser struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
UpdatedAt time.Time `ksql:"updated_at"`
}
err = c.QueryOne(ctx, &untaggedUser, `FROM users WHERE id = `+c.dialect.Placeholder(0), u.ID)
tt.AssertNoErr(t, err)
now := time.Now()
tt.AssertApproxTime(t,
2*time.Second, untaggedUser.UpdatedAt, now,
"updatedAt should be set to %v, but got: %v", now, untaggedUser.UpdatedAt,
)
})
t.Run("should be set to time.Now().UTC() on updates", func(t *testing.T) {
db, closer := newDBAdapter(t)
defer closer.Close()
c := newTestDB(db, driver)
type userWithNoTags struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
UpdatedAt time.Time `ksql:"updated_at"`
}
untaggedUser := userWithNoTags{
Name: "Laura Ribeiro",
// Any time different from now:
UpdatedAt: tt.ParseTime(t, "2000-08-05T14:00:00Z"),
}
err := c.Insert(ctx, usersTable, &untaggedUser)
tt.AssertNoErr(t, err)
tt.AssertNotEqual(t, untaggedUser.ID, 0)
type taggedUser struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
UpdatedAt time.Time `ksql:"updated_at,timeNowUTC"`
}
u := taggedUser{
ID: untaggedUser.ID,
Name: "Laurinha Ribeiro",
}
err = c.Patch(ctx, usersTable, u)
tt.AssertNoErr(t, err)
var untaggedUser2 userWithNoTags
err = c.QueryOne(ctx, &untaggedUser2, "FROM users WHERE id = "+c.dialect.Placeholder(0), u.ID)
tt.AssertNoErr(t, err)
tt.AssertNotEqual(t, untaggedUser2.ID, 0)
now := time.Now()
tt.AssertApproxTime(t,
2*time.Second, untaggedUser2.UpdatedAt, now,
"updatedAt should be set to %v, but got: %v", now, untaggedUser2.UpdatedAt,
)
})
t.Run("should not alter the value on queries", func(t *testing.T) {
db, closer := newDBAdapter(t)
defer closer.Close()
c := newTestDB(db, driver)
type userWithNoTags struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
UpdatedAt time.Time `ksql:"updated_at"`
}
untaggedUser := userWithNoTags{
Name: "Marta Ribeiro",
// Any time different from now:
UpdatedAt: tt.ParseTime(t, "2000-08-05T14:00:00Z"),
}
err := c.Insert(ctx, usersTable, &untaggedUser)
tt.AssertNoErr(t, err)
tt.AssertNotEqual(t, untaggedUser.ID, 0)
var taggedUser struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
UpdatedAt time.Time `ksql:"updated_at,timeNowUTC"`
}
err = c.QueryOne(ctx, &taggedUser, "FROM users WHERE id = "+c.dialect.Placeholder(0), untaggedUser.ID)
tt.AssertNoErr(t, err)
tt.AssertEqual(t, taggedUser.ID, untaggedUser.ID)
tt.AssertEqual(t, taggedUser.Name, "Marta Ribeiro")
tt.AssertEqual(t, taggedUser.UpdatedAt, tt.ParseTime(t, "2000-08-05T14:00:00Z"))
})
})
t.Run("timeNowUTC/skipUpdates modifier", func(t *testing.T) {
t.Run("should be set to time.Now().UTC() on insertion", func(t *testing.T) {
db, closer := newDBAdapter(t)
defer closer.Close()
c := newTestDB(db, driver)
type tsUser struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
CreatedAt time.Time `ksql:"created_at,timeNowUTC/skipUpdates"`
}
u := tsUser{
Name: "Letícia",
}
err := c.Insert(ctx, usersTable, &u)
tt.AssertNoErr(t, err)
tt.AssertNotEqual(t, u.ID, 0)
var untaggedUser struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
CreatedAt time.Time `ksql:"created_at"`
}
err = c.QueryOne(ctx, &untaggedUser, `FROM users WHERE id = `+c.dialect.Placeholder(0), u.ID)
tt.AssertNoErr(t, err)
now := time.Now()
tt.AssertApproxTime(t,
2*time.Second, untaggedUser.CreatedAt, now,
"updatedAt should be set to %v, but got: %v", now, untaggedUser.CreatedAt,
)
})
t.Run("should be ignored on updates", func(t *testing.T) {
db, closer := newDBAdapter(t)
defer closer.Close()
c := newTestDB(db, driver)
type userWithNoTags struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
CreatedAt time.Time `ksql:"created_at"`
}
untaggedUser := userWithNoTags{
Name: "Laura Ribeiro",
// Any time different from now:
CreatedAt: tt.ParseTime(t, "2000-08-05T14:00:00Z"),
}
err := c.Insert(ctx, usersTable, &untaggedUser)
tt.AssertNoErr(t, err)
tt.AssertNotEqual(t, untaggedUser.ID, 0)
type taggedUser struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
CreatedAt time.Time `ksql:"created_at,timeNowUTC/skipUpdates"`
}
u := taggedUser{
ID: untaggedUser.ID,
Name: "Laurinha Ribeiro",
// Some random time that should be ignored:
CreatedAt: tt.ParseTime(t, "1999-08-05T14:00:00Z"),
}
err = c.Patch(ctx, usersTable, u)
tt.AssertNoErr(t, err)
var untaggedUser2 userWithNoTags
err = c.QueryOne(ctx, &untaggedUser2, "FROM users WHERE id = "+c.dialect.Placeholder(0), u.ID)
tt.AssertNoErr(t, err)
tt.AssertEqual(t, untaggedUser2.CreatedAt, tt.ParseTime(t, "2000-08-05T14:00:00Z"))
})
t.Run("should not alter the value on queries", func(t *testing.T) {
db, closer := newDBAdapter(t)
defer closer.Close()
c := newTestDB(db, driver)
type userWithNoTags struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
CreatedAt time.Time `ksql:"created_at"`
}
untaggedUser := userWithNoTags{
Name: "Marta Ribeiro",
// Any time different from now:
CreatedAt: tt.ParseTime(t, "2000-08-05T14:00:00Z"),
}
err := c.Insert(ctx, usersTable, &untaggedUser)
tt.AssertNoErr(t, err)
tt.AssertNotEqual(t, untaggedUser.ID, 0)
var taggedUser struct {
ID uint `ksql:"id"`
Name string `ksql:"name"`
CreatedAt time.Time `ksql:"created_at,timeNowUTC/skipUpdates"`
}
err = c.QueryOne(ctx, &taggedUser, "FROM users WHERE id = "+c.dialect.Placeholder(0), untaggedUser.ID)
tt.AssertNoErr(t, err)
tt.AssertEqual(t, taggedUser.ID, untaggedUser.ID)
tt.AssertEqual(t, taggedUser.Name, "Marta Ribeiro")
tt.AssertEqual(t, taggedUser.CreatedAt, tt.ParseTime(t, "2000-08-05T14:00:00Z"))
})
})
})
}
// ScanRowsTest runs all tests for making sure the ScanRows feature is
// working for a given adapter and driver.
func ScanRowsTest(
@ -2668,28 +2904,36 @@ func createTables(driver string, connStr string) error {
id INTEGER PRIMARY KEY,
age INTEGER,
name TEXT,
address BLOB
address BLOB,
created_at DATETIME,
updated_at DATETIME
)`)
case "postgres":
_, err = db.Exec(`CREATE TABLE users (
id serial PRIMARY KEY,
age INT,
name VARCHAR(50),
address jsonb
address jsonb,
created_at TIMESTAMP,
updated_at TIMESTAMP
)`)
case "mysql":
_, err = db.Exec(`CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
age INT,
name VARCHAR(50),
address JSON
address JSON,
created_at DATETIME,
updated_at DATETIME
)`)
case "sqlserver":
_, err = db.Exec(`CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY,
age INT,
name VARCHAR(50),
address NVARCHAR(4000)
address NVARCHAR(4000),
created_at DATETIME,
updated_at DATETIME
)`)
}
if err != nil {