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

Support json_agg(column_ref). #1280

Open
igalklebanov opened this issue Nov 24, 2024 · 4 comments · May be fixed by #1316
Open

Support json_agg(column_ref). #1280

igalklebanov opened this issue Nov 24, 2024 · 4 comments · May be fixed by #1316
Assignees
Labels
api Related to library's API enhancement New feature or request postgres Related to PostgreSQL typescript Related to Typescript

Comments

@igalklebanov
Copy link
Member

Hey 👋

Currently eb.fn.jsonAgg supports table names and expressions as inputs. In such cases, the return type is a JSON object array.

json_agg also supports passing a column reference. In such cases, the return type is a literal array.
We should support this.

const result = await db
  .selectFrom('person')
  .innerJoin('pet', 'pet.owner_id', 'person.id')
  .select((eb) =>
    eb.fn.jsonAgg('pet.name').as('person_pets')
  )
  .executeTakeFirstOrThrow()

The return type would be { person_pets: string[] }.

@igalklebanov igalklebanov added enhancement New feature or request postgres Related to PostgreSQL api Related to library's API typescript Related to Typescript labels Nov 24, 2024
@koskimas
Copy link
Member

koskimas commented Dec 2, 2024

Good idea! jsonAgg is also quite broken with left joins, and there's nothing we can do about it easily. Not related to this issue though.

We express left joined tables in the DB generic as tables where each column is nullable. While this works in most cases, in case of jsonAgg it produces objects with nullable properties when it should produce nullable objects with normal property types 😬

We have no way to tell if a column is nullable in DB because it's actually nullable or if it was put there using a left join.

We could express left joined tables in DB like this

{
  person: Person,
  leftJoined: Pet | null
}

But that's a big type overhaul and most likely a breaking change.

@SimonSimCity
Copy link

SimonSimCity commented Jan 9, 2025

Another example of how this could be used is when using subqueries instead of joins. Here's something I found out about it:

This query (though correct in TypeScript) fails when executing:

const result = await db
  .selectFrom("person")
  .select((eb) => [
    eb.fn.jsonAgg(eb
      .selectFrom("pet")
      .whereRef("pet.owner_id", "=", "person.id")
      .select("pet.name"))
      .as("person_pets"),
  ])
  .executeTakeFirstOrThrow()

The error message is:

more than one row returned by a subquery used as an expression

But if you have another select on the outer query, you'd even get an error for that you'd have to group on each of the selected columns, which is confusing 🙈

This query can run on postgres, but is invalid as of TypeScript - hence this ticket:

const result = await db
  .selectFrom("person")
  .select((eb) => [
    eb
      .selectFrom("pet")
      .whereRef("pet.owner_id", "=", "person.id")
      .select(eb => eb.fn.jsonAgg("pet.name"))
      .as("person_pets"),
  ])
  .executeTakeFirstOrThrow()

If there's anything I can help with to get this done, please write, and I'll be happy to pick it up and see whether I can get it to PR-state!

@igalklebanov
Copy link
Member Author

Hey 👋

It's yours. Swing by Discord if you've got any questions. DMs (to me) also welcome.

@SimonSimCity SimonSimCity linked a pull request Jan 9, 2025 that will close this issue
@SimonSimCity
Copy link

SimonSimCity commented Jan 9, 2025

If you need a work-around, you can cast the types as following (the type DB is your database):

import { RawBuilder, ExtractTypeFromStringReference } from 'kysely'

const result = await db
  .selectFrom("person")
  .select((eb) => [
    eb
      .selectFrom("pet")
      .whereRef("pet.owner_id", "=", "person.id")
      .select(eb => eb.fn.jsonAgg("pet.name" as unknown as "pet") as unknown as RawBuilder<ExtractTypeFromStringReference<DB,"pet","name">[] | null>)
      .as("person_pets"),
  ])
  .executeTakeFirstOrThrow()
import { RawBuilder, ExtractTypeFromStringReference } from 'kysely'

const result = await db
  .selectFrom('person')
  .innerJoin('pet', 'pet.owner_id', 'person.id')
  .select((eb) =>
    (eb.fn.jsonAgg("pet.name" as unknown as "pet") as unknown as RawBuilder<ExtractTypeFromStringReference<DB,"pet","name">[] | null>).as('person_pets')
  )
  .executeTakeFirstOrThrow()

In case you don't want the aggregated property to be a union with null, you cannot use subqueries (first code-sample above) but have to use join instead and wrap it in a coalesce function:

import { sql, RawBuilder, ExtractTypeFromStringReference } from 'kysely'

const result = await db
  .selectFrom('person')
  .innerJoin('pet', 'pet.owner_id', 'person.id')
  .select((eb) =>
    eb.fn.coalesce(
      (eb.fn.jsonAgg("pet.name" as unknown as "pet") as unknown as RawBuilder<ExtractTypeFromStringReference<Database,"pet","name">[] | null>),
      sql<never[]>`'[]'`,
    ).as('person_pets')
  )
  .groupBy("person.id")
  .executeTakeFirstOrThrow()

This is not because of some typings in Kysely, but coalesce doesn't work on subqueries on my postgres version - at least not on my installation (PostgreSQL 14.13)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api Related to library's API enhancement New feature or request postgres Related to PostgreSQL typescript Related to Typescript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants