Problem/Motivation

The original problem was that SQLite has a variable limit of 999 for versions before 3.32.0 (core 10 requires SQLITE_MINIMUM_VERSION = '3.26') which is not configurable and which makes this database driver unusable for medium sized sites. The solution that we came up with is combining multiple inserts and updates in a single query, which makes SQLite a lot faster. The newer minimum required version of SQLite has a much higher variable limit and therfor it is no longer a problem in combination with Drupal. Only it makes multiple insert queries and multiple upsert queries a lot faster. See #2031261-124: Make SQLite faster by combining multiple inserts and updates in a single query. In the order of from 108k microseconds to 16k microseconds.

Proposed resolution

Combining multiple inserts and in a single query and combining multiple upserts and in a single query. For both queries is having more than 50 variables the limit for merging it into a single query.

Original report by @axel.rutz

SQLite performance Optimization: PDO::ATTR_EMULATE_PREPARES

Like we've done on Mysql and Postgres we should also do on Sqlite. See #827554: PostgreSQL performance Optimization: PDO::ATTR_EMULATE_PREPARES.

(As suggested by Damien Tournoud in #2028713-11: Menu router does "too many SQL variables" on SQLite resulting in silent nondisplay of menu items)

See #2028713: Menu router does "too many SQL variables" on SQLite resulting in silent nondisplay of menu items but this can bite us in any other place too.

A first patch that set PDO::ATTR_EMULATE_PREPARES on the connection (proposed by Damien Tournoud) did not work out probably due to a bug in the underlying driver. This can be reproduced as outlined in #2028713-18: Menu router does "too many SQL variables" on SQLite resulting in silent nondisplay of menu items:

php > $options = array(
php (   PDO::ATTR_EMULATE_PREPARES => true,
php (   PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
php ( );
php > $dbh=new PDO('sqlite:dummy.sqlite', NULL, NULL, $options);
php > $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); // Give it another try.
php > $values = range(1,1111);
php > $placeholders = implode(',', array_fill_keys(array_keys($values), '?'));
php > $query = "select v from (select 0 v) where v not in ($placeholders);";
php > $stmt = $dbh->prepare($query);
PHP Warning:  Uncaught exception 'PDOException' with message 'SQLSTATE[HY000]: General error: 1 too many SQL variables' in php shell code:1
CommentFileSizeAuthor
#174 2031261-174.patch15.22 KBGunjan Rao Naik
#156 2031261-156.patch16.81 KBquietone
#156 interdiff-153-156.txt3.13 KBquietone
#153 interdiff_152-153.txt528 bytespooja saraah
#153 2031261-153.patch17.25 KBpooja saraah
#152 rerolled-1-9.4.x.patch17.25 KB_pratik_
#150 rerolled-9.4.x.patch17.25 KB_pratik_
#149 rerolled-9.4.x.patch17.25 KB_pratik_
#145 2031261-145.patch16.75 KBkarishmaamin
#143 2031261-143.patch16.92 KBkarishmaamin
#137 2031261-137.patch16.6 KBdaffie
#118 2031261-118.patch16.4 KBdaffie
#118 interdiff-2031261-117-118.txt672 bytesdaffie
#117 2031261-117.patch16.33 KBdaffie
#117 interdiff-2031261-116-117.txt2.51 KBdaffie
#116 2031261-116.patch16.34 KBdaffie
#116 interdiff-2031261-115-116.txt4.38 KBdaffie
#115 interdiff-114-115.txt787 bytesjungle
#115 2031261-115.patch13.02 KBjungle
#114 interdiff-110-114.patch1.42 KBjungle
#114 2031261-114.patch13.02 KBjungle
#112 2031261-112.patch13.16 KBjungle
#110 interdiff.txt2.59 KBeffulgentsia
#110 2031261-110.patch13.19 KBeffulgentsia
#109 2031261-109.patch13.16 KBdaffie
#109 interdiff-2031261-104-109.txt4.62 KBdaffie
#104 2031261-104.patch10.49 KBdaffie
#97 3113403-30.patch14.87 KBdaffie
#91 2031261-91-nested-condition-should-fail.patch17.4 KBdaffie
#91 2031261-91-combined-with-3113403-16.patch26.44 KBdaffie
#86 sqlite-variable-limit-2031261-85.patch16.54 KBeffulgentsia
#86 sqlite-variable-limit-2031261-85-test-only.patch5.51 KBeffulgentsia
#72 2031261-72.patch4.63 KBjofitz
#57 2031261-57-with-fix-from-comment-43.patch4.65 KBdaffie
#54 2031261-54-with-fix-from-comment-23.patch8.57 KBdaffie
#54 2031261-54-tests-only.patch3.08 KBdaffie
#53 2031261-53-tests-only.patch3.08 KBdaffie
#53 2031261-53-with-fix-from-comment-23.patch8.57 KBdaffie
#51 2031261-51-with-fix-from-comment-23.patch8.57 KBdaffie
#49 interdiff-2031261-46-49-tests-only.txt1.84 KBdaffie
#49 2031261-49-tests-only.patch3.08 KBdaffie
#46 2031261-46-tests-only.patch3.09 KBdaffie
#43 2031261-43-tests-only.patch3.09 KBdaffie
#43 2031261-43.patch4.65 KBdaffie
#41 2031261-41-test-only.patch1.28 KBdaffie
#27 drupal-8.x-2031261-27-Fix-SQLite-variable-limit-TEST-only.patch1.76 KBgeek-merlin
#23 interdiff.txt763 bytesgeek-merlin
#23 drupal-8.x-2031261-23-Fix-SQLite-variable-limit.patch8.68 KBgeek-merlin
#22 drupal-8.x-2031261-22-Fix-SQLite-variable-limit-TEST-only.patch3.1 KBgeek-merlin
#13 drupal-8.x-2031261-13.patch5.95 KBgeek-merlin
#12 drupal-7.x-2031261-12.patch6.38 KBgeek-merlin
#8 drupal-2031261-8.patch6.26 KBgeek-merlin
#7 drupal-2031261-7.patch5.71 KBgeek-merlin
#3 drupal-8.x-Issue-2031261-by-axel.rutz-Fixed-SQLite-performance-.patch1.1 KBgeek-merlin
#2 drupal-7.x-Issue-2031261-by-axel.rutz-Fixed-SQLite-performance-.patch993 bytesgeek-merlin

Issue fork drupal-2031261

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

geek-merlin’s picture

Version: 8.x-dev » 7.x-dev

Patch flying in for 7.x.

geek-merlin’s picture

Status: Active » Needs review
FileSize
993 bytes
geek-merlin’s picture

Patch flying in for 8.x

geek-merlin’s picture

geek-merlin’s picture

Priority: Normal » Major

Setting prio to major as this fixes #2028713: Menu router does "too many SQL variables" on SQLite resulting in silent nondisplay of menu items, which makes drupal on sqlite unusable for sites with more than 999 menu entries.

THe patch itself is a nobrainer and mimics what we do in all other db drivers. See #827554: PostgreSQL performance Optimization: PDO::ATTR_EMULATE_PREPARES.

geek-merlin’s picture

Issue summary: View changes

Updated issue summary.

geek-merlin’s picture

Title: SQLite performance Optimization: PDO::ATTR_EMULATE_PREPARES » Fix SQLite variable limit
Issue summary: View changes
geek-merlin’s picture

Version: 8.x-dev » 7.x-dev
FileSize
5.71 KB

Improved patch according to the issue summary flying in, first for 7.x

geek-merlin’s picture

Version: 7.x-dev » 8.x-dev
FileSize
6.26 KB

Same for 8.x

geek-merlin’s picture

Status: Needs review » Needs work

Huh, this still has issues, probably with quoting.

geek-merlin’s picture

Status: Needs work » Needs review

8: drupal-2031261-8.patch queued for re-testing.

geek-merlin’s picture

Issue summary: View changes
geek-merlin’s picture

Version: 8.x-dev » 7.x-dev
FileSize
6.38 KB

Completely reworked patch flying in.

geek-merlin’s picture

Version: 7.x-dev » 8.x-dev
FileSize
5.95 KB

And the same for 8.x

xjm’s picture

Issue tags: +sqlite
xjm’s picture

13: drupal-8.x-2031261-13.patch queued for re-testing.

mradcliffe’s picture

This is related to the new drupalci_testbot project, which stores simpletest data in sqlite. Discovered this when the postgresql test bot couldn't stuff all the errors into simpletest table.

# --sqlite database is used for the test runner only (and only contains the simpletest module database schema)

ricardoamaro’s picture

The new drupalci_testbot project can use both options, installing via "drush" or "none". This last one runs tests with sqlite.

DCI_INSTALLER="none" # Try to use core non install tests. The other option is "drush".

so i'm running a test with :

~/drupalci_testbot# ./scripts/build_all.sh refresh pgsql-9.1
~/drupalci_testbot# sudo \
DCI_PATCH="https://www.drupal.org/files/issues/drupal-2181291-pgsql-aborted-transaction-15.patch,." \
DCI_DBTYPE='pgsql' \
DCI_DBVER='9.1'  \
DCI_TESTGROUPS='--all' \
DCI_DRUPALBRANCH='8.0.x' \
DCI_VERBOSE="true" \
DCI_CONCURRENCY="8" \
DCI_INSTALLER="drush" \
./containers/web/run.sh
jhedstrom’s picture

Would it be possible to write a test that illustrates the current failing behavior?

geek-merlin’s picture

ybabel’s picture

patch #12 worked for me ... at first
but then, after a drush cc all, I got errors on rules & WSOD

 137032  21/Sep 09:28  warnin  php  Warning: Creating default object from empty value in RulesEntityController->attachLoad() (line 57 of 
                       g            /var/www/drupal/sites/all/modules/contrib/rules/includes/rules.core.inc).                     
 137033  21/Sep 09:28  notice  php  Notice: unserialize(): Error at offset 17 of 20 bytes in EntityAPIController->load() (line 261 of    
                                    /var/www/drupal/sites/all/modules/contrib/entity/entity/includes/entity.controller. 

I tried drush rr but in vain.
If I remove the patch, the error disapear.

geek-merlin’s picture

geek-merlin’s picture

This fix should do no harm to blobs.
(EDIT: which should fix #20)

geek-merlin’s picture

Status: Needs review » Needs work

The last submitted patch, 23: drupal-8.x-2031261-23-Fix-SQLite-variable-limit.patch, failed testing.

Status: Needs review » Needs work

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

lilbebel’s picture

I am having this issue with D8.1.x-dev

https://www.drupal.org/node/2746027#comment-11281037

This is really serious. Can someone please help? I have a site that needs to go live and now the thing has this major issue. I'm in a serious situation here having spend many many hours building the site to this point only for this to happen. There is no way I can start from scratch and need to know how to fix this.

Thank you.

m

daffie’s picture

@lilbebel: If this issue is so important to you, can you do a review.

lilbebel’s picture

Daffie, thank you for comment. I cannot do a review as I am not a developer and it is beyond my skill set. I was referred to this page by a senior member of Drupal and advised to push the matter as it is serious. It won't only affect me. This is already established as being a major issue by senior developers. This is not just about me. This is a major issue that will affect many. The tone of your comment seems curt which is not helpful or appropriate. I am simply asking for help from those who know more than I do. If you did not intend it to sound so, I apologize. I wish that I could do such coding tasks but we are not all coders.

daffie’s picture

@lilbebel: It is normal for people on Drupal to work on issues that they find interesting to them or issues they want to get fixed. With that being the normal work ethic on Drupal, I suggested that you can help this issue by doing a review. I did not know your skill set and I was not out to be negative to you in any way. I am sorry if I came over to you in a negative way. Also I would like to mentioning that English is not my native language. So my expression in English is not always as good as I would like it to be.

cilefen’s picture

@lilbebel English is my native language and I write things like #31 all the time. That is a normal part of working in an open source community. I did not interpret it as snarky at all.

lilbebel’s picture

Daffie, I apologize. My bad. Sometimes, things come across differently in writing than they are intended. I am doing my best to try to find solutions and contribute but am not equipped to write patches and such. I have a theming and creative background. I would love to contribute more if I could.

My server provider looked into things on my end and, I don't know if it helps but, they said the following:

I was able to recreate the error, but nothing is wrong with the database as per the MySQL checks that I've performed via the phpMyAdmin. Also as far as I can see the error is registered in the Apache error log which is rather odd since MySQL has nothing to do with Apache error log.

I've tried changing the PHP versions, but nothing has fixed this issue.

Maybe this information is relevant? If there is anything I can do to help, I am more than happy to.

Thank you all.

M

pwolanin’s picture

@lilbebel - if you are using MySQL then this issue is not relevant to you. You are using MySQL or sqlite as the database? In general I don't think sqlite is really thought to be suited for production sites.

lilbebel’s picture

@pwolanin - I'm using:

Database system SQLite
Database system version 3.8.10.2

On my dev desktop version, it would only allow me to use SQLite. Is it possible to change it now without redoing the site?

Many thanks.

M

daffie’s picture

@axel.rutz: I would like to help you get this issue fixed. If you work on the patch I will do the reviewing. What I found in the first review:

  1. +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php
    @@ -24,64 +24,75 @@ class Statement extends StatementPrefetch implements StatementInterface {
    +    $quote_value = function($value) use($dbh) {
    ...
    +        $get_replacement = function() use($args, $quote_value) {
    ...
    +        $get_replacement = function($match) use($args, $quote_value) {
    

    I am not a great fan of inline functions. I find them difficult to read. Do we really need 3 of them in the function getStatement().

  2. +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php
    @@ -24,64 +24,75 @@ class Statement extends StatementPrefetch implements StatementInterface {
    +        $query = preg_replace_callback('/:[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*\b/u', $get_replacement, $query);
    

    Can you explain how this line of code works and why it is necessary.

  3. +++ b/core/tests/Drupal/Tests/Core/Database/Driver/sqlite/SqliteStatementTest.php
    @@ -0,0 +1,89 @@
    +class SqliteStatementTest extends UnitTestCase {
    

    Can we use KernelTestBase instead of UnitTestBase. KernelTestBase has already a database connection.

If somebody else wants to work on this patch that would also be great.

daffie’s picture

Component: database system » sqlite db driver

Lets move this to the SQLite driver.

geek-merlin’s picture

@daffie, thanks for jumping in here.
I think all your comments make sense. BUT:
I'm struggeling with a much deeper problem, which may be completely unsolvable:

+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php
@@ -24,64 +24,75 @@ class Statement extends StatementPrefetch implements StatementInterface {
+      else {
+        // In this place we can't tell if it's a string or a BLOB. Fortunately
+        // SQLite eats LOB-encoded strings and connection encoding is UTF8.
+        $value = $dbh->quote($value, \PDO::PARAM_LOB);
+        return $value;
+      }
+    };

What i' trying here is doing the job of the DB driver to sanitize and insert variables into the query to overcome the limitations.
So in the above code block we need a way to handle both cases (string and blob) without the knowledge what we actually have.

So we have to
a) either find a way that works for both cases
b) or a robust check for string/blob

We might code a test first that writes some evil values to a table and checks they are written correctly.
So if you want to help, this is what is needed.

daffie’s picture

@alex.rutz: I think that you are right with that it is difficult/impossible to distinct between strings and BLOB's.
I have updated your test patch from comment #27 to use KernelTestBase. The problem is that it passes without any changes to core/lib/Drupal/Core/Database/Driver/sqlite/Statement.php. So am I doing something wrong or is there no problem?

Status: Needs review » Needs work

The last submitted patch, 41: 2031261-41-test-only.patch, failed testing.

daffie’s picture

I found out why the test where passing on my local machine. I am using Ubuntu and they have raised the value of SQLITE_MAX_VARIABLE_NUMBER to 250000. The default is 999 and that is the root cause of this issue.
Created a new patch with tests for integers, floats, strings and blobs. I also added a small change to the Statement class to see if it helps.

The last submitted patch, 43: 2031261-43.patch, failed testing.

The last submitted patch, 43: 2031261-43.patch, failed testing.

daffie’s picture

daffie’s picture

@axel.rutz: Do you have any insight why the tests only patch is not failing?

mradcliffe’s picture

The testbots are also running Ubuntu. Could that be it?

daffie’s picture

Status: Needs review » Needs work

The last submitted patch, 49: 2031261-49-tests-only.patch, failed testing.

daffie’s picture

Status: Needs work » Needs review
FileSize
8.57 KB

The tests are now fixed with the patch from comment #49.

New patch with the fix from comment #23 made by alex.rutz and the new tests.

Status: Needs review » Needs work

The last submitted patch, 51: 2031261-51-with-fix-from-comment-23.patch, failed testing.

The last submitted patch, 53: 2031261-53-with-fix-from-comment-23.patch, failed testing.

The last submitted patch, 53: 2031261-53-tests-only.patch, failed testing.

daffie’s picture

Just to make sure that the fix from comment #43 does not work.

The tests only patch from comment #54 is the one we need for further testing any possible fixes.

Status: Needs review » Needs work

The last submitted patch, 57: 2031261-57-with-fix-from-comment-43.patch, failed testing.

geek-merlin’s picture

Thanks @daffie for working on this!
So #54 fixes the original tests.

I think we need some more tests that check writing edge case strings and blobs.
(Remember that serialized PHP variables contain zero bytes.)

And maybe we just use this to decide string/blob quoting.
mb_check_encoding($s,"UTF-8")
(Also note that $dbh->quote() seems to be buggy for strings/blobs.)

If the above code is too heavy performance-wise we might count variables and only do userland quoting forthe variables >999...

daffie’s picture

Issue summary: View changes
mradcliffe’s picture

And maybe we just use this to decide string/blob quoting.
mb_check_encoding($s,"UTF-8")

I think Unicode::validateUTF8 would work for this if Multibyte support is not available. It uses PCRE though.

geek-merlin’s picture

> I think Unicode::validateUTF8 would work for this if Multibyte support is not available.

aah rite, MB is optional.
then we can use this (i don't remember the stackoverflow link):

preg_match('//u', $s);

which returns different truth values for valid and invalid unicode.

daffie’s picture

If someone writes a patch for the fix, I will do the reviewing.

@alex.rutz: Can you give some more directions for the extra tests that you would like.

geek-merlin’s picture

// Pseudocode:
// Add or use some table with a string and a blob column
...
// Write some serialized object that contains NULL bytes into the blobcolumn... read it and check it is not corrupted.
...
// Write some long random bytes int the blob and check (iterate).
...

// Write some fixed unicode string containing non-ascii into the blob... and check.
...
// Write some random valid unicode into the string and check (iterate).
...

If we don't have random blob/unicode generators in the test base class, this should really go there.
Some research on this:
* unicode - Really Good, Bad UTF-8 example test data - Stack Overflow
* unicode - Generate random UTF-8 string in Python - Stack Overflow
* unicode - Optimal function to create a random UTF-8 string in PHP? (letter characters only) - Stack Overflow

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

andypost’s picture

This blocks usage of *config_installer*

INSERT OR REPLACE INTO
{cache_config} (cid, expire, created, tags, checksum, data,
serialized) VALUES (:db_insert_placeholder_0,........

No very big site but have a 2429 variables, including config translations

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

andypost’s picture

Looks last comments about utf-detection needs separate issue, let's make this issue focused of variables

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

andypost’s picture

Issue tags: +Needs reroll
jofitz’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
FileSize
4.63 KB

Re-rolled.

Status: Needs review » Needs work

The last submitted patch, 72: 2031261-72.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

daffie’s picture

I think that we should use a change of tactics to fix this problem. I think that we should split up a insert or update statement with a large number of variables in multiple statements with less then 999 variables each.

geek-merlin’s picture

> I think that we should split up a insert or update statement with a large number of variables in multiple statements with less then 999 variables each.

This sounds like an interesting approach. Some thoughts:
* While this is not possible in all cases, i think it should be possible in all real-world cases.
* While this schould be quite easy with dynamic queries (db_insert(...)->fields(...)), it is quite difficult (needs a sql parser) with static queries(db_query()).
* Assuming that the problematic queries are dynamic ones, it's good enough to work on that layer.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

andypost’s picture

@davidwbarratt yep, all of them face with limit

davidwbarratt’s picture

Ah. Thanks.

Unfortunately the work-around doesn't work if you are using the Drupal Docker Image because there isn't a way to recompile PHP without starting an image from scratch. :(

andypost’s picture

Version: 8.6.x-dev » 8.8.x-dev

That's a matter of sqlite configuration of build so vary per os package, you can always rebuild sqlite with needed options as workaround

phenaproxima’s picture

I’m hitting this too. One possible mitigation—not a fix—that occurs to me is maybe we could make SQLite insert/update queries “batchable” under the hood. So, if you are about to run a query that exceeds 999 variables, the query is internally split up into two separate queries, 500 variables each, and executed one after the other. Not sure if this would work at all, or even be desirable, but it’s a thought.

andypost’s picture

@phenaproxima not sure it will help because each distro has own limits (SQLITE_MAX_VARIABLE_NUMBER) and I found no way to get this limit at runtime

The good news is that most of distros using to increase this limit to 250k

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

effulgentsia’s picture

Version: 8.9.x-dev » 9.0.x-dev

Moving this to 9.0.x to fix there first, then we can discuss backporting to 8.

effulgentsia’s picture

Here's an approach that I think will work. Just posting the patch for now, and will follow up tomorrow with a comment explaining it.

andypost’s picture

As I'm using sqlite everyday I will prefer to have 999 is not hardcoded limit.
I don't wanna patch every app to replace this limit in code for sane OS I'm using or for docker php image

@effulgentsia interesting approach! Just a question of how to make it opt-in/opt-out

daffie’s picture

Gábor Hojtsy’s picture

In #3107155: Discuss lowering SQLite version requirement from 3.26 to 3.22 in Drupal 9 we were discussing warning people (in system_requirements()) about the low variable limit. Looks like even with the workaround in place, performance would be more optimal if the variable limit would not be in place(?) If so, adding that warning alongside the workaround here would be best IMHO.

daffie’s picture

Status: Needs review » Needs work

@effulgentsia: Great new solution idea!

  1. +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Condition.php
    @@ -0,0 +1,36 @@
    +    if (count($values) > 50) {
    

    Can we create a protected class variable for this?

  2. +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Condition.php
    @@ -0,0 +1,36 @@
    +      $value_fragment[] = "select value from json_each($placeholder)";
    

    Could we add some documentation about this line and what it does.

  3. +++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
    @@ -27,4 +27,32 @@ public function __toString() {
    +    // SQLite has a default limit of 999 placeholders, so upsert in batches
    +    // less than that.
    

    Can we use the json variable solution for this instead of using batches?

  4. +++ b/core/tests/Drupal/KernelTests/Core/Database/InsertTest.php
    @@ -70,6 +70,42 @@ public function testMultiInsert() {
    +   * This test is motivated by SQLite's default limit of 999 placeholders, but
    +   * it's good to ensure that all database drivers can handle large inserts.
    

    The limit of 999 variables for SQLite can be set higher. Lets change the test by getting the actual maximum number of variables from the database + 1 and use that number for testing.

  5. I am missing testing for the merge queries.
daffie’s picture

If I add the following tests to Drupal\KernelTests\Core\Database\SelectTest::testLargeInCondition(), those extra tests should pass.
However they do not. See patch 2031261-91-nested-condition-should-fail.patch

+    // Find the above 3 Beatles with a nested condition.
+    $condition = new Condition('AND');
+    $condition->condition('name', $names, 'IN');
+    $num_records = $this->connection->select('test')
+      ->condition($condition)
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertEqual($num_records, 3);
+
+    // Find the other Beatle with a nested condition.
+    $condition = new Condition('AND');
+    $condition->condition('name', $names, 'NOT IN');
+    $num_records = $this->connection->select('test')
+      ->condition($condition)
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertEqual($num_records, 1);

The patch from @effulgentsia from comment #86 adds driver specific code to the Drupal\Core\Database\Query\Condition class.
The problem is that the database sub-system add the moment does not do driver specific conditions.
And we need that to fix this issue in the correct way. In #3113403: Make Drupal\Core\Database\Query\Condition driver overridable we make the condition driver overridable.
Added the patch 2031261-91-combined-with-3113403-16.patch to see that a combined patch fixes the problem.

daffie’s picture

Status: Needs work » Postponed
Beakerboy’s picture

Is there a minimum number of parameters that is considered universally acceptable? Microsoft SQL server has a limit of 2100. Would this driver also need extra parameter handling? I believe it currently performs some “emulate prepares” operations manually in order to overcome this limit. I’d like to simplify the driver wherever possible.

daffie’s picture

Microsoft SQL server has a limit of 2100

@Beakerboy: I do not think that will be enough. I am working on getting a testbot for custom database drivers. So that we can see what the problems are for your driver. These 2 issues are needed for such a testbot: #3109761: Allow custom db, php and chrome docker images from non-drupalci docker hub profiles and #3110803: Allow contrib modules to run the tests for Drupal core. Both are ready for a review and have an explanation of how to test the patch.

Beakerboy’s picture

I do not think that will be enough

Is there a test for this somewhere? The driver is able to fully install core, and passes all but 2 database kernel tests, and it passes all of the Entity kernel tests. The two failures are related to casting varchar to binary fields. Are there other part of the test suite that I should run on my Travis-CI VM?

daffie’s picture

@Beakerboy: The problems are with the importing of configuration data. Not sure which test it is.

daffie’s picture

daffie’s picture

Issue tags: +Needs reroll

Reroll needed.

daffie’s picture

Status: Needs work » Postponed
Issue tags: -Needs reroll
Beakerboy’s picture

#3113403 is ready for review again, if anyone wants to keep moving this forward.

daffie’s picture

Status: Postponed » Needs work
Issue tags: +Needs reroll

Status: Needs review » Needs work

The last submitted patch, 104: 2031261-104.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

daffie’s picture

Version: 9.0.x-dev » 9.1.x-dev
daffie’s picture

Version: 9.1.x-dev » 9.0.x-dev

Back to 9.0.x, because #3113403: Make Drupal\Core\Database\Query\Condition driver overridable has been landed on 9.0.x

daffie’s picture

Status: Needs work » Needs review
daffie’s picture

Used the same trick for upsert queries as @effulgentsia used for the select queries.

effulgentsia’s picture

Nice!

What do you think of this, as a way to make the execute() method clearer?

daffie’s picture

@effulgentsia: Yes, you are right your changes to the execute() method are an improvement. Thanks. What else needs to be done to get this issue committed?

jungle’s picture

Would like to have an 8.8.x patch to test against with.

Status: Needs review » Needs work

The last submitted patch, 112: 2031261-112.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

jungle’s picture

Fix coding standards only in #110

(Wrong interdiff file extension, sorry!)

jungle’s picture

missed one

-      $json_value = json_encode($values, 0 ,1);
+      $json_value = json_encode($values, 0, 1);
daffie’s picture

Added testing that counts the number of placeholders used in the query.

@jungle: Thanks for the coding standards errors.

daffie’s picture

daffie’s picture

To answer my own review from comment #90:

For #90.1: Lets keep 50 as a sensible default.

For #90.2: Done.

For #90.3: Done.

For #90.4: Added testing that the queries one use one placeholder.

For #90.5: Was not necessary.

The patch is ready for a review from somebody else.

Status: Needs review » Needs work

The last submitted patch, 118: 2031261-118.patch, failed testing. View results

daffie’s picture

Status: Needs work » Needs review

Back to needs review

andypost’s picture

It needs profiling because more code executed each query

Also contrib fallback driver could be another workaround

xjm’s picture

Version: 9.0.x-dev » 8.9.x-dev
Issue tags: +needs profiling

Looks like we can go all the way back to 8.9.x with this as well based on #3113403: Make Drupal\Core\Database\Query\Condition driver overridable.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

daffie’s picture

The XHProf testing for the patch contains 2 parts: one for the select query and the second for the upsert query.

XHProf testing the select query

The part of the code that I used for XHProf testing the select query:

  public function testLargeInCondition() {
    tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_MEMORY | TIDEWAYS_XHPROF_FLAGS_CPU);
    $names = [];
    $names[] = 'John';
    for ($i = 1; $i < 500; $i++) {
      $names[] = "Name $i";
    }
    $names[] = 'George';
    for ($i = 501; $i < 1000; $i++) {
      $names[] = "Name $i";
    }
    $names[] = 'Ringo';

    // Find the above 3 Beatles.
    $num_records = $this->connection->select('test')
      ->condition('name', $names, 'IN')
      ->countQuery()
      ->execute()
      ->fetchField();
    $this->assertEqual($num_records, 3);

    // Find the other Beatle.
    $num_records = $this->connection->select('test')
      ->condition('name', $names, 'NOT IN')
      ->countQuery()
      ->execute()
      ->fetchField();
    $this->assertEqual($num_records, 1);

    // Find the above 3 Beatles with a nested condition.
    $condition = $this->connection->condition('AND');
    $condition->condition('name', $names, 'IN');
    $num_records = $this->connection->select('test')
      ->condition($condition)
      ->countQuery()
      ->execute()
      ->fetchField();
    $this->assertEqual($num_records, 3);

    // Find the other Beatle with a nested condition.
    $condition = $this->connection->condition('AND');
    $condition->condition('name', $names, 'NOT IN');
    $num_records = $this->connection->select('test')
      ->condition($condition)
      ->countQuery()
      ->execute()
      ->fetchField();
    $this->assertEqual($num_records, 1);

    file_put_contents(
        sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid() . '.sqlite-variable-limit-selecttest-with-patch.xhprof',
        serialize(tideways_xhprof_disable())
    );

I ran the XHProf testing 5 time with and without the fix. For the select testing without the patch, I removed the method Drupal\Core\Database\Driver\sqlite\Condition::compileValueList().
The median result for XHProf testing with the patch is:

Overall Summary	
Total Incl. Wall Time (microsec):	16,317 microsecs
Total Incl. CPU (microsecs):	15,537 microsecs
Total Incl. MemUse (bytes):	110,808 bytes
Total Incl. PeakMemUse (bytes):	108,088 bytes
Number of Function Calls:	587

The median result for XHProf testing without the patch is:

Overall Summary	
Total Incl. Wall Time (microsec):	147,901 microsecs
Total Incl. CPU (microsecs):	144,112 microsecs
Total Incl. MemUse (bytes):	110,824 bytes
Total Incl. PeakMemUse (bytes):	634,832 bytes
Number of Function Calls:	4,587

My conclusion is that the select test with the patch is significantly faster.

XHProf testing the upsert query

The part of the code that I used for XHProf testing the select query:

  public function testLargeUpsert() {
    tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_MEMORY | TIDEWAYS_XHPROF_FLAGS_CPU);
    $num_records_before = $this->connection->query('SELECT COUNT(*) FROM {test_people}')->fetchField();

    $upsert = $this->connection->upsert('test_people')
      ->key('job')
      ->fields(['job', 'age', 'name']);

    for ($i = 0; $i < 1000; $i++) {
      $values = [
        'job' => "Job $i",
        'age' => $i,
        'name' => "Name $i",
      ];
      $upsert->values($values);
    }

    $upsert_cloned = clone $upsert;

    $upsert->execute();

    $num_records_after = $this->connection->query('SELECT COUNT(*) FROM {test_people}')->fetchField();
    $this->assertEqual($num_records_before + 1000, $num_records_after, 'Rows were inserted properly.');

    $person = $this->connection->query('SELECT * FROM {test_people} WHERE job = :job', [':job' => 'Job 0'])->fetch();
    $this->assertEqual($person->job, 'Job 0', 'First job set correctly.');
    $this->assertEqual($person->age, 0, 'First age set correctly.');
    $this->assertEqual($person->name, 'Name 0', 'First name set correctly.');

    $person = $this->connection->query('SELECT * FROM {test_people} WHERE job = :job', [':job' => 'Job 999'])->fetch();
    $this->assertEqual($person->job, 'Job 999', 'Last job set correctly.');
    $this->assertEqual($person->age, 999, 'Last age set correctly.');
    $this->assertEqual($person->name, 'Name 999', 'Last name set correctly.');

    file_put_contents(
      sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid() . '.sqlite-variable-limit-upserttest-without-patch.xhprof',
      serialize(tideways_xhprof_disable())
    );

I ran the XHProf testing 5 time with and without the fix. For the upsert testing without the patch, I removed the method Drupal\Core\Database\Driver\sqlite\Upsert::execute().
The median result for XHProf testing with the patch is:

Overall Summary	
Total Incl. Wall Time (microsec):	36,639 microsecs
Total Incl. CPU (microsecs):	30,819 microsecs
Total Incl. MemUse (bytes):	534,344 bytes
Total Incl. PeakMemUse (bytes):	940,064 bytes
Number of Function Calls:	1,208

The median result for XHProf testing without the patch is:

Overall Summary	
Total Incl. Wall Time (microsec):	206,110 microsecs
Total Incl. CPU (microsecs):	188,928 microsecs
Total Incl. MemUse (bytes):	531,592 bytes
Total Incl. PeakMemUse (bytes):	1,633,552 bytes
Number of Function Calls:	1,206

My conclusion is that the upsert test with the patch is significantly faster.

For both the select testing and the upsert testing is the solution with the patch significantly faster.

geek-merlin’s picture

So the patch speeds up upsert and select queries by factor 5-8? Amazing.

In a followup let's factor out this code and see if it can help other drivers.
At least it can help with #1210606: Document that operations that delete in bulk can hit limits for the number of arguments.

andypost’s picture

Great news! 3.32 released and brings limit increase to 32766, also new opcache for prepared statements https://sqlite.org/releaselog/3_32_0.html

Beakerboy’s picture

Microsoft SQL Server has a limit of 2100 parameters. One way the driver gets around this is is performs upsert queries in batches and within a transaction. If each statement has 10 fields, then the batch size is 200 statements.

daffie’s picture

@Beakerboy: Can you use the solution from this issue for the SQL Server?

Beakerboy’s picture

I’ll have to look at the patch closer so see how it works. I don’t get it, but I’ve never used json in sql before.

The large IN clause test will work on SQL Server because it uses EMULATE_PREPARES when a SELECT statement has >2100 parameters.

Hopefully someone has filed a bug report with the PHP group regarding the PDO EMULATE bug. I found a PDO but with the way it manages the ESCAPE part of LIKE statements. They said the fix would have to wait for PHP8!

andypost’s picture

It could use follow-up to make 50 configurable via connection options, often it much easier to build sqlite for own needs in docker image this days

daffie’s picture

Issue tags: +Bug Smash Initiative
daffie’s picture

Issue tags: -needs profiling

Profiling has been added.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

jungle’s picture

To prevent excessive memory allocations, the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, which defaults to 999 for SQLite versions prior to 3.32.0 (2020-05-22) or 32766 for SQLite versions after 3.32.0.

-- From/See https://www.sqlite.org/limits.html

The default value is 32766 nowadays. So should this be a won't fix?

(Edited: @andypost mentioned it in #128 already)

daffie’s picture

@jungle I would still very much like land this issue. I know that 32766 is a high number, only the patch fixes it for any number of variables.

jungle’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Database/Driver/sqlite/Upsert.php
@@ -17,17 +24,76 @@ public function __toString() {
+    // SQLite has a default limit of 999 placeholders. Assuming fewer than 20
+    // fields per row, if fewer than 50 rows are being upserted, then the

+++ b/core/tests/Drupal/KernelTests/Core/Database/SelectTest.php
@@ -614,4 +614,89 @@ public function testEmptyInCondition() {
+   * Tests queries with >1000 items in an IN list.
...
+   * This test is motivated by SQLite's default limit of 999 placeholders, but
...
+    for ($i = 501; $i < 1000; $i++) {

Thanks, @daffie. If so, I think the test needs updating, the default is not always 999. It might be 32766.

daffie’s picture

Because #3159073: Use the new UPSERT capability from SQLite 3.24 landed, the patch needed to be rerolled and updated (the query changed).

@jungle: The default limit of 999 placeholders is the current limit for the minimum supported version of Drupal. It will probably change in D10. Only that is still more then a year from now and it is also not that important as the test makes sure that the query one uses 1 placeholder. The other reason why I would like this patch to land is because when working with config queries, they are with this patch a lot quicker. See comment #124. Could I interest you in doing a review of the patch?

andypost’s picture

Additionally to #134 not clear why ATTR_EMULATE_PREPARES makes sense to use (pgsql and mysql drivers using it) for sqlite.

Original bug is fixed via unbundling of sqlite in PHP 7.4 https://bugs.php.net/bug.php?id=79269

The related still needs work https://bugs.php.net/bug.php?id=79272 (reproducible)

moreover the error message has change in php8 https://3v4l.org/oke3S

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

SHIJU JOHN’s picture

#137 is failing in 9.3.x branch. Is there any latest patch available for this?

quietone’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll

Needs a reroll

karishmaamin’s picture

Re-rolled patch against 9.4.x

Beakerboy’s picture

Status: Needs review » Needs work

Several phpcs failures

karishmaamin’s picture

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

NitinSP’s picture

#145 is failing for drupal 9.4.0-rc2 version. Please let me know if any other patch is compatible with 9.4.0-rc2.

_pratik_’s picture

Assigned: Unassigned » _pratik_
_pratik_’s picture

FileSize
17.25 KB

Try this patch, rerolled for version above 9.4.0

_pratik_’s picture

Assigned: _pratik_ » Unassigned
Status: Needs work » Needs review
FileSize
17.25 KB

Moving for review.
Thanks

_pratik_’s picture

Assigned: Unassigned » _pratik_
_pratik_’s picture

Assigned: _pratik_ » Unassigned
FileSize
17.25 KB
pooja saraah’s picture

Fixed failed commands on #152
Attached patch against Drupal 9.5.x

pooja saraah’s picture

Issue tags: -Needs reroll

Status: Needs review » Needs work

The last submitted patch, 153: 2031261-153.patch, failed testing. View results

quietone’s picture

Thought I would fix the tests.

This patch fixes two namespaces errors, adds types to some @params and adds a constructor in \Drupal\sqlite\Driver\Database\sqlite\Upsert to remove the return option.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

daffie’s picture

Title: Fix SQLite variable limit » Make SQLite faster by combining multiple inserts and updates in a single query
Issue summary: View changes

mondrake made their first commit to this issue’s fork.

andypost’s picture

IS needs actualization

- limit in 999 affects only few distros, the most of them has it 250000+
- profiling in comment#124
- is this still a bug?

daffie’s picture

Category: Bug report » Task
Issue summary: View changes
Status: Needs review » Reviewed & tested by the community
Issue tags: -Bug Smash Initiative, -Needs issue summary update

I have updated the IS.
I have changed the issue to a task as it is no longer a bug.
All code changes look good to me.
Testing has been added.
For me it is RTBC.

2mondrake: Thank you for working on this.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review

The code talks about the 999 limit yet #162 disputes this.

What is the actual minimum limit or our current minimum version?

andypost’s picture

The tricky moment is that real value defined by distro and default value which is 32766 since https://sqlite.org/releaselog/3_32_0.html see #124 is only could be found when someone has build sqlite and php himself

alexpott’s picture

So I think we should have a setting that's based on SQLITE_MAX_VARIABLE_NUMBER - we set it to the current default 32766 and do things based on that number rather than the 999.

Also are we sure about

that real value defined by distro

Reading the docs - it does not say that the max can be lowered at compile time... https://sqlite.org/limits.html#max_variable_number

Can someone give an example of a distro with a default lower than 32766 on an sqlite 3.32 or above...

alexpott’s picture

Oh I'm wrong... see the compile time options here - https://www.sqlite.org/compile.html#_options_to_set_size_limits

So yeah I think we should introduce a setting that defaults to the default upper bound. And use that to work out how to chunk queries. And have a status report where we check the DB value and the setting value and ensure that they match. We can get the MAX_VARIABLE_NUMBER by doing a PRAGMA compile_options; query and parsing the output. Fun. Maybe there is a better way.

alexpott’s picture

For me locally MAX_VARIABLE_NUMBER=500000 which is much higher than the default max /shrug.

andypost’s picture

I recall @effulgentsia did research per distros and it was near 200k but since that new versions released

++ To read real value from pragma

andypost’s picture

Issue summary: View changes

updated IS with a link to variable limit

On Ubuntu 22.04 MAX_VARIABLE_NUMBER=250000

On Fedora 31 there's no option displayed so it could fallback to default

Also #138 has link to https://bugs.php.net/bug.php?id=79272 which is unresolved yet

daffie’s picture

Status: Needs review » Needs work

For some reason the patch does not make the testbot run any faster.
HEAD 10.0.x 58 min https://dispatcher.drupalci.org/job/drupal8_core_regression_tests/70540/
With patch 58 min https://dispatcher.drupalci.org/job/drupal_patches/153946/

effulgentsia’s picture

#3107155-14: Discuss lowering SQLite version requirement from 3.26 to 3.22 in Drupal 9 has the summary of my research from 3 years ago, and based on that, I think we might still be in a situation where Drupal 10 on PHP 8.1 on RHEL/CentOS 8 with SQLite 3.26 would still have a 999 limit. We could check that to make sure, since who knows, maybe that's changed?

effulgentsia’s picture

Back then I opened https://bugzilla.redhat.com/show_bug.cgi?id=1798134 asking RedHat to compile with a higher value and they closed it as won't fix.

Gunjan Rao Naik’s picture

Added patch against #156 in 10.1.x version

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture

PDO::ATTR_EMULATE_PREPARES is fixed in PHP 8.1.22/8.2.9/8.3-dev https://github.com/php/php-src/commit/e0aadc1c0daee1473c77d8794ce0121826...

Probably needs separate issue