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 19 commits into
base: master
Choose a base branch
from
Draft

Adding support for arrays #380

wants to merge 19 commits into from

Conversation

arjen-ag5
Copy link

Hello,

I'm working on support for arrays in go-jet. I was wondering if this is a feature that is considered in the long run and if it maybe merged into upstream in the long term. I'd like to show the progress I made so far and if it's any use trying to get it merged at all. If not I will not spend any effort in making it work with the different database types. Currently I only implemented postgres.

I can see from the Range support that is added earlier that the types from the pgtypes packages are being used. I took this into consideration for use as the SQL types in the generated table types. But the pgtypes.Array* structs are tailored to represent the internal representation of the postgresql array types. In constrast to the pq.Array* types which are much more user friendly, thats why I chose to use the latter.

@arjen-ag5 arjen-ag5 changed the title Adding support for (string) arrays Adding support for arrays Sep 4, 2024
@go-jet
Copy link
Owner

go-jet commented Sep 7, 2024

Hi @arjen-ag5, thanks for starting the work on 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

case "bigint[]":
return pq.Int64Array{}
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...

typeName := columnMetaData.DataType.Name
columnName := columnMetaData.Name

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.

@@ -190,8 +222,7 @@ func getSqlBuilderColumnType(columnMetaData metadata.Column) string {
case "numrange":
return "NumericRange"
default:
fmt.Println("- [SQL Builder] Unsupported sql column '" + columnMetaData.Name + " " + columnMetaData.DataType.Name + "', using StringColumn instead.")
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.

package jet

// ArrayExpression interface
type ArrayExpression[E Expression] interface {
Copy link
Owner

Choose a reason for hiding this comment

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

Maybe just Array[E Expression], so it follows naming convention with Range. Also Expression would repeat twice in the name.

Copy link
Author

Choose a reason for hiding this comment

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

👍 Done

CONCAT(rhs ArrayExpression[E]) ArrayExpression[E]
CONCAT_ELEMENT(E) ArrayExpression[E]

AT(expression IntegerExpression) Expression
Copy link
Owner

Choose a reason for hiding this comment

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

AT should return element type E. Take a look at rangeTypeCaster how to extract element type.

Copy link
Author

Choose a reason for hiding this comment

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

Done

@@ -316,6 +316,32 @@ func (s *complexExpression) serialize(statement StatementType, out *SQLBuilder,
}
}

type arraySubscriptExpression struct {
Copy link
Owner

Choose a reason for hiding this comment

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

This is the old way to construct custom expression. The new and easier way is using CustomExpression and Token. In this case: CustomExpression(array, Token("["), index, Token("]")).

Copy link
Author

Choose a reason for hiding this comment

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

Done

literalExpressionImpl
}

func UnsafeArray[E LiteralExpression](values []interface{}) ArrayExpression[E] {
Copy link
Owner

Choose a reason for hiding this comment

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

Why is it needed?

Copy link
Author

@arjen-ag5 arjen-ag5 Sep 7, 2024

Choose a reason for hiding this comment

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

Was experimenting around, forgot to remove it. Gone now

literalExpressionImpl
}

func Int64Array(values []int64) ArrayExpression[IntegerExpression] {
Copy link
Owner

Choose a reason for hiding this comment

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

Can be done without new literal types. Some of these files need some cleaning.

func Int64Array(values []int64) ArrayExpression[IntegerExpression] {
    return ArrayExp[IntegerExpression](literal(pq.Int64Array(values))
}

Copy link
Author

Choose a reason for hiding this comment

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

Cleanedup

@@ -253,6 +255,19 @@ func argToString(value interface{}) string {
}
}

func stringArrayQuote(val []string) string {
Copy link
Owner

Choose a reason for hiding this comment

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

Maybe we can use pq.StringArray{}.Value() instead, and cast first returned value to string.

Copy link
Author

Choose a reason for hiding this comment

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

Like this?

func stringArrayQuote(val []string) 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)
}

Copy link
Owner

Choose a reason for hiding this comment

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

Yeah, exactly.

Copy link
Author

Choose a reason for hiding this comment

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

Done

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

Copy link
Owner

@go-jet go-jet Sep 7, 2024

Choose a reason for hiding this comment

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

If we add ANY_EQ, we would also need to add, for each of the types, NOT_EQ_ANY, LT_ANY, LT_EQ_ANY, etc... This would lead to number of methods explosion.

Simpler way would be to have ANY and ALL as generic functions:

func ANY[E Expression](array Array[E]) E {
    // check rangeTypeCaster
}

Then we can write for any type:

Table.SomeStringColumn.EQ(ANY(Table.StringArrayColumn))

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, that makes sense. I'll change that

Copy link
Author

Choose a reason for hiding this comment

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

Done

postgres/cast.go Outdated
@@ -45,6 +45,10 @@ type cast interface {
AS_INTERVAL() IntervalExpression
}

type castArray interface {
AS_STRING() jet.ArrayExpression[StringExpression]
Copy link
Owner

Choose a reason for hiding this comment

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

Why separate interface?

Copy link
Author

Choose a reason for hiding this comment

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

Was left over cruft from experimentation, removed it

@@ -967,26 +969,25 @@ func TestScanIntoCustomBaseTypes(t *testing.T) {
ReplacementCost MyFloat64
Rating *model.MpaaRating
LastUpdate MyTime
SpecialFeatures *MyString
SpecialFeatures MyStringArray
Copy link
Owner

Choose a reason for hiding this comment

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

If SpecialFeatures is pq.StringArray you wouldn't need to exclude SpecialFeatures column bellow.

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.

With the type alias like so:

type MyStringArray pq.StringArray

The MyStringArray type does not implement Scanner. The QRM will then see it as a slice because that's what pq.StringArray is and try to apply it's row mapping into the slice. Therefore it will always be null.

Adding a method like this would work:

func (a *MyStringArray) Scan(src interface{}) error {
	x := (*pq.StringArray)(a)
	return x.Scan(src)
}

What do you think?

Copy link
Owner

Choose a reason for hiding this comment

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

No, I'm meant without defining a type, either by realiasing or not using new type at all:

type MyStringArray = pq.StringArray // note '=', MyStringArray now retains Scan method

or

	type film struct {
		FilmID          MyUint16 `sql:"primary_key"`
		Title           MyString
		Description     *MyString
		ReleaseYear     *MyInt16
		LanguageID      MyUint8
		RentalDuration  MyUint8
		RentalRate      MyFloat32
		Length          *MyUint32
		ReplacementCost MyFloat64
		Rating          *model.MpaaRating
		LastUpdate      MyTime
		SpecialFeatures *MyString
		SpecialFeatures pq.StringArray
	}

@@ -2,6 +2,7 @@ package postgres

import (
"database/sql"
"github.com/lib/pq"
"testing"
"time"

Copy link
Owner

Choose a reason for hiding this comment

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

As for testing, maybe we can have a new array only table, similar to what's already been done for range tests.

Copy link
Owner

Choose a reason for hiding this comment

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

Note that test files are in separate repo: https://github.com/go-jet/jet-test-data

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants