Add support for postgres arrays

This commit is contained in:
Arjen Brouwer 2024-09-03 15:39:36 +02:00 committed by go-jet
parent b835e25665
commit d3ada5361e
27 changed files with 558 additions and 74 deletions

View file

@ -42,4 +42,5 @@ type DataType struct {
Name string
Kind DataTypeKind
IsUnsigned bool
Dimensions int // The number of array dimensions
}

View file

@ -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'

View file

@ -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 ""

View file

@ -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 ""
}
}

View file

@ -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)
}

View file

@ -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))
}

View file

@ -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 {

View file

@ -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"`)

View file

@ -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
}

View file

@ -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

View file

@ -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, "&&")

View file

@ -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) + `"`
}

View file

@ -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)
}

View file

@ -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")

View file

@ -26,8 +26,11 @@ var (
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")

View file

@ -45,6 +45,10 @@ type cast interface {
AS_INTERVAL() IntervalExpression
}
type castArray interface {
AS_STRING() jet.ArrayExpression[StringExpression]
}
type castImpl struct {
jet.Cast
}

View file

@ -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.

View file

@ -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

View file

@ -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";
`)
}

View file

@ -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) {

View file

@ -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")

View file

@ -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:

View file

@ -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,

View file

@ -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) {

View file

@ -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")

View file

@ -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`,
}

View file

@ -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
}