jawj / zapatos

Zero-abstraction Postgres for TypeScript: a non-ORM database library
https://jawj.github.io/zapatos/
Other
1.3k stars 46 forks source link

Is there a better way of using db.param with a plpgsql function expecting VARIADIC text[]? #145

Closed faenigma closed 1 year ago

faenigma commented 1 year ago

HI, just discovered this library and I'm really enoying it - I'm leveraging it in an effort I'm performing to modernize an express api (namely by moving it to typescrpit) that's being used on a a DB-first project with a number of plpgsql functions.

I have multiple instances of functions that need to pass in a variadic text array, e.g., mytable.myfunction(VARIADIC _my_input text[]). I'd like to avoid using db.raw, but I'm not seeing how else to pass in something like an array of strings as a parameter.

Currently I'm using the following methodology (plus some pre-validation with zod):

    const mystrings = validQuery.mystrings.map(mystring => `'${mystring}'`).join(',');
    const query = db.sql`
    select * from mytable.myfunction(${db.raw(mystrings)})
    `;

as i need the final format as

   select * from mytable.myfunc('mystring1','mystring2','mystring3')
jawj commented 1 year ago

Interesting question, thanks. Could you give a minimal example of what SQL you're trying to run, and I'll take a look?

faenigma commented 1 year ago

Sure - the functions more or less all follow a format like this -

CREATE OR REPLACE FUNCTION mytable.myfunction(VARIADIC _input_string_array text[])

And that _input_val array is used in a few different contexts, most commonly for an ANY somewhere deep inside of the function -

AND table.column = ANY(_input_string_array)

with pg-promose, we followed the :list syntax , e.g., select * from mytable.myfunc($/input_string_array:list/)

where an array of strings could be provided as the input_string_array

jawj commented 1 year ago

The way I handle this sort of thing within the library is with a utility function mapWithSeparator, which looks like this:

const mapWithSeparator = <TIn, TSep, TOut>(
  arr: readonly TIn[],
  separator: TSep,
  cb: (x: TIn, i: number, a: readonly TIn[]) => TOut
): (TOut | TSep)[] => {

  const result: (TOut | TSep)[] = [];
  for (let i = 0, len = arr.length; i < len; i++) {
    if (i > 0) result.push(separator);
    result.push(cb(arr[i], i, arr));
  }
  return result;
};

You could adapt that to your needs as:

const paramsWithComma = (arr: any[]): (db.Parameter | db.SQLFragment)[] =>
  mapWithSeparator(arr, db.sql`, `, x => db.param(x));

And then use it like:

const
  args = ['a', 'b', 'c'],
  result = await db.sql`SELECT variadicfunction(${paramsWithComma(args)})`.run(pool);

I appreciate the library isn't giving you a lot of help here, but does that work? I guess one thing we could do would be to expose the mapWithSeparator utility function.

faenigma commented 1 year ago

Sorry - just now back to tackling this. That worked perfectly, thanks! And yeah I think that would be a good enough fix, though not sure how often people run into this problem haha.

jawj commented 1 year ago

OK — great. :)

jawj commented 1 year ago

The next version of Zapatos will export mapWithSeparator (https://github.com/jawj/zapatos/commit/609bc9490339f7a1790917b9403dba51d7645089).