0

I'm trying to execute a SQL query from a C extension built for PostgreSQL, using the Server Programming Interface (SPI). The query should create a new schema with quite a large number of tables. (Basically it should setup a workspace for users to work in.) But since the user should be able to create multiple workspaces, I don't know the schema name when writing the script. So I need a way to supply this at runtime. But I can't get it to work.

I'm trying to do this by using SPI_execute_with_args as the documentation states the following:

SPI_execute_with_args executes a command that might include references to externally supplied parameters. The command text refers to a parameter as $n, and the call specifies data types and values for each such symbol. read_only and count have the same interpretation as in SPI_execute.

The main advantage of this routine compared to SPI_execute is that data values can be inserted into the command without tedious quoting/escaping, and thus with much less risk of SQL-injection attacks.

The SQL script looks like the following (If I replace the $1 with a real schema name manually and run it as a normal script, everything works as it should):

CREATE SCHEMA $1;
ALTER SCHEMA $1 OWNER TO some_user;

CREATE FUNCTION $1.foo() ...

CREATE TABLE $1.bar ...
...

But now I want to run it from the C code, and as the documentation lacks any working examples on SPI, I had to google around to find anything that could guide me further. And I found this example on SO where the function looks like this:

...

Datum 
foo(PG_FUNCTION_ARGS)
{
    int ret;
    Datum args[1];
    Oid argtypes[1] = { INT4OID };
    Datum result;
    bool isnull;

    SPI_connect();

    args[0] = PG_GETARG_INT32(0);

    /* ensure expected result type by casting */
    ret = SPI_execute_with_args("SELECT ($1 + 10)::int", 
                                   1, argtypes, args, NULL,
                                   true, 1);

    Assert(SPI_processed == 1);

    result = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull);
    Assert(!isnull);

    SPI_finish();

    PG_RETURN_DATUM(result);
}

...

This works as it should (replaces $1 with a number entered as a parameter).

But as soon I start to modify it to work with my own query, everything breaks. I can't even make it work with only the first line of the query.

I have, for the record, also tried to just run a simple SELECT '$1' query and replace it with various variables. But nothing else than the example works. Either the server crashes, returns invalid syntax at $1 or just returns $1 as the answer.


If I'm right it seems like it matters where and what you want to replace $1 with. And that SPI doesn't just do a "find and replace" on the $1?

I have tried some different OID:s when testing various variable types, like: ANYOID, CSTRINGOID, CHAROID, REGNAMESPACEOID, TEXTOID etc. And I have tried to both send the variable as a pure char array and as a pointer to a text block allocated with SPI_palloc() or palloc(). But no success...

Example code that I've put together from the examples and documentation I've found:

PG_FUNCTION_INFO_V1(foobar);
Datum foobar(PG_FUNCTION_ARGS)
{
    Datum arguments[1];
    Oid argument_types[1] = { ANYOID };
    Datum result;
    bool isnull;
    arguments[0] = "some_text";

    SPI_connect();
    SPI_execute_with_args("SELECT '$1'", 1, argument_types, arguments, NULL, false, 0);
    result = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull);
    SPI_finish();

    PG_RETURN_DATUM(result);
}

When running this code I get the following result:

SELECT foobar();
 foobar
--------
 $1
(1 row)

I'm not sure that this is the best way to do this, but even if it isn't, it would be nice to know more of how this SPI function works as I will need it further into the project.

Does anybody have any working examples to this or something to push me into the right direction?

2
  • Impossible to answer without seeing your code. Commented Oct 18, 2018 at 10:39
  • @LaurenzAlbe So I've added an example of my own code and what it results in. Is there anything else that I should add/remove to make my question easier to answer? Commented Oct 18, 2018 at 11:46

1 Answer 1

1

The $1 in your SELECT '$1' is inside single quotes, so it is a string literal and not a parameter.

Use the following instead:

SELECT $1

Note that you can use a parameter where you could also use a literal of the same type, but you cannot use a parameter for an identifier like a table or column name.

If you need a variable in such a place, you'll have to construct a query string using snprintf.

To avoid SQL injection, use quote_identifier from utils/builtins.h.


Here is a fixed version of your code:

#include "postgres.h"
#include "fmgr.h"
#include "catalog/pg_type.h"
#include "executor/spi.h"
#include "utils/builtins.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(foobar);
Datum foobar(PG_FUNCTION_ARGS)
{   
    Datum arguments[1];
    Oid argument_types[1] = { TEXTOID };
    char *res;
    bool isnull;
    Datum result;
    /* for when we don't want to use the SPI context */
    MemoryContext context = CurrentMemoryContext;

    arguments[0] = CStringGetTextDatum("some_text");

    SPI_connect();

    SPI_execute_with_args("SELECT $1", 1, argument_types, arguments, NULL, false, 0); 

    result = SPI_getbinval(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1, &isnull);

    res = MemoryContextStrdup(context, TextDatumGetCString(result));

    SPI_finish();

    PG_RETURN_TEXT_P(CStringGetTextDatum(res));
}
Sign up to request clarification or add additional context in comments.

4 Comments

When I remove the single quotes from the SELECT $1 the server crashes with the following response: server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. The connection to the server was lost. Attempting reset: Failed.
The main reason for me to do this in C is that I will have a lot of other code that will require C. And I thought that it would be best to have everything in 1 place. And that it is pre-compiled, which is a pro for speed. In this case however it will probably not do that much for speed as it is just a usual query being run.
Alright, I was playing around a bit and tried with CSTRINGOID instead of ANYOID. Which resulted in:"ome_text" and if I try with different strings it always removes the first character. Any clue why?
Ok, I have added a running version of your code to the answer. You'd also need to check the return codes!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.