Repository: codeigniter4/CodeIgniter4 (PHP)

Issue:
Bug:  [QueryBuilder] join() with BETWEEN causes TypeError preg_quote(): Argument #1 ($str) must be of type string, false given
### PHP Version

8.3

### CodeIgniter4 Version

4.5.1

### CodeIgniter4 Installation Method

Composer (using `codeigniter4/appstarter`)

### Which operating systems have you tested for this bug?

Windows, Linux

### Which server did you use?

apache

### Database

MariaDB 11.2.3

### What happened?

After upgrading to 4.5.0 and then 4.5.1, I am now getting TypeError preg_quote(): Argument #1 ($str) must be of type string, false given at SYSTEMPATH/Database/BaseBuilder.php at line 680 with some of my SQL queries.  These queries all use the BETWEEN Operator to compare dates.

These queries worked perfectly fine going all the way back to CI 4.0.x up through 4.4.8.

Below is a screenshot of the error:
![Screenshot 2024-04-15 at 10-50-16 TypeError](https://github.com/codeigniter4/CodeIgniter4/assets/65561814/50643621-8dce-490f-9765-41e8e9a912cd)


### Steps to Reproduce

Execute an SQL query created using the Query Builder Class utilizing the BETWEEN operator.

### Expected Output

A view with the results of the SQL query.

### Anything else?

_No response_


Discussion:
How do we reproduce?
It seems Query Builder does not support BETWEEN.
https://codeigniter4.github.io/CodeIgniter4/database/query_builder.html

I probably did not phrase the bug report correctly.  I have two SQL queries that have worked perfectly from CI version 4.0.x all the way through 4.4.8.  Both of these queries are constructed using the Query Builder Class.   Here are the lines from each Query Builder that appear to be throwing the type error: 

`->join('leases', 'units.unit_id = leases.unit_id AND CURDATE() BETWEEN lease_start_date AND lease_exp_date', 'left')`

`->join('leases', 'units.unit_id = leases.unit_id AND workorder_start BETWEEN lease_start_date AND lease_exp_date', 'left')`

The only common thread seems to be the use of the BETWEEN SQL operator.  I'm not sure that has anything to do with it. 

It does seem to be an issue in Query Builder because it no longer puts together the query without throwing an error.

It could simply be a strict typing issue like some of the other 4.5.0 bugs, but I will do some further troubleshooting.  
Thank you for the feedback.

There is no sample code with `BETWEEN` in `join()`.
https://codeigniter4.github.io/CodeIgniter4/database/query_builder.html#join
and there is no test case:
https://github.com/codeigniter4/CodeIgniter4/blob/develop/tests/system/Database/Builder/JoinTest.php

We need further investigation.

It seems `join()` does not support `BETWEEN`, but it worked by accident in previous versions.

When there is a conditon `'units.unit_id = leases.unit_id AND CURDATE() BETWEEN lease_start_date AND lease_exp_date'`, it is parsed in `$conditions` as
```php
array (
  0 => 'units.unit_id = leases.unit_id',
  1 => 'CURDATE() BETWEEN lease_start_date',
  2 => 'lease_exp_date',
)
```
but it is of course wrong.

Fixing this behavior is difficult, so I sent ad hoc fix PR #8792

Ahh, I understand now.  

I replaced the `BETWEEN` operators in my `JOIN` clauses with a combination of `>=` and `<=` operators to workaround this limitation with Query Builder.

This project also has many `WHERE` clauses constructed by Query Builder utilizing the `BETWEEN` operator that are still working as expected in 4.5.1.  Does `where()` officially support `BETWEEN` or is is this also working by accident?
There is one test case.
https://github.com/codeigniter4/CodeIgniter4/blob/d425bdf6df7d3103b4aeed06d9b71e88fbbaf1fe/tests/system/Database/Builder/WhereTest.php#L198-L210
But note that uses `false` in the third parameter `$escape`:
https://github.com/codeigniter4/CodeIgniter4/blob/d425bdf6df7d3103b4aeed06d9b71e88fbbaf1fe/tests/system/Database/Builder/WhereTest.php#L203

If `$escape` is set to false, the value as is becomes a part of the SQL statement, so you can freely create SQL statements, but QueryBuilder does not provide any protection/escaping.


@oakhurstmgmt If you agree to merge #8792, approve the PR.




Patch:
diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php
index 1f0807cdbbde..46d94661aca6 100644
--- a/system/Database/BaseBuilder.php
+++ b/system/Database/BaseBuilder.php
@@ -654,6 +654,7 @@ public function join(string $table, $cond, string $type = '', ?bool $escape = nu
             $cond = ' ON ' . $cond;
         } else {
             // Split multiple conditions
+            // @TODO This does not parse `BETWEEN a AND b` correctly.
             if (preg_match_all('/\sAND\s|\sOR\s/i', $cond, $joints, PREG_OFFSET_CAPTURE)) {
                 $conditions = [];
                 $joints     = $joints[0];
@@ -676,6 +677,13 @@ public function join(string $table, $cond, string $type = '', ?bool $escape = nu
             foreach ($conditions as $i => $condition) {
                 $operator = $this->getOperator($condition);
 
+                // Workaround for BETWEEN
+                if ($operator === false) {
+                    $cond .= $joints[$i] . $condition;
+
+                    continue;
+                }
+
                 $cond .= $joints[$i];
                 $cond .= preg_match('/(\(*)?([\[\]\w\.\'-]+)' . preg_quote($operator, '/') . '(.*)/i', $condition, $match) ? $match[1] . $this->db->protectIdentifiers($match[2]) . $operator . $this->db->protectIdentifiers($match[3]) : $condition;
             }
diff --git a/system/Database/SQLSRV/Builder.php b/system/Database/SQLSRV/Builder.php
index a624dced38d2..ffc72bd41d66 100755
--- a/system/Database/SQLSRV/Builder.php
+++ b/system/Database/SQLSRV/Builder.php
@@ -145,6 +145,13 @@ public function join(string $table, $cond, string $type = '', ?bool $escape = nu
             foreach ($conditions as $i => $condition) {
                 $operator = $this->getOperator($condition);
 
+                // Workaround for BETWEEN
+                if ($operator === false) {
+                    $cond .= $joints[$i] . $condition;
+
+                    continue;
+                }
+
                 $cond .= $joints[$i];
                 $cond .= preg_match('/(\(*)?([\[\]\w\.\'-]+)' . preg_quote($operator, '/') . '(.*)/i', $condition, $match) ? $match[1] . $this->db->protectIdentifiers($match[2]) . $operator . $this->db->protectIdentifiers($match[3]) : $condition;
             }


Test patch:
diff --git a/tests/system/Database/Builder/JoinTest.php b/tests/system/Database/Builder/JoinTest.php
index 0ab74729ed7a..83c7ea4f3ab9 100644
--- a/tests/system/Database/Builder/JoinTest.php
+++ b/tests/system/Database/Builder/JoinTest.php
@@ -80,6 +80,25 @@ public function testJoinMultipleConditions(): void
         $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
     }
 
+    /**
+     * @see https://github.com/codeigniter4/CodeIgniter4/issues/8791
+     */
+    public function testJoinMultipleConditionsBetween(): void
+    {
+        $builder = new BaseBuilder('table1', $this->db);
+
+        $builder->join(
+            'leases',
+            'units.unit_id = leases.unit_id AND CURDATE() BETWEEN lease_start_date AND lease_exp_date',
+            'LEFT'
+        );
+
+        // @TODO Should be `... CURDATE() BETWEEN "lease_start_date" AND "lease_exp_date"`
+        $expectedSQL = 'SELECT * FROM "table1" LEFT JOIN "leases" ON "units"."unit_id" = "leases"."unit_id" AND CURDATE() BETWEEN lease_start_date AND lease_exp_date';
+
+        $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect()));
+    }
+
     /**
      * @see https://github.com/codeigniter4/CodeIgniter4/issues/3832
      */