Range expression update

* Add Int4 and Int8 integer expression to distinguish int4range and int8range types
* Add range functions to range expressions to avoid go template appearing in sql
* Compact range integration tests and add range update tests
This commit is contained in:
go-jet 2024-02-27 10:48:57 +01:00
parent 9d45aaaba5
commit 43fc77ba99
8 changed files with 472 additions and 460 deletions

View file

@ -470,12 +470,12 @@ func REGEXP_LIKE(stringExp StringExpression, pattern StringExpression, matchType
//----------Range Type Functions ----------------------//
// LOWER_BOUND returns range expressions lower bound
// LOWER_BOUND returns range expressions lower bound. Returns null if range is empty or the requested bound is infinite.
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
// UPPER_BOUND returns range expressions upper bound. Returns null if range is empty or the requested bound is infinite.
func UPPER_BOUND[T Expression](rangeExpression Range[T]) T {
return rangeTypeCaster[T](rangeExpression, NewFunc("UPPER", []Expression{rangeExpression}, nil))
}
@ -483,18 +483,45 @@ func UPPER_BOUND[T Expression](rangeExpression Range[T]) T {
func rangeTypeCaster[T Expression](rangeExpression Range[T], exp Expression) T {
var i Expression
switch rangeExpression.(type) {
case Range[Int4Expression], Range[Int8Expression]:
i = IntExp(exp)
case Range[NumericExpression]:
i = FloatExp(exp)
case Range[DateExpression]:
i = DateExp(exp)
case Range[IntegerExpression]:
i = IntExp(exp)
case Range[TimestampzExpression]:
i = TimestampzExp(exp)
case Range[TimestampExpression]:
i = TimestampExp(exp)
case Range[TimestampzExpression]:
i = TimestampzExp(exp)
}
return i.(T)
}
// IS_EMPTY returns true if range is empty
func IS_EMPTY[T Expression](rangeExpression Range[T]) BoolExpression {
return newBoolFunc("ISEMPTY", rangeExpression)
}
// LOWER_INC returns true if lower bound is inclusive. Returns false for empty range.
func LOWER_INC[T Expression](rangeExpression Range[T]) BoolExpression {
return newBoolFunc("LOWER_INC", rangeExpression)
}
// UPPER_INC returns true if upper bound is inclusive. Returns false for empty range.
func UPPER_INC[T Expression](rangeExpression Range[T]) BoolExpression {
return newBoolFunc("UPPER_INC", rangeExpression)
}
// LOWER_INF returns true if upper bound is infinite. Returns false for empty range.
func LOWER_INF[T Expression](rangeExpression Range[T]) BoolExpression {
return newBoolFunc("LOWER_INF", rangeExpression)
}
// UPPER_INF returns true if lower bound is infinite. Returns false for empty range.
func UPPER_INF[T Expression](rangeExpression Range[T]) BoolExpression {
return newBoolFunc("UPPER_INF", rangeExpression)
}
//----------Data Type Formatting Functions ----------------------//
// TO_CHAR converts expression to string with format
@ -872,30 +899,30 @@ func Func(name string, expressions ...Expression) Expression {
}
func NumRange(lowNum, highNum NumericExpression, bounds ...StringExpression) Range[NumericExpression] {
return RangeExp[NumericExpression](NewFunc("numrange", rangeFuncParamCombiner[NumericExpression](lowNum, highNum, bounds...), nil))
return NumRangeExp(NewFunc("numrange", rangeFuncParamCombiner(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 Int4Range(lowNum, highNum IntegerExpression, bounds ...StringExpression) Range[Int4Expression] {
return Int4RangeExp(NewFunc("int4range", rangeFuncParamCombiner(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 Int8Range(lowNum, highNum Int8Expression, bounds ...StringExpression) Range[Int8Expression] {
return Int8RangeExp(NewFunc("int8range", rangeFuncParamCombiner(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 TsRange(lowTs, highTs TimestampExpression, bounds ...StringExpression) Range[TimestampExpression] {
return TsRangeExp(NewFunc("tsrange", rangeFuncParamCombiner(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 TstzRange(lowTs, highTs TimestampzExpression, bounds ...StringExpression) Range[TimestampzExpression] {
return TstzRangeExp(NewFunc("tstzrange", rangeFuncParamCombiner(lowTs, highTs, bounds...), nil))
}
func DateRange(lowTs, highTs DateExpression, bounds ...StringExpression) Range[DateExpression] {
return RangeExp[DateExpression](NewFunc("daterange", rangeFuncParamCombiner[DateExpression](lowTs, highTs, bounds...), nil))
return DateRangeExp(NewFunc("daterange", rangeFuncParamCombiner(lowTs, highTs, bounds...), nil))
}
func rangeFuncParamCombiner[T Expression](low, high T, bounds ...StringExpression) []Expression {
func rangeFuncParamCombiner(low, high Expression, bounds ...StringExpression) []Expression {
exp := []Expression{low, high}
if len(bounds) != 0 {
exp = append(exp, bounds[0])

View file

@ -31,6 +31,12 @@ type IntegerExpression interface {
BIT_SHIFT_RIGHT(shift IntegerExpression) IntegerExpression
}
// additional integer expression subtypes, used in range expressions.
type (
Int4Expression IntegerExpression
Int8Expression IntegerExpression
)
type integerInterfaceImpl struct {
numericExpressionImpl
parent IntegerExpression

View file

@ -18,10 +18,18 @@ type Range[T Expression] interface {
UNION(rhs Range[T]) Range[T]
INTERSECTION(rhs Range[T]) Range[T]
DIFFERENCE(rhs Range[T]) Range[T]
UPPER_BOUND() T
LOWER_BOUND() T
IS_EMPTY() BoolExpression
LOWER_INC() BoolExpression
UPPER_INC() BoolExpression
LOWER_INF() BoolExpression
UPPER_INF() BoolExpression
}
type rangeInterfaceImpl[T Expression] struct {
parent Expression
parent Range[T]
}
func (r *rangeInterfaceImpl[T]) EQ(rhs Range[T]) BoolExpression {
@ -74,6 +82,34 @@ func (r *rangeInterfaceImpl[T]) DIFFERENCE(rhs Range[T]) Range[T] {
return RangeExp[T](Sub(r.parent, rhs))
}
func (r *rangeInterfaceImpl[T]) UPPER_BOUND() T {
return UPPER_BOUND(r.parent)
}
func (r *rangeInterfaceImpl[T]) LOWER_BOUND() T {
return LOWER_BOUND(r.parent)
}
func (r *rangeInterfaceImpl[T]) IS_EMPTY() BoolExpression {
return IS_EMPTY(r.parent)
}
func (r *rangeInterfaceImpl[T]) LOWER_INC() BoolExpression {
return LOWER_INC(r.parent)
}
func (r *rangeInterfaceImpl[T]) UPPER_INC() BoolExpression {
return UPPER_INC(r.parent)
}
func (r *rangeInterfaceImpl[T]) LOWER_INF() BoolExpression {
return LOWER_INF(r.parent)
}
func (r *rangeInterfaceImpl[T]) UPPER_INF() BoolExpression {
return UPPER_INF(r.parent)
}
//---------------------------------------------------//
type rangeExpressionWrapper[T Expression] struct {
@ -93,3 +129,13 @@ func newRangeExpressionWrap[T Expression](expression Expression) Range[T] {
func RangeExp[T Expression](expression Expression) Range[T] {
return newRangeExpressionWrap[T](expression)
}
// different range expression wrappers
var (
Int4RangeExp = RangeExp[Int4Expression]
Int8RangeExp = RangeExp[Int8Expression]
NumRangeExp = RangeExp[NumericExpression]
DateRangeExp = RangeExp[DateExpression]
TsRangeExp = RangeExp[TimestampExpression]
TstzRangeExp = RangeExp[TimestampzExpression]
)

View file

@ -25,7 +25,7 @@ var (
table1ColTimestampz = TimestampzColumn("col_timestampz")
table1ColBool = BoolColumn("col_bool")
table1ColDate = DateColumn("col_date")
table1ColRange = RangeColumn[IntegerExpression]("col_range")
table1ColRange = RangeColumn[Int8Expression]("col_range")
)
var table1 = NewTable("db", "table1", "", table1Col1, table1ColInt, table1ColFloat, table1Col3, table1ColTime, table1ColTimez, table1ColBool, table1ColDate, table1ColRange, table1ColTimestamp, table1ColTimestampz)
@ -41,7 +41,7 @@ var (
table2ColTimestamp = TimestampColumn("col_timestamp")
table2ColTimestampz = TimestampzColumn("col_timestampz")
table2ColDate = DateColumn("col_date")
table2ColRange = RangeColumn[IntegerExpression]("col_range")
table2ColRange = RangeColumn[Int8Expression]("col_range")
)
var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColRange, table2ColTimestamp, table2ColTimestampz)

View file

@ -89,17 +89,17 @@ 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]
// ColumnInt4Range is interface of SQL int4 range column
type ColumnInt4Range jet.ColumnRange[jet.Int4Expression]
// Int4RangeColumn creates named range with range column
var Int4RangeColumn = jet.RangeColumn[IntegerExpression]
var Int4RangeColumn = jet.RangeColumn[jet.Int4Expression]
// ColumnInt8Range is interface of SQL int range column
type ColumnInt8Range = jet.ColumnRange[IntegerExpression]
// ColumnInt8Range is interface of SQL int8 range column
type ColumnInt8Range jet.ColumnRange[jet.Int8Expression]
// Int8RangeColumn creates named range with range column
var Int8RangeColumn = jet.RangeColumn[IntegerExpression]
var Int8RangeColumn = jet.RangeColumn[jet.Int8Expression]
//------------------------------------------------------//

View file

@ -102,9 +102,14 @@ 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)
}
var (
Int4RangeExp = jet.Int4RangeExp
Int8RangeExp = jet.Int8RangeExp
NumRangeExp = jet.NumRangeExp
DateRangeExp = jet.DateRangeExp
TsRangeExp = jet.TsRangeExp
TstzRangeExp = jet.TstzRangeExp
)
// RawArgs is type used to pass optional arguments to Raw method
type RawArgs = map[string]interface{}
@ -125,8 +130,8 @@ var (
RawTimestampz = jet.RawTimestampz
RawDate = jet.RawDate
RawNumRange = jet.RawRange[jet.NumericExpression]
RawInt4Range = jet.RawRange[jet.IntegerExpression]
RawInt8Range = jet.RawRange[jet.IntegerExpression]
RawInt4Range = jet.RawRange[jet.Int4Expression]
RawInt8Range = jet.RawRange[jet.Int8Expression]
RawTimestampRange = jet.RawRange[jet.TimestampExpression]
RawTimestampzRange = jet.RawRange[jet.TimestampzExpression]
RawDateRange = jet.RawRange[jet.DateExpression]

View file

@ -434,15 +434,16 @@ var CUBE = jet.CUBE
// of the GROUPING function would then be an integer bit mask having 1s for the arguments which have GROUPING(argument) as 1.
var GROUPING = jet.GROUPING
// range constructor functions
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
// NUM_RANGE constructor function to create a numeric range
NUM_RANGE = jet.NumRange
// TS_RANGE constructor function to create a timestamp range
TS_RANGE = jet.TsRange
// TSTZ_RANGE constructor function to create a timestampz range
TSTZ_RANGE = jet.TstzRange
// INT4_RANGE constructor function to create a int4 range
INT4_RANGE = jet.Int4Range
// INT8_RANGE constructor function to create a int8 range

View file

@ -1,8 +1,8 @@
package postgres
import (
"database/sql"
"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"
@ -15,59 +15,168 @@ import (
. "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/table"
)
func TestRangeTable_DateContainsSingle(t *testing.T) {
func TestRangeTableSelect(t *testing.T) {
skipForCockroachDB(t)
expectedSQL := `
SELECT DISTINCT sample_ranges.date_range AS "sample_ranges.date_range",
int4Range := INT4_RANGE(Int(11), Int(20))
int8Range := INT8_RANGE(Int(100), Int(250), String("[)"))
numRange := NUM_RANGE(Int(11), Float(22.22))
dateRange := DATE_RANGE(Date(2010, 10, 1), DateExp(PLUS_INFINITY))
tsRange := TS_RANGE(
Timestamp(2020, 02, 01, 0, 0, 0),
Timestamp(2020, 10, 01, 0, 0, 0),
)
tstzRange := TSTZ_RANGE(
TimestampzExp(MINUS_INFINITY),
TimestampzT(time.Date(2014, 01, 01, 15, 0, 0, 0, time.UTC)),
)
query := SELECT(
SampleRanges.AllColumns,
SampleRanges.Int4Range.EQ(SampleRanges.Int4Range).AS("sample.int4eq"),
SampleRanges.Int8Range.EQ(int8Range).AS("sample.int8eq"),
SampleRanges.Int4Range.NOT_EQ(int4Range).AS("sample.int4neq"),
SampleRanges.NumRange.LT(numRange).IS_TRUE().AS("sample.num_lt"),
SampleRanges.DateRange.LT_EQ(dateRange).IS_FALSE().AS("sample.date_lteq"),
SampleRanges.TimestampRange.GT(tsRange).AS("sample.ts_gt"),
SampleRanges.TimestampzRange.GT_EQ(tstzRange).AS("sample.tstz_gteq"),
SampleRanges.Int4Range.CONTAINS(Int32(22)).AS("sample.int4cont"),
SampleRanges.Int8Range.CONTAINS(Int64(75364)).AS("sample.int8cont"),
SampleRanges.Int8Range.CONTAINS_RANGE(int8Range).AS("sample.int8cont_range"),
SampleRanges.NumRange.OVERLAP(numRange).AS("sample.num_overlap"),
SampleRanges.DateRange.UNION(dateRange).AS("sample.date_union"),
SampleRanges.TimestampRange.INTERSECTION(tsRange).AS("sample.ts_ints"),
SampleRanges.TimestampzRange.DIFFERENCE(tstzRange).AS("sample.tstz_diff"),
SampleRanges.Int4Range.UPPER_BOUND().ADD(Int(5)).AS("sample.int4_upper"),
SampleRanges.Int8Range.LOWER_BOUND().SUB(Int(12)).AS("sample.int8_lower"),
LOWER_BOUND[DateExpression](SampleRanges.DateRange),
UPPER_BOUND[NumericExpression](SampleRanges.NumRange).AS("sample.num_upper"),
SampleRanges.TimestampRange.UPPER_BOUND(),
SampleRanges.DateRange.IS_EMPTY().AS("sample.date_empty"),
SampleRanges.TimestampRange.LOWER_INC().AS("sample.ts_low_inc"),
SampleRanges.TimestampzRange.UPPER_INC().AS("sample.tstz_up_inc"),
SampleRanges.TimestampRange.LOWER_INF().AS("sample.ts_low_inf"),
SampleRanges.TimestampzRange.UPPER_INF().AS("sample.tstz_up_inf"),
RawInt4Range("'[1,2]'").EQ(int4Range),
RawInt8Range("int8range(15, 25)").EQ(int8Range),
RawNumRange("numrange(20.0, 30.0)").NOT_EQ(numRange),
).FROM(
SampleRanges,
).WHERE(
SampleRanges.DateRange.CONTAINS(Date(2023, 12, 12)),
)
testutils.AssertStatementSql(t, query, `
SELECT 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"
sample_ranges.num_range AS "sample_ranges.num_range",
(sample_ranges.int4_range = sample_ranges.int4_range) AS "sample.int4eq",
(sample_ranges.int8_range = int8range($1, $2, $3::text)) AS "sample.int8eq",
(sample_ranges.int4_range != int4range($4, $5)) AS "sample.int4neq",
(sample_ranges.num_range < numrange($6, $7)) IS TRUE AS "sample.num_lt",
(sample_ranges.date_range <= daterange($8::date, $9)) IS FALSE AS "sample.date_lteq",
(sample_ranges.timestamp_range > tsrange($10::timestamp without time zone, $11::timestamp without time zone)) AS "sample.ts_gt",
(sample_ranges.timestampz_range >= tstzrange($12, $13::timestamp with time zone)) AS "sample.tstz_gteq",
(sample_ranges.int4_range @> $14::integer) AS "sample.int4cont",
(sample_ranges.int8_range @> $15::bigint) AS "sample.int8cont",
(sample_ranges.int8_range @> int8range($16, $17, $18::text)) AS "sample.int8cont_range",
(sample_ranges.num_range && numrange($19, $20)) AS "sample.num_overlap",
(sample_ranges.date_range + daterange($21::date, $22)) AS "sample.date_union",
(sample_ranges.timestamp_range * tsrange($23::timestamp without time zone, $24::timestamp without time zone)) AS "sample.ts_ints",
(sample_ranges.timestampz_range - tstzrange($25, $26::timestamp with time zone)) AS "sample.tstz_diff",
(UPPER(sample_ranges.int4_range) + $27) AS "sample.int4_upper",
(LOWER(sample_ranges.int8_range) - $28) AS "sample.int8_lower",
LOWER(sample_ranges.date_range),
UPPER(sample_ranges.num_range) AS "sample.num_upper",
UPPER(sample_ranges.timestamp_range),
ISEMPTY(sample_ranges.date_range) AS "sample.date_empty",
LOWER_INC(sample_ranges.timestamp_range) AS "sample.ts_low_inc",
UPPER_INC(sample_ranges.timestampz_range) AS "sample.tstz_up_inc",
LOWER_INF(sample_ranges.timestamp_range) AS "sample.ts_low_inf",
UPPER_INF(sample_ranges.timestampz_range) AS "sample.tstz_up_inf",
('[1,2]') = int4range($29, $30),
(int8range(15, 25)) = int8range($31, $32, $33::text),
(numrange(20.0, 30.0)) != numrange($34, $35)
FROM test_sample.sample_ranges
WHERE sample_ranges.date_range @> '2023-12-12'::date;
`
WHERE sample_ranges.date_range @> $36::date;
`)
query := SELECT(SampleRanges.AllColumns).
DISTINCT().
FROM(SampleRanges).
WHERE(SampleRanges.DateRange.CONTAINS(Date(2023, 12, 12)))
type sample struct {
model.SampleRanges
testutils.AssertDebugStatementSql(t, query, expectedSQL, "2023-12-12")
sample := model.SampleRanges{}
err := query.Query(db, &sample)
Int4Eq bool
Int8Eq bool
Int4Neq bool
NumLt bool
DateLtEq bool
TsGt bool
TsTzGtEq bool
Int4Cont bool
Int8Cont bool
Int8ContRange bool
NumOverlap bool
DateUnion pgtype.Daterange
TsInts pgtype.Tsrange
TsTzDiff pgtype.Tstzrange
Int4Upper int32
Int8Lower int64
NumUpper float64
DateEmpty bool
TsLowInc bool
TsTzUpInc bool
TsLowInf bool
TsTzUpInf bool
}
var dest sample
err := query.Query(db, &dest)
require.NoError(t, err)
expectedRow := model.SampleRanges{
DateRange: pgtype.Daterange{
expectedRow := sample{
SampleRanges: sampleRangeRow,
Int4Eq: true,
Int8Eq: false,
Int4Neq: false,
NumLt: false,
DateLtEq: true,
TsGt: false,
TsTzGtEq: true,
Int4Cont: false,
Int8Cont: false,
Int8ContRange: false,
NumOverlap: false,
DateUnion: pgtype.Daterange{
Lower: pgtype.Date{
Time: time.Date(2023, 9, 25, 0, 0, 0, 0, time.UTC),
Time: time.Date(2010, 10, 1, 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,
InfinityModifier: pgtype.Infinity,
},
LowerType: pgtype.Inclusive,
UpperType: pgtype.Exclusive,
Status: pgtype.Present,
},
TsInts: pgtype.Tsrange{
Lower: pgtype.Timestamp{
Time: time.Date(2020, 02, 01, 0, 0, 0, 0, time.UTC),
Status: pgtype.Present,
},
Upper: pgtype.Timestamp{
Time: time.Date(2020, 10, 01, 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{
TsTzDiff: pgtype.Tstzrange{
Lower: pgtype.Timestamptz{
Time: time.Date(2024, 05, 07, 15, 0, 0, 0, time.FixedZone("", 0)),
Status: pgtype.Present,
@ -80,410 +189,85 @@ WHERE sample_ranges.date_range @> '2023-12-12'::date;
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,
},
Int4Upper: 25,
Int8Lower: 188,
NumUpper: 5000,
DateEmpty: false,
TsLowInc: true,
TsTzUpInc: false,
TsLowInf: false,
TsTzUpInf: false,
}
testutils.AssertDeepEqual(t, sample, expectedRow, cmp.AllowUnexported(big.Int{}))
testutils.AssertDeepEqual(t, dest, 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);
`
func TestRangeSelectColumnsFromSubQuery(t *testing.T) {
query := SELECT(SampleRanges.AllColumns).
DISTINCT().
FROM(SampleRanges).
WHERE(SampleRanges.Int4Range.CONTAINS_RANGE(INT4_RANGE(Int(12), Int(18), String("[)"))))
subQuery := SELECT(
SampleRanges.AllColumns,
SampleRanges.Int4Range.AS("range4"),
).FROM(
SampleRanges,
).AsTable("sub_query")
testutils.AssertDebugStatementSql(t, query, expectedSQL, int64(12), int64(18), "[)")
int4Range := Int4RangeColumn("range4").From(subQuery)
sample := model.SampleRanges{}
err := query.Query(db, &sample)
stmt := SELECT(
subQuery.AllColumns(),
int4Range,
).FROM(
subQuery,
)
testutils.AssertDebugStatementSql(t, stmt, `
SELECT sub_query."sample_ranges.date_range" AS "sample_ranges.date_range",
sub_query."sample_ranges.timestamp_range" AS "sample_ranges.timestamp_range",
sub_query."sample_ranges.timestampz_range" AS "sample_ranges.timestampz_range",
sub_query."sample_ranges.int4_range" AS "sample_ranges.int4_range",
sub_query."sample_ranges.int8_range" AS "sample_ranges.int8_range",
sub_query."sample_ranges.num_range" AS "sample_ranges.num_range",
sub_query.range4 AS "range4",
sub_query.range4 AS "range4"
FROM (
SELECT 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",
sample_ranges.int4_range AS "range4"
FROM test_sample.sample_ranges
) AS sub_query;
`)
var dest struct {
model.SampleRanges
Range4 pgtype.Int4range
}
err := stmt.Query(db, &dest)
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)
testutils.AssertDeepEqual(t, dest.SampleRanges.Int4Range, sampleRangeRow.Int4Range)
testutils.AssertDeepEqual(t, dest.SampleRanges.Int8Range, sampleRangeRow.Int8Range)
testutils.AssertDeepEqual(t, dest.Range4, sampleRangeRow.Int4Range)
}
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("[)"),
String("()"),
),
DEFAULT,
TIMESTAMPTZ_RANGE(
TSTZ_RANGE(
TimestampzExp(MINUS_INFINITY),
TimestampzT(time.Date(2014, 01, 01, 15, 0, 0, 0, time.UTC)),
String("[)"),
@ -492,11 +276,14 @@ func TestRangeTable_InsertInfinite(t *testing.T) {
INT8_RANGE(Int(1024), Int(2048), String("[]")),
DEFAULT,
).
RETURNING(SampleRanges.AllColumns)
MODEL(
sampleRangeRow,
).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)
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),
('[2023-09-25,2024-02-10)', '[2020-01-01 00:00:00,2021-01-01 15:00:00]', '[2024-05-07 15:00:00Z,2024-10-11 14:00:00Z)', '[11,20)', '[200,2450)', '[2e3,5e3)')
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",
@ -504,11 +291,151 @@ RETURNING sample_ranges.date_range AS "sample_ranges.date_range",
sample_ranges.int8_range AS "sample_ranges.int8_range",
sample_ranges.num_range AS "sample_ranges.num_range";
`
testutils.AssertDebugStatementSql(t, insertQuery, expectedQuery)
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), "[]",
)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
var dest []model.SampleRanges
err := insertQuery.Query(tx, &dest)
require.NoError(t, err)
require.Len(t, dest, 2)
testutils.AssertDeepEqual(t, sampleRangeRow, dest[1], cmp.AllowUnexported(big.Int{}))
})
}
func TestRangeTableUpdate(t *testing.T) {
skipForCockroachDB(t)
t.Run("using model", func(t *testing.T) {
stmt := SampleRanges.UPDATE(SampleRanges.AllColumns).
MODEL(sampleRangeRow).
WHERE(SampleRanges.TimestampRange.LOWER_INF().IS_FALSE()).
RETURNING(SampleRanges.AllColumns)
testutils.AssertStatementSql(t, stmt, `
UPDATE test_sample.sample_ranges
SET (date_range, timestamp_range, timestampz_range, int4_range, int8_range, num_range) = ($1, $2, $3, $4, $5, $6)
WHERE LOWER_INF(sample_ranges.timestamp_range) IS FALSE
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.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
var dest []model.SampleRanges
err := stmt.Query(tx, &dest)
require.NoError(t, err)
require.Len(t, dest, 1)
testutils.AssertDeepEqual(t, sampleRangeRow, dest[0], cmp.AllowUnexported(big.Int{}))
})
})
t.Run("update using SET", func(t *testing.T) {
stmt := SampleRanges.UPDATE().
SET(
SampleRanges.Int4Range.SET(INT4_RANGE(Int32(-12), Int32(78))),
SampleRanges.Int8Range.SET(INT8_RANGE(Int64(-1200), Int64(7800))),
).
WHERE(
SampleRanges.TimestampzRange.LOWER_BOUND().GT(NOW()),
)
testutils.AssertDebugStatementSql(t, stmt, `
UPDATE test_sample.sample_ranges
SET int4_range = int4range(-12::integer, 78::integer),
int8_range = int8range(-1200::bigint, 7800::bigint)
WHERE LOWER(sample_ranges.timestampz_range) > NOW();
`)
testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) {
testutils.AssertExec(t, stmt, tx, 1)
})
})
}
var sampleRangeRow = 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,
},
}