diff --git a/README.md b/README.md index 15fd887b..c84dd4e7 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ https://medium.com/@go.jet/jet-5f3667efa0cc To install Jet package, you need to install Go and set your Go workspace first. -[Go](https://golang.org/) **version 1.9+ is required** +[Go](https://golang.org/) **version 1.18+ is required** ### Installation diff --git a/generator/template/model_template.go b/generator/template/model_template.go index 47f0339f..f89ebd1b 100644 --- a/generator/template/model_template.go +++ b/generator/template/model_template.go @@ -5,6 +5,7 @@ import ( "github.com/go-jet/jet/v2/generator/metadata" "github.com/go-jet/jet/v2/internal/utils/dbidentifier" "github.com/google/uuid" + "github.com/jackc/pgtype" "path" "reflect" "strings" @@ -320,6 +321,18 @@ func toGoType(column metadata.Column) interface{} { return float64(0.0) case "uuid": return uuid.UUID{} + case "daterange": + return pgtype.Daterange{} + case "tsrange": + return pgtype.Tsrange{} + case "tstzrange": + return pgtype.Tstzrange{} + case "int4range": + return pgtype.Int4range{} + case "int8range": + return pgtype.Int8range{} + case "numrange": + return pgtype.Numrange{} default: fmt.Println("- [Model ] Unsupported sql column '" + column.Name + " " + column.DataType.Name + "', using string instead.") return "" diff --git a/generator/template/sql_builder_template.go b/generator/template/sql_builder_template.go index cf7e121d..fe7fba59 100644 --- a/generator/template/sql_builder_template.go +++ b/generator/template/sql_builder_template.go @@ -177,6 +177,18 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { case "real", "numeric", "decimal", "double precision", "float", "float4", "float8", "double": // MySQL return "Float" + case "daterange": + return "DateRange" + case "tsrange": + return "TimestampRange" + case "tstzrange": + return "TimestampzRange" + case "int4range": + return "Int4Range" + case "int8range": + return "Int8Range" + case "numrange": + return "NumericRange" default: fmt.Println("- [SQL Builder] Unsupported sql column '" + columnMetaData.Name + " " + columnMetaData.DataType.Name + "', using StringColumn instead.") return "String" diff --git a/go.mod b/go.mod index 16c19fad..27cae161 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/go-jet/jet/v2 -go 1.11 +go 1.18 require ( github.com/go-sql-driver/mysql v1.7.1 @@ -10,14 +10,34 @@ require ( github.com/mattn/go-sqlite3 v1.14.17 ) -// test dependencies require ( github.com/google/go-cmp v0.5.9 + github.com/jackc/pgtype v1.14.0 github.com/jackc/pgx/v4 v4.18.1 github.com/pkg/profile v1.7.0 github.com/shopspring/decimal v1.3.1 github.com/stretchr/testify v1.8.2 github.com/volatiletech/null/v8 v8.1.2 - golang.org/x/sync v0.3.0 gopkg.in/guregu/null.v4 v4.0.0 ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/fgprof v0.9.3 // indirect + github.com/friendsofgo/errors v0.9.2 // indirect + github.com/gofrs/uuid v4.0.0+incompatible // indirect + github.com/google/pprof v0.0.0-20211214055906-6f57359322fd // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.2 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/volatiletech/inflect v0.0.1 // indirect + github.com/volatiletech/randomize v0.0.1 // indirect + github.com/volatiletech/strmangle v0.0.1 // indirect + golang.org/x/crypto v0.6.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index deb45b83..2e794734 100644 --- a/go.sum +++ b/go.sum @@ -32,7 +32,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= @@ -54,7 +53,6 @@ github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5W github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= @@ -183,8 +181,6 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/internal/jet/column_types.go b/internal/jet/column_types.go index 4748b2c4..a7320615 100644 --- a/internal/jet/column_types.go +++ b/internal/jet/column_types.go @@ -358,3 +358,44 @@ func DateColumn(name string) ColumnDate { dateColumn.ColumnExpressionImpl = NewColumnImpl(name, "", dateColumn) return dateColumn } + +//------------------------------------------------------// + +// ColumnRange is interface for range columns which can be int range, string range +// timestamp range or date range. +type ColumnRange[T Expression] interface { + Range[T] + Column + + From(subQuery SelectTable) ColumnRange[T] + SET(rangeExp Range[T]) ColumnAssigment +} + +type rangeColumnImpl[T Expression] struct { + rangeInterfaceImpl[T] + ColumnExpressionImpl +} + +func (i *rangeColumnImpl[T]) From(subQuery SelectTable) ColumnRange[T] { + newRangeColumn := RangeColumn[T](i.name) + newRangeColumn.setTableName(i.tableName) + newRangeColumn.setSubQuery(subQuery) + + return newRangeColumn +} + +func (i *rangeColumnImpl[T]) SET(rangeExp Range[T]) ColumnAssigment { + return columnAssigmentImpl{ + column: i, + expression: rangeExp, + } +} + +// RangeColumn creates named range column. +func RangeColumn[T Expression](name string) ColumnRange[T] { + rangeColumn := &rangeColumnImpl[T]{} + rangeColumn.rangeInterfaceImpl.parent = rangeColumn + rangeColumn.ColumnExpressionImpl = NewColumnImpl(name, "", rangeColumn) + + return rangeColumn +} diff --git a/internal/jet/func_expression.go b/internal/jet/func_expression.go index 4ff3791d..c56053fa 100644 --- a/internal/jet/func_expression.go +++ b/internal/jet/func_expression.go @@ -468,6 +468,33 @@ func REGEXP_LIKE(stringExp StringExpression, pattern StringExpression, matchType return newBoolFunc("REGEXP_LIKE", stringExp, pattern) } +//----------Range Type Functions ----------------------// + +// LOWER_BOUND returns range expressions lower bound +func LOWER_BOUND[T Expression](rangeExpression Range[T]) T { + return rangeTypeCaster[T](rangeExpression, NewFunc("LOWER", []Expression{rangeExpression}, nil)) +} + +// UPPER_BOUND returns range expressions upper bound +func UPPER_BOUND[T Expression](rangeExpression Range[T]) T { + return rangeTypeCaster[T](rangeExpression, NewFunc("UPPER", []Expression{rangeExpression}, nil)) +} + +func rangeTypeCaster[T Expression](rangeExpression Range[T], exp Expression) T { + var i Expression + switch rangeExpression.(type) { + case Range[DateExpression]: + i = DateExp(exp) + case Range[IntegerExpression]: + i = IntExp(exp) + case Range[TimestampzExpression]: + i = TimestampzExp(exp) + case Range[TimestampExpression]: + i = TimestampExp(exp) + } + return i.(T) +} + //----------Data Type Formatting Functions ----------------------// // TO_CHAR converts expression to string with format @@ -843,3 +870,35 @@ func newTimestampzFunc(name string, expressions ...Expression) *timestampzFunc { func Func(name string, expressions ...Expression) Expression { return NewFunc(name, expressions, nil) } + +func NumRange(lowNum, highNum NumericExpression, bounds ...StringExpression) Range[NumericExpression] { + return RangeExp[NumericExpression](NewFunc("numrange", rangeFuncParamCombiner[NumericExpression](lowNum, highNum, bounds...), nil)) +} + +func Int4Range(lowNum, highNum IntegerExpression, bounds ...StringExpression) Range[IntegerExpression] { + return RangeExp[IntegerExpression](NewFunc("int4range", rangeFuncParamCombiner[IntegerExpression](lowNum, highNum, bounds...), nil)) +} + +func Int8Range(lowNum, highNum IntegerExpression, bounds ...StringExpression) Range[IntegerExpression] { + return RangeExp[IntegerExpression](NewFunc("int8range", rangeFuncParamCombiner[IntegerExpression](lowNum, highNum, bounds...), nil)) +} + +func TimestampRange(lowTs, highTs TimestampExpression, bounds ...StringExpression) Range[TimestampExpression] { + return RangeExp[TimestampExpression](NewFunc("tsrange", rangeFuncParamCombiner[TimestampExpression](lowTs, highTs, bounds...), nil)) +} + +func TimestampzRange(lowTs, highTs TimestampzExpression, bounds ...StringExpression) Range[TimestampzExpression] { + return RangeExp[TimestampzExpression](NewFunc("tstzrange", rangeFuncParamCombiner[TimestampzExpression](lowTs, highTs, bounds...), nil)) +} + +func DateRange(lowTs, highTs DateExpression, bounds ...StringExpression) Range[DateExpression] { + return RangeExp[DateExpression](NewFunc("daterange", rangeFuncParamCombiner[DateExpression](lowTs, highTs, bounds...), nil)) +} + +func rangeFuncParamCombiner[T Expression](low, high T, bounds ...StringExpression) []Expression { + exp := []Expression{low, high} + if len(bounds) != 0 { + exp = append(exp, bounds[0]) + } + return exp +} diff --git a/internal/jet/func_expression_test.go b/internal/jet/func_expression_test.go index 048ade29..ca32589d 100644 --- a/internal/jet/func_expression_test.go +++ b/internal/jet/func_expression_test.go @@ -202,3 +202,14 @@ func TestTO_ASCII(t *testing.T) { func TestFunc(t *testing.T) { assertClauseSerialize(t, Func("FOO", String("test"), NULL, MAX(Int(1))), "FOO($1, NULL, MAX($2))", "test", int64(1)) } + +func Test_rangePointCaster(t *testing.T) { + mainRange := Int8Range(Int8(10), Int8(12)) + exp := NewFunc("UPPER", []Expression{mainRange}, nil) + + got := rangeTypeCaster(mainRange, exp) + _, ok := got.(IntegerExpression) + if !ok { + t.Errorf("expecting to get IntegerExpression but got %v", got) + } +} diff --git a/internal/jet/literal_expression.go b/internal/jet/literal_expression.go index 8c28b446..d6f0b415 100644 --- a/internal/jet/literal_expression.go +++ b/internal/jet/literal_expression.go @@ -333,6 +333,10 @@ var ( NULL = newNullLiteral() // STAR is jet equivalent of SQL * STAR = newStarLiteral() + // PLUS_INFINITY is jet equivalent for sql infinity + PLUS_INFINITY = String("infinity") + // MINUS_INFINITY is jet equivalent for sql -infinity + MINUS_INFINITY = String("-infinity") ) type nullLiteral struct { @@ -490,6 +494,11 @@ func RawDate(raw string, namedArgs ...map[string]interface{}) DateExpression { return DateExp(Raw(raw, namedArgs...)) } +// RawRange helper that for range expressions +func RawRange[T Expression](raw string, namedArgs ...map[string]interface{}) Range[T] { + return RangeExp[T](Raw(raw, namedArgs...)) +} + // UUID is a helper function to create string literal expression from uuid object // value can be any uuid type with a String method func UUID(value fmt.Stringer) StringExpression { diff --git a/internal/jet/operators.go b/internal/jet/operators.go index b73a4518..bf1dedf0 100644 --- a/internal/jet/operators.go +++ b/internal/jet/operators.go @@ -69,6 +69,16 @@ func GtEq(lhs, rhs Expression) BoolExpression { return newBinaryBoolOperatorExpression(lhs, rhs, ">=") } +// Contains returns a representation of "a @> b" +func Contains(lhs Expression, rhs Expression) BoolExpression { + return newBinaryBoolOperatorExpression(lhs, rhs, "@>") +} + +// Overlap returns a representation of "a && b" +func Overlap(lhs, rhs Expression) BoolExpression { + return newBinaryBoolOperatorExpression(lhs, rhs, "&&") +} + // Add notEq returns a representation of "a + b" func Add(lhs, rhs Serializer) Expression { return NewBinaryOperatorExpression(lhs, rhs, "+") diff --git a/internal/jet/range_expression.go b/internal/jet/range_expression.go new file mode 100644 index 00000000..f05fdeae --- /dev/null +++ b/internal/jet/range_expression.go @@ -0,0 +1,95 @@ +package jet + +// Range Expression is interface for date range types +type Range[T Expression] interface { + Expression + + EQ(rhs Range[T]) BoolExpression + NOT_EQ(rhs Range[T]) BoolExpression + + LT(rhs Range[T]) BoolExpression + LT_EQ(rhs Range[T]) BoolExpression + GT(rhs Range[T]) BoolExpression + GT_EQ(rhs Range[T]) BoolExpression + + CONTAINS(rhs T) BoolExpression + CONTAINS_RANGE(rhs Range[T]) BoolExpression + OVERLAP(rhs Range[T]) BoolExpression + UNION(rhs Range[T]) Range[T] + INTERSECTION(rhs Range[T]) Range[T] + DIFFERENCE(rhs Range[T]) Range[T] +} + +type rangeInterfaceImpl[T Expression] struct { + parent Expression +} + +func (r *rangeInterfaceImpl[T]) EQ(rhs Range[T]) BoolExpression { + return Eq(r.parent, rhs) +} + +func (r *rangeInterfaceImpl[T]) NOT_EQ(rhs Range[T]) BoolExpression { + return NotEq(r.parent, rhs) +} + +func (r *rangeInterfaceImpl[T]) LT(rhs Range[T]) BoolExpression { + return Lt(r.parent, rhs) +} + +func (r *rangeInterfaceImpl[T]) LT_EQ(rhs Range[T]) BoolExpression { + return LtEq(r.parent, rhs) + +} + +func (r *rangeInterfaceImpl[T]) GT(rhs Range[T]) BoolExpression { + return Gt(r.parent, rhs) + +} + +func (r *rangeInterfaceImpl[T]) GT_EQ(rhs Range[T]) BoolExpression { + return GtEq(r.parent, rhs) +} + +func (r *rangeInterfaceImpl[T]) CONTAINS(rhs T) BoolExpression { + return Contains(r.parent, rhs) +} + +func (r *rangeInterfaceImpl[T]) CONTAINS_RANGE(rhs Range[T]) BoolExpression { + return Contains(r.parent, rhs) +} + +func (r *rangeInterfaceImpl[T]) OVERLAP(rhs Range[T]) BoolExpression { + return Overlap(r.parent, rhs) +} + +func (r *rangeInterfaceImpl[T]) UNION(rhs Range[T]) Range[T] { + return RangeExp[T](Add(r.parent, rhs)) +} + +func (r *rangeInterfaceImpl[T]) INTERSECTION(rhs Range[T]) Range[T] { + return RangeExp[T](Mul(r.parent, rhs)) +} + +func (r *rangeInterfaceImpl[T]) DIFFERENCE(rhs Range[T]) Range[T] { + return RangeExp[T](Sub(r.parent, rhs)) +} + +//---------------------------------------------------// + +type rangeExpressionWrapper[T Expression] struct { + rangeInterfaceImpl[T] + Expression +} + +func newRangeExpressionWrap[T Expression](expression Expression) Range[T] { + rangeExpressionWrap := rangeExpressionWrapper[T]{Expression: expression} + rangeExpressionWrap.rangeInterfaceImpl.parent = &rangeExpressionWrap + return &rangeExpressionWrap +} + +// RangeExp is range expression wrapper around arbitrary expression. +// Allows go compiler to see any expression as range expression. +// Does not add sql cast to generated sql builder output. +func RangeExp[T Expression](expression Expression) Range[T] { + return newRangeExpressionWrap[T](expression) +} diff --git a/internal/jet/range_expression_test.go b/internal/jet/range_expression_test.go new file mode 100644 index 00000000..e5e8b75e --- /dev/null +++ b/internal/jet/range_expression_test.go @@ -0,0 +1,63 @@ +package jet + +import "testing" + +func TestRangeExpressionEQ(t *testing.T) { + assertClauseSerialize(t, table1ColRange.EQ(table2ColRange), "(table1.col_range = table2.col_range)") + assertClauseSerialize(t, table1ColRange.EQ(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range = int8range($1, $2, $3))", int8(1), int8(4), "[)") +} + +func TestRangeExpressionNOT_EQ(t *testing.T) { + assertClauseSerialize(t, table1ColRange.NOT_EQ(table2ColRange), "(table1.col_range != table2.col_range)") + assertClauseSerialize(t, table1ColRange.NOT_EQ(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range != int8range($1, $2, $3))", int8(1), int8(4), "[)") +} + +func TestRangeExpressionLT(t *testing.T) { + assertClauseSerialize(t, table1ColRange.LT(table2ColRange), "(table1.col_range < table2.col_range)") + assertClauseSerialize(t, table1ColRange.LT(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range < int8range($1, $2, $3))", int8(1), int8(4), "[)") +} + +func TestRangeExpressionLT_EQ(t *testing.T) { + assertClauseSerialize(t, table1ColRange.LT_EQ(table2ColRange), "(table1.col_range <= table2.col_range)") + assertClauseSerialize(t, table1ColRange.LT_EQ(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range <= int8range($1, $2, $3))", int8(1), int8(4), "[)") +} + +func TestRangeExpressionGT(t *testing.T) { + assertClauseSerialize(t, table1ColRange.GT(table2ColRange), "(table1.col_range > table2.col_range)") + assertClauseSerialize(t, table1ColRange.GT(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range > int8range($1, $2, $3))", int8(1), int8(4), "[)") +} + +func TestRangeExpressionGT_EQ(t *testing.T) { + assertClauseSerialize(t, table1ColRange.GT_EQ(table2ColRange), "(table1.col_range >= table2.col_range)") + assertClauseSerialize(t, table1ColRange.GT_EQ(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range >= int8range($1, $2, $3))", int8(1), int8(4), "[)") +} + +func TestRangeExpressionCONTAINS_RANGE(t *testing.T) { + assertClauseSerialize(t, table1ColRange.CONTAINS_RANGE(table2ColRange), "(table1.col_range @> table2.col_range)") + assertClauseSerialize(t, table1ColRange.CONTAINS_RANGE(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range @> int8range($1, $2, $3))", int8(1), int8(4), "[)") +} + +func TestRangeExpressionCONTAINS(t *testing.T) { + assertClauseSerialize(t, table1ColRange.CONTAINS(table2Col3), "(table1.col_range @> table2.col3)") + assertClauseSerialize(t, table1ColRange.CONTAINS(Int8(1)), "(table1.col_range @> $1)", int8(1)) +} + +func TestRangeExpressionOVERLAP(t *testing.T) { + assertClauseSerialize(t, table1ColRange.OVERLAP(table2ColRange), "(table1.col_range && table2.col_range)") + assertClauseSerialize(t, table1ColRange.OVERLAP(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range && int8range($1, $2, $3))", int8(1), int8(4), "[)") +} + +func TestRangeExpressionUNION(t *testing.T) { + assertClauseSerialize(t, table1ColRange.UNION(table2ColRange), "(table1.col_range + table2.col_range)") + assertClauseSerialize(t, table1ColRange.UNION(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range + int8range($1, $2, $3))", int8(1), int8(4), "[)") +} + +func TestRangeExpressionINTERSECTION(t *testing.T) { + assertClauseSerialize(t, table1ColRange.INTERSECTION(table2ColRange), "(table1.col_range * table2.col_range)") + assertClauseSerialize(t, table1ColRange.INTERSECTION(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range * int8range($1, $2, $3))", int8(1), int8(4), "[)") +} + +func TestRangeExpressionDIFFERENCE(t *testing.T) { + assertClauseSerialize(t, table1ColRange.DIFFERENCE(table2ColRange), "(table1.col_range - table2.col_range)") + assertClauseSerialize(t, table1ColRange.DIFFERENCE(Int8Range(Int8(1), Int8(4), String("[)"))), "(table1.col_range - int8range($1, $2, $3))", int8(1), int8(4), "[)") +} diff --git a/internal/jet/testutils.go b/internal/jet/testutils.go index 43f1f5d6..d91e30fc 100644 --- a/internal/jet/testutils.go +++ b/internal/jet/testutils.go @@ -25,8 +25,9 @@ var ( table1ColTimestampz = TimestampzColumn("col_timestampz") table1ColBool = BoolColumn("col_bool") table1ColDate = DateColumn("col_date") + table1ColRange = RangeColumn[IntegerExpression]("col_range") ) -var table1 = NewTable("db", "table1", "", table1Col1, table1ColInt, table1ColFloat, table1Col3, table1ColTime, table1ColTimez, table1ColBool, table1ColDate, table1ColTimestamp, table1ColTimestampz) +var table1 = NewTable("db", "table1", "", table1Col1, table1ColInt, table1ColFloat, table1Col3, table1ColTime, table1ColTimez, table1ColBool, table1ColDate, table1ColRange, table1ColTimestamp, table1ColTimestampz) var ( table2Col3 = IntegerColumn("col3") @@ -40,8 +41,9 @@ var ( table2ColTimestamp = TimestampColumn("col_timestamp") table2ColTimestampz = TimestampzColumn("col_timestampz") table2ColDate = DateColumn("col_date") + table2ColRange = RangeColumn[IntegerExpression]("col_range") ) -var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColTimestamp, table2ColTimestampz) +var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColRange, table2ColTimestamp, table2ColTimestampz) var ( table3Col1 = IntegerColumn("col1") diff --git a/postgres/columns.go b/postgres/columns.go index a25a88cf..aee28963 100644 --- a/postgres/columns.go +++ b/postgres/columns.go @@ -65,6 +65,42 @@ type ColumnTimestampz = jet.ColumnTimestampz // TimestampzColumn creates named timestamp with time zone column. var TimestampzColumn = jet.TimestampzColumn +// ColumnDateRange is interface of SQL date range column +type ColumnDateRange = jet.ColumnRange[DateExpression] + +// DateRangeColumn creates named range with range column +var DateRangeColumn = jet.RangeColumn[DateExpression] + +// ColumnNumericRange is interface of SQL numeric range column +type ColumnNumericRange = jet.ColumnRange[NumericExpression] + +// NumericRangeColumn creates named range with range column +var NumericRangeColumn = jet.RangeColumn[NumericExpression] + +// ColumnTimestampRange is interface of SQL timestamp range column +type ColumnTimestampRange = jet.ColumnRange[TimestampExpression] + +// TimestampRangeColumn creates named range with range column +var TimestampRangeColumn = jet.RangeColumn[TimestampExpression] + +// ColumnTimestampzRange is interface of SQL timestamp range column +type ColumnTimestampzRange = jet.ColumnRange[TimestampzExpression] + +// TimestampzRangeColumn creates named range with range column +var TimestampzRangeColumn = jet.RangeColumn[TimestampzExpression] + +// ColumnInt4Range is interface of SQL int range column +type ColumnInt4Range = jet.ColumnRange[IntegerExpression] + +// Int4RangeColumn creates named range with range column +var Int4RangeColumn = jet.RangeColumn[IntegerExpression] + +// ColumnInt8Range is interface of SQL int range column +type ColumnInt8Range = jet.ColumnRange[IntegerExpression] + +// Int8RangeColumn creates named range with range column +var Int8RangeColumn = jet.RangeColumn[IntegerExpression] + //------------------------------------------------------// // ColumnInterval is interface of PostgreSQL interval columns. diff --git a/postgres/expressions.go b/postgres/expressions.go index faf153d2..a903860b 100644 --- a/postgres/expressions.go +++ b/postgres/expressions.go @@ -36,6 +36,24 @@ type TimestampExpression = jet.TimestampExpression // TimestampzExpression interface type TimestampzExpression = jet.TimestampzExpression +// DateRange Expression interface +type DateRange = jet.Range[DateExpression] + +// TimestampRange Expression interface +type TimestampRange = jet.Range[TimestampExpression] + +// TimestampzRange Expression interface +type TimestampzRange = jet.Range[TimestampzExpression] + +// NumericRange Expression interface +type NumericRange = jet.Range[NumericExpression] + +// Int4Range Expression interface +type Int4Range = jet.Range[IntegerExpression] + +// Int8Range Expression interface +type Int8Range = jet.Range[IntegerExpression] + // BoolExp is bool expression wrapper around arbitrary expression. // Allows go compiler to see any expression as bool expression. // Does not add sql cast to generated sql builder output. @@ -81,6 +99,13 @@ var TimestampExp = jet.TimestampExp // Does not add sql cast to generated sql builder output. var TimestampzExp = jet.TimestampzExp +// RangeExp is range expression wrapper around arbitrary expression. +// Allows go compiler to see any expression as range expression. +// Does not add sql cast to generated sql builder output. +func RangeExp[T Expression](expression T) jet.Range[T] { + return jet.RangeExp[T](expression) +} + // RawArgs is type used to pass optional arguments to Raw method type RawArgs = map[string]interface{} @@ -90,15 +115,21 @@ type RawArgs = map[string]interface{} var ( Raw = jet.Raw - RawBool = jet.RawBool - RawInt = jet.RawInt - RawFloat = jet.RawFloat - RawString = jet.RawString - RawTime = jet.RawTime - RawTimez = jet.RawTimez - RawTimestamp = jet.RawTimestamp - RawTimestampz = jet.RawTimestampz - RawDate = jet.RawDate + RawBool = jet.RawBool + RawInt = jet.RawInt + RawFloat = jet.RawFloat + RawString = jet.RawString + RawTime = jet.RawTime + RawTimez = jet.RawTimez + RawTimestamp = jet.RawTimestamp + RawTimestampz = jet.RawTimestampz + RawDate = jet.RawDate + RawNumRange = jet.RawRange[jet.NumericExpression] + RawInt4Range = jet.RawRange[jet.IntegerExpression] + RawInt8Range = jet.RawRange[jet.IntegerExpression] + RawTimestampRange = jet.RawRange[jet.TimestampExpression] + RawTimestampzRange = jet.RawRange[jet.TimestampzExpression] + RawDateRange = jet.RawRange[jet.DateExpression] ) // Func can be used to call custom or unsupported database functions. diff --git a/postgres/functions.go b/postgres/functions.go index 43a5f393..eddc9307 100644 --- a/postgres/functions.go +++ b/postgres/functions.go @@ -267,6 +267,18 @@ var TO_HEX = jet.TO_HEX //----------Data Type Formatting Functions ----------------------// +// LOWER_BOUND returns range expressions lower bound +func LOWER_BOUND[T Expression](expression jet.Range[T]) T { + return jet.LOWER_BOUND[T](expression) +} + +// UPPER_BOUND returns range expressions upper bound +func UPPER_BOUND[T Expression](expression jet.Range[T]) T { + return jet.UPPER_BOUND[T](expression) +} + +//----------Data Type Formatting Functions ----------------------// + // TO_CHAR converts expression to string with format var TO_CHAR = jet.TO_CHAR @@ -421,3 +433,18 @@ var CUBE = jet.CUBE // It can be also used with multiple parameters to check if a set of columns is included in the current grouping set. The result // of the GROUPING function would then be an integer bit mask having 1’s for the arguments which have GROUPING(argument) as 1. var GROUPING = jet.GROUPING + +var ( + // DATE_RANGE constructor function to create a date range + DATE_RANGE = jet.DateRange + // NUM_Range constructor function to create a numeric range + NUM_Range = jet.NumRange + // TIMESTAMP_RANGE constructor function to create a timestamp range + TIMESTAMP_RANGE = jet.TimestampRange + // TIMESTAMPTZ_RANGE constructor function to create a timestampz range + TIMESTAMPTZ_RANGE = jet.TimestampzRange + // INT4_RANGE constructor function to create a int4 range + INT4_RANGE = jet.Int4Range + // INT8_RANGE constructor function to create a int8 range + INT8_RANGE = jet.Int8Range +) diff --git a/postgres/keywords.go b/postgres/keywords.go index cfc90a23..a468cc58 100644 --- a/postgres/keywords.go +++ b/postgres/keywords.go @@ -12,4 +12,8 @@ var ( NULL = jet.NULL // STAR is jet equivalent of SQL * STAR = jet.STAR + // PLUS_INFINITY is jet equivalent for sql infinity + PLUS_INFINITY = jet.PLUS_INFINITY + // MINUS_INFINITY is jet equivalent for sql -infinity + MINUS_INFINITY = jet.MINUS_INFINITY ) diff --git a/postgres/utils_test.go b/postgres/utils_test.go index 292d7e4e..96bb13b0 100644 --- a/postgres/utils_test.go +++ b/postgres/utils_test.go @@ -17,6 +17,7 @@ var table1ColTimestampz = TimestampzColumn("col_timestampz") var table1ColBool = BoolColumn("col_bool") var table1ColDate = DateColumn("col_date") var table1ColInterval = IntervalColumn("col_interval") +var table1ColRange = Int8RangeColumn("col_range") var table1 = NewTable( "db", @@ -32,6 +33,7 @@ var table1 = NewTable( table1ColTimestamp, table1ColTimestampz, table1ColInterval, + table1ColRange, ) var table2Col3 = IntegerColumn("col3") @@ -46,8 +48,9 @@ var table2ColTimestamp = TimestampColumn("col_timestamp") var table2ColTimestampz = TimestampzColumn("col_timestampz") var table2ColDate = DateColumn("col_date") var table2ColInterval = IntervalColumn("col_interval") +var table2ColRange = Int8RangeColumn("col_range") -var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColTimestamp, table2ColTimestampz, table2ColInterval) +var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColTimestamp, table2ColTimestampz, table2ColInterval, table2ColRange) var table3Col1 = IntegerColumn("col1") var table3ColInt = IntegerColumn("col_int") diff --git a/tests/postgres/generator_test.go b/tests/postgres/generator_test.go index 5aaaa315..479d82f8 100644 --- a/tests/postgres/generator_test.go +++ b/tests/postgres/generator_test.go @@ -561,13 +561,14 @@ func TestGeneratedAllTypesSQLBuilderFiles(t *testing.T) { testutils.AssertFileNamesEqual(t, modelDir, "all_types.go", "all_types_view.go", "employee.go", "link.go", "mood.go", "person.go", "person_phone.go", "weird_names_table.go", "level.go", "user.go", "floats.go", "people.go", - "components.go", "vulnerabilities.go", "all_types_materialized_view.go") + "components.go", "vulnerabilities.go", "all_types_materialized_view.go", "sample_ranges.go") testutils.AssertFileContent(t, modelDir+"/all_types.go", allTypesModelContent) testutils.AssertFileNamesEqual(t, tableDir, "all_types.go", "employee.go", "link.go", "person.go", "person_phone.go", "weird_names_table.go", "user.go", "floats.go", "people.go", "table_use_schema.go", - "components.go", "vulnerabilities.go") + "components.go", "vulnerabilities.go", "sample_ranges.go") testutils.AssertFileContent(t, tableDir+"/all_types.go", allTypesTableContent) + testutils.AssertFileContent(t, tableDir+"/sample_ranges.go", sampleRangeTableContent) testutils.AssertFileNamesEqual(t, viewDir, "all_types_materialized_view.go", "all_types_view.go", "view_use_schema.go") @@ -968,3 +969,96 @@ func newAllTypesTableImpl(schemaName, tableName, alias string) allTypesTable { } } ` + +var sampleRangeTableContent = ` +// +// Code generated by go-jet DO NOT EDIT. +// +// WARNING: Changes to this file may cause incorrect behavior +// and will be lost if the code is regenerated +// + +package table + +import ( + "github.com/go-jet/jet/v2/postgres" +) + +var SampleRanges = newSampleRangesTable("test_sample", "sample_ranges", "") + +type sampleRangesTable struct { + postgres.Table + + // Columns + DateRange postgres.ColumnDateRange + TimestampRange postgres.ColumnTimestampRange + TimestampzRange postgres.ColumnTimestampzRange + Int4Range postgres.ColumnInt4Range + Int8Range postgres.ColumnInt8Range + NumRange postgres.ColumnNumericRange + + AllColumns postgres.ColumnList + MutableColumns postgres.ColumnList +} + +type SampleRangesTable struct { + sampleRangesTable + + EXCLUDED sampleRangesTable +} + +// AS creates new SampleRangesTable with assigned alias +func (a SampleRangesTable) AS(alias string) *SampleRangesTable { + return newSampleRangesTable(a.SchemaName(), a.TableName(), alias) +} + +// Schema creates new SampleRangesTable with assigned schema name +func (a SampleRangesTable) FromSchema(schemaName string) *SampleRangesTable { + return newSampleRangesTable(schemaName, a.TableName(), a.Alias()) +} + +// WithPrefix creates new SampleRangesTable with assigned table prefix +func (a SampleRangesTable) WithPrefix(prefix string) *SampleRangesTable { + return newSampleRangesTable(a.SchemaName(), prefix+a.TableName(), a.TableName()) +} + +// WithSuffix creates new SampleRangesTable with assigned table suffix +func (a SampleRangesTable) WithSuffix(suffix string) *SampleRangesTable { + return newSampleRangesTable(a.SchemaName(), a.TableName()+suffix, a.TableName()) +} + +func newSampleRangesTable(schemaName, tableName, alias string) *SampleRangesTable { + return &SampleRangesTable{ + sampleRangesTable: newSampleRangesTableImpl(schemaName, tableName, alias), + EXCLUDED: newSampleRangesTableImpl("", "excluded", ""), + } +} + +func newSampleRangesTableImpl(schemaName, tableName, alias string) sampleRangesTable { + var ( + DateRangeColumn = postgres.DateRangeColumn("date_range") + TimestampRangeColumn = postgres.TimestampRangeColumn("timestamp_range") + TimestampzRangeColumn = postgres.TimestampzRangeColumn("timestampz_range") + Int4RangeColumn = postgres.Int4RangeColumn("int4_range") + Int8RangeColumn = postgres.Int8RangeColumn("int8_range") + NumRangeColumn = postgres.NumericRangeColumn("num_range") + allColumns = postgres.ColumnList{DateRangeColumn, TimestampRangeColumn, TimestampzRangeColumn, Int4RangeColumn, Int8RangeColumn, NumRangeColumn} + mutableColumns = postgres.ColumnList{DateRangeColumn, TimestampRangeColumn, TimestampzRangeColumn, Int4RangeColumn, Int8RangeColumn, NumRangeColumn} + ) + + return sampleRangesTable{ + Table: postgres.NewTable(schemaName, tableName, alias, allColumns...), + + //Columns + DateRange: DateRangeColumn, + TimestampRange: TimestampRangeColumn, + TimestampzRange: TimestampzRangeColumn, + Int4Range: Int4RangeColumn, + Int8Range: Int8RangeColumn, + NumRange: NumRangeColumn, + + AllColumns: allColumns, + MutableColumns: mutableColumns, + } +} +` diff --git a/tests/postgres/range_test.go b/tests/postgres/range_test.go new file mode 100644 index 00000000..63498eb8 --- /dev/null +++ b/tests/postgres/range_test.go @@ -0,0 +1,514 @@ +package postgres + +import ( + "github.com/go-jet/jet/v2/internal/testutils" + "github.com/go-jet/jet/v2/qrm" + "github.com/google/go-cmp/cmp" + "github.com/jackc/pgtype" + "github.com/stretchr/testify/require" + "math/big" + "testing" + "time" + + . "github.com/go-jet/jet/v2/postgres" + "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/model" + . "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/table" +) + +func TestRangeTable_DateContainsSingle(t *testing.T) { + skipForCockroachDB(t) + expectedSQL := ` +SELECT DISTINCT sample_ranges.date_range AS "sample_ranges.date_range", + sample_ranges.timestamp_range AS "sample_ranges.timestamp_range", + sample_ranges.timestampz_range AS "sample_ranges.timestampz_range", + sample_ranges.int4_range AS "sample_ranges.int4_range", + sample_ranges.int8_range AS "sample_ranges.int8_range", + sample_ranges.num_range AS "sample_ranges.num_range" +FROM test_sample.sample_ranges +WHERE sample_ranges.date_range @> '2023-12-12'::date; +` + + query := SELECT(SampleRanges.AllColumns). + DISTINCT(). + FROM(SampleRanges). + WHERE(SampleRanges.DateRange.CONTAINS(Date(2023, 12, 12))) + + testutils.AssertDebugStatementSql(t, query, expectedSQL, "2023-12-12") + + sample := model.SampleRanges{} + err := query.Query(db, &sample) + + require.NoError(t, err) + + expectedRow := model.SampleRanges{ + DateRange: pgtype.Daterange{ + Lower: pgtype.Date{ + Time: time.Date(2023, 9, 25, 0, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + Upper: pgtype.Date{ + Time: time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + TimestampRange: pgtype.Tsrange{ + Lower: pgtype.Timestamp{ + Time: time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + Upper: pgtype.Timestamp{ + Time: time.Date(2021, 01, 01, 15, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Inclusive, + Status: pgtype.Present, + }, + TimestampzRange: pgtype.Tstzrange{ + Lower: pgtype.Timestamptz{ + Time: time.Date(2024, 05, 07, 15, 0, 0, 0, time.FixedZone("", 0)), + Status: pgtype.Present, + }, + Upper: pgtype.Timestamptz{ + Time: time.Date(2024, 10, 11, 14, 0, 0, 0, time.FixedZone("", 0)), + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + Int4Range: pgtype.Int4range{ + Lower: pgtype.Int4{ + Int: 11, + Status: pgtype.Present, + }, + Upper: pgtype.Int4{ + Int: 20, + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + Int8Range: pgtype.Int8range{ + Lower: pgtype.Int8{ + Int: 200, + Status: pgtype.Present, + }, + Upper: pgtype.Int8{ + Int: 2450, + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + NumRange: pgtype.Numrange{ + Lower: pgtype.Numeric{ + Int: big.NewInt(2), + Exp: 3, + Status: pgtype.Present, + }, + Upper: pgtype.Numeric{ + Int: big.NewInt(5), + Status: pgtype.Present, + Exp: 3, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + } + + testutils.AssertDeepEqual(t, sample, expectedRow, cmp.AllowUnexported(big.Int{})) + requireLogged(t, query) +} + +func TestRangeTable_IntContainsRange(t *testing.T) { + skipForCockroachDB(t) + expectedSQL := ` +SELECT DISTINCT sample_ranges.date_range AS "sample_ranges.date_range", + sample_ranges.timestamp_range AS "sample_ranges.timestamp_range", + sample_ranges.timestampz_range AS "sample_ranges.timestampz_range", + sample_ranges.int4_range AS "sample_ranges.int4_range", + sample_ranges.int8_range AS "sample_ranges.int8_range", + sample_ranges.num_range AS "sample_ranges.num_range" +FROM test_sample.sample_ranges +WHERE sample_ranges.int4_range @> int4range(12, 18, '[)'::text); +` + + query := SELECT(SampleRanges.AllColumns). + DISTINCT(). + FROM(SampleRanges). + WHERE(SampleRanges.Int4Range.CONTAINS_RANGE(INT4_RANGE(Int(12), Int(18), String("[)")))) + + testutils.AssertDebugStatementSql(t, query, expectedSQL, int64(12), int64(18), "[)") + + sample := model.SampleRanges{} + err := query.Query(db, &sample) + + require.NoError(t, err) + + expectedRow := model.SampleRanges{ + DateRange: pgtype.Daterange{ + Lower: pgtype.Date{ + Time: time.Date(2023, 9, 25, 0, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + Upper: pgtype.Date{ + Time: time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + TimestampRange: pgtype.Tsrange{ + Lower: pgtype.Timestamp{ + Time: time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + Upper: pgtype.Timestamp{ + Time: time.Date(2021, 01, 01, 15, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Inclusive, + Status: pgtype.Present, + }, + TimestampzRange: pgtype.Tstzrange{ + Lower: pgtype.Timestamptz{ + Time: time.Date(2024, 05, 07, 15, 0, 0, 0, time.FixedZone("", 0)), + Status: pgtype.Present, + }, + Upper: pgtype.Timestamptz{ + Time: time.Date(2024, 10, 11, 14, 0, 0, 0, time.FixedZone("", 0)), + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + Int4Range: pgtype.Int4range{ + Lower: pgtype.Int4{ + Int: 11, + Status: pgtype.Present, + }, + Upper: pgtype.Int4{ + Int: 20, + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + Int8Range: pgtype.Int8range{ + Lower: pgtype.Int8{ + Int: 200, + Status: pgtype.Present, + }, + Upper: pgtype.Int8{ + Int: 2450, + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + NumRange: pgtype.Numrange{ + Lower: pgtype.Numeric{ + Int: big.NewInt(2), + Exp: 3, + Status: pgtype.Present, + }, + Upper: pgtype.Numeric{ + Int: big.NewInt(5), + Status: pgtype.Present, + Exp: 3, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + } + + testutils.AssertDeepEqual(t, sample, expectedRow, cmp.AllowUnexported(big.Int{})) + requireLogged(t, query) +} + +func TestRangeTable_TimestampContainsRange(t *testing.T) { + skipForCockroachDB(t) + expectedSQL := ` +SELECT DISTINCT sample_ranges.date_range AS "sample_ranges.date_range", + sample_ranges.timestamp_range AS "sample_ranges.timestamp_range", + sample_ranges.timestampz_range AS "sample_ranges.timestampz_range", + sample_ranges.int4_range AS "sample_ranges.int4_range", + sample_ranges.int8_range AS "sample_ranges.int8_range", + sample_ranges.num_range AS "sample_ranges.num_range" +FROM test_sample.sample_ranges +WHERE sample_ranges.timestamp_range @> tsrange('2020-02-01 00:00:00'::timestamp without time zone, '2020-10-01 00:00:00'::timestamp without time zone, '[)'::text); +` + + query := SELECT(SampleRanges.AllColumns). + DISTINCT(). + FROM(SampleRanges). + WHERE(SampleRanges.TimestampRange.CONTAINS_RANGE(TIMESTAMP_RANGE(Timestamp(2020, 02, 01, 0, 0, 0), Timestamp(2020, 10, 01, 0, 0, 0), String("[)")))) + + testutils.AssertDebugStatementSql(t, query, expectedSQL, "2020-02-01 00:00:00", "2020-10-01 00:00:00", "[)") + + sample := model.SampleRanges{} + err := query.Query(db, &sample) + + require.NoError(t, err) + + expectedRow := model.SampleRanges{ + DateRange: pgtype.Daterange{ + Lower: pgtype.Date{ + Time: time.Date(2023, 9, 25, 0, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + Upper: pgtype.Date{ + Time: time.Date(2024, 2, 10, 0, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + TimestampRange: pgtype.Tsrange{ + Lower: pgtype.Timestamp{ + Time: time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + Upper: pgtype.Timestamp{ + Time: time.Date(2021, 01, 01, 15, 0, 0, 0, time.UTC), + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Inclusive, + Status: pgtype.Present, + }, + TimestampzRange: pgtype.Tstzrange{ + Lower: pgtype.Timestamptz{ + Time: time.Date(2024, 05, 07, 15, 0, 0, 0, time.FixedZone("", 0)), + Status: pgtype.Present, + }, + Upper: pgtype.Timestamptz{ + Time: time.Date(2024, 10, 11, 14, 0, 0, 0, time.FixedZone("", 0)), + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + Int4Range: pgtype.Int4range{ + Lower: pgtype.Int4{ + Int: 11, + Status: pgtype.Present, + }, + Upper: pgtype.Int4{ + Int: 20, + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + Int8Range: pgtype.Int8range{ + Lower: pgtype.Int8{ + Int: 200, + Status: pgtype.Present, + }, + Upper: pgtype.Int8{ + Int: 2450, + Status: pgtype.Present, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + NumRange: pgtype.Numrange{ + Lower: pgtype.Numeric{ + Int: big.NewInt(2), + Exp: 3, + Status: pgtype.Present, + }, + Upper: pgtype.Numeric{ + Int: big.NewInt(5), + Status: pgtype.Present, + Exp: 3, + }, + LowerType: pgtype.Inclusive, + UpperType: pgtype.Exclusive, + Status: pgtype.Present, + }, + } + + testutils.AssertDeepEqual(t, sample, expectedRow, cmp.AllowUnexported(big.Int{})) + requireLogged(t, query) +} + +func TestRangeTable_ContainsOutOfRange(t *testing.T) { + skipForCockroachDB(t) + expectedSQL := ` +SELECT DISTINCT sample_ranges.date_range AS "sample_ranges.date_range", + sample_ranges.timestamp_range AS "sample_ranges.timestamp_range", + sample_ranges.timestampz_range AS "sample_ranges.timestampz_range", + sample_ranges.int4_range AS "sample_ranges.int4_range", + sample_ranges.int8_range AS "sample_ranges.int8_range", + sample_ranges.num_range AS "sample_ranges.num_range" +FROM test_sample.sample_ranges +WHERE sample_ranges.int4_range @> int4range(12, 30, '[)'::text); +` + + query := SELECT(SampleRanges.AllColumns). + DISTINCT(). + FROM(SampleRanges). + WHERE(SampleRanges.Int4Range.CONTAINS_RANGE(INT4_RANGE(Int(12), Int(30), String("[)")))) + + testutils.AssertDebugStatementSql(t, query, expectedSQL, int64(12), int64(30), "[)") + + sample := model.SampleRanges{} + err := query.Query(db, &sample) + + require.ErrorIs(t, err, qrm.ErrNoRows) + requireLogged(t, query) +} + +func TestRangeTable_InsertColumn(t *testing.T) { + skipForCockroachDB(t) + + insertQuery := SampleRanges.INSERT(SampleRanges.AllColumns). + VALUES( + DATE_RANGE( + Date(2010, 01, 01), + Date(2014, 01, 01), + String("[)"), + ), + DEFAULT, + TIMESTAMPTZ_RANGE( + TimestampzT(time.Date(2010, 01, 01, 23, 0, 0, 0, time.UTC)), + TimestampzT(time.Date(2014, 01, 01, 15, 0, 0, 0, time.UTC)), + String("[)"), + ), + INT4_RANGE(Int(64), Int(128), String("[]")), + INT8_RANGE(Int(1024), Int(2048), String("[]")), + DEFAULT, + ). + RETURNING(SampleRanges.AllColumns) + + expectedQuery := ` +INSERT INTO test_sample.sample_ranges (date_range, timestamp_range, timestampz_range, int4_range, int8_range, num_range) +VALUES (daterange('2010-01-01'::date, '2014-01-01'::date, '[)'::text), DEFAULT, tstzrange('2010-01-01 23:00:00Z'::timestamp with time zone, '2014-01-01 15:00:00Z'::timestamp with time zone, '[)'::text), int4range(64, 128, '[]'::text), int8range(1024, 2048, '[]'::text), DEFAULT) +RETURNING sample_ranges.date_range AS "sample_ranges.date_range", + sample_ranges.timestamp_range AS "sample_ranges.timestamp_range", + sample_ranges.timestampz_range AS "sample_ranges.timestampz_range", + sample_ranges.int4_range AS "sample_ranges.int4_range", + sample_ranges.int8_range AS "sample_ranges.int8_range", + sample_ranges.num_range AS "sample_ranges.num_range"; +` + testutils.AssertDebugStatementSql(t, insertQuery, expectedQuery, + "2010-01-01", "2014-01-01", "[)", + time.Date(2010, 01, 01, 23, 0, 0, 0, time.UTC), time.Date(2014, 01, 01, 15, 0, 0, 0, time.UTC), "[)", + int64(64), int64(128), "[]", + int64(1024), int64(2048), "[]", + ) +} + +func TestRangeTable_UpperBound(t *testing.T) { + skipForCockroachDB(t) + + expectedSQL := ` +SELECT UPPER(sample_ranges.date_range) +FROM test_sample.sample_ranges +WHERE sample_ranges.date_range @> '2023-12-12'::date; +` + + query := SELECT(UPPER_BOUND[DateExpression](SampleRanges.DateRange)). + FROM(SampleRanges). + WHERE(SampleRanges.DateRange.CONTAINS(Date(2023, 12, 12))) + + testutils.AssertDebugStatementSql(t, query, expectedSQL, "2023-12-12") + + var date time.Time + err := query.Query(db, &date) + require.NoError(t, err) + + expectedYear := 2024 + expectedMonth := time.February + expectedDay := 10 + if expectedYear != date.Year() || expectedMonth != date.Month() || expectedDay != date.Day() { + t.Errorf("expected: 2024-02-10 got: %s", date.Format("2006-01-02")) + } +} + +func TestRangeTable_LowerBound(t *testing.T) { + skipForCockroachDB(t) + + expectedSQL := ` +SELECT LOWER(sample_ranges.date_range) +FROM test_sample.sample_ranges +WHERE sample_ranges.date_range @> '2023-12-12'::date; +` + + query := SELECT(LOWER_BOUND[DateExpression](SampleRanges.DateRange)). + FROM(SampleRanges). + WHERE(SampleRanges.DateRange.CONTAINS(Date(2023, 12, 12))) + + testutils.AssertDebugStatementSql(t, query, expectedSQL, "2023-12-12") + + var date time.Time + err := query.Query(db, &date) + require.NoError(t, err) + + expectedYear := 2023 + expectedMonth := time.September + expectedDay := 25 + if expectedYear != date.Year() || expectedMonth != date.Month() || expectedDay != date.Day() { + t.Errorf("expected: 2023-09-25 got: %s", date.Format("2006-01-02")) + } +} + +func TestRangeTable_InsertInfinite(t *testing.T) { + skipForCockroachDB(t) + + insertQuery := SampleRanges.INSERT(SampleRanges.AllColumns). + VALUES( + DATE_RANGE( + Date(2010, 01, 01), + DateExp(PLUS_INFINITY), + String("[)"), + ), + DEFAULT, + TIMESTAMPTZ_RANGE( + TimestampzExp(MINUS_INFINITY), + TimestampzT(time.Date(2014, 01, 01, 15, 0, 0, 0, time.UTC)), + String("[)"), + ), + INT4_RANGE(Int(64), Int(128), String("[]")), + INT8_RANGE(Int(1024), Int(2048), String("[]")), + DEFAULT, + ). + RETURNING(SampleRanges.AllColumns) + + expectedQuery := ` +INSERT INTO test_sample.sample_ranges (date_range, timestamp_range, timestampz_range, int4_range, int8_range, num_range) +VALUES (daterange('2010-01-01'::date, 'infinity', '[)'::text), DEFAULT, tstzrange('-infinity', '2014-01-01 15:00:00Z'::timestamp with time zone, '[)'::text), int4range(64, 128, '[]'::text), int8range(1024, 2048, '[]'::text), DEFAULT) +RETURNING sample_ranges.date_range AS "sample_ranges.date_range", + sample_ranges.timestamp_range AS "sample_ranges.timestamp_range", + sample_ranges.timestampz_range AS "sample_ranges.timestampz_range", + sample_ranges.int4_range AS "sample_ranges.int4_range", + sample_ranges.int8_range AS "sample_ranges.int8_range", + sample_ranges.num_range AS "sample_ranges.num_range"; +` + + testutils.AssertDebugStatementSql(t, insertQuery, expectedQuery, + "2010-01-01", "infinity", "[)", + "-infinity", time.Date(2014, 01, 01, 15, 0, 0, 0, time.UTC), "[)", + int64(64), int64(128), "[]", + int64(1024), int64(2048), "[]", + ) +} diff --git a/tests/testdata b/tests/testdata index 08bcfcbb..915bdc16 160000 --- a/tests/testdata +++ b/tests/testdata @@ -1 +1 @@ -Subproject commit 08bcfcbb2e1eadfca54c4522802fc65f1fee865c +Subproject commit 915bdc16b723d89becc577c780949baef861a6ae