41

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.

Sebastian
  • 2,109
  • 1
  • 20
  • 15

13 Answers13

32

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.
Larry Lustig
  • 49,320
  • 14
  • 110
  • 160
  • 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. – Sebastian Jan 26 '11 at 00:16
  • 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. – Larry Lustig Jan 26 '11 at 00:51
  • 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. – Sebastian Jan 26 '11 at 08:06
  • 2
    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). – Larry Lustig Jan 26 '11 at 12:46
  • 1
    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. – CashCow Feb 18 '11 at 14:16
  • A suggestion, i didn't try out yet. What happens if you leave out the round brackets? 'SELECT * FROM table WHERE col IN :LST;' – McPeppr Jul 04 '16 at 10:02
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);
hatfinch
  • 3,095
  • 24
  • 35
  • 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 – Sebastian Jun 03 '12 at 14:13
7

Even simpler, build your query like this:

"SELECT * FROM TABLE WHERE col IN (" + ",".join(["?"] * len(lst)) + ")"
Will Manley
  • 2,340
  • 22
  • 17
xtin
  • 87
  • 1
  • 1
  • What language is that? Java?! – Sebastian Mar 29 '13 at 17:07
  • 5
    it's python. What language are you looking for? – xtin Apr 26 '13 at 14:53
  • 5
    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. – Stabledog Jan 28 '21 at 22:02
  • 1
    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. – Donal Fellows Jan 29 '21 at 12:07
  • 5
    @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. – Will Manley Feb 08 '21 at 22:39
  • @DonalFellows: PySQLite includes a statement cache, so as long as there aren't too many different list lengths this should be fine. – Will Manley Feb 08 '21 at 22:41
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

BCS
  • 75,627
  • 68
  • 187
  • 294
3

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])
Alex P.
  • 3,697
  • 9
  • 45
  • 110
  • 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. – Donal Fellows Jan 29 '21 at 12:09
3

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.

burdiyan
  • 315
  • 2
  • 12
1

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);
jasonseminara
  • 436
  • 4
  • 14
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();
            });
        });
    };
Reza Afzalan
  • 5,646
  • 3
  • 26
  • 44
0

this works fine aswell (Javascript ES6):

let myList = [1, 2, 3];
`SELECT * FROM table WHERE col IN (${myList.join()});`
Moritz
  • 9
  • 1
  • 7
    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. https://www.netsparker.com/blog/web-security/sql-injection-cheat-sheet/ – jasonseminara Jan 01 '18 at 21:52
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=", ") , ");"))
Suraj Kumar
  • 5,547
  • 8
  • 20
  • 42
Ksenia
  • 1
  • 1
  • 1
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.

proto-n
  • 545
  • 6
  • 19
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

-1

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 )
pgalilea
  • 261
  • 2
  • 5