Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding support for arrays #380

Draft
wants to merge 28 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c38c302
Initial support for postgres text arrays
arjen-ag5 Sep 3, 2024
f72150a
Support arrays in postgres layer
arjen-ag5 Sep 3, 2024
22b845e
Extend generator with text array support
arjen-ag5 Sep 3, 2024
a9092e3
Introduced more literal types
arjen-ag5 Sep 3, 2024
f51f32a
Use generic types
arjen-ag5 Sep 3, 2024
e6619f1
Update testcases
arjen-ag5 Sep 3, 2024
566374d
Don't use generics in postgres package
arjen-ag5 Sep 3, 2024
d61e0aa
Update generator
arjen-ag5 Sep 3, 2024
647191a
Take array dimensions into consideration
arjen-ag5 Sep 4, 2024
b17d5e2
Use MySQL server container which also support ARM64 to allow running …
arjen-ag5 Sep 4, 2024
709f631
Update test cases for array types
arjen-ag5 Sep 4, 2024
1f81d75
Inadvertently copied function
arjen-ag5 Sep 4, 2024
220034e
Add a function to map SQL array types
arjen-ag5 Sep 7, 2024
de67d7d
Renamed ArrayExpression to Array
arjen-ag5 Sep 7, 2024
cd7160a
AT now returns it's elements generic type
arjen-ag5 Sep 7, 2024
5d0cd24
Use CustomExpression
arjen-ag5 Sep 7, 2024
e4f6a51
Removed old cruft
arjen-ag5 Sep 7, 2024
261df9d
Cleanup literal type construction
arjen-ag5 Sep 7, 2024
1d19220
Use PQ creating a driver value for arrays
arjen-ag5 Sep 7, 2024
cb71a56
Implemented Any/All as standalone functions
arjen-ag5 Sep 9, 2024
c4ddd53
Remove old cruft
arjen-ag5 Sep 9, 2024
ab7fee2
Use proper types
arjen-ag5 Sep 20, 2024
a37f99f
Add boolean with status
arjen-ag5 Sep 20, 2024
d3ee217
Add array functions
arjen-ag5 Sep 20, 2024
3c0a397
Two new literal types
arjen-ag5 Sep 20, 2024
78733ce
Supporting more types
arjen-ag5 Sep 20, 2024
f9368c0
Adding array functions
arjen-ag5 Sep 20, 2024
a5e5a8a
Fixing testcases
arjen-ag5 Sep 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions generator/metadata/column_meta_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ type DataType struct {
Name string
Kind DataTypeKind
IsUnsigned bool
Dimensions int // The number of array dimensions
}
1 change: 1 addition & 0 deletions generator/postgres/query_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ select
not attr.attnotnull as "column.isNullable",
attr.attgenerated = 's' as "column.isGenerated",
attr.atthasdef as "column.hasDefault",
attr.attndims as "dataType.dimensions",
(case
when tp.typtype = 'b' AND tp.typcategory <> 'A' then 'base'
when tp.typtype = 'b' AND tp.typcategory = 'A' then 'array'
Expand Down
18 changes: 17 additions & 1 deletion generator/template/model_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/go-jet/jet/v2/internal/utils/dbidentifier"
"github.com/google/uuid"
"github.com/jackc/pgtype"
"github.com/lib/pq"
"path"
"reflect"
"strings"
Expand Down Expand Up @@ -249,7 +250,7 @@ func getUserDefinedType(column metadata.Column) string {
switch column.DataType.Kind {
case metadata.EnumType:
return dbidentifier.ToGoIdentifier(column.DataType.Name)
case metadata.UserDefinedType, metadata.ArrayType:
case metadata.UserDefinedType:
return "string"
}

Expand All @@ -268,6 +269,11 @@ func getGoType(column metadata.Column) interface{} {

// toGoType returns model type for column info.
func toGoType(column metadata.Column) interface{} {
// We don't support multi-dimensional arrays
if column.DataType.Dimensions > 1 {
return ""
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi dimensional arrays are just arrays containing other arrays. In our case Array[Array[StringExpression]]. But it is fine with PR to go with single dimension arrays only.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, but since we don't have a good model type yet, I haven't included this case yet

switch strings.ToLower(column.DataType.Name) {
case "user-defined", "enum":
return ""
Expand Down Expand Up @@ -333,6 +339,16 @@ func toGoType(column metadata.Column) interface{} {
return pgtype.Int8range{}
case "numrange":
return pgtype.Numrange{}
case "bool[]", "boolean[]":
return pq.BoolArray{}
case "integer[]", "int4[]":
return pq.Int32Array{}
case "bigint[]", "int8[]":
return pq.Int64Array{}
case "bytea[]":
return pq.ByteaArray{}
case "text[]", "jsonb[]", "json[]":
return pq.StringArray{}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine using pq types for one dimensional arrays. Maybe pgtype can be used for multi dimensional arrays.
Other types as well, time, date, etc...

default:
fmt.Println("- [Model ] Unsupported sql column '" + column.Name + " " + column.DataType.Name + "', using string instead.")
return ""
Expand Down
88 changes: 68 additions & 20 deletions generator/template/sql_builder_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,53 +145,101 @@ func DefaultTableSQLBuilderColumn(columnMetaData metadata.Column) TableSQLBuilde
// getSqlBuilderColumnType returns type of jet sql builder column
func getSqlBuilderColumnType(columnMetaData metadata.Column) string {
if columnMetaData.DataType.Kind != metadata.BaseType &&
columnMetaData.DataType.Kind != metadata.RangeType {
columnMetaData.DataType.Kind != metadata.RangeType &&
columnMetaData.DataType.Kind != metadata.ArrayType {
return "String"
}

switch strings.ToLower(columnMetaData.DataType.Name) {
typeName := columnMetaData.DataType.Name
columnName := columnMetaData.Name

var columnType string
var supported bool

if columnMetaData.DataType.Kind == metadata.ArrayType {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not add a case switch for each of the array types, as it is already done for other types?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new sqlArrayToColumnType to convert a type to an array type. The function switches on the type name without the [] suffix because you would have to add infinite brackets for multidimensional arrays.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I think about it again it makes sense to separate arrays from sqlToColumnType. Since every postgres type can be element of array([]bool, []point, []timestamp, etc...), sql builder array type can be constructed with just sqlToColumnType:

columnType = sqlToColumnType(strings.TrimSuffix(typeName, "[]")) + "Array"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can lead to incorrectly generated code, because this PR does not support DateArray for example. What's your opinion on this?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be supported with one line type DateArray Array[DateExpression]. For model type we can fallback to string if there is no pq date array type.

if columnMetaData.DataType.Dimensions > 1 {
fmt.Println("- [SQL Builder] Unsupported sql array with multiple dimensions column '" + columnName + " " + typeName + "', using StringColumn instead.")
return "String"
}

columnType, supported = sqlArrayToColumnType(strings.TrimSuffix(typeName, "[]"))
} else {
columnType, supported = sqlToColumnType(typeName)
}

if !supported {
fmt.Printf("- [SQL Builder] Unsupported SQL column '" + columnName + " " + typeName + "', using StringColumn instead.\n")
return "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. 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", true
case "smallint", "integer", "bigint", "int2", "int4", "int8",
"tinyint", "mediumint", "int", "year": //MySQL
return "IntegerArray", true
case "boolean", "bool":
return "Bool"
return "BoolArray", true
default:
return "", false
}
}

// 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", 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:
fmt.Println("- [SQL Builder] Unsupported sql column '" + columnMetaData.Name + " " + columnMetaData.DataType.Name + "', using StringColumn instead.")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if we add array types, there still gonna be some unsupported types, so warning message can remain.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The warning is handled in the caller

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha, I see. Maybe we an error would be more in go style.

Copy link
Author

@arjen-ag5 arjen-ag5 Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about?

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

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is fine as well.

return "String"
return "", false
}
}

Expand Down
93 changes: 93 additions & 0 deletions internal/jet/array_expression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package jet

// Array interface
type Array[E Expression] interface {
Expression

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

type arrayInterfaceImpl[E Expression] struct {
parent Array[E]
}

type BinaryBoolOp func(Expression, Expression) BoolExpression

func (a arrayInterfaceImpl[E]) EQ(rhs Array[E]) BoolExpression {
return Eq(a.parent, rhs)
}

func (a arrayInterfaceImpl[E]) NOT_EQ(rhs Array[E]) BoolExpression {
return NotEq(a.parent, rhs)
}

func (a arrayInterfaceImpl[E]) LT(rhs Array[E]) BoolExpression {
return Lt(a.parent, rhs)
}

func (a arrayInterfaceImpl[E]) GT(rhs Array[E]) BoolExpression {
return Gt(a.parent, rhs)
}

func (a arrayInterfaceImpl[E]) LT_EQ(rhs Array[E]) BoolExpression {
return LtEq(a.parent, rhs)
}

func (a arrayInterfaceImpl[E]) GT_EQ(rhs Array[E]) BoolExpression {
return GtEq(a.parent, rhs)
}

func (a arrayInterfaceImpl[E]) CONTAINS(rhs Array[E]) BoolExpression {
return Contains(a.parent, rhs)
}

func (a arrayInterfaceImpl[E]) IS_CONTAINED_BY(rhs Array[E]) BoolExpression {
return IsContainedBy(a.parent, rhs)
}

func (a arrayInterfaceImpl[E]) OVERLAP(rhs Array[E]) BoolExpression {
return Overlap(a.parent, rhs)
}

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) Array[E] {
return ArrayExp[E](NewBinaryOperatorExpression(a.parent, rhs, "||"))
}

func (a arrayInterfaceImpl[E]) AT(expression IntegerExpression) E {
return arrayElementTypeCaster[E](a.parent, arraySubscriptExpr(a.parent, expression))
}

type arrayExpressionWrapper[E Expression] struct {
arrayInterfaceImpl[E]
Expression
}

func newArrayExpressionWrap[E Expression](expression Expression) Array[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) Array[E] {
return newArrayExpressionWrap[E](expression)
}
59 changes: 59 additions & 0 deletions internal/jet/array_expression_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package jet

import (
"github.com/lib/pq"
"testing"
)

func TestArrayExpressionEQ(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.EQ(table2ColArray), "(table1.col_array_string = table2.col_array_string)")
}

func TestArrayExpressionNOT_EQ(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.NOT_EQ(table2ColArray), "(table1.col_array_string != table2.col_array_string)")
assertClauseSerialize(t, table1ColStringArray.NOT_EQ(StringArray([]string{"x"})), "(table1.col_array_string != $1)", pq.StringArray{"x"})
}

func TestArrayExpressionLT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.LT(table2ColArray), "(table1.col_array_string < table2.col_array_string)")
}

func TestArrayExpressionGT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.GT(table2ColArray), "(table1.col_array_string > table2.col_array_string)")
}

func TestArrayExpressionLT_EQ(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.LT_EQ(table2ColArray), "(table1.col_array_string <= table2.col_array_string)")
}

func TestArrayExpressionGT_EQ(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.GT_EQ(table2ColArray), "(table1.col_array_string >= table2.col_array_string)")
}

func TestArrayExpressionCONTAINS(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.CONTAINS(table2ColArray), "(table1.col_array_string @> table2.col_array_string)")
assertClauseSerialize(t, table1ColStringArray.CONTAINS(StringArray([]string{"x"})), "(table1.col_array_string @> $1)", pq.StringArray{"x"})
}

func TestArrayExpressionCONTAINED_BY(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.IS_CONTAINED_BY(table2ColArray), "(table1.col_array_string <@ table2.col_array_string)")
assertClauseSerialize(t, table1ColStringArray.IS_CONTAINED_BY(StringArray([]string{"x"})), "(table1.col_array_string <@ $1)", pq.StringArray{"x"})
}

func TestArrayExpressionOVERLAP(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.OVERLAP(table2ColArray), "(table1.col_array_string && table2.col_array_string)")
}

func TestArrayExpressionCONCAT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.CONCAT(table2ColArray), "(table1.col_array_string || table2.col_array_string)")
assertClauseSerialize(t, table1ColStringArray.CONCAT(StringArray([]string{"x"})), "(table1.col_array_string || $1)", pq.StringArray{"x"})
}

func TestArrayExpressionCONCAT_ELEMENT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.CONCAT_ELEMENT(StringExp(table2ColArray.AT(Int(1)))), "(table1.col_array_string || table2.col_array_string[$1])", int64(1))
assertClauseSerialize(t, table1ColStringArray.CONCAT_ELEMENT(String("x")), "(table1.col_array_string || $1)", "x")
}

func TestArrayExpressionAT(t *testing.T) {
assertClauseSerialize(t, table1ColStringArray.AT(Int(1)), "table1.col_array_string[$1]", int64(1))
}
40 changes: 40 additions & 0 deletions internal/jet/column_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,46 @@ func IntegerColumn(name string) ColumnInteger {

//------------------------------------------------------//

type ColumnArray[E Expression] interface {
Array[E]
Column

From(subQuery SelectTable) ColumnArray[E]
SET(stringExp Array[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 Array[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 {
Expand Down
Loading