From 5eb6114bacd43c6a330ef0d1c636237414b76c9c Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Tue, 3 Sep 2024 15:39:36 +0200 Subject: [PATCH 01/34] Initial support for postgres text arrays --- 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 | 13 ++++ internal/jet/expression.go | 27 ++++++++ internal/jet/literal_expression.go | 15 +++++ 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 | 6 +- 11 files changed, 305 insertions(+), 4 deletions(-) create mode 100644 internal/jet/array_expression.go create mode 100644 internal/jet/array_expression_test.go diff --git a/internal/jet/array_expression.go b/internal/jet/array_expression.go new file mode 100644 index 00000000..66cc005b --- /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 00000000..2a4960a2 --- /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, table1ColArray.EQ(table2ColArray), "(table1.col_array_string = table2.col_array_string)") +} + +func TestArrayExpressionNOT_EQ(t *testing.T) { + assertClauseSerialize(t, table1ColArray.NOT_EQ(table2ColArray), "(table1.col_array_string != table2.col_array_string)") + assertClauseSerialize(t, table1ColArray.NOT_EQ(StringArray([]string{"x"})), "(table1.col_array_string != $1)", pq.StringArray{"x"}) +} + +func TestArrayExpressionLT(t *testing.T) { + assertClauseSerialize(t, table1ColArray.LT(table2ColArray), "(table1.col_array_string < table2.col_array_string)") +} + +func TestArrayExpressionGT(t *testing.T) { + assertClauseSerialize(t, table1ColArray.GT(table2ColArray), "(table1.col_array_string > table2.col_array_string)") +} + +func TestArrayExpressionLT_EQ(t *testing.T) { + assertClauseSerialize(t, table1ColArray.LT_EQ(table2ColArray), "(table1.col_array_string <= table2.col_array_string)") +} + +func TestArrayExpressionGT_EQ(t *testing.T) { + assertClauseSerialize(t, table1ColArray.GT_EQ(table2ColArray), "(table1.col_array_string >= table2.col_array_string)") +} + +func TestArrayExpressionCONTAINS(t *testing.T) { + assertClauseSerialize(t, table1ColArray.CONTAINS(table2ColArray), "(table1.col_array_string @> table2.col_array_string)") + assertClauseSerialize(t, table1ColArray.CONTAINS(StringArray([]string{"x"})), "(table1.col_array_string @> $1)", pq.StringArray{"x"}) +} + +func TestArrayExpressionCONTAINED_BY(t *testing.T) { + assertClauseSerialize(t, table1ColArray.IS_CONTAINED_BY(table2ColArray), "(table1.col_array_string <@ table2.col_array_string)") + assertClauseSerialize(t, table1ColArray.IS_CONTAINED_BY(StringArray([]string{"x"})), "(table1.col_array_string <@ $1)", pq.StringArray{"x"}) +} + +func TestArrayExpressionOVERLAP(t *testing.T) { + assertClauseSerialize(t, table1ColArray.OVERLAP(table2ColArray), "(table1.col_array_string && table2.col_array_string)") +} + +func TestArrayExpressionCONCAT(t *testing.T) { + assertClauseSerialize(t, table1ColArray.CONCAT(table2ColArray), "(table1.col_array_string || table2.col_array_string)") + assertClauseSerialize(t, table1ColArray.CONCAT(StringArray([]string{"x"})), "(table1.col_array_string || $1)", pq.StringArray{"x"}) +} + +func TestArrayExpressionCONCAT_ELEMENT(t *testing.T) { + assertClauseSerialize(t, table1ColArray.CONCAT_ELEMENT(StringExp(table2ColArray.AT(Int(1)))), "(table1.col_array_string || (table2.col_array_string[$1]))", int64(1)) + assertClauseSerialize(t, table1ColArray.CONCAT_ELEMENT(String("x")), "(table1.col_array_string || $1)", "x") +} + +func TestArrayExpressionAT(t *testing.T) { + assertClauseSerialize(t, table1ColArray.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 a7320615..17285836 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 059d722d..0beec856 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,18 @@ var subQuery = &selectTableImpl{ alias: "sub_query", } +func TestNewArrayColumn(t *testing.T) { + arrayColumn := ArrayColumn[StringExpression]("colArray").From(subQuery) + assertClauseSerialize(t, arrayColumn, `sub_query."colArray"`) + assertClauseSerialize(t, arrayColumn.EQ(StringArray([]string{"X"})), `(sub_query."colArray" = $1)`, pq.StringArray{"X"}) + assertProjectionSerialize(t, arrayColumn, `sub_query."colArray" AS "colArray"`) + + arrayColumn2 := table1ColArray.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 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 9999803f..28d12dec 100644 --- a/internal/jet/expression.go +++ b/internal/jet/expression.go @@ -316,6 +316,33 @@ 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 +} + func wrap(expressions ...Expression) Expression { return NewFunc("", expressions, nil) } diff --git a/internal/jet/literal_expression.go b/internal/jet/literal_expression.go index 251d3ab9..b09bcfe1 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,20 @@ func Decimal(value string) FloatExpression { return &floatLiteral } +// ---------------------------------------------------// +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 stringLiteral struct { stringInterfaceImpl diff --git a/internal/jet/operators.go b/internal/jet/operators.go index c453c3e0..6ad46d42 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 46f47ad4..288335d6 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 29b24472..f61c6894 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 0f461acc..96bd3b40 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(table1ColArray), "(table2.col_str = ANY(table1.col_array_string))") +} + +func TestStringALL_EQ(t *testing.T) { + assertClauseSerialize(t, table2ColStr.ALL_EQ(table1ColArray), "(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 70b21c77..45765fb6 100644 --- a/internal/jet/testutils.go +++ b/internal/jet/testutils.go @@ -26,8 +26,9 @@ var ( table1ColBool = BoolColumn("col_bool") table1ColDate = DateColumn("col_date") table1ColRange = RangeColumn[Int8Expression]("col_range") + table1ColArray = ArrayColumn[StringExpression]("col_array_string") ) -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, table1ColArray) var ( table2Col3 = IntegerColumn("col3") @@ -42,8 +43,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") From 4591991852182c2e0765133fdaf69241b2c7c8c1 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Tue, 3 Sep 2024 15:39:55 +0200 Subject: [PATCH 02/34] Support arrays in postgres layer --- postgres/columns.go | 4 ++++ postgres/expressions.go | 2 ++ postgres/literal.go | 4 ++++ postgres/utils_test.go | 5 ++++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/postgres/columns.go b/postgres/columns.go index a70c234b..3a679706 100644 --- a/postgres/columns.go +++ b/postgres/columns.go @@ -7,6 +7,10 @@ import ( // Column is common column interface for all types of columns. type Column = jet.ColumnExpression +type ColumnStringArray jet.ColumnArray[StringExpression] + +var StringArrayColumn = jet.ArrayColumn[StringExpression] + // ColumnList function returns list of columns that be used as projection or column list for UPDATE and INSERT statement. type ColumnList = jet.ColumnList diff --git a/postgres/expressions.go b/postgres/expressions.go index d8ad34b4..3ecd796d 100644 --- a/postgres/expressions.go +++ b/postgres/expressions.go @@ -9,6 +9,8 @@ type Expression = jet.Expression // BoolExpression interface type BoolExpression = jet.BoolExpression +type StringArrayExpression = jet.ArrayExpression[StringExpression] + // StringExpression interface type StringExpression = jet.StringExpression diff --git a/postgres/literal.go b/postgres/literal.go index 4f1c2c87..6ae59b71 100644 --- a/postgres/literal.go +++ b/postgres/literal.go @@ -75,6 +75,10 @@ func String(value string) StringExpression { return CAST(jet.String(value)).AS_TEXT() } +func StringArray(elements []string) StringArrayExpression { + return jet.StringArray(elements) +} + // Text is a parameter constructor for the PostgreSQL text type. This constructor also adds an // explicit placeholder type cast to text in the generated query, such as `$3::text`. // Example usage: diff --git a/postgres/utils_test.go b/postgres/utils_test.go index 96bb13b0..db13a5bd 100644 --- a/postgres/utils_test.go +++ b/postgres/utils_test.go @@ -18,6 +18,7 @@ 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 table1 = NewTable( "db", @@ -34,6 +35,7 @@ var table1 = NewTable( table1ColTimestampz, table1ColInterval, table1ColRange, + table1ColStringArray, ) var table2Col3 = IntegerColumn("col3") @@ -49,8 +51,9 @@ 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 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) var table3Col1 = IntegerColumn("col1") var table3ColInt = IntegerColumn("col_int") From ad15139adb6efb6e57db2d74a68b261b7af8141b Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Tue, 3 Sep 2024 15:40:08 +0200 Subject: [PATCH 03/34] Extend generator with text array support --- generator/template/sql_builder_template.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/generator/template/sql_builder_template.go b/generator/template/sql_builder_template.go index a72e8e99..f1877d73 100644 --- a/generator/template/sql_builder_template.go +++ b/generator/template/sql_builder_template.go @@ -160,7 +160,8 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { return "String" } - switch strings.ToLower(columnMetaData.DataType.Name) { + typeName := columnMetaData.DataType.Name + switch strings.ToLower(typeName) { case "boolean", "bool": return "Bool" case "smallint", "integer", "bigint", "int2", "int4", "int8", @@ -200,8 +201,10 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { return "Int8Range" case "numrange": return "NumericRange" + case "text[]": + return "StringArray" default: - fmt.Println("- [SQL Builder] Unsupported sql column '" + columnMetaData.Name + " " + columnMetaData.DataType.Name + "', using StringColumn instead.") + fmt.Println("- [SQL Builder] Unsupported sql column '" + columnMetaData.Name + " " + typeName + "', using StringColumn instead.") return "String" } } From ac24ae6e4492d9540ea6d3301b543db1d303735b Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Tue, 3 Sep 2024 19:26:40 +0200 Subject: [PATCH 04/34] Introduced more literal types --- internal/jet/literal_expression.go | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/internal/jet/literal_expression.go b/internal/jet/literal_expression.go index b09bcfe1..a79a78cc 100644 --- a/internal/jet/literal_expression.go +++ b/internal/jet/literal_expression.go @@ -162,6 +162,39 @@ func Decimal(value string) FloatExpression { } // ---------------------------------------------------// + +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 @@ -175,6 +208,19 @@ func StringArray(values []string) ArrayExpression[StringExpression] { 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 From 7592ddddff82ac68479dadfc7b365c14a4a0f656 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Tue, 3 Sep 2024 19:27:12 +0200 Subject: [PATCH 05/34] Use generic types --- postgres/columns.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/postgres/columns.go b/postgres/columns.go index 3a679706..5a3c9b4c 100644 --- a/postgres/columns.go +++ b/postgres/columns.go @@ -7,9 +7,13 @@ import ( // Column is common column interface for all types of columns. type Column = jet.ColumnExpression -type ColumnStringArray jet.ColumnArray[StringExpression] +// ColumnArray is the generic interface for array types. +type ColumnArray[E Expression] jet.ColumnArray[E] -var StringArrayColumn = jet.ArrayColumn[StringExpression] +// ArrayColumn creates a named array column. +func ArrayColumn[E Expression](name string) ColumnArray[E] { + return jet.ArrayColumn[E](name) +} // ColumnList function returns list of columns that be used as projection or column list for UPDATE and INSERT statement. type ColumnList = jet.ColumnList From a133b2a2c99b4949d8a99f7d9061e56b2acf1fae Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Tue, 3 Sep 2024 19:27:20 +0200 Subject: [PATCH 06/34] Update testcases --- internal/jet/array_expression_test.go | 34 ++++++++++++------------ internal/jet/column_types_test.go | 36 +++++++++++++++++++++----- internal/jet/string_expression_test.go | 4 +-- internal/jet/testutils.go | 28 ++++++++++---------- postgres/utils_test.go | 9 ++++--- 5 files changed, 70 insertions(+), 41 deletions(-) diff --git a/internal/jet/array_expression_test.go b/internal/jet/array_expression_test.go index 2a4960a2..ed7b565b 100644 --- a/internal/jet/array_expression_test.go +++ b/internal/jet/array_expression_test.go @@ -6,54 +6,54 @@ import ( ) func TestArrayExpressionEQ(t *testing.T) { - assertClauseSerialize(t, table1ColArray.EQ(table2ColArray), "(table1.col_array_string = table2.col_array_string)") + assertClauseSerialize(t, table1ColStringArray.EQ(table2ColArray), "(table1.col_array_string = table2.col_array_string)") } func TestArrayExpressionNOT_EQ(t *testing.T) { - assertClauseSerialize(t, table1ColArray.NOT_EQ(table2ColArray), "(table1.col_array_string != table2.col_array_string)") - assertClauseSerialize(t, table1ColArray.NOT_EQ(StringArray([]string{"x"})), "(table1.col_array_string != $1)", pq.StringArray{"x"}) + 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, table1ColArray.LT(table2ColArray), "(table1.col_array_string < table2.col_array_string)") + assertClauseSerialize(t, table1ColStringArray.LT(table2ColArray), "(table1.col_array_string < table2.col_array_string)") } func TestArrayExpressionGT(t *testing.T) { - assertClauseSerialize(t, table1ColArray.GT(table2ColArray), "(table1.col_array_string > table2.col_array_string)") + assertClauseSerialize(t, table1ColStringArray.GT(table2ColArray), "(table1.col_array_string > table2.col_array_string)") } func TestArrayExpressionLT_EQ(t *testing.T) { - assertClauseSerialize(t, table1ColArray.LT_EQ(table2ColArray), "(table1.col_array_string <= table2.col_array_string)") + assertClauseSerialize(t, table1ColStringArray.LT_EQ(table2ColArray), "(table1.col_array_string <= table2.col_array_string)") } func TestArrayExpressionGT_EQ(t *testing.T) { - assertClauseSerialize(t, table1ColArray.GT_EQ(table2ColArray), "(table1.col_array_string >= table2.col_array_string)") + assertClauseSerialize(t, table1ColStringArray.GT_EQ(table2ColArray), "(table1.col_array_string >= table2.col_array_string)") } func TestArrayExpressionCONTAINS(t *testing.T) { - assertClauseSerialize(t, table1ColArray.CONTAINS(table2ColArray), "(table1.col_array_string @> table2.col_array_string)") - assertClauseSerialize(t, table1ColArray.CONTAINS(StringArray([]string{"x"})), "(table1.col_array_string @> $1)", pq.StringArray{"x"}) + 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, table1ColArray.IS_CONTAINED_BY(table2ColArray), "(table1.col_array_string <@ table2.col_array_string)") - assertClauseSerialize(t, table1ColArray.IS_CONTAINED_BY(StringArray([]string{"x"})), "(table1.col_array_string <@ $1)", pq.StringArray{"x"}) + 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, table1ColArray.OVERLAP(table2ColArray), "(table1.col_array_string && table2.col_array_string)") + assertClauseSerialize(t, table1ColStringArray.OVERLAP(table2ColArray), "(table1.col_array_string && table2.col_array_string)") } func TestArrayExpressionCONCAT(t *testing.T) { - assertClauseSerialize(t, table1ColArray.CONCAT(table2ColArray), "(table1.col_array_string || table2.col_array_string)") - assertClauseSerialize(t, table1ColArray.CONCAT(StringArray([]string{"x"})), "(table1.col_array_string || $1)", pq.StringArray{"x"}) + 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, table1ColArray.CONCAT_ELEMENT(StringExp(table2ColArray.AT(Int(1)))), "(table1.col_array_string || (table2.col_array_string[$1]))", int64(1)) - assertClauseSerialize(t, table1ColArray.CONCAT_ELEMENT(String("x")), "(table1.col_array_string || $1)", "x") + 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, table1ColArray.AT(Int(1)), "(table1.col_array_string[$1])", int64(1)) + assertClauseSerialize(t, table1ColStringArray.AT(Int(1)), "(table1.col_array_string[$1])", int64(1)) } diff --git a/internal/jet/column_types_test.go b/internal/jet/column_types_test.go index 0beec856..38d9e96b 100644 --- a/internal/jet/column_types_test.go +++ b/internal/jet/column_types_test.go @@ -9,18 +9,42 @@ var subQuery = &selectTableImpl{ alias: "sub_query", } -func TestNewArrayColumn(t *testing.T) { - arrayColumn := ArrayColumn[StringExpression]("colArray").From(subQuery) - assertClauseSerialize(t, arrayColumn, `sub_query."colArray"`) - assertClauseSerialize(t, arrayColumn.EQ(StringArray([]string{"X"})), `(sub_query."colArray" = $1)`, pq.StringArray{"X"}) - assertProjectionSerialize(t, arrayColumn, `sub_query."colArray" AS "colArray"`) +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 := table1ColArray.From(subQuery) + 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/string_expression_test.go b/internal/jet/string_expression_test.go index 96bd3b40..830937f0 100644 --- a/internal/jet/string_expression_test.go +++ b/internal/jet/string_expression_test.go @@ -77,11 +77,11 @@ func TestStringNOT_REGEXP_LIKE(t *testing.T) { } func TestStringANY_EQ(t *testing.T) { - assertClauseSerialize(t, table2ColStr.ANY_EQ(table1ColArray), "(table2.col_str = ANY(table1.col_array_string))") + 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(table1ColArray), "(table2.col_str = ALL(table1.col_array_string))") + assertClauseSerialize(t, table2ColStr.ALL_EQ(table1ColStringArray), "(table2.col_str = ALL(table1.col_array_string))") } func TestStringExp(t *testing.T) { diff --git a/internal/jet/testutils.go b/internal/jet/testutils.go index 45765fb6..0f4ff8a6 100644 --- a/internal/jet/testutils.go +++ b/internal/jet/testutils.go @@ -15,20 +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") - table1ColArray = ArrayColumn[StringExpression]("col_array_string") + 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, table1ColArray) +var table1 = NewTable("db", "table1", "", table1Col1, table1ColInt, table1ColFloat, table1Col3, table1ColTime, table1ColTimez, table1ColBool, table1ColDate, table1ColRange, table1ColTimestamp, table1ColTimestampz, table1ColStringArray, table1ColBoolArray, table1ColIntArray) var ( table2Col3 = IntegerColumn("col3") diff --git a/postgres/utils_test.go b/postgres/utils_test.go index db13a5bd..1873b28f 100644 --- a/postgres/utils_test.go +++ b/postgres/utils_test.go @@ -18,7 +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 table1ColStringArray = ArrayColumn[StringExpression]("col_string_array") +var table1ColIntArray = ArrayColumn[IntegerExpression]("col_int_array") var table1 = NewTable( "db", @@ -36,6 +37,7 @@ var table1 = NewTable( table1ColInterval, table1ColRange, table1ColStringArray, + table1ColIntArray, ) var table2Col3 = IntegerColumn("col3") @@ -51,9 +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 table2ColStringArray = ArrayColumn[StringExpression]("col_string_array") +var table2ColIntArray = ArrayColumn[IntegerExpression]("col_int_array") -var table2 = NewTable("db", "table2", "", table2Col3, table2Col4, table2ColInt, table2ColFloat, table2ColStr, table2ColBool, table2ColTime, table2ColTimez, table2ColDate, table2ColTimestamp, table2ColTimestampz, table2ColInterval, table2ColRange, table2ColStringArray) +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") From 29f6de448e2d3da7c34d58f54cdc791f2ac9ccd4 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Tue, 3 Sep 2024 20:38:59 +0200 Subject: [PATCH 07/34] Don't use generics in postgres package --- postgres/columns.go | 26 ++++++++++++++++++-------- postgres/expressions.go | 9 ++++++++- postgres/literal.go | 7 +++++++ postgres/utils_test.go | 8 ++++---- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/postgres/columns.go b/postgres/columns.go index 5a3c9b4c..bd158a64 100644 --- a/postgres/columns.go +++ b/postgres/columns.go @@ -7,14 +7,6 @@ import ( // Column is common column interface for all types of columns. type Column = jet.ColumnExpression -// ColumnArray is the generic interface for array types. -type ColumnArray[E Expression] jet.ColumnArray[E] - -// ArrayColumn creates a named array column. -func ArrayColumn[E Expression](name string) ColumnArray[E] { - return jet.ArrayColumn[E](name) -} - // ColumnList function returns list of columns that be used as projection or column list for UPDATE and INSERT statement. type ColumnList = jet.ColumnList @@ -109,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 3ecd796d..6c593a8e 100644 --- a/postgres/expressions.go +++ b/postgres/expressions.go @@ -9,17 +9,24 @@ type Expression = jet.Expression // BoolExpression interface type BoolExpression = jet.BoolExpression -type StringArrayExpression = jet.ArrayExpression[StringExpression] +// 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/literal.go b/postgres/literal.go index 6ae59b71..80392e62 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,7 @@ func Double(value float64) FloatExpression { // Decimal creates new float literal expression var Decimal = jet.Decimal +// String creates new string literal expression // String is a parameter constructor for the PostgreSQL text type. Using the `Text` constructor is // generally preferable. // @@ -75,6 +81,7 @@ 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) } diff --git a/postgres/utils_test.go b/postgres/utils_test.go index 1873b28f..d89b17bd 100644 --- a/postgres/utils_test.go +++ b/postgres/utils_test.go @@ -18,8 +18,8 @@ var table1ColBool = BoolColumn("col_bool") var table1ColDate = DateColumn("col_date") var table1ColInterval = IntervalColumn("col_interval") var table1ColRange = Int8RangeColumn("col_range") -var table1ColStringArray = ArrayColumn[StringExpression]("col_string_array") -var table1ColIntArray = ArrayColumn[IntegerExpression]("col_int_array") +var table1ColStringArray = StringArrayColumn("col_string_array") +var table1ColIntArray = IntegerArrayColumn("col_int_array") var table1 = NewTable( "db", @@ -53,8 +53,8 @@ var table2ColTimestampz = TimestampzColumn("col_timestampz") var table2ColDate = DateColumn("col_date") var table2ColInterval = IntervalColumn("col_interval") var table2ColRange = Int8RangeColumn("col_range") -var table2ColStringArray = ArrayColumn[StringExpression]("col_string_array") -var table2ColIntArray = ArrayColumn[IntegerExpression]("col_int_array") +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, table2ColStringArray, table2ColIntArray) From 6777c42bb93d2d9d84499a82b42ab778ff2d3b1b Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Tue, 3 Sep 2024 20:39:06 +0200 Subject: [PATCH 08/34] Update generator --- generator/template/model_template.go | 11 +++++++- generator/template/sql_builder_template.go | 30 +++++++++++++++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/generator/template/model_template.go b/generator/template/model_template.go index 84f46d70..8cbd3311 100644 --- a/generator/template/model_template.go +++ b/generator/template/model_template.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "github.com/jackc/pgtype" "path/filepath" + "github.com/lib/pq" "reflect" "strings" "time" @@ -251,7 +252,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" } @@ -335,6 +336,14 @@ func toGoType(column metadata.Column) interface{} { return pgtype.Int8range{} case "numrange": return pgtype.Numrange{} + case "bool[]": + return pq.BoolArray{} + case "int32[]": + return pq.Int32Array{} + case "int64[]": + return pq.Int64Array{} + case "text[]": + 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 f1877d73..a5b518e5 100644 --- a/generator/template/sql_builder_template.go +++ b/generator/template/sql_builder_template.go @@ -156,11 +156,36 @@ 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" } typeName := columnMetaData.DataType.Name + columnName := columnMetaData.Name + + if columnMetaData.DataType.Kind == metadata.ArrayType { + 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 column '" + columnName + " " + typeName + "', using StringArrayColumn instead.") + return "StringArray" + } + + 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" @@ -204,8 +229,7 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { case "text[]": return "StringArray" default: - fmt.Println("- [SQL Builder] Unsupported sql column '" + columnMetaData.Name + " " + typeName + "', using StringColumn instead.") - return "String" + return "" } } From 2479eaa51be5cb89bdbf077b60228b12c96fdf7e Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Wed, 4 Sep 2024 08:22:54 +0200 Subject: [PATCH 09/34] Take array dimensions into consideration --- generator/metadata/column_meta_data.go | 1 + generator/postgres/query_set.go | 1 + generator/template/model_template.go | 11 ++++++++--- generator/template/sql_builder_template.go | 9 +++++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/generator/metadata/column_meta_data.go b/generator/metadata/column_meta_data.go index ecd61e22..5b6ce8ab 100644 --- a/generator/metadata/column_meta_data.go +++ b/generator/metadata/column_meta_data.go @@ -28,4 +28,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 fc4135ab..a60c01aa 100644 --- a/generator/postgres/query_set.go +++ b/generator/postgres/query_set.go @@ -66,6 +66,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 8cbd3311..9d1cae7a 100644 --- a/generator/template/model_template.go +++ b/generator/template/model_template.go @@ -271,6 +271,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 "" @@ -338,11 +343,11 @@ func toGoType(column metadata.Column) interface{} { return pgtype.Numrange{} case "bool[]": return pq.BoolArray{} - case "int32[]": + case "integer[]", "int4[]": return pq.Int32Array{} - case "int64[]": + case "bigint[]": return pq.Int64Array{} - case "text[]": + case "text[]", "jsonb[]", "json[]": return pq.StringArray{} default: fmt.Println("- [Model ] Unsupported sql column '" + column.Name + " " + column.DataType.Name + "', using string instead.") diff --git a/generator/template/sql_builder_template.go b/generator/template/sql_builder_template.go index a5b518e5..f0264046 100644 --- a/generator/template/sql_builder_template.go +++ b/generator/template/sql_builder_template.go @@ -165,12 +165,17 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { 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 column '" + columnName + " " + typeName + "', using StringArrayColumn instead.") - return "StringArray" + fmt.Println("- [SQL Builder] Unsupported sql array column '" + columnName + " " + typeName + "', using StringColumn instead.") + return "String" } return c + "Array" From a3f4d2a90d08c84c03eb3046db512b862c920e2a Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Wed, 4 Sep 2024 08:23:36 +0200 Subject: [PATCH 10/34] Use MySQL server container which also support ARM64 to allow running testcases on macOs --- tests/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml index bcbbb25f..408fc19c 100644 --- a/tests/docker-compose.yaml +++ b/tests/docker-compose.yaml @@ -13,7 +13,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: From 2ab72b0049cd27659a135b649921e281140fd273 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Wed, 4 Sep 2024 08:23:53 +0200 Subject: [PATCH 11/34] Update test cases for array types --- tests/postgres/alltypes_test.go | 18 +++++++------ tests/postgres/generator_template_test.go | 2 +- tests/postgres/generator_test.go | 31 ++++++++++++----------- tests/postgres/scan_test.go | 21 +++++++-------- tests/postgres/select_test.go | 25 ++++++++++++++---- 5 files changed, 58 insertions(+), 39 deletions(-) diff --git a/tests/postgres/alltypes_test.go b/tests/postgres/alltypes_test.go index 6c3755a2..c60cd192 100644 --- a/tests/postgres/alltypes_test.go +++ b/tests/postgres/alltypes_test.go @@ -5,6 +5,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/go-jet/jet/v2/qrm" + "database/sql" + "github.com/lib/pq" "testing" "time" @@ -1461,11 +1463,11 @@ var allTypesRow0 = model.AllTypes{ JSON: `{"a": 1, "b": 3}`, JsonbPtr: ptr.Of(`{"a": 1, "b": 3}`), Jsonb: `{"a": 1, "b": 3}`, - IntegerArrayPtr: ptr.Of("{1,2,3}"), - IntegerArray: "{1,2,3}", - TextArrayPtr: ptr.Of("{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: ptr.Of("{{meeting,lunch},{training,presentation}}"), TextMultiDimArray: "{{meeting,lunch},{training,presentation}}", MoodPtr: &moodSad, @@ -1530,10 +1532,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 e518db71..e72cb16c 100644 --- a/tests/postgres/generator_template_test.go +++ b/tests/postgres/generator_template_test.go @@ -446,7 +446,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 93fb9c3a..df3f3a0c 100644 --- a/tests/postgres/generator_test.go +++ b/tests/postgres/generator_test.go @@ -836,6 +836,7 @@ package model import ( "github.com/google/uuid" + "github.com/lib/pq" "time" ) @@ -894,11 +895,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 @@ -999,11 +1000,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 @@ -1102,11 +1103,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 321fc383..931a3504 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/go-jet/jet/v2/internal/utils/ptr" "github.com/volatiletech/null/v8" "testing" @@ -954,6 +955,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 { @@ -968,26 +970,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) - require.NoError(t, err) - var myFilms []film + err := stmt.Query(db, &myFilms) + require.NoError(t, err) - 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)) @@ -1161,7 +1162,7 @@ var film1 = model.Film{ ReplacementCost: 20.99, Rating: &pgRating, LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3), - SpecialFeatures: ptr.Of("{\"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", } @@ -1177,7 +1178,7 @@ var film2 = model.Film{ ReplacementCost: 12.99, Rating: &gRating, LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3), - SpecialFeatures: ptr.Of(`{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 d39987d6..e78e4b27 100644 --- a/tests/postgres/select_test.go +++ b/tests/postgres/select_test.go @@ -3,6 +3,9 @@ package postgres import ( "context" "database/sql" + "encoding/json" + "github.com/lib/pq" + "io/ioutil" "github.com/go-jet/jet/v2/internal/utils/ptr" "testing" "time" @@ -1838,7 +1841,7 @@ ORDER BY film.film_id ASC; Rating: &gRating, RentalDuration: 3, LastUpdate: *testutils.TimestampWithoutTimeZone("2013-05-26 14:50:58.951", 3), - SpecialFeatures: ptr.Of("{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", }) } @@ -3360,7 +3363,10 @@ func TestSelectRecursionScanNxM(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": [ { @@ -3384,7 +3390,10 @@ func TestSelectRecursionScanNxM(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": [ { @@ -3432,7 +3441,10 @@ func TestSelectRecursionScanNxM(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 }, @@ -3448,7 +3460,10 @@ func TestSelectRecursionScanNxM(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 } From 34b2db706c5d102be8a44dc853c20bd08da389db Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Wed, 4 Sep 2024 14:14:28 +0200 Subject: [PATCH 12/34] Inadvertently copied function --- tests/docker-compose.yaml | 1 - tests/postgres/select_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/docker-compose.yaml b/tests/docker-compose.yaml index 408fc19c..9aba1ec2 100644 --- a/tests/docker-compose.yaml +++ b/tests/docker-compose.yaml @@ -1,4 +1,3 @@ -version: '3' services: postgres: image: postgres:14.1 diff --git a/tests/postgres/select_test.go b/tests/postgres/select_test.go index e78e4b27..fecf20e4 100644 --- a/tests/postgres/select_test.go +++ b/tests/postgres/select_test.go @@ -3,7 +3,6 @@ package postgres import ( "context" "database/sql" - "encoding/json" "github.com/lib/pq" "io/ioutil" "github.com/go-jet/jet/v2/internal/utils/ptr" From ea6d437110d25e05161baa223153c973c1a53569 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Sat, 7 Sep 2024 19:58:54 +0200 Subject: [PATCH 13/34] Add a function to map SQL array types --- generator/template/sql_builder_template.go | 36 ++++++++++++++-------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/generator/template/sql_builder_template.go b/generator/template/sql_builder_template.go index f0264046..5f0fb808 100644 --- a/generator/template/sql_builder_template.go +++ b/generator/template/sql_builder_template.go @@ -164,24 +164,19 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { typeName := columnMetaData.DataType.Name columnName := columnMetaData.Name + var columnType string + 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 = sqlArrayToColumnType(strings.TrimSuffix(typeName, "[]")) + } else { + columnType = sqlToColumnType(typeName) } - columnType := sqlToColumnType(typeName) if columnType == "" { fmt.Println("- [SQL Builder] Unsupported sql column '" + columnName + " " + typeName + "', using StringColumn instead.") return "String" @@ -190,6 +185,25 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { return columnType } +// sqlArrayToColumnType maps the type of an SQL array column type to a go jet sql builder column. Note that you don't +// pass the brackets `[]`, signifying an SQL array type, into this function +func sqlArrayToColumnType(typeName string) string { + switch strings.ToLower(typeName) { + case "user-defined", "enum", "text", "character", "character varying", "bytea", "uuid", + "tsvector", "bit", "bit varying", "money", "json", "jsonb", "xml", "point", "line", "ARRAY", + "char", "varchar", "nvarchar", "binary", "varbinary", "bpchar", "varbit", + "tinyblob", "blob", "mediumblob", "longblob", "tinytext", "mediumtext", "longtext": // MySQL + return "StringArray" + case "smallint", "integer", "bigint", "int2", "int4", "int8", + "tinyint", "mediumint", "int", "year": //MySQL + return "IntegerArray" + case "boolean", "bool": + return "BoolArray" + default: + return "" + } +} + func sqlToColumnType(typeName string) string { switch strings.ToLower(typeName) { case "boolean", "bool": @@ -231,8 +245,6 @@ func sqlToColumnType(typeName string) string { return "Int8Range" case "numrange": return "NumericRange" - case "text[]": - return "StringArray" default: return "" } From 8bc73a8525bc11456a56f419e8fb5477e0c09bc1 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Sat, 7 Sep 2024 20:01:37 +0200 Subject: [PATCH 14/34] Renamed ArrayExpression to Array --- internal/jet/array_expression.go | 54 +++++++++++++++--------------- internal/jet/column_types.go | 6 ++-- internal/jet/literal_expression.go | 10 +++--- internal/jet/string_expression.go | 8 ++--- postgres/expressions.go | 6 ++-- 5 files changed, 42 insertions(+), 42 deletions(-) diff --git a/internal/jet/array_expression.go b/internal/jet/array_expression.go index 66cc005b..0399b5ab 100644 --- a/internal/jet/array_expression.go +++ b/internal/jet/array_expression.go @@ -1,72 +1,72 @@ package jet -// ArrayExpression interface -type ArrayExpression[E Expression] interface { +// Array interface +type Array[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 + EQ(rhs Array[E]) BoolExpression + NOT_EQ(rhs Array[E]) BoolExpression + LT(rhs Array[E]) BoolExpression + GT(rhs Array[E]) BoolExpression + LT_EQ(rhs Array[E]) BoolExpression + GT_EQ(rhs Array[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] + CONTAINS(rhs Array[E]) BoolExpression + IS_CONTAINED_BY(rhs Array[E]) BoolExpression + OVERLAP(rhs Array[E]) BoolExpression + CONCAT(rhs Array[E]) Array[E] + CONCAT_ELEMENT(E) Array[E] AT(expression IntegerExpression) Expression } type arrayInterfaceImpl[E Expression] struct { - parent ArrayExpression[E] + parent Array[E] } type BinaryBoolOp func(Expression, Expression) BoolExpression -func (a arrayInterfaceImpl[E]) EQ(rhs ArrayExpression[E]) BoolExpression { +func (a arrayInterfaceImpl[E]) EQ(rhs Array[E]) BoolExpression { return Eq(a.parent, rhs) } -func (a arrayInterfaceImpl[E]) NOT_EQ(rhs ArrayExpression[E]) BoolExpression { +func (a arrayInterfaceImpl[E]) NOT_EQ(rhs Array[E]) BoolExpression { return NotEq(a.parent, rhs) } -func (a arrayInterfaceImpl[E]) LT(rhs ArrayExpression[E]) BoolExpression { +func (a arrayInterfaceImpl[E]) LT(rhs Array[E]) BoolExpression { return Lt(a.parent, rhs) } -func (a arrayInterfaceImpl[E]) GT(rhs ArrayExpression[E]) BoolExpression { +func (a arrayInterfaceImpl[E]) GT(rhs Array[E]) BoolExpression { return Gt(a.parent, rhs) } -func (a arrayInterfaceImpl[E]) LT_EQ(rhs ArrayExpression[E]) BoolExpression { +func (a arrayInterfaceImpl[E]) LT_EQ(rhs Array[E]) BoolExpression { return LtEq(a.parent, rhs) } -func (a arrayInterfaceImpl[E]) GT_EQ(rhs ArrayExpression[E]) BoolExpression { +func (a arrayInterfaceImpl[E]) GT_EQ(rhs Array[E]) BoolExpression { return GtEq(a.parent, rhs) } -func (a arrayInterfaceImpl[E]) CONTAINS(rhs ArrayExpression[E]) BoolExpression { +func (a arrayInterfaceImpl[E]) CONTAINS(rhs Array[E]) BoolExpression { return Contains(a.parent, rhs) } -func (a arrayInterfaceImpl[E]) IS_CONTAINED_BY(rhs ArrayExpression[E]) BoolExpression { +func (a arrayInterfaceImpl[E]) IS_CONTAINED_BY(rhs Array[E]) BoolExpression { return IsContainedBy(a.parent, rhs) } -func (a arrayInterfaceImpl[E]) OVERLAP(rhs ArrayExpression[E]) BoolExpression { +func (a arrayInterfaceImpl[E]) OVERLAP(rhs Array[E]) BoolExpression { return Overlap(a.parent, rhs) } -func (a arrayInterfaceImpl[E]) CONCAT(rhs ArrayExpression[E]) ArrayExpression[E] { +func (a arrayInterfaceImpl[E]) CONCAT(rhs Array[E]) Array[E] { return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||")) } -func (a arrayInterfaceImpl[E]) CONCAT_ELEMENT(rhs E) ArrayExpression[E] { +func (a arrayInterfaceImpl[E]) CONCAT_ELEMENT(rhs E) Array[E] { return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||")) } @@ -79,7 +79,7 @@ type arrayExpressionWrapper[E Expression] struct { Expression } -func newArrayExpressionWrap[E Expression](expression Expression) ArrayExpression[E] { +func newArrayExpressionWrap[E Expression](expression Expression) Array[E] { arrayExpressionWrapper := arrayExpressionWrapper[E]{Expression: expression} arrayExpressionWrapper.arrayInterfaceImpl.parent = &arrayExpressionWrapper return &arrayExpressionWrapper @@ -88,6 +88,6 @@ func newArrayExpressionWrap[E Expression](expression Expression) ArrayExpression // 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] { +func ArrayExp[E Expression](expression Expression) Array[E] { return newArrayExpressionWrap[E](expression) } diff --git a/internal/jet/column_types.go b/internal/jet/column_types.go index 17285836..2c47b103 100644 --- a/internal/jet/column_types.go +++ b/internal/jet/column_types.go @@ -122,11 +122,11 @@ func IntegerColumn(name string) ColumnInteger { //------------------------------------------------------// type ColumnArray[E Expression] interface { - ArrayExpression[E] + Array[E] Column From(subQuery SelectTable) ColumnArray[E] - SET(stringExp ArrayExpression[E]) ColumnAssigment + SET(stringExp Array[E]) ColumnAssigment } type arrayColumnImpl[E Expression] struct { @@ -143,7 +143,7 @@ func (a arrayColumnImpl[E]) From(subQuery SelectTable) ColumnArray[E] { return newArrayColumn } -func (a *arrayColumnImpl[E]) SET(stringExp ArrayExpression[E]) ColumnAssigment { +func (a *arrayColumnImpl[E]) SET(stringExp Array[E]) ColumnAssigment { return columnAssigmentImpl{ column: a, expression: stringExp, diff --git a/internal/jet/literal_expression.go b/internal/jet/literal_expression.go index a79a78cc..94782975 100644 --- a/internal/jet/literal_expression.go +++ b/internal/jet/literal_expression.go @@ -168,7 +168,7 @@ type boolArrayLiteral struct { literalExpressionImpl } -func BoolArray(values []bool) ArrayExpression[BoolExpression] { +func BoolArray(values []bool) Array[BoolExpression] { l := boolArrayLiteral{} l.literalExpressionImpl = *literal(pq.BoolArray(values)) l.arrayInterfaceImpl.parent = &l @@ -181,14 +181,14 @@ type integerArrayLiteral struct { literalExpressionImpl } -func Int64Array(values []int64) ArrayExpression[IntegerExpression] { +func Int64Array(values []int64) Array[IntegerExpression] { l := integerArrayLiteral{} l.literalExpressionImpl = *literal(pq.Int64Array(values)) l.arrayInterfaceImpl.parent = &l return &l } -func Int32Array(values []int32) ArrayExpression[IntegerExpression] { +func Int32Array(values []int32) Array[IntegerExpression] { l := integerArrayLiteral{} l.literalExpressionImpl = *literal(pq.Int32Array(values)) l.arrayInterfaceImpl.parent = &l @@ -200,7 +200,7 @@ type stringArrayLiteral struct { literalExpressionImpl } -func StringArray(values []string) ArrayExpression[StringExpression] { +func StringArray(values []string) Array[StringExpression] { l := stringArrayLiteral{} l.literalExpressionImpl = *literal(pq.StringArray(values)) l.arrayInterfaceImpl.parent = &l @@ -213,7 +213,7 @@ type unsafeArrayLiteral[E Expression] struct { literalExpressionImpl } -func UnsafeArray[E LiteralExpression](values []interface{}) ArrayExpression[E] { +func UnsafeArray[E LiteralExpression](values []interface{}) Array[E] { l := unsafeArrayLiteral[E]{} l.literalExpressionImpl = *literal(pq.Array(values)) l.arrayInterfaceImpl.parent = &l diff --git a/internal/jet/string_expression.go b/internal/jet/string_expression.go index f61c6894..3ec28409 100644 --- a/internal/jet/string_expression.go +++ b/internal/jet/string_expression.go @@ -16,8 +16,8 @@ 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 + ANY_EQ(rhs Array[StringExpression]) BoolExpression + ALL_EQ(rhs Array[StringExpression]) BoolExpression CONCAT(rhs Expression) StringExpression @@ -72,11 +72,11 @@ 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 { +func (i *stringInterfaceImpl) ANY_EQ(rhs Array[StringExpression]) BoolExpression { return Any(i.parent, Eq, rhs) } -func (i *stringInterfaceImpl) ALL_EQ(rhs ArrayExpression[StringExpression]) BoolExpression { +func (i *stringInterfaceImpl) ALL_EQ(rhs Array[StringExpression]) BoolExpression { return All(i.parent, Eq, rhs) } diff --git a/postgres/expressions.go b/postgres/expressions.go index 6c593a8e..8b780377 100644 --- a/postgres/expressions.go +++ b/postgres/expressions.go @@ -10,13 +10,13 @@ type Expression = jet.Expression type BoolExpression = jet.BoolExpression // BoolArrayExpression interface -type BoolArrayExpression = jet.ArrayExpression[BoolExpression] +type BoolArrayExpression = jet.Array[BoolExpression] // StringExpression interface type StringExpression = jet.StringExpression // StringArrayExpression interface -type StringArrayExpression = jet.ArrayExpression[StringExpression] +type StringArrayExpression = jet.Array[StringExpression] // NumericExpression interface type NumericExpression = jet.NumericExpression @@ -25,7 +25,7 @@ type NumericExpression = jet.NumericExpression type IntegerExpression = jet.IntegerExpression // IntegerArrayExpression interface -type IntegerArrayExpression = jet.ArrayExpression[IntegerExpression] +type IntegerArrayExpression = jet.Array[IntegerExpression] // FloatExpression is interface type FloatExpression = jet.FloatExpression From 7a89822d3e973131e752678672ff57b2e1519c1b Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Sat, 7 Sep 2024 20:10:02 +0200 Subject: [PATCH 15/34] AT now returns it's elements generic type --- internal/jet/array_expression.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/internal/jet/array_expression.go b/internal/jet/array_expression.go index 0399b5ab..dcdc5cee 100644 --- a/internal/jet/array_expression.go +++ b/internal/jet/array_expression.go @@ -17,7 +17,7 @@ type Array[E Expression] interface { CONCAT(rhs Array[E]) Array[E] CONCAT_ELEMENT(E) Array[E] - AT(expression IntegerExpression) Expression + AT(expression IntegerExpression) E } type arrayInterfaceImpl[E Expression] struct { @@ -70,8 +70,22 @@ func (a arrayInterfaceImpl[E]) CONCAT_ELEMENT(rhs E) Array[E] { return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||")) } -func (a arrayInterfaceImpl[E]) AT(expression IntegerExpression) Expression { - return arraySubscriptExpr(a.parent, expression) +func (a arrayInterfaceImpl[E]) AT(expression IntegerExpression) E { + return arrayElementTypeCaster[E](a.parent, arraySubscriptExpr(a.parent, expression)) +} + +func arrayElementTypeCaster[E Expression](arrayExp Array[E], exp Expression) E { + var i Expression + switch arrayExp.(type) { + case Array[StringExpression]: + i = StringExp(exp) + case Array[IntegerExpression]: + i = IntExp(exp) + case Array[BoolExpression]: + i = BoolExp(exp) + } + + return i.(E) } type arrayExpressionWrapper[E Expression] struct { From f53d7df051e6a2b2ba464c4bd3a23cb77e6d21c2 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Sat, 7 Sep 2024 20:14:38 +0200 Subject: [PATCH 16/34] Use CustomExpression --- internal/jet/array_expression_test.go | 4 ++-- internal/jet/expression.go | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/internal/jet/array_expression_test.go b/internal/jet/array_expression_test.go index ed7b565b..508b5f96 100644 --- a/internal/jet/array_expression_test.go +++ b/internal/jet/array_expression_test.go @@ -50,10 +50,10 @@ func TestArrayExpressionCONCAT(t *testing.T) { } 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(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)) + assertClauseSerialize(t, table1ColStringArray.AT(Int(1)), "table1.col_array_string[$1]", int64(1)) } diff --git a/internal/jet/expression.go b/internal/jet/expression.go index 28d12dec..678b36ce 100644 --- a/internal/jet/expression.go +++ b/internal/jet/expression.go @@ -316,7 +316,6 @@ func (s *complexExpression) serialize(statement StatementType, out *SQLBuilder, } } - type arraySubscriptExpression struct { ExpressionInterfaceImpl array Expression @@ -337,10 +336,7 @@ func (a arraySubscriptExpression) serialize(statement StatementType, out *SQLBui } func arraySubscriptExpr(array Expression, subscript IntegerExpression) Expression { - arraySubscriptExpression := &arraySubscriptExpression{array: array, subscript: subscript} - arraySubscriptExpression.ExpressionInterfaceImpl.Parent = arraySubscriptExpression - - return arraySubscriptExpression + return CustomExpression(array, Token("["), subscript, Token("]")) } func wrap(expressions ...Expression) Expression { From c1e35c401f9a25a09d5058f1e78dbedc53885c29 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Sat, 7 Sep 2024 20:16:16 +0200 Subject: [PATCH 17/34] Removed old cruft --- internal/jet/literal_expression.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/internal/jet/literal_expression.go b/internal/jet/literal_expression.go index 94782975..8588db87 100644 --- a/internal/jet/literal_expression.go +++ b/internal/jet/literal_expression.go @@ -208,19 +208,6 @@ func StringArray(values []string) Array[StringExpression] { return &l } -type unsafeArrayLiteral[E Expression] struct { - arrayInterfaceImpl[E] - literalExpressionImpl -} - -func UnsafeArray[E LiteralExpression](values []interface{}) Array[E] { - l := unsafeArrayLiteral[E]{} - l.literalExpressionImpl = *literal(pq.Array(values)) - l.arrayInterfaceImpl.parent = &l - - return &l -} - // ---------------------------------------------------// type stringLiteral struct { stringInterfaceImpl From cfd8a7942acdfd53ff094483281177ed4f0f00a6 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Sat, 7 Sep 2024 20:21:23 +0200 Subject: [PATCH 18/34] Cleanup literal type construction --- internal/jet/literal_expression.go | 37 ++++-------------------------- 1 file changed, 4 insertions(+), 33 deletions(-) diff --git a/internal/jet/literal_expression.go b/internal/jet/literal_expression.go index 8588db87..b8238c62 100644 --- a/internal/jet/literal_expression.go +++ b/internal/jet/literal_expression.go @@ -163,49 +163,20 @@ func Decimal(value string) FloatExpression { // ---------------------------------------------------// -type boolArrayLiteral struct { - arrayInterfaceImpl[BoolExpression] - literalExpressionImpl -} - func BoolArray(values []bool) Array[BoolExpression] { - l := boolArrayLiteral{} - l.literalExpressionImpl = *literal(pq.BoolArray(values)) - l.arrayInterfaceImpl.parent = &l - - return &l -} - -type integerArrayLiteral struct { - arrayInterfaceImpl[IntegerExpression] - literalExpressionImpl + return ArrayExp[BoolExpression](literal(pq.BoolArray(values))) } func Int64Array(values []int64) Array[IntegerExpression] { - l := integerArrayLiteral{} - l.literalExpressionImpl = *literal(pq.Int64Array(values)) - l.arrayInterfaceImpl.parent = &l - return &l + return ArrayExp[IntegerExpression](literal(pq.Int64Array(values))) } func Int32Array(values []int32) Array[IntegerExpression] { - l := integerArrayLiteral{} - l.literalExpressionImpl = *literal(pq.Int32Array(values)) - l.arrayInterfaceImpl.parent = &l - return &l -} - -type stringArrayLiteral struct { - arrayInterfaceImpl[StringExpression] - literalExpressionImpl + return ArrayExp[IntegerExpression](literal(pq.Int32Array(values))) } func StringArray(values []string) Array[StringExpression] { - l := stringArrayLiteral{} - l.literalExpressionImpl = *literal(pq.StringArray(values)) - l.arrayInterfaceImpl.parent = &l - - return &l + return ArrayExp[StringExpression](literal(pq.StringArray(values))) } // ---------------------------------------------------// From fc111159926ccf51f0f35a72b21ce098ec44afbc Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Sat, 7 Sep 2024 20:27:27 +0200 Subject: [PATCH 19/34] Use PQ creating a driver value for arrays --- internal/jet/sql_builder.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/internal/jet/sql_builder.go b/internal/jet/sql_builder.go index 288335d6..dce93968 100644 --- a/internal/jet/sql_builder.go +++ b/internal/jet/sql_builder.go @@ -7,6 +7,7 @@ import ( "github.com/go-jet/jet/v2/internal/3rdparty/pq" "github.com/go-jet/jet/v2/internal/utils/is" "github.com/google/uuid" + pq2 "github.com/lib/pq" "reflect" "sort" "strconv" @@ -256,16 +257,10 @@ 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() + // We'll rely on the internals of pq2.StringArray here. We know it will never return an error, and the returned + // value is a string + dv, _ := pq2.StringArray(val).Value() + return dv.(string) } func integerTypesToString(value interface{}) string { From 533602b6aeeae3950c522d786e17737204c65903 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Mon, 9 Sep 2024 08:52:50 +0200 Subject: [PATCH 20/34] Implemented Any/All as standalone functions --- internal/jet/array_expression.go | 14 -------------- internal/jet/func_expression.go | 26 ++++++++++++++++++++++++++ internal/jet/operators.go | 9 --------- internal/jet/string_expression.go | 11 ----------- internal/jet/string_expression_test.go | 4 ++-- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/internal/jet/array_expression.go b/internal/jet/array_expression.go index dcdc5cee..9ac562c6 100644 --- a/internal/jet/array_expression.go +++ b/internal/jet/array_expression.go @@ -74,20 +74,6 @@ func (a arrayInterfaceImpl[E]) AT(expression IntegerExpression) E { return arrayElementTypeCaster[E](a.parent, arraySubscriptExpr(a.parent, expression)) } -func arrayElementTypeCaster[E Expression](arrayExp Array[E], exp Expression) E { - var i Expression - switch arrayExp.(type) { - case Array[StringExpression]: - i = StringExp(exp) - case Array[IntegerExpression]: - i = IntExp(exp) - case Array[BoolExpression]: - i = BoolExp(exp) - } - - return i.(E) -} - type arrayExpressionWrapper[E Expression] struct { arrayInterfaceImpl[E] Expression diff --git a/internal/jet/func_expression.go b/internal/jet/func_expression.go index ddc579e4..bd3c5160 100644 --- a/internal/jet/func_expression.go +++ b/internal/jet/func_expression.go @@ -646,6 +646,32 @@ func LEAST(value Expression, values ...Expression) Expression { return NewFunc("LEAST", allValues, nil) } +// -------------------- Array Expressions Functions ------------------// + +// ANY should be used in combination with a boolean operator. The result of ANY is "true" if any true result is obtained +func ANY[E Expression](arr Array[E]) E { + return arrayElementTypeCaster(arr, Func("ANY", arr)) +} + +// ALL should be used in combination with a boolean operator. TThe result of ALL is “true” if all comparisons yield true +func ALL[E Expression](arr Array[E]) E { + return arrayElementTypeCaster(arr, Func("ALL", arr)) +} + +func arrayElementTypeCaster[E Expression](arrayExp Array[E], exp Expression) E { + var i Expression + switch arrayExp.(type) { + case Array[StringExpression]: + i = StringExp(exp) + case Array[IntegerExpression]: + i = IntExp(exp) + case Array[BoolExpression]: + i = BoolExp(exp) + } + + return i.(E) +} + //--------------------------------------------------------------------// type funcExpressionImpl struct { diff --git a/internal/jet/operators.go b/internal/jet/operators.go index 6ad46d42..46b36ec3 100644 --- a/internal/jet/operators.go +++ b/internal/jet/operators.go @@ -22,15 +22,6 @@ 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 diff --git a/internal/jet/string_expression.go b/internal/jet/string_expression.go index 3ec28409..29b24472 100644 --- a/internal/jet/string_expression.go +++ b/internal/jet/string_expression.go @@ -16,9 +16,6 @@ type StringExpression interface { BETWEEN(min, max StringExpression) BoolExpression NOT_BETWEEN(min, max StringExpression) BoolExpression - ANY_EQ(rhs Array[StringExpression]) BoolExpression - ALL_EQ(rhs Array[StringExpression]) BoolExpression - CONCAT(rhs Expression) StringExpression LIKE(pattern StringExpression) BoolExpression @@ -72,14 +69,6 @@ func (s *stringInterfaceImpl) NOT_BETWEEN(min, max StringExpression) BoolExpress return NewBetweenOperatorExpression(s.parent, min, max, true) } -func (i *stringInterfaceImpl) ANY_EQ(rhs Array[StringExpression]) BoolExpression { - return Any(i.parent, Eq, rhs) -} - -func (i *stringInterfaceImpl) ALL_EQ(rhs Array[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 830937f0..1ff918c8 100644 --- a/internal/jet/string_expression_test.go +++ b/internal/jet/string_expression_test.go @@ -77,11 +77,11 @@ func TestStringNOT_REGEXP_LIKE(t *testing.T) { } func TestStringANY_EQ(t *testing.T) { - assertClauseSerialize(t, table2ColStr.ANY_EQ(table1ColStringArray), "(table2.col_str = ANY(table1.col_array_string))") + assertClauseSerialize(t, table2ColStr.EQ(ANY(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))") + assertClauseSerialize(t, table2ColStr.EQ(ALL(table1ColStringArray)), "(table2.col_str = ALL(table1.col_array_string))") } func TestStringExp(t *testing.T) { From 65c53c7ddc638dfed86a0a0d9be925efbcb3a27a Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Fri, 20 Sep 2024 22:23:14 +0200 Subject: [PATCH 21/34] Use proper types --- postgres/columns.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/postgres/columns.go b/postgres/columns.go index bd158a64..d4a8b1a0 100644 --- a/postgres/columns.go +++ b/postgres/columns.go @@ -102,22 +102,22 @@ type ColumnInt8Range jet.ColumnRange[jet.Int8Expression] var Int8RangeColumn = jet.RangeColumn[jet.Int8Expression] // ColumnStringArray is interface of column -type ColumnStringArray jet.ColumnArray[jet.StringExpression] +type ColumnStringArray jet.ColumnArray[StringExpression] // StringArrayColumn creates named string array column -var StringArrayColumn = jet.ArrayColumn[jet.StringExpression] +var StringArrayColumn = jet.ArrayColumn[StringExpression] // ColumnIntegerArray is interface of column -type ColumnIntegerArray jet.ColumnArray[jet.IntegerExpression] +type ColumnIntegerArray jet.ColumnArray[IntegerExpression] // IntegerArrayColumn creates named integer array column -var IntegerArrayColumn = jet.ArrayColumn[jet.IntegerExpression] +var IntegerArrayColumn = jet.ArrayColumn[IntegerExpression] // ColumnBoolArray is interface of column -type ColumnBoolArray jet.ColumnArray[jet.BoolExpression] +type ColumnBoolArray jet.ColumnArray[BoolExpression] // BoolArrayColumn creates named bool array column -var BoolArrayColumn = jet.ArrayColumn[jet.BoolExpression] +var BoolArrayColumn = jet.ArrayColumn[BoolExpression] //------------------------------------------------------// From fa60ca11da0585ff180b688075050d9be72b8589 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Fri, 20 Sep 2024 22:23:28 +0200 Subject: [PATCH 22/34] Add boolean with status --- generator/template/sql_builder_template.go | 60 ++--- tests/postgres/array_test.go | 277 +++++++++++++++++++++ 2 files changed, 309 insertions(+), 28 deletions(-) create mode 100644 tests/postgres/array_test.go diff --git a/generator/template/sql_builder_template.go b/generator/template/sql_builder_template.go index 5f0fb808..a4bdf5c3 100644 --- a/generator/template/sql_builder_template.go +++ b/generator/template/sql_builder_template.go @@ -165,6 +165,7 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { columnName := columnMetaData.Name var columnType string + var supported bool if columnMetaData.DataType.Kind == metadata.ArrayType { if columnMetaData.DataType.Dimensions > 1 { @@ -172,13 +173,13 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { return "String" } - columnType = sqlArrayToColumnType(strings.TrimSuffix(typeName, "[]")) + columnType, supported = sqlArrayToColumnType(strings.TrimSuffix(typeName, "[]")) } else { - columnType = sqlToColumnType(typeName) + columnType, supported = sqlToColumnType(typeName) } - if columnType == "" { - fmt.Println("- [SQL Builder] Unsupported sql column '" + columnName + " " + typeName + "', using StringColumn instead.") + if !supported { + fmt.Printf("- [SQL Builder] Unsupported SQL column '" + columnName + " " + typeName + "', using StringColumn instead.\n") return "String" } @@ -186,67 +187,70 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string { } // sqlArrayToColumnType maps the type of an SQL array column type to a go jet sql builder column. Note that you don't -// pass the brackets `[]`, signifying an SQL array type, into this function -func sqlArrayToColumnType(typeName string) string { +// pass the brackets `[]`, signifying an SQL array type, into this function. The second return value returns whether the +// given type is supported +func sqlArrayToColumnType(typeName string) (string, bool) { switch strings.ToLower(typeName) { case "user-defined", "enum", "text", "character", "character varying", "bytea", "uuid", "tsvector", "bit", "bit varying", "money", "json", "jsonb", "xml", "point", "line", "ARRAY", "char", "varchar", "nvarchar", "binary", "varbinary", "bpchar", "varbit", "tinyblob", "blob", "mediumblob", "longblob", "tinytext", "mediumtext", "longtext": // MySQL - return "StringArray" + return "StringArray", true case "smallint", "integer", "bigint", "int2", "int4", "int8", "tinyint", "mediumint", "int", "year": //MySQL - return "IntegerArray" + return "IntegerArray", true case "boolean", "bool": - return "BoolArray" + return "BoolArray", true default: - return "" + return "", false } } -func sqlToColumnType(typeName string) string { +// sqlToColumnType maps the type of a SQL column type to a go jet sql builder column. The second return value returns +// whether the given type is supported. +func sqlToColumnType(typeName string) (string, bool) { switch strings.ToLower(typeName) { case "boolean", "bool": - return "Bool" + return "Bool", true case "smallint", "integer", "bigint", "int2", "int4", "int8", "tinyint", "mediumint", "int", "year": //MySQL - return "Integer" + return "Integer", true case "date": - return "Date" + return "Date", true case "timestamp without time zone", "timestamp", "datetime": //MySQL: - return "Timestamp" + return "Timestamp", true case "timestamp with time zone", "timestamptz": - return "Timestampz" + return "Timestampz", true case "time without time zone", "time": //MySQL - return "Time" + return "Time", true case "time with time zone", "timetz": - return "Timez" + return "Timez", true case "interval": - return "Interval" + return "Interval", true case "user-defined", "enum", "text", "character", "character varying", "bytea", "uuid", "tsvector", "bit", "bit varying", "money", "json", "jsonb", "xml", "point", "line", "ARRAY", "char", "varchar", "nvarchar", "binary", "varbinary", "bpchar", "varbit", "tinyblob", "blob", "mediumblob", "longblob", "tinytext", "mediumtext", "longtext": // MySQL - return "String" + return "String", true case "real", "numeric", "decimal", "double precision", "float", "float4", "float8", "double": // MySQL - return "Float" + return "Float", true case "daterange": - return "DateRange" + return "DateRange", true case "tsrange": - return "TimestampRange" + return "TimestampRange", true case "tstzrange": - return "TimestampzRange" + return "TimestampzRange", true case "int4range": - return "Int4Range" + return "Int4Range", true case "int8range": - return "Int8Range" + return "Int8Range", true case "numrange": - return "NumericRange" + return "NumericRange", true default: - return "" + return "", false } } diff --git a/tests/postgres/array_test.go b/tests/postgres/array_test.go new file mode 100644 index 00000000..17f95891 --- /dev/null +++ b/tests/postgres/array_test.go @@ -0,0 +1,277 @@ +package postgres + +import ( + "database/sql" + "github.com/go-jet/jet/v2/internal/testutils" + . "github.com/go-jet/jet/v2/postgres" + "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/model" + . "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/table" + "github.com/google/go-cmp/cmp" + "github.com/lib/pq" + "github.com/stretchr/testify/require" + "math/big" + "testing" +) + +func TestArrayTableSelect(t *testing.T) { + skipForCockroachDB(t) + + textArray := StringArray([]string{"a"}) + boolArray := BoolArray([]bool{true}) + int4Array := Int32Array([]int32{1, 2}) + int8Array := Int64Array([]int64{10, 11}) + + query := SELECT( + SampleArrays.AllColumns, + SampleArrays.TextArray.EQ(SampleArrays.TextArray).AS("sample.text_eq"), + SampleArrays.BoolArray.EQ(boolArray).AS("sample.bool_eq"), + SampleArrays.TextArray.NOT_EQ(textArray).AS("sample.text_neq"), + SampleArrays.Int4Array.LT(int4Array).IS_TRUE().AS("sample.int4_lt"), + SampleArrays.Int8Array.LT_EQ(int8Array).IS_FALSE().AS("sample.int8_lteq"), + SampleArrays.TextArray.GT(textArray).AS("sample.text_gt"), + SampleArrays.Int4Array.GT_EQ(int4Array).AS("sample.bool_gteq"), + Int32(22).EQ(ANY[IntegerExpression](SampleArrays.Int4Array)).AS("sample.int4_eq_any"), + Int32(22).NOT_EQ(ANY[IntegerExpression](SampleArrays.Int4Array)).AS("sample.int4_neq_any"), + Int32(22).EQ(ALL[IntegerExpression](SampleArrays.Int4Array)).AS("sample.int4_eq_all"), + SampleArrays.Int8Array.CONTAINS(Int64Array([]int64{75364})).AS("sample.int8cont"), + SampleArrays.Int8Array.IS_CONTAINED_BY(Int64Array([]int64{75364})).AS("sample.int8cont_by"), + SampleArrays.Int4Array.OVERLAP(int4Array).AS("sample.int4_overlap"), + SampleArrays.BoolArray.CONCAT(boolArray).AS("sample.bool_concat"), + SampleArrays.TextArray.CONCAT_ELEMENT(String("z")).AS("sample.text_concat_el"), + SampleArrays.TextArray.AT(Int32(1)).AS("sample.text_at"), + ARRAY_APPEND[StringExpression](SampleArrays.TextArray, String("after")).AS("sample.text_append"), + ARRAY_CAT[StringExpression](SampleArrays.TextArray, textArray).AS("sample.text_cat"), + ARRAY_LENGTH[StringExpression](SampleArrays.TextArray, Int32(1)).AS("sample.text_length"), + ARRAY_PREPEND[StringExpression](String("before"), SampleArrays.TextArray).AS("sample.text_prepend"), + ).FROM( + SampleArrays, + ).WHERE( + SampleArrays.BoolArray.CONTAINS(BoolArray([]bool{true})), + ) + + testutils.AssertStatementSql(t, query, ` +SELECT sample_arrays.text_array AS "sample_arrays.text_array", + sample_arrays.bool_array AS "sample_arrays.bool_array", + sample_arrays.int4_array AS "sample_arrays.int4_array", + sample_arrays.int8_array AS "sample_arrays.int8_array", + (sample_arrays.text_array = sample_arrays.text_array) AS "sample.text_eq", + (sample_arrays.bool_array = $1) AS "sample.bool_eq", + (sample_arrays.text_array != $2) AS "sample.text_neq", + (sample_arrays.int4_array < $3) IS TRUE AS "sample.int4_lt", + (sample_arrays.int8_array <= $4) IS FALSE AS "sample.int8_lteq", + (sample_arrays.text_array > $5) AS "sample.text_gt", + (sample_arrays.int4_array >= $6) AS "sample.bool_gteq", + ($7::integer = ANY(sample_arrays.int4_array)) AS "sample.int4_eq_any", + ($8::integer != ANY(sample_arrays.int4_array)) AS "sample.int4_neq_any", + ($9::integer = ALL(sample_arrays.int4_array)) AS "sample.int4_eq_all", + (sample_arrays.int8_array @> $10) AS "sample.int8cont", + (sample_arrays.int8_array <@ $11) AS "sample.int8cont_by", + (sample_arrays.int4_array && $12) AS "sample.int4_overlap", + (sample_arrays.bool_array || $13) AS "sample.bool_concat", + (sample_arrays.text_array || $14::text) AS "sample.text_concat_el", + sample_arrays.text_array[$15::integer] AS "sample.text_at", + array_append(sample_arrays.text_array, $16::text) AS "sample.text_append", + array_cat(sample_arrays.text_array, $17) AS "sample.text_cat", + array_length(sample_arrays.text_array, $18::integer) AS "sample.text_length", + array_prepend($19::text, sample_arrays.text_array) AS "sample.text_prepend" +FROM test_sample.sample_arrays +WHERE sample_arrays.bool_array @> $20; +`) + + type sample struct { + model.SampleArrays + TextEq bool + BoolEq bool + TextNeq bool + Int4Lt bool + Int8Lteq bool + TextGt bool + BoolGteq bool + Int4EqAny bool + Int4NeqAny bool + Int4EqAll bool + Int8Cont bool + Int8ContBy bool + Int4Overlap bool + BoolConcat pq.BoolArray + TextConcatEl pq.StringArray + TextAt string + TextAppend pq.StringArray + TextCat pq.StringArray + TextLength int32 + TextPrepend pq.StringArray + } + + var dest sample + err := query.Query(db, &dest) + require.NoError(t, err) + + expectedRow := sample{ + SampleArrays: sampleArrayRow, + TextEq: true, + BoolEq: true, + TextNeq: true, + Int4Lt: false, + Int8Lteq: true, + TextGt: true, + BoolGteq: true, + Int4EqAny: false, + Int4NeqAny: true, + Int4EqAll: false, + Int8Cont: false, + Int8ContBy: false, + Int4Overlap: true, + BoolConcat: pq.BoolArray{true, true}, + TextConcatEl: pq.StringArray{"a", "b", "z"}, + TextAt: "a", + TextAppend: pq.StringArray{"a", "b", "after"}, + TextCat: pq.StringArray{"a", "b", "a"}, + TextLength: 2, + TextPrepend: pq.StringArray{"before", "a", "b"}, + } + + testutils.AssertDeepEqual(t, dest, expectedRow, cmp.AllowUnexported(big.Int{})) + requireLogged(t, query) +} + +func TestArraySelectColumnsFromSubQuery(t *testing.T) { + skipForCockroachDB(t) + + subQuery := SELECT( + SampleArrays.AllColumns, + SampleArrays.Int4Array.AS("array4"), + ).FROM( + SampleArrays, + ).AsTable("sub_query") + + int4Array := IntegerArrayColumn("array4").From(subQuery) + + stmt := SELECT( + subQuery.AllColumns(), + int4Array, + ).FROM( + subQuery, + ) + + testutils.AssertDebugStatementSql(t, stmt, ` +SELECT sub_query."sample_arrays.text_array" AS "sample_arrays.text_array", + sub_query."sample_arrays.bool_array" AS "sample_arrays.bool_array", + sub_query."sample_arrays.int4_array" AS "sample_arrays.int4_array", + sub_query."sample_arrays.int8_array" AS "sample_arrays.int8_array", + sub_query.array4 AS "array4", + sub_query.array4 AS "array4" +FROM ( + SELECT sample_arrays.text_array AS "sample_arrays.text_array", + sample_arrays.bool_array AS "sample_arrays.bool_array", + sample_arrays.int4_array AS "sample_arrays.int4_array", + sample_arrays.int8_array AS "sample_arrays.int8_array", + sample_arrays.int4_array AS "array4" + FROM test_sample.sample_arrays + ) AS sub_query; +`) + + var dest struct { + model.SampleArrays + Array4 pq.Int32Array + } + + err := stmt.Query(db, &dest) + + require.NoError(t, err) + testutils.AssertDeepEqual(t, dest.SampleArrays.Int4Array, sampleArrayRow.Int4Array) + testutils.AssertDeepEqual(t, dest.SampleArrays.Int8Array, sampleArrayRow.Int8Array) + testutils.AssertDeepEqual(t, dest.Array4, sampleArrayRow.Int4Array) +} + +func TestArrayTable_InsertColumn(t *testing.T) { + skipForCockroachDB(t) + + insertQuery := SampleArrays.INSERT(SampleArrays.AllColumns). + VALUES( + ARRAY(String("A"), String("B")), + ARRAY(Bool(true)), + ARRAY(Int32(1)), + ARRAY(Int64(2)), + ). + MODEL( + sampleArrayRow, + ). + RETURNING(SampleArrays.AllColumns) + + expectedQuery := ` +INSERT INTO test_sample.sample_arrays (text_array, bool_array, int4_array, int8_array) +VALUES (ARRAY['A'::text,'B'::text], ARRAY[TRUE::boolean], ARRAY[1::integer], ARRAY[2::bigint]), + ('{"a","b"}', '{t}', '{1,2,3}', '{10,11,12}') +RETURNING sample_arrays.text_array AS "sample_arrays.text_array", + sample_arrays.bool_array AS "sample_arrays.bool_array", + sample_arrays.int4_array AS "sample_arrays.int4_array", + sample_arrays.int8_array AS "sample_arrays.int8_array"; +` + testutils.AssertDebugStatementSql(t, insertQuery, expectedQuery) + + testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) { + var dest []model.SampleArrays + err := insertQuery.Query(tx, &dest) + require.NoError(t, err) + require.Len(t, dest, 2) + testutils.AssertDeepEqual(t, sampleArrayRow, dest[1], cmp.AllowUnexported(big.Int{})) + }) +} + +func TestArrayTableUpdate(t *testing.T) { + skipForCockroachDB(t) + + t.Run("using model", func(t *testing.T) { + stmt := SampleArrays.UPDATE(SampleArrays.AllColumns). + MODEL(sampleArrayRow). + WHERE(String("a").EQ(ANY[StringExpression](SampleArrays.TextArray))). + RETURNING(SampleArrays.AllColumns) + + testutils.AssertStatementSql(t, stmt, ` +UPDATE test_sample.sample_arrays +SET (text_array, bool_array, int4_array, int8_array) = ($1, $2, $3, $4) +WHERE $5::text = ANY(sample_arrays.text_array) +RETURNING sample_arrays.text_array AS "sample_arrays.text_array", + sample_arrays.bool_array AS "sample_arrays.bool_array", + sample_arrays.int4_array AS "sample_arrays.int4_array", + sample_arrays.int8_array AS "sample_arrays.int8_array"; +`) + + testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) { + var dest []model.SampleArrays + + err := stmt.Query(tx, &dest) + require.NoError(t, err) + require.Len(t, dest, 1) + testutils.AssertDeepEqual(t, sampleArrayRow, dest[0], cmp.AllowUnexported(big.Int{})) + }) + }) + + t.Run("update using SET", func(t *testing.T) { + stmt := SampleArrays.UPDATE(). + SET( + SampleArrays.Int4Array.SET(ARRAY(Int32(-10), Int32(11))), + SampleArrays.Int8Array.SET(ARRAY(Int64(-1200), Int64(7800))), + ). + WHERE(String("a").EQ(ANY[StringExpression](SampleArrays.TextArray))) + + testutils.AssertDebugStatementSql(t, stmt, ` +UPDATE test_sample.sample_arrays +SET int4_array = ARRAY[-10::integer,11::integer], + int8_array = ARRAY[-1200::bigint,7800::bigint] +WHERE 'a'::text = ANY(sample_arrays.text_array); +`) + + testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) { + testutils.AssertExec(t, stmt, tx, 1) + }) + }) + +} + +var sampleArrayRow = model.SampleArrays{ + TextArray: pq.StringArray([]string{"a", "b"}), + BoolArray: pq.BoolArray([]bool{true}), + Int4Array: pq.Int32Array([]int32{1, 2, 3}), + Int8Array: pq.Int64Array([]int64{10, 11, 12}), +} From 9702631176e11532ac67fe6448f2c62b0665f905 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Fri, 20 Sep 2024 22:26:51 +0200 Subject: [PATCH 23/34] Add array functions --- internal/jet/string_expression_test.go | 4 +-- postgres/functions.go | 37 ++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/internal/jet/string_expression_test.go b/internal/jet/string_expression_test.go index 1ff918c8..19837d33 100644 --- a/internal/jet/string_expression_test.go +++ b/internal/jet/string_expression_test.go @@ -77,11 +77,11 @@ func TestStringNOT_REGEXP_LIKE(t *testing.T) { } func TestStringANY_EQ(t *testing.T) { - assertClauseSerialize(t, table2ColStr.EQ(ANY(table1ColStringArray)), "(table2.col_str = ANY(table1.col_array_string))") + assertClauseSerialize(t, table2ColStr.EQ(ANY[StringExpression](table1ColStringArray)), "(table2.col_str = ANY(table1.col_array_string))") } func TestStringALL_EQ(t *testing.T) { - assertClauseSerialize(t, table2ColStr.EQ(ALL(table1ColStringArray)), "(table2.col_str = ALL(table1.col_array_string))") + assertClauseSerialize(t, table2ColStr.EQ(ALL[StringExpression](table1ColStringArray)), "(table2.col_str = ALL(table1.col_array_string))") } func TestStringExp(t *testing.T) { diff --git a/postgres/functions.go b/postgres/functions.go index bce2e987..aa3b1d2d 100644 --- a/postgres/functions.go +++ b/postgres/functions.go @@ -267,7 +267,7 @@ var TO_ASCII = jet.TO_ASCII // TO_HEX converts number to its equivalent hexadecimal representation var TO_HEX = jet.TO_HEX -//----------Data Type Formatting Functions ----------------------// +//---------- Range Functions ----------------------// // LOWER_BOUND returns range expressions lower bound func LOWER_BOUND[T Expression](expression jet.Range[T]) T { @@ -279,7 +279,40 @@ func UPPER_BOUND[T Expression](expression jet.Range[T]) T { return jet.UPPER_BOUND[T](expression) } -//----------Data Type Formatting Functions ----------------------// +// ---------- Array Functions ----------------------// + +// ANY should be used in combination with a boolean operator. The result of ANY is "true" if any true result is obtained +func ANY[T Expression](expression jet.Array[T]) T { + return jet.ANY[T](expression) +} + +// ALL should be used in combination with a boolean operator. TThe result of ALL is “true” if all comparisons yield true +func ALL[T Expression](expression jet.Array[T]) T { + return jet.ALL[T](expression) +} + +func ARRAY_APPEND[T Expression](arr jet.Array[T], el T) jet.Array[T] { + return jet.ARRAY_APPEND(arr, el) +} + +func ARRAY_CAT[T Expression](arr1, arr2 jet.Array[T]) jet.Array[T] { + return jet.ARRAY_CAT(arr1, arr2) +} + +func ARRAY_LENGTH[T Expression](expression jet.Array[T], dim IntegerExpression) IntegerExpression { + return jet.ARRAY_LENGTH(expression, dim) +} + +func ARRAY_PREPEND[T Expression](el T, arr jet.Array[T]) jet.Array[T] { + return jet.ARRAY_PREPEND(el, arr) +} + +// ARRAY constructor +func ARRAY[T Expression](elems ...T) jet.Array[T] { + return jet.ARRAY[T](elems...) +} + +//---------- Data Type Formatting Functions ----------------------// // TO_CHAR converts expression to string with format var TO_CHAR = jet.TO_CHAR From aa7ce8d6e7d91e102cd3e4f11850c3a2f3f9137d Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Fri, 20 Sep 2024 22:27:00 +0200 Subject: [PATCH 24/34] Two new literal types --- postgres/literal.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/postgres/literal.go b/postgres/literal.go index 80392e62..951e3f06 100644 --- a/postgres/literal.go +++ b/postgres/literal.go @@ -34,11 +34,21 @@ func Int32(value int32) IntegerExpression { return CAST(jet.Int32(value)).AS_INTEGER() } +// Int32Array creates new 32 bit signed integer literal expression +func Int32Array(elements []int32) IntegerArrayExpression { + return jet.Int32Array(elements) +} + // Int64 is constructor for 64 bit signed integer expressions literals. func Int64(value int64) IntegerExpression { return CAST(jet.Int(value)).AS_BIGINT() } +// Int64Array creates new 64 bit signed integer literal expression +func Int64Array(elements []int64) IntegerArrayExpression { + return jet.Int64Array(elements) +} + // Uint8 is constructor for 8 bit unsigned integer expressions literals. func Uint8(value uint8) IntegerExpression { return CAST(jet.Uint8(value)).AS_SMALLINT() From bf44a3cbdd21385fe6c2a6112dbe7bc0b5e44707 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Fri, 20 Sep 2024 22:28:05 +0200 Subject: [PATCH 25/34] Supporting more types --- generator/template/model_template.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/generator/template/model_template.go b/generator/template/model_template.go index 9d1cae7a..ffce3189 100644 --- a/generator/template/model_template.go +++ b/generator/template/model_template.go @@ -341,12 +341,14 @@ func toGoType(column metadata.Column) interface{} { return pgtype.Int8range{} case "numrange": return pgtype.Numrange{} - case "bool[]": + case "bool[]", "boolean[]": return pq.BoolArray{} case "integer[]", "int4[]": return pq.Int32Array{} - case "bigint[]": + case "bigint[]", "int8[]": return pq.Int64Array{} + case "bytea[]": + return pq.ByteaArray{} case "text[]", "jsonb[]", "json[]": return pq.StringArray{} default: From 6efbf4262c9c4aab69586eca624474b41e2a6249 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Fri, 20 Sep 2024 22:28:14 +0200 Subject: [PATCH 26/34] Adding array functions --- internal/jet/func_expression.go | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/internal/jet/func_expression.go b/internal/jet/func_expression.go index bd3c5160..a095a993 100644 --- a/internal/jet/func_expression.go +++ b/internal/jet/func_expression.go @@ -658,6 +658,39 @@ func ALL[E Expression](arr Array[E]) E { return arrayElementTypeCaster(arr, Func("ALL", arr)) } +func ARRAY_APPEND[E Expression](arr Array[E], el E) Array[E] { + return arrayTypeCaster[E](arr, Func("array_append", arr, el)) +} + +func ARRAY_CAT[E Expression](arr1, arr2 Array[E]) Array[E] { + return arrayTypeCaster[E](arr1, Func("array_cat", arr1, arr2)) +} + +func ARRAY_PREPEND[E Expression](el E, arr Array[E]) Array[E] { + return ArrayExp[E](Func("array_prepend", el, arr)) +} + +func ARRAY_LENGTH[E Expression](arr Array[E], el IntegerExpression) IntegerExpression { + return IntExp(Func("array_length", arr, el)) +} + +func arrayTypeCaster[E Expression](arrayExp Expression, exp Expression) Array[E] { + var i Expression + switch arrayExp.(type) { + case Array[StringExpression]: + i = ArrayExp[StringExpression](exp) + case Array[Int4Expression]: + i = ArrayExp[Int4Expression](exp) + case Array[Int8Expression]: + i = ArrayExp[Int8Expression](exp) + case Array[IntegerExpression]: + i = ArrayExp[IntegerExpression](exp) + case Array[BoolExpression]: + i = ArrayExp[BoolExpression](exp) + } + return i.(Array[E]) +} + func arrayElementTypeCaster[E Expression](arrayExp Array[E], exp Expression) E { var i Expression switch arrayExp.(type) { @@ -672,6 +705,17 @@ func arrayElementTypeCaster[E Expression](arrayExp Array[E], exp Expression) E { return i.(E) } +func ARRAY[E Expression](elems ...E) Array[E] { + var args = make([]Serializer, len(elems)) + for i, each := range elems { + args[i] = each + } + return ArrayExp[E](CustomExpression(Token("ARRAY["), ListSerializer{ + Serializers: args, + Separator: ",", + }, Token("]"))) +} + //--------------------------------------------------------------------// type funcExpressionImpl struct { From 5c7e49232b5f89002a23beb61a87f271d1d98d96 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Fri, 20 Sep 2024 22:28:26 +0200 Subject: [PATCH 27/34] Fixing testcases --- tests/postgres/generator_test.go | 4 ++-- tests/postgres/scan_test.go | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/postgres/generator_test.go b/tests/postgres/generator_test.go index df3f3a0c..4fdc4a31 100644 --- a/tests/postgres/generator_test.go +++ b/tests/postgres/generator_test.go @@ -757,13 +757,13 @@ func TestGeneratedAllTypesSQLBuilderFiles(t *testing.T) { testutils.AssertFileNamesEqual(t, modelDir, "all_types.go", "all_types_view.go", "employee.go", "link.go", "mood.go", "person.go", "person_phone.go", "weird_names_table.go", "level.go", "user.go", "floats.go", "people.go", - "components.go", "vulnerabilities.go", "all_types_materialized_view.go", "sample_ranges.go") + "components.go", "vulnerabilities.go", "all_types_materialized_view.go", "sample_ranges.go", "sample_arrays.go") testutils.AssertFileContent(t, modelDir+"/all_types.go", allTypesModelContent) testutils.AssertFileContent(t, modelDir+"/link.go", linkModelContent) testutils.AssertFileNamesEqual(t, tableDir, "all_types.go", "employee.go", "link.go", "person.go", "person_phone.go", "weird_names_table.go", "user.go", "floats.go", "people.go", "table_use_schema.go", - "components.go", "vulnerabilities.go", "sample_ranges.go") + "components.go", "vulnerabilities.go", "sample_ranges.go", "sample_arrays.go") testutils.AssertFileContent(t, tableDir+"/all_types.go", allTypesTableContent) testutils.AssertFileContent(t, tableDir+"/sample_ranges.go", sampleRangeTableContent) diff --git a/tests/postgres/scan_test.go b/tests/postgres/scan_test.go index 931a3504..594070f4 100644 --- a/tests/postgres/scan_test.go +++ b/tests/postgres/scan_test.go @@ -955,7 +955,6 @@ 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 { @@ -970,13 +969,13 @@ func TestScanIntoCustomBaseTypes(t *testing.T) { ReplacementCost MyFloat64 Rating *model.MpaaRating LastUpdate MyTime - SpecialFeatures MyStringArray + SpecialFeatures pq.StringArray Fulltext MyString } // We'll skip special features, because it's a slice and it does not implement sql.Scanner stmt := SELECT( - Film.AllColumns.Except(Film.SpecialFeatures), + Film.AllColumns, ).FROM( Film, ).ORDER_BY( From d7f14fcd960d7c767739001b96621f23ecb22d4c Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Thu, 13 Feb 2025 10:43:40 +0100 Subject: [PATCH 28/34] Fix testcases --- tests/postgres/alltypes_test.go | 1 - tests/postgres/array_test.go | 9 ++++----- tests/postgres/select_test.go | 3 +-- tests/postgres/values_test.go | 8 ++++++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/postgres/alltypes_test.go b/tests/postgres/alltypes_test.go index c60cd192..48a2b814 100644 --- a/tests/postgres/alltypes_test.go +++ b/tests/postgres/alltypes_test.go @@ -5,7 +5,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/go-jet/jet/v2/qrm" - "database/sql" "github.com/lib/pq" "testing" "time" diff --git a/tests/postgres/array_test.go b/tests/postgres/array_test.go index 17f95891..fa6e88d0 100644 --- a/tests/postgres/array_test.go +++ b/tests/postgres/array_test.go @@ -1,9 +1,9 @@ package postgres import ( - "database/sql" "github.com/go-jet/jet/v2/internal/testutils" . "github.com/go-jet/jet/v2/postgres" + "github.com/go-jet/jet/v2/qrm" "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/model" . "github.com/go-jet/jet/v2/tests/.gentestdata/jetdb/test_sample/table" "github.com/google/go-cmp/cmp" @@ -209,7 +209,7 @@ RETURNING sample_arrays.text_array AS "sample_arrays.text_array", ` testutils.AssertDebugStatementSql(t, insertQuery, expectedQuery) - testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) { + testutils.ExecuteInTxAndRollback(t, db, func(tx qrm.DB) { var dest []model.SampleArrays err := insertQuery.Query(tx, &dest) require.NoError(t, err) @@ -237,9 +237,8 @@ RETURNING sample_arrays.text_array AS "sample_arrays.text_array", sample_arrays.int8_array AS "sample_arrays.int8_array"; `) - testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) { + testutils.ExecuteInTxAndRollback(t, db, func(tx qrm.DB) { var dest []model.SampleArrays - err := stmt.Query(tx, &dest) require.NoError(t, err) require.Len(t, dest, 1) @@ -262,7 +261,7 @@ SET int4_array = ARRAY[-10::integer,11::integer], WHERE 'a'::text = ANY(sample_arrays.text_array); `) - testutils.ExecuteInTxAndRollback(t, db, func(tx *sql.Tx) { + testutils.ExecuteInTxAndRollback(t, db, func(tx qrm.DB) { testutils.AssertExec(t, stmt, tx, 1) }) }) diff --git a/tests/postgres/select_test.go b/tests/postgres/select_test.go index fecf20e4..8d0cdce3 100644 --- a/tests/postgres/select_test.go +++ b/tests/postgres/select_test.go @@ -3,9 +3,8 @@ package postgres import ( "context" "database/sql" - "github.com/lib/pq" - "io/ioutil" "github.com/go-jet/jet/v2/internal/utils/ptr" + "github.com/lib/pq" "testing" "time" diff --git a/tests/postgres/values_test.go b/tests/postgres/values_test.go index bdb631c8..f46ca299 100644 --- a/tests/postgres/values_test.go +++ b/tests/postgres/values_test.go @@ -172,7 +172,9 @@ ORDER BY film_values.title; "ReplacementCost": 15.99, "Rating": "R", "LastUpdate": "2013-05-26T14:50:58.951Z", - "SpecialFeatures": "{Trailers}", + "SpecialFeatures": [ + "Trailers" + ], "Fulltext": "'airport':1 'ancient':18 'confront':14 'epic':4 'girl':11 'india':19 'monkey':16 'moos':8 'must':13 'pollock':2 'tale':5" }, "Title": "Airport Pollock", @@ -194,7 +196,9 @@ ORDER BY film_values.title; "ReplacementCost": 12.99, "Rating": "PG-13", "LastUpdate": "2013-05-26T14:50:58.951Z", - "SpecialFeatures": "{Trailers}", + "SpecialFeatures": [ + "Trailers" + ], "Fulltext": "'boat':20 'bright':1 'conquer':14 'encount':2 'fate':4 'feminist':11 'jet':19 'lumberjack':8 'must':13 'student':16 'yarn':5" }, "Title": "Bright Encounters", From 6b6f990264a502cf4d29994357db7c5de59fe176 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Thu, 13 Feb 2025 11:08:43 +0100 Subject: [PATCH 29/34] Move postgres specific functions to postgres pkg --- internal/jet/func_expression.go | 37 ++++----------------------------- postgres/functions.go | 37 ++++++++++++++++++++++++++------- 2 files changed, 33 insertions(+), 41 deletions(-) diff --git a/internal/jet/func_expression.go b/internal/jet/func_expression.go index a095a993..b776b718 100644 --- a/internal/jet/func_expression.go +++ b/internal/jet/func_expression.go @@ -658,39 +658,6 @@ func ALL[E Expression](arr Array[E]) E { return arrayElementTypeCaster(arr, Func("ALL", arr)) } -func ARRAY_APPEND[E Expression](arr Array[E], el E) Array[E] { - return arrayTypeCaster[E](arr, Func("array_append", arr, el)) -} - -func ARRAY_CAT[E Expression](arr1, arr2 Array[E]) Array[E] { - return arrayTypeCaster[E](arr1, Func("array_cat", arr1, arr2)) -} - -func ARRAY_PREPEND[E Expression](el E, arr Array[E]) Array[E] { - return ArrayExp[E](Func("array_prepend", el, arr)) -} - -func ARRAY_LENGTH[E Expression](arr Array[E], el IntegerExpression) IntegerExpression { - return IntExp(Func("array_length", arr, el)) -} - -func arrayTypeCaster[E Expression](arrayExp Expression, exp Expression) Array[E] { - var i Expression - switch arrayExp.(type) { - case Array[StringExpression]: - i = ArrayExp[StringExpression](exp) - case Array[Int4Expression]: - i = ArrayExp[Int4Expression](exp) - case Array[Int8Expression]: - i = ArrayExp[Int8Expression](exp) - case Array[IntegerExpression]: - i = ArrayExp[IntegerExpression](exp) - case Array[BoolExpression]: - i = ArrayExp[BoolExpression](exp) - } - return i.(Array[E]) -} - func arrayElementTypeCaster[E Expression](arrayExp Array[E], exp Expression) E { var i Expression switch arrayExp.(type) { @@ -698,6 +665,10 @@ func arrayElementTypeCaster[E Expression](arrayExp Array[E], exp Expression) E { i = StringExp(exp) case Array[IntegerExpression]: i = IntExp(exp) + case Array[Int4Expression]: + i = IntExp(exp) + case Array[Int8Expression]: + i = IntExp(exp) case Array[BoolExpression]: i = BoolExp(exp) } diff --git a/postgres/functions.go b/postgres/functions.go index aa3b1d2d..b5d2ea48 100644 --- a/postgres/functions.go +++ b/postgres/functions.go @@ -291,20 +291,41 @@ func ALL[T Expression](expression jet.Array[T]) T { return jet.ALL[T](expression) } -func ARRAY_APPEND[T Expression](arr jet.Array[T], el T) jet.Array[T] { - return jet.ARRAY_APPEND(arr, el) +func ARRAY_APPEND[E Expression](arr jet.Array[E], el E) jet.Array[E] { + return arrayTypeCaster[E](arr, Func("array_append", arr, el)) } -func ARRAY_CAT[T Expression](arr1, arr2 jet.Array[T]) jet.Array[T] { - return jet.ARRAY_CAT(arr1, arr2) +func ARRAY_CAT[E Expression](arr1, arr2 jet.Array[E]) jet.Array[E] { + return arrayTypeCaster[E](arr1, Func("array_cat", arr1, arr2)) } -func ARRAY_LENGTH[T Expression](expression jet.Array[T], dim IntegerExpression) IntegerExpression { - return jet.ARRAY_LENGTH(expression, dim) +func ARRAY_PREPEND[E Expression](el E, arr jet.Array[E]) jet.Array[E] { + return jet.ArrayExp[E](Func("array_prepend", el, arr)) } -func ARRAY_PREPEND[T Expression](el T, arr jet.Array[T]) jet.Array[T] { - return jet.ARRAY_PREPEND(el, arr) +func ARRAY_LENGTH[E Expression](arr jet.Array[E], el IntegerExpression) IntegerExpression { + return IntExp(Func("array_length", arr, el)) +} + +func ARRAY_REMOVE[E Expression](arr jet.Array[E], el Expression) IntegerExpression { + return IntExp(Func("array_remove", arr, el)) +} + +func ARRAY_TO_STRING(arr Expression, delim StringExpression) StringExpression { + return StringExp(Func("array_to_string", arr, delim)) +} + +func arrayTypeCaster[E Expression](arrayExp Expression, exp Expression) jet.Array[E] { + var i Expression + switch arrayExp.(type) { + case jet.Array[StringExpression]: + i = jet.ArrayExp[StringExpression](exp) + case jet.Array[IntegerExpression]: + i = jet.ArrayExp[IntegerExpression](exp) + case jet.Array[BoolExpression]: + i = jet.ArrayExp[BoolExpression](exp) + } + return i.(jet.Array[E]) } // ARRAY constructor From 0ec3e456e591009468affa9969f3b3ae5c31c8ac Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Thu, 13 Feb 2025 11:11:01 +0100 Subject: [PATCH 30/34] Change casing --- postgres/functions.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/postgres/functions.go b/postgres/functions.go index b5d2ea48..c6fae677 100644 --- a/postgres/functions.go +++ b/postgres/functions.go @@ -292,27 +292,27 @@ func ALL[T Expression](expression jet.Array[T]) T { } func ARRAY_APPEND[E Expression](arr jet.Array[E], el E) jet.Array[E] { - return arrayTypeCaster[E](arr, Func("array_append", arr, el)) + return arrayTypeCaster[E](arr, Func("ARRAY_APPEND", arr, el)) } func ARRAY_CAT[E Expression](arr1, arr2 jet.Array[E]) jet.Array[E] { - return arrayTypeCaster[E](arr1, Func("array_cat", arr1, arr2)) + return arrayTypeCaster[E](arr1, Func("ARRAY_CAT", arr1, arr2)) } func ARRAY_PREPEND[E Expression](el E, arr jet.Array[E]) jet.Array[E] { - return jet.ArrayExp[E](Func("array_prepend", el, arr)) + return jet.ArrayExp[E](Func("ARRAY_PREPEND", el, arr)) } func ARRAY_LENGTH[E Expression](arr jet.Array[E], el IntegerExpression) IntegerExpression { - return IntExp(Func("array_length", arr, el)) + return IntExp(Func("ARRAY_LENGTH", arr, el)) } func ARRAY_REMOVE[E Expression](arr jet.Array[E], el Expression) IntegerExpression { - return IntExp(Func("array_remove", arr, el)) + return IntExp(Func("ARRAY_REMOVE", arr, el)) } func ARRAY_TO_STRING(arr Expression, delim StringExpression) StringExpression { - return StringExp(Func("array_to_string", arr, delim)) + return StringExp(Func("ARRAY_TO_STRING", arr, delim)) } func arrayTypeCaster[E Expression](arrayExp Expression, exp Expression) jet.Array[E] { From 1f6afe705e6ed41308caaa0f2decbf087b30b56e Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Thu, 13 Feb 2025 11:14:46 +0100 Subject: [PATCH 31/34] Fix testcases --- tests/postgres/array_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/postgres/array_test.go b/tests/postgres/array_test.go index fa6e88d0..61d857a7 100644 --- a/tests/postgres/array_test.go +++ b/tests/postgres/array_test.go @@ -70,10 +70,10 @@ SELECT sample_arrays.text_array AS "sample_arrays.text_array", (sample_arrays.bool_array || $13) AS "sample.bool_concat", (sample_arrays.text_array || $14::text) AS "sample.text_concat_el", sample_arrays.text_array[$15::integer] AS "sample.text_at", - array_append(sample_arrays.text_array, $16::text) AS "sample.text_append", - array_cat(sample_arrays.text_array, $17) AS "sample.text_cat", - array_length(sample_arrays.text_array, $18::integer) AS "sample.text_length", - array_prepend($19::text, sample_arrays.text_array) AS "sample.text_prepend" + ARRAY_APPEND(sample_arrays.text_array, $16::text) AS "sample.text_append", + ARRAY_CAT(sample_arrays.text_array, $17) AS "sample.text_cat", + ARRAY_LENGTH(sample_arrays.text_array, $18::integer) AS "sample.text_length", + ARRAY_PREPEND($19::text, sample_arrays.text_array) AS "sample.text_prepend" FROM test_sample.sample_arrays WHERE sample_arrays.bool_array @> $20; `) From 3a5643b9491244e76f22b07a5c364fd5972c4e4b Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Thu, 13 Feb 2025 11:19:01 +0100 Subject: [PATCH 32/34] Fixing lint errors --- internal/jet/expression.go | 36 ++++++++++++++++++------------------ internal/jet/sql_builder.go | 4 ---- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/internal/jet/expression.go b/internal/jet/expression.go index 678b36ce..c2488baa 100644 --- a/internal/jet/expression.go +++ b/internal/jet/expression.go @@ -316,24 +316,24 @@ 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(")") - } -} +//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 { return CustomExpression(array, Token("["), subscript, Token("]")) diff --git a/internal/jet/sql_builder.go b/internal/jet/sql_builder.go index dce93968..035bb38f 100644 --- a/internal/jet/sql_builder.go +++ b/internal/jet/sql_builder.go @@ -311,7 +311,3 @@ 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) + `"` -} From 27c34b31f4b41b1e8ddd356fe3d50efebdf28279 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Thu, 13 Feb 2025 11:31:07 +0100 Subject: [PATCH 33/34] Update submodules for now to make tests green --- .gitmodules | 2 +- tests/testdata | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 4ae12907..498b2ea7 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "tests/testdata"] path = tests/testdata - url = https://github.com/go-jet/jet-test-data + url = https://github.com/arjen-ag5/jet-test-data diff --git a/tests/testdata b/tests/testdata index 1c501acb..8433df98 160000 --- a/tests/testdata +++ b/tests/testdata @@ -1 +1 @@ -Subproject commit 1c501acb72bea389788404988ef0130b733f9cee +Subproject commit 8433df982dbd9862f20d3d6dcc5ab80f6d44e0cd From a1b32e39c2a00dd459e620816d1deb75c85e0953 Mon Sep 17 00:00:00 2001 From: Arjen Brouwer Date: Tue, 15 Jul 2025 16:39:35 +0200 Subject: [PATCH 34/34] Added collation function --- postgres/collate_func.go | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 postgres/collate_func.go diff --git a/postgres/collate_func.go b/postgres/collate_func.go new file mode 100644 index 00000000..cfe277e9 --- /dev/null +++ b/postgres/collate_func.go @@ -0,0 +1,7 @@ +package postgres + +import "github.com/go-jet/jet/v2/internal/jet" + +func COLLATE(exp StringExpression, collation string) StringExpression { + return StringExp(CustomExpression(exp, jet.Raw("COLLATE"), jet.Keyword(collation))) +}