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") 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) fmt.Println("Connecting to mariadb on url: ", databaseUrl)

View File

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

View File

@ -11,6 +11,8 @@ var modifiers sync.Map
func init() { func init() {
// These are the builtin modifiers // These are the builtin modifiers
modifiers.Store("json", jsonModifier) modifiers.Store("json", jsonModifier)
modifiers.Store("timeNowUTC", timeNowUTCModifier)
modifiers.Store("timeNowUTC/skipUpdates", timeNowUTCSkipUpdatesModifier)
} }
// RegisterAttrModifier allow users to add custom modifiers on startup // 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 { if err != nil {
return "", nil, err return "", nil, err
} }
for key := range recordMap {
if info.ByName(key).Modifier.SkipOnUpdate {
delete(recordMap, key)
}
}
numAttrs := len(recordMap) numAttrs := len(recordMap)
args = make([]interface{}, numAttrs) args = make([]interface{}, numAttrs)
numNonIDArgs := numAttrs - len(idFieldNames) numNonIDArgs := numAttrs - len(idFieldNames)

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"testing" "testing"
"time"
"github.com/vingarcia/ksql/internal/modifiers" "github.com/vingarcia/ksql/internal/modifiers"
tt "github.com/vingarcia/ksql/internal/testtools" tt "github.com/vingarcia/ksql/internal/testtools"
@ -71,6 +72,7 @@ func RunTestsForAdapter(
PatchTest(t, driver, connStr, newDBAdapter) PatchTest(t, driver, connStr, newDBAdapter)
QueryChunksTest(t, driver, connStr, newDBAdapter) QueryChunksTest(t, driver, connStr, newDBAdapter)
TransactionTest(t, driver, connStr, newDBAdapter) TransactionTest(t, driver, connStr, newDBAdapter)
ModifiersTest(t, driver, connStr, newDBAdapter)
ScanRowsTest(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 // ScanRowsTest runs all tests for making sure the ScanRows feature is
// working for a given adapter and driver. // working for a given adapter and driver.
func ScanRowsTest( func ScanRowsTest(
@ -2668,28 +2904,36 @@ func createTables(driver string, connStr string) error {
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
age INTEGER, age INTEGER,
name TEXT, name TEXT,
address BLOB address BLOB,
created_at DATETIME,
updated_at DATETIME
)`) )`)
case "postgres": case "postgres":
_, err = db.Exec(`CREATE TABLE users ( _, err = db.Exec(`CREATE TABLE users (
id serial PRIMARY KEY, id serial PRIMARY KEY,
age INT, age INT,
name VARCHAR(50), name VARCHAR(50),
address jsonb address jsonb,
created_at TIMESTAMP,
updated_at TIMESTAMP
)`) )`)
case "mysql": case "mysql":
_, err = db.Exec(`CREATE TABLE users ( _, err = db.Exec(`CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
age INT, age INT,
name VARCHAR(50), name VARCHAR(50),
address JSON address JSON,
created_at DATETIME,
updated_at DATETIME
)`) )`)
case "sqlserver": case "sqlserver":
_, err = db.Exec(`CREATE TABLE users ( _, err = db.Exec(`CREATE TABLE users (
id INT IDENTITY(1,1) PRIMARY KEY, id INT IDENTITY(1,1) PRIMARY KEY,
age INT, age INT,
name VARCHAR(50), name VARCHAR(50),
address NVARCHAR(4000) address NVARCHAR(4000),
created_at DATETIME,
updated_at DATETIME
)`) )`)
} }
if err != nil { if err != nil {