DEV Community

Stephen Charles Weiss
Stephen Charles Weiss

Posted on • Originally published at stephencharlesweiss.com on

Using COALESCE And Dynamic Updates To Multiple Records In Postgres

I wanted to update multiple fields on multiple records and handle the case that not all of the information is present in the update for each record.

Basically, I want to patch not put and when it gets to Postgres, I don’t want those empty values to be set to null.

A regular UPDATE, however, would do exactly that - overwrite my values with null if I didn’t supply them. Fortunately, Postgres comes with COALESCE which can accommodate this sort of situation. Let’s take a look.

I’ll use the same example as I did Using Typescript’s Pick to Improve Communication and Decrease Maintenance

A refresher, the table is defined as:

interface IMyTable { id: string foreign_id: string name: string is_enabled: boolean is_custom: boolean display_order?: number name_en: string name_sp?: string name_fr?: string description_en?: string description_sp?: string description_fr?: string } 

The columns I’m looking to update again are is_enabled and display_order

An example request body for the patch might be:

[ { "id": "427001", "is_enabled": true }, { "id": "427002", "display_order": 2 }, { "id": "427003", "is_enabled": true, "display_order": 3 } ] 

Single Record Update

If I was just trying to update one record at a time, I would have more options about conditional statements to append to my query.

async changeMyTable(proposal: readonly MyTableProposal): Promise<...> { const updateQuery = SQL` WITH proposed_vals (id, is_enabled, display_order) AS (VALUES (${proposal.id}, ${proposal.is_enabled}, ${proposal.display_order})` UPDATE my_table AS t SET `; if( proposal.is_enabled ) updateQuery.append(SQL`is_enabled = COALESCE( CAST (p.is_enabled as bool), CAST(t.is_enabled as bool) )`) if( proposal.display_order ) updateQuery.append(SQL`, display_order = COALESCE( CAST (p.display_order as int4), CAST (t.display_order as int4) )`) updateQuery.append(SQL` FROM proposed_vals AS p WHERE t.id = p.id and t.end_version is null RETURNING t.* `); const data = await this._pool.query(updateQuery); return data.rows; } 

NB : this will throw a syntax error if display_order is present without an is_enabled

Handling Multiple Records With Ambiguity

So, how could this work with multiple records?

With multiple records, I’m using the update...with syntax:

This is where the COALESCE statement comes in. Instead of having to understand in advance if the element is present, we can use the initial value as a fail-safe. If the field is not present, we will use the value that’s in place.

My initial attempt to use COALESCE led to:

async changeMyTable(proposal: readonly MyTableProposal[]): Promise<...> { const valuesToUpdate = helpers.SQLJoinStatement( /* ... */ ) const updateQuery = SQL`WITH proposed_vals (id, is_enabled, display_order) AS (VALUES ` .append(valuesToUpdate) .append(SQL` ) UPDATE my_table AS t SET is_enabled = COALESCE( p.is_enabled, t.is_enabled ) , display_order = COALESCE( p.display_order, t.display_order ) FROM proposed_vals AS p WHERE t.id = p.id and t.end_version is null RETURNING t.* ;`); const data = await this._pool.query(updateQuery); return data.rows; } 

This, however, threw the error: 500 COALESCE types text and boolean cannot be matched.

Researching, I found that I could get past this problem with a CAST to ensure the type. I felt comfortable doing this because I knew my proposal was typed appropriately and I’d built the table.

async changeMyTable(proposal: readonly MyTableProposal[]): Promise<...> { const valuesToUpdate = helpers.SQLJoinStatement( /* ... */ ) const updateQuery = SQL`WITH proposed_vals (id, is_enabled, display_order) AS (VALUES ` .append(valuesToUpdate) .append(SQL` ) UPDATE my_table AS t SET is_enabled = COALESCE( CAST (p.is_enabled AS BOOL), CAST(t.is_enabled AS BOOL) ) , display_order = COALESCE( CAST (p.display_order AS INT4), CAST (t.display_order AS INT4) ) FROM proposed_vals AS p WHERE t.id = p.id AND t.end_version IS NULL RETURNING t.* ;`); const data = await this._pool.query(updateQuery); return data.rows; } 

With another set of eyes, I was able to refine this again. The issue is that the values in my proposed_vals implicitly have a type of text or number. If I wanted it to be something separate, I needed to alert Postgres of that.

Instead of the CAST (value AS type) I can use the more terse notation of the double colon :: and apply it only to the proposed values.

UPDATE metadata_lookupvalues AS luv SET is_enabled = COALESCE( p.is_enabled::BOOL, luv.is_enabled ) , display_order = COALESCE( p.display_order::INT4, luv.display_order ) FROM proposed_vals AS p WHERE luv.id = p.id AND luv.end_version IS NULL RETURNING luv.* 

Related

Top comments (0)