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 33 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5eb6114
Initial support for postgres text arrays
arjen-ag5 Sep 3, 2024
4591991
Support arrays in postgres layer
arjen-ag5 Sep 3, 2024
ad15139
Extend generator with text array support
arjen-ag5 Sep 3, 2024
ac24ae6
Introduced more literal types
arjen-ag5 Sep 3, 2024
7592ddd
Use generic types
arjen-ag5 Sep 3, 2024
a133b2a
Update testcases
arjen-ag5 Sep 3, 2024
29f6de4
Don't use generics in postgres package
arjen-ag5 Sep 3, 2024
6777c42
Update generator
arjen-ag5 Sep 3, 2024
2479eaa
Take array dimensions into consideration
arjen-ag5 Sep 4, 2024
a3f4d2a
Use MySQL server container which also support ARM64 to allow running …
arjen-ag5 Sep 4, 2024
2ab72b0
Update test cases for array types
arjen-ag5 Sep 4, 2024
34b2db7
Inadvertently copied function
arjen-ag5 Sep 4, 2024
ea6d437
Add a function to map SQL array types
arjen-ag5 Sep 7, 2024
8bc73a8
Renamed ArrayExpression to Array
arjen-ag5 Sep 7, 2024
7a89822
AT now returns it's elements generic type
arjen-ag5 Sep 7, 2024
f53d7df
Use CustomExpression
arjen-ag5 Sep 7, 2024
c1e35c4
Removed old cruft
arjen-ag5 Sep 7, 2024
cfd8a79
Cleanup literal type construction
arjen-ag5 Sep 7, 2024
fc11115
Use PQ creating a driver value for arrays
arjen-ag5 Sep 7, 2024
533602b
Implemented Any/All as standalone functions
arjen-ag5 Sep 9, 2024
65c53c7
Use proper types
arjen-ag5 Sep 20, 2024
fa60ca1
Add boolean with status
arjen-ag5 Sep 20, 2024
9702631
Add array functions
arjen-ag5 Sep 20, 2024
aa7ce8d
Two new literal types
arjen-ag5 Sep 20, 2024
bf44a3c
Supporting more types
arjen-ag5 Sep 20, 2024
6efbf42
Adding array functions
arjen-ag5 Sep 20, 2024
5c7e492
Fixing testcases
arjen-ag5 Sep 20, 2024
d7f14fc
Fix testcases
arjen-ag5 Feb 13, 2025
6b6f990
Move postgres specific functions to postgres pkg
arjen-ag5 Feb 13, 2025
0ec3e45
Change casing
arjen-ag5 Feb 13, 2025
1f6afe7
Fix testcases
arjen-ag5 Feb 13, 2025
3a5643b
Fixing lint errors
arjen-ag5 Feb 13, 2025
27c34b3
Update submodules for now to make tests green
arjen-ag5 Feb 13, 2025
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
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -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
Copy link
Owner

Choose a reason for hiding this comment

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

This change will need to be part of separate pull request for https://github.com/go-jet/jet-test-data repo.

1 change: 1 addition & 0 deletions generator/metadata/column_meta_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,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 @@ -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'
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 @@ -7,6 +7,7 @@ import (
"github.com/google/uuid"
"github.com/jackc/pgtype"
"path/filepath"
"github.com/lib/pq"
"reflect"
"strings"
"time"
Expand Down Expand Up @@ -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"
}

Expand All @@ -270,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 ""
}

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 @@ -335,6 +341,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{}
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 @@ -156,53 +156,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.")
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