Shaul Behr
Shaul Behr

Reputation: 38003

How to do variable substitution in plpgsql?

I've got a bit of complex sql code that I'm converting from MSSql to Postgres (using Entity Framework Core 2.1), to deal with potential race conditions when inserting to a table with a unique index. Here's the dumbed-down version:

const string QUERY = @"
DO
$$
    BEGIN
        insert into Foo (Field1,Field2,Field3)
        values (@value1,@value2,@value3);
    EXCEPTION WHEN others THEN
        -- do nothing; it's a race condition
    END;
$$ LANGUAGE plpgsql;

select *
from Foo
where Field1 = @value1
and Field2 = @value2;
";

return DbContext.Foos
                .FromSql(QUERY,
                    new NpgsqlParameter("value1", value1),
                    new NpgsqlParameter("value2", value2),
                    new NpgsqlParameter("value3", value3))
                .First();

In other words, try to insert the record, but don't throw an exception if the attempt to insert it results in a unique index violation (index is on Field1+Field2), and return the record, whether it was created by me or another thread.

This concept worked fine in MSSql, using a TRY..CATCH block. As far as I can tell, the way to handle Postgres exceptions is as I've done, in a plpgsql block.

BUT...

It appears that variable substitution in plpgsql blocks doesn't work. The code above fails on the .First() (no elements in sequence), and when I comment out the EXCEPTION line, I see the real problem, which is:

Npgsql.PostgresException : 42703: column "value1" does not exist

When I test using regular Sql, i.e. doing the insert without using a plpgsql block, this works fine.

So, what is the correct way to do variable substitution in a plpgsql block?

Upvotes: 1

Views: 2450

Answers (1)

Alex
Alex

Reputation: 14618

The reason this doesn't work is that the body of the DO statement is actually a string, a text. See reference

$$ is just another way to delimit text in postgresql. It can be just as well be replaced with ' or $somestuff$.

As it is a string, Npgsql and Postgresql have no reason to mess with @value1 in it.

Solutions? Only a very ugly one, so not using this construction, as you're not able to pass it any values. And messing with string concatenation is no different than doing concatenation in C# in the first place.

Alternatives? Yes!

You don't need to handle exceptions in plpgsql blocks. Simply insert, use the ON CONFLICT DO NOTHING, and be on your way.

INSERT INTO Foo (Field1,Field2,Field3)
VALUES (@value1,@value2,@value3)
ON CONFLICT DO NOTHING;

select *
from Foo
where Field1 = @value1
and Field2 = @value2;

Or if you really want to keep using plpgsql, you can simply create a temporary table, using the ON COMMIT DROP option, fill it up with these parameters as one row, then use it in the DO statement. For that to work all your code must execute as part of one transaction. You can use one explicitly just in case.

The only ways to pass parameters to plpgsql code is via these 2 methods:

  1. Declaring a function, then calling it with arguments
  2. When already inside a plpgsql block you can call:
    EXECUTE $$ INSERT ... VALUES ($1, $2, $3); $$ USING 3, 'text value', 5.234;




End notes: As a fellow T-SQL developer who loved its freedom, but transitioned to Postgresql, I have to say that the BIG difference is that on one side there's T-SQL which gives the power, and on the other side it's a very powerful Postgresql-flavored SQL. plpgsql is very rarely warranted. In fact, in a code base of megabytes of complex SQL stuff, I can rewrite pretty much every plpgsql code in SQL. That's how powerful it really is compared to MSSQL-flavored SQL. It just takes some getting used to, and befriending the very ample documentation. Good luck!

Upvotes: 2

Related Questions