Add support for EXTRACT time/date function.

This commit is contained in:
go-jet 2022-05-06 11:54:44 +02:00
parent bc776f947b
commit 2101088d0e
15 changed files with 222 additions and 14 deletions

View file

@ -263,6 +263,25 @@ func (p *betweenOperatorExpression) serialize(statement StatementType, out *SQLB
p.max.serialize(statement, out, FallTrough(options)...) 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 { type complexExpression struct {
ExpressionInterfaceImpl ExpressionInterfaceImpl
expressions Expression expressions Expression

View file

@ -492,6 +492,11 @@ func TO_TIMESTAMP(timestampzStr, format StringExpression) TimestampzExpression {
//----------------- Date/Time Functions and Operators ---------------// //----------------- 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 // CURRENT_DATE returns current date
func CURRENT_DATE() DateExpression { func CURRENT_DATE() DateExpression {
dateFunc := NewDateFunc("CURRENT_DATE") dateFunc := NewDateFunc("CURRENT_DATE")

View file

@ -96,3 +96,10 @@ func (s serializerImpl) serialize(statement StatementType, out *SQLBuilder, opti
clause.Serialize(statement, out, FallTrough(options)...) 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))
}

View file

@ -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 // 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) _, err := stmt.ExecContext(ctx, db)
require.Error(t, err, errorStr) require.Error(t, err, errorStr)

View file

@ -224,6 +224,12 @@ var REGEXP_LIKE = jet.REGEXP_LIKE
//----------------- Date/Time Functions and Operators ------------// //----------------- 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 // CURRENT_DATE returns current date
var CURRENT_DATE = jet.CURRENT_DATE var CURRENT_DATE = jet.CURRENT_DATE

View file

@ -39,10 +39,11 @@ const (
type Interval = jet.Interval type Interval = jet.Interval
// INTERVAL creates new temporal interval. // INTERVAL creates new temporal interval.
// In a case of MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR unit type // 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) // value parameter has to be a number.
// In a case of other unit types, value should be string with appropriate format. // INTERVAL(1, DAY)
// For example: INTERVAL("10:08:50", HOUR_SECOND) // 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 { func INTERVAL(value interface{}, unitType unitType) Interval {
switch unitType { switch unitType {
case MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR: case MICROSECOND, SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR:

View file

@ -1,6 +1,8 @@
package postgres 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 // 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. // in the Go code and in the generated SQL.
@ -279,6 +281,26 @@ var TO_TIMESTAMP = jet.TO_TIMESTAMP
//----------------- Date/Time Functions and Operators ------------// //----------------- 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 // CURRENT_DATE returns current date
var CURRENT_DATE = jet.CURRENT_DATE var CURRENT_DATE = jet.CURRENT_DATE

View file

@ -10,10 +10,11 @@ import (
) )
type quantityAndUnit = float64 type quantityAndUnit = float64
type unit = float64
// Interval unit types // Interval unit types
const ( const (
YEAR quantityAndUnit = 123456789 + iota YEAR unit = 123456789 + iota
MONTH MONTH
WEEK WEEK
DAY DAY
@ -119,7 +120,7 @@ type intervalExpression struct {
} }
// INTERVAL creates new interval expression from the list of quantity-unit pairs. // 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 { func INTERVAL(quantityAndUnit ...quantityAndUnit) IntervalExpression {
quantityAndUnitLen := len(quantityAndUnit) quantityAndUnitLen := len(quantityAndUnit)
if quantityAndUnitLen == 0 || quantityAndUnitLen%2 != 0 { if quantityAndUnitLen == 0 || quantityAndUnitLen%2 != 0 {
@ -208,6 +209,27 @@ func unitToString(unit quantityAndUnit) string {
return "CENTURY" return "CENTURY"
case MILLENNIUM: case MILLENNIUM:
return "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: default:
panic("jet: invalid INTERVAL unit type") panic("jet: invalid INTERVAL unit type")
} }

View file

@ -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 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 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: install-jet-gen:
go build -o ${GOPATH}/bin/jet ../cmd/jet/ go build -o ${GOPATH}/bin/${target} ../cmd/jet/
jet-gen-postgres: jet-gen-postgres:
jet -dsn=postgres://jet:jet@localhost:50901/jetdb?sslmode=disable -schema=dvds -path=./.gentestdata/ jet -dsn=postgres://jet:jet@localhost:50901/jetdb?sslmode=disable -schema=dvds -path=./.gentestdata/

View file

@ -30,6 +30,7 @@ func init() {
flag.Parse() flag.Parse()
} }
// Database names
const ( const (
Postgres = "postgres" Postgres = "postgres"
MySql = "mysql" MySql = "mysql"

View file

@ -539,6 +539,8 @@ func TestTimeExpressions(t *testing.T) {
AllTypes.Time.ADD(INTERVAL(20, MINUTE)).SUB(INTERVAL(11, HOUR)), AllTypes.Time.ADD(INTERVAL(20, MINUTE)).SUB(INTERVAL(11, HOUR)),
EXTRACT(DAY_HOUR, AllTypes.Time),
CURRENT_TIME(), CURRENT_TIME(),
CURRENT_TIME(3), 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 all_types.small_int MINUTE,
all_types.time - INTERVAL 3 MINUTE, all_types.time - INTERVAL 3 MINUTE,
(all_types.time + INTERVAL 20 MINUTE) - INTERVAL 11 HOUR, (all_types.time + INTERVAL 20 MINUTE) - INTERVAL 11 HOUR,
EXTRACT(DAY_HOUR FROM all_types.time),
CURRENT_TIME, CURRENT_TIME,
CURRENT_TIME(3) CURRENT_TIME(3)
FROM test_sample.all_types; FROM test_sample.all_types;
@ -936,6 +939,62 @@ func TestINTERVAL(t *testing.T) {
require.NoError(t, err) 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) { func TestAllTypesInsert(t *testing.T) {
tx, err := db.Begin() tx, err := db.Begin()
require.NoError(t, err) require.NoError(t, err)

View file

@ -899,9 +899,9 @@ func TestTimeExpression(t *testing.T) {
NOW(), NOW(),
) )
//fmt.Println(query.DebugSql()) // fmt.Println(query.DebugSql())
dest := []struct{}{} var dest []struct{}
err := query.Query(db, &dest) err := query.Query(db, &dest)
require.NoError(t, err) require.NoError(t, err)
@ -963,6 +963,66 @@ func TestInterval(t *testing.T) {
requireLogged(t, stmt) 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) { func TestSubQueryColumnReference(t *testing.T) {
type expected struct { type expected struct {
sql string sql string

View file

@ -380,6 +380,6 @@ func TestInsertWithExecContext(t *testing.T) {
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) { 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")
}) })
} }

View file

@ -344,7 +344,7 @@ func TestUpdateExecContext(t *testing.T) {
time.Sleep(10 * time.Millisecond) 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) { func TestUpdateFrom(t *testing.T) {

@ -1 +1 @@
Subproject commit 895bf5760d055c717df77c3b872af276f34d06f1 Subproject commit 3895a98c275c11840213e05e8d83cde80eeb0a0c