kysely-org / kysely

A type-safe typescript SQL query builder
https://kysely.dev
MIT License
10.8k stars 275 forks source link

Passing context to plugins on a query by query basis #627

Open jordanjoyce opened 1 year ago

jordanjoyce commented 1 year ago

Have a use case where I need to stop certain columns from transforming to snake case. I've forked the CamelCasePlugin and got the functionality working however to get this to work on a query by query basis I currently need to use the following pattern

qb.withoutPlugins().withPlugin(new CamelCasePlugin({}, noTransformCols]))

I've recently migrated from Knex and they had a queryContext function that I used for similar functionality. Wondering if there's an easier way to do this that I've missed or not.

Thanks

igalklebanov commented 1 year ago

Hey 👋

To get this to work per-query, all you have to do is use .withPlugin(new CamelCasePlugin({}, noTransformCols)). I don't understand, from your post, why you'd need .withoutPlugins().

jordanjoyce commented 1 year ago

@igalklebanov Thanks for the quick response, should have mentioned the CamelCasePlugin is used globally, so I have to use .withoutPlugins() to stop it running twice. This pattern is fine as it's only used in a few queries. Was just reaching out to make sure I wasn't missing a more obvious solution!

igalklebanov commented 1 year ago

@jordanjoyce I see. Given your usage, there is no better way right now.

@koskimas a few ideas...

1. maybe we could offer some kind of state that can be global and local (overriding globals) that all plugins and drivers can consume? this would remove the forced usage of .withoutPlugins (that can be risky - removing more than you need) or .withPlugin (no need to instantiate a new plugin instance per invocation) everywhere.

const db = new Kysely<DB>({
  // ...
  plugins: [new MyCamelCasePlugin()],
  vars: {
    // some global vars.
  }
})

db
  .$withVars({ noCamelCase: ['column_0', 'column_1'] })
  .selectFrom('table').select(['column_0', 'column_1', 'column_2'])
  .execute()

This can also facilitate prepared statements (passing names as vars and driver consuming these to prepare if not prepared with that name yet).

2. plugin dedup/override by some name.

koskimas commented 1 year ago

Don't know about this. I added something like this to objection and it opened a door for all kinds of hacks people shouldn't have done. I'm afraid adding something like this will make people add ORM features as plugins which just leads to a really really shitty ORM and a LOT of issues and feature requests for us to solve.

This is not optimal but you could do something like this

export db = new Kysely<DB>({
  dialect,
  plugins: PLUGINS
})
export const PLUGINS = [
  new CamelCasePlugin(camelCaseConfig),
  ...OTHER_PLUGINS
]

export function noCamelCaseFor(columns: string[]) {
  return <T extends SelectQueryBuilder<any, any, any>>(qb: T) {
   let qb = qb
     .withoutPlugins()
     .withPlugin(new CamelCasePlugin(camelCaseConfig), columns)

    for (const p of OTHER_PLUGINS) {
      qb = qb.withPlugin(p)
    }

    return qb
  }
}

And usage:

db.selectFrom('person')
  .select('first_name')
  .$call(noCamelCaseFor(['first_name']))
geetesh911 commented 9 months ago

Don't know about this. I added something like this to objection and it opened a door for all kinds of hacks people shouldn't have done. I'm afraid adding something like this will make people add ORM features as plugins which just leads to a really really shitty ORM and a LOT of issues and feature requests for us to solve.

This is not optimal but you could do something like this

export db = new Kysely<DB>({
  dialect,
  plugins: PLUGINS
})
export const PLUGINS = [
  new CamelCasePlugin(camelCaseConfig),
  ...OTHER_PLUGINS
]

export function noCamelCaseFor(columns: string[]) {
  return <T extends SelectQueryBuilder<any, any, any>>(qb: T) {
   let qb = qb
     .withoutPlugins()
     .withPlugin(new CamelCasePlugin(camelCaseConfig), columns)

    for (const p of OTHER_PLUGINS) {
      qb = qb.withPlugin(p)
    }

    return qb
  }
}

And usage:

db.selectFrom('person')
  .select('first_name')
  .$call(noCamelCaseFor(['first_name']))

qb does not have withoutPlugins method. Can you suggest any other way?

ethanresnick commented 7 months ago

I also have a plugin that I’d like to be able to opt-out of for individual queries. If something like $withVars is a bad idea, maybe two new query builder methods like this could be convenient?

// A new overload for withoutPlugins that returns an 
// identical query builder with just these plugins removed
// (where each plugin is identified by === equality)
withoutPlugins(plugins: readonly KyselyPlugin[]): …

// returns a qb that retains only those plugins which pass the predicate.
// Useful when the consumer doesn’t have a reference to
// the exact plugin object (including in certain mocking/testing scenarios)
filterPlugins(predicate: (it: KyselyPlugin) => boolean): …

I’m not sure if those methods are worth it, but their intention seems slightly clearer, and they could be implemented more efficiently than the version shown above where the plugins have to be added back one-by-one, creating a new qb each time.