Fix parameter numbering with whereIn
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
## 2.1.0 [2024-09-10]
|
||||
|
||||
### Bug fixes
|
||||
1. Parameter numbering with whereIn
|
||||
|
||||
## 2.0.0 [2024-07-26]
|
||||
|
||||
### Features
|
||||
|
||||
@@ -44,6 +44,7 @@ $ composer require glushkovds/phpclickhouse-laravel
|
||||
'settings' => [ // optional
|
||||
'max_partitions_per_insert_block' => 300,
|
||||
],
|
||||
'fix_default_query_builder' => true,
|
||||
],
|
||||
```
|
||||
|
||||
@@ -171,6 +172,10 @@ $rows = MyTable::select(['field_one', new RawColumn('sum(field_two)', 'field_two
|
||||
->getRows();
|
||||
```
|
||||
|
||||
## Known issues
|
||||
|
||||
[Some of the problems are described here](/docs/known_issues.md).
|
||||
|
||||
## Advanced usage
|
||||
|
||||
### Columns casting
|
||||
@@ -344,6 +349,7 @@ MyTable::insertAssoc([[1, 'str', new InsertArray(['a','b'])]]);
|
||||
'timeout_query' => 2,
|
||||
'https' => false,
|
||||
'retries' => 0,
|
||||
'fix_default_query_builder' => true,
|
||||
],
|
||||
```
|
||||
|
||||
@@ -415,6 +421,7 @@ Your config/database.php should look like:
|
||||
'settings' => [ // optional
|
||||
'max_partitions_per_insert_block' => 300,
|
||||
],
|
||||
'fix_default_query_builder' => true,
|
||||
],
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
```shell
|
||||
sudo sh -c "docker compose -f docker-compose.test.yaml down --remove-orphans -v && docker compose -f docker-compose.test.yaml rm -f -v && docker compose -f docker-compose.test.yaml run php sh /src/tests.bootstrap.sh"
|
||||
```
|
||||
@@ -0,0 +1,57 @@
|
||||
## Parameter numbering with whereIn
|
||||
|
||||
The problem is described here: https://github.com/glushkovds/phpclickhouse-laravel/pull/34
|
||||
|
||||
The reason for the problem is that for the `DB::table('my-table')` construction
|
||||
the builder used was not from this library `\PhpClickHouseLaravel\Builder`,
|
||||
but the standard `\Illuminate\Database\Query\Builder`.
|
||||
|
||||
### To fix this problem
|
||||
|
||||
Add `'fix_default_query_builder' => true,` to connection config in `config/database.php` like this:
|
||||
|
||||
```php
|
||||
'clickhouse' => [
|
||||
'driver' => 'clickhouse',
|
||||
'host' => env('CLICKHOUSE_HOST'),
|
||||
'port' => env('CLICKHOUSE_PORT','8123'),
|
||||
'database' => env('CLICKHOUSE_DATABASE','default'),
|
||||
'username' => env('CLICKHOUSE_USERNAME','default'),
|
||||
'password' => env('CLICKHOUSE_PASSWORD',''),
|
||||
'timeout_connect' => env('CLICKHOUSE_TIMEOUT_CONNECT',2),
|
||||
'timeout_query' => env('CLICKHOUSE_TIMEOUT_QUERY',2),
|
||||
'https' => (bool)env('CLICKHOUSE_HTTPS', null),
|
||||
'retries' => env('CLICKHOUSE_RETRIES', 0),
|
||||
'settings' => [ // optional
|
||||
'max_partitions_per_insert_block' => 300,
|
||||
],
|
||||
'fix_default_query_builder' => true,
|
||||
],
|
||||
```
|
||||
|
||||
### Breaking changes
|
||||
|
||||
This fix will break your existing code if you use constructs like this:
|
||||
```php
|
||||
$rows = DB::table('my-table')
|
||||
...
|
||||
->get(); // Collection
|
||||
```
|
||||
You need to change your code to:
|
||||
```php
|
||||
$rows = DB::table('my-table')
|
||||
...
|
||||
->get() // Statement
|
||||
->rows(); // array
|
||||
```
|
||||
|
||||
### Why not use \Illuminate\Database\Query\Builder
|
||||
|
||||
Because this library use [the-tinderbox/ClickhouseBuilder](https://github.com/the-tinderbox/ClickhouseBuilder),
|
||||
which offers its own builder.
|
||||
It offers special methods for working with Clickhouse, which are not available in the standard builder.
|
||||
And vice versa, the standard builder has methods that cannot be implemented for Clickhouse.
|
||||
|
||||
### In the feature release v3
|
||||
In the feature release v3 this fix will be applied by default.
|
||||
|
||||
@@ -13,6 +13,7 @@ class Builder extends BaseBuilder
|
||||
{
|
||||
|
||||
use WithClient;
|
||||
use BuilderMethodsFromLaravel;
|
||||
|
||||
/** @var string */
|
||||
protected $tableSources;
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
namespace PhpClickHouseLaravel;
|
||||
|
||||
use Illuminate\Contracts\Database\Query\Expression as ExpressionContract;
|
||||
use Tinderbox\ClickhouseBuilder\Query\Expression;
|
||||
|
||||
trait BuilderMethodsFromLaravel
|
||||
{
|
||||
|
||||
public function useWritePdo()
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection instance containing the values of a given column.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @param string|null $key
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function pluck($column, $key = null)
|
||||
{
|
||||
// First, we will need to select the results of the query accounting for the
|
||||
// given columns / key. Once we have the results, we will be able to take
|
||||
// the results and get the exact data that was requested for the query.
|
||||
$queryResult = $this->onceWithColumns(
|
||||
is_null($key) ? [$column] : [$column, $key],
|
||||
function () {
|
||||
return $this->runSelect();
|
||||
}
|
||||
);
|
||||
|
||||
if (empty($queryResult)) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
// If the columns are qualified with a table or have an alias, we cannot use
|
||||
// those directly in the "pluck" operations since the results from the DB
|
||||
// are only keyed by the column itself. We'll strip the table out here.
|
||||
$column = $this->stripTableForPluck($column);
|
||||
|
||||
$key = $this->stripTableForPluck($key);
|
||||
|
||||
return is_array($queryResult[0])
|
||||
? $this->pluckFromArrayColumn($queryResult, $column, $key)
|
||||
: $this->pluckFromObjectColumn($queryResult, $column, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip off the table name or alias from a column identifier.
|
||||
*
|
||||
* @param string $column
|
||||
* @return string|null
|
||||
*/
|
||||
protected function stripTableForPluck($column)
|
||||
{
|
||||
if (is_null($column)) {
|
||||
return $column;
|
||||
}
|
||||
|
||||
$columnString = $column instanceof ExpressionContract
|
||||
? $column->getValue(new \Illuminate\Database\Query\Grammars\Grammar())
|
||||
: $column;
|
||||
|
||||
$separator = str_contains(strtolower($columnString), ' as ') ? ' as ' : '\.';
|
||||
|
||||
return last(preg_split('~' . $separator . '~i', $columnString));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve column values from rows represented as objects.
|
||||
*
|
||||
* @param array $queryResult
|
||||
* @param string $column
|
||||
* @param string $key
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function pluckFromObjectColumn($queryResult, $column, $key)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
if (is_null($key)) {
|
||||
foreach ($queryResult as $row) {
|
||||
$results[] = $row->$column;
|
||||
}
|
||||
} else {
|
||||
foreach ($queryResult as $row) {
|
||||
$results[$row->$key] = $row->$column;
|
||||
}
|
||||
}
|
||||
|
||||
return collect($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve column values from rows represented as arrays.
|
||||
*
|
||||
* @param array $queryResult
|
||||
* @param string $column
|
||||
* @param string $key
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function pluckFromArrayColumn($queryResult, $column, $key)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
if (is_null($key)) {
|
||||
foreach ($queryResult as $row) {
|
||||
$results[] = $row[$column];
|
||||
}
|
||||
} else {
|
||||
foreach ($queryResult as $row) {
|
||||
$results[$row[$key]] = $row[$column];
|
||||
}
|
||||
}
|
||||
|
||||
return collect($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the given callback while selecting the given columns.
|
||||
*
|
||||
* After running the callback, the columns are reset to the original value.
|
||||
*
|
||||
* @param array $columns
|
||||
* @param callable $callback
|
||||
* @return mixed
|
||||
*/
|
||||
protected function onceWithColumns($columns, $callback)
|
||||
{
|
||||
$original = $this->columns;
|
||||
|
||||
if (is_null($original)) {
|
||||
$this->columns = $columns;
|
||||
}
|
||||
|
||||
$result = $callback();
|
||||
|
||||
$this->columns = $original;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the query as a "select" statement against the connection.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function runSelect()
|
||||
{
|
||||
return $this->getRows();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve the minimum value of a given column.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @return mixed
|
||||
*/
|
||||
public function min($column)
|
||||
{
|
||||
return $this->aggregate(__FUNCTION__, [$column]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the maximum value of a given column.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @return mixed
|
||||
*/
|
||||
public function max($column)
|
||||
{
|
||||
return $this->aggregate(__FUNCTION__, [$column]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the sum of the values of a given column.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @return mixed
|
||||
*/
|
||||
public function sum($column)
|
||||
{
|
||||
$result = $this->aggregate(__FUNCTION__, [$column]);
|
||||
|
||||
return $result ?: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the average of the values of a given column.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @return mixed
|
||||
*/
|
||||
public function avg($column)
|
||||
{
|
||||
return $this->aggregate(__FUNCTION__, [$column]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for the "avg" method.
|
||||
*
|
||||
* @param \Illuminate\Contracts\Database\Query\Expression|string $column
|
||||
* @return mixed
|
||||
*/
|
||||
public function average($column)
|
||||
{
|
||||
return $this->avg($column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an aggregate function on the database.
|
||||
*
|
||||
* @param string $function
|
||||
* @param array $columns
|
||||
* @return mixed
|
||||
*/
|
||||
public function aggregate($function, $columns = ['*'])
|
||||
{
|
||||
$results = $this->cloneWithout($this->unions || $this->havings ? [] : ['columns'])
|
||||
->setAggregate($function, $columns)
|
||||
->getRows();
|
||||
|
||||
if ($results) {
|
||||
return array_change_key_case((array)$results[0])['aggregate'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the aggregate property without running the query.
|
||||
*
|
||||
* @param string $function
|
||||
* @param array $columns
|
||||
* @return $this
|
||||
*/
|
||||
protected function setAggregate($function, $columns)
|
||||
{
|
||||
$this->select(new Expression("$function(" . implode(',', $columns) . ") AS aggregate"));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function insert(array $values)
|
||||
{
|
||||
$table = $this->tableSources ?? (string)$this->getFrom()->getTable();
|
||||
$this->client->insertAssocBulk($table, $values);
|
||||
}
|
||||
}
|
||||
@@ -99,4 +99,17 @@ class Connection extends BaseConnection
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new query builder instance.
|
||||
*
|
||||
* @return Builder|\Illuminate\Database\Query\Builder
|
||||
*/
|
||||
public function query()
|
||||
{
|
||||
if ($this->config['fix_default_query_builder'] ?? false) {
|
||||
return new Builder();
|
||||
}
|
||||
return parent::query();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ class BaseTest extends TestCase
|
||||
{
|
||||
public function testWorkWithClient()
|
||||
{
|
||||
Artisan::call('migrate');
|
||||
/** @var \ClickHouseDB\Client $db */
|
||||
$db = DB::connection('clickhouse')->getClient();
|
||||
$db->write("TRUNCATE TABLE examples");
|
||||
|
||||
+14
-1
@@ -2,12 +2,13 @@
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PhpClickHouseLaravel\Builder;
|
||||
use Tests\Models\Example;
|
||||
|
||||
class BindingsTest extends TestCase
|
||||
{
|
||||
public function testBindings()
|
||||
public function testBindingsByModel()
|
||||
{
|
||||
$query = Example::select()
|
||||
->where(function (Builder $q) {
|
||||
@@ -18,4 +19,16 @@ class BindingsTest extends TestCase
|
||||
->whereIn('f_int2', [10, 11]);
|
||||
$this->assertEquals("SELECT * FROM `examples` WHERE (`f_int` = 1 OR `f_int` = 2 OR `f_int` = 3) AND `f_int2` IN (10, 11)", $query->toSql());
|
||||
}
|
||||
|
||||
public function testBindingsByTableMethod()
|
||||
{
|
||||
$query = DB::table('examples')
|
||||
->where(function (Builder $q) {
|
||||
$q->where('f_int', 1)
|
||||
->orWhere('f_int', 2)
|
||||
->orWhere('f_int', 3);
|
||||
})
|
||||
->whereIn('f_int2', [10, 11]);
|
||||
$this->assertEquals("SELECT * FROM `examples` WHERE (`f_int` = 1 OR `f_int` = 2 OR `f_int` = 3) AND `f_int2` IN (10, 11)", $query->toSql());
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ return [
|
||||
'settings' => [
|
||||
'max_partitions_per_insert_block' => 300,
|
||||
],
|
||||
'fix_default_query_builder' => true,
|
||||
],
|
||||
|
||||
'clickhouse2' => [
|
||||
|
||||
Reference in New Issue
Block a user