From 2101088d0e70db844141cbdc5c12b9ebd1e383fc Mon Sep 17 00:00:00 2001 From: go-jet Date: Fri, 6 May 2022 11:54:44 +0200 Subject: [PATCH] Add support for EXTRACT time/date function. --- internal/jet/expression.go | 19 ++++++++++ internal/jet/func_expression.go | 5 +++ internal/jet/serializer.go | 7 ++++ internal/testutils/test_utils.go | 2 +- mysql/functions.go | 6 +++ mysql/interval.go | 9 +++-- postgres/functions.go | 24 +++++++++++- postgres/interval_expression.go | 26 ++++++++++++- tests/Makefile | 8 +++- tests/init/init.go | 1 + tests/mysql/alltypes_test.go | 59 +++++++++++++++++++++++++++++ tests/postgres/alltypes_test.go | 64 +++++++++++++++++++++++++++++++- tests/postgres/insert_test.go | 2 +- tests/postgres/update_test.go | 2 +- tests/testdata | 2 +- 15 files changed, 222 insertions(+), 14 deletions(-) diff --git a/internal/jet/expression.go b/internal/jet/expression.go index 0fe78df..436b9d6 100644 --- a/internal/jet/expression.go +++ b/internal/jet/expression.go @@ -263,6 +263,25 @@ func (p *betweenOperatorExpression) serialize(statement StatementType, out *SQLB p.max.serialize(statement, out, FallTrough(options)...) } +type customExpression struct { + ExpressionInterfaceImpl + parts []Serializer +} + +func newCustomExpression(parts ...Serializer) Expression { + ret := customExpression{ + parts: parts, + } + ret.ExpressionInterfaceImpl.Parent = &ret + return &ret +} + +func (c *customExpression) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { + for _, expression := range c.parts { + expression.serialize(statement, out, options...) + } +} + type complexExpression struct { ExpressionInterfaceImpl expressions Expression diff --git a/internal/jet/func_expression.go b/internal/jet/func_expression.go index cfac71f..6900457 100644 --- a/internal/jet/func_expression.go +++ b/internal/jet/func_expression.go @@ -492,6 +492,11 @@ func TO_TIMESTAMP(timestampzStr, format StringExpression) TimestampzExpression { //----------------- Date/Time Functions and Operators ---------------// +// EXTRACT extracts time component from time expression +func EXTRACT(field string, from Expression) Expression { + return newCustomExpression(Token("EXTRACT("), Token(field), Token("FROM"), from, Token(")")) +} + // CURRENT_DATE returns current date func CURRENT_DATE() DateExpression { dateFunc := NewDateFunc("CURRENT_DATE") diff --git a/internal/jet/serializer.go b/internal/jet/serializer.go index 866d60e..93a1d3b 100644 --- a/internal/jet/serializer.go +++ b/internal/jet/serializer.go @@ -96,3 +96,10 @@ func (s serializerImpl) serialize(statement StatementType, out *SQLBuilder, opti clause.Serialize(statement, out, FallTrough(options)...) } } + +// Token can be used to construct complex custom expressions +type Token string + +func (t Token) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { + out.WriteString(string(t)) +} diff --git a/internal/testutils/test_utils.go b/internal/testutils/test_utils.go index 7e6e21a..7d231e0 100644 --- a/internal/testutils/test_utils.go +++ b/internal/testutils/test_utils.go @@ -72,7 +72,7 @@ func AssertExecErr(t *testing.T, stmt jet.Statement, db qrm.DB, errorStr string) } // AssertExecContextErr assert statement execution for failed execution with error string errorStr -func AssertExecContextErr(t *testing.T, stmt jet.Statement, ctx context.Context, db qrm.DB, errorStr string) { +func AssertExecContextErr(ctx context.Context, t *testing.T, stmt jet.Statement, db qrm.DB, errorStr string) { _, err := stmt.ExecContext(ctx, db) require.Error(t, err, errorStr) diff --git a/mysql/functions.go b/mysql/functions.go index b794ef7..2a8e227 100644 --- a/mysql/functions.go +++ b/mysql/functions.go @@ -224,6 +224,12 @@ var REGEXP_LIKE = jet.REGEXP_LIKE //----------------- Date/Time Functions and Operators ------------// +// EXTRACT function retrieves subfields such as year or hour from date/time values +// EXTRACT(DAY, User.CreatedAt) +func EXTRACT(field unitType, from Expression) IntegerExpression { + return IntExp(jet.EXTRACT(string(field), from)) +} + // CURRENT_DATE returns current date var CURRENT_DATE = jet.CURRENT_DATE diff --git a/mysql/interval.go b/mysql/interval.go index c563855..ea24632 100644 --- a/mysql/interval.go +++ b/mysql/interval.go @@ -39,10 +39,11 @@ const ( type Interval = jet.Interval // INTERVAL creates new temporal interval. -// In a case of MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR unit type -// value parameter should be number. For example: INTERVAL(1, DAY) -// In a case of other unit types, value should be string with appropriate format. -// For example: INTERVAL("10:08:50", HOUR_SECOND) +// In a case of MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR unit type +// value parameter has to be a number. +// INTERVAL(1, DAY) +// In a case of other unit types, value should be string with appropriate format. +// INTERVAL("10:08:50", HOUR_SECOND) func INTERVAL(value interface{}, unitType unitType) Interval { switch unitType { case MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR: diff --git a/postgres/functions.go b/postgres/functions.go index cd2c130..7cc0335 100644 --- a/postgres/functions.go +++ b/postgres/functions.go @@ -1,6 +1,8 @@ package postgres -import "github.com/go-jet/jet/v2/internal/jet" +import ( + "github.com/go-jet/jet/v2/internal/jet" +) // This functions can be used, instead of its method counterparts, to have a better indentation of a complex condition // in the Go code and in the generated SQL. @@ -279,6 +281,26 @@ var TO_TIMESTAMP = jet.TO_TIMESTAMP //----------------- Date/Time Functions and Operators ------------// +// Additional time unit types for EXTRACT function +const ( + DOW unit = MILLENNIUM + 1 + iota + DOY + EPOCH + ISODOW + ISOYEAR + JULIAN + QUARTER + TIMEZONE + TIMEZONE_HOUR + TIMEZONE_MINUTE +) + +// EXTRACT function retrieves subfields such as year or hour from date/time values +// EXTRACT(DAY, User.CreatedAt) +func EXTRACT(field unit, from Expression) FloatExpression { + return FloatExp(jet.EXTRACT(unitToString(field), from)) +} + // CURRENT_DATE returns current date var CURRENT_DATE = jet.CURRENT_DATE diff --git a/postgres/interval_expression.go b/postgres/interval_expression.go index 68d33bc..d1c4788 100644 --- a/postgres/interval_expression.go +++ b/postgres/interval_expression.go @@ -10,10 +10,11 @@ import ( ) type quantityAndUnit = float64 +type unit = float64 // Interval unit types const ( - YEAR quantityAndUnit = 123456789 + iota + YEAR unit = 123456789 + iota MONTH WEEK DAY @@ -119,7 +120,7 @@ type intervalExpression struct { } // INTERVAL creates new interval expression from the list of quantity-unit pairs. -// For example: INTERVAL(1, DAY, 3, MINUTE) +// INTERVAL(1, DAY, 3, MINUTE) func INTERVAL(quantityAndUnit ...quantityAndUnit) IntervalExpression { quantityAndUnitLen := len(quantityAndUnit) if quantityAndUnitLen == 0 || quantityAndUnitLen%2 != 0 { @@ -208,6 +209,27 @@ func unitToString(unit quantityAndUnit) string { return "CENTURY" case MILLENNIUM: return "MILLENNIUM" + // additional field units for EXTRACT function + case DOW: + return "DOW" + case DOY: + return "DOY" + case EPOCH: + return "EPOCH" + case ISODOW: + return "ISODOW" + case ISOYEAR: + return "ISOYEAR" + case JULIAN: + return "JULIAN" + case QUARTER: + return "QUARTER" + case TIMEZONE: + return "TIMEZONE" + case TIMEZONE_HOUR: + return "TIMEZONE_HOUR" + case TIMEZONE_MINUTE: + return "TIMEZONE_MINUTE" default: panic("jet: invalid INTERVAL unit type") } diff --git a/tests/Makefile b/tests/Makefile index 26d63cd..1a84778 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -32,8 +32,14 @@ init-sqlite: # jet-gen will call generator on each of the test databases to generate sql builder and model files need to run the tests. jet-gen-all: install-jet-gen jet-gen-postgres jet-gen-mysql jet-gen-mariadb jet-gen-sqlite +ifeq ($(OS),Windows_NT) + target := jet.exe +else + target := jet +endif + install-jet-gen: - go build -o ${GOPATH}/bin/jet ../cmd/jet/ + go build -o ${GOPATH}/bin/${target} ../cmd/jet/ jet-gen-postgres: jet -dsn=postgres://jet:jet@localhost:50901/jetdb?sslmode=disable -schema=dvds -path=./.gentestdata/ diff --git a/tests/init/init.go b/tests/init/init.go index 5b7273b..633457e 100644 --- a/tests/init/init.go +++ b/tests/init/init.go @@ -30,6 +30,7 @@ func init() { flag.Parse() } +// Database names const ( Postgres = "postgres" MySql = "mysql" diff --git a/tests/mysql/alltypes_test.go b/tests/mysql/alltypes_test.go index 85268c7..bf25751 100644 --- a/tests/mysql/alltypes_test.go +++ b/tests/mysql/alltypes_test.go @@ -539,6 +539,8 @@ func TestTimeExpressions(t *testing.T) { AllTypes.Time.ADD(INTERVAL(20, MINUTE)).SUB(INTERVAL(11, HOUR)), + EXTRACT(DAY_HOUR, AllTypes.Time), + CURRENT_TIME(), CURRENT_TIME(3), ) @@ -574,6 +576,7 @@ SELECT CAST('20:34:58' AS TIME), all_types.time - INTERVAL all_types.small_int MINUTE, all_types.time - INTERVAL 3 MINUTE, (all_types.time + INTERVAL 20 MINUTE) - INTERVAL 11 HOUR, + EXTRACT(DAY_HOUR FROM all_types.time), CURRENT_TIME, CURRENT_TIME(3) FROM test_sample.all_types; @@ -936,6 +939,62 @@ func TestINTERVAL(t *testing.T) { require.NoError(t, err) } +func TestTimeEXTRACT(t *testing.T) { + stmt := SELECT( + EXTRACT(MICROSECOND, TimeT(time.Now())), + EXTRACT(SECOND, AllTypes.Time), + EXTRACT(MINUTE, AllTypes.Timestamp), + EXTRACT(HOUR, AllTypes.Timestamp), + EXTRACT(DAY, AllTypes.Date), + EXTRACT(WEEK, AllTypes.Timestamp), + EXTRACT(MONTH, AllTypes.Timestamp.ADD(INTERVAL(1, DAY))), + EXTRACT(QUARTER, AllTypes.Timestamp), + EXTRACT(YEAR, AllTypes.Timestamp).EQ(Int(1189654)), + EXTRACT(SECOND_MICROSECOND, AllTypes.Time), + EXTRACT(MINUTE_MICROSECOND, AllTypes.DateTime), + EXTRACT(MINUTE_SECOND, AllTypes.Timestamp), + EXTRACT(HOUR_MICROSECOND, AllTypes.Timestamp), + EXTRACT(HOUR_SECOND, AllTypes.Timestamp), + EXTRACT(HOUR_MINUTE, AllTypes.Timestamp), + EXTRACT(DAY_MICROSECOND, AllTypes.Timestamp), + EXTRACT(DAY_SECOND, AllTypes.Timestamp), + EXTRACT(DAY_MINUTE, AllTypes.Timestamp), + EXTRACT(DAY_HOUR, AllTypes.Timestamp), + EXTRACT(YEAR_MONTH, AllTypes.Timestamp), + ).FROM( + AllTypes, + ) + + //fmt.Println(stmt.Sql()) + + testutils.AssertStatementSql(t, stmt, ` +SELECT EXTRACT(MICROSECOND FROM CAST(? AS TIME)), + EXTRACT(SECOND FROM all_types.time), + EXTRACT(MINUTE FROM all_types.timestamp), + EXTRACT(HOUR FROM all_types.timestamp), + EXTRACT(DAY FROM all_types.date), + EXTRACT(WEEK FROM all_types.timestamp), + EXTRACT(MONTH FROM all_types.timestamp + INTERVAL 1 DAY), + EXTRACT(QUARTER FROM all_types.timestamp), + EXTRACT(YEAR FROM all_types.timestamp) = ?, + EXTRACT(SECOND_MICROSECOND FROM all_types.time), + EXTRACT(MINUTE_MICROSECOND FROM all_types.date_time), + EXTRACT(MINUTE_SECOND FROM all_types.timestamp), + EXTRACT(HOUR_MICROSECOND FROM all_types.timestamp), + EXTRACT(HOUR_SECOND FROM all_types.timestamp), + EXTRACT(HOUR_MINUTE FROM all_types.timestamp), + EXTRACT(DAY_MICROSECOND FROM all_types.timestamp), + EXTRACT(DAY_SECOND FROM all_types.timestamp), + EXTRACT(DAY_MINUTE FROM all_types.timestamp), + EXTRACT(DAY_HOUR FROM all_types.timestamp), + EXTRACT(YEAR_MONTH FROM all_types.timestamp) +FROM test_sample.all_types; +`) + + err := stmt.Query(db, &struct{}{}) + require.NoError(t, err) +} + func TestAllTypesInsert(t *testing.T) { tx, err := db.Begin() require.NoError(t, err) diff --git a/tests/postgres/alltypes_test.go b/tests/postgres/alltypes_test.go index f1c82af..2a1e0e2 100644 --- a/tests/postgres/alltypes_test.go +++ b/tests/postgres/alltypes_test.go @@ -899,9 +899,9 @@ func TestTimeExpression(t *testing.T) { NOW(), ) - //fmt.Println(query.DebugSql()) + // fmt.Println(query.DebugSql()) - dest := []struct{}{} + var dest []struct{} err := query.Query(db, &dest) require.NoError(t, err) @@ -963,6 +963,66 @@ func TestInterval(t *testing.T) { requireLogged(t, stmt) } +func TestTimeEXTRACT(t *testing.T) { + stmt := SELECT( + EXTRACT(CENTURY, AllTypes.Timestampz), + EXTRACT(DAY, AllTypes.Timestamp), + EXTRACT(DECADE, AllTypes.Date), + EXTRACT(DOW, AllTypes.TimestampzPtr), + EXTRACT(DOY, DateT(time.Now())), + EXTRACT(EPOCH, TimestampT(time.Now())), + EXTRACT(HOUR, AllTypes.Time.ADD(INTERVAL(1, HOUR))), + EXTRACT(ISODOW, AllTypes.Timestampz), + EXTRACT(ISOYEAR, AllTypes.Timestampz), + EXTRACT(JULIAN, AllTypes.Timestampz).EQ(Float(3456.123)), + EXTRACT(MICROSECOND, AllTypes.Timestampz), + EXTRACT(MILLENNIUM, AllTypes.Timestampz), + EXTRACT(MILLISECOND, AllTypes.Timez), + EXTRACT(MINUTE, INTERVAL(1, HOUR, 2, MINUTE)), + EXTRACT(MONTH, AllTypes.Timestampz), + EXTRACT(QUARTER, AllTypes.Timestampz), + EXTRACT(SECOND, AllTypes.Timestampz), + EXTRACT(TIMEZONE, AllTypes.Timestampz), + EXTRACT(TIMEZONE_HOUR, AllTypes.Timestampz), + EXTRACT(TIMEZONE_MINUTE, AllTypes.Timestampz), + EXTRACT(WEEK, AllTypes.Timestampz), + EXTRACT(YEAR, AllTypes.Timestampz), + ).FROM( + AllTypes, + ) + + // fmt.Println(stmt.Sql()) + + testutils.AssertStatementSql(t, stmt, ` +SELECT EXTRACT(CENTURY FROM all_types.timestampz), + EXTRACT(DAY FROM all_types.timestamp), + EXTRACT(DECADE FROM all_types.date), + EXTRACT(DOW FROM all_types.timestampz_ptr), + EXTRACT(DOY FROM $1::date), + EXTRACT(EPOCH FROM $2::timestamp without time zone), + EXTRACT(HOUR FROM all_types.time + INTERVAL '1 HOUR'), + EXTRACT(ISODOW FROM all_types.timestampz), + EXTRACT(ISOYEAR FROM all_types.timestampz), + EXTRACT(JULIAN FROM all_types.timestampz) = $3, + EXTRACT(MICROSECOND FROM all_types.timestampz), + EXTRACT(MILLENNIUM FROM all_types.timestampz), + EXTRACT(MILLISECOND FROM all_types.timez), + EXTRACT(MINUTE FROM INTERVAL '1 HOUR 2 MINUTE'), + EXTRACT(MONTH FROM all_types.timestampz), + EXTRACT(QUARTER FROM all_types.timestampz), + EXTRACT(SECOND FROM all_types.timestampz), + EXTRACT(TIMEZONE FROM all_types.timestampz), + EXTRACT(TIMEZONE_HOUR FROM all_types.timestampz), + EXTRACT(TIMEZONE_MINUTE FROM all_types.timestampz), + EXTRACT(WEEK FROM all_types.timestampz), + EXTRACT(YEAR FROM all_types.timestampz) +FROM test_sample.all_types; +`) + + err := stmt.Query(db, &struct{}{}) + require.NoError(t, err) +} + func TestSubQueryColumnReference(t *testing.T) { type expected struct { sql string diff --git a/tests/postgres/insert_test.go b/tests/postgres/insert_test.go index 1274869..e34405e 100644 --- a/tests/postgres/insert_test.go +++ b/tests/postgres/insert_test.go @@ -380,6 +380,6 @@ func TestInsertWithExecContext(t *testing.T) { time.Sleep(10 * time.Millisecond) testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) { - testutils.AssertExecContextErr(t, stmt, ctx, tx, "context deadline exceeded") + testutils.AssertExecContextErr(ctx, t, stmt, tx, "context deadline exceeded") }) } diff --git a/tests/postgres/update_test.go b/tests/postgres/update_test.go index 975b684..e0c7da2 100644 --- a/tests/postgres/update_test.go +++ b/tests/postgres/update_test.go @@ -344,7 +344,7 @@ func TestUpdateExecContext(t *testing.T) { time.Sleep(10 * time.Millisecond) - testutils.AssertExecContextErr(t, updateStmt, ctx, db, "context deadline exceeded") + testutils.AssertExecContextErr(ctx, t, updateStmt, db, "context deadline exceeded") } func TestUpdateFrom(t *testing.T) { diff --git a/tests/testdata b/tests/testdata index 895bf57..3895a98 160000 --- a/tests/testdata +++ b/tests/testdata @@ -1 +1 @@ -Subproject commit 895bf5760d055c717df77c3b872af276f34d06f1 +Subproject commit 3895a98c275c11840213e05e8d83cde80eeb0a0c