diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
old mode 100644
new mode 100755
index 3991ffa..a063547
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,52 +1,47 @@
name: tests
on:
- push:
- branches:
- - master
- pull_request:
+ push:
+ branches:
+ - master
+ - 9.x
+ pull_request:
jobs:
- tests:
-
- runs-on: ubuntu-latest
- strategy:
- fail-fast: true
- matrix:
- php: [7.2, 7.3, 7.4]
- laravel: [^6.0, ^7.0, ^8.0]
- scout: [^7.0, ^8.0]
- exclude:
- - php: 7.2
- laravel: ^8.0
- - laravel: ^8.0
- scout: ^7.0
- include:
- - php: 8.0
- laravel: ^8.0
- scout: ^8.0
-
- name: Test PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Scout ${{ matrix.scout }}
-
- steps:
- - name: Checkout code
- uses: actions/checkout@v2
-
- - name: Setup PHP
- uses: shivammathur/setup-php@v2
- with:
- php-version: ${{ matrix.php }}
- extensions: dom, curl, libxml, mbstring, zip
- tools: composer:v2
- coverage: none
-
- - name: Install dependencies
- run: |
- composer require "illuminate/contracts=${{ matrix.laravel }}" --no-update
- composer require "illuminate/database=${{ matrix.laravel }}" --no-update
- composer require "illuminate/support=${{ matrix.laravel }}" --no-update
- composer require "laravel/scout=${{ matrix.scout }}" --no-update
- composer update --prefer-dist --no-interaction --no-progress
-
- - name: Run tests
- run: vendor/bin/phpunit --verbose
\ No newline at end of file
+ tests:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: true
+ matrix:
+ php: [8.3, 8.4]
+ laravel: [^11, ^12]
+ scout: [^10]
+
+ name: Test PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - Scout ${{ matrix.scout }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: dom, curl, libxml, mbstring, zip
+ tools: composer:v2
+ coverage: none
+
+ - name: Install dependencies
+ run: |
+ composer remove larastan/larastan --dev --no-update
+ composer remove orchestra/testbench --dev --no-update
+ composer remove tightenco/duster --dev --no-update
+ composer remove laravel/pint --dev --no-update
+ composer require "illuminate/contracts=${{ matrix.laravel }}" --no-update
+ composer require "illuminate/database=${{ matrix.laravel }}" --no-update
+ composer require "illuminate/support=${{ matrix.laravel }}" --no-update
+ composer require "laravel/scout=${{ matrix.scout }}" --no-update
+ composer update --prefer-dist --no-interaction --no-progress
+
+ - name: Run tests
+ run: vendor/bin/phpunit
diff --git a/.gitignore b/.gitignore
index 382f5a0..1335ee0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
/vendor
.phpunit.result.cache
-composer.lock
\ No newline at end of file
+composer.lock
+.DS_Store
+.idea
diff --git a/README.md b/README.md
index 065c6ac..511a028 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,15 @@
# PostgreSQL Full Text Search Engine for Laravel Scout
-[](https://packagist.org/packages/pmatseykanets/laravel-scout-postgres)
-[](LICENSE.md)
-
-[](https://styleci.io/repos/67233265)
-[](https://packagist.org/packages/pmatseykanets/laravel-scout-postgres)
-[](https://github.com/pmatseykanets/laravel-scout-postgres/blob/master/LICENSE.md)
+
+[](https://packagist.org/packages/devnoiseconsulting/laravel-scout-postgres-tsvector)
+[](LICENSE.md)
This package makes it easy to use native PostgreSQL Full Text Search capabilities with [Laravel Scout](http://laravel.com/docs/master/scout).
-If you find this package usefull, please consider bying me a coffee.
-
-
-
## Contents
- [Installation](#installation)
- [Laravel](#laravel)
- - [Lumen](#lumen)
- [Configuration](#configuration)
- [Configuring the Engine](#configuring-the-engine)
- [Configuring PostgreSQL](#configuring-postgresql)
@@ -36,8 +28,8 @@ If you find this package usefull, please consider bying me a coffee.
You can install the package via composer:
-``` bash
-composer require pmatseykanets/laravel-scout-postgres
+```bash
+composer require devnoiseconsulting/laravel-scout-postgres-tsvector
```
### Laravel
@@ -52,52 +44,6 @@ If you're using Laravel < 5.5 or if you have package auto-discovery turned off y
],
```
-### Lumen
-
-Scout service provider uses `config_path` helper that is not included in Lumen.
-To fix this include the following snippet either directly in `bootstrap.app` or in your autoloaded helpers file i.e. `app/helpers.php`.
-
-```php
-if (! function_exists('config_path')) {
- /**
- * Get the configuration path.
- *
- * @param string $path
- * @return string
- */
- function config_path($path = '')
- {
- return app()->basePath() . '/config'.($path ? DIRECTORY_SEPARATOR.$path : $path);
- }
-}
-```
-
-Create the `scout.php` config file in `app/config` folder with the following contents
-
-```php
- env('SCOUT_DRIVER', 'pgsql'),
- 'prefix' => env('SCOUT_PREFIX', ''),
- 'queue' => false,
- 'pgsql' => [
- 'connection' => 'pgsql',
- 'maintain_index' => true,
- 'config' => 'english',
- ],
-];
-```
-
-Register service providers:
-
-```php
-// bootstrap/app.php
-$app->register(Laravel\Scout\ScoutServiceProvider::class);
-$app->configure('scout');
-$app->register(ScoutEngines\Postgres\PostgresEngineServiceProvider::class);
-```
-
## Configuration
### Configuring the Engine
@@ -126,7 +72,7 @@ Specify the database connection that should be used to access indexed documents
### Configuring PostgreSQL
-Make sure that an appropriate [default text search configuration](https://www.postgresql.org/docs/9.5/static/runtime-config-client.html#GUC-DEFAULT-TEXT-SEARCH-CONFIG) is set globbaly (in `postgresql.conf`), for a particular database (`ALTER DATABASE ... SET default_text_search_config TO ...`) or alternatively set `default_text_search_config` in each session.
+Make sure that an appropriate [default text search configuration](https://www.postgresql.org/docs/14/runtime-config-client.html#GUC-DEFAULT-TEXT-SEARCH-CONFIG) is set globbaly (in `postgresql.conf`), for a particular database (`ALTER DATABASE ... SET default_text_search_config TO ...`) or alternatively set `default_text_search_config` in each session.
To check the current value
@@ -265,7 +211,7 @@ $posts = App\Post::search('fat & (cat | rat)')
->usingTsQuery()->get()
// websearch_to_tsquery()
-// uses web search syntax
+// uses web search syntax
$posts = App\Post::search('"sad cat" or "fat rat" -mouse')
->usingWebSearchQuery()->get()
@@ -281,13 +227,13 @@ Please see the [official documentation](http://laravel.com/docs/master/scout) on
## Testing
-``` bash
+```bash
composer test
```
## Security
-If you discover any security related issues, please email pmatseykanets@gmail.com instead of using the issue tracker.
+If you discover any security related issues, please email flynnmj@devnoise.com instead of using the issue tracker.
## Changelog
@@ -299,6 +245,7 @@ Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
## Credits
+- [Michael Flynn](https://github.com/devNoiseConsulting)
- [Peter Matseykanets](https://github.com/pmatseykanets)
- [All Contributors](../../contributors)
diff --git a/composer.json b/composer.json
old mode 100644
new mode 100755
index 305a86c..b109621
--- a/composer.json
+++ b/composer.json
@@ -1,5 +1,5 @@
{
- "name": "pmatseykanets/laravel-scout-postgres",
+ "name": "devnoiseconsulting/laravel-scout-postgres-tsvector",
"description": "PostgreSQL Full Text Search Driver for Laravel Scout",
"keywords": [
"laravel",
@@ -9,13 +9,18 @@
"full text search",
"FTS"
],
- "homepage": "https://github.com/pmatseykanets/laravel-scout-postgres",
+ "homepage": "https://github.com/devNoiseConsulting/laravel-scout-postgres-tsvector",
"license": "MIT",
"support": {
- "issues": "https://github.com/pmatseykanets/laravel-scout-postgres/issues",
- "source": "https://github.com/pmatseykanets/laravel-scout-postgres"
+ "issues": "https://github.com/devNoiseConsulting/laravel-scout-postgres-tsvector/issues",
+ "source": "https://github.com/devNoiseConsulting/laravel-scout-postgres-tsvector"
},
"authors": [
+ {
+ "name": "Michael Flynn",
+ "email": "flynnmj@devnoise.com",
+ "homepage": "https://github.com/devNoiseConsulting"
+ },
{
"name": "Peter Matseykanets",
"email": "pmatseykanets@gmail.com",
@@ -23,15 +28,19 @@
}
],
"require": {
- "php": "^7.2|^8.0",
- "illuminate/contracts": "~6.0|~7.0|~8.0",
- "illuminate/database": "~6.0|~7.0|~8.0",
- "illuminate/support": "~6.0|~7.0|~8.0",
- "laravel/scout": "~7.0|~8.0"
+ "php": "^8.1|^8.2|^8.3|^8.4",
+ "illuminate/contracts": "^10|^11|^12",
+ "illuminate/database": "^10|^11|^12",
+ "illuminate/support": "^10|^11|^12",
+ "laravel/scout": "^10|^11|^12"
},
"require-dev": {
- "phpunit/phpunit": "^8.3",
- "mockery/mockery": "^1.2.3"
+ "larastan/larastan": "^3.3",
+ "laravel/pint": "^1.22",
+ "mockery/mockery": "^1.6",
+ "orchestra/testbench": "^10.2",
+ "phpunit/phpunit": "^12.1",
+ "tightenco/duster": "^2.7"
},
"autoload": {
"psr-4": {
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 0000000..00726a5
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,21 @@
+parameters:
+ ignoreErrors:
+ -
+ message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:searchableAs\\(\\)\\.$#"
+ count: 2
+ path: src/PostgresEngine.php
+
+ -
+ message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:toSearchableArray\\(\\)\\.$#"
+ count: 1
+ path: src/PostgresEngine.php
+ -
+ message: "#^Call to an undefined method TModel of Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:searchableAs\\(\\)\\.$#"
+ count: 2
+ path: src/PostgresEngine.php
+
+ -
+ message: "#^Call to an undefined method TModel of Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getDeletedAtColumn\\(\\)\\.$#"
+ count: 2
+ path: src/PostgresEngine.php
+
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..2aeb4fe
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,10 @@
+includes:
+ - phpstan-baseline.neon
+
+parameters:
+
+ paths:
+ - src/
+
+ # Level 9 is the highest level
+ level: 9
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index f2eebe0..530b980 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,22 +1,13 @@
-
+
+
+
+ src/
+
+
tests
-
-
- src/
-
-
diff --git a/pint.json b/pint.json
new file mode 100644
index 0000000..2964efd
--- /dev/null
+++ b/pint.json
@@ -0,0 +1,45 @@
+{
+ "preset": "laravel",
+ "rules": {
+ "blank_line_between_import_groups": true,
+ "concat_space": {
+ "spacing": "one"
+ },
+ "class_attributes_separation": {
+ "elements": {
+ "method": "one"
+ }
+ },
+ "curly_braces_position": {
+ "control_structures_opening_brace": "same_line",
+ "functions_opening_brace": "next_line_unless_newline_at_signature_end",
+ "anonymous_functions_opening_brace": "same_line",
+ "classes_opening_brace": "next_line_unless_newline_at_signature_end",
+ "anonymous_classes_opening_brace": "next_line_unless_newline_at_signature_end",
+ "allow_single_line_empty_anonymous_classes": true,
+ "allow_single_line_anonymous_functions": false
+ },
+ "explicit_string_variable": true,
+ "global_namespace_import": {
+ "import_classes": true,
+ "import_constants": true,
+ "import_functions": true
+ },
+ "new_with_braces": {
+ "named_class": false,
+ "anonymous_class": false
+ },
+ "ordered_imports": {
+ "sort_algorithm": "alpha",
+ "imports_order": [
+ "const",
+ "class",
+ "function"
+ ]
+ },
+ "php_unit_test_annotation": {
+ "style": "annotation"
+ },
+ "simple_to_complex_string_variable": true
+ }
+}
diff --git a/src/PostgresEngine.php b/src/PostgresEngine.php
old mode 100644
new mode 100755
index b92b3e0..d1fd1cd
--- a/src/PostgresEngine.php
+++ b/src/PostgresEngine.php
@@ -2,22 +2,30 @@
namespace ScoutEngines\Postgres;
+use Exception;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\PostgresConnection;
use Illuminate\Support\Arr;
+use Illuminate\Support\LazyCollection;
+use InvalidArgumentException;
use Laravel\Scout\Builder;
use Laravel\Scout\Engines\Engine;
use ScoutEngines\Postgres\TsQuery\PhraseToTsQuery;
use ScoutEngines\Postgres\TsQuery\PlainToTsQuery;
use ScoutEngines\Postgres\TsQuery\ToTsQuery;
+/**
+ * @template TModel of \Illuminate\Database\Eloquent\Model
+ * @template TID of int|string
+ */
class PostgresEngine extends Engine
{
/**
* Database connection.
*
- * @var \Illuminate\Database\Connection
+ * @var \Illuminate\Database\PostgresConnection
*/
protected $database;
@@ -31,7 +39,7 @@ class PostgresEngine extends Engine
/**
* Config values.
*
- * @var array
+ * @var array
*/
protected $config = [];
@@ -43,8 +51,7 @@ class PostgresEngine extends Engine
/**
* Create a new instance of PostgresEngine.
*
- * @param \Illuminate\Database\ConnectionResolverInterface $resolver
- * @param $config
+ * @param array $config
*/
public function __construct(ConnectionResolverInterface $resolver, $config)
{
@@ -57,7 +64,7 @@ public function __construct(ConnectionResolverInterface $resolver, $config)
/**
* Update the given models in the index.
*
- * @param \Illuminate\Database\Eloquent\Collection $models
+ * @param \Illuminate\Database\Eloquent\Collection $models
* @return void
*/
public function update($models)
@@ -71,11 +78,207 @@ public function update($models)
}
}
+ /**
+ * Remove the given model from the index.
+ *
+ * @param \Illuminate\Database\Eloquent\Collection $models
+ * @return void
+ */
+ public function delete($models)
+ {
+ $model = $models->first();
+
+ if ($model) {
+ if (! $this->shouldMaintainIndex($model)) {
+ return;
+ }
+
+ $indexColumn = $this->getIndexColumn($model);
+ $key = $model->getKeyName();
+
+ $ids = $models->pluck($key)->all();
+
+ $this->database
+ ->table($model->searchableAs())
+ ->whereIn($key, $ids)
+ ->update([$indexColumn => null]);
+
+ }
+ }
+
+ /**
+ * Perform the given search on the engine.
+ *
+ * @param \Laravel\Scout\Builder $builder
+ * @return mixed
+ */
+ public function search(Builder $builder)
+ {
+ return $this->performSearch($builder, $builder->limit);
+ }
+
+ /**
+ * Perform the given search on the engine.
+ *
+ * @param \Laravel\Scout\Builder $builder
+ * @param int $perPage
+ * @param int $page
+ * @return mixed
+ */
+ public function paginate(Builder $builder, $perPage, $page)
+ {
+ return $this->performSearch($builder, $perPage, $page);
+ }
+
+ /**
+ * Get the total count from a raw result returned by the engine.
+ *
+ * @param mixed $results
+ * @return int
+ */
+ public function getTotalCount($results)
+ {
+ if (empty($results)) {
+ return 0;
+ }
+
+ /** @var array $results */
+ /** @var object{'id': mixed, 'rank': string, 'total_count': int} $result */
+ $result = Arr::first($results);
+
+ return (int) $result->total_count;
+ }
+
+ /**
+ * Returns the default query method.
+ *
+ * @param string $query
+ * @param string $config
+ * @return \ScoutEngines\Postgres\TsQuery\TsQueryable
+ */
+ public function defaultQueryMethod($query, $config)
+ {
+ switch (strtolower($this->stringConfig('search_using', 'plainquery'))) {
+ case 'tsquery':
+ return new ToTsQuery($query, $config);
+ case 'phrasequery':
+ return new PhraseToTsQuery($query, $config);
+ case 'plainquery':
+ default:
+ return new PlainToTsQuery($query, $config);
+ }
+ }
+
+ /**
+ * Pluck and return the primary keys of the given results.
+ *
+ * @param mixed $results
+ * @return \Illuminate\Support\Collection
+ */
+ public function mapIds($results)
+ {
+ $keyName = $this->model !== null ? $this->model->getKeyName() : 'id';
+
+ /** @var array $results */
+ return collect($results)
+ ->pluck($keyName)
+ ->values();
+ }
+
+ /**
+ * Map the given results to instances of the given model.
+ *
+ * @param \Laravel\Scout\Builder $builder
+ * @param mixed $results
+ * @param \Illuminate\Database\Eloquent\Model $model
+ * @return \Illuminate\Database\Eloquent\Collection
+ */
+ public function map(Builder $builder, $results, $model)
+ {
+ $resultModels = Collection::make();
+
+ if (empty($results)) {
+ return $resultModels;
+ }
+
+ $keys = $this->mapIds($results);
+
+ $models = $model->whereIn($model->getKeyName(), $keys->all())
+ ->get()
+ ->keyBy($model->getKeyName());
+
+ // The models didn't come out of the database in the correct order.
+ // This will map the models into the resultsModel based on the results order.
+ /** @var int $key */
+ foreach ($keys as $key) {
+ if ($models->has($key)) {
+ $resultModels->push($models[$key]);
+ }
+ }
+
+ return $resultModels;
+ }
+
+ /**
+ * Map the given results to instances of the given model via a lazy collection.
+ *
+ * @param \Laravel\Scout\Builder $builder
+ * @param mixed $results
+ * @param \Illuminate\Database\Eloquent\Model $model
+ * @return \Illuminate\Support\LazyCollection
+ */
+ public function lazyMap(Builder $builder, $results, $model)
+ {
+ // return LazyCollection::make($model->newCollection());
+ return LazyCollection::make($this->map($builder, $results, $model)->all());
+ }
+
+ /**
+ * Create a search index.
+ *
+ * @param string $name
+ * @param array $options
+ * @return mixed
+ */
+ public function createIndex($name, $options = [])
+ {
+ throw new Exception('PostgreSQL indexes should be created through Laravel database migrations.');
+ }
+
+ /**
+ * Delete a search index.
+ *
+ * @param string $name
+ * @return mixed
+ */
+ public function deleteIndex($name)
+ {
+ throw new Exception('PostgreSQL indexes should be deleted through Laravel database migrations.');
+ }
+
+ /**
+ * Flush all of the model's records from the engine.
+ *
+ * @param \Illuminate\Database\Eloquent\Model $model
+ * @return void
+ */
+ public function flush($model)
+ {
+ if (! $this->shouldMaintainIndex($model)) {
+ return;
+ }
+
+ $indexColumn = $this->getIndexColumn($model);
+
+ $this->database
+ ->table($model->searchableAs())
+ ->update([$indexColumn => null]);
+ }
+
/**
* Perform update of the given model.
*
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return bool
+ * @return bool|int
*/
protected function performUpdate(Model $model)
{
@@ -93,22 +296,21 @@ protected function performUpdate(Model $model)
return $query->update($data->all());
}
+ $modelKeyInfo = collect([$model->getKeyName() => $model->getKey()]);
+
return $query->insert(
- $data->merge([
- $model->getKeyName() => $model->getKey(),
- ])->all()
+ $data->merge($modelKeyInfo)->all()
);
}
/**
* Get the indexed value for a given model.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return string
*/
- protected function toVector(Model $model)
+ protected function toVector(Model $model): mixed
{
- $fields = collect($model->toSearchableArray())
+ /** @var array $searchableArray */
+ $searchableArray = $model->toSearchableArray();
+ $fields = collect($searchableArray)
->map(function ($value) {
return $value === null ? '' : $value;
});
@@ -127,7 +329,7 @@ protected function toVector(Model $model)
// Set a field weight if it was specified in Model's searchableOptions()
if ($label = $this->rankFieldWeightLabel($model, $key)) {
- $vector = "setweight($vector, ?)";
+ $vector = "setweight({$vector}, ?)";
$bindings->push($label);
}
@@ -136,84 +338,17 @@ protected function toVector(Model $model)
return $this->database
->query()
- ->selectRaw("$select AS tsvector", $bindings->all())
+ ->selectRaw("{$select} AS tsvector", $bindings->all())
->value('tsvector');
}
- /**
- * Remove the given model from the index.
- *
- * @param \Illuminate\Database\Eloquent\Collection $models
- * @return void
- */
- public function delete($models)
- {
- $model = $models->first();
-
- if (! $this->shouldMaintainIndex($model)) {
- return;
- }
-
- $indexColumn = $this->getIndexColumn($model);
- $key = $model->getKeyName();
-
- $ids = $models->pluck($key)->all();
-
- $this->database
- ->table($model->searchableAs())
- ->whereIn($key, $ids)
- ->update([$indexColumn => null]);
- }
-
- /**
- * Perform the given search on the engine.
- *
- * @param \Laravel\Scout\Builder $builder
- * @return mixed
- */
- public function search(Builder $builder)
- {
- return $this->performSearch($builder, $builder->limit);
- }
-
/**
* Perform the given search on the engine.
*
- * @param \Laravel\Scout\Builder $builder
- * @param int $perPage
- * @param int $page
- * @return mixed
- */
- public function paginate(Builder $builder, $perPage, $page)
- {
- return $this->performSearch($builder, $perPage, $page);
- }
-
- /**
- * Get the total count from a raw result returned by the engine.
- *
- * @param mixed $results
- * @return int
+ * @param \Laravel\Scout\Builder $builder
+ * @return array
*/
- public function getTotalCount($results)
- {
- if (empty($results)) {
- return 0;
- }
-
- return (int) Arr::first($results)
- ->total_count;
- }
-
- /**
- * Perform the given search on the engine.
- *
- * @param \Laravel\Scout\Builder $builder
- * @param int|null $perPage
- * @param int $page
- * @return array
- */
- protected function performSearch(Builder $builder, $perPage = 0, $page = 1)
+ protected function performSearch(Builder $builder, ?int $perPage = 0, int $page = 1): ?array
{
// We have to preserve the model in order to allow for
// correct behavior of mapIds() method which currently
@@ -228,19 +363,37 @@ protected function performSearch(Builder $builder, $perPage = 0, $page = 1)
->select($builder->model->getKeyName())
->selectRaw("{$this->rankingExpression($builder->model, $indexColumn)} AS rank")
->selectRaw('COUNT(*) OVER () AS total_count')
- ->whereRaw("$indexColumn @@ \"tsquery\"");
+ ->whereRaw("{$indexColumn} @@ \"tsquery\"");
+
+ // Apply query callback if set
+ if ($builder->queryCallback) {
+ call_user_func($builder->queryCallback, $builder);
+ }
// Apply where clauses that were set on the builder instance if any
foreach ($builder->wheres as $key => $value) {
+ if ($key == '__soft_deleted') {
+ if ($this->usesSoftDeletes($builder->model)) {
+ if ($value == 1) {
+ $query->whereNotNull($builder->model->getDeletedAtColumn());
+ } else {
+ $query->whereNull($builder->model->getDeletedAtColumn());
+ }
+ }
+
+ continue;
+ }
$query->where($key, $value);
}
- // If parsed documents are being stored in the model's table
- if (! $this->isExternalIndex($builder->model)) {
- // and the model uses soft deletes we need to exclude trashed rows
- if ($this->usesSoftDeletes($builder->model)) {
- $query->whereNull($builder->model->getDeletedAtColumn());
- }
+ // Apply whereIn clauses that were set on the builder instance if any
+ foreach ($builder->whereIns as $key => $value) {
+ $query->whereIn($key, $value);
+ }
+
+ // Apply whereNoIn clauses that were set on the builder instance if any
+ foreach ($builder->whereNotIns as $key => $value) {
+ $query->whereNotIn($key, $value);
}
// Apply order by clauses that were set on the builder instance if any
@@ -267,7 +420,8 @@ protected function performSearch(Builder $builder, $perPage = 0, $page = 1)
? call_user_func($builder->callback, $builder, $this->searchConfig($builder->model), $query)
: $this->defaultQueryMethod($builder->query, $this->searchConfig($builder->model));
- $query->crossJoin($this->database->raw($tsQuery->sql().' AS "tsquery"'));
+ /** @var \ScoutEngines\Postgres\TsQuery\BaseTsQueryable $tsQuery */
+ $query->crossJoin($this->database->raw($tsQuery->sql() . ' AS "tsquery"'));
// Add TS bindings to the query
$query->addBinding($tsQuery->bindings(), 'join');
@@ -275,74 +429,10 @@ protected function performSearch(Builder $builder, $perPage = 0, $page = 1)
->select($query->toSql(), $query->getBindings());
}
- /**
- * Returns the default query method.
- *
- * @param string $query
- * @param string $config
- * @return \ScoutEngines\Postgres\TsQuery\TsQueryable
- */
- public function defaultQueryMethod($query, $config)
- {
- switch (strtolower($this->config('search_using', 'plain'))) {
- case 'tsquery':
- return new ToTsQuery($query, $config);
- case 'phrasequery':
- return new PhraseToTsQuery($query, $config);
- case 'plainquery':
- default:
- return new PlainToTsQuery($query, $config);
- }
- }
-
- /**
- * Pluck and return the primary keys of the given results.
- *
- * @param mixed $results
- * @return \Illuminate\Support\Collection
- */
- public function mapIds($results)
- {
- $keyName = $this->model ? $this->model->getKeyName() : 'id';
-
- return collect($results)
- ->pluck($keyName)
- ->values();
- }
-
- /**
- * Map the given results to instances of the given model.
- *
- * @param \Laravel\Scout\Builder $builder
- * @param mixed $results
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return \Illuminate\Database\Eloquent\Collection
- */
- public function map(Builder $builder, $results, $model)
- {
- if (empty($results)) {
- return Collection::make();
- }
-
- $keys = $this->mapIds($results);
-
- $results = collect($results);
-
- $models = $model->whereIn($model->getKeyName(), $keys->all())
- ->get()
- ->keyBy($model->getKeyName());
-
- return $results->pluck($model->getKeyName())
- ->intersect($models->keys()) // Filter out no longer existing models (i.e. soft deleted)
- ->map(function ($key) use ($models) {
- return $models[$key];
- });
- }
-
/**
* Connect to the database.
*/
- protected function connect()
+ protected function connect(): void
{
// Already connected
if ($this->database !== null) {
@@ -350,66 +440,55 @@ protected function connect()
}
$connection = $this->resolver
- ->connection($this->config('connection'));
+ ->connection($this->stringConfig('connection'));
- if ($connection->getDriverName() !== 'pgsql') {
- throw new \InvalidArgumentException('Connection should use pgsql driver.');
+ if ($connection instanceof PostgresConnection) {
+ $this->database = $connection;
+ } else {
+ throw new InvalidArgumentException('Connection should use pgsql driver.');
}
-
- $this->database = $connection;
}
/**
* Build ranking expression that will be used in a search.
* ts_rank([ weights, ] vector, query [, normalization ])
* ts_rank_cd([ weights, ] vector, query [, normalization ]).
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @param string $indexColumn
- * @return string
*/
- protected function rankingExpression(Model $model, $indexColumn)
+ protected function rankingExpression(Model $model, string $indexColumn): string
{
$args = collect([$indexColumn, '"tsquery"']);
if ($weights = $this->rankWeights($model)) {
- $args->prepend("'$weights'");
+ $args->prepend("'{$weights}'");
}
if ($norm = $this->rankNormalization($model)) {
- $args->push($norm);
+ $args->push((string) $norm);
}
$fn = $this->rankFunction($model);
- return "$fn({$args->implode(',')})";
+ return "{$fn}({$args->implode(',')})";
}
/**
* Get rank function.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return int
*/
- protected function rankFunction(Model $model)
+ protected function rankFunction(Model $model): string
{
$default = 'ts_rank';
- $function = $this->option($model, 'rank.function', $default);
+ $function = $this->stringOption($model, 'rank.function', $default);
return collect(['ts_rank', 'ts_rank_cd'])->contains($function) ? $function : $default;
}
/**
* Get the rank weight label for a given field.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @param string $field
- * @return string
*/
- protected function rankFieldWeightLabel(Model $model, $field)
+ protected function rankFieldWeightLabel(Model $model, string $field): string
{
- $label = $this->option($model, "rank.fields.$field");
+ $label = $this->stringOption($model, "rank.fields.{$field}");
return collect(['A', 'B', 'C', 'D'])
->contains($label) ? $label : '';
@@ -417,11 +496,8 @@ protected function rankFieldWeightLabel(Model $model, $field)
/**
* Get rank weights.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return string
*/
- protected function rankWeights(Model $model)
+ protected function rankWeights(Model $model): string
{
$weights = $this->option($model, 'rank.weights');
@@ -429,68 +505,53 @@ protected function rankWeights(Model $model)
return '';
}
- return '{'.implode(',', $weights).'}';
+ return '{' . implode(',', $weights) . '}';
}
/**
* Get rank normalization.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return int
*/
- protected function rankNormalization(Model $model)
+ protected function rankNormalization(Model $model): int
{
- return $this->option($model, 'rank.normalization', 0);
+ return $this->intOption($model, 'rank.normalization', 0);
}
/**
* See if the index should be maintained for a given model.
- *
- * @param \Illuminate\Database\Eloquent\Model|null $model
- * @return bool
*/
- protected function shouldMaintainIndex(Model $model = null)
+ protected function shouldMaintainIndex(?Model $model = null): bool
{
if ((bool) $this->config('maintain_index', true) === false) {
return false;
}
if ($model !== null) {
- return $this->option($model, 'maintain_index', true);
+ return (bool) $this->option($model, 'maintain_index', true);
}
+
+ return false;
}
/**
* Get the name of the column that holds indexed documents.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return string
*/
- protected function getIndexColumn(Model $model)
+ protected function getIndexColumn(Model $model): string
{
- return $this->option($model, 'column', 'searchable');
+ return $this->stringOption($model, 'column', 'searchable');
}
/**
* See if indexed documents are stored in a external table.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return mixed
*/
- protected function isExternalIndex(Model $model)
+ protected function isExternalIndex(Model $model): mixed
{
return $this->option($model, 'external', false);
}
/**
* Get the model specific option value or a default.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @param string $key
- * @param mixed $default
- * @return mixed
*/
- protected function option(Model $model, $key, $default = null)
+ protected function option(Model $model, string $key, mixed $default = null): mixed
{
if (! method_exists($model, 'searchableOptions')) {
return $default;
@@ -502,63 +563,73 @@ protected function option(Model $model, $key, $default = null)
}
/**
- * Get the config value or a default.
- *
- * @param string $key
- * @param mixed $default
- * @return mixed
+ * Get the model specific option value or a default as an int.
*/
- protected function config($key, $default = null)
+ protected function intOption(Model $model, string $key, int $default): int
{
- return Arr::get($this->config, $key, $default);
+ $value = $this->option($model, $key, $default);
+
+ if (is_int($value)) {
+ return $value;
+ } else {
+ return $default;
+ }
}
/**
- * @param \Illuminate\Database\Eloquent\Model $model
+ * Get the model specific option value or a default as a string.
*/
- protected function preserveModel(Model $model)
+ protected function stringOption(Model $model, string $key, string $default = ''): string
{
- $this->model = $model;
+ $value = $this->option($model, $key, $default);
+
+ if (is_string($value)) {
+ return $value;
+ } else {
+ return $default;
+ }
}
/**
- * Returns a search config name for a model.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return string
+ * Get the config value or a default.
*/
- protected function searchConfig(Model $model)
+ protected function config(string $key, mixed $default = null): mixed
{
- return $this->option($model, 'config', $this->config('config', '')) ?: null;
+ return Arr::get($this->config, $key, $default);
}
/**
- * Checks if the model uses the SoftDeletes trait.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return bool
+ * Get the config value or a default as a string.
*/
- protected function usesSoftDeletes(Model $model)
+ protected function stringConfig(string $key, string $default = ''): string
{
- return method_exists($model, 'getDeletedAtColumn');
+ $value = $this->config($key, $default);
+
+ if (is_string($value)) {
+ return $value;
+ } else {
+ return $default;
+ }
+ }
+
+ protected function preserveModel(Model $model): void
+ {
+ $this->model = $model;
}
/**
- * Flush all of the model's records from the engine.
- *
- * @param \Illuminate\Database\Eloquent\Model $model
- * @return void
+ * Returns a search config name for a model.
*/
- public function flush($model)
+ protected function searchConfig(Model $model): string
{
- if (! $this->shouldMaintainIndex($model)) {
- return;
- }
-
- $indexColumn = $this->getIndexColumn($model);
+ return $this->stringOption($model, 'config', $this->stringConfig('config', '')) ?: '';
+ }
- $this->database
- ->table($model->searchableAs())
- ->update([$indexColumn => null]);
+ /**
+ * Checks if the model uses the SoftDeletes trait.
+ */
+ protected function usesSoftDeletes(Model $model): bool
+ {
+ return method_exists($model, 'getDeletedAtColumn');
}
}
diff --git a/src/PostgresEngineServiceProvider.php b/src/PostgresEngineServiceProvider.php
index 3946ffd..81d3cd1 100644
--- a/src/PostgresEngineServiceProvider.php
+++ b/src/PostgresEngineServiceProvider.php
@@ -10,9 +10,16 @@
use ScoutEngines\Postgres\TsQuery\ToTsQuery;
use ScoutEngines\Postgres\TsQuery\WebSearchToTsQuery;
+/**
+ * @template TModel of \Illuminate\Database\Eloquent\Model
+ * @template TID of int|string
+ */
class PostgresEngineServiceProvider extends ServiceProvider
{
- public static function builderMacros()
+ /**
+ * @return array
+ */
+ public static function builderMacros(): array
{
return [
'usingPhraseQuery' => PhraseToTsQuery::class,
@@ -22,13 +29,20 @@ public static function builderMacros()
];
}
- public function boot()
+ /**
+ * Bootstrap the application events.
+ */
+ public function boot(): void
{
$this->app->make(EngineManager::class)->extend('pgsql', function () {
- return new PostgresEngine(
- $this->app->get('db'),
- $this->app->get('config')->get('scout.pgsql', [])
- );
+ /** @var \Illuminate\Database\ConnectionResolverInterface $db */
+ $db = $this->app->get('db');
+ /** @var \Illuminate\Support\Facades\Config $config */
+ $config = $this->app->get('config');
+ /** @var array $pgScoutConfig */
+ $pgScoutConfig = $config->get('scout.pgsql', []);
+
+ return new PostgresEngine($db, $pgScoutConfig);
});
foreach (self::builderMacros() as $macro => $class) {
@@ -36,10 +50,11 @@ public function boot()
}
}
- protected function registerBuilderMacro($name, $class)
+ protected function registerBuilderMacro(string $name, string $class): void
{
if (! Builder::hasMacro($name)) {
Builder::macro($name, function () use ($class) {
+ /** @var Builder $this */
$this->callback = function ($builder, $config) use ($class) {
return new $class($builder->query, $config);
};
diff --git a/src/TsQuery/BaseTsQueryable.php b/src/TsQuery/BaseTsQueryable.php
index 48dddad..fb02a75 100644
--- a/src/TsQuery/BaseTsQueryable.php
+++ b/src/TsQuery/BaseTsQueryable.php
@@ -28,8 +28,8 @@ abstract class BaseTsQueryable implements TsQueryable
/**
* Create a new instance.
*
- * @param string $query
- * @param string $config
+ * @param string $query
+ * @param string $config
*/
public function __construct($query, $config = null)
{
@@ -50,7 +50,7 @@ public function sql()
/**
* Return value bindings for the SQL representation.
*
- * @return array
+ * @return array
*/
public function bindings()
{
diff --git a/src/TsQuery/TsQueryable.php b/src/TsQuery/TsQueryable.php
index 836a340..4f53439 100644
--- a/src/TsQuery/TsQueryable.php
+++ b/src/TsQuery/TsQueryable.php
@@ -14,7 +14,7 @@ public function sql();
/**
* Return value bindings for the SQL representation.
*
- * @return array
+ * @return array
*/
public function bindings();
}
diff --git a/tests/PostgresEngineTest.php b/tests/PostgresEngineTest.php
index f3a060e..f475d36 100644
--- a/tests/PostgresEngineTest.php
+++ b/tests/PostgresEngineTest.php
@@ -2,84 +2,106 @@
namespace ScoutEngines\Postgres\Test;
-use Illuminate\Database\Connection;
+use Exception;
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Database\PostgresConnection;
use Laravel\Scout\Builder;
use Mockery;
use ScoutEngines\Postgres\PostgresEngine;
class PostgresEngineTest extends TestCase
{
- public function test_it_can_be_instantiated()
+ /**
+ * @test
+ */
+ public function it_can_be_instantiated()
{
[$engine] = $this->getEngine();
$this->assertInstanceOf(PostgresEngine::class, $engine);
}
- public function test_update_adds_object_to_index()
+ /**
+ * @test
+ */
+ public function update_adds_object_to_index()
{
[$engine, $db] = $this->getEngine();
$db->shouldReceive('query')
- ->andReturn($query = Mockery::mock('stdClass'));
+ ->andReturn($query = Mockery::mock('stdClass'))->once();
$query->shouldReceive('selectRaw')
->with(
'to_tsvector(COALESCE(?, get_current_ts_config()), ?) || setweight(to_tsvector(COALESCE(?, get_current_ts_config()), ?), ?) AS tsvector',
[null, 'Foo', null, '', 'B']
)
- ->andReturnSelf();
+ ->andReturnSelf()->once();
$query->shouldReceive('value')
->with('tsvector')
- ->andReturn('foo');
+ ->andReturn('foo')->once();
$db->shouldReceive('table')
->andReturn($table = Mockery::mock('stdClass'));
$table->shouldReceive('where')
->with('id', '=', 1)
- ->andReturnSelf();
+ ->andReturnSelf()->once();
$table->shouldReceive('update')
- ->with(['searchable' => 'foo']);
+ ->with(['searchable' => 'foo'])->once();
- $engine->update(Collection::make([new TestModel()]));
+ $engine->update(Collection::make([new TestModel]));
}
- public function test_update_do_nothing_if_index_maintenance_turned_off_globally()
+ /**
+ * @test
+ */
+ public function update_do_nothing_if_index_maintenance_turned_off_globally()
{
[$engine] = $this->getEngine(['maintain_index' => false]);
- $engine->update(Collection::make([new TestModel()]));
+ $this->assertNull($engine->update(Collection::make([new TestModel])));
}
- public function test_delete_removes_object_from_index()
+ /**
+ * @test
+ */
+ public function delete_removes_object_from_index()
{
[$engine, $db] = $this->getEngine();
$db->shouldReceive('table')
- ->andReturn($table = Mockery::mock('stdClass'));
+ ->andReturn($table = Mockery::mock('stdClass'))
+ ->once();
$table->shouldReceive('whereIn')
->with('id', [1])
- ->andReturnSelf();
+ ->andReturnSelf()
+ ->once();
$table->shouldReceive('update')
- ->with(['searchable' => null]);
+ ->with(['searchable' => null])
+ ->once();
- $engine->delete(Collection::make([new TestModel()]));
+ $engine->delete(Collection::make([new TestModel]));
}
- public function test_delete_do_nothing_if_index_maintenance_turned_off_globally()
+ /**
+ * @test
+ */
+ public function delete_do_nothing_if_index_maintenance_turned_off_globally()
{
[$engine, $db] = $this->getEngine(['maintain_index' => false]);
$db->shouldNotReceive('table');
- $engine->delete(Collection::make([new TestModel()]));
+ $engine->delete(Collection::make([new TestModel]));
}
- public function test_flush_removes_all_objects_from_index()
+ /**
+ * @test
+ */
+ public function flush_removes_all_objects_from_index()
{
[$engine, $db] = $this->getEngine();
@@ -90,19 +112,25 @@ public function test_flush_removes_all_objects_from_index()
->once()
->with(['searchable' => null]);
- $engine->flush(new TestModel());
+ $engine->flush(new TestModel);
}
- public function test_flush_does_nothing_if_index_maintenance_turned_off_globally()
+ /**
+ * @test
+ */
+ public function flush_does_nothing_if_index_maintenance_turned_off_globally()
{
[$engine, $db] = $this->getEngine(['maintain_index' => false]);
$db->shouldNotReceive('table');
- $engine->flush(new TestModel());
+ $engine->flush(new TestModel);
}
- public function test_search()
+ /**
+ * @test
+ */
+ public function search()
{
[$engine, $db] = $this->getEngine();
@@ -117,9 +145,10 @@ public function test_search()
->shouldReceive('getBindings')->andReturn([null, 'foo', 1, 'qux']);
$db->shouldReceive('select')
- ->with(null, $table->getBindings());
+ ->with(null, $table->getBindings())
+ ->once();
- $builder = new Builder(new TestModel(), 'foo');
+ $builder = new Builder(new TestModel, 'foo');
$builder->where('bar', 1)
->where('baz', 'qux')
->take(5);
@@ -127,7 +156,10 @@ public function test_search()
$engine->search($builder);
}
- public function test_search_with_order_by()
+ /**
+ * @test
+ */
+ public function search_with_order_by()
{
[$engine, $db] = $this->getEngine();
@@ -138,16 +170,99 @@ public function test_search_with_order_by()
->shouldReceive('getBindings')->andReturn([null, 'foo']);
$db->shouldReceive('select')
- ->with(null, $table->getBindings());
+ ->with(null, $table->getBindings())
+ ->once();
- $builder = new Builder(new TestModel(), 'foo');
+ $builder = new Builder(new TestModel, 'foo');
$builder->orderBy('bar', 'desc')
->orderBy('baz', 'asc');
$engine->search($builder);
}
- public function test_search_with_global_config()
+ /**
+ * @test
+ */
+ public function search_with_queryCallback()
+ {
+ [$engine, $db] = $this->getEngine();
+
+ $skip = 0;
+ $limit = 5;
+ $table = $this->setDbExpectations($db);
+
+ $table->shouldReceive('skip')->with($skip)->andReturnSelf()
+ ->shouldReceive('limit')->with($limit)->andReturnSelf()
+ ->shouldReceive('where')->with('bar', 1)->andReturnSelf()
+ ->shouldReceive('where')->with('baz', 'qux')
+ ->shouldReceive('getBindings')->andReturn([null, 'foo', 1, 'qux']);
+
+ $db->shouldReceive('select')
+ ->with(null, $table->getBindings())
+ ->once();
+
+ $builder = new Builder(new TestModel, 'foo');
+ $builder->query(function ($q) {
+ $q->where('bar', 1)
+ ->where('baz', 'qux')
+ ->take(5);
+ });
+
+ $engine->search($builder);
+ }
+
+ /**
+ * @test
+ */
+ public function search_with_whereIn()
+ {
+ [$engine, $db] = $this->getEngine();
+
+ $skip = 0;
+ $limit = 5;
+ $table = $this->setDbExpectations($db);
+
+ $table->shouldReceive('whereIn')->with('bar', [1])->andReturnSelf()
+ ->shouldReceive('getBindings')->andReturn([null, 'foo', [1]]);
+
+ $db->shouldReceive('select')
+ ->with(null, $table->getBindings())
+ ->once();
+
+ $builder = new Builder(new TestModel, 'foo');
+ $builder->whereIn('bar', [1]);
+
+ $engine->search($builder);
+ }
+
+ /**
+ * @test
+ */
+ public function search_with_whereNotIn()
+ {
+ [$engine, $db] = $this->getEngine();
+
+ $skip = 0;
+ $limit = 5;
+ $table = $this->setDbExpectations($db);
+
+ $table->shouldReceive('whereNotIn')->with('bar', [1])->andReturnSelf()
+ ->shouldReceive('getBindings')->andReturn([null, 'foo', [1]]);
+
+ $db->shouldReceive('select')
+ ->with(null, $table->getBindings())
+ ->once();
+
+ $builder = new Builder(new TestModel, 'foo');
+ $builder->whereNotIn('bar', [1]);
+
+ $engine->search($builder);
+ }
+
+ /**
+ * @test
+ */
+ public function search_with_global_config()
{
[$engine, $db] = $this->getEngine(['config' => 'simple']);
@@ -160,15 +275,18 @@ public function test_search_with_global_config()
->shouldReceive('where')->with('bar', 1)
->shouldReceive('getBindings')->andReturn(['simple', 'foo', 1]);
- $db->shouldReceive('select')->with(null, $table->getBindings());
+ $db->shouldReceive('select')->with(null, $table->getBindings())->once();
- $builder = new Builder(new TestModel(), 'foo');
+ $builder = new Builder(new TestModel, 'foo');
$builder->where('bar', 1)->take(5);
$engine->search($builder);
}
- public function test_search_with_model_config()
+ /**
+ * @test
+ */
+ public function search_with_model_config()
{
[$engine, $db] = $this->getEngine(['config' => 'simple']);
@@ -181,9 +299,9 @@ public function test_search_with_model_config()
->shouldReceive('where')->with('bar', 1)
->shouldReceive('getBindings')->andReturn(['english', 'foo', 1]);
- $db->shouldReceive('select')->with(null, $table->getBindings());
+ $db->shouldReceive('select')->with(null, $table->getBindings())->once();
- $model = new TestModel();
+ $model = new TestModel;
$model->searchableOptions['config'] = 'english';
$builder = new Builder($model, 'foo');
@@ -192,7 +310,10 @@ public function test_search_with_model_config()
$engine->search($builder);
}
- public function test_search_with_soft_deletes()
+ /**
+ * @test
+ */
+ public function search_with_soft_deletes()
{
[$engine, $db] = $this->getEngine();
@@ -204,22 +325,25 @@ public function test_search_with_soft_deletes()
->shouldReceive('whereNull')->with('deleted_at')
->shouldReceive('getBindings')->andReturn([null, 'foo', 1]);
- $db->shouldReceive('select')->with(null, $table->getBindings());
+ $db->shouldReceive('select')->with(null, $table->getBindings())->once();
- $builder = new Builder(new SoftDeletableTestModel(), 'foo');
+ $builder = new Builder(new SoftDeletableTestModel, 'foo');
$builder->where('bar', 1)->take(5);
$engine->search($builder);
}
- public function test_maps_results_to_models()
+ /**
+ * @test
+ */
+ public function maps_results_to_models()
{
[$engine] = $this->getEngine();
$model = Mockery::mock('StdClass');
$model->shouldReceive('getKeyName')->andReturn('id');
$model->shouldReceive('whereIn')->once()->with('id', [1])->andReturn($model);
- $model->shouldReceive('get')->once()->andReturn(Collection::make([new TestModel()]));
+ $model->shouldReceive('get')->once()->andReturn(Collection::make([new TestModel]));
$results = $engine->map(
new Builder(new TestModel, 'foo'),
@@ -230,7 +354,10 @@ public function test_maps_results_to_models()
$this->assertCount(1, $results);
}
- public function test_map_filters_out_no_longer_existing_models()
+ /**
+ * @test
+ */
+ public function map_filters_out_no_longer_existing_models()
{
[$engine] = $this->getEngine();
@@ -238,7 +365,7 @@ public function test_map_filters_out_no_longer_existing_models()
$model->shouldReceive('getKeyName')->andReturn('id');
$model->shouldReceive('whereIn')->once()->with('id', [1, 2])->andReturn($model);
- $expectedModel = new SoftDeletableTestModel();
+ $expectedModel = new SoftDeletableTestModel;
$expectedModel->id = 2;
$model->shouldReceive('get')->once()->andReturn(Collection::make([$expectedModel]));
@@ -253,7 +380,10 @@ public function test_map_filters_out_no_longer_existing_models()
$this->assertEquals(2, $models->first()->id);
}
- public function test_it_returns_total_count()
+ /**
+ * @test
+ */
+ public function it_returns_total_count()
{
[$engine] = $this->getEngine();
@@ -264,7 +394,10 @@ public function test_it_returns_total_count()
$this->assertEquals(100, $count);
}
- public function test_map_ids_returns_right_key()
+ /**
+ * @test
+ */
+ public function map_ids_returns_right_key()
{
[$engine, $db] = $this->getEngine();
@@ -282,11 +415,37 @@ public function test_map_ids_returns_right_key()
$this->assertEquals([1, 2], $ids->all());
}
+ /**
+ * @test
+ */
+ public function create_index()
+ {
+ [$engine, $db] = $this->getEngine();
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('PostgreSQL indexes should be created through Laravel database migrations.');
+
+ $engine->createIndex('bad_index');
+ }
+
+ /**
+ * @test
+ */
+ public function delete_index()
+ {
+ [$engine, $db] = $this->getEngine();
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('PostgreSQL indexes should be deleted through Laravel database migrations.');
+
+ $engine->deleteIndex('bad_index');
+ }
+
protected function getEngine($config = [])
{
$resolver = Mockery::mock(ConnectionResolverInterface::class);
$resolver->shouldReceive('connection')
- ->andReturn($db = Mockery::mock(Connection::class));
+ ->andReturn($db = Mockery::mock(PostgresConnection::class));
$db->shouldReceive('getDriverName')->andReturn('pgsql');
@@ -302,30 +461,30 @@ protected function setDbExpectations($db, $withDefaultOrderBy = true)
->andReturn('plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS "tsquery"');
$table->shouldReceive('crossJoin')
- ->with('plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS "tsquery"')
- ->andReturnSelf()
+ ->with('plainto_tsquery(COALESCE(?, get_current_ts_config()), ?) AS "tsquery"')
+ ->andReturnSelf()
->shouldReceive('addBinding')
- ->with(Mockery::type('array'), 'join')
- ->andReturnSelf()
+ ->with(Mockery::type('array'), 'join')
+ ->andReturnSelf()
->shouldReceive('select')
- ->with('id')
- ->andReturnSelf()
+ ->with('id')
+ ->andReturnSelf()
->shouldReceive('selectRaw')
- ->with('ts_rank(searchable,"tsquery") AS rank')
- ->andReturnSelf()
+ ->with('ts_rank(searchable,"tsquery") AS rank')
+ ->andReturnSelf()
->shouldReceive('selectRaw')
- ->with('COUNT(*) OVER () AS total_count')
- ->andReturnSelf()
+ ->with('COUNT(*) OVER () AS total_count')
+ ->andReturnSelf()
->shouldReceive('whereRaw')
- ->andReturnSelf();
+ ->andReturnSelf();
if ($withDefaultOrderBy) {
$table->shouldReceive('orderBy')
- ->with('rank', 'desc')
- ->andReturnSelf()
+ ->with('rank', 'desc')
+ ->andReturnSelf()
->shouldReceive('orderBy')
- ->with('id')
- ->andReturnSelf();
+ ->with('id')
+ ->andReturnSelf();
}
$table->shouldReceive('toSql');