Skip to content

[FEATURE]: Allow passing in an array into functions #1289

Description

@some1chan

Describe what you want

Currently, the default behavior of Drizzle will turn arrays into either its singular element, or a record. This behavior is unintuitive, when you're trying to pass in an array in a PostgreSQL function.

For example, let's say you create a Postgres function called insert_vote...

CREATE OR REPLACE FUNCTION public.insert_vote(
    IN poll_id uuid,
    IN voting_user_id uuid,
    IN choice_ids uuid[]
) RETURNS TABLE (
    status text,
    existing_choices uuid[],
    votes_created uuid[],
    votes_deleted uuid[],
	max_votes smallint
) AS $BODY$
BEGIN
	-- Left empty
END;
$BODY$ LANGUAGE plpgsql;

...and you wish to interact with it with Drizzle.

import "dotenv/config";
import postgres from 'postgres';
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';

const client = postgres({
    hostname: process.env.POSTGRES_HOST,
    database: process.env.POSTGRES_DB,
    username: process.env.POSTGRES_USER,
    password: process.env.POSTGRES_PASSWORD,
    port: process.env.POSTGRES_PORT
        ? Number(process.env.POSTGRES_PORT)
        : 5432,
});
const db = drizzle(client, { schema, logger: new DefaultLogger() });

// IDs are the same, but this won't matter for our example. These are also ULIDs, and not UUIDs,
// but this also won't matter as Postgres doesn't do any validation on these.
const pollHexId = "018abb841f4ae4257adbba232b34f848";
const userId = "018abb841f4ae4257adbba232b34f848";
const choiceIds = ["018abb841f4ae4257adbba232b34f848"];
const sqlQuery = sql`SELECT public.insert_vote(${pollHexId}::uuid, ${userId}::uuid, ${choiceIds}::uuid[]);`;
console.log((await import('util')).inspect(sqlQuery, undefined, 5));

const insertVote = await db.execute(sqlQuery);
console.log("Finished!");

What you'll see in your logs is this:

// Note that the following sql`` structure is correct
SQL {
  queryChunks: [
    StringChunk { value: [ 'SELECT public.insert_vote(' ] },
    '018abb841f4ae4257adbba232b34f848',
    StringChunk { value: [ '::uuid, ' ] },
    '018abb841f4ae4257adbba232b34f848',
    StringChunk { value: [ '::uuid, ' ] },
    [ '018abb841f4ae4257adbba232b34f848' ],
    StringChunk { value: [ '::uuid[]);' ] }
  ],
  decoder: { mapFromDriverValue: [Function: mapFromDriverValue] },
  shouldInlineParams: false
}
// However, this isn't, as the 3rd parameter becomes a non-array.
Query: SELECT public.insert_vote($1, $2, ($3)); -- params: ["018abb841f4ae4257adbba232b34f848", "018abb841f4ae4257adbba232b34f848, "018abb841f4ae4257adbba232b34f848"]
PostgresError: malformed array literal: "018ac05623d3b7dec2b69bb78c8e0844"

For some reason, our array of our one item becomes our one item. We'll run into the record type if our array has more than one element.
You can get the weird record behavior by just adding another uuid into the array.

// ...
const choiceIds = ["018abb841f4ae4257adbba232b34f848", "018abb841f4ae4257adbba232b34f848"];
// ...
// This is using inlineParams() for when I was testing. Error is the same for either-or.
Query: SELECT public.insert_vote('018abb841f4ae4257adbba232b34f848'::uuid, '018abb841f4ae4257adbba232b34f848'::uuid, ('018abb841f4ae4257adbba232b34f848', '018abb841f4ae4257adbba232b34f848')::uuid[]);
PostgresError: cannot cast type record to uuid[]
// Hope is that it would've turned into this:
// SELECT public.insert_vote(
//    '018abb841f4ae4257adbba232b34f848'::uuid,
//    '018abb841f4ae4257adbba232b34f848'::uuid,
//    '{018abb841f4ae4257adbba232b34f848, 018abb841f4ae4257adbba232b34f848}'::uuid[]);

This isn't very useful for us in our use case here.

Since Drizzle doesn't support the syntax around arrays passed to functions, we'll need to do this workaround:

import "dotenv/config";
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
import { DefaultLogger, SQL, sql } from "drizzle-orm";

const client = postgres({
	hostname: process.env.POSTGRES_HOST,
	database: process.env.POSTGRES_DB,
	username: process.env.POSTGRES_USER,
	password: process.env.POSTGRES_PASSWORD,
	port: process.env.POSTGRES_PORT ? Number(process.env.POSTGRES_PORT) : 5432,
});
const db = drizzle(client, { logger: new DefaultLogger() });
const pollHexId = "018abb841f4ae4257adbba232b34f848";
const userId = "018abb841f4ae4257adbba232b34f848";
const choiceIds = [
	"018abb841f4ae4257adbba232b34f848",
	"018abb841f4ae4257adbba232b34f848",
];

const sqlQuery =
	sql`SELECT public.insert_vote(${pollHexId}::uuid, ${userId}::uuid, `.inlineParams();

// Main code starts here
const sanitizedChoiceIds: SQL<unknown>[] = [sql`'{`];
const rawChoiceIds: SQL<unknown>[] = [];
for (const choiceId of choiceIds) {
	// Sanitization happens with sql`${choiceId}`
	const sanitizedChoiceIdChunk = sql`${choiceId}`.queryChunks[1];
	// queryChunks = [
	// 	StringChunk{ value: [ '' ] },
	//  '018abb841f4ae4257adbba232b34f848',
	//  'StringChunk { value: [ '' ] }
	// ]
	rawChoiceIds.push(sql.raw(sanitizedChoiceIdChunk!.toString()));
}
sanitizedChoiceIds.push(sql.join(rawChoiceIds, sql`, `));
sanitizedChoiceIds.push(sql`}'::uuid[]`);
const sanitizedChoiceIdsQuery = sql.join(sanitizedChoiceIds);
sqlQuery.append(sanitizedChoiceIdsQuery);
sqlQuery.append(sql`)`);
// ...and now we have our query!

console.log((await import("util")).inspect(sqlQuery, undefined, 8));
const insertVote = await db.execute(sqlQuery);
console.log((await import("util")).inspect(insertVote, undefined, 4));

console.log("Finished!");

This will finally output the right query:

// Ignoring the extremely long SQL query object inspect...
Query: SELECT public.insert_vote('018abb841f4ae4257adbba232b34f848'::uuid, '018abb841f4ae4257adbba232b34f848'::uuid, '{018abb841f4ae4257adbba232b34f848, 018abb841f4ae4257adbba232b34f848}'::uuid[])

Obviously, this isn't ideal. I'd personally love to see support for passing arrays into functions in Drizzle, as this will streamline the above code to simply be our original example.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions