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

feat: EXPOSED-359 Add support for multidimensional arrays #2250

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

obabichevjb
Copy link
Collaborator

Description

Summary of the change: Added support for multi-dimensional array types in Exposed for PostgreSQL database.

Detailed description:

  • What: This PR introduces new functions and types for supporting multi-dimensional arrays. Specifically, it includes the multi2Array, multi3Array, and multiArray functions for defining multi-dimensional array columns. It also adds corresponding literal functions multi2ArrayLiteral, multi3ArrayLiteral, and multiArrayLiteral.

Type of Change

Please mark the relevant options with an "X":

  • New feature
  • Documentation update

Updates/remove existing public API methods:

  • Is breaking change

Affected databases:

  • Postgres

Related Issues

EXPOSED-359 Add support for multidimensional arrays

@obabichevjb obabichevjb force-pushed the obabichev/exposed-359-multi-arrays branch from 358a5d3 to dc91966 Compare September 23, 2024 14:03
@obabichevjb
Copy link
Collaborator Author

I called new column type MultiArrayColumnType, column creator functions multiArray, multi2Array, multi3Array, and literals multi2ArrayLiteral, multi3ArrayLiteral, multiArrayLiteral, but it's discussible, I'm open for other options.

@obabichevjb obabichevjb force-pushed the obabichev/exposed-359-multi-arrays branch from f2b53c4 to ef054cb Compare September 26, 2024 09:06
Copy link
Member

@bog-walk bog-walk left a comment

Choose a reason for hiding this comment

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

Left comments about documentation and examples with breaking changes. Please also update title with feat!:.

It'd probably also be best to get other opinions about the naming of these new columns.

Current proposal is:

// existing 1D type
val col1 = array<Int>("col1")
// new types
val col2 = array2<Int>("col2") // 2D
val col3 = array3<Int>("col3") // 3D
val col4 = arrayN<Int, List<List<List<List<Int>>>>>("col4", 4) // 4D

To me, the hanging numbers at the end look incomplete. Also most other ORMs have a single array() that can take 1+ dimensions, so I'm proposing something like:

// overload existing type to default to 1, unless arg passed to 'dimensions'
val col1 = array<Int>("col1")
val col4 = array<Int, List<List<List<List<Int>>>>>("col4", 4)

val col2 = array2d<Int>("col2")
val col3 = array3d<Int>("col3") 

Other options could maybe be array2D(), array2Multi(), array2Dim().

@e5l @joc-a Could you please let us know if you have suggestions/thoughts?

Comment on lines +5 to +6
* `ArrayColumnType` supports multidimensional arrays now and has one more generic parameter.
If the was used directly for one dimensional arrays with parameter `T` like `ArrayColumnType<T>`, now it should
Copy link
Member

Choose a reason for hiding this comment

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

Small type in second line.

Comment on lines +336 to +340
object Teams : Table("teams") {
val memberIds = array2<UUID>("member_ids")
val memberNames = array3<String>("member_names")
val budgets = array2<Double>("budgets")
}
Copy link
Member

Choose a reason for hiding this comment

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

Please review these blocks through the Writerside plugin, as there are many errors and column types are not appearing. For example, 'code-block's do not accept nested < or >, since they will be confused for html tags. You need to use &lt;, &gt;, and &quot; or wrap the entire code in <![CDATA[]]>. Please see previous blocks in file.

Also please fix indentation.

@@ -325,6 +325,40 @@
</code-block>
</chapter>
</chapter>
<chapter title="How to use Multi-Dimensional Array types" id="how-to-use-multi-dimensional-array-types">
Copy link
Member

Choose a reason for hiding this comment

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

I personally think this would be better placed directly as a sub-chapter under 'How to use Array Types', before sub-chapter on functions. That way you could then add an example of using a function with nD arrays in the function section after.

Comment on lines +705 to +707
inline fun <reified T : Any, R : List<Any>> arrayNLiteral(value: R, delegateType: ColumnType<T>? = null, dimensions: Int): LiteralOp<R> {
@OptIn(InternalApi::class)
return LiteralOp(ArrayColumnType(delegateType ?: resolveColumnType(T::class)), value)
return LiteralOp(ArrayColumnType(delegateType ?: resolveColumnType(T::class), dimensions), value)
Copy link
Member

Choose a reason for hiding this comment

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

Imo it looks cleaner if both of them are called arrayLiteral() (same for arrayParam(). Allows easy switch if user wants to for some reason use multi- instead and didn't cause any issues with the overload.
Could you also consider moving dimensions to before the default delegateType, so it doesn't always have to be named when invoked without delegate?

) : ColumnType<List<E>>() {
class ArrayColumnType<T, R : List<Any?>>(
val delegate: ColumnType<T & Any>,
val dimensions: Int,
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason that dimensions can't be given a default value of 1?
If there is, then at minimum this needs to be included in the breaking changes doc.
Anybody that uses ArrayColumnType in custom function will now need to add a value for dimensions, for example:

class ArrayAggFunction<T>(
    expression: ExpressionWithColumnType<T>,
    delegate: ColumnType<T & Any>
) : CustomFunction<List<T>>(
    functionName = "ARRAY_AGG",
    columnType = ArrayColumnType(delegate), // won't compile anymore unless dimensions value passed
    expr = arrayOf(expression)
)

Comment on lines +1197 to +1198
* **Note:** The maximum cardinality is considered for each dimension, but it is ignored by the PostgreSQL database.
* Validation is performed on the client side.
Copy link
Member

Choose a reason for hiding this comment

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

I'm a bit confused by changes to maximumCardinality. I guess I see why the type had to change, even though it isn't used by PostgreSQL and PostgreSQL is the only db that supports multiple-dimensions (but I can see that may change).

From the line above, it sounds like Exposed is going to do some validation. Did you mean to maybe override validateValueBeforeUpdate() here?

If not, then maybe the line should say: "Any validation should be implemented on the client side."

class ArrayColumnType<T, R : List<Any?>>(
val delegate: ColumnType<T & Any>,
val dimensions: Int,
val maximumCardinality: List<Int>? = null
Copy link
Member

Choose a reason for hiding this comment

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

If type must change, this is another breaking change that should be mentioned in docs.
If ArrayColumnType instance was being used with original value, it will now be mistaken as dimensions or cause type does not conform error:

class ArrayAggFunction<T>(
    expression: ExpressionWithColumnType<T>,
    delegate: ColumnType<T & Any>
) : CustomFunction<List<T>>(
    functionName = "ARRAY_AGG",
    columnType = ArrayColumnType(delegate, 5), // will compile but argument will be now wrongly for dimensions
    expr = arrayOf(expression)
)

class ArrayAggFunction<T>(
    expression: ExpressionWithColumnType<T>,
    delegate: ColumnType<T & Any>
) : CustomFunction<List<T>>(
    functionName = "ARRAY_AGG",
    columnType = ArrayColumnType(delegate, maximumCardinality = 5), // won't compile
    expr = arrayOf(expression)
)

Comment on lines +974 to +975
* **Note:** Providing an array size limit when using the PostgreSQL dialect is allowed, but this value will be ignored by the database.
* The whole validation is performed on the client side.
Copy link
Member

Choose a reason for hiding this comment

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

Same comment about validation as in previous file.

Comment on lines +961 to +962
inline fun <reified T : Any> Table.array2(name: String, maximumCardinality: List<Int>? = null): Column<List<List<T>>> =
arrayN<T, List<List<T>>>(name, dimensions = 2, maximumCardinality)
Copy link
Member

Choose a reason for hiding this comment

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

I'd suggest discussing naming options with the rest of the team too.
To me, there's something odd about a hanging number at the end array2<Int>(). I'd still vote for array2d() or maybe even array2Multi() or array2Dim().

* @throws IllegalArgumentException If [dimensions] is less than or equal to 1.
* @throws IllegalStateException If no column type mapping is found.
*/
inline fun <reified T : Any, R : List<Any>> Table.arrayN(name: String, dimensions: Int, maximumCardinality: List<Int>? = null): Column<R> {
Copy link
Member

Choose a reason for hiding this comment

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

Same comment as for arrayLiteral() and arrayParam(). I think it would be cleaner to have single-named array() that has multiple overloads with/without dimensions.

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