-
Notifications
You must be signed in to change notification settings - Fork 125
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
base: master
Are you sure you want to change the base?
Changes from all commits
c38c302
f72150a
22b845e
a9092e3
f51f32a
e6619f1
566374d
d61e0aa
647191a
b17d5e2
709f631
1f81d75
220034e
de67d7d
cd7160a
5d0cd24
e4f6a51
261df9d
1d19220
cb71a56
c4ddd53
ab7fee2
a37f99f
d3ee217
3c0a397
78733ce
f9368c0
a5e5a8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import ( | |
"github.com/go-jet/jet/v2/internal/utils/dbidentifier" | ||
"github.com/google/uuid" | ||
"github.com/jackc/pgtype" | ||
"github.com/lib/pq" | ||
"path" | ||
"reflect" | ||
"strings" | ||
|
@@ -249,7 +250,7 @@ func getUserDefinedType(column metadata.Column) string { | |
switch column.DataType.Kind { | ||
case metadata.EnumType: | ||
return dbidentifier.ToGoIdentifier(column.DataType.Name) | ||
case metadata.UserDefinedType, metadata.ArrayType: | ||
case metadata.UserDefinedType: | ||
return "string" | ||
} | ||
|
||
|
@@ -268,6 +269,11 @@ func getGoType(column metadata.Column) interface{} { | |
|
||
// toGoType returns model type for column info. | ||
func toGoType(column metadata.Column) interface{} { | ||
// We don't support multi-dimensional arrays | ||
if column.DataType.Dimensions > 1 { | ||
return "" | ||
} | ||
|
||
switch strings.ToLower(column.DataType.Name) { | ||
case "user-defined", "enum": | ||
return "" | ||
|
@@ -333,6 +339,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{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine using |
||
default: | ||
fmt.Println("- [Model ] Unsupported sql column '" + column.Name + " " + column.DataType.Name + "', using string instead.") | ||
return "" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a new There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 columnType = sqlToColumnType(strings.TrimSuffix(typeName, "[]")) + "Array" There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can be supported with one line |
||
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.") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The warning is handled in the caller There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, that is fine as well. |
||
return "String" | ||
return "", false | ||
} | ||
} | ||
|
||
|
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) | ||
} |
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)) | ||
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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