From e1dda34625673091b45fbb52d0b82753d3926ef6 Mon Sep 17 00:00:00 2001 From: MeiKatz Date: Fri, 25 Feb 2022 10:57:04 +0100 Subject: [PATCH 1/4] [feature] add support for SQLite3 --- README.md | 2 +- src/ModelToSearchThrough.php | 3 ++- src/Searcher.php | 32 +++++++++++++++++++++++++++++--- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 96d92e9..6b29480 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Hey! We've built a Docker-based deployment tool to launch apps and sites fully c ## Requirements * PHP 8.0 + 8.1 -* MySQL 5.7+ +* MySQL 5.7+ / SQLite3 * Laravel 8.0 or 9.0 ## Features diff --git a/src/ModelToSearchThrough.php b/src/ModelToSearchThrough.php index c929bc9..a616f28 100644 --- a/src/ModelToSearchThrough.php +++ b/src/ModelToSearchThrough.php @@ -138,7 +138,8 @@ public function getModel(): Model */ public function getModelKey($suffix = 'key'): string { - return implode('_', [ + // prefix with _ for SQLite support + return '_' . implode('_', [ $this->key, Str::snake(class_basename($this->getModel())), $suffix, diff --git a/src/Searcher.php b/src/Searcher.php index 652f9ce..03cd4da 100644 --- a/src/Searcher.php +++ b/src/Searcher.php @@ -509,12 +509,23 @@ private function addRelevanceQueryToBuilder($builder, $modelToSearchThrough) throw OrderByRelevanceException::new(); } + $lengthFunctionName = ( + $this->isCurrentConnectionSqlite() + ? 'LENGTH' + : 'CHAR_LENGTH' + ); + $expressionsAndBindings = $modelToSearchThrough->getQualifiedColumns()->flatMap(function ($field) { $field = (new MySqlGrammar)->wrap($field); return $this->termsWithoutWildcards->map(function ($term) use ($field) { + return [ - 'expression' => "COALESCE(CHAR_LENGTH(LOWER({$field})) - CHAR_LENGTH(REPLACE(LOWER({$field}), ?, ?)), 0)", + 'expression' => sprintf( + 'COALESCE(%1$s(LOWER(%2$s)) - %1$s(REPLACE(LOWER(%2$s), ?, ?)), 0)', + $lengthFunctionName, + $field + ), 'bindings' => [Str::lower($term), Str::substr(Str::lower($term), 1)], ]; }); @@ -574,7 +585,7 @@ protected function makeOrderBy(): string { $modelOrderKeys = $this->modelsToSearchThrough->map->getModelKey('order')->implode(','); - return "COALESCE({$modelOrderKeys})"; + return "COALESCE({$modelOrderKeys}, NULL)"; } /** @@ -587,7 +598,7 @@ protected function makeOrderByModel(): string { $modelOrderKeys = $this->modelsToSearchThrough->map->getModelKey('model_order')->implode(','); - return "COALESCE({$modelOrderKeys})"; + return "COALESCE({$modelOrderKeys}, NULL)"; } /** @@ -776,4 +787,19 @@ public function search(string $terms = null) ->pipe(fn (Collection $models) => new EloquentCollection($models)) ->when($this->pageName, fn (EloquentCollection $models) => $results->setCollection($models)); } + + /** + * @return string Returns current connection driver as string + */ + private function getCurrentConnectionDriver(): string { + $connection = config('database.default'); + return config("database.connections.{$connection}.driver"); + } + + /** + * @return bool Returns true if the current connection driver is SQLite, otherwise false. + */ + private function isCurrentConnectionSqlite(): bool { + return $this->getCurrentConnectionDriver() === 'sqlite'; + } } From 7806b1dcd519ab074235309ede3f9fe4df4c1c33 Mon Sep 17 00:00:00 2001 From: MeiKatz Date: Fri, 25 Feb 2022 11:04:01 +0100 Subject: [PATCH 2/4] [fix] pass variable lengthFunctionName to most inner function scope --- src/Searcher.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Searcher.php b/src/Searcher.php index 03cd4da..0ee31a3 100644 --- a/src/Searcher.php +++ b/src/Searcher.php @@ -515,10 +515,10 @@ private function addRelevanceQueryToBuilder($builder, $modelToSearchThrough) : 'CHAR_LENGTH' ); - $expressionsAndBindings = $modelToSearchThrough->getQualifiedColumns()->flatMap(function ($field) { + $expressionsAndBindings = $modelToSearchThrough->getQualifiedColumns()->flatMap(function ($field) use ($lengthFunctionName) { $field = (new MySqlGrammar)->wrap($field); - return $this->termsWithoutWildcards->map(function ($term) use ($field) { + return $this->termsWithoutWildcards->map(function ($term) use ($field, $lengthFunctionName) { return [ 'expression' => sprintf( From 53bd87d6e84c47b90def05f02b3e471f59388621 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Tue, 19 Apr 2022 09:52:14 +0200 Subject: [PATCH 3/4] Refactor + added tests --- .github/workflows/run-tests.yml | 125 +++++++++++++++++--------------- src/Searcher.php | 27 +++---- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 47f3119..2771271 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -3,61 +3,70 @@ name: run-tests on: [push, pull_request] jobs: - test: - runs-on: ubuntu-latest - strategy: - fail-fast: true - matrix: - php: [8.1, 8.0] - laravel: [9.*, 8.*] - dependency-version: [prefer-lowest, prefer-stable] - include: - - laravel: 9.* - testbench: 7.* - - laravel: 8.* - testbench: 6.* - - name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - - services: - mysql: - image: mysql:5.7 - env: - MYSQL_ALLOW_EMPTY_PASSWORD: no - MYSQL_USER: protone_media_db_test - MYSQL_DATABASE: protone_media_db_test - MYSQL_PASSWORD: secret - MYSQL_ROOT_PASSWORD: secret - ports: - - 3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Cache dependencies - uses: actions/cache@v2 - with: - path: ~/.composer/cache/files - key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, mysql, mysqli, pdo_mysql - coverage: none - - - name: Install dependencies - run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update - composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest - - - name: Execute tests - run: vendor/bin/phpunit - env: - DB_DATABASE: protone_media_db_test - DB_USERNAME: protone_media_db_test - DB_PASSWORD: secret - DB_PORT: ${{ job.services.mysql.ports[3306] }} + test: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + php: [8.1, 8.0] + laravel: [9.*, 8.*] + db: [mysql, sqlite] + dependency-version: [prefer-lowest, prefer-stable] + include: + - laravel: 9.* + testbench: 7.* + - laravel: 8.* + testbench: 6.* + + name: P${{ matrix.php }} - L${{ matrix.laravel }} - DB ${{ matrix.db }} - ${{ matrix.dependency-version }} + + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: no + MYSQL_USER: protone_media_db_test + MYSQL_DATABASE: protone_media_db_test + MYSQL_PASSWORD: secret + MYSQL_ROOT_PASSWORD: secret + ports: + - 3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ~/.composer/cache/files + key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, mysql, mysqli, pdo_mysql + coverage: none + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest + + - name: Execute tests (MySQL) + run: vendor/bin/phpunit + if: ${{ matrix.db == 'mysql' }} + env: + DB_DATABASE: protone_media_db_test + DB_USERNAME: protone_media_db_test + DB_PASSWORD: secret + DB_PORT: ${{ job.services.mysql.ports[3306] }} + + - name: Execute tests (SQLite) + run: vendor/bin/phpunit + if: ${{ matrix.db == 'sqlite' }} + env: + DB_CONNECTION: sqlite + DB_DATABASE: ":memory:" diff --git a/src/Searcher.php b/src/Searcher.php index 0ee31a3..738ac39 100644 --- a/src/Searcher.php +++ b/src/Searcher.php @@ -8,6 +8,7 @@ use Illuminate\Database\Query\Builder as BaseBuilder; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Grammars\MySqlGrammar; +use Illuminate\Database\SQLiteConnection; use Illuminate\Pagination\Paginator; use Illuminate\Support\Arr; use Illuminate\Support\Collection; @@ -509,24 +510,19 @@ private function addRelevanceQueryToBuilder($builder, $modelToSearchThrough) throw OrderByRelevanceException::new(); } - $lengthFunctionName = ( - $this->isCurrentConnectionSqlite() - ? 'LENGTH' - : 'CHAR_LENGTH' - ); + $lengthFunctionName = $this->usesSQLiteConnection() ? 'LENGTH' : 'CHAR_LENGTH'; $expressionsAndBindings = $modelToSearchThrough->getQualifiedColumns()->flatMap(function ($field) use ($lengthFunctionName) { $field = (new MySqlGrammar)->wrap($field); return $this->termsWithoutWildcards->map(function ($term) use ($field, $lengthFunctionName) { - return [ 'expression' => sprintf( 'COALESCE(%1$s(LOWER(%2$s)) - %1$s(REPLACE(LOWER(%2$s), ?, ?)), 0)', $lengthFunctionName, $field ), - 'bindings' => [Str::lower($term), Str::substr(Str::lower($term), 1)], + 'bindings' => [Str::lower($term), Str::substr(Str::lower($term), 1)], ]; }); }); @@ -789,17 +785,12 @@ public function search(string $terms = null) } /** - * @return string Returns current connection driver as string - */ - private function getCurrentConnectionDriver(): string { - $connection = config('database.default'); - return config("database.connections.{$connection}.driver"); - } - - /** - * @return bool Returns true if the current connection driver is SQLite, otherwise false. + * Returns true if the current connection driver is SQLite, otherwise false. + * + * @return bool */ - private function isCurrentConnectionSqlite(): bool { - return $this->getCurrentConnectionDriver() === 'sqlite'; + private function usesSQLiteConnection(): bool + { + return $this->modelsToSearchThrough->first()->getModel()->getConnection() instanceof SQLiteConnection; } } From 11e7ef12ea73e502f89555f5e2d96632ed05b9c7 Mon Sep 17 00:00:00 2001 From: Pascal Baljet Date: Tue, 19 Apr 2022 09:58:10 +0200 Subject: [PATCH 4/4] Update create_tables.php --- tests/create_tables.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/create_tables.php b/tests/create_tables.php index 37505f2..a03fc86 100644 --- a/tests/create_tables.php +++ b/tests/create_tables.php @@ -43,9 +43,11 @@ public function up() $table->string('subtitle'); $table->string('body'); - $table->fullText('title'); - $table->fullText(['title', 'subtitle']); - $table->fullText(['title', 'subtitle', 'body']); + if (config('database.default') === 'mysql') { + $table->fullText('title'); + $table->fullText(['title', 'subtitle']); + $table->fullText(['title', 'subtitle', 'body']); + } $table->unsignedInteger('video_id')->nullable(); @@ -58,9 +60,11 @@ public function up() $table->string('subtitle')->nullable(); $table->string('body')->nullable(); - $table->fullText('title'); - $table->fullText(['title', 'subtitle']); - $table->fullText(['title', 'subtitle', 'body']); + if (config('database.default') === 'mysql') { + $table->fullText('title'); + $table->fullText(['title', 'subtitle']); + $table->fullText(['title', 'subtitle', 'body']); + } $table->unsignedInteger('video_id')->nullable();