diff --git a/adapters/kmysql/kmysql_test.go b/adapters/kmysql/kmysql_test.go index b8d19bb..6433cad 100644 --- a/adapters/kmysql/kmysql_test.go +++ b/adapters/kmysql/kmysql_test.go @@ -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) diff --git a/internal/modifiers/contract.go b/internal/modifiers/contract.go index 69356e6..e9aee51 100644 --- a/internal/modifiers/contract.go +++ b/internal/modifiers/contract.go @@ -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. diff --git a/internal/modifiers/global_modifiers.go b/internal/modifiers/global_modifiers.go index f904a1b..c27fb68 100644 --- a/internal/modifiers/global_modifiers.go +++ b/internal/modifiers/global_modifiers.go @@ -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 diff --git a/internal/modifiers/time_modifiers.go b/internal/modifiers/time_modifiers.go new file mode 100644 index 0000000..171060c --- /dev/null +++ b/internal/modifiers/time_modifiers.go @@ -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 + }, +} diff --git a/internal/testtools/time.go b/internal/testtools/time.go new file mode 100644 index 0000000..3e65663 --- /dev/null +++ b/internal/testtools/time.go @@ -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 +} diff --git a/ksql.go b/ksql.go index 4a9c4f0..7aec5f2 100644 --- a/ksql.go +++ b/ksql.go @@ -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) diff --git a/test_adapters.go b/test_adapters.go index 6cee443..ca52fe3 100644 --- a/test_adapters.go +++ b/test_adapters.go @@ -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 {