From 17646ca99c7898fa0455793a5c614babb984fdfa Mon Sep 17 00:00:00 2001 From: go-jet Date: Sat, 8 Mar 2025 19:01:37 +0100 Subject: [PATCH] Encode json values implicitly in the sql queries according the golang json package spec. --- internal/jet/alias.go | 52 +- internal/jet/blob_expression.go | 63 +- internal/jet/bool_expression.go | 7 +- internal/jet/clause.go | 10 +- internal/jet/column.go | 39 +- internal/jet/column_list.go | 8 +- internal/jet/column_test.go | 3 +- internal/jet/column_types.go | 111 +++- internal/jet/date_expression.go | 7 +- internal/jet/date_expression_test.go | 13 - internal/jet/dialect.go | 8 + internal/jet/expression.go | 20 +- internal/jet/float_expression.go | 7 +- internal/jet/integer_expression.go | 8 +- internal/jet/interval.go | 37 -- internal/jet/interval_expression.go | 112 ++++ internal/jet/literal_expression.go | 11 - internal/jet/projection.go | 23 +- internal/jet/range_expression.go | 7 +- internal/jet/row_expression.go | 13 +- internal/jet/serializer.go | 4 - internal/jet/sql_builder.go | 10 + internal/jet/statement.go | 7 +- internal/jet/string_expression.go | 7 +- internal/jet/time_expression.go | 9 +- internal/jet/time_expression_test.go | 8 - internal/jet/timestamp_expression.go | 7 +- internal/jet/timestamp_expression_test.go | 8 - internal/jet/timestampz_expression.go | 7 +- internal/jet/timestampz_expression_test.go | 8 - internal/jet/timez_expression.go | 9 +- internal/jet/timez_expression_test.go | 8 - internal/jet/utils.go | 2 +- internal/testutils/test_utils.go | 2 +- mysql/dialect.go | 19 +- mysql/{interval.go => interval_literal.go} | 7 +- ...erval_test.go => interval_literal_test.go} | 0 mysql/select_json.go | 2 +- postgres/columns.go | 44 +- postgres/dialect.go | 20 +- postgres/expressions.go | 8 + postgres/functions.go | 30 +- postgres/interval_expression.go | 257 -------- postgres/interval_literal.go | 140 +++++ ...ssion_test.go => interval_literal_test.go} | 0 postgres/select_json.go | 1 + qrm/qrm.go | 6 +- tests/mysql/alltypes_test.go | 198 ++++++- tests/mysql/select_json_test.go | 161 ++--- tests/postgres/alltypes_test.go | 560 ++++++++++++++++-- tests/postgres/chinook_db_test.go | 11 +- tests/postgres/main_test.go | 2 + tests/postgres/northwind_test.go | 2 +- tests/postgres/select_json_test.go | 67 +-- 54 files changed, 1446 insertions(+), 744 deletions(-) delete mode 100644 internal/jet/date_expression_test.go delete mode 100644 internal/jet/interval.go create mode 100644 internal/jet/interval_expression.go rename mysql/{interval.go => interval_literal.go} (96%) rename mysql/{interval_test.go => interval_literal_test.go} (100%) delete mode 100644 postgres/interval_expression.go create mode 100644 postgres/interval_literal.go rename postgres/{interval_expression_test.go => interval_literal_test.go} (100%) diff --git a/internal/jet/alias.go b/internal/jet/alias.go index 5374031..d096a65 100644 --- a/internal/jet/alias.go +++ b/internal/jet/alias.go @@ -18,10 +18,45 @@ func (a *alias) fromImpl(subQuery SelectTable) Projection { // Generated columns have default aliasing. tableName, columnName := extractTableAndColumnName(a.alias) - column := NewColumnImpl(columnName, tableName, nil) - column.subQuery = subQuery + newDummyColumn := newDummyColumnForExpression(a.expression, columnName) + newDummyColumn.setTableName(tableName) + newDummyColumn.setSubQuery(subQuery) - return &column + return newDummyColumn +} + +// This function is used to create dummy columns when exporting sub-query columns using subQuery.AllColumns() +// In most case we don't care about type of the column, except when sub-query columns are used as SELECT_JSON projection. +// We need to know type to encode value for json unmarshal. At the moment only bool, time and blob columns are of interest, +// so we don't have to support every column type. +func newDummyColumnForExpression(exp Expression, name string) ColumnExpression { + + switch exp.(type) { + case BoolExpression: + return BoolColumn(name) + case IntegerExpression: + return IntegerColumn(name) + case FloatExpression: + return FloatColumn(name) + case BlobExpression: + return BlobColumn(name) + case DateExpression: + return DateColumn(name) + case TimeExpression: + return TimeColumn(name) + case TimezExpression: + return TimezColumn(name) + case TimestampExpression: + return TimestampColumn(name) + case TimestampzExpression: + return TimestampzColumn(name) + case IntervalExpression: + return IntervalColumn(name) + case StringExpression: + return StringColumn(name) + } + + return StringColumn(name) } func (a *alias) serializeForProjection(statement StatementType, out *SQLBuilder) { @@ -31,7 +66,14 @@ func (a *alias) serializeForProjection(statement StatementType, out *SQLBuilder) out.WriteAlias(a.alias) } -func (a *alias) serializeForJsonObj(statement StatementType, out *SQLBuilder) { +func (a *alias) serializeForJsonObjEntry(statement StatementType, out *SQLBuilder) { out.WriteJsonObjKey(a.alias) - a.expression.serialize(statement, out) + a.expression.serializeForJsonValue(statement, out) +} + +func (a *alias) serializeForRowToJsonProjection(statement StatementType, out *SQLBuilder) { + a.expression.serializeForJsonValue(statement, out) + + out.WriteString("AS") + out.WriteAlias(a.alias) } diff --git a/internal/jet/blob_expression.go b/internal/jet/blob_expression.go index 47cdf9a..435c68e 100644 --- a/internal/jet/blob_expression.go +++ b/internal/jet/blob_expression.go @@ -28,71 +28,72 @@ type blobInterfaceImpl struct { parent BlobExpression } -func (s *blobInterfaceImpl) isStringOrBlob() {} +func (b *blobInterfaceImpl) isStringOrBlob() {} -func (s *blobInterfaceImpl) EQ(rhs BlobExpression) BoolExpression { - return Eq(s.parent, rhs) +func (b *blobInterfaceImpl) EQ(rhs BlobExpression) BoolExpression { + return Eq(b.parent, rhs) } -func (s *blobInterfaceImpl) NOT_EQ(rhs BlobExpression) BoolExpression { - return NotEq(s.parent, rhs) +func (b *blobInterfaceImpl) NOT_EQ(rhs BlobExpression) BoolExpression { + return NotEq(b.parent, rhs) } -func (s *blobInterfaceImpl) IS_DISTINCT_FROM(rhs BlobExpression) BoolExpression { - return IsDistinctFrom(s.parent, rhs) +func (b *blobInterfaceImpl) IS_DISTINCT_FROM(rhs BlobExpression) BoolExpression { + return IsDistinctFrom(b.parent, rhs) } -func (s *blobInterfaceImpl) IS_NOT_DISTINCT_FROM(rhs BlobExpression) BoolExpression { - return IsNotDistinctFrom(s.parent, rhs) +func (b *blobInterfaceImpl) IS_NOT_DISTINCT_FROM(rhs BlobExpression) BoolExpression { + return IsNotDistinctFrom(b.parent, rhs) } -func (s *blobInterfaceImpl) GT(rhs BlobExpression) BoolExpression { - return Gt(s.parent, rhs) +func (b *blobInterfaceImpl) GT(rhs BlobExpression) BoolExpression { + return Gt(b.parent, rhs) } -func (s *blobInterfaceImpl) GT_EQ(rhs BlobExpression) BoolExpression { - return GtEq(s.parent, rhs) +func (b *blobInterfaceImpl) GT_EQ(rhs BlobExpression) BoolExpression { + return GtEq(b.parent, rhs) } -func (s *blobInterfaceImpl) LT(rhs BlobExpression) BoolExpression { - return Lt(s.parent, rhs) +func (b *blobInterfaceImpl) LT(rhs BlobExpression) BoolExpression { + return Lt(b.parent, rhs) } -func (s *blobInterfaceImpl) LT_EQ(rhs BlobExpression) BoolExpression { - return LtEq(s.parent, rhs) +func (b *blobInterfaceImpl) LT_EQ(rhs BlobExpression) BoolExpression { + return LtEq(b.parent, rhs) } -func (s *blobInterfaceImpl) BETWEEN(min, max BlobExpression) BoolExpression { - return NewBetweenOperatorExpression(s.parent, min, max, false) +func (b *blobInterfaceImpl) BETWEEN(min, max BlobExpression) BoolExpression { + return NewBetweenOperatorExpression(b.parent, min, max, false) } -func (s *blobInterfaceImpl) NOT_BETWEEN(min, max BlobExpression) BoolExpression { - return NewBetweenOperatorExpression(s.parent, min, max, true) +func (b *blobInterfaceImpl) NOT_BETWEEN(min, max BlobExpression) BoolExpression { + return NewBetweenOperatorExpression(b.parent, min, max, true) } -func (s *blobInterfaceImpl) CONCAT(rhs BlobExpression) BlobExpression { - return BlobExp(newBinaryStringOperatorExpression(s.parent, rhs, StringConcatOperator)) +func (b *blobInterfaceImpl) CONCAT(rhs BlobExpression) BlobExpression { + return BlobExp(newBinaryStringOperatorExpression(b.parent, rhs, StringConcatOperator)) } -func (s *blobInterfaceImpl) LIKE(pattern BlobExpression) BoolExpression { - return newBinaryBoolOperatorExpression(s.parent, pattern, "LIKE") +func (b *blobInterfaceImpl) LIKE(pattern BlobExpression) BoolExpression { + return newBinaryBoolOperatorExpression(b.parent, pattern, "LIKE") } -func (s *blobInterfaceImpl) NOT_LIKE(pattern BlobExpression) BoolExpression { - return newBinaryBoolOperatorExpression(s.parent, pattern, "NOT LIKE") +func (b *blobInterfaceImpl) NOT_LIKE(pattern BlobExpression) BoolExpression { + return newBinaryBoolOperatorExpression(b.parent, pattern, "NOT LIKE") } //---------------------------------------------------// type blobExpressionWrapper struct { - blobInterfaceImpl Expression + blobInterfaceImpl } func newBlobExpressionWrap(expression Expression) BlobExpression { - blobExpressionWrap := blobExpressionWrapper{Expression: expression} - blobExpressionWrap.blobInterfaceImpl.parent = &blobExpressionWrap - return &blobExpressionWrap + blobExpressionWrap := &blobExpressionWrapper{Expression: expression} + blobExpressionWrap.blobInterfaceImpl.parent = blobExpressionWrap + expression.setParent(blobExpressionWrap) + return blobExpressionWrap } // BlobExp is blob expression wrapper around arbitrary expression. diff --git a/internal/jet/bool_expression.go b/internal/jet/bool_expression.go index 41bdcc4..450375c 100644 --- a/internal/jet/bool_expression.go +++ b/internal/jet/bool_expression.go @@ -102,9 +102,10 @@ type boolExpressionWrapper struct { } func newBoolExpressionWrap(expression Expression) BoolExpression { - boolExpressionWrap := boolExpressionWrapper{Expression: expression} - boolExpressionWrap.boolInterfaceImpl.parent = &boolExpressionWrap - return &boolExpressionWrap + boolExpressionWrap := &boolExpressionWrapper{Expression: expression} + boolExpressionWrap.boolInterfaceImpl.parent = boolExpressionWrap + expression.setParent(boolExpressionWrap) + return boolExpressionWrap } // BoolExp is bool expression wrapper around arbitrary expression. diff --git a/internal/jet/clause.go b/internal/jet/clause.go index 68d3fee..dab2f81 100644 --- a/internal/jet/clause.go +++ b/internal/jet/clause.go @@ -41,6 +41,8 @@ type ClauseSelect struct { DistinctOnColumns []ColumnExpression ProjectionList []Projection + IsForRowToJson bool + // MySQL only OptimizerHints optimizerHints } @@ -70,7 +72,13 @@ func (s *ClauseSelect) Serialize(statementType StatementType, out *SQLBuilder, o out.WriteByte(')') } - out.WriteProjections(statementType, s.ProjectionList) + if s.IsForRowToJson { + out.IncreaseIdent() + out.WriteRowToJsonProjections(statementType, s.ProjectionList) + out.DecreaseIdent() + } else { + out.WriteProjections(statementType, s.ProjectionList) + } } // ClauseFrom struct diff --git a/internal/jet/column.go b/internal/jet/column.go index 702518a..dda19ff 100644 --- a/internal/jet/column.go +++ b/internal/jet/column.go @@ -39,19 +39,19 @@ type ColumnExpressionImpl struct { } // NewColumnImpl creates new ColumnExpressionImpl -func NewColumnImpl(name string, tableName string, parent ColumnExpression) ColumnExpressionImpl { - bc := ColumnExpressionImpl{ +func NewColumnImpl(name string, tableName string, parent ColumnExpression) *ColumnExpressionImpl { + newColumn := &ColumnExpressionImpl{ name: name, tableName: tableName, } if parent != nil { - bc.ExpressionInterfaceImpl.Parent = parent + newColumn.ExpressionInterfaceImpl.Parent = parent } else { - bc.ExpressionInterfaceImpl.Parent = &bc + newColumn.ExpressionInterfaceImpl.Parent = newColumn } - return bc + return newColumn } // Name returns name of the column @@ -80,13 +80,6 @@ func (c *ColumnExpressionImpl) defaultAlias() string { return c.name } -func (c *ColumnExpressionImpl) fromImpl(subQuery SelectTable) Projection { - newColumn := NewColumnImpl(c.name, c.tableName, nil) - newColumn.setSubQuery(subQuery) - - return &newColumn -} - func (c *ColumnExpressionImpl) serializeForOrderBy(statement StatementType, out *SQLBuilder) { if statement == SetStatementType { // set Statement (UNION, EXCEPT ...) can reference only select projections in order by clause @@ -97,24 +90,28 @@ func (c *ColumnExpressionImpl) serializeForOrderBy(statement StatementType, out c.serialize(statement, out) } -func (c ColumnExpressionImpl) serializeForProjection(statement StatementType, out *SQLBuilder) { +func (c *ColumnExpressionImpl) serializeForProjection(statement StatementType, out *SQLBuilder) { c.serialize(statement, out) out.WriteString("AS") - if statement.IsSelectJSON() { - out.WriteAlias(snaker.SnakeToCamel(c.name, false)) - } else { - out.WriteAlias(c.defaultAlias()) - } + out.WriteAlias(c.defaultAlias()) } -func (c ColumnExpressionImpl) serializeForJsonObj(statement StatementType, out *SQLBuilder) { +func (c *ColumnExpressionImpl) serializeForJsonObjEntry(statement StatementType, out *SQLBuilder) { out.WriteJsonObjKey(snaker.SnakeToCamel(c.name, false)) - c.serialize(statement, out) + c.Parent.serializeForJsonValue(statement, out) } -func (c ColumnExpressionImpl) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { +func (c *ColumnExpressionImpl) serializeForRowToJsonProjection(statement StatementType, out *SQLBuilder) { + c.Parent.serializeForJsonValue(statement, out) + + out.WriteString("AS") + + out.WriteAlias(snaker.SnakeToCamel(c.name, false)) +} + +func (c *ColumnExpressionImpl) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { if c.subQuery != nil { out.WriteIdentifier(c.subQuery.Alias()) diff --git a/internal/jet/column_list.go b/internal/jet/column_list.go index 18acd81..0ea88fe 100644 --- a/internal/jet/column_list.go +++ b/internal/jet/column_list.go @@ -78,12 +78,18 @@ func (cl ColumnList) serializeForProjection(statement StatementType, out *SQLBui SerializeProjectionList(statement, projections, out) } -func (cl ColumnList) serializeForJsonObj(statement StatementType, out *SQLBuilder) { +func (cl ColumnList) serializeForJsonObjEntry(statement StatementType, out *SQLBuilder) { projections := ColumnListToProjectionList(cl) SerializeProjectionListJsonObj(statement, projections, out) } +func (cl ColumnList) serializeForRowToJsonProjection(statement StatementType, out *SQLBuilder) { + projections := ColumnListToProjectionList(cl) + + out.WriteRowToJsonProjections(statement, projections) +} + // dummy column interface implementation // Name is placeholder for ColumnList to implement Column interface diff --git a/internal/jet/column_test.go b/internal/jet/column_test.go index 7e6c0ed..23e581a 100644 --- a/internal/jet/column_test.go +++ b/internal/jet/column_test.go @@ -4,11 +4,10 @@ import "testing" func TestColumn(t *testing.T) { column := NewColumnImpl("col", "", nil) - column.ExpressionInterfaceImpl.Parent = &column assertClauseSerialize(t, column, "col") column.setTableName("table1") assertClauseSerialize(t, column, "table1.col") - assertProjectionSerialize(t, &column, `table1.col AS "table1.col"`) + assertProjectionSerialize(t, column, `table1.col AS "table1.col"`) assertProjectionSerialize(t, column.AS("alias1"), `table1.col AS "alias1"`) } diff --git a/internal/jet/column_types.go b/internal/jet/column_types.go index 98e9117..00e333e 100644 --- a/internal/jet/column_types.go +++ b/internal/jet/column_types.go @@ -11,7 +11,11 @@ type ColumnBool interface { type boolColumnImpl struct { boolInterfaceImpl - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *boolColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *boolColumnImpl) From(subQuery SelectTable) ColumnBool { @@ -51,7 +55,11 @@ type ColumnFloat interface { type floatColumnImpl struct { floatInterfaceImpl - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *floatColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *floatColumnImpl) From(subQuery SelectTable) ColumnFloat { @@ -92,7 +100,11 @@ type ColumnInteger interface { type integerColumnImpl struct { integerInterfaceImpl - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *integerColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *integerColumnImpl) From(subQuery SelectTable) ColumnInteger { @@ -134,7 +146,11 @@ type ColumnString interface { type stringColumnImpl struct { stringInterfaceImpl - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *stringColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *stringColumnImpl) From(subQuery SelectTable) ColumnString { @@ -175,7 +191,11 @@ type ColumnBlob interface { type blobColumnImpl struct { blobInterfaceImpl - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *blobColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *blobColumnImpl) From(subQuery SelectTable) ColumnBlob { @@ -215,7 +235,11 @@ type ColumnTime interface { type timeColumnImpl struct { timeInterfaceImpl - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *timeColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *timeColumnImpl) From(subQuery SelectTable) ColumnTime { @@ -254,7 +278,11 @@ type ColumnTimez interface { type timezColumnImpl struct { timezInterfaceImpl - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *timezColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *timezColumnImpl) From(subQuery SelectTable) ColumnTimez { @@ -294,7 +322,11 @@ type ColumnTimestamp interface { type timestampColumnImpl struct { timestampInterfaceImpl - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *timestampColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *timestampColumnImpl) From(subQuery SelectTable) ColumnTimestamp { @@ -334,7 +366,11 @@ type ColumnTimestampz interface { type timestampzColumnImpl struct { timestampzInterfaceImpl - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *timestampzColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *timestampzColumnImpl) From(subQuery SelectTable) ColumnTimestampz { @@ -374,7 +410,11 @@ type ColumnDate interface { type dateColumnImpl struct { dateInterfaceImpl - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *dateColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *dateColumnImpl) From(subQuery SelectTable) ColumnDate { @@ -402,6 +442,51 @@ func DateColumn(name string) ColumnDate { //------------------------------------------------------// +// ColumnInterval is interface of PostgreSQL interval columns. +type ColumnInterval interface { + IntervalExpression + Column + + From(subQuery SelectTable) ColumnInterval + SET(intervalExp IntervalExpression) ColumnAssigment +} + +//------------------------------------------------------// + +type intervalColumnImpl struct { + *ColumnExpressionImpl + intervalInterfaceImpl +} + +func (i *intervalColumnImpl) SET(intervalExp IntervalExpression) ColumnAssigment { + return columnAssigmentImpl{ + column: i, + expression: intervalExp, + } +} + +func (i *intervalColumnImpl) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) +} + +func (i *intervalColumnImpl) From(subQuery SelectTable) ColumnInterval { + newIntervalColumn := IntervalColumn(i.name) + newIntervalColumn.setTableName(i.tableName) + newIntervalColumn.setSubQuery(subQuery) + + return newIntervalColumn +} + +// IntervalColumn creates named interval column. +func IntervalColumn(name string) ColumnInterval { + intervalColumn := &intervalColumnImpl{} + intervalColumn.ColumnExpressionImpl = NewColumnImpl(name, "", intervalColumn) + intervalColumn.intervalInterfaceImpl.parent = intervalColumn + return intervalColumn +} + +//------------------------------------------------------// + // ColumnRange is interface for range columns which can be int range, string range // timestamp range or date range. type ColumnRange[T Expression] interface { @@ -414,7 +499,11 @@ type ColumnRange[T Expression] interface { type rangeColumnImpl[T Expression] struct { rangeInterfaceImpl[T] - ColumnExpressionImpl + *ColumnExpressionImpl +} + +func (i *rangeColumnImpl[T]) fromImpl(subQuery SelectTable) Projection { + return i.From(subQuery) } func (i *rangeColumnImpl[T]) From(subQuery SelectTable) ColumnRange[T] { diff --git a/internal/jet/date_expression.go b/internal/jet/date_expression.go index 560688a..2f62a50 100644 --- a/internal/jet/date_expression.go +++ b/internal/jet/date_expression.go @@ -80,9 +80,10 @@ type dateExpressionWrapper struct { } func newDateExpressionWrap(expression Expression) DateExpression { - dateExpressionWrap := dateExpressionWrapper{Expression: expression} - dateExpressionWrap.dateInterfaceImpl.parent = &dateExpressionWrap - return &dateExpressionWrap + dateExpressionWrap := &dateExpressionWrapper{Expression: expression} + dateExpressionWrap.dateInterfaceImpl.parent = dateExpressionWrap + expression.setParent(dateExpressionWrap) + return dateExpressionWrap } // DateExp is date expression wrapper around arbitrary expression. diff --git a/internal/jet/date_expression_test.go b/internal/jet/date_expression_test.go deleted file mode 100644 index 14fdd76..0000000 --- a/internal/jet/date_expression_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package jet - -import ( - "testing" -) - -func TestDateArithmetic(t *testing.T) { - timestamp := Timestamp(2000, 1, 1, 0, 0, 0) - assertClauseDebugSerialize(t, table1ColDate.ADD(NewInterval(String("1 HOUR"))).EQ(timestamp), - "((table1.col_date + INTERVAL '1 HOUR') = '2000-01-01 00:00:00')") - assertClauseDebugSerialize(t, table1ColDate.SUB(NewInterval(String("1 HOUR"))).EQ(timestamp), - "((table1.col_date - INTERVAL '1 HOUR') = '2000-01-01 00:00:00')") -} diff --git a/internal/jet/dialect.go b/internal/jet/dialect.go index e38c581..678e044 100644 --- a/internal/jet/dialect.go +++ b/internal/jet/dialect.go @@ -17,6 +17,7 @@ type Dialect interface { IsReservedWord(name string) bool SerializeOrderBy() func(expression Expression, ascending, nullsFirst *bool) SerializerFunc ValuesDefaultColumnName(index int) string + JsonValueEncode(expr Expression) Expression } // SerializerFunc func @@ -41,6 +42,7 @@ type DialectParams struct { ReservedWords []string SerializeOrderBy func(expression Expression, ascending, nullsFirst *bool) SerializerFunc ValuesDefaultColumnName func(index int) string + JsonValueEncode func(expr Expression) Expression } // NewDialect creates new dialect with params @@ -57,6 +59,7 @@ func NewDialect(params DialectParams) Dialect { reservedWords: arrayOfStringsToMapOfStrings(params.ReservedWords), serializeOrderBy: params.SerializeOrderBy, valuesDefaultColumnName: params.ValuesDefaultColumnName, + jsonValueEncode: params.JsonValueEncode, } } @@ -72,6 +75,7 @@ type dialectImpl struct { reservedWords map[string]bool serializeOrderBy func(expression Expression, ascending, nullsFirst *bool) SerializerFunc valuesDefaultColumnName func(index int) string + jsonValueEncode func(expr Expression) Expression } func (d *dialectImpl) Name() string { @@ -125,6 +129,10 @@ func (d *dialectImpl) ValuesDefaultColumnName(index int) string { return d.valuesDefaultColumnName(index) } +func (d *dialectImpl) JsonValueEncode(expr Expression) Expression { + return d.jsonValueEncode(expr) +} + func arrayOfStringsToMapOfStrings(arr []string) map[string]bool { ret := map[string]bool{} for _, elem := range arr { diff --git a/internal/jet/expression.go b/internal/jet/expression.go index 1065010..2ec5edc 100644 --- a/internal/jet/expression.go +++ b/internal/jet/expression.go @@ -10,6 +10,9 @@ type Expression interface { GroupByClause OrderByClause + serializeForJsonValue(statement StatementType, out *SQLBuilder) + setParent(parent Expression) + // IS_NULL tests expression whether it is a NULL value. IS_NULL() BoolExpression // IS_NOT_NULL tests expression whether it is a non-NULL value. @@ -34,6 +37,10 @@ type ExpressionInterfaceImpl struct { Parent Expression } +func (e *ExpressionInterfaceImpl) setParent(parent Expression) { + e.Parent = parent +} + func (e *ExpressionInterfaceImpl) fromImpl(subQuery SelectTable) Projection { panic(fmt.Sprintf("jet: can't export unaliased expression subQuery: %s, expression: %s", subQuery.Alias(), serializeToDefaultDebugString(e.Parent))) @@ -89,16 +96,21 @@ func (e *ExpressionInterfaceImpl) serializeForGroupBy(statement StatementType, o } func (e *ExpressionInterfaceImpl) serializeForProjection(statement StatementType, out *SQLBuilder) { - if statement.IsSelectJSON() { - panic("jet: expression need to be aliased when used as SELECT JSON projection.") - } e.Parent.serialize(statement, out, NoWrap) } -func (e *ExpressionInterfaceImpl) serializeForJsonObj(statement StatementType, out *SQLBuilder) { +func (e *ExpressionInterfaceImpl) serializeForJsonObjEntry(statement StatementType, out *SQLBuilder) { panic("jet: expression need to be aliased when used as SELECT JSON projection.") } +func (e *ExpressionInterfaceImpl) serializeForRowToJsonProjection(statement StatementType, out *SQLBuilder) { + panic("jet: expression need to be aliased when used as SELECT JSON projection.") +} + +func (e *ExpressionInterfaceImpl) serializeForJsonValue(statement StatementType, out *SQLBuilder) { + out.Dialect.JsonValueEncode(e.Parent).serialize(statement, out) +} + func (e *ExpressionInterfaceImpl) serializeForOrderBy(statement StatementType, out *SQLBuilder) { e.Parent.serialize(statement, out, NoWrap) } diff --git a/internal/jet/float_expression.go b/internal/jet/float_expression.go index 52c97eb..f695ec3 100644 --- a/internal/jet/float_expression.go +++ b/internal/jet/float_expression.go @@ -102,9 +102,10 @@ type floatExpressionWrapper struct { } func newFloatExpressionWrap(expression Expression) FloatExpression { - floatExpressionWrap := floatExpressionWrapper{Expression: expression} - floatExpressionWrap.floatInterfaceImpl.parent = &floatExpressionWrap - return &floatExpressionWrap + floatExpressionWrap := &floatExpressionWrapper{Expression: expression} + floatExpressionWrap.floatInterfaceImpl.parent = floatExpressionWrap + expression.setParent(floatExpressionWrap) + return floatExpressionWrap } // FloatExp is date expression wrapper around arbitrary expression. diff --git a/internal/jet/integer_expression.go b/internal/jet/integer_expression.go index f5451a0..d2761fc 100644 --- a/internal/jet/integer_expression.go +++ b/internal/jet/integer_expression.go @@ -141,11 +141,11 @@ type integerExpressionWrapper struct { } func newIntExpressionWrap(expression Expression) IntegerExpression { - intExpressionWrap := integerExpressionWrapper{Expression: expression} + intExpressionWrap := &integerExpressionWrapper{Expression: expression} + intExpressionWrap.integerInterfaceImpl.parent = intExpressionWrap + expression.setParent(intExpressionWrap) - intExpressionWrap.integerInterfaceImpl.parent = &intExpressionWrap - - return &intExpressionWrap + return intExpressionWrap } // IntExp is int expression wrapper around arbitrary expression. diff --git a/internal/jet/interval.go b/internal/jet/interval.go deleted file mode 100644 index debcb57..0000000 --- a/internal/jet/interval.go +++ /dev/null @@ -1,37 +0,0 @@ -package jet - -// Interval is internal common representation of sql interval -type Interval interface { - Serializer - IsInterval -} - -// IsInterval interface -type IsInterval interface { - isInterval() -} - -// IsIntervalImpl is implementation of IsInterval interface -type IsIntervalImpl struct{} - -func (i *IsIntervalImpl) isInterval() {} - -// NewInterval creates new interval from serializer -func NewInterval(s Serializer) *IntervalImpl { - newInterval := &IntervalImpl{ - Value: s, - } - - return newInterval -} - -// IntervalImpl is implementation of Interval type -type IntervalImpl struct { - Value Serializer - IsIntervalImpl -} - -func (i IntervalImpl) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { - out.WriteString("INTERVAL") - i.Value.serialize(statement, out, FallTrough(options)...) -} diff --git a/internal/jet/interval_expression.go b/internal/jet/interval_expression.go new file mode 100644 index 0000000..5d49462 --- /dev/null +++ b/internal/jet/interval_expression.go @@ -0,0 +1,112 @@ +package jet + +// IntervalExpression interface +type IntervalExpression interface { + Expression + isInterval() + + EQ(rhs IntervalExpression) BoolExpression + NOT_EQ(rhs IntervalExpression) BoolExpression + IS_DISTINCT_FROM(rhs IntervalExpression) BoolExpression + IS_NOT_DISTINCT_FROM(rhs IntervalExpression) BoolExpression + + LT(rhs IntervalExpression) BoolExpression + LT_EQ(rhs IntervalExpression) BoolExpression + GT(rhs IntervalExpression) BoolExpression + GT_EQ(rhs IntervalExpression) BoolExpression + BETWEEN(min, max IntervalExpression) BoolExpression + NOT_BETWEEN(min, max IntervalExpression) BoolExpression + + ADD(rhs IntervalExpression) IntervalExpression + SUB(rhs IntervalExpression) IntervalExpression + + MUL(rhs NumericExpression) IntervalExpression + DIV(rhs NumericExpression) IntervalExpression +} + +type intervalInterfaceImpl struct { + parent IntervalExpression +} + +func (i *intervalInterfaceImpl) isInterval() {} + +func (i *intervalInterfaceImpl) EQ(rhs IntervalExpression) BoolExpression { + return Eq(i.parent, rhs) +} + +func (i *intervalInterfaceImpl) NOT_EQ(rhs IntervalExpression) BoolExpression { + return NotEq(i.parent, rhs) +} + +func (i *intervalInterfaceImpl) IS_DISTINCT_FROM(rhs IntervalExpression) BoolExpression { + return IsDistinctFrom(i.parent, rhs) +} + +func (i *intervalInterfaceImpl) IS_NOT_DISTINCT_FROM(rhs IntervalExpression) BoolExpression { + return IsNotDistinctFrom(i.parent, rhs) +} + +func (i *intervalInterfaceImpl) LT(rhs IntervalExpression) BoolExpression { + return Lt(i.parent, rhs) +} + +func (i *intervalInterfaceImpl) LT_EQ(rhs IntervalExpression) BoolExpression { + return LtEq(i.parent, rhs) +} + +func (i *intervalInterfaceImpl) GT(rhs IntervalExpression) BoolExpression { + return Gt(i.parent, rhs) +} + +func (i *intervalInterfaceImpl) GT_EQ(rhs IntervalExpression) BoolExpression { + return GtEq(i.parent, rhs) +} + +func (i *intervalInterfaceImpl) BETWEEN(min, max IntervalExpression) BoolExpression { + return NewBetweenOperatorExpression(i.parent, min, max, false) +} + +func (i *intervalInterfaceImpl) NOT_BETWEEN(min, max IntervalExpression) BoolExpression { + return NewBetweenOperatorExpression(i.parent, min, max, true) +} + +func (i *intervalInterfaceImpl) ADD(rhs IntervalExpression) IntervalExpression { + return IntervalExp(Add(i.parent, rhs)) +} + +func (i *intervalInterfaceImpl) SUB(rhs IntervalExpression) IntervalExpression { + return IntervalExp(Sub(i.parent, rhs)) +} + +func (i *intervalInterfaceImpl) MUL(rhs NumericExpression) IntervalExpression { + return IntervalExp(Mul(i.parent, rhs)) +} + +func (i *intervalInterfaceImpl) DIV(rhs NumericExpression) IntervalExpression { + return IntervalExp(Div(i.parent, rhs)) +} + +type intervalWrapper struct { + intervalInterfaceImpl + Expression +} + +func newIntervalExpressionWrap(expression Expression) IntervalExpression { + intervalWrap := &intervalWrapper{Expression: expression} + intervalWrap.intervalInterfaceImpl.parent = intervalWrap + expression.setParent(intervalWrap) + return intervalWrap +} + +// IntervalExp is interval expression wrapper around arbitrary expression. +// Allows go compiler to see any expression as interval expression. +// Does not add sql cast to generated sql builder output. +func IntervalExp(expression Expression) IntervalExpression { + return newIntervalExpressionWrap(expression) +} + +// Interval interface +type Interval interface { + Serializer + isInterval() +} diff --git a/internal/jet/literal_expression.go b/internal/jet/literal_expression.go index ff0faa1..a2eec63 100644 --- a/internal/jet/literal_expression.go +++ b/internal/jet/literal_expression.go @@ -412,17 +412,6 @@ func Raw(raw string, namedArgs ...map[string]interface{}) Expression { return rawExp } -// RawWithParent is a Raw constructor used for construction dialect specific expression -func RawWithParent(raw string, parent ...Expression) Expression { - rawExp := &rawExpression{ - Raw: raw, - noWrap: true, - } - rawExp.ExpressionInterfaceImpl.Parent = OptionalOrDefaultExpression(rawExp, parent...) - - return rawExp -} - // RawBool helper that for raw string boolean expressions func RawBool(raw string, namedArgs ...map[string]interface{}) BoolExpression { return BoolExp(Raw(raw, namedArgs...)) diff --git a/internal/jet/projection.go b/internal/jet/projection.go index 647c1ba..03a94f1 100644 --- a/internal/jet/projection.go +++ b/internal/jet/projection.go @@ -3,7 +3,8 @@ package jet // Projection is interface for all projection types. Types that can be part of, for instance SELECT clause. type Projection interface { serializeForProjection(statement StatementType, out *SQLBuilder) - serializeForJsonObj(statement StatementType, out *SQLBuilder) + serializeForJsonObjEntry(statement StatementType, out *SQLBuilder) + serializeForRowToJsonProjection(statement StatementType, out *SQLBuilder) fromImpl(subQuery SelectTable) Projection } @@ -29,7 +30,7 @@ func (pl ProjectionList) serializeForProjection(statement StatementType, out *SQ SerializeProjectionList(statement, pl, out) } -func (pl ProjectionList) serializeForJsonObj(statement StatementType, out *SQLBuilder) { +func (pl ProjectionList) serializeForJsonObjEntry(statement StatementType, out *SQLBuilder) { SerializeProjectionListJsonObj(statement, pl, out) } @@ -85,9 +86,17 @@ func (pl ProjectionList) Except(toExclude ...Column) ProjectionList { return ret } -// JsonProjectionList redefines []Projection so projections can be serialized as json object key/values -type JsonProjectionList []Projection - -func (j JsonProjectionList) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { - SerializeProjectionListJsonObj(statement, j, out) +func (pl ProjectionList) serializeForRowToJsonProjection(statement StatementType, out *SQLBuilder) { + out.WriteRowToJsonProjections(statement, pl) +} + +// JsonObjProjectionList redefines []Projection so projections can be serialized as json object key/values +type JsonObjProjectionList []Projection + +func (j JsonObjProjectionList) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { + out.IncreaseIdent() + out.NewLine() + SerializeProjectionListJsonObj(statement, j, out) + out.DecreaseIdent() + out.NewLine() } diff --git a/internal/jet/range_expression.go b/internal/jet/range_expression.go index 9e15c2b..77ed290 100644 --- a/internal/jet/range_expression.go +++ b/internal/jet/range_expression.go @@ -118,9 +118,10 @@ type rangeExpressionWrapper[T Expression] struct { } func newRangeExpressionWrap[T Expression](expression Expression) Range[T] { - rangeExpressionWrap := rangeExpressionWrapper[T]{Expression: expression} - rangeExpressionWrap.rangeInterfaceImpl.parent = &rangeExpressionWrap - return &rangeExpressionWrap + rangeExpressionWrap := &rangeExpressionWrapper[T]{Expression: expression} + rangeExpressionWrap.rangeInterfaceImpl.parent = rangeExpressionWrap + expression.setParent(rangeExpressionWrap) + return rangeExpressionWrap } // RangeExp is range expression wrapper around arbitrary expression. diff --git a/internal/jet/row_expression.go b/internal/jet/row_expression.go index e7d5ed5..a07d140 100644 --- a/internal/jet/row_expression.go +++ b/internal/jet/row_expression.go @@ -17,9 +17,9 @@ type RowExpression interface { } type rowInterfaceImpl struct { - parent Expression - dialect Dialect - elemCount int + parent Expression + dialect Dialect + expressions []Expression } func (n *rowInterfaceImpl) EQ(rhs RowExpression) BoolExpression { @@ -57,9 +57,8 @@ func (n *rowInterfaceImpl) LT_EQ(rhs RowExpression) BoolExpression { func (n *rowInterfaceImpl) projections() ProjectionList { var ret ProjectionList - for i := 0; i < n.elemCount; i++ { - rowColumn := NewColumnImpl(n.dialect.ValuesDefaultColumnName(i), "", nil) - ret = append(ret, &rowColumn) + for i, expression := range n.expressions { + ret = append(ret, newDummyColumnForExpression(expression, n.dialect.ValuesDefaultColumnName(i))) } return ret @@ -77,7 +76,7 @@ func newRowExpression(name string, dialect Dialect, expressions ...Expression) R ret.Expression = NewFunc(name, expressions, ret) ret.dialect = dialect - ret.elemCount = len(expressions) + ret.expressions = expressions return ret } diff --git a/internal/jet/serializer.go b/internal/jet/serializer.go index 9d457a9..d876f36 100644 --- a/internal/jet/serializer.go +++ b/internal/jet/serializer.go @@ -22,10 +22,6 @@ func (s SerializeOption) WithFallTrough(options []SerializeOption) []SerializeOp // StatementType is type of the SQL statement type StatementType string -func (s StatementType) IsSelectJSON() bool { - return s == SelectJsonObjStatementType || s == SelectJsonArrStatementType -} - // Statement types const ( SelectStatementType StatementType = "SELECT" diff --git a/internal/jet/sql_builder.go b/internal/jet/sql_builder.go index 9465ec6..d4113a0 100644 --- a/internal/jet/sql_builder.go +++ b/internal/jet/sql_builder.go @@ -61,6 +61,16 @@ func (s *SQLBuilder) WriteProjections(statement StatementType, projections []Pro s.DecreaseIdent() } +func (s *SQLBuilder) WriteRowToJsonProjections(statement StatementType, projections []Projection) { + for i, projection := range projections { + if i > 0 { + s.WriteString(",") + s.NewLine() + } + projection.serializeForRowToJsonProjection(statement, s) + } +} + // NewLine adds new line to output SQL func (s *SQLBuilder) NewLine() { s.write([]byte{'\n'}) diff --git a/internal/jet/statement.go b/internal/jet/statement.go index 20e4665..bf25cc6 100644 --- a/internal/jet/statement.go +++ b/internal/jet/statement.go @@ -257,12 +257,13 @@ type expressionStatementImpl struct { } func (s *expressionStatementImpl) serializeForProjection(statement StatementType, out *SQLBuilder) { - if statement.IsSelectJSON() { - panic("jet: SELECT JSON statements need to be aliased when used as a projection.") - } s.serialize(statement, out) } +func (e *expressionStatementImpl) serializeForRowToJsonProjection(statement StatementType, out *SQLBuilder) { + panic("jet: SELECT JSON statements need to be aliased when used as a projection.") +} + // NewStatementImpl creates new statementImpl func NewStatementImpl(Dialect Dialect, statementType StatementType, parent SerializerStatement, clauses ...Clause) SerializerStatement { return &statementImpl{ diff --git a/internal/jet/string_expression.go b/internal/jet/string_expression.go index 20e9e39..c7f1158 100644 --- a/internal/jet/string_expression.go +++ b/internal/jet/string_expression.go @@ -105,9 +105,10 @@ type stringExpressionWrapper struct { } func newStringExpressionWrap(expression Expression) StringExpression { - stringExpressionWrap := stringExpressionWrapper{Expression: expression} - stringExpressionWrap.stringInterfaceImpl.parent = &stringExpressionWrap - return &stringExpressionWrap + stringExpressionWrap := &stringExpressionWrapper{Expression: expression} + stringExpressionWrap.stringInterfaceImpl.parent = stringExpressionWrap + expression.setParent(stringExpressionWrap) + return stringExpressionWrap } // StringExp is string expression wrapper around arbitrary expression. diff --git a/internal/jet/time_expression.go b/internal/jet/time_expression.go index efd146f..215d3bc 100644 --- a/internal/jet/time_expression.go +++ b/internal/jet/time_expression.go @@ -75,14 +75,15 @@ func (t *timeInterfaceImpl) SUB(rhs Interval) TimeExpression { //---------------------------------------------------// type timeExpressionWrapper struct { - timeInterfaceImpl Expression + timeInterfaceImpl } func newTimeExpressionWrap(expression Expression) TimeExpression { - timeExpressionWrap := timeExpressionWrapper{Expression: expression} - timeExpressionWrap.timeInterfaceImpl.parent = &timeExpressionWrap - return &timeExpressionWrap + timeExpressionWrap := &timeExpressionWrapper{Expression: expression} + timeExpressionWrap.timeInterfaceImpl.parent = timeExpressionWrap + expression.setParent(timeExpressionWrap) + return timeExpressionWrap } // TimeExp is time expression wrapper around arbitrary expression. diff --git a/internal/jet/time_expression_test.go b/internal/jet/time_expression_test.go index 61ee29f..2b3d015 100644 --- a/internal/jet/time_expression_test.go +++ b/internal/jet/time_expression_test.go @@ -52,11 +52,3 @@ func TestTimeExp(t *testing.T) { assertClauseSerialize(t, TimeExp(table1ColFloat).LT(Time(1, 1, 1, 1*time.Millisecond)), "(table1.col_float < $1)", string("01:01:01.001")) } - -func TestTimeArithmetic(t *testing.T) { - time := Time(10, 20, 3) - assertClauseDebugSerialize(t, table1ColTime.ADD(NewInterval(String("1 HOUR"))).EQ(time), - "((table1.col_time + INTERVAL '1 HOUR') = '10:20:03')") - assertClauseDebugSerialize(t, table1ColTime.SUB(NewInterval(String("1 HOUR"))).EQ(time), - "((table1.col_time - INTERVAL '1 HOUR') = '10:20:03')") -} diff --git a/internal/jet/timestamp_expression.go b/internal/jet/timestamp_expression.go index 1013ce1..222b1e4 100644 --- a/internal/jet/timestamp_expression.go +++ b/internal/jet/timestamp_expression.go @@ -80,9 +80,10 @@ type timestampExpressionWrapper struct { } func newTimestampExpressionWrap(expression Expression) TimestampExpression { - timestampExpressionWrap := timestampExpressionWrapper{Expression: expression} - timestampExpressionWrap.timestampInterfaceImpl.parent = ×tampExpressionWrap - return ×tampExpressionWrap + timestampExpressionWrap := ×tampExpressionWrapper{Expression: expression} + timestampExpressionWrap.timestampInterfaceImpl.parent = timestampExpressionWrap + expression.setParent(timestampExpressionWrap) + return timestampExpressionWrap } // TimestampExp is timestamp expression wrapper around arbitrary expression. diff --git a/internal/jet/timestamp_expression_test.go b/internal/jet/timestamp_expression_test.go index e34d8dd..9a9ceb4 100644 --- a/internal/jet/timestamp_expression_test.go +++ b/internal/jet/timestamp_expression_test.go @@ -53,11 +53,3 @@ func TestTimestampExp(t *testing.T) { assertClauseSerialize(t, TimestampExp(table1ColFloat).LT(timestamp), "(table1.col_float < $1)", "2000-01-31 10:20:00.003") } - -func TestTimestampArithmetic(t *testing.T) { - timestamp := Timestamp(2000, 1, 1, 0, 0, 0) - assertClauseDebugSerialize(t, table1ColTimestamp.ADD(NewInterval(String("1 HOUR"))).EQ(timestamp), - "((table1.col_timestamp + INTERVAL '1 HOUR') = '2000-01-01 00:00:00')") - assertClauseDebugSerialize(t, table1ColTimestamp.SUB(NewInterval(String("1 HOUR"))).EQ(timestamp), - "((table1.col_timestamp - INTERVAL '1 HOUR') = '2000-01-01 00:00:00')") -} diff --git a/internal/jet/timestampz_expression.go b/internal/jet/timestampz_expression.go index b8fe103..6500251 100644 --- a/internal/jet/timestampz_expression.go +++ b/internal/jet/timestampz_expression.go @@ -80,9 +80,10 @@ type timestampzExpressionWrapper struct { } func newTimestampzExpressionWrap(expression Expression) TimestampzExpression { - timestampzExpressionWrap := timestampzExpressionWrapper{Expression: expression} - timestampzExpressionWrap.timestampzInterfaceImpl.parent = ×tampzExpressionWrap - return ×tampzExpressionWrap + timestampzExpressionWrap := ×tampzExpressionWrapper{Expression: expression} + timestampzExpressionWrap.timestampzInterfaceImpl.parent = timestampzExpressionWrap + expression.setParent(timestampzExpressionWrap) + return timestampzExpressionWrap } // TimestampzExp is timestamp with time zone expression wrapper around arbitrary expression. diff --git a/internal/jet/timestampz_expression_test.go b/internal/jet/timestampz_expression_test.go index 1ff1eac..6880c93 100644 --- a/internal/jet/timestampz_expression_test.go +++ b/internal/jet/timestampz_expression_test.go @@ -53,11 +53,3 @@ func TestTimestampzExp(t *testing.T) { assertClauseSerialize(t, TimestampzExp(table1ColFloat).LT(timestampz), "(table1.col_float < $1)", "2000-01-31 10:20:05.000023 +200") } - -func TestTimestampzArithmetic(t *testing.T) { - timestampz := Timestampz(2000, 1, 1, 0, 0, 0, 100, "UTC") - assertClauseDebugSerialize(t, table1ColTimestampz.ADD(NewInterval(String("1 HOUR"))).EQ(timestampz), - "((table1.col_timestampz + INTERVAL '1 HOUR') = '2000-01-01 00:00:00.0000001 UTC')") - assertClauseDebugSerialize(t, table1ColTimestampz.SUB(NewInterval(String("1 HOUR"))).EQ(timestampz), - "((table1.col_timestampz - INTERVAL '1 HOUR') = '2000-01-01 00:00:00.0000001 UTC')") -} diff --git a/internal/jet/timez_expression.go b/internal/jet/timez_expression.go index 8896dce..590ec96 100644 --- a/internal/jet/timez_expression.go +++ b/internal/jet/timez_expression.go @@ -75,14 +75,15 @@ func (t *timezInterfaceImpl) SUB(rhs Interval) TimezExpression { //---------------------------------------------------// type timezExpressionWrapper struct { - timezInterfaceImpl Expression + timezInterfaceImpl } func newTimezExpressionWrap(expression Expression) TimezExpression { - timezExpressionWrap := timezExpressionWrapper{Expression: expression} - timezExpressionWrap.timezInterfaceImpl.parent = &timezExpressionWrap - return &timezExpressionWrap + timezExpressionWrap := &timezExpressionWrapper{Expression: expression} + timezExpressionWrap.timezInterfaceImpl.parent = timezExpressionWrap + expression.setParent(timezExpressionWrap) + return timezExpressionWrap } // TimezExp is time with time zone expression wrapper around arbitrary expression. diff --git a/internal/jet/timez_expression_test.go b/internal/jet/timez_expression_test.go index 9f21c08..104e613 100644 --- a/internal/jet/timez_expression_test.go +++ b/internal/jet/timez_expression_test.go @@ -51,11 +51,3 @@ func TestTimezExp(t *testing.T) { assertClauseSerialize(t, TimezExp(table1ColFloat).LT(Timez(1, 1, 1, 1, "+4:00")), "(table1.col_float < $1)", string("01:01:01.000000001 +4:00")) } - -func TestTimezArithmetic(t *testing.T) { - timez := Timez(0, 0, 0, 100, "UTC") - assertClauseDebugSerialize(t, table1ColTimez.ADD(NewInterval(String("1 HOUR"))).EQ(timez), - "((table1.col_timez + INTERVAL '1 HOUR') = '00:00:00.0000001 UTC')") - assertClauseDebugSerialize(t, table1ColTimez.SUB(NewInterval(String("1 HOUR"))).EQ(timez), - "((table1.col_timez - INTERVAL '1 HOUR') = '00:00:00.0000001 UTC')") -} diff --git a/internal/jet/utils.go b/internal/jet/utils.go index 1a62807..d793f24 100644 --- a/internal/jet/utils.go +++ b/internal/jet/utils.go @@ -71,7 +71,7 @@ func SerializeProjectionListJsonObj(statement StatementType, projections []Proje panic("jet: Projection is nil") } - p.serializeForJsonObj(statement, out) + p.serializeForJsonObjEntry(statement, out) } } diff --git a/internal/testutils/test_utils.go b/internal/testutils/test_utils.go index 2e2fc44..5bd1303 100644 --- a/internal/testutils/test_utils.go +++ b/internal/testutils/test_utils.go @@ -122,7 +122,7 @@ func AssertJsonEqual(t require.TestingT, actual, expected interface{}, option .. expectedJsonData, err := json.MarshalIndent(expected, "", "\t") require.NoError(t, err) - require.Equal(t, actualJsonData, expectedJsonData) + require.Equal(t, string(actualJsonData), string(expectedJsonData)) } // SaveJSONFile saves v as json at testRelativePath diff --git a/mysql/dialect.go b/mysql/dialect.go index 175b2c1..8f95156 100644 --- a/mysql/dialect.go +++ b/mysql/dialect.go @@ -6,7 +6,7 @@ import ( "github.com/go-jet/jet/v2/internal/jet" ) -// Dialect is implementation of MySQL dialect for SQL Builder serialisation. +// Dialect is implementation of MySQL dialect for SQL Builder serialization. var Dialect = newDialect() func newDialect() jet.Dialect { @@ -34,6 +34,23 @@ func newDialect() jet.Dialect { ValuesDefaultColumnName: func(index int) string { return fmt.Sprintf("column_%d", index) }, + JsonValueEncode: func(expr Expression) Expression { + switch e := expr.(type) { + case BlobExpression: + return TO_BASE64(e) + + // CustomExpression used bellow (instead DATE_FORMAT function) so that only expr is parametrized + case TimestampExpression: + return CustomExpression(Token("DATE_FORMAT("), e, Token(",'%Y-%m-%dT%H:%i:%s.%fZ')")) + case TimeExpression: + return CustomExpression(Token("CONCAT('0000-01-01T', DATE_FORMAT("), e, Token(",'%H:%i:%s.%fZ'))")) + case DateExpression: + return CustomExpression(Token("CONCAT(DATE_FORMAT("), e, Token(",'%Y-%m-%d')"), Token(", 'T00:00:00Z')")) + case BoolExpression: + return CustomExpression(e, Token(" = 1")) + } + return expr + }, } return jet.NewDialect(mySQLDialectParams) diff --git a/mysql/interval.go b/mysql/interval_literal.go similarity index 96% rename from mysql/interval.go rename to mysql/interval_literal.go index 23be21f..4fff705 100644 --- a/mysql/interval.go +++ b/mysql/interval_literal.go @@ -98,13 +98,10 @@ func INTERVAL(value interface{}, unitType unitType) Interval { // INTERVALe creates new temporal interval from expresion and unit type. func INTERVALe(expr Expression, unitType unitType) Interval { - return jet.NewInterval(jet.ListSerializer{ - Serializers: []jet.Serializer{expr, jet.RawWithParent(string(unitType))}, - Separator: " ", - }) + return jet.IntervalExp(CustomExpression(Token("INTERVAL"), expr, Token(unitType))) } -// INTERVALd temoral interval from time.Duration +// INTERVALd creates new temporal interval from time.Duration func INTERVALd(duration time.Duration) Interval { var sign int64 = 1 if duration < 0 { diff --git a/mysql/interval_test.go b/mysql/interval_literal_test.go similarity index 100% rename from mysql/interval_test.go rename to mysql/interval_literal_test.go diff --git a/mysql/select_json.go b/mysql/select_json.go index 990e4ea..cfeb30f 100644 --- a/mysql/select_json.go +++ b/mysql/select_json.go @@ -43,7 +43,7 @@ func newSelectStatementJson(projections []Projection, statementType jet.Statemen } func constructJsonFunc(projections []Projection, statementType jet.StatementType) Expression { - jsonObj := Func("JSON_OBJECT", CustomExpression(jet.JsonProjectionList(projections))) + jsonObj := Func("JSON_OBJECT", CustomExpression(jet.JsonObjProjectionList(projections))) if statementType == jet.SelectJsonArrStatementType { return Func("JSON_ARRAYAGG", jsonObj) diff --git a/postgres/columns.go b/postgres/columns.go index 390c23d..01af0d7 100644 --- a/postgres/columns.go +++ b/postgres/columns.go @@ -71,6 +71,12 @@ type ColumnTimestampz = jet.ColumnTimestampz // TimestampzColumn creates named timestamp with time zone column. var TimestampzColumn = jet.TimestampzColumn +// ColumnInterval is interface of PostgreSQL interval columns. +type ColumnInterval = jet.ColumnInterval + +// IntervalColumn creates named interval column +var IntervalColumn = jet.IntervalColumn + // ColumnDateRange is interface of SQL date range column type ColumnDateRange = jet.ColumnRange[DateExpression] @@ -106,41 +112,3 @@ type ColumnInt8Range jet.ColumnRange[jet.Int8Expression] // Int8RangeColumn creates named range with range column var Int8RangeColumn = jet.RangeColumn[jet.Int8Expression] - -//------------------------------------------------------// - -// ColumnInterval is interface of PostgreSQL interval columns. -type ColumnInterval interface { - IntervalExpression - jet.Column - - From(subQuery SelectTable) ColumnInterval - SET(intervalExp IntervalExpression) ColumnAssigment -} - -//------------------------------------------------------// - -type intervalColumnImpl struct { - jet.ColumnExpressionImpl - intervalInterfaceImpl -} - -func (i *intervalColumnImpl) SET(intervalExp IntervalExpression) ColumnAssigment { - return jet.NewColumnAssignment(i, intervalExp) -} - -func (i *intervalColumnImpl) From(subQuery SelectTable) ColumnInterval { - newIntervalColumn := IntervalColumn(i.Name()) - jet.SetTableName(newIntervalColumn, i.TableName()) - jet.SetSubQuery(newIntervalColumn, subQuery) - - return newIntervalColumn -} - -// IntervalColumn creates named interval column. -func IntervalColumn(name string) ColumnInterval { - intervalColumn := &intervalColumnImpl{} - intervalColumn.ColumnExpressionImpl = jet.NewColumnImpl(name, "", intervalColumn) - intervalColumn.intervalInterfaceImpl.parent = intervalColumn - return intervalColumn -} diff --git a/postgres/dialect.go b/postgres/dialect.go index 9e6160f..9ffa5f7 100644 --- a/postgres/dialect.go +++ b/postgres/dialect.go @@ -3,9 +3,8 @@ package postgres import ( "encoding/hex" "fmt" - "strconv" - "github.com/go-jet/jet/v2/internal/jet" + "strconv" ) // Dialect is implementation of postgres dialect for SQL Builder serialisation. @@ -32,6 +31,23 @@ func newDialect() jet.Dialect { ValuesDefaultColumnName: func(index int) string { return fmt.Sprintf("column%d", index+1) }, + JsonValueEncode: func(expr Expression) Expression { + switch e := expr.(type) { + case ByteaExpression: + return ENCODE(e, Base64) + + // CustomExpression used bellow (instead TO_CHAR function) so that only expr is parametrized + case TimeExpression: + return CustomExpression(Token("'0000-01-01T' || to_char('2000-10-10'::date + "), e, Token(`, 'HH24:MI:SS.USZ')`)) + case TimezExpression: + return CustomExpression(Token("'0000-01-01T' || to_char('2000-10-10'::date + "), e, Token(`, 'HH24:MI:SS.USTZH:TZM')`)) + case TimestampExpression: + return CustomExpression(Token("to_char("), e, Token(`, 'YYYY-MM-DD"T"HH24:MI:SS.USZ')`)) + case DateExpression: + return CustomExpression(Token("to_char("), e, Token(`::timestamp, 'YYYY-MM-DD') || 'T00:00:00Z'`)) + } + return expr + }, } return jet.NewDialect(dialectParams) diff --git a/postgres/expressions.go b/postgres/expressions.go index 510b4bf..f4fbb13 100644 --- a/postgres/expressions.go +++ b/postgres/expressions.go @@ -41,6 +41,9 @@ type TimestampzExpression = jet.TimestampzExpression // RowExpression interface type RowExpression = jet.RowExpression +// IntervalExpression interface +type IntervalExpression = jet.IntervalExpression + // DateRange Expression interface type DateRange = jet.Range[DateExpression] @@ -109,6 +112,11 @@ var TimestampExp = jet.TimestampExp // Does not add sql cast to generated sql builder output. var TimestampzExp = jet.TimestampzExp +// IntervalExp is interval expression wrapper around arbitrary expression. +// Allows go compiler to see any expression as interval expression. +// Does not add sql cast to generated sql builder output. +var IntervalExp = jet.IntervalExp + // RowExp serves as a wrapper for an arbitrary expression, treating it as a row expression. // This enables the Go compiler to interpret any expression as a row expression // Note: This does not modify the generated SQL builder output by adding a SQL CAST operation. diff --git a/postgres/functions.go b/postgres/functions.go index cf1f11e..2e92648 100644 --- a/postgres/functions.go +++ b/postgres/functions.go @@ -194,18 +194,18 @@ func CONCAT_WS(separator Expression, expressions ...Expression) StringExpression // Character encodings for CONVERT, CONVERT_FROM and CONVERT_TO functions var ( - UTF8 = String("UTF8") - LATIN1 = String("LATIN1") - LATIN2 = String("LATIN2") - LATIN3 = String("LATIN3") - LATIN4 = String("LATIN4") - WIN1252 = String("WIN1252") - ISO_8859_5 = String("ISO_8859_5") - ISO_8859_6 = String("ISO_8859_6") - ISO_8859_7 = String("ISO_8859_7") - ISO_8859_8 = String("ISO_8859_8") - KOI8R = String("KOI8R") - KOI8U = String("KOI8U") + UTF8 = StringExp(jet.FixedLiteral("UTF8")) + LATIN1 = StringExp(jet.FixedLiteral("LATIN1")) + LATIN2 = StringExp(jet.FixedLiteral("LATIN2")) + LATIN3 = StringExp(jet.FixedLiteral("LATIN3")) + LATIN4 = StringExp(jet.FixedLiteral("LATIN4")) + WIN1252 = StringExp(jet.FixedLiteral("WIN1252")) + ISO_8859_5 = StringExp(jet.FixedLiteral("ISO_8859_5")) + ISO_8859_6 = StringExp(jet.FixedLiteral("ISO_8859_6")) + ISO_8859_7 = StringExp(jet.FixedLiteral("ISO_8859_7")) + ISO_8859_8 = StringExp(jet.FixedLiteral("ISO_8859_8")) + KOI8R = StringExp(jet.FixedLiteral("KOI8R")) + KOI8U = StringExp(jet.FixedLiteral("KOI8U")) ) // CONVERT converts string to dest_encoding. The original encoding is @@ -223,9 +223,9 @@ var CONVERT_TO = jet.CONVERT_TO // ENCODE/DECODE textual formats var ( - Base64 StringExpression = String("base64") - Escape StringExpression = String("escape") - Hex StringExpression = String("hex") + Base64 = StringExp(jet.FixedLiteral("base64")) + Escape = StringExp(jet.FixedLiteral("escape")) + Hex = StringExp(jet.FixedLiteral("hex")) ) // ENCODE encodes binary data into a textual representation. diff --git a/postgres/interval_expression.go b/postgres/interval_expression.go deleted file mode 100644 index 91d8d8f..0000000 --- a/postgres/interval_expression.go +++ /dev/null @@ -1,257 +0,0 @@ -package postgres - -import ( - "fmt" - "github.com/go-jet/jet/v2/internal/jet" - "github.com/go-jet/jet/v2/internal/utils/datetime" - "strconv" - "strings" - "time" -) - -type quantityAndUnit = float64 -type unit = float64 - -// Interval unit types -const ( - YEAR unit = 123456789 + iota - MONTH - WEEK - DAY - HOUR - MINUTE - SECOND - MILLISECOND - MICROSECOND - DECADE - CENTURY - MILLENNIUM -) - -// IntervalExpression is representation of postgres INTERVAL -type IntervalExpression interface { - jet.IsInterval - jet.Expression - - EQ(rhs IntervalExpression) BoolExpression - NOT_EQ(rhs IntervalExpression) BoolExpression - IS_DISTINCT_FROM(rhs IntervalExpression) BoolExpression - IS_NOT_DISTINCT_FROM(rhs IntervalExpression) BoolExpression - - LT(rhs IntervalExpression) BoolExpression - LT_EQ(rhs IntervalExpression) BoolExpression - GT(rhs IntervalExpression) BoolExpression - GT_EQ(rhs IntervalExpression) BoolExpression - BETWEEN(min, max IntervalExpression) BoolExpression - NOT_BETWEEN(min, max IntervalExpression) BoolExpression - - ADD(rhs IntervalExpression) IntervalExpression - SUB(rhs IntervalExpression) IntervalExpression - - MUL(rhs NumericExpression) IntervalExpression - DIV(rhs NumericExpression) IntervalExpression -} - -type intervalInterfaceImpl struct { - jet.IsIntervalImpl - - parent IntervalExpression -} - -func (i *intervalInterfaceImpl) EQ(rhs IntervalExpression) BoolExpression { - return jet.Eq(i.parent, rhs) -} - -func (i *intervalInterfaceImpl) NOT_EQ(rhs IntervalExpression) BoolExpression { - return jet.NotEq(i.parent, rhs) -} - -func (i *intervalInterfaceImpl) IS_DISTINCT_FROM(rhs IntervalExpression) BoolExpression { - return jet.IsDistinctFrom(i.parent, rhs) -} - -func (i *intervalInterfaceImpl) IS_NOT_DISTINCT_FROM(rhs IntervalExpression) BoolExpression { - return jet.IsNotDistinctFrom(i.parent, rhs) -} - -func (i *intervalInterfaceImpl) LT(rhs IntervalExpression) BoolExpression { - return jet.Lt(i.parent, rhs) -} - -func (i *intervalInterfaceImpl) LT_EQ(rhs IntervalExpression) BoolExpression { - return jet.LtEq(i.parent, rhs) -} - -func (i *intervalInterfaceImpl) GT(rhs IntervalExpression) BoolExpression { - return jet.Gt(i.parent, rhs) -} - -func (i *intervalInterfaceImpl) GT_EQ(rhs IntervalExpression) BoolExpression { - return jet.GtEq(i.parent, rhs) -} - -func (i *intervalInterfaceImpl) BETWEEN(min, max IntervalExpression) BoolExpression { - return jet.NewBetweenOperatorExpression(i.parent, min, max, false) -} - -func (i *intervalInterfaceImpl) NOT_BETWEEN(min, max IntervalExpression) BoolExpression { - return jet.NewBetweenOperatorExpression(i.parent, min, max, true) -} - -func (i *intervalInterfaceImpl) ADD(rhs IntervalExpression) IntervalExpression { - return IntervalExp(jet.Add(i.parent, rhs)) -} - -func (i *intervalInterfaceImpl) SUB(rhs IntervalExpression) IntervalExpression { - return IntervalExp(jet.Sub(i.parent, rhs)) -} - -func (i *intervalInterfaceImpl) MUL(rhs NumericExpression) IntervalExpression { - return IntervalExp(jet.Mul(i.parent, rhs)) -} - -func (i *intervalInterfaceImpl) DIV(rhs NumericExpression) IntervalExpression { - return IntervalExp(jet.Div(i.parent, rhs)) -} - -type intervalExpression struct { - jet.Expression - intervalInterfaceImpl -} - -// INTERVAL creates new interval expression from the list of quantity-unit pairs. -// -// INTERVAL(1, DAY, 3, MINUTE) -func INTERVAL(quantityAndUnit ...quantityAndUnit) IntervalExpression { - quantityAndUnitLen := len(quantityAndUnit) - if quantityAndUnitLen == 0 || quantityAndUnitLen%2 != 0 { - panic("jet: invalid number of quantity and unit fields") - } - - var fields []string - - for i := 0; i < len(quantityAndUnit); i += 2 { - quantity := strconv.FormatFloat(quantityAndUnit[i], 'f', -1, 64) - unitString := unitToString(quantityAndUnit[i+1]) - fields = append(fields, quantity+" "+unitString) - } - - intervalStr := fmt.Sprintf("INTERVAL '%s'", strings.Join(fields, " ")) - - newInterval := &intervalExpression{} - - newInterval.Expression = jet.RawWithParent(intervalStr, newInterval) - newInterval.intervalInterfaceImpl.parent = newInterval - - return newInterval -} - -// INTERVALd creates interval expression from time.Duration -func INTERVALd(duration time.Duration) IntervalExpression { - days, hours, minutes, seconds, microseconds := datetime.ExtractTimeComponents(duration) - - var quantityAndUnits []quantityAndUnit - - if days > 0 { - quantityAndUnits = append(quantityAndUnits, quantityAndUnit(days)) - quantityAndUnits = append(quantityAndUnits, DAY) - } - - if hours > 0 { - quantityAndUnits = append(quantityAndUnits, quantityAndUnit(hours)) - quantityAndUnits = append(quantityAndUnits, HOUR) - } - - if minutes > 0 { - quantityAndUnits = append(quantityAndUnits, quantityAndUnit(minutes)) - quantityAndUnits = append(quantityAndUnits, MINUTE) - } - - if seconds > 0 { - quantityAndUnits = append(quantityAndUnits, quantityAndUnit(seconds)) - quantityAndUnits = append(quantityAndUnits, SECOND) - } - - if microseconds > 0 { - quantityAndUnits = append(quantityAndUnits, quantityAndUnit(microseconds)) - quantityAndUnits = append(quantityAndUnits, MICROSECOND) - } - - if len(quantityAndUnits) == 0 { - return INTERVAL(0, MICROSECOND) - } - - return INTERVAL(quantityAndUnits...) -} - -func unitToString(unit quantityAndUnit) string { - switch unit { - case YEAR: - return "YEAR" - case MONTH: - return "MONTH" - case WEEK: - return "WEEK" - case DAY: - return "DAY" - case HOUR: - return "HOUR" - case MINUTE: - return "MINUTE" - case SECOND: - return "SECOND" - case MILLISECOND: - return "MILLISECOND" - case MICROSECOND: - return "MICROSECOND" - case DECADE: - return "DECADE" - case CENTURY: - return "CENTURY" - case MILLENNIUM: - return "MILLENNIUM" - // additional field units for EXTRACT function - case DOW: - return "DOW" - case DOY: - return "DOY" - case EPOCH: - return "EPOCH" - case ISODOW: - return "ISODOW" - case ISOYEAR: - return "ISOYEAR" - case JULIAN: - return "JULIAN" - case QUARTER: - return "QUARTER" - case TIMEZONE: - return "TIMEZONE" - case TIMEZONE_HOUR: - return "TIMEZONE_HOUR" - case TIMEZONE_MINUTE: - return "TIMEZONE_MINUTE" - default: - panic("jet: invalid INTERVAL unit type") - } -} - -//---------------------------------------------------// - -type intervalWrapper struct { - intervalInterfaceImpl - Expression -} - -func newIntervalExpressionWrap(expression Expression) IntervalExpression { - intervalWrap := &intervalWrapper{Expression: expression} - intervalWrap.intervalInterfaceImpl.parent = intervalWrap - return intervalWrap -} - -// IntervalExp is interval expression wrapper around arbitrary expression. -// Allows go compiler to see any expression as interval expression. -// Does not add sql cast to generated sql builder output. -func IntervalExp(expression Expression) IntervalExpression { - return newIntervalExpressionWrap(expression) -} diff --git a/postgres/interval_literal.go b/postgres/interval_literal.go new file mode 100644 index 0000000..c36f539 --- /dev/null +++ b/postgres/interval_literal.go @@ -0,0 +1,140 @@ +package postgres + +import ( + "fmt" + "github.com/go-jet/jet/v2/internal/utils/datetime" + "strconv" + "strings" + "time" +) + +type quantityAndUnit = float64 +type unit = float64 + +// Interval unit types +const ( + YEAR unit = 123456789 + iota + MONTH + WEEK + DAY + HOUR + MINUTE + SECOND + MILLISECOND + MICROSECOND + DECADE + CENTURY + MILLENNIUM +) + +// INTERVAL creates new interval expression from the list of quantity-unit pairs. +// +// INTERVAL(1, DAY, 3, MINUTE) +func INTERVAL(quantityAndUnit ...quantityAndUnit) IntervalExpression { + quantityAndUnitLen := len(quantityAndUnit) + if quantityAndUnitLen == 0 || quantityAndUnitLen%2 != 0 { + panic("jet: invalid number of quantity and unit fields") + } + + var fields []string + + for i := 0; i < len(quantityAndUnit); i += 2 { + quantity := strconv.FormatFloat(quantityAndUnit[i], 'f', -1, 64) + unitString := unitToString(quantityAndUnit[i+1]) + fields = append(fields, quantity+" "+unitString) + } + + return IntervalExp(CustomExpression(Token(fmt.Sprintf("INTERVAL '%s'", strings.Join(fields, " "))))) +} + +// INTERVALd creates interval expression from time.Duration +func INTERVALd(duration time.Duration) IntervalExpression { + days, hours, minutes, seconds, microseconds := datetime.ExtractTimeComponents(duration) + + var quantityAndUnits []quantityAndUnit + + if days > 0 { + quantityAndUnits = append(quantityAndUnits, quantityAndUnit(days)) + quantityAndUnits = append(quantityAndUnits, DAY) + } + + if hours > 0 { + quantityAndUnits = append(quantityAndUnits, quantityAndUnit(hours)) + quantityAndUnits = append(quantityAndUnits, HOUR) + } + + if minutes > 0 { + quantityAndUnits = append(quantityAndUnits, quantityAndUnit(minutes)) + quantityAndUnits = append(quantityAndUnits, MINUTE) + } + + if seconds > 0 { + quantityAndUnits = append(quantityAndUnits, quantityAndUnit(seconds)) + quantityAndUnits = append(quantityAndUnits, SECOND) + } + + if microseconds > 0 { + quantityAndUnits = append(quantityAndUnits, quantityAndUnit(microseconds)) + quantityAndUnits = append(quantityAndUnits, MICROSECOND) + } + + if len(quantityAndUnits) == 0 { + return INTERVAL(0, MICROSECOND) + } + + return INTERVAL(quantityAndUnits...) +} + +func unitToString(unit quantityAndUnit) string { + switch unit { + case YEAR: + return "YEAR" + case MONTH: + return "MONTH" + case WEEK: + return "WEEK" + case DAY: + return "DAY" + case HOUR: + return "HOUR" + case MINUTE: + return "MINUTE" + case SECOND: + return "SECOND" + case MILLISECOND: + return "MILLISECOND" + case MICROSECOND: + return "MICROSECOND" + case DECADE: + return "DECADE" + case CENTURY: + return "CENTURY" + case MILLENNIUM: + return "MILLENNIUM" + // additional field units for EXTRACT function + case DOW: + return "DOW" + case DOY: + return "DOY" + case EPOCH: + return "EPOCH" + case ISODOW: + return "ISODOW" + case ISOYEAR: + return "ISOYEAR" + case JULIAN: + return "JULIAN" + case QUARTER: + return "QUARTER" + case TIMEZONE: + return "TIMEZONE" + case TIMEZONE_HOUR: + return "TIMEZONE_HOUR" + case TIMEZONE_MINUTE: + return "TIMEZONE_MINUTE" + default: + panic("jet: invalid INTERVAL unit type") + } +} + +//---------------------------------------------------// diff --git a/postgres/interval_expression_test.go b/postgres/interval_literal_test.go similarity index 100% rename from postgres/interval_expression_test.go rename to postgres/interval_literal_test.go diff --git a/postgres/select_json.go b/postgres/select_json.go index 5d23277..3e5ca51 100644 --- a/postgres/select_json.go +++ b/postgres/select_json.go @@ -105,6 +105,7 @@ func newSelectStatementJson(projections []Projection, statementType jet.Statemen } newSelectJson.setOperatorsImpl.stmtRoot = newSelectJson + newSelectJson.subQuery.Select.IsForRowToJson = true newSelectJson.setSubQueryAlias("") diff --git a/qrm/qrm.go b/qrm/qrm.go index f1ef6ad..572c621 100644 --- a/qrm/qrm.go +++ b/qrm/qrm.go @@ -3,9 +3,9 @@ package qrm import ( "context" "database/sql" + "encoding/json" "errors" "fmt" - "github.com/go-jet/jet/v2/internal/3rdparty/json" "github.com/go-jet/jet/v2/internal/utils/must" "reflect" ) @@ -67,8 +67,8 @@ func QueryJsonArr(ctx context.Context, db Queryable, query string, args []interf return queryJson(ctx, db, query, args, destPtr) } -var jsonDestObjErr = "jet: destination has to be a pointer to struct or pointer to map[string]any" -var jsonDestArrErr = "jet: destination has to be a pointer to slice of struct or pointer to []map[string]any" +var jsonDestObjErr = "jet: SELECT_JSON_OBJ destination has to be a pointer to struct or pointer to map[string]any" +var jsonDestArrErr = "jet: SELECT_JSON_ARR destination has to be a pointer to slice of struct or pointer to []map[string]any" func queryJson(ctx context.Context, db Queryable, query string, args []interface{}, destPtr interface{}) (rowsProcessed int64, err error) { must.BeInitializedPtr(db, "jet: db is nil") diff --git a/tests/mysql/alltypes_test.go b/tests/mysql/alltypes_test.go index dfba358..84902af 100644 --- a/tests/mysql/alltypes_test.go +++ b/tests/mysql/alltypes_test.go @@ -43,29 +43,82 @@ func TestAllTypesJSON(t *testing.T) { AllTypes.JSONPtr, AllTypes.Bit, AllTypes.BitPtr, - AllTypes.Blob, - AllTypes.BlobPtr, - AllTypes.Binary, - AllTypes.BinaryPtr, - AllTypes.VarBinary, - AllTypes.VarBinaryPtr, ), CAST(AllTypes.JSON).AS_CHAR().AS("Json"), CAST(AllTypes.JSONPtr).AS_CHAR().AS("JsonPtr"), CAST(AllTypes.Bit).AS_CHAR().AS("Bit"), CAST(AllTypes.BitPtr).AS_CHAR().AS("BitPtr"), - - // TODO: remove when binary string is implemented - CONCAT(String("\\x"), HEX(AllTypes.Blob)).AS("Blob"), - CONCAT(String("\\x"), HEX(AllTypes.BlobPtr)).AS("BlobPtr"), - - CONCAT(String("\\x"), HEX(AllTypes.Binary)).AS("Binary"), - CONCAT(String("\\x"), HEX(AllTypes.BinaryPtr)).AS("BinaryPtr"), - - CONCAT(String("\\x"), HEX(AllTypes.VarBinary)).AS("VarBinary"), - CONCAT(String("\\x"), HEX(AllTypes.VarBinaryPtr)).AS("VarBinaryPtr"), ).FROM(AllTypes) + testutils.AssertStatementSql(t, stmt, strings.ReplaceAll(` +SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'id', all_types.id, + 'boolean', all_types.boolean = 1, + 'booleanPtr', all_types.boolean_ptr = 1, + 'tinyInt', all_types.tiny_int, + 'uTinyInt', all_types.u_tiny_int, + 'smallInt', all_types.small_int, + 'uSmallInt', all_types.u_small_int, + 'mediumInt', all_types.medium_int, + 'uMediumInt', all_types.u_medium_int, + 'integer', all_types.''integer'', + 'uInteger', all_types.u_integer, + 'bigInt', all_types.big_int, + 'uBigInt', all_types.u_big_int, + 'tinyIntPtr', all_types.tiny_int_ptr, + 'uTinyIntPtr', all_types.u_tiny_int_ptr, + 'smallIntPtr', all_types.small_int_ptr, + 'uSmallIntPtr', all_types.u_small_int_ptr, + 'mediumIntPtr', all_types.medium_int_ptr, + 'uMediumIntPtr', all_types.u_medium_int_ptr, + 'integerPtr', all_types.integer_ptr, + 'uIntegerPtr', all_types.u_integer_ptr, + 'bigIntPtr', all_types.big_int_ptr, + 'uBigIntPtr', all_types.u_big_int_ptr, + 'decimal', all_types.''decimal'', + 'decimalPtr', all_types.decimal_ptr, + 'numeric', all_types.''numeric'', + 'numericPtr', all_types.numeric_ptr, + 'float', all_types.''float'', + 'floatPtr', all_types.float_ptr, + 'double', all_types.''double'', + 'doublePtr', all_types.double_ptr, + 'real', all_types.''real'', + 'realPtr', all_types.real_ptr, + 'time', CONCAT('0000-01-01T', DATE_FORMAT(all_types.time,'%H:%i:%s.%fZ')), + 'timePtr', CONCAT('0000-01-01T', DATE_FORMAT(all_types.time_ptr,'%H:%i:%s.%fZ')), + 'date', CONCAT(DATE_FORMAT(all_types.date,'%Y-%m-%d'), 'T00:00:00Z'), + 'datePtr', CONCAT(DATE_FORMAT(all_types.date_ptr,'%Y-%m-%d'), 'T00:00:00Z'), + 'dateTime', DATE_FORMAT(all_types.date_time,'%Y-%m-%dT%H:%i:%s.%fZ'), + 'dateTimePtr', DATE_FORMAT(all_types.date_time_ptr,'%Y-%m-%dT%H:%i:%s.%fZ'), + 'timestamp', DATE_FORMAT(all_types.timestamp,'%Y-%m-%dT%H:%i:%s.%fZ'), + 'timestampPtr', DATE_FORMAT(all_types.timestamp_ptr,'%Y-%m-%dT%H:%i:%s.%fZ'), + 'year', all_types.year, + 'yearPtr', all_types.year_ptr, + 'char', all_types.''char'', + 'charPtr', all_types.char_ptr, + 'varChar', all_types.var_char, + 'varCharPtr', all_types.var_char_ptr, + 'binary', TO_BASE64(all_types.''binary''), + 'binaryPtr', TO_BASE64(all_types.binary_ptr), + 'varBinary', TO_BASE64(all_types.var_binary), + 'varBinaryPtr', TO_BASE64(all_types.var_binary_ptr), + 'blob', TO_BASE64(all_types.''blob''), + 'blobPtr', TO_BASE64(all_types.blob_ptr), + 'text', all_types.text, + 'textPtr', all_types.text_ptr, + 'enum', all_types.enum, + 'enumPtr', all_types.enum_ptr, + 'set', all_types.''set'', + 'setPtr', all_types.set_ptr, + 'Json', CAST(all_types.json AS CHAR), + 'JsonPtr', CAST(all_types.json_ptr AS CHAR), + 'Bit', CAST(all_types.bit AS CHAR), + 'BitPtr', CAST(all_types.bit_ptr AS CHAR) + )) AS "json" +FROM test_sample.all_types; +`, "''", "`")) + var dest []model.AllTypes err := stmt.QueryJSON(ctx, db, &dest) @@ -76,7 +129,6 @@ func TestAllTypesJSON(t *testing.T) { dest[0].FloatPtr = ptr.Of(3.33) dest[1].Float = 3.33 - //fmt.Println(allTypesJson) testutils.AssertJSON(t, dest, allTypesJson) } @@ -1182,6 +1234,118 @@ func TestAllTypesInsertOnDuplicateKeyUpdate(t *testing.T) { require.NoError(t, err) } +func TestAllTypesSubQueryFrom(t *testing.T) { + subQuery := SELECT( + AllTypes.Boolean, + AllTypes.Integer, + AllTypes.Double, + AllTypes.Text, + AllTypes.Date, + AllTypes.Time, + AllTypes.Timestamp, + AllTypes.Blob, + ).FROM( + AllTypes, + ).AsTable("sub_query") + + stmt := SELECT( + AllTypes.Boolean.From(subQuery), + AllTypes.Integer.From(subQuery), + AllTypes.Double.From(subQuery), + AllTypes.Text.From(subQuery), + AllTypes.Date.From(subQuery), + AllTypes.Time.From(subQuery), + AllTypes.Timestamp.From(subQuery), + AllTypes.Blob.From(subQuery), + ).FROM( + subQuery, + ) + + testutils.AssertStatementSql(t, stmt, strings.ReplaceAll(` +SELECT sub_query.''all_types.boolean'' AS "all_types.boolean", + sub_query.''all_types.integer'' AS "all_types.integer", + sub_query.''all_types.double'' AS "all_types.double", + sub_query.''all_types.text'' AS "all_types.text", + sub_query.''all_types.date'' AS "all_types.date", + sub_query.''all_types.time'' AS "all_types.time", + sub_query.''all_types.timestamp'' AS "all_types.timestamp", + sub_query.''all_types.blob'' AS "all_types.blob" +FROM ( + SELECT all_types.boolean AS "all_types.boolean", + all_types.''integer'' AS "all_types.integer", + all_types.''double'' AS "all_types.double", + all_types.text AS "all_types.text", + all_types.date AS "all_types.date", + all_types.time AS "all_types.time", + all_types.timestamp AS "all_types.timestamp", + all_types.''blob'' AS "all_types.blob" + FROM test_sample.all_types + ) AS sub_query; +`, "''", "`")) + + var dest []model.AllTypes + + err := stmt.Query(db, &dest) + require.NoError(t, err) + require.NotEmpty(t, dest) + + t.Run("using SELECT_JSON", func(t *testing.T) { + stmtJson := SELECT_JSON_ARR( + AllTypes.Boolean.From(subQuery), + AllTypes.Integer.From(subQuery), + AllTypes.Double.From(subQuery), + AllTypes.Text.From(subQuery), + AllTypes.Date.From(subQuery), + AllTypes.Time.From(subQuery), + AllTypes.Timestamp.From(subQuery), + AllTypes.Blob.From(subQuery), + ).FROM( + subQuery, + ) + + testutils.AssertDebugStatementSql(t, stmtJson, strings.ReplaceAll(` +SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'boolean', sub_query.''all_types.boolean'' = 1, + 'integer', sub_query.''all_types.integer'', + 'double', sub_query.''all_types.double'', + 'text', sub_query.''all_types.text'', + 'date', CONCAT(DATE_FORMAT(sub_query.''all_types.date'','%Y-%m-%d'), 'T00:00:00Z'), + 'time', CONCAT('0000-01-01T', DATE_FORMAT(sub_query.''all_types.time'','%H:%i:%s.%fZ')), + 'timestamp', DATE_FORMAT(sub_query.''all_types.timestamp'','%Y-%m-%dT%H:%i:%s.%fZ'), + 'blob', TO_BASE64(sub_query.''all_types.blob'') + )) AS "json" +FROM ( + SELECT all_types.boolean AS "all_types.boolean", + all_types.''integer'' AS "all_types.integer", + all_types.''double'' AS "all_types.double", + all_types.text AS "all_types.text", + all_types.date AS "all_types.date", + all_types.time AS "all_types.time", + all_types.timestamp AS "all_types.timestamp", + all_types.''blob'' AS "all_types.blob" + FROM test_sample.all_types + ) AS sub_query; +`, "''", "`")) + + var destJson []model.AllTypes + + err := stmtJson.QueryJSON(ctx, db, &destJson) + require.NoError(t, err) + + t.Run("using AllColumns()", func(t *testing.T) { + stmtJsonAllColumns := SELECT_JSON_ARR( + subQuery.AllColumns(), + ).FROM( + subQuery, + ) + + require.Equal(t, stmtJson.DebugSql(), stmtJsonAllColumns.DebugSql()) + }) + + testutils.AssertJsonEqual(t, dest, destJson) + }) +} + var toInsert = model.AllTypes{ Boolean: false, BooleanPtr: ptr.Of(true), diff --git a/tests/mysql/select_json_test.go b/tests/mysql/select_json_test.go index 2d0ccef..ad05787 100644 --- a/tests/mysql/select_json_test.go +++ b/tests/mysql/select_json_test.go @@ -23,10 +23,12 @@ func TestSelectJsonObj(t *testing.T) { WHERE(Actor.ActorID.EQ(Int(2))) testutils.AssertStatementSql(t, stmt, ` -SELECT JSON_OBJECT('actorID', actor.actor_id, - 'firstName', actor.first_name, - 'lastName', actor.last_name, - 'lastUpdate', actor.last_update) AS "json" +SELECT JSON_OBJECT( + 'actorID', actor.actor_id, + 'firstName', actor.first_name, + 'lastName', actor.last_name, + 'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ') + ) AS "json" FROM dvds.actor WHERE actor.actor_id = ?; `, int64(2)) @@ -57,30 +59,34 @@ func TestSelectJsonObj_NestedObj(t *testing.T) { ) testutils.AssertStatementSql(t, stmt, ` -SELECT JSON_OBJECT('actorID', actor.actor_id, - 'firstName', actor.first_name, - 'lastName', actor.last_name, - 'lastUpdate', actor.last_update, - 'LongestFilm', ( - SELECT JSON_OBJECT('filmID', film.film_id, - 'title', film.title, - 'description', film.description, - 'releaseYear', film.release_year, - 'languageID', film.language_id, - 'originalLanguageID', film.original_language_id, - 'rentalDuration', film.rental_duration, - 'rentalRate', film.rental_rate, - 'length', film.length, - 'replacementCost', film.replacement_cost, - 'rating', film.rating, - 'specialFeatures', film.special_features, - 'lastUpdate', film.last_update) AS "json" - FROM dvds.film_actor - INNER JOIN dvds.film ON (film.film_id = film_actor.film_id) - WHERE actor.actor_id = film_actor.actor_id - ORDER BY film.length DESC - LIMIT ? - )) AS "json" +SELECT JSON_OBJECT( + 'actorID', actor.actor_id, + 'firstName', actor.first_name, + 'lastName', actor.last_name, + 'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ'), + 'LongestFilm', ( + SELECT JSON_OBJECT( + 'filmID', film.film_id, + 'title', film.title, + 'description', film.description, + 'releaseYear', film.release_year, + 'languageID', film.language_id, + 'originalLanguageID', film.original_language_id, + 'rentalDuration', film.rental_duration, + 'rentalRate', film.rental_rate, + 'length', film.length, + 'replacementCost', film.replacement_cost, + 'rating', film.rating, + 'specialFeatures', film.special_features, + 'lastUpdate', DATE_FORMAT(film.last_update,'%Y-%m-%dT%H:%i:%s.%fZ') + ) AS "json" + FROM dvds.film_actor + INNER JOIN dvds.film ON (film.film_id = film_actor.film_id) + WHERE actor.actor_id = film_actor.actor_id + ORDER BY film.length DESC + LIMIT ? + ) + ) AS "json" FROM dvds.actor WHERE actor.actor_id = ?; `) @@ -125,10 +131,12 @@ func TestSelectJsonArr(t *testing.T) { ORDER_BY(Actor.ActorID) testutils.AssertDebugStatementSql(t, stmt, ` -SELECT JSON_ARRAYAGG(JSON_OBJECT('actorID', actor.actor_id, - 'firstName', actor.first_name, - 'lastName', actor.last_name, - 'lastUpdate', actor.last_update)) AS "json" +SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'actorID', actor.actor_id, + 'firstName', actor.first_name, + 'lastName', actor.last_name, + 'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ') + )) AS "json" FROM dvds.actor ORDER BY actor.actor_id; `) @@ -169,29 +177,33 @@ func TestSelectJsonArr_NestedArr(t *testing.T) { ) testutils.AssertDebugStatementSql(t, stmt, ` -SELECT JSON_ARRAYAGG(JSON_OBJECT('actorID', actor.actor_id, - 'firstName', actor.first_name, - 'lastName', actor.last_name, - 'lastUpdate', actor.last_update, - 'Films', ( - SELECT JSON_ARRAYAGG(JSON_OBJECT('filmID', film.film_id, - 'title', film.title, - 'description', film.description, - 'releaseYear', film.release_year, - 'languageID', film.language_id, - 'originalLanguageID', film.original_language_id, - 'rentalDuration', film.rental_duration, - 'rentalRate', film.rental_rate, - 'length', film.length, - 'replacementCost', film.replacement_cost, - 'rating', film.rating, - 'specialFeatures', film.special_features, - 'lastUpdate', film.last_update)) AS "json" - FROM dvds.film_actor - INNER JOIN dvds.film ON ((film.film_id = film_actor.film_id) AND (actor.actor_id = film_actor.actor_id)) - WHERE (film.film_id % 17) = 0 - ORDER BY film.length DESC - ))) AS "json" +SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'actorID', actor.actor_id, + 'firstName', actor.first_name, + 'lastName', actor.last_name, + 'lastUpdate', DATE_FORMAT(actor.last_update,'%Y-%m-%dT%H:%i:%s.%fZ'), + 'Films', ( + SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'filmID', film.film_id, + 'title', film.title, + 'description', film.description, + 'releaseYear', film.release_year, + 'languageID', film.language_id, + 'originalLanguageID', film.original_language_id, + 'rentalDuration', film.rental_duration, + 'rentalRate', film.rental_rate, + 'length', film.length, + 'replacementCost', film.replacement_cost, + 'rating', film.rating, + 'specialFeatures', film.special_features, + 'lastUpdate', DATE_FORMAT(film.last_update,'%Y-%m-%dT%H:%i:%s.%fZ') + )) AS "json" + FROM dvds.film_actor + INNER JOIN dvds.film ON ((film.film_id = film_actor.film_id) AND (actor.actor_id = film_actor.actor_id)) + WHERE (film.film_id % 17) = 0 + ORDER BY film.length DESC + ) + )) AS "json" FROM dvds.actor WHERE actor.actor_id BETWEEN 1 AND 3 ORDER BY actor.actor_id; @@ -337,22 +349,26 @@ func TestSelectJson_GroupBy(t *testing.T) { ).FROM(subQuery) testutils.AssertDebugStatementSql(t, stmt, strings.ReplaceAll(` -SELECT JSON_ARRAYAGG(JSON_OBJECT('customerID', customers_info.""customer.customer_id"", - 'storeID', customers_info.""customer.store_id"", - 'firstName', customers_info.""customer.first_name"", - 'lastName', customers_info.""customer.last_name"", - 'email', customers_info.""customer.email"", - 'addressID', customers_info.""customer.address_id"", - 'active', customers_info.""customer.active"", - 'createDate', customers_info.""customer.create_date"", - 'lastUpdate', customers_info.""customer.last_update"", - 'amount', ( - SELECT JSON_OBJECT('sum', customers_info.sum, - 'avg', customers_info.avg, - 'max', customers_info.max, - 'min', customers_info.min, - 'count', customers_info.count) AS "json" - ))) AS "json" +SELECT JSON_ARRAYAGG(JSON_OBJECT( + 'customerID', customers_info.''customer.customer_id'', + 'storeID', customers_info.''customer.store_id'', + 'firstName', customers_info.''customer.first_name'', + 'lastName', customers_info.''customer.last_name'', + 'email', customers_info.''customer.email'', + 'addressID', customers_info.''customer.address_id'', + 'active', customers_info.''customer.active'' = 1, + 'createDate', DATE_FORMAT(customers_info.''customer.create_date'','%Y-%m-%dT%H:%i:%s.%fZ'), + 'lastUpdate', DATE_FORMAT(customers_info.''customer.last_update'','%Y-%m-%dT%H:%i:%s.%fZ'), + 'amount', ( + SELECT JSON_OBJECT( + 'sum', customers_info.sum, + 'avg', customers_info.avg, + 'max', customers_info.max, + 'min', customers_info.min, + 'count', customers_info.count + ) AS "json" + ) + )) AS "json" FROM ( SELECT customer.customer_id AS "customer.customer_id", customer.store_id AS "customer.store_id", @@ -374,7 +390,7 @@ FROM ( HAVING SUM(payment.amount) > 125 ORDER BY customer.customer_id, SUM(payment.amount) ASC ) AS customers_info; -`, `""`, "`")) +`, "''", "`")) var dest []struct { model.Customer @@ -389,7 +405,6 @@ FROM ( } err := stmt.QueryJSON(ctx, db, &dest) - fmt.Println(err) require.Nil(t, err) testutils.AssertJSONFile(t, dest, "./testdata/results/mysql/customer_payment_sum.json") diff --git a/tests/postgres/alltypes_test.go b/tests/postgres/alltypes_test.go index 96f15ad..0b0c0ec 100644 --- a/tests/postgres/alltypes_test.go +++ b/tests/postgres/alltypes_test.go @@ -2,10 +2,10 @@ package postgres import ( "encoding/base64" + "fmt" "github.com/go-jet/jet/v2/internal/utils/ptr" - "github.com/stretchr/testify/assert" - "github.com/go-jet/jet/v2/qrm" + "github.com/stretchr/testify/assert" "testing" "time" @@ -46,6 +46,7 @@ func TestAllTypesSelectJson(t *testing.T) { AllTypes.JsonbArray, AllTypes.IntegerArray, AllTypes.IntegerArrayPtr, AllTypes.TextMultiDimArray, AllTypes.TextMultiDimArrayPtr, ), + // unsupported at the moment, casting to text allows these columns to be assigned to string fields CAST(AllTypes.JSONPtr).AS_TEXT().AS("jsonPtr"), CAST(AllTypes.JSON).AS_TEXT().AS("JSON"), CAST(AllTypes.JsonbPtr).AS_TEXT().AS("jsonbPtr"), @@ -59,7 +60,75 @@ func TestAllTypesSelectJson(t *testing.T) { CAST(AllTypes.TextMultiDimArrayPtr).AS_TEXT().AS("TextMultiDimArrayPtr"), ).FROM(AllTypes) - //fmt.Println(stmt.DebugSql()) + testutils.AssertStatementSql(t, stmt, ` +SELECT json_agg(row_to_json(records)) AS "json" +FROM ( + SELECT all_types.small_int_ptr AS "smallIntPtr", + all_types.small_int AS "smallInt", + all_types.integer_ptr AS "integerPtr", + all_types.integer AS "integer", + all_types.big_int_ptr AS "bigIntPtr", + all_types.big_int AS "bigInt", + all_types.decimal_ptr AS "decimalPtr", + all_types.decimal AS "decimal", + all_types.numeric_ptr AS "numericPtr", + all_types.numeric AS "numeric", + all_types.real_ptr AS "realPtr", + all_types.real AS "real", + all_types.double_precision_ptr AS "doublePrecisionPtr", + all_types.double_precision AS "doublePrecision", + all_types.smallserial AS "smallserial", + all_types.serial AS "serial", + all_types.bigserial AS "bigserial", + all_types.var_char_ptr AS "varCharPtr", + all_types.var_char AS "varChar", + all_types.char_ptr AS "charPtr", + all_types.char AS "char", + all_types.text_ptr AS "textPtr", + all_types.text AS "text", + ENCODE(all_types.bytea_ptr, 'base64') AS "byteaPtr", + ENCODE(all_types.bytea, 'base64') AS "bytea", + all_types.timestampz_ptr AS "timestampzPtr", + all_types.timestampz AS "timestampz", + to_char(all_types.timestamp_ptr, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "timestampPtr", + to_char(all_types.timestamp, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "timestamp", + to_char(all_types.date_ptr::timestamp, 'YYYY-MM-DD') || 'T00:00:00Z' AS "datePtr", + to_char(all_types.date::timestamp, 'YYYY-MM-DD') || 'T00:00:00Z' AS "date", + '0000-01-01T' || to_char('2000-10-10'::date + all_types.timez_ptr, 'HH24:MI:SS.USTZH:TZM') AS "timezPtr", + '0000-01-01T' || to_char('2000-10-10'::date + all_types.timez, 'HH24:MI:SS.USTZH:TZM') AS "timez", + '0000-01-01T' || to_char('2000-10-10'::date + all_types.time_ptr, 'HH24:MI:SS.USZ') AS "timePtr", + '0000-01-01T' || to_char('2000-10-10'::date + all_types.time, 'HH24:MI:SS.USZ') AS "time", + all_types.interval_ptr AS "intervalPtr", + all_types.interval AS "interval", + all_types.boolean_ptr AS "booleanPtr", + all_types.boolean AS "boolean", + all_types.point_ptr AS "pointPtr", + all_types.bit_ptr AS "bitPtr", + all_types.bit AS "bit", + all_types.bit_varying_ptr AS "bitVaryingPtr", + all_types.bit_varying AS "bitVarying", + all_types.tsvector_ptr AS "tsvectorPtr", + all_types.tsvector AS "tsvector", + all_types.uuid_ptr AS "uuidPtr", + all_types.uuid AS "uuid", + all_types.xml_ptr AS "xmlPtr", + all_types.xml AS "xml", + all_types.mood_ptr AS "moodPtr", + all_types.mood AS "mood", + all_types.json_ptr::text AS "jsonPtr", + all_types.json::text AS "JSON", + all_types.jsonb_ptr::text AS "jsonbPtr", + all_types.jsonb::text AS "Jsonb", + all_types.text_array_ptr::text AS "TextArrayPtr", + all_types.text_array::text AS "TextArray", + all_types.jsonb_array::text AS "JsonbArray", + all_types.integer_array::text AS "IntegerArray", + all_types.integer_array_ptr::text AS "IntegerArrayPtr", + all_types.text_multi_dim_array::text AS "TextMultiDimArray", + all_types.text_multi_dim_array_ptr::text AS "TextMultiDimArrayPtr" + FROM test_sample.all_types + ) AS records; +`) var dest []model.AllTypes @@ -76,24 +145,30 @@ func TestAllTypesSelectJson(t *testing.T) { dest[1].CharPtr = allTypesRow1.CharPtr } - // set time local before comparison - dest[0].Timestampz = toCET(t, dest[0].Timestampz) + minus8 := time.FixedZone("UTC", -8*60*60) + plus1 := time.FixedZone("UTC", 60*60) - if dest[0].TimestampzPtr != nil { - dest[0].TimestampzPtr = ptr.Of(toCET(t, *dest[0].TimestampzPtr)) - } - dest[1].Timestampz = toCET(t, dest[1].Timestampz) + // set time local before comparison + dest[0].Timez = *toTZ(&dest[0].Timez, minus8) + dest[0].TimezPtr = toTZ(dest[0].TimezPtr, minus8) + dest[1].Timez = *toTZ(&dest[1].Timez, minus8) + dest[1].TimezPtr = toTZ(dest[1].TimezPtr, minus8) + + dest[0].Timestampz = *toTZ(&dest[0].Timestampz, plus1) + dest[0].TimestampzPtr = toTZ(dest[0].TimestampzPtr, plus1) + dest[1].Timestampz = *toTZ(&dest[1].Timestampz, plus1) + dest[1].TimestampzPtr = toTZ(dest[1].TimestampzPtr, plus1) testutils.AssertJsonEqual(t, dest[0], allTypesRow0) testutils.AssertJsonEqual(t, dest[1], allTypesRow1) } -func toCET(t *testing.T, tm time.Time) time.Time { - cet, err := time.LoadLocation("CET") // "Europe/Berlin" also works - if err != nil { - t.Fail() +func toTZ(tm *time.Time, loc *time.Location) *time.Time { + if tm == nil { + return nil } - return tm.In(cet) + + return ptr.Of(tm.In(loc)) } func TestAllTypesViewSelect(t *testing.T) { @@ -192,7 +267,7 @@ WHERE all_types.uuid = 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'; requireLogged(t, query) } -func TestBytea(t *testing.T) { +func TestByteaInsert(t *testing.T) { byteArrHex := "\\x48656c6c6f20476f7068657221" byteArrBin := []byte("\x48\x65\x6c\x6c\x6f\x20\x47\x6f\x70\x68\x65\x72\x21") @@ -602,22 +677,22 @@ func TestStringOperators(t *testing.T) { require.NoError(t, err) } -func TestBlob(t *testing.T) { +func TestBytea(t *testing.T) { - var sampleBlob = Bytea([]byte{11, 0, 22, 33, 44}) - var textBlob = Bytea([]byte("text blob")) + var sampleBytea = Bytea([]byte{11, 0, 22, 33, 44}) + var textBytea = Bytea([]byte("text blob")) stmt := SELECT( - AllTypes.Bytea.EQ(sampleBlob), + AllTypes.Bytea.EQ(sampleBytea), AllTypes.Bytea.EQ(AllTypes.ByteaPtr), - AllTypes.Bytea.NOT_EQ(sampleBlob), - AllTypes.Bytea.GT(textBlob), + AllTypes.Bytea.NOT_EQ(sampleBytea), + AllTypes.Bytea.GT(textBytea), AllTypes.Bytea.GT_EQ(AllTypes.ByteaPtr), AllTypes.Bytea.LT(AllTypes.ByteaPtr), - AllTypes.Bytea.LT_EQ(sampleBlob), + AllTypes.Bytea.LT_EQ(sampleBytea), AllTypes.Bytea.BETWEEN(Bytea([]byte("min")), Bytea([]byte("max"))), AllTypes.Bytea.NOT_BETWEEN(AllTypes.Bytea, AllTypes.ByteaPtr), - AllTypes.Bytea.CONCAT(textBlob), + AllTypes.Bytea.CONCAT(textBytea), func() ProjectionList { if sourceIsCockroachDB() { @@ -629,25 +704,25 @@ func TestBlob(t *testing.T) { AllTypes.Bytea.NOT_LIKE(Bytea("b'%pattern%'")), BTRIM(AllTypes.Bytea, Bytea([]byte{33})), - RTRIM(AllTypes.ByteaPtr, sampleBlob), - LTRIM(sampleBlob, textBlob), - CONCAT(sampleBlob, AllTypes.ByteaPtr, textBlob), - BIT_COUNT(sampleBlob).EQ(Int(3)), - LENGTH(textBlob, UTF8).EQ(Int(4)), + RTRIM(AllTypes.ByteaPtr, sampleBytea), + LTRIM(sampleBytea, textBytea), + CONCAT(sampleBytea, AllTypes.ByteaPtr, textBytea), + BIT_COUNT(sampleBytea).EQ(Int(3)), + LENGTH(textBytea, UTF8).EQ(Int(4)), - CONVERT(textBlob, UTF8, WIN1252), - CONVERT(AllTypes.Bytea, UTF8, LATIN1).EQ(sampleBlob), + CONVERT(textBytea, UTF8, WIN1252), + CONVERT(AllTypes.Bytea, UTF8, LATIN1).EQ(sampleBytea), } }(), - BIT_LENGTH(textBlob), - OCTET_LENGTH(textBlob), + BIT_LENGTH(textBytea), + OCTET_LENGTH(textBytea), - GET_BIT(textBlob, Int(2)).EQ(Int(23)), - GET_BYTE(sampleBlob, Int(1)).EQ(Int(0)), - SET_BIT(textBlob, Int(1), Int(0)).EQ(sampleBlob), - SET_BYTE(textBlob, Int(1), Int(0)).EQ(textBlob), - LENGTH(sampleBlob), + GET_BIT(textBytea, Int(2)).EQ(Int(23)), + GET_BYTE(sampleBytea, Int(1)).EQ(Int(0)), + SET_BIT(textBytea, Int(1), Int(0)).EQ(sampleBytea), + SET_BYTE(textBytea, Int(1), Int(0)).EQ(textBytea), + LENGTH(sampleBytea), SUBSTR(AllTypes.Bytea, Int(0), Int(2)), @@ -657,16 +732,16 @@ func TestBlob(t *testing.T) { SHA384(AllTypes.Bytea), SHA512(AllTypes.Bytea), - ENCODE(sampleBlob, Base64), - DECODE(String("A234C12B"), Hex).EQ(sampleBlob), + ENCODE(sampleBytea, Base64), + DECODE(String("A234C12B"), Hex).EQ(sampleBytea), CONVERT_FROM(AllTypes.ByteaPtr, UTF8).EQ(AllTypes.VarChar), - CONVERT_TO(AllTypes.Text, UTF8).NOT_EQ(textBlob), + CONVERT_TO(AllTypes.Text, UTF8).NOT_EQ(textBytea), RawBytea("DECODE(#1::text, #2)", RawArgs{ "#1": "A234C12B", "#2": "hex", - }).EQ(sampleBlob), + }).EQ(sampleBytea), ).FROM( AllTypes, ) @@ -690,27 +765,27 @@ SELECT all_types.bytea = $1::bytea, LTRIM($12::bytea, $13::bytea), CONCAT($14::bytea, all_types.bytea_ptr, $15::bytea), BIT_COUNT($16::bytea) = $17, - LENGTH($18::bytea, $19::text) = $20, - CONVERT($21::bytea, $22::text, $23::text), - CONVERT(all_types.bytea, $24::text, $25::text) = $26::bytea, - BIT_LENGTH($27::bytea), - OCTET_LENGTH($28::bytea), - GET_BIT($29::bytea, $30) = $31, - GET_BYTE($32::bytea, $33) = $34, - SET_BIT($35::bytea, $36, $37) = $38::bytea, - SET_BYTE($39::bytea, $40, $41) = $42::bytea, - LENGTH($43::bytea), - SUBSTR(all_types.bytea, $44, $45), + LENGTH($18::bytea, 'UTF8') = $19, + CONVERT($20::bytea, 'UTF8', 'WIN1252'), + CONVERT(all_types.bytea, 'UTF8', 'LATIN1') = $21::bytea, + BIT_LENGTH($22::bytea), + OCTET_LENGTH($23::bytea), + GET_BIT($24::bytea, $25) = $26, + GET_BYTE($27::bytea, $28) = $29, + SET_BIT($30::bytea, $31, $32) = $33::bytea, + SET_BYTE($34::bytea, $35, $36) = $37::bytea, + LENGTH($38::bytea), + SUBSTR(all_types.bytea, $39, $40), MD5(all_types.bytea), SHA224(all_types.bytea), SHA256(all_types.bytea), SHA384(all_types.bytea), SHA512(all_types.bytea), - ENCODE($46::bytea, $47::text), - DECODE($48::text, $49::text) = $50::bytea, - CONVERT_FROM(all_types.bytea_ptr, $51::text) = all_types.var_char, - CONVERT_TO(all_types.text, $52::text) != $53::bytea, - (DECODE($54::text, $55)) = $56::bytea + ENCODE($41::bytea, 'base64'), + DECODE($42::text, 'hex') = $43::bytea, + CONVERT_FROM(all_types.bytea_ptr, 'UTF8') = all_types.var_char, + CONVERT_TO(all_types.text, 'UTF8') != $44::bytea, + (DECODE($45::text, $46)) = $47::bytea FROM test_sample.all_types; `) } @@ -727,28 +802,75 @@ func TestBlobConversion(t *testing.T) { printable := []byte("this is blob") stmt := SELECT( - Bytea(nonPrintable).AS("non_printable"), - Bytea(printable).AS("printable"), + Bytea(nonPrintable).AS("test_dest.non_printable"), + Bytea(printable).AS("test_dest.printable"), - ENCODE(Bytea(nonPrintable), Base64).AS("non_printable_base64"), - CONVERT_FROM(Bytea(printable), UTF8).AS("printable_utf8"), + Bytea(nonPrintable).CONCAT(Bytea(printable)).AS("test_dest.bytea_concat"), + + ENCODE(Bytea(nonPrintable), Base64).AS("test_dest.non_printable_base64"), + CONVERT_FROM(Bytea(printable), UTF8).AS("test_dest.printable_utf8"), ) - var dest struct { + testutils.AssertDebugStatementSql(t, stmt, ` +SELECT '\x0b16212c37'::bytea AS "test_dest.non_printable", + '\x7468697320697320626c6f62'::bytea AS "test_dest.printable", + ('\x0b16212c37'::bytea || '\x7468697320697320626c6f62'::bytea) AS "test_dest.bytea_concat", + ENCODE('\x0b16212c37'::bytea, 'base64') AS "test_dest.non_printable_base64", + CONVERT_FROM('\x7468697320697320626c6f62'::bytea, 'UTF8') AS "test_dest.printable_utf8"; +`) + + type testDest struct { NonPrintable []byte Printable []byte - NonPrintableBase64 []byte + ByteaConcat []byte + NonPrintableBase64 string PrintableUTF8 string } + var dest testDest + err := stmt.Query(db, &dest) require.NoError(t, err) require.Equal(t, dest.NonPrintable, nonPrintable) require.Equal(t, dest.Printable, printable) - require.Equal(t, dest.NonPrintableBase64, []byte(base64.StdEncoding.EncodeToString(nonPrintable))) + + require.Equal(t, dest.ByteaConcat, append(nonPrintable, printable...)) + require.Equal(t, dest.NonPrintableBase64, base64.StdEncoding.EncodeToString(nonPrintable)) require.Equal(t, dest.PrintableUTF8, string(printable)) + + t.Run("using select json", func(t *testing.T) { + stmtJson := SELECT_JSON_OBJ( + Bytea(nonPrintable).AS("nonPrintable"), + Bytea(printable).AS("printable"), + + Bytea(nonPrintable).CONCAT(Bytea(printable)).AS("byteaConcat"), + + ENCODE(Bytea(nonPrintable), Base64).AS("nonPrintableBase64"), + CONVERT_FROM(Bytea(printable), UTF8).AS("printableUtf8"), + ) + + testutils.AssertStatementSql(t, stmtJson, ` +SELECT row_to_json(records) AS "json" +FROM ( + SELECT ENCODE($1::bytea, 'base64') AS "nonPrintable", + ENCODE($2::bytea, 'base64') AS "printable", + ENCODE($3::bytea || $4::bytea, 'base64') AS "byteaConcat", + ENCODE($5::bytea, 'base64') AS "nonPrintableBase64", + CONVERT_FROM($6::bytea, 'UTF8') AS "printableUtf8" + ) AS records; +`) + + var destSelectJson testDest + + err := stmtJson.QueryJSON(ctx, db, &destSelectJson) + require.NoError(t, err) + testutils.PrintJson(destSelectJson) + + require.Equal(t, dest, destSelectJson) + + }) } func TestBoolOperators(t *testing.T) { @@ -1140,6 +1262,190 @@ func TestTimeExpression(t *testing.T) { require.NoError(t, err) } +func TestTimeScan(t *testing.T) { + loc, err := time.LoadLocation("Japan") + require.NoError(t, err) + + timeT := time.Date(3, 3, 3, 11, 22, 33, 0, time.UTC) + timeWithNanoSeconds := time.Date(3, 3, 3, 1, 2, 3, 1000, time.UTC) + + timez := time.Date(3, 3, 3, 7, 8, 9, 0, time.UTC) + timezWithNanoSeconds := time.Date(3, 3, 3, 4, 5, 6, 1000, loc) + + // '1999-01-08 04:05:06' + timestamp := time.Date(1999, 01, 8, 4, 5, 6, 0, time.UTC) + timestampWithNanoSeconds := time.Date(3, 3, 3, 8, 9, 10, 1000, time.UTC) + + timestampz := time.Date(2003, 10, 3, 9, 10, 11, 0, loc) + timestampzWithNanoSeconds := time.Date(3, 3, 3, 8, 9, 10, 1000, loc) + + date := time.Date(2010, 2, 3, 0, 0, 0, 0, time.UTC) + + stmt := SELECT( + TimeT(timeT).AS("time"), + TimeT(timeWithNanoSeconds).AS("timeWithNanoSeconds"), + TimezT(timez).AS("timez"), + TimezT(timezWithNanoSeconds).AS("timezWithNanoSeconds"), + Timestamp(1999, 01, 8, 4, 5, 6).AS("timestamp"), + TimestampT(timestampWithNanoSeconds).AS("timestampWithNanoSeconds"), + TimestampzT(timestampz).AS("timestampz"), + TimestampzT(timestampzWithNanoSeconds).AS("timestampzWithNanoSeconds"), + DateT(date).AS("date"), + + TimeT(timeT).ADD(INTERVAL(2, HOUR)).AS("timeExpression"), + + SELECT_JSON_OBJ( + TimeT(timeT).AS("time"), + TimeT(timeWithNanoSeconds).AS("timeWithNanoSeconds"), + TimezT(timez).AS("timez"), + TimezT(timezWithNanoSeconds).AS("timezWithNanoSeconds"), + TimestampT(timestamp).AS("timestamp"), + TimestampT(timestampWithNanoSeconds).AS("timestampWithNanoSeconds"), + TimestampzT(timestampz).AS("timestampz"), + TimestampzT(timestampzWithNanoSeconds).AS("timestampzWithNanoSeconds"), + DateT(date).AS("date"), + + TimeT(timeT).ADD(INTERVAL(2, HOUR)).AS("timeExpression"), + ).AS("json"), + ) + + testutils.AssertStatementSql(t, stmt, ` +SELECT $1::time without time zone AS "time", + $2::time without time zone AS "timeWithNanoSeconds", + $3::time with time zone AS "timez", + $4::time with time zone AS "timezWithNanoSeconds", + $5::timestamp without time zone AS "timestamp", + $6::timestamp without time zone AS "timestampWithNanoSeconds", + $7::timestamp with time zone AS "timestampz", + $8::timestamp with time zone AS "timestampzWithNanoSeconds", + $9::date AS "date", + ($10::time without time zone + INTERVAL '2 HOUR') AS "timeExpression", + ( + SELECT row_to_json(json_records) AS "json_json" + FROM ( + SELECT '0000-01-01T' || to_char('2000-10-10'::date + $11::time without time zone, 'HH24:MI:SS.USZ') AS "time", + '0000-01-01T' || to_char('2000-10-10'::date + $12::time without time zone, 'HH24:MI:SS.USZ') AS "timeWithNanoSeconds", + '0000-01-01T' || to_char('2000-10-10'::date + $13::time with time zone, 'HH24:MI:SS.USTZH:TZM') AS "timez", + '0000-01-01T' || to_char('2000-10-10'::date + $14::time with time zone, 'HH24:MI:SS.USTZH:TZM') AS "timezWithNanoSeconds", + to_char($15::timestamp without time zone, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "timestamp", + to_char($16::timestamp without time zone, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "timestampWithNanoSeconds", + $17::timestamp with time zone AS "timestampz", + $18::timestamp with time zone AS "timestampzWithNanoSeconds", + to_char($19::date::timestamp, 'YYYY-MM-DD') || 'T00:00:00Z' AS "date", + '0000-01-01T' || to_char('2000-10-10'::date + ($20::time without time zone + INTERVAL '2 HOUR'), 'HH24:MI:SS.USZ') AS "timeExpression" + ) AS json_records + ) AS "json"; +`) + + var dest struct { + Time time.Time + TimeWithNanoSeconds time.Time + Timez time.Time + TimezWithNanoSeconds time.Time + Timestamp time.Time + TimestampWithNanoSeconds time.Time + Timestampz time.Time + TimestampzWithNanoSeconds time.Time + Date time.Time + + TimeExpression time.Time + + Json struct { + Time time.Time + TimeWithNanoSeconds time.Time + Timez time.Time + TimezWithNanoSeconds time.Time + Timestamp time.Time + TimestampWithNanoSeconds time.Time + Timestampz time.Time + TimestampzWithNanoSeconds time.Time + Date time.Time + + TimeExpression time.Time + } `json_column:"json"` + } + + err = stmt.Query(db, &dest) + require.NoError(t, err) + + ensureTimezEqual(t, timeT.Add(2*time.Hour), dest.TimeExpression, loc) + ensureTimezEqual(t, timeT.Add(2*time.Hour), dest.Json.TimeExpression, loc) + + ensureTimezEqual(t, timeT, dest.Time, loc) + ensureTimezEqual(t, timeT, dest.Json.Time, loc) + ensureTimezEqual(t, timeWithNanoSeconds, dest.TimeWithNanoSeconds, loc) + ensureTimezEqual(t, timeWithNanoSeconds, dest.Json.TimeWithNanoSeconds, loc) + + ensureTimezEqual(t, timez, dest.Timez, loc) + ensureTimezEqual(t, timez, dest.Json.Timez, loc) + ensureTimezEqual(t, timezWithNanoSeconds, dest.TimezWithNanoSeconds, loc) + ensureTimezEqual(t, timezWithNanoSeconds, dest.Json.TimezWithNanoSeconds, loc) + + ensureTimezEqual(t, timestamp, dest.Timestamp, loc) + ensureTimezEqual(t, timestamp, dest.Json.Timestamp, loc) + ensureTimezEqual(t, timestampWithNanoSeconds, dest.TimestampWithNanoSeconds, loc) + ensureTimezEqual(t, timestampWithNanoSeconds, dest.Json.TimestampWithNanoSeconds, loc) + + ensureTimezEqual(t, timestampz, dest.Timestampz, loc) + ensureTimezEqual(t, timestampz, dest.Json.Timestampz, loc) + ensureTimezEqual(t, timestampzWithNanoSeconds, dest.TimestampzWithNanoSeconds, loc) + ensureTimezEqual(t, timestampzWithNanoSeconds, dest.Json.TimestampzWithNanoSeconds, loc) + + ensureTimezEqual(t, date, dest.Date, loc) + ensureTimezEqual(t, date, dest.Json.Date, loc) + + t.Run("json only", func(t *testing.T) { + stmtJson := SELECT_JSON_OBJ( + TimeT(timeT).AS("time"), + TimeT(timeWithNanoSeconds).AS("timeWithNanoSeconds"), + + TimezT(timez).AS("timez"), + TimezT(timezWithNanoSeconds).AS("timezWithNanoSeconds"), + + Timestamp(1999, 01, 8, 4, 5, 6).AS("timestamp"), + TimestampT(timestampWithNanoSeconds).AS("timestampWithNanoSeconds"), + + TimestampzT(timestampz).AS("timestampz"), + TimestampzT(timestampzWithNanoSeconds).AS("timestampzWithNanoSeconds"), + + DateT(date).AS("date"), + ) + + var jsonDest struct { + Time time.Time + TimeWithNanoSeconds time.Time + + Timez time.Time + TimezWithNanoSeconds time.Time + + Timestamp time.Time + TimestampWithNanoSeconds time.Time + + Timestampz time.Time + TimestampzWithNanoSeconds time.Time + + Date time.Time + } + + err := stmtJson.QueryJSON(ctx, db, &jsonDest) + require.NoError(t, err) + }) +} + +func ensureTimezEqual(t *testing.T, time1, time2 time.Time, loc *time.Location) { + time1Loc := time1.In(loc) + time2Loc := time2.In(loc) + + require.Equal(t, time1Loc.Hour(), time2Loc.Hour()) + require.Equal(t, time1Loc.Minute(), time2Loc.Minute()) + require.Equal(t, time1Loc.Second(), time2Loc.Second()) + require.Equal(t, toMicroSeconds(time1Loc.Nanosecond()), toMicroSeconds(time2Loc.Nanosecond())) +} + +func toMicroSeconds(nanoseconds int) int { + return nanoseconds / 1000 +} + func TestIntervalSetFunctionality(t *testing.T) { t.Run("updateQueryIntervalTest", func(t *testing.T) { @@ -1251,7 +1557,50 @@ func TestInterval(t *testing.T) { AllTypes.IntervalPtr.DIV(Float(22.222)).EQ(AllTypes.IntervalPtr), ).FROM(AllTypes) - //fmt.Println(stmt.DebugSql()) + fmt.Println(stmt.Sql()) + + testutils.AssertDebugStatementSql(t, stmt, ` +SELECT INTERVAL '1 YEAR', + INTERVAL '1 MONTH', + INTERVAL '1 WEEK', + INTERVAL '1 DAY', + INTERVAL '1 HOUR', + INTERVAL '1 MINUTE', + INTERVAL '1 SECOND', + INTERVAL '1 MILLISECOND', + INTERVAL '1 MICROSECOND', + INTERVAL '1 DECADE', + INTERVAL '1 CENTURY', + INTERVAL '1 MILLENNIUM', + INTERVAL '1 YEAR 10 MONTH', + INTERVAL '1 YEAR 10 MONTH 20 DAY', + INTERVAL '1 YEAR 10 MONTH 20 DAY 3 HOUR', + INTERVAL '1 YEAR' IS NOT NULL, + INTERVAL '1 YEAR' AS "one year", + INTERVAL '0 MICROSECOND', + INTERVAL '1 MICROSECOND', + INTERVAL '1000 MICROSECOND', + INTERVAL '1 SECOND', + INTERVAL '1 MINUTE', + INTERVAL '1 HOUR', + INTERVAL '1 DAY', + INTERVAL '1 DAY 2 HOUR 3 MINUTE 4 SECOND 5 MICROSECOND', + (all_types.interval = INTERVAL '2 HOUR 20 MINUTE') = TRUE::boolean, + (all_types.interval_ptr != INTERVAL '2 HOUR 20 MINUTE') = FALSE::boolean, + (all_types.interval IS DISTINCT FROM INTERVAL '2 HOUR 20 MINUTE') = all_types.boolean, + (all_types.interval_ptr IS NOT DISTINCT FROM INTERVAL '10 MICROSECOND') = all_types.boolean, + (all_types.interval < all_types.interval_ptr) = all_types.boolean_ptr, + (all_types.interval <= all_types.interval_ptr) = all_types.boolean_ptr, + (all_types.interval > all_types.interval_ptr) = all_types.boolean_ptr, + (all_types.interval >= all_types.interval_ptr) = all_types.boolean_ptr, + all_types.interval BETWEEN INTERVAL '1 HOUR' AND INTERVAL '2 HOUR', + all_types.interval NOT BETWEEN all_types.interval_ptr AND INTERVAL '30 SECOND', + (all_types.interval + all_types.interval_ptr) = INTERVAL '17 SECOND', + (all_types.interval - all_types.interval_ptr) = INTERVAL '100 MICROSECOND', + (all_types.interval_ptr * 11) = all_types.interval, + (all_types.interval_ptr / 22.222) = all_types.interval_ptr +FROM test_sample.all_types; +`) err := stmt.Query(db, &struct{}{}) require.NoError(t, err) @@ -1368,6 +1717,7 @@ func TestAllTypesSubQueryFrom(t *testing.T) { AllTypes.Time, AllTypes.Timez, AllTypes.Timestamp, + AllTypes.Timestampz, AllTypes.Interval, AllTypes.Bytea, ).FROM( @@ -1383,6 +1733,7 @@ func TestAllTypesSubQueryFrom(t *testing.T) { AllTypes.Time.From(subQuery), AllTypes.Timez.From(subQuery), AllTypes.Timestamp.From(subQuery), + AllTypes.Timestampz.From(subQuery), AllTypes.Interval.From(subQuery), AllTypes.Bytea.From(subQuery), ).FROM( @@ -1398,6 +1749,7 @@ SELECT "subQuery"."all_types.boolean" AS "all_types.boolean", "subQuery"."all_types.time" AS "all_types.time", "subQuery"."all_types.timez" AS "all_types.timez", "subQuery"."all_types.timestamp" AS "all_types.timestamp", + "subQuery"."all_types.timestampz" AS "all_types.timestampz", "subQuery"."all_types.interval" AS "all_types.interval", "subQuery"."all_types.bytea" AS "all_types.bytea" FROM ( @@ -1409,6 +1761,7 @@ FROM ( all_types.time AS "all_types.time", all_types.timez AS "all_types.timez", all_types.timestamp AS "all_types.timestamp", + all_types.timestampz AS "all_types.timestampz", all_types.interval AS "all_types.interval", all_types.bytea AS "all_types.bytea" FROM test_sample.all_types @@ -1419,6 +1772,83 @@ FROM ( err := stmt.Query(db, &dest) require.NoError(t, err) + + t.Run("using SELECT_JSON", func(t *testing.T) { + stmtJson := SELECT_JSON_ARR( + AllTypes.Boolean.From(subQuery), + AllTypes.Integer.From(subQuery), + AllTypes.DoublePrecision.From(subQuery), + AllTypes.Text.From(subQuery), + AllTypes.Date.From(subQuery), + AllTypes.Time.From(subQuery), + AllTypes.Timez.From(subQuery), + AllTypes.Timestamp.From(subQuery), + AllTypes.Timestampz.From(subQuery), + AllTypes.Interval.From(subQuery), + AllTypes.Bytea.From(subQuery), + ).FROM( + subQuery, + ) + + testutils.AssertDebugStatementSql(t, stmtJson, ` +SELECT json_agg(row_to_json(records)) AS "json" +FROM ( + SELECT "subQuery"."all_types.boolean" AS "boolean", + "subQuery"."all_types.integer" AS "integer", + "subQuery"."all_types.double_precision" AS "doublePrecision", + "subQuery"."all_types.text" AS "text", + to_char("subQuery"."all_types.date"::timestamp, 'YYYY-MM-DD') || 'T00:00:00Z' AS "date", + '0000-01-01T' || to_char('2000-10-10'::date + "subQuery"."all_types.time", 'HH24:MI:SS.USZ') AS "time", + '0000-01-01T' || to_char('2000-10-10'::date + "subQuery"."all_types.timez", 'HH24:MI:SS.USTZH:TZM') AS "timez", + to_char("subQuery"."all_types.timestamp", 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "timestamp", + "subQuery"."all_types.timestampz" AS "timestampz", + "subQuery"."all_types.interval" AS "interval", + ENCODE("subQuery"."all_types.bytea", 'base64') AS "bytea" + FROM ( + SELECT all_types.boolean AS "all_types.boolean", + all_types.integer AS "all_types.integer", + all_types.double_precision AS "all_types.double_precision", + all_types.text AS "all_types.text", + all_types.date AS "all_types.date", + all_types.time AS "all_types.time", + all_types.timez AS "all_types.timez", + all_types.timestamp AS "all_types.timestamp", + all_types.timestampz AS "all_types.timestampz", + all_types.interval AS "all_types.interval", + all_types.bytea AS "all_types.bytea" + FROM test_sample.all_types + ) AS "subQuery" + ) AS records; +`) + + var destJson []model.AllTypes + + err := stmtJson.QueryJSON(ctx, db, &destJson) + require.NoError(t, err) + + t.Run("using AllColumns()", func(t *testing.T) { + stmtJsonAllColumns := SELECT_JSON_ARR( + subQuery.AllColumns(), + ).FROM( + subQuery, + ) + + require.Equal(t, stmtJson.DebugSql(), stmtJsonAllColumns.DebugSql()) + }) + + // fix timezone before comparisons + minus8 := time.FixedZone("UTC", -8*60*60) + destJson[0].Timez = *toTZ(&destJson[0].Timez, minus8) + destJson[1].Timez = *toTZ(&destJson[1].Timez, minus8) + + destJson[0].Timestampz = *toTZ(&destJson[0].Timestampz, time.UTC) + destJson[1].Timestampz = *toTZ(&destJson[1].Timestampz, time.UTC) + + dest[0].Timestampz = *toTZ(&dest[0].Timestampz, time.UTC) + dest[1].Timestampz = *toTZ(&dest[1].Timestampz, time.UTC) + + testutils.AssertJsonEqual(t, dest, destJson) + }) } func TestAllTypesUpdateSet(t *testing.T) { diff --git a/tests/postgres/chinook_db_test.go b/tests/postgres/chinook_db_test.go index a001ab5..8646895 100644 --- a/tests/postgres/chinook_db_test.go +++ b/tests/postgres/chinook_db_test.go @@ -251,9 +251,8 @@ func testJoinEverythingJSON(t require.TestingT) { WHERE(MediaType.MediaTypeId.EQ(Track.MediaTypeId)).AS("MediaType"), SELECT_JSON_ARR(Playlist.AllColumns). - FROM(Playlist.INNER_JOIN( - PlaylistTrack, - Playlist.PlaylistId.EQ(PlaylistTrack.PlaylistId))). + FROM(Playlist. + INNER_JOIN(PlaylistTrack, Playlist.PlaylistId.EQ(PlaylistTrack.PlaylistId))). WHERE(PlaylistTrack.TrackId.EQ(Track.TrackId)). ORDER_BY(Playlist.PlaylistId).AS("Playlists"), @@ -273,9 +272,9 @@ func testJoinEverythingJSON(t require.TestingT) { WHERE(Employee.EmployeeId.EQ(Customer.SupportRepId)).AS("Employee"), ).FROM(Customer). WHERE(Customer.CustomerId.EQ(Invoice.CustomerId)).AS("Customer"), - ).FROM(Invoice.INNER_JOIN( - InvoiceLine, - InvoiceLine.InvoiceId.EQ(Invoice.InvoiceId)), + ).FROM( + Invoice. + INNER_JOIN(InvoiceLine, InvoiceLine.InvoiceId.EQ(Invoice.InvoiceId)), ).WHERE(InvoiceLine.TrackId.EQ(Track.TrackId)). ORDER_BY(Invoice.InvoiceId).AS("Invoices"), ).FROM(Track). diff --git a/tests/postgres/main_test.go b/tests/postgres/main_test.go index c1d48d9..915387a 100644 --- a/tests/postgres/main_test.go +++ b/tests/postgres/main_test.go @@ -20,6 +20,8 @@ import ( _ "github.com/jackc/pgx/v4/stdlib" ) +var ctx = context.Background() + var db *stmtcache.DB var testRoot string diff --git a/tests/postgres/northwind_test.go b/tests/postgres/northwind_test.go index 63e1b1d..e2ae1d5 100644 --- a/tests/postgres/northwind_test.go +++ b/tests/postgres/northwind_test.go @@ -139,7 +139,7 @@ func testNorthwindJoinEverythingJson(t require.TestingT) { Territories, EmployeeTerritories.TerritoryID.EQ(Territories.TerritoryID)), ).WHERE( - EmployeeTerritories.EmployeeID.EQ(Employees.EmployeeID), // TODO: move to join + EmployeeTerritories.EmployeeID.EQ(Employees.EmployeeID), ).AS("Territories"), ).FROM(Employees). WHERE(Orders.EmployeeID.EQ(Employees.EmployeeID)).AS("Employee"), diff --git a/tests/postgres/select_json_test.go b/tests/postgres/select_json_test.go index a67eb71..c324597 100644 --- a/tests/postgres/select_json_test.go +++ b/tests/postgres/select_json_test.go @@ -1,7 +1,6 @@ package postgres import ( - "context" "github.com/go-jet/jet/v2/internal/testutils" "github.com/go-jet/jet/v2/internal/utils/ptr" "github.com/go-jet/jet/v2/qrm" @@ -15,8 +14,6 @@ import ( . "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/dvds/table" ) -var ctx = context.Background() - func TestSelectJsonObject(t *testing.T) { stmt := SELECT_JSON_OBJ(Actor.AllColumns). FROM(Actor). @@ -28,7 +25,7 @@ FROM ( SELECT actor.actor_id AS "actorID", actor.first_name AS "firstName", actor.last_name AS "lastName", - actor.last_update AS "lastUpdate" + to_char(actor.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate" FROM dvds.actor WHERE actor.actor_id = $1::integer ) AS records; @@ -38,7 +35,7 @@ FROM ( err := stmt.QueryJSON(ctx, db, &dest) require.NoError(t, err) - testutils.AssertDeepEqual(t, dest, actor2) + testutils.AssertJsonEqual(t, dest, actor2) requireLogged(t, stmt) t.Run("scan to map", func(t *testing.T) { @@ -51,7 +48,7 @@ FROM ( "actorID": float64(2), "firstName": "Nick", "lastName": "Wahlberg", - "lastUpdate": "2013-05-26T14:47:57.62", + "lastUpdate": "2013-05-26T14:47:57.620000Z", }) }) } @@ -158,19 +155,19 @@ FROM ( customer.email AS "email", customer.address_id AS "addressID", customer.activebool AS "activebool", - customer.create_date AS "createDate", - customer.last_update AS "lastUpdate", + to_char(customer.create_date::timestamp, 'YYYY-MM-DD') || 'T00:00:00Z' AS "createDate", + to_char(customer.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate", customer.active AS "active", ( SELECT json_agg(row_to_json(rentals_records)) AS "rentals_json" FROM ( SELECT rental.rental_id AS "rentalID", - rental.rental_date AS "rentalDate", + to_char(rental.rental_date, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "rentalDate", rental.inventory_id AS "inventoryID", rental.customer_id AS "customerID", - rental.return_date AS "returnDate", + to_char(rental.return_date, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "returnDate", rental.staff_id AS "staffID", - rental.last_update AS "lastUpdate" + to_char(rental.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate" FROM dvds.rental WHERE rental.customer_id = customer.customer_id ORDER BY rental.rental_id @@ -225,12 +222,12 @@ SELECT customer.customer_id AS "customer.customer_id", SELECT json_agg(row_to_json(rentals_records)) AS "rentals_json" FROM ( SELECT rental.rental_id AS "rentalID", - rental.rental_date AS "rentalDate", + to_char(rental.rental_date, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "rentalDate", rental.inventory_id AS "inventoryID", rental.customer_id AS "customerID", - rental.return_date AS "returnDate", + to_char(rental.return_date, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "returnDate", rental.staff_id AS "staffID", - rental.last_update AS "lastUpdate" + to_char(rental.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate" FROM dvds.rental WHERE rental.customer_id = customer.customer_id ORDER BY rental.rental_id @@ -310,8 +307,8 @@ FROM ( customer.email AS "email", customer.address_id AS "addressID", customer.activebool AS "activebool", - customer.create_date AS "createDate", - customer.last_update AS "lastUpdate", + to_char(customer.create_date::timestamp, 'YYYY-MM-DD') || 'T00:00:00Z' AS "createDate", + to_char(customer.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate", customer.active AS "active", ( SELECT row_to_json(amount_records) AS "amount_json" @@ -404,7 +401,7 @@ FROM ( SELECT actor.actor_id AS "actorID", actor.first_name AS "firstName", actor.last_name AS "lastName", - actor.last_update AS "lastUpdate", + to_char(actor.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate", ( SELECT json_agg(row_to_json(films_records)) AS "films_json" FROM ( @@ -418,7 +415,7 @@ FROM ( film.length AS "length", film.replacement_cost AS "replacementCost", film.rating AS "rating", - film.last_update AS "lastUpdate", + to_char(film.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate", film.fulltext AS "fulltext", film.special_features::text AS "SpecialFeatures", ( @@ -426,7 +423,7 @@ FROM ( FROM ( SELECT language.language_id AS "languageID", language.name AS "name", - language.last_update AS "lastUpdate" + to_char(language.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate" FROM dvds.language WHERE (language.language_id = film.language_id) AND (language.name = 'English'::char(20)) ) AS language_records @@ -436,7 +433,7 @@ FROM ( FROM ( SELECT category.category_id AS "categoryID", category.name AS "name", - category.last_update AS "lastUpdate" + to_char(category.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate" FROM dvds.category INNER JOIN dvds.film_category ON (film_category.category_id = category.category_id) WHERE (film_category.film_id = film.film_id) AND (category.name != 'Action'::text) @@ -518,8 +515,8 @@ RETURNING rental.rental_id AS "rental.rental_id", customer.email AS "email", customer.address_id AS "addressID", customer.activebool AS "activebool", - customer.create_date AS "createDate", - customer.last_update AS "lastUpdate", + to_char(customer.create_date::timestamp, 'YYYY-MM-DD') || 'T00:00:00Z' AS "createDate", + to_char(customer.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate", customer.active AS "active" FROM dvds.customer WHERE customer.customer_id = rental.customer_id @@ -575,7 +572,7 @@ FROM ( SELECT actor.actor_id AS "actorID", actor.first_name AS "firstName", actor.last_name AS "lastName", - actor.last_update AS "lastUpdate" + to_char(actor.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate" FROM dvds.actor ORDER BY actor.actor_id OFFSET 2 @@ -624,7 +621,7 @@ FROM ( SELECT actor.actor_id AS "actorID", actor.first_name AS "firstName", actor.last_name AS "lastName", - actor.last_update AS "lastUpdate" + to_char(actor.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate" FROM dvds.actor WHERE actor.actor_id = 200 FOR UPDATE NOWAIT @@ -667,7 +664,7 @@ func TestSelectJson_UNION(t *testing.T) { SELECT actor.actor_id AS "actorID", actor.first_name AS "firstName", actor.last_name AS "lastName", - actor.last_update AS "lastUpdate" + to_char(actor.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate" FROM dvds.actor WHERE actor.actor_id = 20 ) AS records @@ -679,7 +676,7 @@ UNION ALL SELECT actor.actor_id AS "actorID", actor.first_name AS "firstName", actor.last_name AS "lastName", - actor.last_update AS "lastUpdate" + to_char(actor.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate" FROM dvds.actor WHERE actor.actor_id = 21 ) AS records @@ -788,12 +785,12 @@ FROM ( SELECT json_agg(row_to_json(rentals_records)) AS "rentals_json" FROM ( SELECT rental.rental_id AS "rentalID", - rental.rental_date AS "rentalDate", + to_char(rental.rental_date, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "rentalDate", rental.inventory_id AS "inventoryID", rental.customer_id AS "customerID", - rental.return_date AS "returnDate", + to_char(rental.return_date, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "returnDate", rental.staff_id AS "staffID", - rental.last_update AS "lastUpdate" + to_char(rental.last_update, 'YYYY-MM-DD"T"HH24:MI:SS.USZ') AS "lastUpdate" FROM dvds.rental WHERE customer_list.id = rental.customer_id ORDER BY rental.customer_id @@ -813,9 +810,9 @@ FROM ( require.NoError(t, err) if sourceIsCockroachDB() { - require.Equal(t, string(dest.Json), `[{"Rentals": [{"customerID": 1, "inventoryID": 3021, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-05-25T11:30:37", "rentalID": 76, "returnDate": "2005-06-03T12:00:37", "staffID": 2}, {"customerID": 1, "inventoryID": 4020, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-05-28T10:35:23", "rentalID": 573, "returnDate": "2005-06-03T06:32:23", "staffID": 1}, {"customerID": 1, "inventoryID": 2785, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-06-15T00:54:12", "rentalID": 1185, "returnDate": "2005-06-23T02:42:12", "staffID": 2}, {"customerID": 1, "inventoryID": 1021, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-06-15T18:02:53", "rentalID": 1422, "returnDate": "2005-06-19T15:54:53", "staffID": 2}, {"customerID": 1, "inventoryID": 1407, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-06-15T21:08:46", "rentalID": 1476, "returnDate": "2005-06-25T02:26:46", "staffID": 1}, {"customerID": 1, "inventoryID": 726, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-06-16T15:18:57", "rentalID": 1725, "returnDate": "2005-06-17T21:05:57", "staffID": 1}, {"customerID": 1, "inventoryID": 197, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-06-18T08:41:48", "rentalID": 2308, "returnDate": "2005-06-22T03:36:48", "staffID": 2}, {"customerID": 1, "inventoryID": 3497, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-06-18T13:33:59", "rentalID": 2363, "returnDate": "2005-06-19T17:40:59", "staffID": 1}, {"customerID": 1, "inventoryID": 4566, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-06-21T06:24:45", "rentalID": 3284, "returnDate": "2005-06-28T03:28:45", "staffID": 1}, {"customerID": 1, "inventoryID": 1443, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-08T03:17:05", "rentalID": 4526, "returnDate": "2005-07-14T01:19:05", "staffID": 2}, {"customerID": 1, "inventoryID": 3486, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-08T07:33:56", "rentalID": 4611, "returnDate": "2005-07-12T13:25:56", "staffID": 2}, {"customerID": 1, "inventoryID": 3726, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-09T13:24:07", "rentalID": 5244, "returnDate": "2005-07-14T14:01:07", "staffID": 2}, {"customerID": 1, "inventoryID": 797, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-09T16:38:01", "rentalID": 5326, "returnDate": "2005-07-13T18:02:01", "staffID": 1}, {"customerID": 1, "inventoryID": 1330, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-11T10:13:46", "rentalID": 6163, "returnDate": "2005-07-19T13:15:46", "staffID": 2}, {"customerID": 1, "inventoryID": 2465, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-27T11:31:22", "rentalID": 7273, "returnDate": "2005-07-31T06:50:22", "staffID": 1}, {"customerID": 1, "inventoryID": 1092, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-28T09:04:45", "rentalID": 7841, "returnDate": "2005-07-30T12:37:45", "staffID": 2}, {"customerID": 1, "inventoryID": 4268, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-28T16:18:23", "rentalID": 8033, "returnDate": "2005-07-30T17:56:23", "staffID": 1}, {"customerID": 1, "inventoryID": 1558, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-28T17:33:39", "rentalID": 8074, "returnDate": "2005-07-29T20:17:39", "staffID": 1}, {"customerID": 1, "inventoryID": 4497, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-28T19:20:07", "rentalID": 8116, "returnDate": "2005-07-29T22:54:07", "staffID": 1}, {"customerID": 1, "inventoryID": 108, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-29T03:58:49", "rentalID": 8326, "returnDate": "2005-08-01T05:16:49", "staffID": 2}, {"customerID": 1, "inventoryID": 2219, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-31T02:42:18", "rentalID": 9571, "returnDate": "2005-08-02T23:26:18", "staffID": 2}, {"customerID": 1, "inventoryID": 14, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-01T08:51:04", "rentalID": 10437, "returnDate": "2005-08-10T12:12:04", "staffID": 1}, {"customerID": 1, "inventoryID": 3232, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-02T15:36:52", "rentalID": 11299, "returnDate": "2005-08-10T16:40:52", "staffID": 2}, {"customerID": 1, "inventoryID": 1440, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-02T18:01:38", "rentalID": 11367, "returnDate": "2005-08-04T13:19:38", "staffID": 1}, {"customerID": 1, "inventoryID": 2639, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-17T12:37:54", "rentalID": 11824, "returnDate": "2005-08-19T10:11:54", "staffID": 2}, {"customerID": 1, "inventoryID": 921, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-18T03:57:29", "rentalID": 12250, "returnDate": "2005-08-22T23:05:29", "staffID": 1}, {"customerID": 1, "inventoryID": 3019, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-19T09:55:16", "rentalID": 13068, "returnDate": "2005-08-20T14:44:16", "staffID": 2}, {"customerID": 1, "inventoryID": 2269, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-19T13:56:54", "rentalID": 13176, "returnDate": "2005-08-23T08:50:54", "staffID": 2}, {"customerID": 1, "inventoryID": 4249, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-21T23:33:57", "rentalID": 14762, "returnDate": "2005-08-23T01:30:57", "staffID": 1}, {"customerID": 1, "inventoryID": 1449, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-22T01:27:57", "rentalID": 14825, "returnDate": "2005-08-27T07:01:57", "staffID": 2}, {"customerID": 1, "inventoryID": 1446, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-22T19:41:37", "rentalID": 15298, "returnDate": "2005-08-28T22:49:37", "staffID": 1}, {"customerID": 1, "inventoryID": 312, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-22T20:03:46", "rentalID": 15315, "returnDate": "2005-08-30T01:51:46", "staffID": 2}], "address": "1913 Hanoi Way", "city": "Sasebo", "country": "Japan", "id": 1, "name": "Mary Smith", "notes": "active", "phone": "28303384290", "sid": 1, "zip code": "35200"}, {"Rentals": [{"customerID": 2, "inventoryID": 1090, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-05-27T00:09:24", "rentalID": 320, "returnDate": "2005-05-28T04:30:24", "staffID": 2}, {"customerID": 2, "inventoryID": 352, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-06-17T20:54:58", "rentalID": 2128, "returnDate": "2005-06-24T00:41:58", "staffID": 2}, {"customerID": 2, "inventoryID": 4116, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-10T06:31:24", "rentalID": 5636, "returnDate": "2005-07-13T02:36:24", "staffID": 1}, {"customerID": 2, "inventoryID": 2760, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-10T12:38:56", "rentalID": 5755, "returnDate": "2005-07-19T17:02:56", "staffID": 1}, {"customerID": 2, "inventoryID": 741, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-27T14:30:42", "rentalID": 7346, "returnDate": "2005-08-02T16:48:42", "staffID": 1}, {"customerID": 2, "inventoryID": 488, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-27T15:23:02", "rentalID": 7376, "returnDate": "2005-08-04T10:35:02", "staffID": 2}, {"customerID": 2, "inventoryID": 2053, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-27T18:40:20", "rentalID": 7459, "returnDate": "2005-08-02T21:07:20", "staffID": 2}, {"customerID": 2, "inventoryID": 1937, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-29T00:12:59", "rentalID": 8230, "returnDate": "2005-08-06T19:52:59", "staffID": 2}, {"customerID": 2, "inventoryID": 626, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-29T12:56:59", "rentalID": 8598, "returnDate": "2005-08-01T08:39:59", "staffID": 2}, {"customerID": 2, "inventoryID": 4038, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-29T17:14:29", "rentalID": 8705, "returnDate": "2005-08-02T16:01:29", "staffID": 1}, {"customerID": 2, "inventoryID": 2377, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-30T06:06:10", "rentalID": 9031, "returnDate": "2005-08-04T10:45:10", "staffID": 2}, {"customerID": 2, "inventoryID": 4030, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-30T13:47:43", "rentalID": 9236, "returnDate": "2005-08-08T18:52:43", "staffID": 1}, {"customerID": 2, "inventoryID": 1382, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-30T14:14:11", "rentalID": 9248, "returnDate": "2005-08-05T11:19:11", "staffID": 1}, {"customerID": 2, "inventoryID": 4088, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-30T16:21:13", "rentalID": 9296, "returnDate": "2005-08-08T11:57:13", "staffID": 1}, {"customerID": 2, "inventoryID": 3084, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-30T22:39:53", "rentalID": 9465, "returnDate": "2005-08-06T16:43:53", "staffID": 2}, {"customerID": 2, "inventoryID": 3142, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-07-31T21:58:56", "rentalID": 10136, "returnDate": "2005-08-03T19:44:56", "staffID": 1}, {"customerID": 2, "inventoryID": 138, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-01T09:45:26", "rentalID": 10466, "returnDate": "2005-08-06T06:28:26", "staffID": 1}, {"customerID": 2, "inventoryID": 3418, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-02T02:10:56", "rentalID": 10918, "returnDate": "2005-08-02T21:23:56", "staffID": 1}, {"customerID": 2, "inventoryID": 654, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-02T07:41:41", "rentalID": 11087, "returnDate": "2005-08-10T10:37:41", "staffID": 2}, {"customerID": 2, "inventoryID": 1149, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-02T10:43:48", "rentalID": 11177, "returnDate": "2005-08-10T10:55:48", "staffID": 2}, {"customerID": 2, "inventoryID": 2060, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-02T13:44:53", "rentalID": 11256, "returnDate": "2005-08-04T16:39:53", "staffID": 1}, {"customerID": 2, "inventoryID": 805, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-17T03:52:18", "rentalID": 11614, "returnDate": "2005-08-20T07:04:18", "staffID": 1}, {"customerID": 2, "inventoryID": 1521, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-19T06:26:04", "rentalID": 12963, "returnDate": "2005-08-23T11:37:04", "staffID": 2}, {"customerID": 2, "inventoryID": 3164, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-21T13:24:32", "rentalID": 14475, "returnDate": "2005-08-27T08:59:32", "staffID": 2}, {"customerID": 2, "inventoryID": 4570, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-21T22:41:56", "rentalID": 14743, "returnDate": "2005-08-29T00:18:56", "staffID": 1}, {"customerID": 2, "inventoryID": 2179, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-22T13:53:04", "rentalID": 15145, "returnDate": "2005-08-31T15:51:04", "staffID": 1}, {"customerID": 2, "inventoryID": 2898, "lastUpdate": "2006-02-16T02:30:53", "rentalDate": "2005-08-23T17:39:35", "rentalID": 15907, "returnDate": "2005-08-25T23:23:35", "staffID": 1}], "address": "1121 Loja Avenue", "city": "San Bernardino", "country": "United States", "id": 2, "name": "Patricia Johnson", "notes": "active", "phone": "838635286649", "sid": 1, "zip code": "17886"}]`) + require.Equal(t, string(dest.Json), `[{"Rentals": [{"customerID": 1, "inventoryID": 3021, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-05-25T11:30:37.000000Z", "rentalID": 76, "returnDate": "2005-06-03T12:00:37.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 4020, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-05-28T10:35:23.000000Z", "rentalID": 573, "returnDate": "2005-06-03T06:32:23.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 2785, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-06-15T00:54:12.000000Z", "rentalID": 1185, "returnDate": "2005-06-23T02:42:12.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 1021, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-06-15T18:02:53.000000Z", "rentalID": 1422, "returnDate": "2005-06-19T15:54:53.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 1407, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-06-15T21:08:46.000000Z", "rentalID": 1476, "returnDate": "2005-06-25T02:26:46.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 726, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-06-16T15:18:57.000000Z", "rentalID": 1725, "returnDate": "2005-06-17T21:05:57.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 197, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-06-18T08:41:48.000000Z", "rentalID": 2308, "returnDate": "2005-06-22T03:36:48.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 3497, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-06-18T13:33:59.000000Z", "rentalID": 2363, "returnDate": "2005-06-19T17:40:59.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 4566, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-06-21T06:24:45.000000Z", "rentalID": 3284, "returnDate": "2005-06-28T03:28:45.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 1443, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-08T03:17:05.000000Z", "rentalID": 4526, "returnDate": "2005-07-14T01:19:05.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 3486, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-08T07:33:56.000000Z", "rentalID": 4611, "returnDate": "2005-07-12T13:25:56.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 3726, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-09T13:24:07.000000Z", "rentalID": 5244, "returnDate": "2005-07-14T14:01:07.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 797, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-09T16:38:01.000000Z", "rentalID": 5326, "returnDate": "2005-07-13T18:02:01.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 1330, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-11T10:13:46.000000Z", "rentalID": 6163, "returnDate": "2005-07-19T13:15:46.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 2465, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-27T11:31:22.000000Z", "rentalID": 7273, "returnDate": "2005-07-31T06:50:22.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 1092, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-28T09:04:45.000000Z", "rentalID": 7841, "returnDate": "2005-07-30T12:37:45.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 4268, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-28T16:18:23.000000Z", "rentalID": 8033, "returnDate": "2005-07-30T17:56:23.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 1558, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-28T17:33:39.000000Z", "rentalID": 8074, "returnDate": "2005-07-29T20:17:39.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 4497, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-28T19:20:07.000000Z", "rentalID": 8116, "returnDate": "2005-07-29T22:54:07.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 108, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-29T03:58:49.000000Z", "rentalID": 8326, "returnDate": "2005-08-01T05:16:49.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 2219, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-31T02:42:18.000000Z", "rentalID": 9571, "returnDate": "2005-08-02T23:26:18.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 14, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-01T08:51:04.000000Z", "rentalID": 10437, "returnDate": "2005-08-10T12:12:04.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 3232, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-02T15:36:52.000000Z", "rentalID": 11299, "returnDate": "2005-08-10T16:40:52.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 1440, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-02T18:01:38.000000Z", "rentalID": 11367, "returnDate": "2005-08-04T13:19:38.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 2639, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-17T12:37:54.000000Z", "rentalID": 11824, "returnDate": "2005-08-19T10:11:54.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 921, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-18T03:57:29.000000Z", "rentalID": 12250, "returnDate": "2005-08-22T23:05:29.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 3019, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-19T09:55:16.000000Z", "rentalID": 13068, "returnDate": "2005-08-20T14:44:16.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 2269, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-19T13:56:54.000000Z", "rentalID": 13176, "returnDate": "2005-08-23T08:50:54.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 4249, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-21T23:33:57.000000Z", "rentalID": 14762, "returnDate": "2005-08-23T01:30:57.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 1449, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-22T01:27:57.000000Z", "rentalID": 14825, "returnDate": "2005-08-27T07:01:57.000000Z", "staffID": 2}, {"customerID": 1, "inventoryID": 1446, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-22T19:41:37.000000Z", "rentalID": 15298, "returnDate": "2005-08-28T22:49:37.000000Z", "staffID": 1}, {"customerID": 1, "inventoryID": 312, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-22T20:03:46.000000Z", "rentalID": 15315, "returnDate": "2005-08-30T01:51:46.000000Z", "staffID": 2}], "address": "1913 Hanoi Way", "city": "Sasebo", "country": "Japan", "id": 1, "name": "Mary Smith", "notes": "active", "phone": "28303384290", "sid": 1, "zip code": "35200"}, {"Rentals": [{"customerID": 2, "inventoryID": 1090, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-05-27T00:09:24.000000Z", "rentalID": 320, "returnDate": "2005-05-28T04:30:24.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 352, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-06-17T20:54:58.000000Z", "rentalID": 2128, "returnDate": "2005-06-24T00:41:58.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 4116, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-10T06:31:24.000000Z", "rentalID": 5636, "returnDate": "2005-07-13T02:36:24.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 2760, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-10T12:38:56.000000Z", "rentalID": 5755, "returnDate": "2005-07-19T17:02:56.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 741, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-27T14:30:42.000000Z", "rentalID": 7346, "returnDate": "2005-08-02T16:48:42.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 488, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-27T15:23:02.000000Z", "rentalID": 7376, "returnDate": "2005-08-04T10:35:02.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 2053, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-27T18:40:20.000000Z", "rentalID": 7459, "returnDate": "2005-08-02T21:07:20.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 1937, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-29T00:12:59.000000Z", "rentalID": 8230, "returnDate": "2005-08-06T19:52:59.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 626, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-29T12:56:59.000000Z", "rentalID": 8598, "returnDate": "2005-08-01T08:39:59.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 4038, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-29T17:14:29.000000Z", "rentalID": 8705, "returnDate": "2005-08-02T16:01:29.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 2377, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-30T06:06:10.000000Z", "rentalID": 9031, "returnDate": "2005-08-04T10:45:10.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 4030, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-30T13:47:43.000000Z", "rentalID": 9236, "returnDate": "2005-08-08T18:52:43.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 1382, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-30T14:14:11.000000Z", "rentalID": 9248, "returnDate": "2005-08-05T11:19:11.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 4088, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-30T16:21:13.000000Z", "rentalID": 9296, "returnDate": "2005-08-08T11:57:13.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 3084, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-30T22:39:53.000000Z", "rentalID": 9465, "returnDate": "2005-08-06T16:43:53.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 3142, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-07-31T21:58:56.000000Z", "rentalID": 10136, "returnDate": "2005-08-03T19:44:56.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 138, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-01T09:45:26.000000Z", "rentalID": 10466, "returnDate": "2005-08-06T06:28:26.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 3418, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-02T02:10:56.000000Z", "rentalID": 10918, "returnDate": "2005-08-02T21:23:56.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 654, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-02T07:41:41.000000Z", "rentalID": 11087, "returnDate": "2005-08-10T10:37:41.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 1149, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-02T10:43:48.000000Z", "rentalID": 11177, "returnDate": "2005-08-10T10:55:48.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 2060, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-02T13:44:53.000000Z", "rentalID": 11256, "returnDate": "2005-08-04T16:39:53.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 805, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-17T03:52:18.000000Z", "rentalID": 11614, "returnDate": "2005-08-20T07:04:18.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 1521, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-19T06:26:04.000000Z", "rentalID": 12963, "returnDate": "2005-08-23T11:37:04.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 3164, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-21T13:24:32.000000Z", "rentalID": 14475, "returnDate": "2005-08-27T08:59:32.000000Z", "staffID": 2}, {"customerID": 2, "inventoryID": 4570, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-21T22:41:56.000000Z", "rentalID": 14743, "returnDate": "2005-08-29T00:18:56.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 2179, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-22T13:53:04.000000Z", "rentalID": 15145, "returnDate": "2005-08-31T15:51:04.000000Z", "staffID": 1}, {"customerID": 2, "inventoryID": 2898, "lastUpdate": "2006-02-16T02:30:53.000000Z", "rentalDate": "2005-08-23T17:39:35.000000Z", "rentalID": 15907, "returnDate": "2005-08-25T23:23:35.000000Z", "staffID": 1}], "address": "1121 Loja Avenue", "city": "San Bernardino", "country": "United States", "id": 2, "name": "Patricia Johnson", "notes": "active", "phone": "838635286649", "sid": 1, "zip code": "17886"}]`) } else { - require.Equal(t, string(dest.Json), `[{"id":1,"name":"Mary Smith","address":"1913 Hanoi Way","zip code":"35200","phone":"28303384290","city":"Sasebo","country":"Japan","notes":"active","sid":1,"Rentals":[{"rentalID":76,"rentalDate":"2005-05-25T11:30:37","inventoryID":3021,"customerID":1,"returnDate":"2005-06-03T12:00:37","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":573,"rentalDate":"2005-05-28T10:35:23","inventoryID":4020,"customerID":1,"returnDate":"2005-06-03T06:32:23","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":1185,"rentalDate":"2005-06-15T00:54:12","inventoryID":2785,"customerID":1,"returnDate":"2005-06-23T02:42:12","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":1422,"rentalDate":"2005-06-15T18:02:53","inventoryID":1021,"customerID":1,"returnDate":"2005-06-19T15:54:53","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":1476,"rentalDate":"2005-06-15T21:08:46","inventoryID":1407,"customerID":1,"returnDate":"2005-06-25T02:26:46","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":1725,"rentalDate":"2005-06-16T15:18:57","inventoryID":726,"customerID":1,"returnDate":"2005-06-17T21:05:57","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":2308,"rentalDate":"2005-06-18T08:41:48","inventoryID":197,"customerID":1,"returnDate":"2005-06-22T03:36:48","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":2363,"rentalDate":"2005-06-18T13:33:59","inventoryID":3497,"customerID":1,"returnDate":"2005-06-19T17:40:59","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":3284,"rentalDate":"2005-06-21T06:24:45","inventoryID":4566,"customerID":1,"returnDate":"2005-06-28T03:28:45","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":4526,"rentalDate":"2005-07-08T03:17:05","inventoryID":1443,"customerID":1,"returnDate":"2005-07-14T01:19:05","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":4611,"rentalDate":"2005-07-08T07:33:56","inventoryID":3486,"customerID":1,"returnDate":"2005-07-12T13:25:56","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":5244,"rentalDate":"2005-07-09T13:24:07","inventoryID":3726,"customerID":1,"returnDate":"2005-07-14T14:01:07","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":5326,"rentalDate":"2005-07-09T16:38:01","inventoryID":797,"customerID":1,"returnDate":"2005-07-13T18:02:01","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":6163,"rentalDate":"2005-07-11T10:13:46","inventoryID":1330,"customerID":1,"returnDate":"2005-07-19T13:15:46","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":7273,"rentalDate":"2005-07-27T11:31:22","inventoryID":2465,"customerID":1,"returnDate":"2005-07-31T06:50:22","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":7841,"rentalDate":"2005-07-28T09:04:45","inventoryID":1092,"customerID":1,"returnDate":"2005-07-30T12:37:45","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":8033,"rentalDate":"2005-07-28T16:18:23","inventoryID":4268,"customerID":1,"returnDate":"2005-07-30T17:56:23","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":8074,"rentalDate":"2005-07-28T17:33:39","inventoryID":1558,"customerID":1,"returnDate":"2005-07-29T20:17:39","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":8116,"rentalDate":"2005-07-28T19:20:07","inventoryID":4497,"customerID":1,"returnDate":"2005-07-29T22:54:07","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":8326,"rentalDate":"2005-07-29T03:58:49","inventoryID":108,"customerID":1,"returnDate":"2005-08-01T05:16:49","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":9571,"rentalDate":"2005-07-31T02:42:18","inventoryID":2219,"customerID":1,"returnDate":"2005-08-02T23:26:18","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":10437,"rentalDate":"2005-08-01T08:51:04","inventoryID":14,"customerID":1,"returnDate":"2005-08-10T12:12:04","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":11299,"rentalDate":"2005-08-02T15:36:52","inventoryID":3232,"customerID":1,"returnDate":"2005-08-10T16:40:52","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":11367,"rentalDate":"2005-08-02T18:01:38","inventoryID":1440,"customerID":1,"returnDate":"2005-08-04T13:19:38","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":11824,"rentalDate":"2005-08-17T12:37:54","inventoryID":2639,"customerID":1,"returnDate":"2005-08-19T10:11:54","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":12250,"rentalDate":"2005-08-18T03:57:29","inventoryID":921,"customerID":1,"returnDate":"2005-08-22T23:05:29","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":13068,"rentalDate":"2005-08-19T09:55:16","inventoryID":3019,"customerID":1,"returnDate":"2005-08-20T14:44:16","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":13176,"rentalDate":"2005-08-19T13:56:54","inventoryID":2269,"customerID":1,"returnDate":"2005-08-23T08:50:54","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":14762,"rentalDate":"2005-08-21T23:33:57","inventoryID":4249,"customerID":1,"returnDate":"2005-08-23T01:30:57","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":14825,"rentalDate":"2005-08-22T01:27:57","inventoryID":1449,"customerID":1,"returnDate":"2005-08-27T07:01:57","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":15298,"rentalDate":"2005-08-22T19:41:37","inventoryID":1446,"customerID":1,"returnDate":"2005-08-28T22:49:37","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":15315,"rentalDate":"2005-08-22T20:03:46","inventoryID":312,"customerID":1,"returnDate":"2005-08-30T01:51:46","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}]}, {"id":2,"name":"Patricia Johnson","address":"1121 Loja Avenue","zip code":"17886","phone":"838635286649","city":"San Bernardino","country":"United States","notes":"active","sid":1,"Rentals":[{"rentalID":320,"rentalDate":"2005-05-27T00:09:24","inventoryID":1090,"customerID":2,"returnDate":"2005-05-28T04:30:24","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":2128,"rentalDate":"2005-06-17T20:54:58","inventoryID":352,"customerID":2,"returnDate":"2005-06-24T00:41:58","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":5636,"rentalDate":"2005-07-10T06:31:24","inventoryID":4116,"customerID":2,"returnDate":"2005-07-13T02:36:24","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":5755,"rentalDate":"2005-07-10T12:38:56","inventoryID":2760,"customerID":2,"returnDate":"2005-07-19T17:02:56","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":7346,"rentalDate":"2005-07-27T14:30:42","inventoryID":741,"customerID":2,"returnDate":"2005-08-02T16:48:42","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":7376,"rentalDate":"2005-07-27T15:23:02","inventoryID":488,"customerID":2,"returnDate":"2005-08-04T10:35:02","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":7459,"rentalDate":"2005-07-27T18:40:20","inventoryID":2053,"customerID":2,"returnDate":"2005-08-02T21:07:20","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":8230,"rentalDate":"2005-07-29T00:12:59","inventoryID":1937,"customerID":2,"returnDate":"2005-08-06T19:52:59","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":8598,"rentalDate":"2005-07-29T12:56:59","inventoryID":626,"customerID":2,"returnDate":"2005-08-01T08:39:59","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":8705,"rentalDate":"2005-07-29T17:14:29","inventoryID":4038,"customerID":2,"returnDate":"2005-08-02T16:01:29","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":9031,"rentalDate":"2005-07-30T06:06:10","inventoryID":2377,"customerID":2,"returnDate":"2005-08-04T10:45:10","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":9236,"rentalDate":"2005-07-30T13:47:43","inventoryID":4030,"customerID":2,"returnDate":"2005-08-08T18:52:43","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":9248,"rentalDate":"2005-07-30T14:14:11","inventoryID":1382,"customerID":2,"returnDate":"2005-08-05T11:19:11","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":9296,"rentalDate":"2005-07-30T16:21:13","inventoryID":4088,"customerID":2,"returnDate":"2005-08-08T11:57:13","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":9465,"rentalDate":"2005-07-30T22:39:53","inventoryID":3084,"customerID":2,"returnDate":"2005-08-06T16:43:53","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":10136,"rentalDate":"2005-07-31T21:58:56","inventoryID":3142,"customerID":2,"returnDate":"2005-08-03T19:44:56","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":10466,"rentalDate":"2005-08-01T09:45:26","inventoryID":138,"customerID":2,"returnDate":"2005-08-06T06:28:26","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":10918,"rentalDate":"2005-08-02T02:10:56","inventoryID":3418,"customerID":2,"returnDate":"2005-08-02T21:23:56","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":11087,"rentalDate":"2005-08-02T07:41:41","inventoryID":654,"customerID":2,"returnDate":"2005-08-10T10:37:41","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":11177,"rentalDate":"2005-08-02T10:43:48","inventoryID":1149,"customerID":2,"returnDate":"2005-08-10T10:55:48","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":11256,"rentalDate":"2005-08-02T13:44:53","inventoryID":2060,"customerID":2,"returnDate":"2005-08-04T16:39:53","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":11614,"rentalDate":"2005-08-17T03:52:18","inventoryID":805,"customerID":2,"returnDate":"2005-08-20T07:04:18","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":12963,"rentalDate":"2005-08-19T06:26:04","inventoryID":1521,"customerID":2,"returnDate":"2005-08-23T11:37:04","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":14475,"rentalDate":"2005-08-21T13:24:32","inventoryID":3164,"customerID":2,"returnDate":"2005-08-27T08:59:32","staffID":2,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":14743,"rentalDate":"2005-08-21T22:41:56","inventoryID":4570,"customerID":2,"returnDate":"2005-08-29T00:18:56","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":15145,"rentalDate":"2005-08-22T13:53:04","inventoryID":2179,"customerID":2,"returnDate":"2005-08-31T15:51:04","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}, {"rentalID":15907,"rentalDate":"2005-08-23T17:39:35","inventoryID":2898,"customerID":2,"returnDate":"2005-08-25T23:23:35","staffID":1,"lastUpdate":"2006-02-16T02:30:53"}]}]`) + require.Equal(t, string(dest.Json), `[{"id":1,"name":"Mary Smith","address":"1913 Hanoi Way","zip code":"35200","phone":"28303384290","city":"Sasebo","country":"Japan","notes":"active","sid":1,"Rentals":[{"rentalID":76,"rentalDate":"2005-05-25T11:30:37.000000Z","inventoryID":3021,"customerID":1,"returnDate":"2005-06-03T12:00:37.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":573,"rentalDate":"2005-05-28T10:35:23.000000Z","inventoryID":4020,"customerID":1,"returnDate":"2005-06-03T06:32:23.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":1185,"rentalDate":"2005-06-15T00:54:12.000000Z","inventoryID":2785,"customerID":1,"returnDate":"2005-06-23T02:42:12.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":1422,"rentalDate":"2005-06-15T18:02:53.000000Z","inventoryID":1021,"customerID":1,"returnDate":"2005-06-19T15:54:53.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":1476,"rentalDate":"2005-06-15T21:08:46.000000Z","inventoryID":1407,"customerID":1,"returnDate":"2005-06-25T02:26:46.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":1725,"rentalDate":"2005-06-16T15:18:57.000000Z","inventoryID":726,"customerID":1,"returnDate":"2005-06-17T21:05:57.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":2308,"rentalDate":"2005-06-18T08:41:48.000000Z","inventoryID":197,"customerID":1,"returnDate":"2005-06-22T03:36:48.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":2363,"rentalDate":"2005-06-18T13:33:59.000000Z","inventoryID":3497,"customerID":1,"returnDate":"2005-06-19T17:40:59.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":3284,"rentalDate":"2005-06-21T06:24:45.000000Z","inventoryID":4566,"customerID":1,"returnDate":"2005-06-28T03:28:45.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":4526,"rentalDate":"2005-07-08T03:17:05.000000Z","inventoryID":1443,"customerID":1,"returnDate":"2005-07-14T01:19:05.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":4611,"rentalDate":"2005-07-08T07:33:56.000000Z","inventoryID":3486,"customerID":1,"returnDate":"2005-07-12T13:25:56.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":5244,"rentalDate":"2005-07-09T13:24:07.000000Z","inventoryID":3726,"customerID":1,"returnDate":"2005-07-14T14:01:07.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":5326,"rentalDate":"2005-07-09T16:38:01.000000Z","inventoryID":797,"customerID":1,"returnDate":"2005-07-13T18:02:01.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":6163,"rentalDate":"2005-07-11T10:13:46.000000Z","inventoryID":1330,"customerID":1,"returnDate":"2005-07-19T13:15:46.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":7273,"rentalDate":"2005-07-27T11:31:22.000000Z","inventoryID":2465,"customerID":1,"returnDate":"2005-07-31T06:50:22.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":7841,"rentalDate":"2005-07-28T09:04:45.000000Z","inventoryID":1092,"customerID":1,"returnDate":"2005-07-30T12:37:45.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":8033,"rentalDate":"2005-07-28T16:18:23.000000Z","inventoryID":4268,"customerID":1,"returnDate":"2005-07-30T17:56:23.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":8074,"rentalDate":"2005-07-28T17:33:39.000000Z","inventoryID":1558,"customerID":1,"returnDate":"2005-07-29T20:17:39.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":8116,"rentalDate":"2005-07-28T19:20:07.000000Z","inventoryID":4497,"customerID":1,"returnDate":"2005-07-29T22:54:07.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":8326,"rentalDate":"2005-07-29T03:58:49.000000Z","inventoryID":108,"customerID":1,"returnDate":"2005-08-01T05:16:49.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":9571,"rentalDate":"2005-07-31T02:42:18.000000Z","inventoryID":2219,"customerID":1,"returnDate":"2005-08-02T23:26:18.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":10437,"rentalDate":"2005-08-01T08:51:04.000000Z","inventoryID":14,"customerID":1,"returnDate":"2005-08-10T12:12:04.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":11299,"rentalDate":"2005-08-02T15:36:52.000000Z","inventoryID":3232,"customerID":1,"returnDate":"2005-08-10T16:40:52.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":11367,"rentalDate":"2005-08-02T18:01:38.000000Z","inventoryID":1440,"customerID":1,"returnDate":"2005-08-04T13:19:38.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":11824,"rentalDate":"2005-08-17T12:37:54.000000Z","inventoryID":2639,"customerID":1,"returnDate":"2005-08-19T10:11:54.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":12250,"rentalDate":"2005-08-18T03:57:29.000000Z","inventoryID":921,"customerID":1,"returnDate":"2005-08-22T23:05:29.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":13068,"rentalDate":"2005-08-19T09:55:16.000000Z","inventoryID":3019,"customerID":1,"returnDate":"2005-08-20T14:44:16.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":13176,"rentalDate":"2005-08-19T13:56:54.000000Z","inventoryID":2269,"customerID":1,"returnDate":"2005-08-23T08:50:54.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":14762,"rentalDate":"2005-08-21T23:33:57.000000Z","inventoryID":4249,"customerID":1,"returnDate":"2005-08-23T01:30:57.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":14825,"rentalDate":"2005-08-22T01:27:57.000000Z","inventoryID":1449,"customerID":1,"returnDate":"2005-08-27T07:01:57.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":15298,"rentalDate":"2005-08-22T19:41:37.000000Z","inventoryID":1446,"customerID":1,"returnDate":"2005-08-28T22:49:37.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":15315,"rentalDate":"2005-08-22T20:03:46.000000Z","inventoryID":312,"customerID":1,"returnDate":"2005-08-30T01:51:46.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}]}, {"id":2,"name":"Patricia Johnson","address":"1121 Loja Avenue","zip code":"17886","phone":"838635286649","city":"San Bernardino","country":"United States","notes":"active","sid":1,"Rentals":[{"rentalID":320,"rentalDate":"2005-05-27T00:09:24.000000Z","inventoryID":1090,"customerID":2,"returnDate":"2005-05-28T04:30:24.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":2128,"rentalDate":"2005-06-17T20:54:58.000000Z","inventoryID":352,"customerID":2,"returnDate":"2005-06-24T00:41:58.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":5636,"rentalDate":"2005-07-10T06:31:24.000000Z","inventoryID":4116,"customerID":2,"returnDate":"2005-07-13T02:36:24.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":5755,"rentalDate":"2005-07-10T12:38:56.000000Z","inventoryID":2760,"customerID":2,"returnDate":"2005-07-19T17:02:56.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":7346,"rentalDate":"2005-07-27T14:30:42.000000Z","inventoryID":741,"customerID":2,"returnDate":"2005-08-02T16:48:42.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":7376,"rentalDate":"2005-07-27T15:23:02.000000Z","inventoryID":488,"customerID":2,"returnDate":"2005-08-04T10:35:02.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":7459,"rentalDate":"2005-07-27T18:40:20.000000Z","inventoryID":2053,"customerID":2,"returnDate":"2005-08-02T21:07:20.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":8230,"rentalDate":"2005-07-29T00:12:59.000000Z","inventoryID":1937,"customerID":2,"returnDate":"2005-08-06T19:52:59.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":8598,"rentalDate":"2005-07-29T12:56:59.000000Z","inventoryID":626,"customerID":2,"returnDate":"2005-08-01T08:39:59.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":8705,"rentalDate":"2005-07-29T17:14:29.000000Z","inventoryID":4038,"customerID":2,"returnDate":"2005-08-02T16:01:29.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":9031,"rentalDate":"2005-07-30T06:06:10.000000Z","inventoryID":2377,"customerID":2,"returnDate":"2005-08-04T10:45:10.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":9236,"rentalDate":"2005-07-30T13:47:43.000000Z","inventoryID":4030,"customerID":2,"returnDate":"2005-08-08T18:52:43.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":9248,"rentalDate":"2005-07-30T14:14:11.000000Z","inventoryID":1382,"customerID":2,"returnDate":"2005-08-05T11:19:11.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":9296,"rentalDate":"2005-07-30T16:21:13.000000Z","inventoryID":4088,"customerID":2,"returnDate":"2005-08-08T11:57:13.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":9465,"rentalDate":"2005-07-30T22:39:53.000000Z","inventoryID":3084,"customerID":2,"returnDate":"2005-08-06T16:43:53.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":10136,"rentalDate":"2005-07-31T21:58:56.000000Z","inventoryID":3142,"customerID":2,"returnDate":"2005-08-03T19:44:56.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":10466,"rentalDate":"2005-08-01T09:45:26.000000Z","inventoryID":138,"customerID":2,"returnDate":"2005-08-06T06:28:26.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":10918,"rentalDate":"2005-08-02T02:10:56.000000Z","inventoryID":3418,"customerID":2,"returnDate":"2005-08-02T21:23:56.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":11087,"rentalDate":"2005-08-02T07:41:41.000000Z","inventoryID":654,"customerID":2,"returnDate":"2005-08-10T10:37:41.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":11177,"rentalDate":"2005-08-02T10:43:48.000000Z","inventoryID":1149,"customerID":2,"returnDate":"2005-08-10T10:55:48.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":11256,"rentalDate":"2005-08-02T13:44:53.000000Z","inventoryID":2060,"customerID":2,"returnDate":"2005-08-04T16:39:53.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":11614,"rentalDate":"2005-08-17T03:52:18.000000Z","inventoryID":805,"customerID":2,"returnDate":"2005-08-20T07:04:18.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":12963,"rentalDate":"2005-08-19T06:26:04.000000Z","inventoryID":1521,"customerID":2,"returnDate":"2005-08-23T11:37:04.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":14475,"rentalDate":"2005-08-21T13:24:32.000000Z","inventoryID":3164,"customerID":2,"returnDate":"2005-08-27T08:59:32.000000Z","staffID":2,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":14743,"rentalDate":"2005-08-21T22:41:56.000000Z","inventoryID":4570,"customerID":2,"returnDate":"2005-08-29T00:18:56.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":15145,"rentalDate":"2005-08-22T13:53:04.000000Z","inventoryID":2179,"customerID":2,"returnDate":"2005-08-31T15:51:04.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}, {"rentalID":15907,"rentalDate":"2005-08-23T17:39:35.000000Z","inventoryID":2898,"customerID":2,"returnDate":"2005-08-25T23:23:35.000000Z","staffID":1,"lastUpdate":"2006-02-16T02:30:53.000000Z"}]}]`) } } @@ -849,8 +846,8 @@ func TestSelectJson_InvalidDestination(t *testing.T) { stmt := SELECT_JSON_OBJ(Actor.AllColumns). FROM(Actor) - testutils.AssertQueryJsonPanicErr(t, stmt, db, &[]model.Actor{}, "jet: destination has to be a pointer to struct or pointer to map[string]any") - testutils.AssertQueryJsonPanicErr(t, stmt, db, model.Actor{}, "jet: destination has to be a pointer to struct or pointer to map[string]any") + testutils.AssertQueryJsonPanicErr(t, stmt, db, &[]model.Actor{}, "jet: SELECT_JSON_OBJ destination has to be a pointer to struct or pointer to map[string]any") + testutils.AssertQueryJsonPanicErr(t, stmt, db, model.Actor{}, "jet: SELECT_JSON_OBJ destination has to be a pointer to struct or pointer to map[string]any") testutils.AssertQueryJsonPanicErr(t, stmt, nil, &model.Actor{}, "jet: db is nil") testutils.AssertQueryJsonPanicErr(t, stmt, db, nil, "jet: destination is nil") }) @@ -859,8 +856,8 @@ func TestSelectJson_InvalidDestination(t *testing.T) { stmt := SELECT_JSON_ARR(Actor.AllColumns). FROM(Actor) - testutils.AssertQueryJsonPanicErr(t, stmt, db, &model.Actor{}, "jet: destination has to be a pointer to slice of struct or pointer to []map[string]any") - testutils.AssertQueryJsonPanicErr(t, stmt, db, []model.Actor{}, "jet: destination has to be a pointer to slice of struct or pointer to []map[string]any") + testutils.AssertQueryJsonPanicErr(t, stmt, db, &model.Actor{}, "jet: SELECT_JSON_ARR destination has to be a pointer to slice of struct or pointer to []map[string]any") + testutils.AssertQueryJsonPanicErr(t, stmt, db, []model.Actor{}, "jet: SELECT_JSON_ARR destination has to be a pointer to slice of struct or pointer to []map[string]any") testutils.AssertQueryJsonPanicErr(t, stmt, nil, &[]model.Actor{}, "jet: db is nil") testutils.AssertQueryJsonPanicErr(t, stmt, db, nil, "jet: destination is nil") })