From d3ada5361e29098d199d7d3ebd4891867fb4659f Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Tue, 3 Sep 2024 15:39:36 +0200 Subject: [PATCH] Add support for postgres arrays --- generator/metadata/column_meta_data.go | 1 + generator/postgres/query_set.go | 1 + generator/template/model_template.go | 16 +++- generator/template/sql_builder_template.go | 39 ++++++++- internal/jet/array_expression.go | 93 ++++++++++++++++++++++ internal/jet/array_expression_test.go | 59 ++++++++++++++ internal/jet/column_types.go | 40 ++++++++++ internal/jet/column_types_test.go | 37 +++++++++ internal/jet/expression.go | 26 ++++++ internal/jet/literal_expression.go | 61 ++++++++++++++ internal/jet/operators.go | 14 ++++ internal/jet/sql_builder.go | 23 +++++- internal/jet/string_expression.go | 11 +++ internal/jet/string_expression_test.go | 8 ++ internal/jet/testutils.go | 30 ++++--- postgres/cast.go | 4 + postgres/columns.go | 18 +++++ postgres/expressions.go | 9 +++ postgres/insert_statement_test.go | 23 +++--- postgres/literal.go | 10 +++ postgres/utils_test.go | 8 +- tests/docker-compose.yaml | 3 +- tests/postgres/alltypes_test.go | 17 ++-- tests/postgres/generator_template_test.go | 2 +- tests/postgres/generator_test.go | 31 ++++---- tests/postgres/scan_test.go | 21 ++--- tests/postgres/select_test.go | 27 +++++-- 27 files changed, 558 insertions(+), 74 deletions(-) create mode 100644 internal/jet/array_expression.go create mode 100644 internal/jet/array_expression_test.go diff --git a/generator/metadata/column_meta_data.go b/generator/metadata/column_meta_data.go index 1502719..fd421b7 100644 --- a/generator/metadata/column_meta_data.go +++ b/generator/metadata/column_meta_data.go @@ -42,4 +42,5 @@ type DataType struct { Name string Kind DataTypeKind IsUnsigned bool + Dimensions int // The number of array dimensions } diff --git a/generator/postgres/query_set.go b/generator/postgres/query_set.go index abb21ba..2d86b50 100644 --- a/generator/postgres/query_set.go +++ b/generator/postgres/query_set.go @@ -65,6 +65,7 @@ select not attr.attnotnull as "column.isNullable", attr.attgenerated = 's' as "column.isGenerated", attr.atthasdef as "column.hasDefault", + attr.attndims as "dataType.dimensions", (case when tp.typtype = 'b' AND tp.typcategory <> 'A' then 'base' when tp.typtype = 'b' AND tp.typcategory = 'A' then 'array' diff --git a/generator/template/model_template.go b/generator/template/model_template.go index f89ebd1..8ef8bcb 100644 --- a/generator/template/model_template.go +++ b/generator/template/model_template.go @@ -6,6 +6,7 @@ import ( "github.com/go-jet/jet/v2/internal/utils/dbidentifier" "github.com/google/uuid" "github.com/jackc/pgtype" + "github.com/lib/pq" "path" "reflect" "strings" @@ -249,7 +250,7 @@ func getUserDefinedType(column metadata.Column) string { switch column.DataType.Kind { case metadata.EnumType: return dbidentifier.ToGoIdentifier(column.DataType.Name) - case metadata.UserDefinedType, metadata.ArrayType: + case metadata.UserDefinedType: return "string" } @@ -268,6 +269,11 @@ func getGoType(column metadata.Column) interface{} { // toGoType returns model type for column info. func toGoType(column metadata.Column) interface{} { + // We don't support multi-dimensional arrays + if column.DataType.Dimensions > 1 { + return "" + } + switch strings.ToLower(column.DataType.Name) { case "user-defined", "enum": return "" @@ -333,6 +339,14 @@ func toGoType(column metadata.Column) interface{} { return pgtype.Int8range{} case "numrange": return pgtype.Numrange{} + case "bool[]": + return pq.BoolArray{} + case "integer[]", "int4[]": + return pq.Int32Array{} + case "bigint[]": + return pq.Int64Array{} + case "text[]", "jsonb[]", "json[]": + return pq.StringArray{} default: fmt.Println("- [Model ] Unsupported sql column '" + column.Name + " " + column.DataType.Name + "', using string instead.") return "" diff --git a/generator/template/sql_builder_template.go b/generator/template/sql_builder_template.go index fe7fba5..94314a6 100644 --- a/generator/template/sql_builder_template.go +++ b/generator/template/sql_builder_template.go @@ -5,6 +5,7 @@ import ( "github.com/go-jet/jet/v2/generator/metadata" "github.com/go-jet/jet/v2/internal/utils/dbidentifier" "path" + "slices" "strings" "unicode" ) @@ -145,11 +146,42 @@ func DefaultTableSQLBuilderColumn(columnMetaData metadata.Column) TableSQLBuilde // getSqlBuilderColumnType returns type of jet sql builder column func getSqlBuilderColumnType(columnMetaData metadata.Column) string { if columnMetaData.DataType.Kind != metadata.BaseType && - columnMetaData.DataType.Kind != metadata.RangeType { + columnMetaData.DataType.Kind != metadata.RangeType && + columnMetaData.DataType.Kind != metadata.ArrayType { return "String" } - switch strings.ToLower(columnMetaData.DataType.Name) { + typeName := columnMetaData.DataType.Name + columnName := columnMetaData.Name + + if columnMetaData.DataType.Kind == metadata.ArrayType { + if columnMetaData.DataType.Dimensions > 1 { + fmt.Println("- [SQL Builder] Unsupported sql array with multiple dimensions column '" + columnName + " " + typeName + "', using StringColumn instead.") + return "String" + } + + c := sqlToColumnType(strings.TrimSuffix(typeName, "[]")) + + // These are the supported array types + if slices.Index([]string{"Bool", "String", "Integer"}, c) == -1 { + fmt.Println("- [SQL Builder] Unsupported sql array column '" + columnName + " " + typeName + "', using StringColumn instead.") + return "String" + } + + return c + "Array" + } + + columnType := sqlToColumnType(typeName) + if columnType == "" { + fmt.Println("- [SQL Builder] Unsupported sql column '" + columnName + " " + typeName + "', using StringColumn instead.") + return "String" + } + + return columnType +} + +func sqlToColumnType(typeName string) string { + switch strings.ToLower(typeName) { case "boolean", "bool": return "Bool" case "smallint", "integer", "bigint", "int2", "int4", "int8", @@ -190,8 +222,7 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { case "numrange": return "NumericRange" default: - fmt.Println("- [SQL Builder] Unsupported sql column '" + columnMetaData.Name + " " + columnMetaData.DataType.Name + "', using StringColumn instead.") - return "String" + return "" } } diff --git a/internal/jet/array_expression.go b/internal/jet/array_expression.go new file mode 100644 index 0000000..66cc005 --- /dev/null +++ b/internal/jet/array_expression.go @@ -0,0 +1,93 @@ +package jet + +// ArrayExpression interface +type ArrayExpression[E Expression] interface { + Expression + + EQ(rhs ArrayExpression[E]) BoolExpression + NOT_EQ(rhs ArrayExpression[E]) BoolExpression + LT(rhs ArrayExpression[E]) BoolExpression + GT(rhs ArrayExpression[E]) BoolExpression + LT_EQ(rhs ArrayExpression[E]) BoolExpression + GT_EQ(rhs ArrayExpression[E]) BoolExpression + + CONTAINS(rhs ArrayExpression[E]) BoolExpression + IS_CONTAINED_BY(rhs ArrayExpression[E]) BoolExpression + OVERLAP(rhs ArrayExpression[E]) BoolExpression + CONCAT(rhs ArrayExpression[E]) ArrayExpression[E] + CONCAT_ELEMENT(E) ArrayExpression[E] + + AT(expression IntegerExpression) Expression +} + +type arrayInterfaceImpl[E Expression] struct { + parent ArrayExpression[E] +} + +type BinaryBoolOp func(Expression, Expression) BoolExpression + +func (a arrayInterfaceImpl[E]) EQ(rhs ArrayExpression[E]) BoolExpression { + return Eq(a.parent, rhs) +} + +func (a arrayInterfaceImpl[E]) NOT_EQ(rhs ArrayExpression[E]) BoolExpression { + return NotEq(a.parent, rhs) +} + +func (a arrayInterfaceImpl[E]) LT(rhs ArrayExpression[E]) BoolExpression { + return Lt(a.parent, rhs) +} + +func (a arrayInterfaceImpl[E]) GT(rhs ArrayExpression[E]) BoolExpression { + return Gt(a.parent, rhs) +} + +func (a arrayInterfaceImpl[E]) LT_EQ(rhs ArrayExpression[E]) BoolExpression { + return LtEq(a.parent, rhs) +} + +func (a arrayInterfaceImpl[E]) GT_EQ(rhs ArrayExpression[E]) BoolExpression { + return GtEq(a.parent, rhs) +} + +func (a arrayInterfaceImpl[E]) CONTAINS(rhs ArrayExpression[E]) BoolExpression { + return Contains(a.parent, rhs) +} + +func (a arrayInterfaceImpl[E]) IS_CONTAINED_BY(rhs ArrayExpression[E]) BoolExpression { + return IsContainedBy(a.parent, rhs) +} + +func (a arrayInterfaceImpl[E]) OVERLAP(rhs ArrayExpression[E]) BoolExpression { + return Overlap(a.parent, rhs) +} + +func (a arrayInterfaceImpl[E]) CONCAT(rhs ArrayExpression[E]) ArrayExpression[E] { + return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||")) +} + +func (a arrayInterfaceImpl[E]) CONCAT_ELEMENT(rhs E) ArrayExpression[E] { + return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||")) +} + +func (a arrayInterfaceImpl[E]) AT(expression IntegerExpression) Expression { + return arraySubscriptExpr(a.parent, expression) +} + +type arrayExpressionWrapper[E Expression] struct { + arrayInterfaceImpl[E] + Expression +} + +func newArrayExpressionWrap[E Expression](expression Expression) ArrayExpression[E] { + arrayExpressionWrapper := arrayExpressionWrapper[E]{Expression: expression} + arrayExpressionWrapper.arrayInterfaceImpl.parent = &arrayExpressionWrapper + return &arrayExpressionWrapper +} + +// ArrayExp is array expression wrapper around arbitrary expression. +// Allows go compiler to see any expression as array expression. +// Does not add sql cast to generated sql builder output. +func ArrayExp[E Expression](expression Expression) ArrayExpression[E] { + return newArrayExpressionWrap[E](expression) +} diff --git a/internal/jet/array_expression_test.go b/internal/jet/array_expression_test.go new file mode 100644 index 0000000..ed7b565 --- /dev/null +++ b/internal/jet/array_expression_test.go @@ -0,0 +1,59 @@ +package jet + +import ( + "github.com/lib/pq" + "testing" +) + +func TestArrayExpressionEQ(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.EQ(table2ColArray), "(table1.col_array_string = table2.col_array_string)") +} + +func TestArrayExpressionNOT_EQ(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.NOT_EQ(table2ColArray), "(table1.col_array_string != table2.col_array_string)") + assertClauseSerialize(t, table1ColStringArray.NOT_EQ(StringArray([]string{"x"})), "(table1.col_array_string != $1)", pq.StringArray{"x"}) +} + +func TestArrayExpressionLT(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.LT(table2ColArray), "(table1.col_array_string < table2.col_array_string)") +} + +func TestArrayExpressionGT(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.GT(table2ColArray), "(table1.col_array_string > table2.col_array_string)") +} + +func TestArrayExpressionLT_EQ(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.LT_EQ(table2ColArray), "(table1.col_array_string <= table2.col_array_string)") +} + +func TestArrayExpressionGT_EQ(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.GT_EQ(table2ColArray), "(table1.col_array_string >= table2.col_array_string)") +} + +func TestArrayExpressionCONTAINS(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.CONTAINS(table2ColArray), "(table1.col_array_string @> table2.col_array_string)") + assertClauseSerialize(t, table1ColStringArray.CONTAINS(StringArray([]string{"x"})), "(table1.col_array_string @> $1)", pq.StringArray{"x"}) +} + +func TestArrayExpressionCONTAINED_BY(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.IS_CONTAINED_BY(table2ColArray), "(table1.col_array_string <@ table2.col_array_string)") + assertClauseSerialize(t, table1ColStringArray.IS_CONTAINED_BY(StringArray([]string{"x"})), "(table1.col_array_string <@ $1)", pq.StringArray{"x"}) +} + +func TestArrayExpressionOVERLAP(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.OVERLAP(table2ColArray), "(table1.col_array_string && table2.col_array_string)") +} + +func TestArrayExpressionCONCAT(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.CONCAT(table2ColArray), "(table1.col_array_string || table2.col_array_string)") + assertClauseSerialize(t, table1ColStringArray.CONCAT(StringArray([]string{"x"})), "(table1.col_array_string || $1)", pq.StringArray{"x"}) +} + +func TestArrayExpressionCONCAT_ELEMENT(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.CONCAT_ELEMENT(StringExp(table2ColArray.AT(Int(1)))), "(table1.col_array_string || (table2.col_array_string[$1]))", int64(1)) + assertClauseSerialize(t, table1ColStringArray.CONCAT_ELEMENT(String("x")), "(table1.col_array_string || $1)", "x") +} + +func TestArrayExpressionAT(t *testing.T) { + assertClauseSerialize(t, table1ColStringArray.AT(Int(1)), "(table1.col_array_string[$1])", int64(1)) +} diff --git a/internal/jet/column_types.go b/internal/jet/column_types.go index a732061..1728583 100644 --- a/internal/jet/column_types.go +++ b/internal/jet/column_types.go @@ -121,6 +121,46 @@ func IntegerColumn(name string) ColumnInteger { //------------------------------------------------------// +type ColumnArray[E Expression] interface { + ArrayExpression[E] + Column + + From(subQuery SelectTable) ColumnArray[E] + SET(stringExp ArrayExpression[E]) ColumnAssigment +} + +type arrayColumnImpl[E Expression] struct { + arrayInterfaceImpl[E] + + ColumnExpressionImpl +} + +func (a arrayColumnImpl[E]) From(subQuery SelectTable) ColumnArray[E] { + newArrayColumn := ArrayColumn[E](a.name) + newArrayColumn.setTableName(a.tableName) + newArrayColumn.setSubQuery(subQuery) + + return newArrayColumn +} + +func (a *arrayColumnImpl[E]) SET(stringExp ArrayExpression[E]) ColumnAssigment { + return columnAssigmentImpl{ + column: a, + expression: stringExp, + } +} + +// StringColumn creates named string column. +func ArrayColumn[E Expression](name string) ColumnArray[E] { + arrayColumn := &arrayColumnImpl[E]{} + arrayColumn.arrayInterfaceImpl.parent = arrayColumn + arrayColumn.ColumnExpressionImpl = NewColumnImpl(name, "", arrayColumn) + + return arrayColumn +} + +//------------------------------------------------------// + // ColumnString is interface for SQL text, character, character varying // bytea, uuid columns and enums types. type ColumnString interface { diff --git a/internal/jet/column_types_test.go b/internal/jet/column_types_test.go index 059d722..38d9e96 100644 --- a/internal/jet/column_types_test.go +++ b/internal/jet/column_types_test.go @@ -1,6 +1,7 @@ package jet import ( + "github.com/lib/pq" "testing" ) @@ -8,6 +9,42 @@ var subQuery = &selectTableImpl{ alias: "sub_query", } +func TestNewArrayColumnString(t *testing.T) { + stringArrayColumn := ArrayColumn[StringExpression]("colArray").From(subQuery) + assertClauseSerialize(t, stringArrayColumn, `sub_query."colArray"`) + assertClauseSerialize(t, stringArrayColumn.EQ(StringArray([]string{"X"})), `(sub_query."colArray" = $1)`, pq.StringArray{"X"}) + assertProjectionSerialize(t, stringArrayColumn, `sub_query."colArray" AS "colArray"`) + + arrayColumn2 := table1ColStringArray.From(subQuery) + assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_string"`) + assertClauseSerialize(t, arrayColumn2.EQ(StringArray([]string{"X"})), `(sub_query."table1.col_array_string" = $1)`, pq.StringArray{"X"}) + assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_string" AS "table1.col_array_string"`) +} + +func TestNewArrayColumnBool(t *testing.T) { + boolArrayColumn := ArrayColumn[BoolExpression]("colArrayBool").From(subQuery) + assertClauseSerialize(t, boolArrayColumn, `sub_query."colArrayBool"`) + assertClauseSerialize(t, boolArrayColumn.EQ(BoolArray([]bool{true})), `(sub_query."colArrayBool" = $1)`, pq.BoolArray{true}) + assertProjectionSerialize(t, boolArrayColumn, `sub_query."colArrayBool" AS "colArrayBool"`) + + arrayColumn2 := table1ColBoolArray.From(subQuery) + assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_bool"`) + assertClauseSerialize(t, arrayColumn2.EQ(BoolArray([]bool{true})), `(sub_query."table1.col_array_bool" = $1)`, pq.BoolArray{true}) + assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_bool" AS "table1.col_array_bool"`) +} + +func TestNewArrayColumnInteger(t *testing.T) { + intArrayColumn := ArrayColumn[IntegerExpression]("colArrayInt").From(subQuery) + assertClauseSerialize(t, intArrayColumn, `sub_query."colArrayInt"`) + assertClauseSerialize(t, intArrayColumn.EQ(Int32Array([]int32{42})), `(sub_query."colArrayInt" = $1)`, pq.Int32Array{42}) + assertProjectionSerialize(t, intArrayColumn, `sub_query."colArrayInt" AS "colArrayInt"`) + + arrayColumn2 := table1ColIntArray.From(subQuery) + assertClauseSerialize(t, arrayColumn2, `sub_query."table1.col_array_int"`) + assertClauseSerialize(t, arrayColumn2.EQ(Int32Array([]int32{42})), `(sub_query."table1.col_array_int" = $1)`, pq.Int32Array{42}) + assertProjectionSerialize(t, arrayColumn2, `sub_query."table1.col_array_int" AS "table1.col_array_int"`) +} + func TestNewBoolColumn(t *testing.T) { boolColumn := BoolColumn("colBool").From(subQuery) assertClauseSerialize(t, boolColumn, `sub_query."colBool"`) diff --git a/internal/jet/expression.go b/internal/jet/expression.go index 05b1797..0a08336 100644 --- a/internal/jet/expression.go +++ b/internal/jet/expression.go @@ -316,6 +316,32 @@ func (s *complexExpression) serialize(statement StatementType, out *SQLBuilder, } } +type arraySubscriptExpression struct { + ExpressionInterfaceImpl + array Expression + subscript IntegerExpression +} + +func (a arraySubscriptExpression) serialize(statement StatementType, out *SQLBuilder, options ...SerializeOption) { + if !contains(options, NoWrap) { + out.WriteString("(") + } + a.array.serialize(statement, out, FallTrough(options)...) // FallTrough here because complexExpression is just a wrapper + out.WriteString("[") + a.subscript.serialize(statement, out, FallTrough(options)...) // FallTrough here because complexExpression is just a wrapper + out.WriteString("]") + if !contains(options, NoWrap) { + out.WriteString(")") + } +} + +func arraySubscriptExpr(array Expression, subscript IntegerExpression) Expression { + arraySubscriptExpression := &arraySubscriptExpression{array: array, subscript: subscript} + arraySubscriptExpression.ExpressionInterfaceImpl.Parent = arraySubscriptExpression + + return arraySubscriptExpression +} + type skipParenthesisWrap struct { Expression } diff --git a/internal/jet/literal_expression.go b/internal/jet/literal_expression.go index d6f0b41..dab6add 100644 --- a/internal/jet/literal_expression.go +++ b/internal/jet/literal_expression.go @@ -2,6 +2,7 @@ package jet import ( "fmt" + "github.com/lib/pq" "time" ) @@ -160,6 +161,66 @@ func Decimal(value string) FloatExpression { return &floatLiteral } +// ---------------------------------------------------// + +type boolArrayLiteral struct { + arrayInterfaceImpl[BoolExpression] + literalExpressionImpl +} + +func BoolArray(values []bool) ArrayExpression[BoolExpression] { + l := boolArrayLiteral{} + l.literalExpressionImpl = *literal(pq.BoolArray(values)) + l.arrayInterfaceImpl.parent = &l + + return &l +} + +type integerArrayLiteral struct { + arrayInterfaceImpl[IntegerExpression] + literalExpressionImpl +} + +func Int64Array(values []int64) ArrayExpression[IntegerExpression] { + l := integerArrayLiteral{} + l.literalExpressionImpl = *literal(pq.Int64Array(values)) + l.arrayInterfaceImpl.parent = &l + return &l +} + +func Int32Array(values []int32) ArrayExpression[IntegerExpression] { + l := integerArrayLiteral{} + l.literalExpressionImpl = *literal(pq.Int32Array(values)) + l.arrayInterfaceImpl.parent = &l + return &l +} + +type stringArrayLiteral struct { + arrayInterfaceImpl[StringExpression] + literalExpressionImpl +} + +func StringArray(values []string) ArrayExpression[StringExpression] { + l := stringArrayLiteral{} + l.literalExpressionImpl = *literal(pq.StringArray(values)) + l.arrayInterfaceImpl.parent = &l + + return &l +} + +type unsafeArrayLiteral[E Expression] struct { + arrayInterfaceImpl[E] + literalExpressionImpl +} + +func UnsafeArray[E LiteralExpression](values []interface{}) ArrayExpression[E] { + l := unsafeArrayLiteral[E]{} + l.literalExpressionImpl = *literal(pq.Array(values)) + l.arrayInterfaceImpl.parent = &l + + return &l +} + // ---------------------------------------------------// type stringLiteral struct { stringInterfaceImpl diff --git a/internal/jet/operators.go b/internal/jet/operators.go index c453c3e..6ad46d4 100644 --- a/internal/jet/operators.go +++ b/internal/jet/operators.go @@ -22,6 +22,15 @@ func BIT_NOT(expr IntegerExpression) IntegerExpression { return newPrefixIntegerOperatorExpression(expr, "~") } +// ----------- Array operators -------------- // +func Any(lhs Expression, op BinaryBoolOp, rhs Expression) BoolExpression { + return op(lhs, Func("ANY", rhs)) +} + +func All(lhs Expression, op BinaryBoolOp, rhs Expression) BoolExpression { + return op(lhs, Func("ALL", rhs)) +} + //----------- Comparison operators ---------------// // EXISTS checks for existence of the rows in subQuery @@ -74,6 +83,11 @@ func Contains(lhs Expression, rhs Expression) BoolExpression { return newBinaryBoolOperatorExpression(lhs, rhs, "@>") } +// IsContainedBy returns a representation of "a <@ b" +func IsContainedBy(lhs Expression, rhs Expression) BoolExpression { + return newBinaryBoolOperatorExpression(lhs, rhs, "<@") +} + // Overlap returns a representation of "a && b" func Overlap(lhs, rhs Expression) BoolExpression { return newBinaryBoolOperatorExpression(lhs, rhs, "&&") diff --git a/internal/jet/sql_builder.go b/internal/jet/sql_builder.go index 46f47ad..288335d 100644 --- a/internal/jet/sql_builder.go +++ b/internal/jet/sql_builder.go @@ -81,11 +81,11 @@ func (s *SQLBuilder) write(data []byte) { } func isPreSeparator(b byte) bool { - return b == ' ' || b == '.' || b == ',' || b == '(' || b == '\n' || b == ':' + return b == ' ' || b == '.' || b == ',' || b == '(' || b == '\n' || b == ':' || b == '[' } func isPostSeparator(b byte) bool { - return b == ' ' || b == '.' || b == ',' || b == ')' || b == '\n' || b == ':' + return b == ' ' || b == '.' || b == ',' || b == ')' || b == '\n' || b == ':' || b == '[' || b == ']' } // WriteAlias is used to add alias to output SQL @@ -226,6 +226,8 @@ func argToString(value interface{}) string { case string: return stringQuote(bindVal) + case []string: + return stringArrayQuote(bindVal) case []byte: return stringQuote(string(bindVal)) case uuid.UUID: @@ -253,6 +255,19 @@ func argToString(value interface{}) string { } } +func stringArrayQuote(val []string) string { + var sb strings.Builder + sb.WriteString(`'{`) + for i := 0; i < len(val); i++ { + if i > 0 { + sb.WriteString(`, `) + } + sb.WriteString(stringDoubleQuote(val[i])) + } + sb.WriteString(`}'`) + return sb.String() +} + func integerTypesToString(value interface{}) string { switch bindVal := value.(type) { case int: @@ -301,3 +316,7 @@ func shouldQuoteIdentifier(identifier string) bool { func stringQuote(value string) string { return `'` + strings.Replace(value, "'", "''", -1) + `'` } + +func stringDoubleQuote(value string) string { + return `"` + strings.Replace(value, `"`, `""`, -1) + `"` +} diff --git a/internal/jet/string_expression.go b/internal/jet/string_expression.go index 29b2447..f61c689 100644 --- a/internal/jet/string_expression.go +++ b/internal/jet/string_expression.go @@ -16,6 +16,9 @@ type StringExpression interface { BETWEEN(min, max StringExpression) BoolExpression NOT_BETWEEN(min, max StringExpression) BoolExpression + ANY_EQ(rhs ArrayExpression[StringExpression]) BoolExpression + ALL_EQ(rhs ArrayExpression[StringExpression]) BoolExpression + CONCAT(rhs Expression) StringExpression LIKE(pattern StringExpression) BoolExpression @@ -69,6 +72,14 @@ func (s *stringInterfaceImpl) NOT_BETWEEN(min, max StringExpression) BoolExpress return NewBetweenOperatorExpression(s.parent, min, max, true) } +func (i *stringInterfaceImpl) ANY_EQ(rhs ArrayExpression[StringExpression]) BoolExpression { + return Any(i.parent, Eq, rhs) +} + +func (i *stringInterfaceImpl) ALL_EQ(rhs ArrayExpression[StringExpression]) BoolExpression { + return All(i.parent, Eq, rhs) +} + func (s *stringInterfaceImpl) CONCAT(rhs Expression) StringExpression { return newBinaryStringOperatorExpression(s.parent, rhs, StringConcatOperator) } diff --git a/internal/jet/string_expression_test.go b/internal/jet/string_expression_test.go index 0f461ac..830937f 100644 --- a/internal/jet/string_expression_test.go +++ b/internal/jet/string_expression_test.go @@ -76,6 +76,14 @@ func TestStringNOT_REGEXP_LIKE(t *testing.T) { assertClauseSerialize(t, table3StrCol.NOT_REGEXP_LIKE(String("JOHN"), true), "(table3.col2 NOT REGEXP $1)", "JOHN") } +func TestStringANY_EQ(t *testing.T) { + assertClauseSerialize(t, table2ColStr.ANY_EQ(table1ColStringArray), "(table2.col_str = ANY(table1.col_array_string))") +} + +func TestStringALL_EQ(t *testing.T) { + assertClauseSerialize(t, table2ColStr.ALL_EQ(table1ColStringArray), "(table2.col_str = ALL(table1.col_array_string))") +} + func TestStringExp(t *testing.T) { assertClauseSerialize(t, StringExp(table2ColFloat), "table2.col_float") assertClauseSerialize(t, StringExp(table2ColFloat).NOT_LIKE(String("abc")), "(table2.col_float NOT LIKE $1)", "abc") diff --git a/internal/jet/testutils.go b/internal/jet/testutils.go index 70b21c7..0f4ff8a 100644 --- a/internal/jet/testutils.go +++ b/internal/jet/testutils.go @@ -15,19 +15,22 @@ var defaultDialect = NewDialect(DialectParams{ // just for tests }) var ( - table1Col1 = IntegerColumn("col1") - table1ColInt = IntegerColumn("col_int") - table1ColFloat = FloatColumn("col_float") - table1Col3 = IntegerColumn("col3") - table1ColTime = TimeColumn("col_time") - table1ColTimez = TimezColumn("col_timez") - table1ColTimestamp = TimestampColumn("col_timestamp") - table1ColTimestampz = TimestampzColumn("col_timestampz") - table1ColBool = BoolColumn("col_bool") - table1ColDate = DateColumn("col_date") - table1ColRange = RangeColumn[Int8Expression]("col_range") + table1Col1 = IntegerColumn("col1") + table1ColInt = IntegerColumn("col_int") + table1ColFloat = FloatColumn("col_float") + table1Col3 = IntegerColumn("col3") + table1ColTime = TimeColumn("col_time") + table1ColTimez = TimezColumn("col_timez") + table1ColTimestamp = TimestampColumn("col_timestamp") + table1ColTimestampz = TimestampzColumn("col_timestampz") + table1ColBool = BoolColumn("col_bool") + table1ColDate = DateColumn("col_date") + table1ColRange = RangeColumn[Int8Expression]("col_range") + table1ColStringArray = ArrayColumn[StringExpression]("col_array_string") + table1ColBoolArray = ArrayColumn[BoolExpression]("col_array_bool") + table1ColIntArray = ArrayColumn[IntegerExpression]("col_array_int") ) -var table1 = NewTable("db", "table1", "", table1Col1, table1ColInt, table1ColFloat, table1Col3, table1ColTime, table1ColTimez, table1ColBool, table1ColDate, table1ColRange, table1ColTimestamp, table1ColTimestampz) +var table1 = NewTable("db", "table1", "", table1Col1, table1ColInt, table1ColFloat, table1Col3, table1ColTime, table1ColTimez, table1ColBool, table1ColDate, table1ColRange, table1ColTimestamp, table1ColTimestampz, table1ColStringArray, table1ColBoolArray, table1ColIntArray) var ( table2Col3 = IntegerColumn("col3") @@ -42,8 +45,9 @@ var ( table2ColTimestampz = TimestampzColumn("col_timestampz") table2ColDate = DateColumn("col_date") table2ColRange = RangeColumn[Int8Expression]("col_range") + table2ColArray = ArrayColumn[StringExpression]("col_array_string") ) -var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColRange, table2ColTimestamp, table2ColTimestampz) +var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColRange, table2ColTimestamp, table2ColTimestampz, table2ColArray) var ( table3Col1 = IntegerColumn("col1") diff --git a/postgres/cast.go b/postgres/cast.go index e3a17a4..6447148 100644 --- a/postgres/cast.go +++ b/postgres/cast.go @@ -45,6 +45,10 @@ type cast interface { AS_INTERVAL() IntervalExpression } +type castArray interface { + AS_STRING() jet.ArrayExpression[StringExpression] +} + type castImpl struct { jet.Cast } diff --git a/postgres/columns.go b/postgres/columns.go index 819da38..c75e03d 100644 --- a/postgres/columns.go +++ b/postgres/columns.go @@ -101,6 +101,24 @@ type ColumnInt8Range jet.ColumnRange[jet.Int8Expression] // Int8RangeColumn creates named range with range column var Int8RangeColumn = jet.RangeColumn[jet.Int8Expression] +// ColumnStringArray is interface of column +type ColumnStringArray jet.ColumnArray[jet.StringExpression] + +// StringArrayColumn creates named string array column +var StringArrayColumn = jet.ArrayColumn[jet.StringExpression] + +// ColumnIntegerArray is interface of column +type ColumnIntegerArray jet.ColumnArray[jet.IntegerExpression] + +// IntegerArrayColumn creates named integer array column +var IntegerArrayColumn = jet.ArrayColumn[jet.IntegerExpression] + +// ColumnBoolArray is interface of column +type ColumnBoolArray jet.ColumnArray[jet.BoolExpression] + +// BoolArrayColumn creates named bool array column +var BoolArrayColumn = jet.ArrayColumn[jet.BoolExpression] + //------------------------------------------------------// // ColumnInterval is interface of PostgreSQL interval columns. diff --git a/postgres/expressions.go b/postgres/expressions.go index 9872910..921341b 100644 --- a/postgres/expressions.go +++ b/postgres/expressions.go @@ -9,15 +9,24 @@ type Expression = jet.Expression // BoolExpression interface type BoolExpression = jet.BoolExpression +// BoolArrayExpression interface +type BoolArrayExpression = jet.ArrayExpression[BoolExpression] + // StringExpression interface type StringExpression = jet.StringExpression +// StringArrayExpression interface +type StringArrayExpression = jet.ArrayExpression[StringExpression] + // NumericExpression interface type NumericExpression = jet.NumericExpression // IntegerExpression interface type IntegerExpression = jet.IntegerExpression +// IntegerArrayExpression interface +type IntegerArrayExpression = jet.ArrayExpression[IntegerExpression] + // FloatExpression is interface type FloatExpression = jet.FloatExpression diff --git a/postgres/insert_statement_test.go b/postgres/insert_statement_test.go index 25300c2..4aa9c50 100644 --- a/postgres/insert_statement_test.go +++ b/postgres/insert_statement_test.go @@ -175,27 +175,30 @@ RETURNING table1.col1 AS "table1.col1", } func TestInsert_ON_CONFLICT_ON_CONSTRAINT(t *testing.T) { - stmt := table1.INSERT(table1Col1, table1ColBool). - VALUES("one", "two"). - VALUES("1", "2"). + stmt := table1.INSERT(table1Col1, table1ColBool, table1ColStringArray). + VALUES("one", "two", "three"). + VALUES("1", "2", "3"). ON_CONFLICT().ON_CONSTRAINT("idk_primary_key").DO_UPDATE( SET(table1ColBool.SET(Bool(false)), table2ColInt.SET(Int(1)), - ColumnList{table1Col1, table1ColBool}.SET(jet.ROW(Int(2), String("two"))), + table1ColStringArray.SET(StringArray([]string{"one"})), + ColumnList{table1Col1, table1ColBool, table1ColStringArray}.SET(jet.ROW(Int(2), String("two"), StringArray([]string{"two"}))), ).WHERE(table1Col1.GT(Int(2))), ). - RETURNING(table1Col1, table1ColBool) + RETURNING(table1Col1, table1ColBool, table1ColStringArray) assertDebugStatementSql(t, stmt, ` -INSERT INTO db.table1 (col1, col_bool) -VALUES ('one', 'two'), - ('1', '2') +INSERT INTO db.table1 (col1, col_bool, col_string_array) +VALUES ('one', 'two', 'three'), + ('1', '2', '3') ON CONFLICT ON CONSTRAINT idk_primary_key DO UPDATE SET col_bool = FALSE::boolean, col_int = 1, - (col1, col_bool) = ROW(2, 'two'::text) + col_string_array = '{"one"}', + (col1, col_bool, col_string_array) = ROW(2, 'two'::text, '{"two"}') WHERE table1.col1 > 2 RETURNING table1.col1 AS "table1.col1", - table1.col_bool AS "table1.col_bool"; + table1.col_bool AS "table1.col_bool", + table1.col_string_array AS "table1.col_string_array"; `) } diff --git a/postgres/literal.go b/postgres/literal.go index e3a95b3..4d7ab96 100644 --- a/postgres/literal.go +++ b/postgres/literal.go @@ -11,6 +11,11 @@ func Bool(value bool) BoolExpression { return CAST(jet.Bool(value)).AS_BOOL() } +// BoolArray creates new bool array literal expression +func BoolArray(elements []bool) BoolArrayExpression { + return jet.BoolArray(elements) +} + // Int is constructor for 64 bit signed integer expressions literals. var Int = jet.Int @@ -65,6 +70,11 @@ func String(value string) StringExpression { return CAST(jet.String(value)).AS_TEXT() } +// StringArray creates new string array literal expression +func StringArray(elements []string) StringArrayExpression { + return jet.StringArray(elements) +} + // Json creates new json literal expression func Json(value interface{}) StringExpression { switch value.(type) { diff --git a/postgres/utils_test.go b/postgres/utils_test.go index 96bb13b..d89b17b 100644 --- a/postgres/utils_test.go +++ b/postgres/utils_test.go @@ -18,6 +18,8 @@ var table1ColBool = BoolColumn("col_bool") var table1ColDate = DateColumn("col_date") var table1ColInterval = IntervalColumn("col_interval") var table1ColRange = Int8RangeColumn("col_range") +var table1ColStringArray = StringArrayColumn("col_string_array") +var table1ColIntArray = IntegerArrayColumn("col_int_array") var table1 = NewTable( "db", @@ -34,6 +36,8 @@ var table1 = NewTable( table1ColTimestampz, table1ColInterval, table1ColRange, + table1ColStringArray, + table1ColIntArray, ) var table2Col3 = IntegerColumn("col3") @@ -49,8 +53,10 @@ var table2ColTimestampz = TimestampzColumn("col_timestampz") var table2ColDate = DateColumn("col_date") var table2ColInterval = IntervalColumn("col_interval") var table2ColRange = Int8RangeColumn("col_range") +var table2ColStringArray = StringArrayColumn("col_string_array") +var table2ColIntArray = IntegerArrayColumn("col_int_array") -var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColTimestamp, table2ColTimestampz, table2ColInterval, table2ColRange) +var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColTimestamp, table2ColTimestampz, table2ColInterval, table2ColRange, table2ColStringArray, table2ColIntArray) var table3Col1 = IntegerColumn("col1") var table3ColInt = IntegerColumn("col_int") diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml index 9b3af50..7c1c116 100644 --- a/tests/docker-compose.yaml +++ b/tests/docker-compose.yaml @@ -1,4 +1,3 @@ -version: '3' services: postgres: image: postgres:14.1 @@ -13,7 +12,7 @@ services: - ./testdata/init/postgres:/docker-entrypoint-initdb.d mysql: - image: mysql:8.0.27 + image: mysql/mysql-server:8.0.27 command: ['--default-authentication-plugin=mysql_native_password', '--log_bin_trust_function_creators=1'] restart: always environment: diff --git a/tests/postgres/alltypes_test.go b/tests/postgres/alltypes_test.go index d41feee..b15056f 100644 --- a/tests/postgres/alltypes_test.go +++ b/tests/postgres/alltypes_test.go @@ -2,6 +2,7 @@ package postgres import ( "database/sql" + "github.com/lib/pq" "testing" "time" @@ -1361,11 +1362,11 @@ var allTypesRow0 = model.AllTypes{ JSON: `{"a": 1, "b": 3}`, JsonbPtr: testutils.StringPtr(`{"a": 1, "b": 3}`), Jsonb: `{"a": 1, "b": 3}`, - IntegerArrayPtr: testutils.StringPtr("{1,2,3}"), - IntegerArray: "{1,2,3}", - TextArrayPtr: testutils.StringPtr("{breakfast,consulting}"), - TextArray: "{breakfast,consulting}", - JsonbArray: `{"{\"a\": 1, \"b\": 2}","{\"a\": 3, \"b\": 4}"}`, + IntegerArrayPtr: &pq.Int32Array{1, 2, 3}, + IntegerArray: pq.Int32Array{1, 2, 3}, + TextArrayPtr: &pq.StringArray{"breakfast", "consulting"}, + TextArray: pq.StringArray{"breakfast", "consulting"}, + JsonbArray: pq.StringArray{`{"a": 1, "b": 2}`, `{"a": 3, "b": 4}`}, TextMultiDimArrayPtr: testutils.StringPtr("{{meeting,lunch},{training,presentation}}"), TextMultiDimArray: "{{meeting,lunch},{training,presentation}}", MoodPtr: &moodSad, @@ -1430,10 +1431,10 @@ var allTypesRow1 = model.AllTypes{ JsonbPtr: nil, Jsonb: `{"a": 1, "b": 3}`, IntegerArrayPtr: nil, - IntegerArray: "{1,2,3}", + IntegerArray: pq.Int32Array{1, 2, 3}, TextArrayPtr: nil, - TextArray: "{breakfast,consulting}", - JsonbArray: `{"{\"a\": 1, \"b\": 2}","{\"a\": 3, \"b\": 4}"}`, + TextArray: pq.StringArray{"breakfast", "consulting"}, + JsonbArray: pq.StringArray{`{"a": 1, "b": 2}`, `{"a": 3, "b": 4}`}, TextMultiDimArrayPtr: nil, TextMultiDimArray: "{{meeting,lunch},{training,presentation}}", MoodPtr: nil, diff --git a/tests/postgres/generator_template_test.go b/tests/postgres/generator_template_test.go index 4d87295..819a19b 100644 --- a/tests/postgres/generator_template_test.go +++ b/tests/postgres/generator_template_test.go @@ -447,7 +447,7 @@ func TestGeneratorTemplate_Model_ChangeFieldTypes(t *testing.T) { require.Contains(t, data, "\"database/sql\"") require.Contains(t, data, "Description sql.NullString") require.Contains(t, data, "ReleaseYear sql.NullInt32") - require.Contains(t, data, "SpecialFeatures sql.NullString") + require.Contains(t, data, "SpecialFeatures *pq.StringArray") } func TestGeneratorTemplate_SQLBuilder_ChangeColumnTypes(t *testing.T) { diff --git a/tests/postgres/generator_test.go b/tests/postgres/generator_test.go index fe1407f..682d9d3 100644 --- a/tests/postgres/generator_test.go +++ b/tests/postgres/generator_test.go @@ -677,6 +677,7 @@ package model import ( "github.com/google/uuid" + "github.com/lib/pq" "time" ) @@ -735,11 +736,11 @@ type AllTypes struct { JSON string JsonbPtr *string Jsonb string - IntegerArrayPtr *string - IntegerArray string - TextArrayPtr *string - TextArray string - JsonbArray string + IntegerArrayPtr *pq.Int32Array + IntegerArray pq.Int32Array + TextArrayPtr *pq.StringArray + TextArray pq.StringArray + JsonbArray pq.StringArray TextMultiDimArrayPtr *string TextMultiDimArray string MoodPtr *Mood @@ -821,11 +822,11 @@ type allTypesTable struct { JSON postgres.ColumnString JsonbPtr postgres.ColumnString Jsonb postgres.ColumnString - IntegerArrayPtr postgres.ColumnString - IntegerArray postgres.ColumnString - TextArrayPtr postgres.ColumnString - TextArray postgres.ColumnString - JsonbArray postgres.ColumnString + IntegerArrayPtr postgres.ColumnIntegerArray + IntegerArray postgres.ColumnIntegerArray + TextArrayPtr postgres.ColumnStringArray + TextArray postgres.ColumnStringArray + JsonbArray postgres.ColumnStringArray TextMultiDimArrayPtr postgres.ColumnString TextMultiDimArray postgres.ColumnString MoodPtr postgres.ColumnString @@ -924,11 +925,11 @@ func newAllTypesTableImpl(schemaName, tableName, alias string) allTypesTable { JSONColumn = postgres.StringColumn("json") JsonbPtrColumn = postgres.StringColumn("jsonb_ptr") JsonbColumn = postgres.StringColumn("jsonb") - IntegerArrayPtrColumn = postgres.StringColumn("integer_array_ptr") - IntegerArrayColumn = postgres.StringColumn("integer_array") - TextArrayPtrColumn = postgres.StringColumn("text_array_ptr") - TextArrayColumn = postgres.StringColumn("text_array") - JsonbArrayColumn = postgres.StringColumn("jsonb_array") + IntegerArrayPtrColumn = postgres.IntegerArrayColumn("integer_array_ptr") + IntegerArrayColumn = postgres.IntegerArrayColumn("integer_array") + TextArrayPtrColumn = postgres.StringArrayColumn("text_array_ptr") + TextArrayColumn = postgres.StringArrayColumn("text_array") + JsonbArrayColumn = postgres.StringArrayColumn("jsonb_array") TextMultiDimArrayPtrColumn = postgres.StringColumn("text_multi_dim_array_ptr") TextMultiDimArrayColumn = postgres.StringColumn("text_multi_dim_array") MoodPtrColumn = postgres.StringColumn("mood_ptr") diff --git a/tests/postgres/scan_test.go b/tests/postgres/scan_test.go index 24b5949..f195f30 100644 --- a/tests/postgres/scan_test.go +++ b/tests/postgres/scan_test.go @@ -2,6 +2,7 @@ package postgres import ( "context" + "github.com/lib/pq" "github.com/volatiletech/null/v8" "testing" "time" @@ -953,6 +954,7 @@ func TestScanIntoCustomBaseTypes(t *testing.T) { type MyFloat32 float32 type MyFloat64 float64 type MyString string + type MyStringArray pq.StringArray type MyTime = time.Time type film struct { @@ -967,26 +969,25 @@ func TestScanIntoCustomBaseTypes(t *testing.T) { ReplacementCost MyFloat64 Rating *model.MpaaRating LastUpdate MyTime - SpecialFeatures *MyString + SpecialFeatures MyStringArray Fulltext MyString } + // We'll skip special features, because it's a slice and it does not implement sql.Scanner stmt := SELECT( - Film.AllColumns, + Film.AllColumns.Except(Film.SpecialFeatures), ).FROM( Film, ).ORDER_BY( Film.FilmID.ASC(), ).LIMIT(3) - var films []model.Film - - err := stmt.Query(db, &films) + var myFilms []film + err := stmt.Query(db, &myFilms) require.NoError(t, err) - var myFilms []film - - err = stmt.Query(db, &myFilms) + var films []model.Film + err = stmt.Query(db, &films) require.NoError(t, err) require.Equal(t, testutils.ToJSON(films), testutils.ToJSON(myFilms)) @@ -1160,7 +1161,7 @@ var film1 = model.Film{ ReplacementCost: 20.99, Rating: &pgRating, LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3), - SpecialFeatures: testutils.StringPtr("{\"Deleted Scenes\",\"Behind the Scenes\"}"), + SpecialFeatures: &pq.StringArray{"Deleted Scenes", "Behind the Scenes"}, Fulltext: "'academi':1 'battl':15 'canadian':20 'dinosaur':2 'drama':5 'epic':4 'feminist':8 'mad':11 'must':14 'rocki':21 'scientist':12 'teacher':17", } @@ -1176,7 +1177,7 @@ var film2 = model.Film{ ReplacementCost: 12.99, Rating: &gRating, LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3), - SpecialFeatures: testutils.StringPtr(`{Trailers,"Deleted Scenes"}`), + SpecialFeatures: &pq.StringArray{"Trailers", "Deleted Scenes"}, Fulltext: `'ace':1 'administr':9 'ancient':19 'astound':4 'car':17 'china':20 'databas':8 'epistl':5 'explor':12 'find':15 'goldfing':2 'must':14`, } diff --git a/tests/postgres/select_test.go b/tests/postgres/select_test.go index 048db14..ccd5287 100644 --- a/tests/postgres/select_test.go +++ b/tests/postgres/select_test.go @@ -3,6 +3,7 @@ package postgres import ( "context" "database/sql" + "github.com/lib/pq" "testing" "time" @@ -1837,7 +1838,7 @@ ORDER BY film.film_id ASC; Rating: &gRating, RentalDuration: 3, LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3), - SpecialFeatures: testutils.StringPtr("{Trailers,\"Deleted Scenes\"}"), + SpecialFeatures: &pq.StringArray{"Trailers", "Deleted Scenes"}, Fulltext: "'ace':1 'administr':9 'ancient':19 'astound':4 'car':17 'china':20 'databas':8 'epistl':5 'explor':12 'find':15 'goldfing':2 'must':14", }) } @@ -2793,7 +2794,7 @@ ORDER BY actor.actor_id ASC, film.film_id ASC; err := stmt.Query(db, &dest) require.NoError(t, err) - //jsonSave("./testdata/quick-start-dest.json", dest) + //testutils.SaveJSONFile(dest, "./testdata/results/postgres/quick-start-dest.json") testutils.AssertJSONFile(t, dest, "./testdata/results/postgres/quick-start-dest.json") var dest2 []struct { @@ -2806,7 +2807,7 @@ ORDER BY actor.actor_id ASC, film.film_id ASC; err = stmt.Query(db, &dest2) require.NoError(t, err) - //jsonSave("./testdata/quick-start-dest2.json", dest2) + //testutils.SaveJSONFile(dest, "./testdata/results/postgres/quick-start-dest2.json") testutils.AssertJSONFile(t, dest2, "./testdata/results/postgres/quick-start-dest2.json") } @@ -3382,7 +3383,10 @@ func TestRecursionScanNxM(t *testing.T) { "ReplacementCost": 20.99, "Rating": "PG", "LastUpdate": "2013-05-26T14:50:58.951Z", - "SpecialFeatures": "{\"Deleted Scenes\",\"Behind the Scenes\"}", + "SpecialFeatures": [ + "Deleted Scenes", + "Behind the Scenes" + ], "Fulltext": "'academi':1 'battl':15 'canadian':20 'dinosaur':2 'drama':5 'epic':4 'feminist':8 'mad':11 'must':14 'rocki':21 'scientist':12 'teacher':17", "Actors": [ { @@ -3406,7 +3410,10 @@ func TestRecursionScanNxM(t *testing.T) { "ReplacementCost": 9.99, "Rating": "R", "LastUpdate": "2013-05-26T14:50:58.951Z", - "SpecialFeatures": "{Trailers,\"Deleted Scenes\"}", + "SpecialFeatures": [ + "Trailers", + "Deleted Scenes" + ], "Fulltext": "'anaconda':1 'australia':18 'confess':2 'dentist':8,11 'display':5 'fight':14 'girl':16 'lacklustur':4 'must':13", "Actors": [ { @@ -3454,7 +3461,10 @@ func TestRecursionScanNxM(t *testing.T) { "ReplacementCost": 20.99, "Rating": "PG", "LastUpdate": "2013-05-26T14:50:58.951Z", - "SpecialFeatures": "{\"Deleted Scenes\",\"Behind the Scenes\"}", + "SpecialFeatures": [ + "Deleted Scenes", + "Behind the Scenes" + ], "Fulltext": "'academi':1 'battl':15 'canadian':20 'dinosaur':2 'drama':5 'epic':4 'feminist':8 'mad':11 'must':14 'rocki':21 'scientist':12 'teacher':17", "Actors": null }, @@ -3470,7 +3480,10 @@ func TestRecursionScanNxM(t *testing.T) { "ReplacementCost": 9.99, "Rating": "R", "LastUpdate": "2013-05-26T14:50:58.951Z", - "SpecialFeatures": "{Trailers,\"Deleted Scenes\"}", + "SpecialFeatures": [ + "Trailers", + "Deleted Scenes" + ], "Fulltext": "'anaconda':1 'australia':18 'confess':2 'dentist':8,11 'display':5 'fight':14 'girl':16 'lacklustur':4 'must':13", "Actors": null }