48

all I want to do is send a query like

SELECT * FROM table WHERE col IN (110, 130, 90);

So I prepared the following statement

SELECT * FROM table WHERE col IN (:LST);

Then I use

sqlite_bind_text(stmt, 1, "110, 130, 90", -1, SQLITE_STATIC);

Unfortunately this becomes

SELECT * FROM table WHERE col IN ('110, 130, 90');

and is useless (note the two additional single quotes). I already tried putting extra ' in the string but they get escaped. I didn't find an option to turn off the escaping or prevent the text from being enclosed by single quotes. The last thing I can think of is not using a prepared statement, but I'd only take it as last option. Do you have any ideas or suggestions?

Thanks

Edit:

The number of parameters is dynamic, so it might be three numbers, as in the example above, one or twelve.

13 Answers 13

40

You can dynamically build a parameterized SQL statement of the form

 SELECT * FROM TABLE WHERE col IN (?, ?, ?)

and then call sqlite_bind_int once for each "?" you added to the statement.

There is no way to directly bind a text parameter to multiple integer (or, for that matter, multiple text) parameters.

Here's pseudo code for what I have in mind:

-- Args is an array of parameter values
for i = Lo(Args) to Hi(Args)
   paramlist = paramlist + ', ?'

sql = 'SELECT * FROM TABLE WHERE col IN (' + Right(paramlist, 3)  + ')'

for i = Lo(Args) to Hi(Args)
  sql_bind_int(sql, i, Args[i]

-- execute query here.
Sign up to request clarification or add additional context in comments.

6 Comments

Thanks for your reply. I didn't mention it in my original post (just edited), but the number of parameters is dynamic. I actually still take your answer as "not possible", which means I simply can't use a prepared statement.
No, Sebastian, my suggestion is do dynamically build an SQL statement with a variable number of parameters and then call sqlite_bind_in in a loop for each parameter. It should work for you. I'll add pseudo-code to the answer.
To be able to call sql_bind_* one needs to call sql_prepare first. I don't see the point in dynamically build a statement, prepare it, use bind, execute and finalize, instead of inserting the parameters directly when building the query, prepare, execute and finalize. Either way I'd need to call sql_prepare every time I want to execute the query and that's what I'd like to save and do only once.
No, you will definitely have to prepare the statement each time. There's no way the statement could be prepared without knowing how many values will be in the IN () clause (that information, presumably, contributes to the execution plan).
I came across this trying to do the same thing. Quite horrible in my opinion that the language does not support it, at least as an extension.
|
11

I just faced this question myself, but answered it by creating a temporary table and inserting all the values into that, so that I could then do:

SELECT * FROM TABLE WHERE col IN (SELECT col FROM temporarytable);

2 Comments

Do you have any performance comparisons? I can imagine that the overhead of creating a temporary table outweighs the execution plan creation. That however, will depend on the table sizes.. I guess one could keep the temporary table around and clear and re-use it every time (proper synchronization assumed). Still it'd be filling vs execution plan
Another motivation for not dynamically generating SQL is that if all the SQL that could be prepared is statically known at compile time, then SQL injection becomes (nearly) impossible, which is likely to result in that being an imposed-from-on-high policy for some cases.
8

Even simpler, build your query like this:

"SELECT * FROM TABLE WHERE col IN (" + ",".join(["?"] * len(lst)) + ")"

6 Comments

What language is that? Java?!
it's python. What language are you looking for?
This kind of solution should always come with a security warning: if there is any chance at all that lst is not absolutely clean data (i.e. it's been touched by a user...) then the system may be vulnerable to SQL injection with such methods.
It also forces recompilation of the SQL on each call. Maybe the system will detect that it can use an internal cache… or maybe it won't, but you're making things hard for it. Bound parameters are strongly recommended for both performance and security reasons.
@Stabledog: This example is using prepared statements. The SQL only depends on the length of the list, not the contents. It might be a DOS vector, but it's not an arbitrary SQL injection vector.
|
8

One solution (which I haven't tried yet in code, but only on the SQLite shell) is to use json_each function from SQLite.

So you could do something like:

SELECT * FROM table
WHERE col IN (SELECT value FROM json_each(?));

The caveat is that you'd have to manually assemble a valid JSON array with the values you're trying to bind.

1 Comment

this works very well and fast
6

Depending on your build of sqlite (it's not part of the default build), you may be able to use:

SELECT * FROM table WHERE col IN carray(?42);

and then bind ?42 using (assuming the C API):

int32_t data[] = {110, 130, 90};
sqlite3_carray_bind(
    stmtPtr, 42,
    data, sizeof(data)/sizeof(data[0]),
    CARRAY_INT32, SQLITE_TRANSIENT);

I haven't actually tested that, I just read the docs: https://sqlite.org/carray.html

Comments

4

You cannot pass an array as one parameter, but you can pass each array value as a separate parameter (IN (?, ?, ?)).

The safe way to do this for dynamic number parameters (you should not use string concatenation, .format(), etc. to insert the values themselves into the query, it can lead to SQL injections) is to generate the query string with the needed number of ? placeholders and then bind the array elements. Use array concatenation or spread syntax (* or ... in most languages) if you need to pass other parameters too.

Here is an example for Python 3:

c.execute('SELECT * FROM TABLE WHERE col IN ({}) LIMIT ?'
       .format(', '.join(['?'] * len(values))), [*values, limit])

1 Comment

If you have a lot of values, put them into a temporary table first (which resides in memory in SQLite), and then use that table in the main query.
3

A much simpler and safer answer simply involves generating the mask (as opposed to the data part of the query) and allowing the SQL-injection formatter engine to do its job.

Suppose we have some ids in an array, and some cb callback:

/* we need to generate a '?' for each item in our mask */
const mask = Array(ids.length).fill('?').join();

db.get(`
  SELECT *
    FROM films f
   WHERE f.id
      IN (${mask})
`, ids, cb);

Comments

0

Working on a same functionality lead me to this approach: (nodejs, es6, Promise)

    var deleteRecords = function (tblName, data) {
        return new Promise((resolve, reject) => {
            var jdata = JSON.stringify(data);
            this.run(`DELETE FROM ${tblName} WHERE id IN (?)`, jdata.substr(1, jdata.length - 2), function (err) {
                err ? reject('deleteRecords failed with : ' + err) : resolve();
            });
        });
    };

Comments

0

this works fine aswell (Javascript ES6):

let myList = [1, 2, 3];
`SELECT * FROM table WHERE col IN (${myList.join()});`

2 Comments

It is a really bad idea to manually inject data into a query at runtime. It's a major security risk that leaves you open to a SQL-injection attack. netsparker.com/blog/web-security/sql-injection-cheat-sheet
it is a problem only if the myList is user-supplied. In any case, one can check myList before adding it to the query.
0

You can try this

RSQLite in R:
lst <- c("a", "b", "c")

dbGetQuery(db_con, paste0("SELECT * FROM table WHERE col IN (", paste0(shQuote(lst), collapse=", ") , ");"))

Comments

0

My solution for node (ES6, Promises):

let records = await db.all(`
    SELECT * FROM table
    WHERE (column1 = ?) and column2 IN ( ${[...val2s].fill('?').join(',')} )
`, [val1, ...val2s])

Works with a variable number of possible values.

This uses sqlite-async but you can modify it for the callback style version trivially.

Comments

0

if you are using Python the easiest way to handle this, in practice, is to create a local function that tests against a string value of the list (which can be passed as a bind variable).

I used this when providing "Query By Example" functionality in a Python GUI app.

pros: can use common approach in parsing and building the SQL across entries as I would when parsing LIKE xxx and > xxx etc

just one extra call to set the function up - either at connection time
or if the function call is detected in the created sql

cons: function needs to parse string list for each row. This is bad if the query is running against a large table

embedded commas, blanks and other similar stuff may be difficult to handle

For example

  1. user enters IN 18C, 356, 013 into Account field in application
  2. application creates sql with ... WHERE inz( Account , ? ) ...
  3. application creates string bind value 18C, 356, 013
  4. application issues <sqlite3.connection>.create_function("inz", 2, inz ) to bind local python function inz (see below) to sqlite function inz.
  5. application issues query

the coding for the inz function is as follows

def inz(  val , possibles ) :
    """implements the  IN list function allowing one bind variable
    
       use <sqlite3.connection>.create_function("inz", 2, inz )
       and  ensure that the bind variable is a comma delimited list 
       in string form (without quotes)
       
       matches can be string or integer but do not allow for leading
       or trailing spaces or  contained commas, quotes etc or floating points

    """
    
    poss  = [ x.strip() for x in possibles.split(',') ] 
    
    if val in poss :
        return True  
    if isinstance(  val, int ) :
        ipos = [ int(x) for x in poss if x.isdecimal() ] 
        if val in ipos :
            return True 
            
    return False

Comments

-2

For example, if you want the sql query:

select * from table where col in (110, 130, 90)

What about:

my_list = [110, 130, 90]
my_list_str = repr(my_list).replace('[','(').replace(']',')') 
cur.execute("select * from table where col in %s" % my_list_str )

Comments

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.