Expanding the elements of an array with PostgreSQL and Exposed

A picture of the main characters from the TV show The Expanse with one white guy in the center, another guy on his left looking toward the horizon, both are wearing beards. At the bottom are 3 women and in the middle is the first 2 thirds of my face. The title says The Expanse and the subtitle says a store about a postgresql array.

One of the things I really like about Postgres is the various types of columns you can use (without saying that this is limited to Postgres). In this context, the array column type makes it easy to store a list of “stuff”. You can then use the various functions that PostgreSQL provides to query and transform those data. Recently I needed to get all the unique elements from all the values of an array column and once it worked, I pushed a little further to achieve the same result with Exposed.

PostgreSQL’s Array Type

In the name of everything that is holy and pure in this world, I implore you not to quote me on this, but my understanding is that the array type allows you to store a list of any available types, built-in and user-defined all the same. That said, in this example, we’re going to use a simple list of strings. This is what our table looks like:

CREATE TABLE books (
    name text,
    genres text[]
);

We’re also going to add a few books, like so:

INSERT INTO "books"("name", "genres") VALUES('When Women Where Dragons', '{Romance,Fantasy,Feminism}');
INSERT INTO "books"("name", "genres") VALUES('Pride and Prejudice', '{Romance,Classics}');
INSERT INTO "books"("name", "genres") VALUES('Salt: A World History', '{Food,Science}');

Getting a list of all the genres

Once we have a list of genres stored into the books table, getting a (distinct, ideally) list of genres means expanding the “genres” array of each row into a set of rows, using the unnest function. Since we want a unique list of genres, we throw a distinct into the mix, which gives us the following query:

SELECT DISTINCT unnest(genres) genre FROM books ORDER BY genre

After running this query, we get 6 rows, one for each genre:

  genre   
----------
 Fantasy
 Food
 Romance
 Science
 Feminism
 Classics
(6 rows)

Exposed does not support most of the array functions for postgres. Fortunately, it’s fairly trivial to add custom functions.

Adding support in Exposed

First, let’s define the table using Exposed:

object Books: Table("books") {
    val name = varchar("name", 255)
    val genres = array<String>("genres")
}

There are many ways to create custom functions with exposed but in this case, I’ve found that the easiest way is to extend the CustomFunction class:

class Unnest<T>(
    expr: Column<List<T>>,
    column: IColumnType<T & Any>,
): CustomFunction<T>("UNNEST", column, expr)

Then, create an helper function to make it easy to use it in a query (as long as the column is an array of String, otherwise you need adapt it):

fun Column<List<String>>.unnest() = Unnest(this, TextColumnType())

val genres = BookTable.select(Books.genres.unnest()).withDistinct(true).map { it[Books.genres.unnest()] }

Conclusion

In situations like these, I originally reverted to using raw SQL queries, but having gained a little more knowledge about the internals of Exposed, adding extensions makes the codebase a little nicer and more consistent.