springm has asked for the wisdom of the Perl Monks concerning the following question:

To the enlightened ones: Setting up a nice frontend for csv files I stumbled accross the following problem:
use DBI; my $dbh = DBI->connect("DBI:CSV:f_dir=/tmp"); $dbh->do("CREATE TABLE csvtable (id INTEGER, name CHAR(64))"); $dbh->do("insert into csvtable (id,name) values (1,'bi')"); $dbh->do("insert into csvtable (id,name) values (2,'ma')"); my $result = $dbh->selectall_arrayref("SELECT * FROM csvtable limit + ?,?", {}, 0, 1);
but the disdainful answer always is
SQL ERROR: Bad limit clause!
I even tried it with the newest, hottest SQL::Statement 1.15, but to no avail. Unnecessary to mention that it works fine when I embed the limit figures in the statement without placeholder...

Update Thanks for the eplanations and hints. I think I will dynamically generate the statement and carefully untaint the values used for limit and offset, as they come from a web form.

Replies are listed 'Best First'.
Re: SQL::Statement limit clause with placeholders
by jZed (Prior) on Jun 03, 2005 at 14:09 UTC
    Placeholders are not supported in the LIMIT clause, try using values.

    Update although I will certainly entertain arguments about whether they should be supported. MySQL didn't used to support them, now it sort of does but it's causing problems. Thoughts?

      AFACT, SQL::Statement can bind the placeholders prior to "generating an execution plan for the query." As each DBD differs slightly anyways (DBD::mysql and DBD::Pg anyone?!), you can advertise this as a "feature" with little developer cost.

      I've always thought that the execution plan argument against LIMIT placeholders was a bunch of hooey. The execution plan isn't be any worse because you're taking the 100th-119th records vs. full set. You still have to go retrieve the whole record set anyways. The only things I can see are

      • If your LIMIT starts at 0, you might lose an optimization if your tables are split across disks.
      • If your LIMIT starts at a higher number than your cardinality in one of the indices, you execute the query even though you can know you'll be returning an empty set.

      Those are all lost optimizations. They're not degradations of the base query's execution plan. Oracle's developers are just lazy.


      • In general, if you think something isn't in Perl, try it out, because it usually is. :-)
      • "What is the sound of Perl? Is it not the sound of a wall that people have stopped banging their heads against?"
      Just tried PostgreSQL
      $dbh2->selectall_arrayref("SELECT * FROM sessions limit ? offset ?", { +}, 2,3);
      works like a charm
      OK. Facts accepted. So what would be the best way to implement paging of records in chunks of, say, 20 per page?
        The immediate solution is to dynamically generate the SQL. This could be as simple as:
        my $sql = <<__END_SQL__; YOUR STATEMENT HERE __END_SQL__ $sql .= "LIMIT $start, $chunk_size\n"; my $sth = $dbh->prepare( $sql ); # etc.

        • In general, if you think something isn't in Perl, try it out, because it usually is. :-)
        • "What is the sound of Perl? Is it not the sound of a wall that people have stopped banging their heads against?"
        Variable interpolation?
Re: SQL::Statement limit clause with placeholders
by dragonchild (Archbishop) on Jun 03, 2005 at 14:20 UTC
    The immediate source of your problem is that SQL::Parser (the parser used by SQL::Statement) has the following subroutine:
    sub LIMIT_CLAUSE { my($self,$limit_clause) = @_; # $limit_clause = trim($limit_clause); $limit_clause =~ s/^\s+//; $limit_clause =~ s/\s+$//; return 1 if !$limit_clause; my($offset,$limit,$junk) = split /,/, $limit_clause; return $self->do_err('Bad limit clause!') if (defined $limit and $limit =~ /[^\d]/) or ( defined $offset and $offset =~ /[^\d]/ ) or defined $junk; if (defined $offset and !defined $limit) { $limit = $offset; undef $offset; } $self->{"struct"}->{"limit_clause"} = { limit => $limit, offset => $offset, }; return 1; }

    The important part is the middle. SQL::Parser demands that all parameters to a LIMIT clause be numeric. This is inline with most (but not all) DBD::'s. The major exception is DBD::mysql, which allows parameters in LIMIT clauses (with certain restrictions).

    If you feel that this is a useful feature, provide a few test cases for the author. Better is the patch as well, but the test cases are a minimum.


    • In general, if you think something isn't in Perl, try it out, because it usually is. :-)
    • "What is the sound of Perl? Is it not the sound of a wall that people have stopped banging their heads against?"
Re: SQL::Statement limit clause with placeholders
by cees (Curate) on Jun 03, 2005 at 14:16 UTC

    Most databases do not support placeholders in the limit clause (I believe MySQL, as always, is at least one exception to the rule). It is the same with the ORDER BY and GROUP BY clauses. Placeholders are for column values.

    My guess is that DBD::CSV is sticking with the norm here.

Re: SQL::Statement limit clause with placeholders
by punkish (Priest) on Jun 03, 2005 at 14:18 UTC
    From the DBI docs --

    With most drivers, placeholders can't be used for any element of a statement that would prevent the database server from validating the statement and creating a query execution plan for it.

    I believe the LIMIT clause is applied _after_ the data are retrieved, hence, the database has no way of creating a valid statement and its execution plan if you use placeholders for it.

    --

    when small people start casting long shadows, it is time to go to bed

      Actually, the LIMIT clause is applied before the data are retrieved, as it can influence the execution plan. Something with a LIMIT above the number of records in the table shouldn't use indexes, whereas something with a LIMIT of 1 should. See 7.2.12. How MySQL Optimizes LIMIT.

      My normal rule of thumb is that placeholders work on values (field values, or values derived from them in the case of some where clauses), not parameters (limit clause, 'DESC' in the case of ordering), names (field names, table names, schema names, etc). Unfortunately, the mysql documentation doesn't seem to cover what it's allowed to replace. (There's a section in the documentation 13.7. SQL Syntax for Prepared Statements, and the closest I can find is the explaination of the various data types for placeholders in 24.2.5. C API Prepared Statement Data types)

        DBD::mysql allows placeholders in the LIMIT clause. It's not clear whether MySQL itself is as accomodating.

        The difference arises because DBD::mysql handles placeholders client-side. There is a push to have the DBD hand off placeholders to the server (now that MySQL supports server-side prepares), but the current CPAN version doesn't have that functionality yet. q.v. New twist for DBD::mysql for more on the topic.


        • In general, if you think something isn't in Perl, try it out, because it usually is. :-)
        • "What is the sound of Perl? Is it not the sound of a wall that people have stopped banging their heads against?"
        In SQL::Statement there are two kinds of LIMIT clauses -those that can be applied during a search and those that can only be applied afterwards. Strictly speaking, a LIMIT clause without an ORDER BY clause makes no sense since it depends on the physical ordering of rows (which nothing ever should). But given the way S::S operates, I allow it to do LIMIT without ORDER BY since that's the only way to actually limit what is being searched as distinct from limiting a result set after a search.
Re: SQL::Statement limit clause with placeholders
by trammell (Priest) on Jun 03, 2005 at 15:23 UTC
Re: SQL::Statement limit clause with placeholders
by gellyfish (Monsignor) on Jun 03, 2005 at 14:10 UTC

    Yep, the placeholders can only be used in certain places in the statement and as the argument to a LIMIT clause is not one of those places.

    /J\

Re: SQL::Statement limit clause with placeholders
by TedPride (Priest) on Jun 03, 2005 at 17:30 UTC
    Just define your limit implicitly, or do a regex on the limit vars to remove all non-number characters, and put them in directly. I don't see the problem.