diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..afde5bb9d --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,99 @@ +version: 2.1 + +orbs: + ruby: circleci/ruby@1.0 + +workflows: + test: + jobs: + - test: + name: "Sphinx 2.2" + sphinx_version: 2.2.11 + sphinx_engine: sphinx + debian: jessie + ruby: '2.4.6' + matrix: + parameters: + database: [ 'mysql2', 'postgresql' ] + rails: [ '4_2', '5_0', '5_1', '5_2' ] + +jobs: + test: + parameters: + ruby: + type: string + rails: + type: string + database: + type: string + sphinx_version: + type: string + sphinx_engine: + type: string + debian: + type: string + + docker: + - image: circleci/ruby:<< parameters.ruby >>-<< parameters.debian >> + + - image: circleci/postgres:10 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: thinking_sphinx + POSTGRES_DB: thinking_sphinx + + - image: circleci/mysql:5.7 + environment: + MYSQL_ROOT_PASSWORD: thinking_sphinx + MYSQL_DATABASE: thinking_sphinx + + working_directory: ~/app + + steps: + - checkout + + - restore_cache: + keys: + - v1-dependencies-<< parameters.ruby >>-<< parameters.rails >> + + - run: + name: install bundler + command: | + export BUNDLER_VERSION=1.17.3 + export BUNDLE_PATH=vendor/bundle + gem install bundler:$BUNDLER_VERSION + + - run: + name: install dependencies + command: | + bundle install --jobs=4 --retry=3 --path vendor/bundle + bundle update + + - run: + name: set up appraisal + command: bundle exec appraisal generate + + - run: + name: update gems + environment: + BUNDLE_GEMFILE: "./gemfiles/rails_<< parameters.rails >>.gemfile" + command: bundle update + + - save_cache: + paths: + - ./vendor/bundle + key: v1-dependencies-<< parameters.ruby >>-<< parameters.rails >> + + - run: + name: set up sphinx + command: "./bin/loadsphinx << parameters.sphinx_version >> << parameters.sphinx_engine >>" + + - run: + name: tests + environment: + CI: "true" + DATABASE: << parameters.database >> + SPHINX_VERSION: << parameters.sphinx_version >> + SPHINX_ENGINE: << parameters.sphinx_engine >> + BUNDLE_GEMFILE: "./gemfiles/rails_<< parameters.rails >>.gemfile" + command: bundle exec rspec diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml new file mode 100644 index 000000000..3a026b4e5 --- /dev/null +++ b/.github/actions/test/action.yml @@ -0,0 +1,46 @@ +name: "Test" +description: "Run RSpec in given environment" +inputs: + ruby-version: + description: "Ruby version" + required: true + rails-version: + description: "Rails version" + required: true + sphinx-version: + description: "Sphinx version" + required: true + sphinx-engine: + description: "Sphinx engine" + required: true + database: + description: "Database engine" + required: true +runs: + using: "composite" + steps: + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ inputs.ruby-version }} + bundler-cache: true + - name: Set up Sphinx + shell: bash + run: | + ./bin/loadsphinx ${{ inputs.sphinx-version }} ${{ inputs.sphinx-engine }} + - name: Set up Appraisal + shell: bash + run: "bundle exec appraisal generate" + - name: Install Appraisal's gems + shell: bash + env: + BUNDLE_GEMFILE: "gemfiles/rails_${{ inputs.rails-version }}.gemfile" + run: bundle install + - name: Test + shell: bash + env: + CI: "true" + DATABASE: ${{ inputs.database }} + SPHINX_VERSION: ${{ inputs.sphinx-version }} + SPHINX_ENGINE: ${{ inputs.sphinx-engine }} + BUNDLE_GEMFILE: "gemfiles/rails_${{ inputs.rails-version }}.gemfile" + run: "bundle exec rspec" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..79b850124 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,129 @@ +name: test + +on: [push, pull_request] + +jobs: + sphinx: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + ruby: [ '2.7', '3.0', '3.1', '3.2' ] + rails: [ '5_0', '5_1', '5_2', '6_0', '6_1', '7_0', '7_1' ] + database: [ 'mysql2', 'postgresql' ] + sphinx_version: [ '2.2.11', '3.4.1' ] + sphinx_engine: [ 'sphinx' ] + exclude: + - database: 'postgresql' + sphinx_version: '3.4.1' + sphinx_engine: 'sphinx' + - ruby: '3.0' + rails: '5_0' + - ruby: '3.0' + rails: '5_1' + - ruby: '3.0' + rails: '5_2' + - ruby: '3.1' + rails: '5_0' + - ruby: '3.1' + rails: '5_1' + - ruby: '3.1' + rails: '5_2' + - ruby: '3.2' + rails: '5_0' + - ruby: '3.2' + rails: '5_1' + - ruby: '3.2' + rails: '5_2' + + services: + postgres: + image: postgres:10 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: thinking_sphinx + POSTGRES_DB: thinking_sphinx + ports: ['5432:5432'] + # needed because the postgres container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + mysql: + image: mysql:5.7 + env: + MYSQL_ROOT_PASSWORD: thinking_sphinx + MYSQL_DATABASE: thinking_sphinx + ports: ['3306:3306'] + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/test + with: + ruby-version: ${{ matrix.ruby }} + rails-version: ${{ matrix.rails }} + sphinx-version: ${{ matrix.sphinx_version }} + sphinx-engine: ${{ matrix.sphinx_engine }} + database: ${{ matrix.database }} + timeout-minutes: 12 + + manticore: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + ruby: [ '2.7', '3.0', '3.1', '3.2' ] + rails: [ '5_0', '5_1', '5_2', '6_0', '6_1', '7_0', '7_1' ] + database: [ 'mysql2', 'postgresql' ] + sphinx_version: [ '4.0.2', '6.0.0' ] + sphinx_engine: [ 'manticore' ] + exclude: + - ruby: '3.0' + rails: '5_0' + - ruby: '3.0' + rails: '5_1' + - ruby: '3.0' + rails: '5_2' + - ruby: '3.1' + rails: '5_0' + - ruby: '3.1' + rails: '5_1' + - ruby: '3.1' + rails: '5_2' + - ruby: '3.2' + rails: '5_0' + - ruby: '3.2' + rails: '5_1' + - ruby: '3.2' + rails: '5_2' + + services: + postgres: + image: postgres:10 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: thinking_sphinx + POSTGRES_DB: thinking_sphinx + ports: ['5432:5432'] + # needed because the postgres container does not provide a healthcheck + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + + mysql: + image: mysql:8.0 + env: + MYSQL_ROOT_PASSWORD: thinking_sphinx + MYSQL_DATABASE: thinking_sphinx + ports: ['3306:3306'] + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + - uses: actions/checkout@v2 + - uses: ./.github/actions/test + with: + ruby-version: ${{ matrix.ruby }} + rails-version: ${{ matrix.rails }} + sphinx-version: ${{ matrix.sphinx_version }} + sphinx-engine: ${{ matrix.sphinx_engine }} + database: ${{ matrix.database }} + timeout-minutes: 12 diff --git a/.gitignore b/.gitignore index 3ef667d9f..66ac7a2c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,17 @@ *.gem +*.sublime-* .bundle +.overmind.env .rbx +.rspec +.tool-versions +data/* +gemfiles Gemfile.lock -*.sublime-* pkg/* spec/internal/config/test.sphinx.conf spec/internal/db/sphinx -spec/internal/log/*.log +spec/internal/log !spec/internal/tmp/.gitkeep spec/internal/tmp/* tmp diff --git a/.travis.yml b/.travis.yml index fa3722781..1daa3cfee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,36 @@ language: ruby +dist: xenial rvm: - - 1.9.3 - - 2.0.0 - - 2.1.0 - - jruby-19mode +- 2.4.10 +- 2.5.8 +- 2.6.6 +- 2.7.1 before_install: - - gem update --system +- gem update --system +- gem install bundler -v '1.17.3' +install: bundle _1.17.3_ install --jobs=3 --retry=3 before_script: - - "mysql -e 'create database thinking_sphinx;' > /dev/null" - - "psql -c 'create database thinking_sphinx;' -U postgres >/dev/null" +- mysql -e 'create database thinking_sphinx;' > /dev/null +- psql -c 'create database thinking_sphinx;' -U postgres >/dev/null +- "./bin/loadsphinx $SPHINX_VERSION $SPHINX_ENGINE" +- bundle _1.17.3_ exec appraisal install +script: bundle _1.17.3_ exec appraisal rspec env: - - DATABASE=mysql2 SPHINX_BIN=/usr/local/sphinx-2.0.10/bin/ SPHINX_VERSION=2.0.10 - - DATABASE=postgresql SPHINX_BIN=/usr/local/sphinx-2.0.10/bin/ SPHINX_VERSION=2.0.10 - - DATABASE=mysql2 SPHINX_BIN=/usr/local/sphinx-2.1.8/bin/ SPHINX_VERSION=2.1.8 - - DATABASE=postgresql SPHINX_BIN=/usr/local/sphinx-2.1.8/bin/ SPHINX_VERSION=2.1.8 - - DATABASE=mysql2 SPHINX_BIN=/usr/local/sphinx-2.2.2-beta/bin/ SPHINX_VERSION=2.2.2 - - DATABASE=postgresql SPHINX_BIN=/usr/local/sphinx-2.2.2-beta/bin/ SPHINX_VERSION=2.2.2 -gemfile: - - gemfiles/rails_3_2.gemfile - - gemfiles/rails_4_0.gemfile - - gemfiles/rails_4_1.gemfile - - gemfiles/rails_4_2.gemfile + matrix: + - DATABASE=mysql2 SPHINX_VERSION=2.2.11 SPHINX_ENGINE=sphinx + - DATABASE=postgresql SPHINX_VERSION=2.2.11 SPHINX_ENGINE=sphinx + - DATABASE=mysql2 SPHINX_VERSION=3.3.1 SPHINX_ENGINE=sphinx + - DATABASE=mysql2 SPHINX_VERSION=2.8.2 SPHINX_ENGINE=manticore + - DATABASE=postgresql SPHINX_VERSION=2.8.2 SPHINX_ENGINE=manticore + - DATABASE=mysql2 SPHINX_VERSION=3.5.0 SPHINX_ENGINE=manticore + - DATABASE=postgresql SPHINX_VERSION=3.5.0 SPHINX_ENGINE=manticore + # - DATABASE=postgresql SPHINX_VERSION=3.3.1 SPHINX_ENGINE=sphinx +sudo: false +addons: + postgresql: '9.4' + apt: + packages: + - libodbc1 +services: +- mysql +- postgresql diff --git a/Appraisals b/Appraisals index 36aa693ad..59cda54c2 100644 --- a/Appraisals +++ b/Appraisals @@ -1,15 +1,53 @@ -appraise 'rails_3_2' do - gem 'rails', '~> 3.2.21' -end +appraise 'rails_4_2' do + gem 'rails', '~> 4.2.6' + gem 'mysql2', '~> 0.4.0', :platform => :ruby +end if RUBY_VERSION.to_f <= 2.4 -appraise 'rails_4_0' do - gem 'rails', '~> 4.0.12' -end +appraise 'rails_5_0' do + if RUBY_PLATFORM == "java" + gem 'rails', '5.0.6' + else + gem 'rails', '~> 5.0.7' + end -appraise 'rails_4_1' do - gem 'rails', '~> 4.1.8' -end + gem 'mysql2', '~> 0.4.0', :platform => :ruby -appraise 'rails_4_2' do - gem 'rails', '~> 4.2.0' -end + gem 'jdbc-mysql', '~> 5.1.36', :platform => :jruby + gem 'activerecord-jdbcmysql-adapter', '~> 50.0', :platform => :jruby + gem 'activerecord-jdbcpostgresql-adapter', '~> 50.0', :platform => :jruby +end if (RUBY_PLATFORM != "java" || ENV["SPHINX_VERSION"].to_f > 2.1) && RUBY_VERSION.to_f < 3.0 + +appraise 'rails_5_1' do + gem 'rails', '~> 5.1.0' + gem 'mysql2', '~> 0.4.0', :platform => :ruby +end if RUBY_PLATFORM != 'java' && RUBY_VERSION.to_f < 3.0 + +appraise 'rails_5_2' do + gem 'rails', '~> 5.2.0' + gem 'mysql2', '~> 0.5.0', :platform => :ruby + gem 'pg', '~> 1.0', :platform => :ruby +end if RUBY_PLATFORM != 'java' && RUBY_VERSION.to_f < 3.0 + +appraise 'rails_6_0' do + gem 'rails', '~> 6.0.0' + gem 'mysql2', '~> 0.5.0', :platform => :ruby + gem 'pg', '~> 1.0', :platform => :ruby +end if RUBY_PLATFORM != 'java' && RUBY_VERSION.to_f >= 2.5 + +appraise 'rails_6_1' do + gem 'rails', '~> 6.1.0' + gem 'mysql2', '~> 0.5.0', :platform => :ruby + gem 'pg', '~> 1.0', :platform => :ruby +end if RUBY_PLATFORM != 'java' && RUBY_VERSION.to_f >= 2.5 + +appraise 'rails_7_0' do + gem 'rails', '~> 7.0.0' + gem 'mysql2', '~> 0.5.0', :platform => :ruby + gem 'pg', '~> 1.0', :platform => :ruby +end if RUBY_PLATFORM != 'java' && RUBY_VERSION.to_f >= 2.7 + +appraise 'rails_7_1' do + gem 'rails', '~> 7.1.0' + gem 'mysql2', '~> 0.5.0', :platform => :ruby + gem 'pg', '~> 1.0', :platform => :ruby +end if RUBY_PLATFORM != 'java' && RUBY_VERSION.to_f >= 2.7 diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown new file mode 100644 index 000000000..5ed4d753a --- /dev/null +++ b/CHANGELOG.markdown @@ -0,0 +1,771 @@ +# Changelog + +All notable changes to this project (at least, from v3.0.0 onwards) are documented in this file. + +## 5.6.0 - 2024-07-07 + +### Added + +* Support for Manticore 6.0 ([#1242](https://github.com/pat/thinking-sphinx/pull/1242)) +* `sphinx`-prefixed search methods, in case the standard `search` is overridden from something unrelated. ([#1265](https://github.com/pat/thinking-sphinx/pull/1265)) +* `none` / `search_none` scopes that can be chained to searches and will return no results. +* Added `ThinkingSphinx::Processor#sync` to synchronise updates/deletions based on a real-time index's scope, by @akostadinov in [@1258](https://github.com/pat/thinking-sphinx/pull/1258). + +### Changed + +* Improved Rails 7.1 support, by @jdelstrother in [#1252](https://github.com/pat/thinking-sphinx/pull/1252). + +### Fixed + +* Handle both SQL and RT indices correctly for inheritance column checks, by @akostadinov in [#1249](https://github.com/pat/thinking-sphinx/pull/1249). +* Ensure tests and CI work with recent Manticore versions, by @jdelstrother in [#1263](https://github.com/pat/thinking-sphinx/pull/1263). +* Use `rm -rf` to delete test and temporary directories (instead of `rm -r`). + +## 5.5.1 - 2022-12-31 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v5.5.1) + +### Changed + +* Fixed total count of results in pagination information for Manticore 5.0+, by disabling the cutoff limit. ([#1239](https://github.com/pat/thinking-sphinx/pull/1239)). + +## 5.5.0 - 2022-12-30 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v5.5.0) + +### Added + +* ThinkingSphinx::Processor, a public interface to perform index-related operations on model instances or model name/id combinations. In collaboration with @akostadinov ([#1215](https://github.com/pat/thinking-sphinx/issues/1215)). + +### Changed + +* Confirmed support by testing against Ruby 3.1 and 3.2 by @jdelStrother ([#1237](https://github.com/pat/thinking-sphinx/pull/1237)). + +### Fixed + +* Fix YAML loading, by @aepyornis ([#1217](https://github.com/pat/thinking-sphinx/pull/1217)). +* Further fixes for File.exist? instead of the deprecated File.exists?, by @funsim ([#1221](https://github.com/pat/thinking-sphinx/pull/1221)) and @graaf ([1233](https://github.com/pat/thinking-sphinx/pull/1233)). +* Treat unknown column errors as QueryErrors, so retrying the query occurs automatically. +* Fix MariaDB error handling. + +## 5.4.0 - 2021-12-21 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v5.4.0) + +### Added + +* Rails 7 support, including contributions from @anthonyshull in [#1205](https://github.com/pat/thinking-sphinx/pull/1205). + +### Changed + +* Confirmed support by testing against Manticore 4.0 and Sphinx 3.4. + +### Fixed + +* Include instance_exec in ThinkingSphinx::Search::CORE_METHODS by @jdelStrother in [#1210](https://github.com/pat/thinking-sphinx/pull/1210). +* Use File.exist? instead of the deprecated File.exists? ([#1211](https://github.com/pat/thinking-sphinx/issues/1211)). + +## 5.3.0 - 2021-08-19 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v5.3.0) + +### Changed + +* StaleIdsExceptions now include a URL in their error message with recommendations on how to resolve the problem. +* Fire real-time callbacks on `after_commit` (including deletions) to ensure data is fully persisted to the database before updating Sphinx. More details in [#1204](https://github.com/pat/thinking-sphinx/pull/1204). + +### Fixed + +* Ensure Thinking Sphinx's ActiveRecord components are loaded by either Rails' after_initialise hook or ActiveSupport's on_load notification, because the order of these two events are not consistent. +* Remove `app/indices` from eager_load_paths in Rails 4.2 and 5, to match the behaviour in 6. + +## 5.2.1 - 2021-08-09 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v5.2.1) + +### Fixed + +* Ensure ActiveRecord components are loaded for rake tasks, but only after the Rails application has initialised. More details in [#1199](https://github.com/pat/thinking-sphinx/issues/1199). + +## 5.2.0 - 2021-06-12 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v5.2.0) + +### Added + +* Confirmed support for Ruby 3.0. +* Orphaned records in real-time indices can now be cleaned up without running `rails ts:rebuild`. Disabled by default, can be enabled by setting `real_time_tidy` to true per environment in `config/thinking_sphinx.yml` (and will need `ts:rebuild` to restructure indices upon initial deploy). More details in [#1192](https://github.com/pat/thinking-sphinx/pull/1192). + +### Fixed + +* Avoid loading ActiveRecord during Rails initialisation so app configuration can still have an impact ([@jdelStrother](https://github.com/jdelStrother) in [#1194](https://github.com/pat/thinking-sphinx/pull/1194)). +* Remove `app/indices` (in both the Rails app and engines) from Rails' eager load paths, which was otherwise leading to indices being loaded more than once. (See [#1191](https://github.com/pat/thinking-sphinx/issues/1191) and [#1195](https://github.com/pat/thinking-sphinx/issues/1195)). + +## 5.1.0 - 2020-12-28 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v5.1.0) + +### Added + +* Support for Sphinx v3.3 and Manticore v3.5. +* Support for Rails 6.1 (via [joiner](https://rubygems.org/gems/joiner) v0.6.0). + +### Changed + +* `enable_star` is no longer available as a configuration option, as it's been enabled by default in Sphinx since v2.2.2, and is no longer allowed in Sphinx v3.3.1. +* All timestamp attributes are now considered plain integer values from Sphinx's perspective. Sphinx was already expecting integers, but since Sphinx v3.3.1 it doesn't recognise timestamps as a data type. There is no functional difference with this change - Thinking Sphinx was always converting times to their UNIX epoch integer values. +* Allow configuration of the maximum statement length ([@kalsan](https://github.com/kalsan) in [#1179](https://github.com/pat/thinking-sphinx/pull/1179)). +* Respect `:path` values to navigate associations for Thinking Sphinx callbacks on SQL-backed indices. Discussed in [#1182](https://github.com/pat/thinking-sphinx/issues/1182). + +### Fixed + +* Don't attempt to update delta flags on frozen model instances. + +## 5.0.0 - 2020-07-20 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v5.0.0) + +### Added + +* New interface for adding callbacks to indexed models (which is no longer done automatically). Discussed in [#1173](https://github.com/pat/thinking-sphinx/issues/1173) and committed via [#1175](https://github.com/pat/thinking-sphinx/pull/1175). **This is a breaking change - you will need to add these callbacks. See [the full release notes](https://github.com/pat/thinking-sphinx/releases/tag/v5.0.0) for examples.** +* Fields and attributes can be overriden - whichever's defined last with a given name is the definition that's used. This is an edge case, but useful if you want to override any of the default fields/indices. (Requested by @kalsan in [#1172](https://github.com/pat/thinking-sphinx/issues/1172).) +* Custom index_set_class implementations can now expect the `:instances` option to be set alongside `:classes`, which is useful in cases to limit the indices returned if you're splitting index data for given classes/models into shards. (Introduced in PR [#1171](https://github.com/pat/thinking-sphinx/pull/1171) after discussions with @lunaru in [#1166](https://github.com/pat/thinking-sphinx/issues/1166).) + +### Changed + +* Sphinx 2.2.11 or newer is required, or Manticore 2.8.2 or newer. +* Ruby 2.4 or newer is required. +* Rails 4.2 or newer is required. +* Remove internal uses of `send`, replaced with `public_send` as that's available in all supported Ruby versions. +* Deletion statements are simplified by avoiding the need to calculate document keys/offsets (@njakobsen via [#1134](https://github.com/pat/thinking-sphinx/issues/1134)). +* Real-time data is deleted before replacing it, to avoid duplicate data when offsets change (@njakobsen via [#1134](https://github.com/pat/thinking-sphinx/issues/1134)). +* Use `reference_name` as per custom `index_set_class` definitions. Previously, the class method was called on `ThinkingSphinx::IndexSet` even if a custom subclass was configured. (As per discussions with @kalsan in [#1172](https://github.com/pat/thinking-sphinx/issues/1172).) + +## 4.4.1 - 2019-08-23 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.4.1) + +### Changed + +* Automatically remove `app/indices` from Zeitwerk's autoload paths in Rails 6.0 onwards (if using Zeitwerk as the autoloader). + +## 4.4.0 - 2019-08-21 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.4.0) + +### Added + +* Confirmed Rails 6.0 support. +* Added ability to have custom real-time index processors (which handles all indices) and populators (which handles a particular index). These are available to get/set via `ThinkingSphinx::RealTime.processor` and `ThinkingSphinx::RealTime.populator` (and discussed in more detail in the [release notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.4.0)). + +### Changed + +* Improve failure message when tables don't exist for models associated with Sphinx indices ([Kiril Mitov](https://github.com/thebravoman) in [#1139](https://github.com/pat/thinking-sphinx/pull/1139)). + +### Fixed + +* Injected has-many/habtm collection search calls as default extensions to associations in Rails 5+, as it's a more reliable approach in Rails 6.0.0. + +## 4.3.2 - 2019-07-10 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.3.2) + +### Fixed + +* Reverted loading change behaviour from v4.3.1 for Rails v5 ([Eduardo J.](https://github.com/eduardoj) in [#1138](https://github.com/pat/thinking-sphinx/pull/1138)). + +## 4.3.1 - 2019-06-27 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.3.1) + +### Fixed + +* Fixed loading of index files to work with Rails 6 and Zeitwerk ([#1137](https://github.com/pat/thinking-sphinx/issues/1137)). + +## 4.3.0 - 2019-05-18 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.3.0) + +### Added + +* Allow overriding of Sphinx's running state, which is useful when Sphinx commands are interacting with a remote Sphinx daemon. As per discussions in [#1131](https://github.com/pat/thinking-sphinx/pull/1131). +* Allow skipping of directory creation, as per discussions in [#1131](https://github.com/pat/thinking-sphinx/pull/1131). + +### Fixed + +* Use ActiveSupport's lock monitor where possible (Rails 5.1.5 onwards) to avoid database deadlocks. Essential investigation by [Jonathan del Strother](https://github.com/jdelstrother) ([#1132](https://github.com/pat/thinking-sphinx/pull/1132)). +* Allow facet searching on distributed indices ([#1135](https://github.com/pat/thinking-sphinx/pull/1132)). + +## 4.2.0 - 2019-03-09 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.2.0) + +### Added + +* Allow changing the default encoding for MySQL database connections from utf8 to something else via the `mysql_encoding` setting in `config/thinking_sphinx.yml`. In the next significant release, the default will change to utf8mb4 (which is supported in MySQL 5.5.3 and newer). +* Added Rails 6.0 and Manticore 2.8 to the test matrix. + +### Changed + +* Use Arel's SQL literals for generated order clauses, to avoid warnings from Rails 6. + +### Fixed + +* Fix usage of alternative primary keys in update and deletion callbacks and attribute access. +* Ensure `respond_to?` takes Sphinx scopes into account ([Jonathan del Strother](https://github.com/jdelstrother) in [#1124](https://github.com/pat/thinking-sphinx/pull/1124)). +* Add `:excerpts` as a known option for search requests. +* Fix depolymorphed association join construction with Rails 6.0.0.beta2. +* Reset ThinkingSphinx::Configuration's cached values when Rails reloads, to avoid holding onto stale references to ActiveRecord models ([#1125](https://github.com/pat/thinking-sphinx/issues/1125)). +* Don't join against associations in `sql_query` if they're only used by query-sourced properties ([Hans de Graaff](https://github.com/graaff) in [#1127](https://github.com/pat/thinking-sphinx/pull/1127)). + +## 4.1.0 - 2018-12-28 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.1.0) + +### Added + +* The `:sql` search option can now accept per-model settings with model names as keys. e.g. `ThinkingSphinx.search "foo", :sql => {'Article' => {:include => :user}}` (Sergey Malykh in [#1120](https://github.com/pat/thinking-sphinx/pull/1120)). + +### Changed + +* Drop MRI 2.2 from the test matrix, and thus no longer officially supported (though the code will likely continue to work with 2.2 for a while). +* Added MRI 2.6, Sphinx 3.1 and Manticore 2.7 to the test matrix. + +### Fixed + +* Real-time indices now work with non-default integer primary keys (alongside UUIDs or other non-integer primary keys). + +## 4.0.0 - 2018-04-10 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v4.0.0) + +### Added + +* Support Sphinx 3.0. +* Allow disabling of docinfo setting via `skip_docinfo: true` in `config/thinking_sphinx.yml`. +* Support merging of delta indices into their core counterparts using ts:merge. +* Support UNIX sockets as an alternative for TCP connections to the daemon (MRI-only). +* Translate relative paths to absolute when generating configuration when `absolute_paths: true` is set per environment in `config/thinking_sphinx.yml`. + +### Changed + +* Drop Sphinx 2.0 support. +* Drop auto-typing of filter values. +* INDEX_FILTER environment variable is applied when running ts:index on SQL-backed indices. +* Drop MRI 2.0/2.1 support. +* Display a useful error message if processing real-time indices but the daemon isn't running. +* Refactor interface code into separate command classes, and allow for a custom rake interface. +* Add frozen_string_literal pragma comments. +* Log exceptions when processing real-time indices, but don't stop. +* Update polymorphic properties to support Rails 5.2. +* Allow configuration of the index guard approach. +* Output a warning if guard files exist when calling ts:index. +* Delete index guard files as part of ts:rebuild and ts:clear. + +### Fixed + +* Handle situations where no exit code is provided for Sphinx binary calls. +* Don't attempt to interpret indices for models that don't have a database table. + +## 3.4.2 - 2017-09-29 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.4.2) + +### Changed + +* Allow use of deletion callbacks for rollback events. +* Remove extra deletion code in the Populator - it's also being done by the real-time rake interface. + +### Fixed + +* Real-time callback syntax for namespaced models accepts a string (as documented). +* Fix up logged warnings. +* Add missing search options to known values to avoid incorrect warnings. + +## 3.4.1 - 2017-08-29 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.4.1) + +### Changed + +* Treat "Lost connection to MySQL server" as a connection error (Manuel Schnitzer). + +### Fixed + +* Index normalisation will now work even when index model tables don't exist. + +## 3.4.0 - 2017-08-28 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.4.0) + +### Added + +* Rake tasks are now unified, so the original tasks will operate on real-time indices as well. +* Output warnings when unknown options are used in search calls. +* Allow generation of a single real-time index (Tim Brown). +* Automatically use UTF8 in Sphinx for encodings that are extensions of UTF8. +* Basic type checking for attribute filters. + +### Changed + +* Delta callback logic now prioritises checking for high level settings rather than model changes. +* Allow for unsaved records when calculating document ids (and return nil). +* Display SphinxQL deletion statements in the log. +* Add support for Ruby's frozen string literals feature. +* Use saved_changes if it's available (in Rails 5.1+). +* Set a default connection timeout of 5 seconds. +* Don't search multi-table inheritance ancestors. +* Handle non-computable queries as parse errors. + +### Fixed + +* Index normalisation now occurs consistently, and removes unneccesary sphinx_internal_class_name fields from real-time indices. +* Fix Sphinx connections in JRuby. +* Fix long SphinxQL query handling in JRuby. +* Always close the SphinxQL connection if Innertube's asking (@cmaion). +* Get bigint primary keys working in Rails 5.1. +* Fix handling of attached starts of Sphinx (via Henne Vogelsang). +* Fix multi-field conditions. +* Use the base class of STI models for polymorphic join generation (via Andrés Cirugeda). +* Ensure ts:index now respects rake silent/quiet flags. + +## 3.3.0 - 2016-12-13 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.3.0) + +### Added + +* Real-time callbacks can now be used with after_commit hooks if that's preferred over after_save. +* Allow for custom batch sizes when populating real-time indices. + +### Changed + +* Only toggle the delta value if the record has changed or is new (rather than on every single save call). +* Delta indexing is now quiet by default (rather than verbose). +* Use Riddle's reworked command interface for interacting with Sphinx's command-line tools. +* Respect Rake's quiet and silent flags for the Thinking Sphinx rake tasks. +* ts:start and ts:stop tasks default to verbose. +* Sort engine paths for loading indices to ensure they're consistent. +* Custom exception class for invalid database adapters. +* Memoize the default primary keys per context. + +### Fixed + +* Explicit source method in the SQLQuery Builder instead of relying on method missing, thus avoiding any global methods named 'source' (Asaf Bartov). +* Load indices before deleting index files, to ensure the files are actually found and deleted. +* Avoid loading ActiveRecord earlier than necessary. This avoids loading Rails out of order, which caused problems with Rails 5. +* Handle queries that are too long for Sphinx. +* Improve Rails 5 / JRuby support. +* Fixed handling of multiple field tokens in wildcarding logic. +* Ensure custom primary key columns are handled consistently (Julio Monteiro). + +## 3.2.0 - 2016-05-13 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.2.0) + +### Added + +* Add JSON attribute support for real-time indices. +* Add ability to disable *all* Sphinx-related callbacks via ThinkingSphinx::Callbacks.suspend! and ThinkingSphinx::Callbacks.resume!. Particularly useful for unit tests. +* Add native OutOfBoundsError for search queries outside the pagination bounds. +* Support MySQL SSL options on a per-index level (@arrtchiu). +* Allow for different indexing strategies (e.g. all at once, or one by one). +* Allow rand_seed as a select option (Mattia Gheda). +* Add primary_key option for index definitions (Nathaneal Gray). +* Add ability to start searchd in the foreground (Andrey Novikov). + +### Changed + +* Improved error messages for duplicate property names and missing columns. +* Don't populate search results when requesting just the count values (Andrew Roth). +* Reset delta column before core indexing begins (reverting behaviour introduced in 3.1.0). See issue #958 for further discussion. +* Use Sphinx's bulk insert ability (Chance Downs). +* Reduce memory/object usage for model references (Jonathan del Strother). +* Disable deletion callbacks when real-time indices are in place and all other real-time callbacks are disabled. +* Only use ERB to parse the YAML file if ERB is loaded. + +### Fixed + +* Ensure SQL table aliases are reliable for SQL-backed index queries. +* Fixed mysql2 compatibility for memory references (Roman Usherenko). +* Fixed JRuby compatibility with camelCase method names (Brandon Dewitt). +* Fix stale id handling for multiple search contexts (Jonathan del Strother). +* Handle quoting of namespaced tables (Roman Usherenko). +* Make preload_indices thread-safe. +* Improved handling of marshalled/demarshalled search results. + +## 3.1.4 - 2015-06-01 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.1.4) + +### Added + +* Add JSON as a Sphinx type for attributes (Daniel Vandersluis). +* minimal_group_by? can now be set in config/thinking_sphinx.yml to automatically apply to all index definitions. + +### Changed + +* Add a contributor code of conduct. +* Remove polymorphic association and HABTM query support (when related to Thinking Sphinx) when ActiveRecord 3.2 is involved. +* Remove default charset_type - no longer required for Sphinx 2.2. +* Removing sql_query_info setting, as it's no longer used by Sphinx (nor is it actually used by Thinking Sphinx). + +### Fixed + +* Kaminari expects prev_page to be available. +* Don't try to delete guard files if they don't exist (@exAspArk). +* Handle database settings reliably, now that ActiveRecord 4.2 uses strings all the time. +* More consistent with escaping table names. +* Bug fix for association creation (with polymophic fields/attributes). + +## 3.1.3 - 2015-01-21 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.1.3) + +### Added + +* Allow for custom offset references with the :offset_as option - thus one model across many schemas with Apartment can be treated differently. +* Allow for custom IndexSet classes. + +### Changed + +* Log excerpt SphinxQL queries just like the search queries. +* Load Railtie if Rails::Railtie is defined, instead of just Rails (Andrew Cone). +* Convert raw Sphinx results to an array when querying (Bryan Ricker). +* Add bigint support for real-time indices, and use bigints for the sphinx_internal_id attribute (mapped to model primary keys) (Chance Downs). + +### Fixed + +* Generate de-polymorphised associations properly for Rails 4.2 +* Use reflect_on_association instead of reflections, to stick to the public ActiveRecord::Base API. +* Don't load ActiveRecord early - fixes a warning in Rails 4.2. +* Don't double-up on STI filtering, already handled by Rails. + +## 3.1.2 - 2014-11-04 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.1.2) + +### Added + +* Allow for custom paths for index files using :path option in the ThinkingSphinx::Index.define call. +* Allow the binlog path to be an empty string (Bobby Uhlenbrock). +* Add status task to report on whether Sphinx is running. +* Real-time index callbacks can take a block for dynamic scoping. +* Allow casting of document ids pre-offset as bigints (via big_documents_id option). + +### Changed + +* regenerate task now only deletes index files for real-time indices. +* Raise an exception when a populated search query is modified (as it can't be requeried). +* Log indices that aren't processed due to guard files existing. +* Paginate records by 1000 results at a time when flagging as deleted. +* Default the Capistrano TS Rails environment to use rails_env, and then fall back to stage. +* rebuild task uses clear between stopping the daemon and indexing. + +### Fixed + +* Ensure indexing guard files are removed when an exception is raised (Bobby Uhlenbrock). +* Don't update real-time indices for objects that are not persisted (Chance Downs). +* Use STI base class for polymorphic association replacements. +* Convert database setting keys to symbols for consistency with Rails (@dimko). +* Field weights and other search options are now respected from set_property. +* Models with more than one index have correct facet counts (using Sphinx 2.1.x or newer). +* Some association fixes for Rails 4.1. +* Clear connections when raising connection errors. + +## 3.1.1 - 2014-04-22 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.1.1) + +### Added + +* Allow for common section in generated Sphinx configuration files for Sphinx 2.2.x (disabled by default, though) (Trevor Smith). +* Basic support for HABTM associations and MVAs with query/ranged-query sources. +* Real-time indices callbacks can be disabled (useful for unit tests). +* ThinkingSphinx::Test has a clear method and no-index option for starting for real-time setups. +* Allow disabling of distributed indices. + +### Changed + +* Include full statements when query execution errors are raised (uglier, but more useful when debugging). +* Connection error messages now mention Sphinx, instead of just MySQL. +* Raise an exception when a referenced column does not exist. +* Capistrano tasks use thinking_sphinx_rails_env (defaults to standard environment) (Robert Coleman). +* Alias group and count columns for easier referencing in other clauses. +* Log real-time index updates (Demian Ferreiro). +* All indices now respond to a public attributes method. + +### Fixed + +* Don't apply attribute-only updates to real-time indices. +* Don't instantiate blank strings (via inheritance type columns) as constants. +* Don't presume all indices for a model have delta pairs, even if one does. +* Always use connection options for connection information. +* respond_to? works reliably with masks (Konstantin Burnaev). +* Avoid null values in MVA query/ranged-query sources. +* Don't send unicode null characters to real-time Sphinx indices. +* :populate option is now respected for single-model searches. +* :thinking_sphinx_roles is now used consistently in Capistrano v3 tasks. +* Only expand log directory if it exists. +* Handle JDBC connection errors appropriately (Adam Hutchison). +* Fixing wildcarding of Unicode strings. +* Improved handling of association searches with real-time indices, including via has_many :though associations (Rob Anderton). + +## 3.1.0 - 2014-01-11 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.1.0) + +### Added + +* Support for Capistrano v3 (Alexander Tipugin). +* JRuby support (with Sphinx 2.1 or newer). +* Support for Sphinx 2.2.x's HAVING and GROUP N BY SphinxQL options. +* Adding max_predicted_time search option (Sphinx 2.2.x). +* Wildcard/starring can be applied directly to strings using ThinkingSphinx::Query.wildcard('pancakes'), and escaping via ThinkingSphinx::Query.escape('pancakes'). +* Capistrano recipe now includes tasks for realtime indices. +* :group option within :sql options in a search call is passed through to the underlying ActiveRecord relation (Siarhei Hanchuk). +* Persistent connections can be disabled if you wish. +* Track what's being indexed, and don't double-up while indexing is running. Single indices (e.g. deltas) can be processed while a full index is happening, though. +* Pass through :delta_options to delta processors (Timo Virkalla). +* All delta records can have their core pairs marked as deleted after a suspended delta (use ThinkingSphinx::Deltas.suspend_and_update instead of ThinkingSphinx::Deltas.suspend). +* Set custom database settings within the index definition, using the set_database method. A more sane approach with multiple databases. + +### Changed + +* Updating Riddle requirement to >= 1.5.10. +* Extracting join generation into its own gem: Joiner. +* Geodist calculation is now prepended to the SELECT statement, so it can be referred to by other dynamic attributes. +* Auto-wildcard/starring (via :star => true) now treats escaped characters as word separators. +* Capistrano recipe no longer automatically adds thinking_sphinx:index and thinking_sphinx:start to be run after deploy:cold. +* UTF-8 forced encoding is now disabled by default (in line with Sphinx 2.1.x). +* Sphinx functions are now the default, instead of the legacy special variables (in line with Sphinx 2.1.x). +* Rails 3.1 is no longer supported. +* MRI 1.9.2 is no longer supported. +* Insist on at least * for SphinxQL SELECT statements. +* Reset the delta column to true after core indexing is completed, instead of before, and don't filter out delta records from the core source. +* Provide a distributed index per model that covers both core and delta indices. + +### Fixed + +* Indices will be detected in Rails engines upon configuration. +* Destroy callbacks are ignored for non-persisted objects. +* Blank STI values are converted to the parent class in Sphinx index data (Jonathan Greenberg). +* Track indices on parent STI models when marking documents as deleted. +* Separate per_page/max_matches values are respected in facet searches (Timo Virkkala). +* Don't split function calls when casting timestamps (Timo Virkalla). + +## 3.0.6 - 2013-10-20 + +[Release Notes](https://github.com/pat/thinking-sphinx/releases/tag/v3.0.6) + +### Added + +* Raise an error if no indices match the search criteria (Bryan Ricker). +* skip_time_zone setting is now available per environment via config/thinking_sphinx.yml to avoid the sql_query_pre time zone command. +* Added new search options in Sphinx 2.1.x. +* Added ability to disable UTF-8 forced encoding, now that Sphinx 2.1.2 returns UTF-8 strings by default. This will be disabled by default in Thinking Sphinx 3.1.0. +* Added ability to switch between Sphinx special variables and the equivalent functions. Sphinx 2.1.x requires the latter, and that behaviour will become the default in Sphinx 3.1.0. +* Adding search_for_ids on scoped search calls. +* MySQL users can enable a minimal GROUP BY statement, to speed up queries: set_property :minimal_group_by? => true. + +### Changed + +* Updating Riddle dependency to be >= 1.5.9. +* Separated directory preparation from data generation for real-time index (re)generation tasks. +* Have tests index UTF-8 characters where appropriate (Pedro Cunha). +* Always use DISTINCT in group concatenation. +* Sphinx connection failures now have their own class, ThinkingSphinx::ConnectionError, instead of the standard Mysql2::Error. +* Don't clobber custom :select options for facet searches (Timo Virkkala). +* Automatically load Riddle's Sphinx 2.0.5 compatability changes. +* Realtime fields and attributes now accept symbols as well as column objects, and fields can be sortable (with a _sort prefix for the matching attribute). +* Insist on the log directory existing, to ensure correct behaviour for symlinked paths. (Michael Pearson). +* Rake's silent mode is respected for indexing (@endoscient). + +### Fixed + +* Cast every column to a timestamp for timestamp attributes with multiple columns. +* Don't use Sphinx ordering if SQL order option is supplied to a search. +* Custom middleware and mask options now function correctly with model-scoped searches. +* Suspended deltas now no longer update core indices as well. +* Use alphabetical ordering for index paths consistently (@grin). +* Convert very small floats to fixed format for geo-searches. + +## 3.0.5 - 2013-08-26 + +### Added + +* Allow scoping of real-time index models. + +### Changed + +* Updating Riddle dependency to be >= 1.5.8. +* Real-time index population presentation and logic are now separated. +* Using the connection pool for update callbacks, excerpts, deletions. +* Don't add the sphinx_internal_class_name unless STI models are indexed. +* Use Mysql2's reconnect option and have it turned on by default. +* Improved auto-starring with escaped characters. + +### Fixed + +* Respect existing sql_query_range/sql_query_info settings. +* Don't add select clauses or joins to sql_query if they're for query/ranged-query properties. +* Set database timezones as part of the indexing process. +* Chaining scopes with just options works again. + +## 3.0.4 - 2013-07-09 + +### Added + +* ts:regenerate rake task for rebuilding Sphinx when realtime indices are involved. +* ts:clear task removes all Sphinx index and binlog files. +* Facet search calls now respect the limit option (which otherwise defaults to max_matches) (Demian Ferreiro). +* Excerpts words can be overwritten with the words option (@groe). +* The :facets option can be used in facet searches to limit which facets are queried. +* A separate role can be set for Sphinx actions with Capistrano (Andrey Chernih). +* Facet searches can now be called from Sphinx scopes. + +### Changed + +* Updating Riddle dependency to be >= 1.5.7. +* Glaze now responds to respond_to? (@groe). +* Deleted ActiveRecord objects are deleted in realtime indices as well. +* Realtime callbacks are no longer automatically added, but they're now more flexible (for association situations). +* Cleaning and refactoring so Code Climate ranks this as A-level code (Philip Arndt, Shevaun Coker, Garrett Heinlen). +* Exceptions raised when communicating with Sphinx are now mentioned in the logs when queries are retried (instead of STDOUT). +* Excerpts now use just the query and standard conditions, instead of parsing Sphinx's keyword metadata (which had model names in it). +* Get database connection details from ActiveRecord::Base, not each model, as this is where changes are reflected. +* Default Sphinx scopes are applied to new facet searches. + +### Fixed + +* Empty queries with the star option set to true are handled gracefully. +* Excerpts are now wildcard-friendly. +* Facet searches now use max_matches value (with a default of 1000) to ensure as many results as possible are returned. +* The settings cache is now cleared when the configuration singleton is reset (Pedro Cunha). +* Escaped @'s in queries are considered part of each word, instead of word separators. +* Internal class name conditions are ignored with auto-starred queries. +* RDoc doesn't like constant hierarchies split over multiple lines. + +## 3.0.3 - 2013-05-07 + +### Added + +* INDEX_ONLY environment flag is passed through when invoked through Capistrano (Demian Ferreiro). +* use_64_bit option returns as cast_to_timestamp instead (Denis Abushaev). +* Collection of hooks (lambdas) that get called before indexing. Useful for delta libraries. + +### Changed + +* Updating Riddle dependency to be >= 1.5.6 +* Delta jobs get common classes to allow third-party delta behaviours to leverage Thinking Sphinx. +* Raise ThinkingSphinx::MixedScopesError if a search is called through an ActiveRecord scope. +* GroupEnumeratorsMask is now a default mask, as masks need to be in place before search results are populated/the middleware is called (and previously it was being added within a middleware call). +* The current_page method is now a part of ThinkingSphinx::Search, as it is used when populating results. + +### Fixed + +* Update to association handling for Rails/ActiveRecord 4.0.0.rc1. +* Cast and concatenate multi-column attributes correctly. +* Don't load fields or attributes when building a real-time index - otherwise the index is translated before it has a chance to be built. +* Default search panes are cloned for each search. +* Index-level settings (via set_property) are now applied consistently after global settings (in thinking_sphinx.yml). +* All string values returned from Sphinx are now properly converted to UTF8. +* The default search masks are now cloned for each search, instead of referring to the constant (and potentially modifying it often). + +## 3.0.2 - 2013-03-23 + +### Added + +* Ruby 2.0 support. +* Rails 4.0.0 beta1 support. +* Indexes defined in app/indices in engines are now loaded (Antonio Tapiador del Dujo). +* Query errors are classified as such, instead of getting the base SphinxError. + +### Changed + +* per_page now accepts an optional paging limit, to match WillPaginate's behaviour. If none is supplied, it just returns the page size. +* Strings and regular expressions in ThinkingSphinx::Search::Query are now treated as UTF-8. +* Setting a custom framework will rebuild the core configuration around its provided settings (path and environment). +* Search masks don't rely on respond_to?, and so Object/Kernel methods are passed through to the underlying array instead. +* Empty search conditions are now ignored, instead of being appended with no value (Nicholas Klick). +* Custom conditions are no longer added to the sql_query_range value, as they may involve associations. + +### Fixed + +* :utf8? option within index definitions is now supported, and defaults to true if the database configuration's encoding is set to 'utf8'. +* indices_location and configuration_file values in thinking_sphinx.yml will be applied to the configuration. +* Primary keys that are not 'id' now work correctly. +* Search options specified in index definitions and thinking_sphinx.yml are now used in search requests (eg: max_matches, field_weights). +* Custom association conditions are no longer presumed to be an array. +* Capistrano tasks use the correct ts rake task prefix (David Celis). + +## 3.0.1 - 2013-02-04 + +### Added + +* Provide Capistrano deployment tasks (David Celis). +* Allow specifying of Sphinx version. Is only useful for Flying Sphinx purposes at this point - has no impact on Riddle or Sphinx. +* Support new JDBC configuration style (when JDBC can be used) (Kyle Stevens). +* Mysql2::Errors are wrapped as ThinkingSphinx::SphinxErrors, with subclasses of SyntaxError and ParseError used appropriately. Syntax and parse errors do not prompt a retry on a new connection. +* Polymorphic associations can be used within index definitions when the appropriate classes are set out. +* Allow custom strings for SQL joins in index definitions. +* indexer and searchd settings are added to the appropriate objects from config/thinking_sphinx.yml (@ygelfand). + +### Changed + +* Use connection pool for search queries. If a query fails, it will be retried on a new connection before raising if necessary. +* Glaze always passes methods through to the underlying ActiveRecord::Base object if they don't exist on any of the panes. + +### Fixed + +* Referring to associations via polymorphic associations in an index definition now works. +* Don't override foreign keys for polymorphic association replacements. +* Quote namespaced model names in class field condition. +* New lines are maintained and escaped in custom source queries. +* Subclasses of indexed models fire delta callbacks properly. +* Thinking Sphinx can be loaded via thinking/sphinx, to satisfy Bundler. +* New lines are maintained and escaped in sql_query values. + +## 3.0.0 - 2013-01-02 + +### Added + +* Initial realtime index support, including the ts:generate task for building index datasets. Sphinx 2.0.6 is required. +* SphinxQL connection pooling via the Innertube gem. + +### Changed + +* Updating Riddle dependency to 1.5.4. +* UTF-8 is now the default charset again (as it was in earlier Thinking Sphinx versions). +* Removing ts:version rake task. + +### Fixed + +* Respect source options as well as underlying settings via the set_property method in index definitions. +* Load real-time index definitions when listing fields, attributes, and/or conditions. + +## 3.0.0.rc - 2012-12-22 + +### Added + +* Source type support (query and ranged query) for both attributes and fields. Custom SQL strings can be supplied as well. +* Wordcount attributes and fields now supported. +* Support for Sinatra and other non-Rails frameworks. +* A sphinx scope can be defined as the default. +* An index can have multiple sources, by using define_source within the index definition. +* sanitize_sql is available within an index definition. +* Providing :prefixes => true or :infixes => true as an option when declaring a field means just the noted fields have infixes/prefixes applied. +* ThinkingSphinx::Search#query_time returns the time Sphinx took to make the query. +* Namespaced model support. +* Default settings for index definition arguments can be set in config/thinking_sphinx.yml. +* A custom Riddle/Sphinx controller can be supplied. Useful for Flying Sphinx to have an API layer over Sphinx commands, without needing custom gems for different Thinking Sphinx/Flying Sphinx combinations. + +### Fixed + +* Correctly escape nulls in inheritance column (Darcy Laycock). +* Use ThinkingSphinx::Configuration#render_to_file instead of ThinkingSphinx::Configuration#build in test helpers (Darcy Laycock). +* Suppressing delta output in test helpers now works (Darcy Laycock). + +## 3.0.0.pre - 2012-10-06 + +First pre-release of v3. Not quite feature complete, but the important stuff is certainly covered. See the README for more the finer details. diff --git a/Gemfile b/Gemfile index 851e7be82..212c9e950 100644 --- a/Gemfile +++ b/Gemfile @@ -1,9 +1,17 @@ +# frozen_string_literal: true + source 'https://rubygems.org' gemspec -gem 'mysql2', '~> 0.3.12b4', :platform => :ruby -gem 'pg', '~> 0.16.0', :platform => :ruby +gem 'mysql2', '~> 0.5.0', :platform => :ruby +gem 'pg', '~> 0.18.4', :platform => :ruby + +gem 'activerecord', '< 7' if RUBY_VERSION.to_f <= 2.4 -gem 'activerecord-jdbcmysql-adapter', '~> 1.3.4', :platform => :jruby -gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3.4', :platform => :jruby +if RUBY_PLATFORM == 'java' + gem 'jdbc-mysql', '5.1.35', :platform => :jruby + gem 'activerecord-jdbcmysql-adapter', '>= 1.3.23', :platform => :jruby + gem 'activerecord-jdbcpostgresql-adapter', '>= 1.3.23', :platform => :jruby + gem 'activerecord', '>= 3.2.22' +end diff --git a/HISTORY b/HISTORY deleted file mode 100644 index e1d552b9e..000000000 --- a/HISTORY +++ /dev/null @@ -1,235 +0,0 @@ -2015-01-21: 3.1.3 -* [CHANGE] Log excerpt SphinxQL queries just like the search queries. -* [CHANGE] Load Railtie if Rails::Railtie is defined, instead of just Rails (Andrew Cone). -* [CHANGE] Convert raw Sphinx results to an array when querying (Bryan Ricker). -* [FIX] Generate de-polymorphised associations properly for Rails 4.2 -* [FIX] Use reflect_on_association instead of reflections, to stick to the public ActiveRecord::Base API. -* [FIX] Don't load ActiveRecord early - fixes a warning in Rails 4.2. -* [FEATURE] Allow for custom offset references with the :offset_as option - thus one model across many schemas with Apartment can be treated differently. -* [FEATURE] Allow for custom IndexSet classes. -* [FIX] Don't double-up on STI filtering, already handled by Rails. -* [CHANGE] Add bigint support for real-time indices, and use bigints for the sphinx_internal_id attribute (mapped to model primary keys) (Chance Downs). - -2014-11-04: 3.1.2 -* [CHANGE] regenerate task now only deletes index files for real-time indices. -* [CHANGE] Raise an exception when a populated search query is modified (as it can't be requeried). -* [FEATURE] Allow for custom paths for index files using :path option in the ThinkingSphinx::Index.define call. -* [FIX] Ensure indexing guard files are removed when an exception is raised (Bobby Uhlenbrock). -* [FIX] Don't update real-time indices for objects that are not persisted (Chance Downs). -* [FEATURE] Allow the binlog path to be an empty string (Bobby Uhlenbrock). -* [FIX] Use STI base class for polymorphic association replacements. -* [FIX] Convert database setting keys to symbols for consistency with Rails (@dimko). -* [FIX] Field weights and other search options are now respected from set_property. -* [CHANGE] Log indices that aren't processed due to guard files existing. -* [FEATURE] Add status task to report on whether Sphinx is running. -* [FIX] Models with more than one index have correct facet counts (using Sphinx 2.1.x or newer). -* [FEATURE] Real-time index callbacks can take a block for dynamic scoping. -* [FIX] Some association fixes for Rails 4.1. -* [CHANGE] Paginate records by 1000 results at a time when flagging as deleted. -* [CHANGE] Default the Capistrano TS Rails environment to use rails_env, and then fall back to stage. -* [CHANGE] rebuild task uses clear between stopping the daemon and indexing. -* [FIX] Clear connections when raising connection errors. -* [FEATURE] Allow casting of document ids pre-offset as bigints (via big_documents_id option). - -2014-04-22: 3.1.1 -* [CHANGE] Include full statements when query execution errors are raised (uglier, but more useful when debugging). -* [FEATURE] Allow for common section in generated Sphinx configuration files for Sphinx 2.2.x (disabled by default, though) (Trevor Smith). -* [FEATURE] Basic support for HABTM associations and MVAs with query/ranged-query sources. -* [CHANGE] Connection error messages now mention Sphinx, instead of just MySQL. -* [FIX] Don't apply attribute-only updates to real-time indices. -* [FIX] Don't instantiate blank strings (via inheritance type columns) as constants. -* [FIX] Don't presume all indices for a model have delta pairs, even if one does. -* [CHANGE] Raise an exception when a referenced column does not exist. -* [CHANGE] Capistrano tasks use thinking_sphinx_rails_env (defaults to standard environment) (Robert Coleman). -* [FIX] Always use connection options for connection information. -* [FIX] respond_to? works reliably with masks (Konstantin Burnaev). -* [FEATURE] Real-time indices callbacks can be disabled (useful for unit tests). -* [FEATURE] ThinkingSphinx::Test has a clear method and no-index option for starting for real-time setups. -* [FIX] Avoid null values in MVA query/ranged-query sources. -* [CHANGE] Alias group and count columns for easier referencing in other clauses. -* [FEATURE] Allow disabling of distributed indices. -* [FIX] Don't send unicode null characters to real-time Sphinx indices. -* [FIX] :populate option is now respected for single-model searches. -* [FIX] :thinking_sphinx_roles is now used consistently in Capistrano v3 tasks. -* [CHANGE] Log real-time index updates (Demian Ferreiro). -* [FIX] Only expand log directory if it exists. -* [FIX] Handle JDBC connection errors appropriately (Adam Hutchison). -* [FIX] Fixing wildcarding of Unicode strings. -* [CHANGE] All indices now respond to a public attributes method. -* [FIX] Improved handling of association searches with real-time indices, including via has_many :though associations (Rob Anderton). - -2014-01-11: 3.1.0 -* [CHANGE] Updating Riddle requirement to >= 1.5.10. -* [CHANGE] Extracting join generation into its own gem: Joiner. -* [FEATURE] Support for Capistrano v3 (Alexander Tipugin). -* [FEATURE] JRuby support (with Sphinx 2.1 or newer). -* [CHANGE] Geodist calculation is now prepended to the SELECT statement, so it can be referred to by other dynamic attributes. -* [FIX] Indices will be detected in Rails engines upon configuration. -* [FEATURE] Support for Sphinx 2.2.x's HAVING and GROUP N BY SphinxQL options. -* [FEATURE] Adding max_predicted_time search option (Sphinx 2.2.x). -* [FEATURE] Wildcard/starring can be applied directly to strings using ThinkingSphinx::Query.wildcard('pancakes'), and escaping via ThinkingSphinx::Query.escape('pancakes'). -* [CHANGE] Auto-wildcard/starring (via :star => true) now treats escaped characters as word separators. -* [FEATURE] Capistrano recipe now includes tasks for realtime indices. -* [CHANGE] Capistrano recipe no longer automatically adds thinking_sphinx:index and thinking_sphinx:start to be run after deploy:cold. -* [CHANGE] UTF-8 forced encoding is now disabled by default (in line with Sphinx 2.1.x). -* [CHANGE] Sphinx functions are now the default, instead of the legacy special variables (in line with Sphinx 2.1.x). -* [CHANGE] Rails 3.1 is no longer supported. -* [CHANGE] MRI 1.9.2 is no longer supported. -* [FIX] Destroy callbacks are ignored for non-persisted objects. -* [FEATURE] :group option within :sql options in a search call is passed through to the underlying ActiveRecord relation (Siarhei Hanchuk). -* [FIX] Blank STI values are converted to the parent class in Sphinx index data (Jonathan Greenberg). -* [CHANGE] Insist on at least * for SphinxQL SELECT statements. -* [FIX] Track indices on parent STI models when marking documents as deleted. -* [FEATURE] Persistent connections can be disabled if you wish. -* [FIX] Separate per_page/max_matches values are respected in facet searches (Timo Virkkala). -* [FIX] Don't split function calls when casting timestamps (Timo Virkalla). -* [FEATURE] Track what's being indexed, and don't double-up while indexing is running. Single indices (e.g. deltas) can be processed while a full index is happening, though. -* [FEATURE] Pass through :delta_options to delta processors (Timo Virkalla). -* [FEATURE] All delta records can have their core pairs marked as deleted after a suspended delta (use ThinkingSphinx::Deltas.suspend_and_update instead of ThinkingSphinx::Deltas.suspend). -* [CHANGE] Reset the delta column to true after core indexing is completed, instead of before, and don't filter out delta records from the core source. -* [FEATURE] Set custom database settings within the index definition, using the set_database method. A more sane approach with multiple databases. -* [CHANGE] Provide a distributed index per model that covers both core and delta indices. - -2013-10-20: 3.0.6 -* [FEATURE] Raise an error if no indices match the search criteria (Bryan Ricker). -* [FEATURE] skip_time_zone setting is now available per environment via config/thinking_sphinx.yml to avoid the sql_query_pre time zone command. -* [CHANGE] Updating Riddle dependency to be >= 1.5.9. -* [FEATURE] Added new search options in Sphinx 2.1.x. -* [FEATURE] Added ability to disable UTF-8 forced encoding, now that Sphinx 2.1.2 returns UTF-8 strings by default. This will be disabled by default in Thinking Sphinx 3.1.0. -* [FEATURE] Added ability to switch between Sphinx special variables and the equivalent functions. Sphinx 2.1.x requires the latter, and that behaviour will become the default in Sphinx 3.1.0. -* [FIX] Cast every column to a timestamp for timestamp attributes with multiple columns. -* [CHANGE] Separated directory preparation from data generation for real-time index (re)generation tasks. -* [CHANGE] Have tests index UTF-8 characters where appropriate (Pedro Cunha). -* [FIX] Don't use Sphinx ordering if SQL order option is supplied to a search. -* [CHANGE] Always use DISTINCT in group concatenation. -* [CHANGE] Sphinx connection failures now have their own class, ThinkingSphinx::ConnectionError, instead of the standard Mysql2::Error. -* [FIX] Custom middleware and mask options now function correctly with model-scoped searches. -* [FEATURE] Adding search_for_ids on scoped search calls. -* [CHANGE] Don't clobber custom :select options for facet searches (Timo Virkkala). -* [CHANGE] Automatically load Riddle's Sphinx 2.0.5 compatability changes. -* [FIX] Suspended deltas now no longer update core indices as well. -* [CHANGE] Realtime fields and attributes now accept symbols as well as column objects, and fields can be sortable (with a _sort prefix for the matching attribute). -* [FEATURE] MySQL users can enable a minimal GROUP BY statement, to speed up queries: set_property :minimal_group_by? => true. -* [CHANGE] Insist on the log directory existing, to ensure correct behaviour for symlinked paths. (Michael Pearson). -* [FIX] Use alphabetical ordering for index paths consistently (@grin). -* [FIX] Convert very small floats to fixed format for geo-searches. -* [CHANGE] Rake's silent mode is respected for indexing (@endoscient). - -2013-08-26: 3.0.5 -* [CHANGE] Updating Riddle dependency to be >= 1.5.8. -* [FEATURE] Allow scoping of real-time index models. -* [CHANGE] Real-time index population presentation and logic are now separated. -* [CHANGE] Using the connection pool for update callbacks, excerpts, deletions. -* [FIX] Respect existing sql_query_range/sql_query_info settings. -* [CHANGE] Don't add the sphinx_internal_class_name unless STI models are indexed. -* [FIX] Don't add select clauses or joins to sql_query if they're for query/ranged-query properties. -* [CHANGE] Use Mysql2's reconnect option and have it turned on by default. -* [FIX] Set database timezones as part of the indexing process. -* [CHANGE] Improved auto-starring with escaped characters. -* [FIX] Chaining scopes with just options works again. - -2013-07-09: 3.0.4 -* [CHANGE] Updating Riddle dependency to be >= 1.5.7. -* [FEATURE] ts:regenerate rake task for rebuilding Sphinx when realtime indices are involved. -* [FEATURE] ts:clear task removes all Sphinx index and binlog files. -* [CHANGE] Glaze now responds to respond_to? (@groe). -* [FEATURE] Facet search calls now respect the limit option (which otherwise defaults to max_matches) (Demian Ferreiro). -* [FEATURE] Excerpts words can be overwritten with the words option (@groe). -* [FIX] Empty queries with the star option set to true are handled gracefully. -* [CHANGE] Deleted ActiveRecord objects are deleted in realtime indices as well. -* [CHANGE] Realtime callbacks are no longer automatically added, but they're now more flexible (for association situations). -* [CHANGE] Cleaning and refactoring so Code Climate ranks this as A-level code (Philip Arndt, Shevaun Coker, Garrett Heinlen). -* [FIX] Excerpts are now wildcard-friendly. -* [FIX] Facet searches now use max_matches value (with a default of 1000) to ensure as many results as possible are returned. -* [CHANGE] Exceptions raised when communicating with Sphinx are now mentioned in the logs when queries are retried (instead of STDOUT). -* [CHANGE] Excerpts now use just the query and standard conditions, instead of parsing Sphinx's keyword metadata (which had model names in it). -* [FIX] The settings cache is now cleared when the configuration singleton is reset (Pedro Cunha). -* [FEATURE] The :facets option can be used in facet searches to limit which facets are queried. -* [FIX] Escaped @'s in queries are considered part of each word, instead of word separators. -* [FIX] Internal class name conditions are ignored with auto-starred queries. -* [FEATURE] A separate role can be set for Sphinx actions with Capistrano (Andrey Chernih). -* [FIX] RDoc doesn't like constant hierarchies split over multiple lines. -* [CHANGE] Get database connection details from ActiveRecord::Base, not each model, as this is where changes are reflected. -* [CHANGE] Default Sphinx scopes are applied to new facet searches. -* [FEATURE] Facet searches can now be called from Sphinx scopes. - -2013-05-07: 3.0.3 -* [CHANGE] Updating Riddle dependency to be >= 1.5.6 -* [FEATURE] INDEX_ONLY environment flag is passed through when invoked through Capistrano (Demian Ferreiro). -* [FEATURE] use_64_bit option returns as cast_to_timestamp instead (Denis Abushaev). -* [FIX] Update to association handling for Rails/ActiveRecord 4.0.0.rc1. -* [CHANGE] Delta jobs get common classes to allow third-party delta behaviours to leverage Thinking Sphinx. -* [FEATURE] Collection of hooks (lambdas) that get called before indexing. Useful for delta libraries. -* [FIX] Cast and concatenate multi-column attributes correctly. -* [FIX] Don't load fields or attributes when building a real-time index - otherwise the index is translated before it has a chance to be built. -* [CHANGE] Raise ThinkingSphinx::MixedScopesError if a search is called through an ActiveRecord scope. -* [FIX] Default search panes are cloned for each search. -* [FIX] Index-level settings (via set_property) are now applied consistently after global settings (in thinking_sphinx.yml). -* [FIX] All string values returned from Sphinx are now properly converted to UTF8. -* [CHANGE] GroupEnumeratorsMask is now a default mask, as masks need to be in place before search results are populated/the middleware is called (and previously it was being added within a middleware call). -* [FIX] The default search masks are now cloned for each search, instead of referring to the constant (and potentially modifying it often). -* [CHANGE] The current_page method is now a part of ThinkingSphinx::Search, as it is used when populating results. - -2013-03-23: 3.0.2 -* [CHANGE] per_page now accepts an optional paging limit, to match WillPaginate's behaviour. If none is supplied, it just returns the page size. -* [FEATURE] Ruby 2.0 support. -* [FEATURE] Rails 4.0.0 beta1 support. -* [FIX] :utf8? option within index definitions is now supported, and defaults to true if the database configuration's encoding is set to 'utf8'. -* [FIX] indices_location and configuration_file values in thinking_sphinx.yml will be applied to the configuration. -* [CHANGE] Strings and regular expressions in ThinkingSphinx::Search::Query are now treated as UTF-8. -* [FIX] Primary keys that are not 'id' now work correctly. -* [CHANGE] Setting a custom framework will rebuild the core configuration around its provided settings (path and environment). -* [CHANGE] Search masks don't rely on respond_to?, and so Object/Kernel methods are passed through to the underlying array instead. -* [FIX] Search options specified in index definitions and thinking_sphinx.yml are now used in search requests (eg: max_matches, field_weights). -* [FEATURE] Indexes defined in app/indices in engines are now loaded (Antonio Tapiador del Dujo). -* [FIX] Custom association conditions are no longer presumed to be an array. -* [CHANGE] Empty search conditions are now ignored, instead of being appended with no value (Nicholas Klick). -* [CHANGE] Custom conditions are no longer added to the sql_query_range value, as they may involve associations. -* [FIX] Capistrano tasks use the correct ts rake task prefix (David Celis). -* [FEATURE] Query errors are classified as such, instead of getting the base SphinxError. - -2013-02-04: 3.0.1 -* [FEATURE] Provide Capistrano deployment tasks (David Celis). -* [FEATURE] Allow specifying of Sphinx version. Is only useful for Flying Sphinx purposes at this point - has no impact on Riddle or Sphinx. -* [FEATURE] Support new JDBC configuration style (when JDBC can be used) (Kyle Stevens). -* [FIX] Referring to associations via polymorphic associations in an index definition now works. -* [FEATURE] Mysql2::Errors are wrapped as ThinkingSphinx::SphinxErrors, with subclasses of SyntaxError and ParseError used appropriately. Syntax and parse errors do not prompt a retry on a new connection. -* [CHANGE] Use connection pool for search queries. If a query fails, it will be retried on a new connection before raising if necessary. -* [CHANGE] Glaze always passes methods through to the underlying ActiveRecord::Base object if they don't exist on any of the panes. -* [FIX] Don't override foreign keys for polymorphic association replacements. -* [FIX] Quote namespaced model names in class field condition. -* [FEATURE] Polymorphic associations can be used within index definitions when the appropriate classes are set out. -* [FEATURE] Allow custom strings for SQL joins in index definitions. -* [FIX] New lines are maintained and escaped in custom source queries. -* [FIX] Subclasses of indexed models fire delta callbacks properly. -* [FIX] Thinking Sphinx can be loaded via thinking/sphinx, to satisfy Bundler. -* [FEATURE] indexer and searchd settings are added to the appropriate objects from config/thinking_sphinx.yml (@ygelfand). -* [FIX] New lines are maintained and escaped in sql_query values. - -2013-01-02: 3.0.0 -* [CHANGE] Updating Riddle dependency to 1.5.4. -* [FIX] Respect source options as well as underlying settings via the set_property method in index definitions. -* [FIX] Load real-time index definitions when listing fields, attributes, and/or conditions. -* [CHANGE] UTF-8 is now the default charset again (as it was in earlier Thinking Sphinx versions). -* [FEATURE] Initial realtime index support, including the ts:generate task for building index datasets. Sphinx 2.0.6 is required. -* [CHANGE] Removing ts:version rake task. -* [FEATURE] SphinxQL connection pooling via the Innertube gem. - -2012-12-22: 3.0.0.rc -* [FEATURE] Source type support (query and ranged query) for both attributes and fields. Custom SQL strings can be supplied as well. -* [FEATURE] Wordcount attributes and fields now supported. -* [FEATURE] Support for Sinatra and other non-Rails frameworks. -* [FEATURE] A sphinx scope can be defined as the default. -* [FEATURE] An index can have multiple sources, by using define_source within the index definition. -* [FEATURE] sanitize_sql is available within an index definition. -* [FEATURE] Providing :prefixes => true or :infixes => true as an option when declaring a field means just the noted fields have infixes/prefixes applied. -* [FEATURE] ThinkingSphinx::Search#query_time returns the time Sphinx took to make the query. -* [FEATURE] Namespaced model support. -* [FEATURE] Default settings for index definition arguments can be set in config/thinking_sphinx.yml. -* [FIX] Correctly escape nulls in inheritance column (Darcy Laycock). -* [FIX] Use ThinkingSphinx::Configuration#render_to_file instead of ThinkingSphinx::Configuration#build in test helpers (Darcy Laycock). -* [FIX] Suppressing delta output in test helpers now works (Darcy Laycock). -* [FEATURE] A custom Riddle/Sphinx controller can be supplied. Useful for Flying Sphinx to have an API layer over Sphinx commands, without needing custom gems for different Thinking Sphinx/Flying Sphinx combinations. - -2012-10-06: 3.0.0.pre -* First pre-release. Not quite feature complete, but the important stuff is certainly covered. See the README for more the finer details. diff --git a/Procfile.support b/Procfile.support new file mode 100644 index 000000000..2bb81ac45 --- /dev/null +++ b/Procfile.support @@ -0,0 +1,2 @@ +postgres: postgres -D data/postgres -p ${POSTGRES_PORT:-5432} +mysql: $(brew --prefix mysql@5.7)/bin/mysqld --datadir=$(PWD)/data/mysql --port ${MYSQL_PORT:-3306} --socket=mysql.sock diff --git a/README.textile b/README.textile index e2a8c16bd..b24088cb0 100644 --- a/README.textile +++ b/README.textile @@ -1,56 +1,58 @@ h1. Thinking Sphinx -Thinking Sphinx is a library for connecting ActiveRecord to the Sphinx full-text search tool, and integrates closely with Rails (but also works with other Ruby web frameworks). The current release is v3.1.3. +Thinking Sphinx is a library for connecting ActiveRecord to the Sphinx full-text search tool, and integrates closely with Rails (but also works with other Ruby web frameworks). The current release is v5.6.0. h2. Upgrading -Please refer to the release notes for any changes you need to make when upgrading: +Please refer to "the changelog":https://github.com/pat/thinking-sphinx/blob/develop/CHANGELOG.markdown and "release notes":https://github.com/pat/thinking-sphinx/releases for any changes you need to make when upgrading. The release notes in particular are quite good at covering breaking changes and more details for new features. -* "v3.1.3":https://github.com/pat/thinking-sphinx/releases/tag/v3.1.3 -* "v3.1.2":https://github.com/pat/thinking-sphinx/releases/tag/v3.1.2 -* "v3.1.1":https://github.com/pat/thinking-sphinx/releases/tag/v3.1.1 -* "v3.1.0":https://github.com/pat/thinking-sphinx/releases/tag/v3.1.0 -* "v3.0.6":https://github.com/pat/thinking-sphinx/releases/tag/v3.0.6 - -If you're upgrading from pre-v3, then the documentation has "pretty extensive notes":http://pat.github.io/thinking-sphinx/upgrading.html on what's changed. +The documentation also has more details on what's involved for upgrading from "v4 to v5":https://freelancing-gods.com/thinking-sphinx/v5/upgrading.html, "v3 to v4":https://freelancing-gods.com/thinking-sphinx/v4/upgrading.html, and "v1/v2 to v3":https://freelancing-gods.com/thinking-sphinx/v3/upgrading.html. h2. Installation It's a gem, so install it like you would any other gem. You will also need to specify the mysql2 gem if you're using MRI, or jdbc-mysql if you're using JRuby: -
gem 'mysql2',          '~> 0.3.13', :platform => :ruby
-gem 'jdbc-mysql',      '~> 5.1.28', :platform => :jruby
-gem 'thinking-sphinx', '~> 3.1.3'
+
gem 'mysql2',          '~> 0.4',    :platform => :ruby
+gem 'jdbc-mysql',      '~> 5.1.35', :platform => :jruby
+gem 'thinking-sphinx', '~> 5.5'
The MySQL gems mentioned are required for connecting to Sphinx, so please include it even when you're using PostgreSQL for your database. -You'll also need to install Sphinx - this is covered in "the extended documentation":http://pat.github.io/thinking-sphinx/installing_sphinx.html. +You'll also need to install Sphinx - this is covered in "the extended documentation":https://freelancing-gods.com/thinking-sphinx/installing_sphinx.html. h2. Usage -Begin by reading the "quick-start guide":http://pat.github.io/thinking-sphinx/quickstart.html, and beyond that, "the documentation":http://pat.github.io/thinking-sphinx/ should serve you pretty well. +Begin by reading the "quick-start guide":https://freelancing-gods.com/thinking-sphinx/quickstart.html, and beyond that, "the documentation":https://freelancing-gods.com/thinking-sphinx/ should serve you pretty well. + +h2. Requirements -h3. Extending with Middleware, Glazes and Panes +The current release of Thinking Sphinx works with the following versions of its dependencies: -These are covered in "a blog post":http://freelancing-gods.com/posts/rewriting_thinking_sphinx_middleware_glazes_and_panes. +|_. Library |_. Minimum |_. Tested Against | +| Ruby | v2.4 | v2.4, v2.5, v2.6, v2.7, v3.0, v3.1, v3.2 | +| Sphinx | v2.2.11 | v2.2.11, v3.4.1 | +| Manticore | v2.8 | v4.0, v6.0 | +| ActiveRecord | v4.2 | v4.2..v7.0 | -h2. Requirements +It _might_ work with older versions of Ruby, but it's highly recommended to update to a supported release. -h3. Sphinx +It should also work with JRuby, but the test environment for that in CI has been unreliable, hence that's not actively tested against at the moment. -Thinking Sphinx v3 is currently built for Sphinx 2.0.5 or newer, and releases since v3.1.0 expect Sphinx 2.1.2 or newer by default. +h3. Sphinx or Manticore + +If you're using Sphinx, v2.2.11 is recommended even though it's quite old, as it works well with PostgreSQL databases (but if you're using MySQL - or real-time indices - then v3.3.1 should also be fine). + +If you're opting for Manticore instead, v2.8 or newer works, but v4 or newer is recommended as that's what is actively tested against. The v4.2 and 5.0 releases had bugs with facet searching, but that's been fixed in Manticore v6.0. h3. Rails and ActiveRecord -Currently Thinking Sphinx 3 is built to support Rails/ActiveRecord 3.2 or newer. If you're using Sinatra and ActiveRecord instead of Rails, that's fine - just make sure you add the @:require => 'thinking_sphinx/sinatra'@ option when listing @thinking-sphinx@ in your Gemfile. +Currently Thinking Sphinx is built to support Rails/ActiveRecord 4.2 or newer. If you're using Sinatra and ActiveRecord instead of Rails, that's fine - just make sure you add the @:require => 'thinking_sphinx/sinatra'@ option when listing @thinking-sphinx@ in your Gemfile. -If you want ActiveRecord 3.1 support, then refer to the 3.0.x releases of Thinking Sphinx. Anything older than that, then you're stuck with Thinking Sphinx v2.x (for Rails/ActiveRecord 3.0) or v1.x (Rails 2.3). Please note that these older versions are no longer actively supported. +If you want ActiveRecord 3.2-4.1 support, then refer to the 4.x releases of Thinking Sphinx. Or, for ActiveRecord 3.1 support, then refer to the 3.0.x releases. Anything older than that, then you're stuck with Thinking Sphinx v2.x (for Rails/ActiveRecord 3.0) or v1.x (Rails 2.3). Please note that these older versions are no longer actively supported. h3. Ruby -You'll need either the standard Ruby (v1.9.3 or newer) or JRuby (1.7.9 or newer). I'm open to patches to improve Rubinius support (if required - it may work with it right now). - -JRuby is only supported as of Thinking Sphinx v3.1.0, and requires Sphinx 2.1.2 or newer. +You'll need either the standard Ruby (v2.4 or newer) or JRuby (9.1 or newer). h3. Database Versions @@ -58,6 +60,8 @@ MySQL 5.x and Postgres 8.4 or better are supported. h2. Contributing +Please note that this project has a "Contributor Code of Conduct":http://contributor-covenant.org/version/1/0/0/. By participating in this project you agree to abide by its terms. + To contribute, clone this repository and have a good look through the specs - you'll notice the distinction between acceptance tests that actually use Sphinx and go through the full stack, and unit tests (everything else) which use liberal test doubles to ensure they're only testing the behaviour of the class in question. I've found this leads to far better code design. All development is done on the @develop@ branch; please base any pull requests off of that branch. Please write the tests and then the code to get them passing, and send through a pull request. @@ -77,4 +81,4 @@ You can then run the unit tests with @rake spec:unit@, the acceptance tests with h2. Licence -Copyright (c) 2007-2015, Thinking Sphinx is developed and maintained by Pat Allan, and is released under the open MIT Licence. Many thanks to "all who have contributed patches":https://github.com/pat/thinking-sphinx/contributors. +Copyright (c) 2007-2024, Thinking Sphinx is developed and maintained by Pat Allan, and is released under the open MIT Licence. Many thanks to "all who have contributed patches":https://github.com/pat/thinking-sphinx/contributors. diff --git a/Rakefile b/Rakefile index 4c9988551..7df04a89b 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'bundler' require 'appraisal' diff --git a/bin/console b/bin/console new file mode 100755 index 000000000..4999689b1 --- /dev/null +++ b/bin/console @@ -0,0 +1,15 @@ +#! /usr/bin/env ruby +# frozen_string_literal: true + +require "bundler/setup" +require "thinking_sphinx" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start(__FILE__) diff --git a/bin/loadsphinx b/bin/loadsphinx new file mode 100755 index 000000000..946e54e2a --- /dev/null +++ b/bin/loadsphinx @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +version=$1 +engine=$2 + +set -e + +load_sphinx () { + distro="xenial" + + case $version in + 2.1.9) + url="http://sphinxsearch.com/files/sphinxsearch_2.1.9-release-0ubuntu11~trusty_amd64.deb" + format="deb" + distro="trusty";; + 2.2.11) + url="http://sphinxsearch.com/files/sphinxsearch_2.2.11-release-1~jessie_amd64.deb" + format="deb" + distro="trusty";; + 3.0.3) + url="http://sphinxsearch.com/files/sphinx-3.0.3-facc3fb-linux-amd64.tar.gz" + format="gz";; + 3.1.1) + url="http://sphinxsearch.com/files/sphinx-3.1.1-612d99f-linux-amd64.tar.gz" + format="gz";; + 3.2.1) + url="http://sphinxsearch.com/files/sphinx-3.2.1-f152e0b-linux-amd64.tar.gz" + format="gz";; + 3.3.1) + url="http://sphinxsearch.com/files/sphinx-3.3.1-b72d67b-linux-amd64.tar.gz" + format="gz";; + 3.4.1) + url="http://sphinxsearch.com/files/sphinx-3.4.1-efbcc65-linux-amd64.tar.gz" + format="gz";; + *) + echo "No Sphinx version $version available" + exit 1;; + esac + + if [ "$distro" == "trusty" ]; then + curl --location http://launchpadlibrarian.net/247512886/libmysqlclient18_5.6.28-1ubuntu3_amd64.deb -o libmysql.deb + sudo apt-get install ./libmysql.deb + fi + + if [ "$format" == "deb" ]; then + curl --location $url -o sphinx.deb + sudo apt-get install libodbc1 + sudo dpkg -i ./sphinx.deb + sudo apt-get install -f + else + curl $url -o sphinx.tar.gz + tar -zxvf sphinx.tar.gz + sudo mv sphinx-$version/bin/* /usr/local/bin/. + fi +} + +load_manticore () { + url="https://github.com/manticoresoftware/manticore/releases/download/$version/manticore_$version.deb" + + case $version in + 2.6.4) + url="https://github.com/manticoresoftware/manticoresearch/releases/download/2.6.4/manticore_2.6.4-180503-37308c3-release-stemmer.xenial_amd64-bin.deb";; + 2.7.5) + url="https://github.com/manticoresoftware/manticoresearch/releases/download/2.7.5/manticore_2.7.5-181204-4a31c54-release-stemmer.xenial_amd64-bin.deb";; + 2.8.2) + url="https://github.com/manticoresoftware/manticoresearch/releases/download/2.8.2/manticore_2.8.2-190402-4e81114d-release-stemmer.stretch_amd64-bin.deb";; + 3.4.2) + url="https://github.com/manticoresoftware/manticoresearch/releases/download/3.4.2/manticore_3.4.2-200410-6903305-release.xenial_amd64-bin.deb";; + 3.5.4) + url="https://repo.manticoresearch.com/repository/manticoresearch_focal/dists/focal/main/binary-amd64/manticore_3.5.4-210107-f70faec5_amd64.deb";; + 4.0.2) + url="https://repo.manticoresearch.com/repository/manticoresearch_focal/dists/focal/main/binary-amd64/manticore_4.0.2-210921-af497f245_amd64.deb";; + 4.2.0) + url="https://repo.manticoresearch.com/repository/manticoresearch_focal/dists/focal/main/binary-amd64/manticore_4.2.0-211223-15e927b28_amd64.deb";; + 6.0.0) + url="skipped";; + *) + echo "No Manticore version $version available" + exit 1;; + esac + + if [ "$version" == "6.0.0" ]; then + curl --location https://repo.manticoresearch.com/manticore-repo.noarch.deb -o repo.deb + sudo dpkg -i repo.deb + sudo apt update + sudo apt install manticore + else + sudo apt-get install default-libmysqlclient-dev + curl --location $url -o manticore.deb + sudo dpkg -i ./manticore.deb + sudo apt-get install -f + fi +} + +if [ "$engine" == "sphinx" ]; then + load_sphinx +else + load_manticore +fi diff --git a/gemfiles/.gitignore b/gemfiles/.gitignore deleted file mode 100644 index 33905cb38..000000000 --- a/gemfiles/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.gemfile.lock \ No newline at end of file diff --git a/gemfiles/rails_3_2.gemfile b/gemfiles/rails_3_2.gemfile deleted file mode 100644 index 6e8bfa3bf..000000000 --- a/gemfiles/rails_3_2.gemfile +++ /dev/null @@ -1,11 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "mysql2", "~> 0.3.12b4", :platform=>:ruby -gem "pg", "~> 0.16.0", :platform=>:ruby -gem "activerecord-jdbcmysql-adapter", "~> 1.3.4", :platform=>:jruby -gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.4", :platform=>:jruby -gem "rails", "~> 3.2.21" - -gemspec :path=>"../" \ No newline at end of file diff --git a/gemfiles/rails_4_0.gemfile b/gemfiles/rails_4_0.gemfile deleted file mode 100644 index 7b8dfd189..000000000 --- a/gemfiles/rails_4_0.gemfile +++ /dev/null @@ -1,11 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "mysql2", "~> 0.3.12b4", :platform=>:ruby -gem "pg", "~> 0.16.0", :platform=>:ruby -gem "activerecord-jdbcmysql-adapter", "~> 1.3.4", :platform=>:jruby -gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.4", :platform=>:jruby -gem "rails", "~> 4.0.12" - -gemspec :path=>"../" \ No newline at end of file diff --git a/gemfiles/rails_4_1.gemfile b/gemfiles/rails_4_1.gemfile deleted file mode 100644 index f79b9af5d..000000000 --- a/gemfiles/rails_4_1.gemfile +++ /dev/null @@ -1,11 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "mysql2", "~> 0.3.12b4", :platform=>:ruby -gem "pg", "~> 0.16.0", :platform=>:ruby -gem "activerecord-jdbcmysql-adapter", "~> 1.3.4", :platform=>:jruby -gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.4", :platform=>:jruby -gem "rails", "~> 4.1.8" - -gemspec :path=>"../" \ No newline at end of file diff --git a/gemfiles/rails_4_2.gemfile b/gemfiles/rails_4_2.gemfile deleted file mode 100644 index 7bf8eb808..000000000 --- a/gemfiles/rails_4_2.gemfile +++ /dev/null @@ -1,11 +0,0 @@ -# This file was generated by Appraisal - -source "https://rubygems.org" - -gem "mysql2", "~> 0.3.12b4", :platform=>:ruby -gem "pg", "~> 0.16.0", :platform=>:ruby -gem "activerecord-jdbcmysql-adapter", "~> 1.3.4", :platform=>:jruby -gem "activerecord-jdbcpostgresql-adapter", "~> 1.3.4", :platform=>:jruby -gem "rails", "~> 4.2.0" - -gemspec :path=>"../" \ No newline at end of file diff --git a/lib/thinking-sphinx.rb b/lib/thinking-sphinx.rb index 861ec024f..60f264cb4 100644 --- a/lib/thinking-sphinx.rb +++ b/lib/thinking-sphinx.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + require 'thinking_sphinx' diff --git a/lib/thinking/sphinx.rb b/lib/thinking/sphinx.rb index 861ec024f..60f264cb4 100644 --- a/lib/thinking/sphinx.rb +++ b/lib/thinking/sphinx.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + require 'thinking_sphinx' diff --git a/lib/thinking_sphinx.rb b/lib/thinking_sphinx.rb index c219fca57..89de63a2a 100644 --- a/lib/thinking_sphinx.rb +++ b/lib/thinking_sphinx.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + if RUBY_PLATFORM == 'java' require 'java' require 'jdbc/mysql' @@ -16,7 +18,7 @@ module ThinkingSphinx def self.count(query = '', options = {}) - search(query, options).total_entries + search_for_ids(query, options).total_entries end def self.facets(query = '', options = {}) @@ -32,22 +34,45 @@ def self.search_for_ids(query = '', options = {}) ThinkingSphinx::Search::Merger.new(search).merge! nil, :ids_only => true end + def self.none + ThinkingSphinx::Search.new nil, :none => true + end + def self.before_index_hooks @before_index_hooks end @before_index_hooks = [] + def self.output + @output + end + + @output = STDOUT + + def self.rake_interface + @rake_interface ||= ThinkingSphinx::RakeInterface + end + + def self.rake_interface=(interface) + @rake_interface = interface + end + + module Hooks; end + module IndexingStrategies; end module Subscribers; end end # Core +require 'thinking_sphinx/attribute_types' require 'thinking_sphinx/batched_search' require 'thinking_sphinx/callbacks' require 'thinking_sphinx/core' +require 'thinking_sphinx/with_output' +require 'thinking_sphinx/commander' +require 'thinking_sphinx/commands' require 'thinking_sphinx/configuration' require 'thinking_sphinx/connection' -require 'thinking_sphinx/controller' require 'thinking_sphinx/deletion' require 'thinking_sphinx/errors' require 'thinking_sphinx/excerpter' @@ -56,25 +81,31 @@ module Subscribers; end require 'thinking_sphinx/float_formatter' require 'thinking_sphinx/frameworks' require 'thinking_sphinx/guard' +require 'thinking_sphinx/hooks/guard_presence' require 'thinking_sphinx/index' +require 'thinking_sphinx/indexing_strategies/all_at_once' +require 'thinking_sphinx/indexing_strategies/one_at_a_time' require 'thinking_sphinx/index_set' +require 'thinking_sphinx/interfaces' require 'thinking_sphinx/masks' require 'thinking_sphinx/middlewares' require 'thinking_sphinx/panes' +require 'thinking_sphinx/processor' require 'thinking_sphinx/query' require 'thinking_sphinx/rake_interface' require 'thinking_sphinx/scopes' require 'thinking_sphinx/search' -require 'thinking_sphinx/sphinxql' +require 'thinking_sphinx/settings' require 'thinking_sphinx/subscribers/populator_subscriber' require 'thinking_sphinx/test' require 'thinking_sphinx/utf8' require 'thinking_sphinx/wildcard' # Extended -require 'thinking_sphinx/active_record' require 'thinking_sphinx/deltas' require 'thinking_sphinx/distributed' require 'thinking_sphinx/logger' require 'thinking_sphinx/real_time' require 'thinking_sphinx/railtie' if defined?(Rails::Railtie) + +ThinkingSphinx.before_index_hooks << ThinkingSphinx::Hooks::GuardPresence diff --git a/lib/thinking_sphinx/active_record.rb b/lib/thinking_sphinx/active_record.rb index c78f668ed..4baca3c11 100644 --- a/lib/thinking_sphinx/active_record.rb +++ b/lib/thinking_sphinx/active_record.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + require 'active_record' require 'joiner' module ThinkingSphinx::ActiveRecord module Callbacks; end + module Depolymorph; end end require 'thinking_sphinx/active_record/property' @@ -14,7 +17,6 @@ module Callbacks; end require 'thinking_sphinx/active_record/column_sql_presenter' require 'thinking_sphinx/active_record/database_adapters' require 'thinking_sphinx/active_record/field' -require 'thinking_sphinx/active_record/filter_reflection' require 'thinking_sphinx/active_record/index' require 'thinking_sphinx/active_record/interpreter' require 'thinking_sphinx/active_record/join_association' @@ -23,9 +25,20 @@ module Callbacks; end require 'thinking_sphinx/active_record/property_query' require 'thinking_sphinx/active_record/property_sql_presenter' require 'thinking_sphinx/active_record/simple_many_query' +require 'thinking_sphinx/active_record/source_joins' require 'thinking_sphinx/active_record/sql_builder' require 'thinking_sphinx/active_record/sql_source' +require 'thinking_sphinx/active_record/callbacks/association_delta_callbacks' require 'thinking_sphinx/active_record/callbacks/delete_callbacks' require 'thinking_sphinx/active_record/callbacks/delta_callbacks' require 'thinking_sphinx/active_record/callbacks/update_callbacks' + +require 'thinking_sphinx/active_record/depolymorph/base_reflection' +require 'thinking_sphinx/active_record/depolymorph/association_reflection' +require 'thinking_sphinx/active_record/depolymorph/conditions_reflection' +require 'thinking_sphinx/active_record/depolymorph/overridden_reflection' +require 'thinking_sphinx/active_record/depolymorph/scoped_reflection' +require 'thinking_sphinx/active_record/filter_reflection' + +ActiveRecord::Base.include ThinkingSphinx::ActiveRecord::Base diff --git a/lib/thinking_sphinx/active_record/association.rb b/lib/thinking_sphinx/active_record/association.rb index 927a73b69..de3b6f833 100644 --- a/lib/thinking_sphinx/active_record/association.rb +++ b/lib/thinking_sphinx/active_record/association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Association def initialize(column) @column = column diff --git a/lib/thinking_sphinx/active_record/association_proxy.rb b/lib/thinking_sphinx/active_record/association_proxy.rb index f61d94733..da3e68ce6 100644 --- a/lib/thinking_sphinx/active_record/association_proxy.rb +++ b/lib/thinking_sphinx/active_record/association_proxy.rb @@ -1,6 +1,6 @@ -module ThinkingSphinx::ActiveRecord::AssociationProxy - extend ActiveSupport::Concern +# frozen_string_literal: true +module ThinkingSphinx::ActiveRecord::AssociationProxy def search(query = nil, options = {}) perform_search super(*normalise_search_arguments(query, options)) end @@ -10,6 +10,7 @@ def search_for_ids(query = nil, options = {}) end private + def normalise_search_arguments(query, options) query, options = nil, query if query.is_a?(Hash) options[:ignore_scopes] = true diff --git a/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb b/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb index 6a165f287..158549706 100644 --- a/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb +++ b/lib/thinking_sphinx/active_record/association_proxy/attribute_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::AssociationProxy::AttributeFinder def initialize(association) @association = association @@ -29,7 +31,7 @@ def indices @indices ||= begin configuration.preload_indices configuration.indices_for_references( - *@association.klass.name.underscore.to_sym + *configuration.index_set_class.reference_name(@association.klass) ).reject &:distributed? end end diff --git a/lib/thinking_sphinx/active_record/association_proxy/attribute_matcher.rb b/lib/thinking_sphinx/active_record/association_proxy/attribute_matcher.rb index eccfd3937..ada0667b0 100644 --- a/lib/thinking_sphinx/active_record/association_proxy/attribute_matcher.rb +++ b/lib/thinking_sphinx/active_record/association_proxy/attribute_matcher.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::AssociationProxy::AttributeMatcher def initialize(attribute, foreign_key) @attribute, @foreign_key = attribute, foreign_key.to_s diff --git a/lib/thinking_sphinx/active_record/attribute.rb b/lib/thinking_sphinx/active_record/attribute.rb index 6b8569efc..7198475d4 100644 --- a/lib/thinking_sphinx/active_record/attribute.rb +++ b/lib/thinking_sphinx/active_record/attribute.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Attribute < ThinkingSphinx::ActiveRecord::Property diff --git a/lib/thinking_sphinx/active_record/attribute/sphinx_presenter.rb b/lib/thinking_sphinx/active_record/attribute/sphinx_presenter.rb index d253f111c..28b87fef0 100644 --- a/lib/thinking_sphinx/active_record/attribute/sphinx_presenter.rb +++ b/lib/thinking_sphinx/active_record/attribute/sphinx_presenter.rb @@ -1,13 +1,16 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Attribute::SphinxPresenter SPHINX_TYPES = { :integer => :uint, :boolean => :bool, - :timestamp => :timestamp, + :timestamp => :uint, :float => :float, :string => :string, :bigint => :bigint, :ordinal => :str2ordinal, - :wordcount => :str2wordcount + :wordcount => :str2wordcount, + :json => :json } def initialize(attribute, source) diff --git a/lib/thinking_sphinx/active_record/attribute/type.rb b/lib/thinking_sphinx/active_record/attribute/type.rb index f85a620a1..df89fbcc7 100644 --- a/lib/thinking_sphinx/active_record/attribute/type.rb +++ b/lib/thinking_sphinx/active_record/attribute/type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Attribute::Type UPDATEABLE_TYPES = [:integer, :timestamp, :boolean, :float] @@ -41,7 +43,7 @@ def associations end def big_integer? - database_column.type == :integer && database_column.sql_type[/bigint/i] + type_symbol == :integer && database_column.sql_type[/bigint/i] end def column_name @@ -72,19 +74,33 @@ def single_column_reference? def type_from_database raise ThinkingSphinx::MissingColumnError, - "column #{column_name} does not exist" if database_column.nil? + "Cannot determine the database type of column #{column_name}, as it does not exist" if database_column.nil? return :bigint if big_integer? - case database_column.type + case type_symbol when :datetime, :date :timestamp when :text :string when :decimal :float + when :integer, :boolean, :timestamp, :float, :string, :bigint, :json + type_symbol else - database_column.type + raise ThinkingSphinx::UnknownAttributeType, + <<-ERROR +Unable to determine an equivalent Sphinx attribute type from #{database_column.type.class.name} for attribute #{attribute.name}. You may want to manually set the type. + +e.g. + has my_column, :type => :integer + ERROR end end + + def type_symbol + return database_column.type if database_column.type.is_a?(Symbol) + + database_column.type.class.name.demodulize.downcase.to_sym + end end diff --git a/lib/thinking_sphinx/active_record/attribute/values.rb b/lib/thinking_sphinx/active_record/attribute/values.rb index 1b1fc5be0..5058b1f20 100644 --- a/lib/thinking_sphinx/active_record/attribute/values.rb +++ b/lib/thinking_sphinx/active_record/attribute/values.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Attribute::Values def initialize(attribute) @attribute = attribute diff --git a/lib/thinking_sphinx/active_record/base.rb b/lib/thinking_sphinx/active_record/base.rb index 282214180..a093e0628 100644 --- a/lib/thinking_sphinx/active_record/base.rb +++ b/lib/thinking_sphinx/active_record/base.rb @@ -1,35 +1,67 @@ +# frozen_string_literal: true + module ThinkingSphinx::ActiveRecord::Base extend ActiveSupport::Concern included do - after_destroy ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks - before_save ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks - after_update ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks - after_commit ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks + # Avoid method collisions for public Thinking Sphinx methods added to all + # ActiveRecord models. The `sphinx_`-prefixed versions will always exist, + # and the non-prefixed versions will be added if a method of that name + # doesn't already exist. + # + # If a method is overwritten later by something else, that's also fine - the + # prefixed versions will still be there. + class_module = ThinkingSphinx::ActiveRecord::Base::ClassMethods + class_module.public_instance_methods.each do |method_name| + short_method = method_name.to_s.delete_prefix("sphinx_").to_sym + next if methods.include?(short_method) + + define_singleton_method(short_method, method(method_name)) + end + + if ActiveRecord::VERSION::STRING.to_i >= 5 + [ + ::ActiveRecord::Reflection::HasManyReflection, + ::ActiveRecord::Reflection::HasAndBelongsToManyReflection + ].each do |reflection_class| + reflection_class.include DefaultReflectionAssociations + end + else + ::ActiveRecord::Associations::CollectionProxy.include( + ThinkingSphinx::ActiveRecord::AssociationProxy + ) + end + end - ::ActiveRecord::Associations::CollectionProxy.send :include, - ThinkingSphinx::ActiveRecord::AssociationProxy + module DefaultReflectionAssociations + def extensions + super + [ThinkingSphinx::ActiveRecord::AssociationProxy] + end end module ClassMethods - def facets(query = nil, options = {}) + def sphinx_facets(query = nil, options = {}) merge_search ThinkingSphinx.facets, query, options end - def search(query = nil, options = {}) + def sphinx_search(query = nil, options = {}) merge_search ThinkingSphinx.search, query, options end - def search_count(query = nil, options = {}) - search(query, options).total_entries + def sphinx_search_count(query = nil, options = {}) + search_for_ids(query, options).total_entries end - def search_for_ids(query = nil, options = {}) + def sphinx_search_for_ids(query = nil, options = {}) ThinkingSphinx::Search::Merger.new( search(query, options) ).merge! nil, :ids_only => true end + def sphinx_search_none + merge_search ThinkingSphinx.search, nil, none: true + end + private def default_sphinx_scope? diff --git a/lib/thinking_sphinx/active_record/callbacks/association_delta_callbacks.rb b/lib/thinking_sphinx/active_record/callbacks/association_delta_callbacks.rb new file mode 100644 index 000000000..f51f3ae24 --- /dev/null +++ b/lib/thinking_sphinx/active_record/callbacks/association_delta_callbacks.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class ThinkingSphinx::ActiveRecord::Callbacks::AssociationDeltaCallbacks + def initialize(path) + @path = path + end + + def after_commit(instance) + Array(objects_for(instance)).each do |object| + object.update :delta => true unless object.frozen? + end + end + + private + + attr_reader :path + + def objects_for(instance) + path.inject(instance) { |object, method| object.send method } + end +end diff --git a/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb b/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb index 8d3c961e8..1f8e75612 100644 --- a/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb +++ b/lib/thinking_sphinx/active_record/callbacks/delete_callbacks.rb @@ -1,21 +1,27 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks < ThinkingSphinx::Callbacks - callbacks :after_destroy + callbacks :after_commit, :after_destroy, :after_rollback + + def after_commit + delete_from_sphinx + end def after_destroy - return if instance.new_record? + delete_from_sphinx + end - indices.each { |index| - ThinkingSphinx::Deletion.perform index, instance.id - } + def after_rollback + delete_from_sphinx end private - def indices - ThinkingSphinx::Configuration.instance.index_set_class.new( - :classes => [instance.class] - ).to_a + def delete_from_sphinx + return if ThinkingSphinx::Callbacks.suspended? + + ThinkingSphinx::Processor.new(instance: instance).delete end end diff --git a/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb b/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb index 1789b6bc2..6cdf4422f 100644 --- a/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb +++ b/lib/thinking_sphinx/active_record/callbacks/delta_callbacks.rb @@ -1,12 +1,12 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks < ThinkingSphinx::Callbacks callbacks :after_commit, :before_save def after_commit - return unless delta_indices? && processors.any? { |processor| - processor.toggled?(instance) - } && !ThinkingSphinx::Deltas.suspended? + return unless !suspended? && delta_indices? && toggled? delta_indices.each do |index| index.delta_processor.index index @@ -18,7 +18,8 @@ def after_commit end def before_save - return unless delta_indices? + return unless !ThinkingSphinx::Callbacks.suspended? && delta_indices? && + new_or_changed? processors.each { |processor| processor.toggle instance } end @@ -42,10 +43,24 @@ def delta_indices? end def indices - @indices ||= config.index_set_class.new :classes => [instance.class] + @indices ||= config.index_set_class.new( + :instances => [instance], :classes => [instance.class] + ).select { |index| index.type == "plain" } + end + + def new_or_changed? + instance.new_record? || instance.changed? end def processors delta_indices.collect &:delta_processor end + + def suspended? + ThinkingSphinx::Callbacks.suspended? || ThinkingSphinx::Deltas.suspended? + end + + def toggled? + processors.any? { |processor| processor.toggled?(instance) } + end end diff --git a/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb b/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb index 16120bb71..ee9add052 100644 --- a/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb +++ b/lib/thinking_sphinx/active_record/callbacks/update_callbacks.rb @@ -1,10 +1,18 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks < ThinkingSphinx::Callbacks + if ActiveRecord::Base.instance_methods.grep(/saved_changes/).any? + CHANGED_ATTRIBUTES = lambda { |instance| instance.saved_changes.keys } + else + CHANGED_ATTRIBUTES = lambda { |instance| instance.changed } + end + callbacks :after_update def after_update - return unless updates_enabled? + return unless !ThinkingSphinx::Callbacks.suspended? && updates_enabled? indices.each do |index| update index unless index.distributed? @@ -15,7 +23,7 @@ def after_update def attributes_hash_for(index) updateable_attributes_for(index).inject({}) do |hash, attribute| - if instance.changed.include?(attribute.columns.first.__name.to_s) + if changed_attributes.include?(attribute.columns.first.__name.to_s) hash[attribute.name] = attribute.value_for(instance) end @@ -23,6 +31,10 @@ def attributes_hash_for(index) end end + def changed_attributes + @changed_attributes ||= CHANGED_ATTRIBUTES.call instance + end + def configuration ThinkingSphinx::Configuration.instance end @@ -35,7 +47,7 @@ def indices end def reference - instance.class.name.underscore.to_sym + configuration.index_set_class.reference_name(instance.class) end def update(index) @@ -43,7 +55,9 @@ def update(index) return if attributes.empty? sphinxql = Riddle::Query.update( - index.name, index.document_id_for_key(instance.id), attributes + index.name, + index.document_id_for_key(instance.public_send(index.primary_key)), + attributes ) ThinkingSphinx::Connection.take do |connection| connection.execute(sphinxql) diff --git a/lib/thinking_sphinx/active_record/column.rb b/lib/thinking_sphinx/active_record/column.rb index 598e92c4e..99bbe48d0 100644 --- a/lib/thinking_sphinx/active_record/column.rb +++ b/lib/thinking_sphinx/active_record/column.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Column def initialize(*stack) @stack = stack diff --git a/lib/thinking_sphinx/active_record/column_sql_presenter.rb b/lib/thinking_sphinx/active_record/column_sql_presenter.rb index c0b47f54c..0339bed37 100644 --- a/lib/thinking_sphinx/active_record/column_sql_presenter.rb +++ b/lib/thinking_sphinx/active_record/column_sql_presenter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::ColumnSQLPresenter def initialize(model, column, adapter, associations) @model, @column, @adapter, @associations = model, column, adapter, associations @@ -13,7 +15,9 @@ def with_table return __name if string? return nil unless exists? - "#{associations.alias_for(__stack)}.#{adapter.quote __name}" + quoted_table = escape_table? ? escape_table(table) : table + + "#{quoted_table}.#{adapter.quote __name}" end private @@ -22,6 +26,14 @@ def with_table delegate :__stack, :__name, :string?, :to => :column + def escape_table(table_name) + table_name.split('.').map { |t| adapter.quote(t) }.join('.') + end + + def escape_table? + table[/[`"]/].nil? + end + def exists? path.model.column_names.include?(column.__name.to_s) rescue Joiner::AssociationNotFound @@ -31,4 +43,12 @@ def exists? def path Joiner::Path.new model, column.__stack end + + def table + associations.alias_for __stack + end + + def version + ActiveRecord::VERSION + end end diff --git a/lib/thinking_sphinx/active_record/database_adapters.rb b/lib/thinking_sphinx/active_record/database_adapters.rb index 2a444586f..4d1c79458 100644 --- a/lib/thinking_sphinx/active_record/database_adapters.rb +++ b/lib/thinking_sphinx/active_record/database_adapters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::ActiveRecord::DatabaseAdapters class << self attr_accessor :default @@ -12,7 +14,7 @@ def adapter_for(model) when :postgresql PostgreSQLAdapter else - raise "Invalid Database Adapter '#{adapter}': Thinking Sphinx only supports MySQL and PostgreSQL." + raise ThinkingSphinx::InvalidDatabaseAdapter, "Invalid adapter '#{adapter}': Thinking Sphinx only supports MySQL and PostgreSQL." end klass.new model diff --git a/lib/thinking_sphinx/active_record/database_adapters/abstract_adapter.rb b/lib/thinking_sphinx/active_record/database_adapters/abstract_adapter.rb index eb3460996..7c3240c09 100644 --- a/lib/thinking_sphinx/active_record/database_adapters/abstract_adapter.rb +++ b/lib/thinking_sphinx/active_record/database_adapters/abstract_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::DatabaseAdapters::AbstractAdapter def initialize(model) @model = model diff --git a/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb b/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb index 943fbaffc..14d1f8f69 100644 --- a/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb +++ b/lib/thinking_sphinx/active_record/database_adapters/mysql_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter < ThinkingSphinx::ActiveRecord::DatabaseAdapters::AbstractAdapter @@ -38,6 +40,12 @@ def time_zone_query_pre end def utf8_query_pre - ['SET NAMES utf8'] + ["SET NAMES #{settings['mysql_encoding']}"] + end + + private + + def settings + ThinkingSphinx::Configuration.instance.settings end end diff --git a/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb b/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb index 3acc963c3..eec4327ae 100644 --- a/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb +++ b/lib/thinking_sphinx/active_record/database_adapters/postgresql_adapter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter < ThinkingSphinx::ActiveRecord::DatabaseAdapters::AbstractAdapter diff --git a/lib/thinking_sphinx/active_record/depolymorph/association_reflection.rb b/lib/thinking_sphinx/active_record/depolymorph/association_reflection.rb new file mode 100644 index 000000000..96e556bcb --- /dev/null +++ b/lib/thinking_sphinx/active_record/depolymorph/association_reflection.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# This custom association approach is only available in Rails 4.1-5.1. This +# behaviour is superseded by OverriddenReflection for Rails 5.2, and was +# preceded by ScopedReflection for Rails 4.0. +class ThinkingSphinx::ActiveRecord::Depolymorph::AssociationReflection < + ThinkingSphinx::ActiveRecord::Depolymorph::BaseReflection + + # Since Rails 4.2, the macro argument has been removed. The underlying + # behaviour remains the same, though. + def call + if explicit_macro? + klass.new name, nil, options, reflection.active_record + else + klass.new reflection.macro, name, nil, options, reflection.active_record + end + end + + private + + def explicit_macro? + ActiveRecord::Reflection::MacroReflection.instance_method(:initialize). + arity == 4 + end + + def options + super + + @options[:sphinx_internal_filtered] = true + @options + end +end diff --git a/lib/thinking_sphinx/active_record/depolymorph/base_reflection.rb b/lib/thinking_sphinx/active_record/depolymorph/base_reflection.rb new file mode 100644 index 000000000..c6f389301 --- /dev/null +++ b/lib/thinking_sphinx/active_record/depolymorph/base_reflection.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class ThinkingSphinx::ActiveRecord::Depolymorph::BaseReflection + def initialize(reflection, name, class_name) + @reflection = reflection + @name = name + @class_name = class_name + + @options = reflection.options.clone + end + + def call + # Should be implemented by subclasses. + end + + private + + attr_reader :reflection, :name, :class_name + + def klass + reflection.class + end + + def options + @options.delete :polymorphic + @options[:class_name] = class_name + @options[:foreign_key] ||= "#{reflection.name}_id" + @options[:foreign_type] = reflection.foreign_type + + @options + end +end diff --git a/lib/thinking_sphinx/active_record/depolymorph/conditions_reflection.rb b/lib/thinking_sphinx/active_record/depolymorph/conditions_reflection.rb new file mode 100644 index 000000000..e88e140ab --- /dev/null +++ b/lib/thinking_sphinx/active_record/depolymorph/conditions_reflection.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# The conditions approach is only available in Rails 3. This behaviour is +# superseded by ScopedReflection for Rails 4.0. +class ThinkingSphinx::ActiveRecord::Depolymorph::ConditionsReflection < + ThinkingSphinx::ActiveRecord::Depolymorph::BaseReflection + + def call + klass.new reflection.macro, name, options, active_record + end + + private + + delegate :foreign_type, :active_record, :to => :reflection + + def condition + "::ts_join_alias::.#{quoted_foreign_type} = '#{class_name}'" + end + + def options + super + + case @options[:conditions] + when nil + @options[:conditions] = condition + when Array + @options[:conditions] << condition + when Hash + @options[:conditions].merge! foreign_type => @options[:class_name] + else + @options[:conditions] = "#{@options[:conditions]} AND #{condition}" + end + + @options + end + + def quoted_foreign_type + active_record.connection.quote_column_name foreign_type + end +end diff --git a/lib/thinking_sphinx/active_record/depolymorph/overridden_reflection.rb b/lib/thinking_sphinx/active_record/depolymorph/overridden_reflection.rb new file mode 100644 index 000000000..0b40f1637 --- /dev/null +++ b/lib/thinking_sphinx/active_record/depolymorph/overridden_reflection.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# This overriding approach is only available in Rails 5.2+. This behaviour +# was preceded by AssociationReflection for Rails 4.1-5.1. +class ThinkingSphinx::ActiveRecord::Depolymorph::OverriddenReflection < + ThinkingSphinx::ActiveRecord::Depolymorph::BaseReflection + + module BuildJoinConstraint + def build_join_constraint(table, foreign_table) + super.and( + foreign_table[options[:foreign_type]].eq( + options[:class_name].constantize.base_class.name + ) + ) + end + end + + module JoinScope + def join_scope(table, foreign_table, foreign_klass) + super.where( + foreign_table[options[:foreign_type]].eq( + options[:class_name].constantize.base_class.name + ) + ) + end + end + + def self.overridden_classes + @overridden_classes ||= {} + end + + def call + klass.new name, nil, options, reflection.active_record + end + + private + + def klass + self.class.overridden_classes[reflection.class] ||= begin + subclass = Class.new reflection.class + subclass.include extension(reflection) + subclass + end + end + + def extension(reflection) + reflection.respond_to?(:build_join_constraint) ? + BuildJoinConstraint : JoinScope + end +end diff --git a/lib/thinking_sphinx/active_record/depolymorph/scoped_reflection.rb b/lib/thinking_sphinx/active_record/depolymorph/scoped_reflection.rb new file mode 100644 index 000000000..e99a2089f --- /dev/null +++ b/lib/thinking_sphinx/active_record/depolymorph/scoped_reflection.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# This scoped approach is only available in Rails 4.0. This behaviour is +# superseded by AssociationReflection for Rails 4.1, and was preceded by +# ConditionsReflection for Rails 3.2. +class ThinkingSphinx::ActiveRecord::Depolymorph::ScopedReflection < + ThinkingSphinx::ActiveRecord::Depolymorph::BaseReflection + + def call + klass.new reflection.macro, name, scope, options, + reflection.active_record + end + + private + + def scope + lambda { |association| + reflection = association.reflection + klass = reflection.class_name.constantize + where( + association.parent.aliased_table_name.to_sym => + {reflection.foreign_type => klass.base_class.name} + ) + } + end +end diff --git a/lib/thinking_sphinx/active_record/field.rb b/lib/thinking_sphinx/active_record/field.rb index 86f62acb0..fefbee6c0 100644 --- a/lib/thinking_sphinx/active_record/field.rb +++ b/lib/thinking_sphinx/active_record/field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Field < ThinkingSphinx::ActiveRecord::Property include ThinkingSphinx::Core::Field diff --git a/lib/thinking_sphinx/active_record/filter_reflection.rb b/lib/thinking_sphinx/active_record/filter_reflection.rb index d2dff4845..816520b0e 100644 --- a/lib/thinking_sphinx/active_record/filter_reflection.rb +++ b/lib/thinking_sphinx/active_record/filter_reflection.rb @@ -1,75 +1,18 @@ -class ThinkingSphinx::ActiveRecord::FilterReflection - attr_reader :reflection, :class_name - - delegate :foreign_type, :active_record, :to => :reflection - - def self.call(reflection, name, class_name) - filter = new(reflection, class_name) - klass = reflection.class - - if defined?(ActiveRecord::Reflection::MacroReflection) - klass.new name, filter.scope, filter.options, reflection.active_record - elsif reflection.respond_to?(:scope) - klass.new reflection.macro, name, filter.scope, filter.options, - reflection.active_record - else - klass.new reflection.macro, name, filter.options, - reflection.active_record - end - end - - def initialize(reflection, class_name) - @reflection, @class_name = reflection, class_name - @options = reflection.options.clone - end - - def options - @options.delete :polymorphic - @options[:class_name] = class_name - @options[:foreign_key] ||= "#{reflection.name}_id" - @options[:foreign_type] = reflection.foreign_type - - if reflection.respond_to?(:scope) - @options[:sphinx_internal_filtered] = true - return @options - end +# frozen_string_literal: true - case @options[:conditions] - when nil - @options[:conditions] = condition - when Array - @options[:conditions] << condition - when Hash - @options[:conditions].merge!(reflection.foreign_type => @options[:class_name]) - else - @options[:conditions] << " AND #{condition}" - end - - @options - end - - def scope - if ::Joiner::Joins.instance_methods.include?(:join_association_class) - return nil - end - - lambda { |association| - reflection = association.reflection - klass = reflection.class_name.constantize - where( - association.parent.aliased_table_name.to_sym => - {reflection.foreign_type => klass.base_class.name} - ) - } - end - - private - - def condition - "::ts_join_alias::.#{quoted_foreign_type} = '#{class_name}'" +class ThinkingSphinx::ActiveRecord::FilterReflection + ReflectionGenerator = case ActiveRecord::VERSION::STRING.to_f + when 5.2..7.1 + ThinkingSphinx::ActiveRecord::Depolymorph::OverriddenReflection + when 4.1..5.1 + ThinkingSphinx::ActiveRecord::Depolymorph::AssociationReflection + when 4.0 + ThinkingSphinx::ActiveRecord::Depolymorph::ScopedReflection + when 3.2 + ThinkingSphinx::ActiveRecord::Depolymorph::ConditionsReflection end - def quoted_foreign_type - active_record.connection.quote_column_name foreign_type + def self.call(reflection, name, class_name) + ReflectionGenerator.new(reflection, name, class_name).call end end diff --git a/lib/thinking_sphinx/active_record/index.rb b/lib/thinking_sphinx/active_record/index.rb index eac534bef..a6b659254 100644 --- a/lib/thinking_sphinx/active_record/index.rb +++ b/lib/thinking_sphinx/active_record/index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Index < Riddle::Configuration::Index include ThinkingSphinx::Core::Index @@ -28,6 +30,10 @@ def facets @facets ||= sources.collect(&:facets).flatten end + def fields + sources.collect(&:fields).flatten + end + def sources interpret_definition! super @@ -44,10 +50,6 @@ def adapter adapter_for(model) end - def fields - sources.collect(&:fields).flatten - end - def interpreter ThinkingSphinx::ActiveRecord::Interpreter end @@ -63,7 +65,7 @@ def source_options :delta? => @options[:delta?], :delta_processor => @options[:delta_processor], :delta_options => @options[:delta_options], - :primary_key => @options[:primary_key] || model.primary_key || :id + :primary_key => primary_key } end end diff --git a/lib/thinking_sphinx/active_record/interpreter.rb b/lib/thinking_sphinx/active_record/interpreter.rb index 25652d344..a6e736d98 100644 --- a/lib/thinking_sphinx/active_record/interpreter.rb +++ b/lib/thinking_sphinx/active_record/interpreter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Interpreter < ::ThinkingSphinx::Core::Interpreter @@ -11,15 +13,15 @@ def group_by(*columns) end def has(*columns) - __source.attributes += build_properties( + build_properties( ::ThinkingSphinx::ActiveRecord::Attribute, columns - ) + ).each { |attribute| __source.add_attribute attribute } end def indexes(*columns) - __source.fields += build_properties( + build_properties( ::ThinkingSphinx::ActiveRecord::Field, columns - ) + ).each { |field| __source.add_field field } end def join(*columns) @@ -39,10 +41,10 @@ def sanitize_sql(*arguments) end def set_database(hash_or_key) - configuration = hash_or_key.is_a?(::Hash) ? hash_or_key.symbolize_keys : + configuration = hash_or_key.is_a?(::Hash) ? hash_or_key : ::ActiveRecord::Base.configurations[hash_or_key.to_s] - __source.set_database_settings configuration + __source.set_database_settings configuration.symbolize_keys end def set_property(properties) diff --git a/lib/thinking_sphinx/active_record/join_association.rb b/lib/thinking_sphinx/active_record/join_association.rb index b95127b1b..d4cb5dc7b 100644 --- a/lib/thinking_sphinx/active_record/join_association.rb +++ b/lib/thinking_sphinx/active_record/join_association.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::JoinAssociation < ::ActiveRecord::Associations::JoinDependency::JoinAssociation @@ -5,7 +7,9 @@ def build_constraint(klass, table, key, foreign_table, foreign_key) constraint = super constraint = constraint.and( - foreign_table[reflection.options[:foreign_type]].eq(base_klass.name) + foreign_table[reflection.options[:foreign_type]].eq( + base_klass.base_class.name + ) ) if reflection.options[:sphinx_internal_filtered] constraint diff --git a/lib/thinking_sphinx/active_record/log_subscriber.rb b/lib/thinking_sphinx/active_record/log_subscriber.rb index c03de51c7..7187b6ce3 100644 --- a/lib/thinking_sphinx/active_record/log_subscriber.rb +++ b/lib/thinking_sphinx/active_record/log_subscriber.rb @@ -1,18 +1,37 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::LogSubscriber < ActiveSupport::LogSubscriber def guard(event) - identifier = color 'Sphinx', GREEN, true + identifier = colored_text "Sphinx" warn " #{identifier} #{event.payload[:guard]}" end def message(event) - identifier = color 'Sphinx', GREEN, true + identifier = colored_text "Sphinx" debug " #{identifier} #{event.payload[:message]}" end def query(event) - identifier = color('Sphinx Query (%.1fms)' % event.duration, GREEN, true) + identifier = colored_text("Sphinx Query (%.1fms)" % event.duration) debug " #{identifier} #{event.payload[:query]}" end + + def caution(event) + identifier = colored_text "Sphinx" + warn " #{identifier} #{event.payload[:caution]}" + end + + private + + if Rails.gem_version >= Gem::Version.new("7.1.0") + def colored_text(text) + color text, GREEN, bold: true + end + else + def colored_text(text) + color text, GREEN, true + end + end end ThinkingSphinx::ActiveRecord::LogSubscriber.attach_to :thinking_sphinx diff --git a/lib/thinking_sphinx/active_record/polymorpher.rb b/lib/thinking_sphinx/active_record/polymorpher.rb index 92c061c6c..f03e7788e 100644 --- a/lib/thinking_sphinx/active_record/polymorpher.rb +++ b/lib/thinking_sphinx/active_record/polymorpher.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Polymorpher def initialize(source, column, class_names) @source, @column, @class_names = source, column, class_names diff --git a/lib/thinking_sphinx/active_record/property.rb b/lib/thinking_sphinx/active_record/property.rb index c2c4a8fb4..27ef3c8c3 100644 --- a/lib/thinking_sphinx/active_record/property.rb +++ b/lib/thinking_sphinx/active_record/property.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::Property include ThinkingSphinx::Core::Property diff --git a/lib/thinking_sphinx/active_record/property_query.rb b/lib/thinking_sphinx/active_record/property_query.rb index df2ca96c8..677677545 100644 --- a/lib/thinking_sphinx/active_record/property_query.rb +++ b/lib/thinking_sphinx/active_record/property_query.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::PropertyQuery def initialize(property, source, type = nil) @property, @source, @type = property, source, type @@ -25,6 +27,7 @@ def to_s attr_reader :property, :source, :type delegate :unscoped, :to => :base_association_class, :prefix => true + delegate :sql, :to => Arel def base_association reflections.first @@ -133,7 +136,7 @@ def to_sql relation = relation.joins(joins) if joins.present? relation = relation.where("#{quoted_foreign_key} BETWEEN $start AND $end") if ranged? relation = relation.where("#{quoted_foreign_key} IS NOT NULL") - relation = relation.order("#{quoted_foreign_key} ASC") if type.nil? + relation = relation.order(sql("#{quoted_foreign_key} ASC")) if type.nil? relation.to_sql end diff --git a/lib/thinking_sphinx/active_record/property_sql_presenter.rb b/lib/thinking_sphinx/active_record/property_sql_presenter.rb index c846be8b8..ea122c065 100644 --- a/lib/thinking_sphinx/active_record/property_sql_presenter.rb +++ b/lib/thinking_sphinx/active_record/property_sql_presenter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::PropertySQLPresenter attr_reader :property, :adapter, :associations diff --git a/lib/thinking_sphinx/active_record/simple_many_query.rb b/lib/thinking_sphinx/active_record/simple_many_query.rb index 943694bde..6e868454b 100644 --- a/lib/thinking_sphinx/active_record/simple_many_query.rb +++ b/lib/thinking_sphinx/active_record/simple_many_query.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::SimpleManyQuery < ThinkingSphinx::ActiveRecord::PropertyQuery diff --git a/lib/thinking_sphinx/active_record/source_joins.rb b/lib/thinking_sphinx/active_record/source_joins.rb new file mode 100644 index 000000000..60f9f8ce5 --- /dev/null +++ b/lib/thinking_sphinx/active_record/source_joins.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +class ThinkingSphinx::ActiveRecord::SourceJoins + def self.call(model, source) + new(model, source).call + end + + def initialize(model, source) + @model, @source = model, source + end + + def call + append_specified_associations + append_property_associations + + joins + end + + private + + attr_reader :model, :source + + def append_property_associations + source.properties.collect(&:columns).each do |columns| + columns.each { |column| append_column_associations column } + end + end + + def append_column_associations(column) + return if column.__stack.empty? or column_included_in_queries?(column) + + joins.add_join_to column.__stack if column_exists?(column) + end + + def append_specified_associations + source.associations.reject(&:string?).each do |association| + joins.add_join_to association.stack + end + end + + def column_exists?(column) + Joiner::Path.new(model, column.__stack).model + true + rescue Joiner::AssociationNotFound + false + end + + def joins + @joins ||= begin + joins = Joiner::Joins.new model + if joins.respond_to?(:join_association_class) + joins.join_association_class = ThinkingSphinx::ActiveRecord::JoinAssociation + end + joins + end + end + + def source_query_properties + source.properties.select { |field| field.source_type == :query } + end + + # Use "first" here instead of a more intuitive flatten because flatten + # will also ask each column to become an Array and that will start + # to retrieve data. + def column_included_in_queries?(column) + source_query_properties.collect(&:columns).collect(&:first).include?(column) + end +end diff --git a/lib/thinking_sphinx/active_record/sql_builder.rb b/lib/thinking_sphinx/active_record/sql_builder.rb index f611bb477..875baaf8a 100644 --- a/lib/thinking_sphinx/active_record/sql_builder.rb +++ b/lib/thinking_sphinx/active_record/sql_builder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module ActiveRecord class SQLBuilder @@ -16,20 +18,10 @@ def sql_query_range statement.to_query_range_relation.to_sql end - def sql_query_info - statement.to_query_info_relation.to_sql - end - def sql_query_pre query.to_query end - def sql_query_post_index - return [] unless delta_processor && !source.delta? - - [delta_processor.reset_query] - end - private delegate :adapter, :model, :delta_processor, :to => :source @@ -53,18 +45,9 @@ def relation end def associations - @associations ||= begin - joins = Joiner::Joins.new model - if joins.respond_to?(:join_association_class) - joins.join_association_class = ThinkingSphinx::ActiveRecord::JoinAssociation - end - - source.associations.reject(&:string?).each do |association| - joins.add_join_to association.stack - end - - joins - end + @associations ||= ThinkingSphinx::ActiveRecord::SourceJoins.call( + model, source + ) end def quote_column(column) @@ -96,10 +79,6 @@ def document_id "#{column} AS #{quoted_alias}" end - def reversed_document_id - "($id - #{source.offset}) / #{config.indices.count}" - end - def range_condition condition = [] condition << "#{quoted_primary_key} BETWEEN $start AND $end" unless source.disable_range? diff --git a/lib/thinking_sphinx/active_record/sql_builder/clause_builder.rb b/lib/thinking_sphinx/active_record/sql_builder/clause_builder.rb index 397c73bba..a7ad895b2 100644 --- a/lib/thinking_sphinx/active_record/sql_builder/clause_builder.rb +++ b/lib/thinking_sphinx/active_record/sql_builder/clause_builder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module ActiveRecord class SQLBuilder::ClauseBuilder diff --git a/lib/thinking_sphinx/active_record/sql_builder/query.rb b/lib/thinking_sphinx/active_record/sql_builder/query.rb index 579352692..5de6bd7fd 100644 --- a/lib/thinking_sphinx/active_record/sql_builder/query.rb +++ b/lib/thinking_sphinx/active_record/sql_builder/query.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module ActiveRecord class SQLBuilder::Query @@ -18,10 +20,17 @@ def to_query def filter_by_query_pre scope_by_time_zone + scope_by_delta_processor scope_by_session scope_by_utf8 end + def scope_by_delta_processor + return unless delta_processor && !source.delta? + + self.scope << delta_processor.reset_query + end + def scope_by_session return unless max_len = source.options[:group_concat_max_len] @@ -38,6 +47,10 @@ def scope_by_utf8 self.scope += utf8_query_pre if source.options[:utf8?] end + def source + report.source + end + def method_missing(*args, &block) report.send *args, &block end diff --git a/lib/thinking_sphinx/active_record/sql_builder/statement.rb b/lib/thinking_sphinx/active_record/sql_builder/statement.rb index 0c468cfbd..00caceb3b 100644 --- a/lib/thinking_sphinx/active_record/sql_builder/statement.rb +++ b/lib/thinking_sphinx/active_record/sql_builder/statement.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'thinking_sphinx/active_record/sql_builder/clause_builder' module ThinkingSphinx @@ -20,12 +22,6 @@ def to_query_range_relation scope end - def to_query_info_relation - filter_by_query_info - - scope - end - def to_query_pre filter_by_query_pre @@ -49,10 +45,6 @@ def filter_by_query_range ) end - def filter_by_query_info - @scope = scope.where("#{quoted_primary_key} = #{reversed_document_id}") - end - def filter_by_scopes scope_by_select scope_by_where_clause @@ -140,10 +132,16 @@ def group_clause builder.compose( presenters_to_group(field_presenters), presenters_to_group(attribute_presenters) - ) unless source.options[:minimal_group_by?] + ) unless minimal_group_by? builder.compose(groupings).separated end + + def minimal_group_by? + source.options[:minimal_group_by?] || + config.settings['minimal_group_by?'] || + config.settings['minimal_group_by'] + end end end end diff --git a/lib/thinking_sphinx/active_record/sql_source.rb b/lib/thinking_sphinx/active_record/sql_source.rb index 6ac56227a..b732439d7 100644 --- a/lib/thinking_sphinx/active_record/sql_source.rb +++ b/lib/thinking_sphinx/active_record/sql_source.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + module ThinkingSphinx module ActiveRecord class SQLSource < Riddle::Configuration::SQLSource include ThinkingSphinx::Core::Settings - attr_reader :model, :database_settings, :options + + attr_reader :model, :options attr_accessor :fields, :attributes, :associations, :conditions, :groupings, :polymorphs @@ -12,9 +15,8 @@ class SQLSource < Riddle::Configuration::SQLSource def initialize(model, options = {}) @model = model - @database_settings = model.connection.instance_variable_get(:@config).clone @options = { - :utf8? => (@database_settings[:encoding] == 'utf8') + :utf8? => (database_settings[:encoding].to_s[/^utf8/]) }.merge options @fields = [] @@ -37,6 +39,18 @@ def adapter @adapter ||= DatabaseAdapters.adapter_for(@model) end + def add_attribute(attribute) + attributes.delete_if { |existing| existing.name == attribute.name } + + attributes << attribute + end + + def add_field(field) + fields.delete_if { |existing| existing.name == field.name } + + fields << field + end + def delta_processor options[:delta_processor].try(:new, adapter, @options[:delta_options] || {}) end @@ -61,6 +75,10 @@ def primary_key options[:primary_key] end + def properties + fields + attributes + end + def render prepare_for_render unless @prepared @@ -74,6 +92,9 @@ def set_database_settings(settings) @sql_db ||= settings[:database] @sql_port ||= settings[:port] @sql_sock ||= settings[:socket] + @mysql_ssl_cert ||= settings[:sslcert] + @mysql_ssl_key ||= settings[:sslkey] + @mysql_ssl_ca ||= settings[:sslca] end def type @@ -83,7 +104,7 @@ def type when DatabaseAdapters::PostgreSQLAdapter 'pgsql' else - raise "Unknown Adapter Type: #{adapter.class.name}" + raise UnknownDatabaseAdapter, "Provided type: #{adapter.class.name}" end end @@ -118,15 +139,23 @@ def build_sql_fields def build_sql_query @sql_query = builder.sql_query @sql_query_range ||= builder.sql_query_range - @sql_query_info ||= builder.sql_query_info @sql_query_pre += builder.sql_query_pre - @sql_query_post_index += builder.sql_query_post_index end def config ThinkingSphinx::Configuration.instance end + def database_settings + @database_settings ||= begin + if model.connection.respond_to?(:config) + model.connection.config.clone + else + model.connection.instance_variable_get(:@config).clone + end + end + end + def prepare_for_render polymorphs.each &:morph! append_presenter_to_attribute_array @@ -137,10 +166,6 @@ def prepare_for_render @prepared = true end - - def properties - fields + attributes - end end end end diff --git a/lib/thinking_sphinx/active_record/sql_source/template.rb b/lib/thinking_sphinx/active_record/sql_source/template.rb index 13f4adb86..722d731bb 100644 --- a/lib/thinking_sphinx/active_record/sql_source/template.rb +++ b/lib/thinking_sphinx/active_record/sql_source/template.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::ActiveRecord::SQLSource::Template attr_reader :source @@ -16,14 +18,14 @@ def apply private def add_attribute(column, name, type, options = {}) - source.attributes << ThinkingSphinx::ActiveRecord::Attribute.new( + source.add_attribute ThinkingSphinx::ActiveRecord::Attribute.new( source.model, ThinkingSphinx::ActiveRecord::Column.new(column), options.merge(:as => name, :type => type) ) end def add_field(column, name, options = {}) - source.fields << ThinkingSphinx::ActiveRecord::Field.new( + source.add_field ThinkingSphinx::ActiveRecord::Field.new( source.model, ThinkingSphinx::ActiveRecord::Column.new(column), options.merge(:as => name) ) @@ -48,6 +50,6 @@ def model end def primary_key - source.model.primary_key.to_sym + source.options[:primary_key].to_sym end end diff --git a/lib/thinking_sphinx/attribute_types.rb b/lib/thinking_sphinx/attribute_types.rb new file mode 100644 index 000000000..9bff5952f --- /dev/null +++ b/lib/thinking_sphinx/attribute_types.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +class ThinkingSphinx::AttributeTypes + def self.call + @call ||= new.call + end + + def self.reset + @call = nil + end + + def call + return {} unless File.exist?(configuration_file) + + realtime_indices.each { |index| + map_types_with_prefix index, :rt, + [:uint, :bigint, :float, :timestamp, :string, :bool, :json] + + index.rt_attr_multi.each { |name| attributes[name] << :uint } + index.rt_attr_multi_64.each { |name| attributes[name] << :bigint } + } + + plain_sources.each { |source| + map_types_with_prefix source, :sql, + [:uint, :bigint, :float, :timestamp, :string, :bool, :json] + + source.sql_attr_str2ordinal { |name| attributes[name] << :uint } + source.sql_attr_str2wordcount { |name| attributes[name] << :uint } + source.sql_attr_multi.each { |setting| + type, name, *ignored = setting.split(/\s+/) + attributes[name] << type.to_sym + } + } + + attributes.values.each &:uniq! + attributes + end + + private + + def attributes + @attributes ||= Hash.new { |hash, key| hash[key] = [] } + end + + def configuration + @configuration ||= Riddle::Configuration.parse!( + File.read(configuration_file) + ) + end + + def configuration_file + ThinkingSphinx::Configuration.instance.configuration_file + end + + def map_types_with_prefix(object, prefix, types) + types.each do |type| + object.public_send("#{prefix}_attr_#{type}").each do |name| + attributes[name] << type + end + end + end + + def plain_sources + configuration.indices.select { |index| + index.type == 'plain' || index.type.nil? + }.collect(&:sources).flatten + end + + def realtime_indices + configuration.indices.select { |index| index.type == 'rt' } + end +end diff --git a/lib/thinking_sphinx/batched_search.rb b/lib/thinking_sphinx/batched_search.rb index f771b680d..628752de3 100644 --- a/lib/thinking_sphinx/batched_search.rb +++ b/lib/thinking_sphinx/batched_search.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::BatchedSearch attr_accessor :searches diff --git a/lib/thinking_sphinx/callbacks.rb b/lib/thinking_sphinx/callbacks.rb index 0cf2d648a..b8d2d789e 100644 --- a/lib/thinking_sphinx/callbacks.rb +++ b/lib/thinking_sphinx/callbacks.rb @@ -1,6 +1,15 @@ +# frozen_string_literal: true + class ThinkingSphinx::Callbacks attr_reader :instance + def self.append(model, reference = nil, options, &block) + reference ||= ThinkingSphinx::Configuration.instance.index_set_class. + reference_name(model) + + ThinkingSphinx::Callbacks::Appender.call(model, reference, options, &block) + end + def self.callbacks(*methods) mod = Module.new methods.each do |method| @@ -9,7 +18,27 @@ def self.callbacks(*methods) extend mod end + def self.resume! + @suspended = false + end + + def self.suspend(&block) + suspend! + yield + resume! + end + + def self.suspend! + @suspended = true + end + + def self.suspended? + @suspended + end + def initialize(instance) @instance = instance end end + +require "thinking_sphinx/callbacks/appender" diff --git a/lib/thinking_sphinx/callbacks/appender.rb b/lib/thinking_sphinx/callbacks/appender.rb new file mode 100644 index 000000000..ba5894698 --- /dev/null +++ b/lib/thinking_sphinx/callbacks/appender.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Callbacks::Appender + def self.call(model, reference, options, &block) + new(model, reference, options, &block).call + end + + def initialize(model, reference, options, &block) + @model = model + @reference = reference + @options = options + @block = block + end + + def call + add_core_callbacks + add_delta_callbacks if behaviours.include?(:deltas) + add_real_time_callbacks if behaviours.include?(:real_time) + add_update_callbacks if behaviours.include?(:updates) + end + + private + + attr_reader :model, :reference, :options, :block + + def add_core_callbacks + model.after_commit( + ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks, + on: :destroy + ) + end + + def add_delta_callbacks + if path.empty? + model.before_save ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks + model.after_commit ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks + else + model.after_commit( + ThinkingSphinx::ActiveRecord::Callbacks::AssociationDeltaCallbacks + .new(path) + ) + end + end + + def add_real_time_callbacks + model.after_commit( + ThinkingSphinx::RealTime.callback_for(reference, path, &block), + on: [:create, :update] + ) + end + + def add_update_callbacks + model.after_update ThinkingSphinx::ActiveRecord::Callbacks::UpdateCallbacks + end + + def behaviours + options[:behaviours] || [] + end + + def path + options[:path] || [] + end +end diff --git a/lib/thinking_sphinx/capistrano.rb b/lib/thinking_sphinx/capistrano.rb index c976e45cd..0ad84aa21 100644 --- a/lib/thinking_sphinx/capistrano.rb +++ b/lib/thinking_sphinx/capistrano.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + if defined?(Capistrano::VERSION) if Gem::Version.new(Capistrano::VERSION).release >= Gem::Version.new('3.0.0') recipe_version = 3 diff --git a/lib/thinking_sphinx/capistrano/v2.rb b/lib/thinking_sphinx/capistrano/v2.rb index d319ed868..0098e69cc 100644 --- a/lib/thinking_sphinx/capistrano/v2.rb +++ b/lib/thinking_sphinx/capistrano/v2.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Capistrano::Configuration.instance(:must_exist).load do _cset(:thinking_sphinx_roles) { :db } _cset(:thinking_sphinx_options) { {:roles => fetch(:thinking_sphinx_roles)} } diff --git a/lib/thinking_sphinx/capistrano/v3.rb b/lib/thinking_sphinx/capistrano/v3.rb index 1f8919bbd..ddb28771e 100644 --- a/lib/thinking_sphinx/capistrano/v3.rb +++ b/lib/thinking_sphinx/capistrano/v3.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + namespace :load do task :defaults do set :thinking_sphinx_roles, :db diff --git a/lib/thinking_sphinx/commander.rb b/lib/thinking_sphinx/commander.rb new file mode 100644 index 000000000..57a14c734 --- /dev/null +++ b/lib/thinking_sphinx/commander.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commander + def self.call(command, configuration, options, stream = STDOUT) + raise ThinkingSphinx::UnknownCommand unless registry.keys.include?(command) + + registry[command].call configuration, options, stream + end + + def self.registry + @registry ||= { + :clear_real_time => ThinkingSphinx::Commands::ClearRealTime, + :clear_sql => ThinkingSphinx::Commands::ClearSQL, + :configure => ThinkingSphinx::Commands::Configure, + :index_sql => ThinkingSphinx::Commands::IndexSQL, + :index_real_time => ThinkingSphinx::Commands::IndexRealTime, + :merge => ThinkingSphinx::Commands::Merge, + :merge_and_update => ThinkingSphinx::Commands::MergeAndUpdate, + :prepare => ThinkingSphinx::Commands::Prepare, + :rotate => ThinkingSphinx::Commands::Rotate, + :running => ThinkingSphinx::Commands::Running, + :start_attached => ThinkingSphinx::Commands::StartAttached, + :start_detached => ThinkingSphinx::Commands::StartDetached, + :stop => ThinkingSphinx::Commands::Stop + } + end +end diff --git a/lib/thinking_sphinx/commands.rb b/lib/thinking_sphinx/commands.rb new file mode 100644 index 000000000..47b6cd50a --- /dev/null +++ b/lib/thinking_sphinx/commands.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module ThinkingSphinx::Commands + # +end + +require 'thinking_sphinx/commands/base' +require 'thinking_sphinx/commands/clear_real_time' +require 'thinking_sphinx/commands/clear_sql' +require 'thinking_sphinx/commands/configure' +require 'thinking_sphinx/commands/index_sql' +require 'thinking_sphinx/commands/index_real_time' +require 'thinking_sphinx/commands/merge' +require 'thinking_sphinx/commands/merge_and_update' +require 'thinking_sphinx/commands/prepare' +require 'thinking_sphinx/commands/rotate' +require 'thinking_sphinx/commands/running' +require 'thinking_sphinx/commands/start_attached' +require 'thinking_sphinx/commands/start_detached' +require 'thinking_sphinx/commands/stop' diff --git a/lib/thinking_sphinx/commands/base.rb b/lib/thinking_sphinx/commands/base.rb new file mode 100644 index 000000000..4a34cf975 --- /dev/null +++ b/lib/thinking_sphinx/commands/base.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::Base + include ThinkingSphinx::WithOutput + + def self.call(configuration, options, stream = STDOUT) + new(configuration, options, stream).call_with_handling + end + + def call_with_handling + call + rescue Riddle::CommandFailedError => error + handle_failure error.command_result + end + + private + + delegate :controller, :to => :configuration + + def command(command, extra_options = {}) + ThinkingSphinx::Commander.call( + command, configuration, options.merge(extra_options), stream + ) + end + + def command_output(output) + return "See above\n" if output.nil? + + "\n\t" + output.gsub("\n", "\n\t") + end + + def handle_failure(result) + stream.puts <<-TXT + +The Sphinx #{type} command failed: + Command: #{result.command} + Status: #{result.status} + Output: #{command_output result.output} +There may be more information about the failure in #{configuration.searchd.log}. + TXT + exit(result.status || 1) + end + + def log(message) + return if options[:silent] + + stream.puts message + end + + def skip_directories? + configuration.settings['skip_directory_creation'] + end +end diff --git a/lib/thinking_sphinx/commands/clear_real_time.rb b/lib/thinking_sphinx/commands/clear_real_time.rb new file mode 100644 index 000000000..9312e6caf --- /dev/null +++ b/lib/thinking_sphinx/commands/clear_real_time.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::ClearRealTime < ThinkingSphinx::Commands::Base + def call + options[:indices].each do |index| + index.render + Dir["#{index.path}.*"].each { |path| FileUtils.rm path } + end + + FileUtils.rm_rf(binlog_path) if File.exist?(binlog_path) + end + + private + + def binlog_path + configuration.searchd.binlog_path + end + + def type + 'clear_realtime' + end +end diff --git a/lib/thinking_sphinx/commands/clear_sql.rb b/lib/thinking_sphinx/commands/clear_sql.rb new file mode 100644 index 000000000..512c97d04 --- /dev/null +++ b/lib/thinking_sphinx/commands/clear_sql.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::ClearSQL < ThinkingSphinx::Commands::Base + def call + options[:indices].each do |index| + index.render + Dir["#{index.path}.*"].each { |path| FileUtils.rm path } + end + + FileUtils.rm_rf Dir["#{configuration.indices_location}/ts-*.tmp"] + end + + private + + def type + 'clear_sql' + end +end diff --git a/lib/thinking_sphinx/commands/configure.rb b/lib/thinking_sphinx/commands/configure.rb new file mode 100644 index 000000000..e0a298ffc --- /dev/null +++ b/lib/thinking_sphinx/commands/configure.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::Configure < ThinkingSphinx::Commands::Base + def call + log "Generating configuration to #{configuration.configuration_file}" + + configuration.render_to_file + end + + private + + def type + 'configure' + end +end diff --git a/lib/thinking_sphinx/commands/index_real_time.rb b/lib/thinking_sphinx/commands/index_real_time.rb new file mode 100644 index 000000000..cfc2a7680 --- /dev/null +++ b/lib/thinking_sphinx/commands/index_real_time.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::IndexRealTime < ThinkingSphinx::Commands::Base + def call + ThinkingSphinx::RealTime.processor.call options[:indices] do + command :rotate + end + end + + private + + def type + 'indexing' + end +end diff --git a/lib/thinking_sphinx/commands/index_sql.rb b/lib/thinking_sphinx/commands/index_sql.rb new file mode 100644 index 000000000..681660a81 --- /dev/null +++ b/lib/thinking_sphinx/commands/index_sql.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::IndexSQL < ThinkingSphinx::Commands::Base + def call + if indices.empty? + ThinkingSphinx.before_index_hooks.each { |hook| hook.call } + end + + configuration.indexing_strategy.call(indices) do |index_names| + configuration.guarding_strategy.call(index_names) do |names| + controller.index *names, :verbose => options[:verbose] + end + end + end + + private + + def indices + options[:indices] || [] + end + + def type + 'indexing' + end +end diff --git a/lib/thinking_sphinx/commands/merge.rb b/lib/thinking_sphinx/commands/merge.rb new file mode 100644 index 000000000..c78bd6e0f --- /dev/null +++ b/lib/thinking_sphinx/commands/merge.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::Merge < ThinkingSphinx::Commands::Base + def call + return unless indices_exist? + + controller.merge( + options[:core_index].name, + options[:delta_index].name, + :filters => options[:filters], + :verbose => options[:verbose] + ) + end + + private + + delegate :controller, :to => :configuration + + def indices_exist? + File.exist?("#{options[:core_index].path}.spi") && + File.exist?("#{options[:delta_index].path}.spi") + end + + def type + 'merging' + end +end diff --git a/lib/thinking_sphinx/commands/merge_and_update.rb b/lib/thinking_sphinx/commands/merge_and_update.rb new file mode 100644 index 000000000..b3876b94b --- /dev/null +++ b/lib/thinking_sphinx/commands/merge_and_update.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::MergeAndUpdate < ThinkingSphinx::Commands::Base + def call + configuration.preload_indices + configuration.render + + index_pairs.each do |(core_index, delta_index)| + command :merge, + :core_index => core_index, + :delta_index => delta_index, + :filters => {:sphinx_deleted => 0} + + core_index.model.where(:delta => true).update_all(:delta => false) + end + end + + private + + delegate :controller, :to => :configuration + + def core_indices + indices.select { |index| !index.delta? }.select do |index| + name_filters.empty? || + name_filters.include?(index.name.gsub(/_core$/, '')) + end + end + + def delta_for(core_index) + name = core_index.name.gsub(/_core$/, "_delta") + indices.detect { |index| index.name == name } + end + + def index_pairs + core_indices.collect { |core_index| + [core_index, delta_for(core_index)] + } + end + + def indices + @indices ||= configuration.indices.select { |index| + index.type == "plain" && index.options[:delta_processor] + } + end + + def indices_exist?(*indices) + indices.all? { |index| File.exist?("#{index.path}.spi") } + end + + def name_filters + @name_filters ||= options[:index_names] || [] + end + + def type + 'merging_and_updating' + end +end diff --git a/lib/thinking_sphinx/commands/prepare.rb b/lib/thinking_sphinx/commands/prepare.rb new file mode 100644 index 000000000..8dd7f69e3 --- /dev/null +++ b/lib/thinking_sphinx/commands/prepare.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::Prepare < ThinkingSphinx::Commands::Base + def call + return if skip_directories? + + FileUtils.mkdir_p configuration.indices_location + end + + private + + def type + 'prepare' + end +end diff --git a/lib/thinking_sphinx/commands/rotate.rb b/lib/thinking_sphinx/commands/rotate.rb new file mode 100644 index 000000000..461393044 --- /dev/null +++ b/lib/thinking_sphinx/commands/rotate.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::Rotate < ThinkingSphinx::Commands::Base + def call + controller.rotate + end + + private + + def type + 'rotate' + end +end diff --git a/lib/thinking_sphinx/commands/running.rb b/lib/thinking_sphinx/commands/running.rb new file mode 100644 index 000000000..f3b71b29f --- /dev/null +++ b/lib/thinking_sphinx/commands/running.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::Running < ThinkingSphinx::Commands::Base + def call + return true if configuration.settings['skip_running_check'] + + controller.running? + end + + private + + def type + 'running' + end +end diff --git a/lib/thinking_sphinx/commands/start_attached.rb b/lib/thinking_sphinx/commands/start_attached.rb new file mode 100644 index 000000000..21df8ac1e --- /dev/null +++ b/lib/thinking_sphinx/commands/start_attached.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::StartAttached < ThinkingSphinx::Commands::Base + def call + FileUtils.mkdir_p configuration.indices_location unless skip_directories? + + unless pid = fork + controller.start :verbose => options[:verbose], :nodetach => true + end + + Signal.trap('TERM') { Process.kill(:TERM, pid) } + Signal.trap('INT') { Process.kill(:TERM, pid) } + + Process.wait(pid) + end + + private + + def type + 'start' + end +end diff --git a/lib/thinking_sphinx/commands/start_detached.rb b/lib/thinking_sphinx/commands/start_detached.rb new file mode 100644 index 000000000..451d89adb --- /dev/null +++ b/lib/thinking_sphinx/commands/start_detached.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::StartDetached < ThinkingSphinx::Commands::Base + def call + FileUtils.mkdir_p configuration.indices_location unless skip_directories? + + result = controller.start :verbose => options[:verbose] + + if command :running + log "Started searchd successfully (pid: #{controller.pid})." + else + handle_failure result + end + end + + private + + def type + 'start' + end +end diff --git a/lib/thinking_sphinx/commands/stop.rb b/lib/thinking_sphinx/commands/stop.rb new file mode 100644 index 000000000..368120a1d --- /dev/null +++ b/lib/thinking_sphinx/commands/stop.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Commands::Stop < ThinkingSphinx::Commands::Base + def call + unless command :running + log 'searchd is not currently running.' + return + end + + pid = controller.pid + until !command :running do + controller.stop options + sleep(0.5) + end + + log "Stopped searchd daemon (pid: #{pid})." + end + + private + + def type + 'stop' + end +end diff --git a/lib/thinking_sphinx/configuration.rb b/lib/thinking_sphinx/configuration.rb index 1dfdb16c1..38704353f 100644 --- a/lib/thinking_sphinx/configuration.rb +++ b/lib/thinking_sphinx/configuration.rb @@ -1,16 +1,22 @@ +# frozen_string_literal: true + require 'pathname' class ThinkingSphinx::Configuration < Riddle::Configuration - attr_accessor :configuration_file, :indices_location, :version + attr_accessor :configuration_file, :indices_location, :version, :batch_size attr_reader :index_paths - attr_writer :controller, :index_set_class + attr_writer :controller, :index_set_class, :indexing_strategy, + :guarding_strategy delegate :environment, :to => :framework + @@mutex = defined?(ActiveSupport::Concurrency::LoadInterlockAwareMonitor) ? + ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new : Mutex.new + def initialize super - setup + reset end def self.instance @@ -27,7 +33,7 @@ def bin_path def controller @controller ||= begin - rc = ThinkingSphinx::Controller.new self, configuration_file + rc = Riddle::Controller.new self, configuration_file rc.bin_path = bin_path.gsub(/([^\/])$/, '\1/') if bin_path.present? rc end @@ -39,14 +45,14 @@ def framework def framework=(framework) @framework = framework - setup + reset framework end def engine_index_paths return [] unless defined?(Rails) - engine_indice_paths.flatten.compact + engine_indice_paths.flatten.compact.sort end def engine_indice_paths @@ -56,10 +62,18 @@ def engine_indice_paths end end + def guarding_strategy + @guarding_strategy ||= ThinkingSphinx::Guard::Files + end + def index_set_class @index_set_class ||= ThinkingSphinx::IndexSet end + def indexing_strategy + @indexing_strategy ||= ThinkingSphinx::IndexingStrategies::AllAtOnce + end + def indices_for_references(*references) index_set_class.new(:references => references).to_a end @@ -69,105 +83,105 @@ def next_offset(reference) end def preload_indices - return if @preloaded_indices + @@mutex.synchronize do + return if @preloaded_indices - index_paths.each do |path| - Dir["#{path}/**/*.rb"].sort.each do |file| - ActiveSupport::Dependencies.require_or_load file + index_paths.each do |path| + Dir["#{path}/**/*.rb"].sort.each { |file| preload_index file } end - end - if settings['distributed_indices'].nil? || settings['distributed_indices'] - ThinkingSphinx::Configuration::DistributedIndices.new(indices).reconcile + normalise + verify + + @preloaded_indices = true end + end - @preloaded_indices = true + def preload_index(file) + if ActiveRecord::VERSION::MAJOR <= 5 + ActiveSupport::Dependencies.require_or_load file + else + load file + end end def render preload_indices - ThinkingSphinx::Configuration::ConsistentIds.new(indices).reconcile - ThinkingSphinx::Configuration::MinimumFields.new(indices).reconcile - super end def render_to_file - FileUtils.mkdir_p searchd.binlog_path unless searchd.binlog_path.blank? + unless settings['skip_directory_creation'] || searchd.binlog_path.blank? + FileUtils.mkdir_p searchd.binlog_path + end open(configuration_file, 'w') { |file| file.write render } end def settings - @settings ||= File.exists?(settings_file) ? settings_to_hash : {} + @settings ||= ThinkingSphinx::Settings.call self end - private - - def configure_searchd - configure_searchd_log_files + def setup + @configuration_file = settings['configuration_file'] + @index_paths = engine_index_paths + + [Pathname.new(framework.root).join('app', 'indices').to_s] + @indices_location = settings['indices_location'] + @version = settings['version'] || '2.2.11' + @batch_size = settings['batch_size'] || 1000 - searchd.binlog_path = tmp_path.join('binlog', environment).to_s - searchd.address = settings['address'].presence || Defaults::ADDRESS - searchd.mysql41 = settings['mysql41'] || settings['port'] || Defaults::PORT - searchd.workers = 'threads' - searchd.mysql_version_string = '5.5.21' if RUBY_PLATFORM == 'java' - end + if settings['common_sphinx_configuration'] + common.common_sphinx_configuration = true + indexer.common_sphinx_configuration = true + end - def configure_searchd_log_files - searchd.pid_file = log_root.join("#{environment}.sphinx.pid").to_s - searchd.log = log_root.join("#{environment}.searchd.log").to_s - searchd.query_log = log_root.join("#{environment}.searchd.query.log").to_s - end + configure_searchd - def log_root - real_path 'log' - end + apply_sphinx_settings! - def framework_root - Pathname.new(framework.root) + @offsets = {} end - def real_path(*arguments) - path = framework_root.join(*arguments) - path.exist? ? path.realpath : path - end + private - def settings_to_hash - contents = YAML.load(ERB.new(File.read(settings_file)).result) - contents && contents[environment] || {} - end + def apply_sphinx_settings! + sphinx_sections.each do |object| + settings.each do |key, value| + next unless object.class.settings.include?(key.to_sym) - def settings_file - framework_root.join 'config', 'thinking_sphinx.yml' + object.send("#{key}=", value) + end + end end - def setup - @settings = nil - @configuration_file = settings['configuration_file'] || framework_root.join( - 'config', "#{environment}.sphinx.conf" - ).to_s - @index_paths = engine_index_paths + [framework_root.join('app', 'indices').to_s] - @indices_location = settings['indices_location'] || framework_root.join( - 'db', 'sphinx', environment - ).to_s - @version = settings['version'] || '2.1.4' + def configure_searchd + searchd.socket = "#{settings["socket"]}:mysql41" if socket? - if settings['common_sphinx_configuration'] - common.common_sphinx_configuration = true - indexer.common_sphinx_configuration = true + if tcp? + searchd.address = settings['address'].presence || Defaults::ADDRESS + searchd.mysql41 = settings['mysql41'] || settings['port'] || Defaults::PORT end - configure_searchd + searchd.mysql_version_string = '5.5.21' if RUBY_PLATFORM == 'java' + end - apply_sphinx_settings! + def normalise + if settings['distributed_indices'].nil? || settings['distributed_indices'] + ThinkingSphinx::Configuration::DistributedIndices.new(indices).reconcile + end - @offsets = {} + ThinkingSphinx::Configuration::ConsistentIds.new(indices).reconcile + ThinkingSphinx::Configuration::MinimumFields.new(indices).reconcile + end + + def reset + @settings = nil + setup end - def tmp_path - real_path 'tmp' + def socket? + settings["socket"].present? end def sphinx_sections @@ -176,18 +190,20 @@ def sphinx_sections sections end - def apply_sphinx_settings! - sphinx_sections.each do |object| - settings.each do |key, value| - next unless object.class.settings.include?(key.to_sym) + def tcp? + settings["socket"].nil? || + settings["address"].present? || + settings["mysql41"].present? || + settings["port"].present? + end - object.send("#{key}=", value) - end - end + def verify + ThinkingSphinx::Configuration::DuplicateNames.new(indices).reconcile end end require 'thinking_sphinx/configuration/consistent_ids' require 'thinking_sphinx/configuration/defaults' require 'thinking_sphinx/configuration/distributed_indices' +require 'thinking_sphinx/configuration/duplicate_names' require 'thinking_sphinx/configuration/minimum_fields' diff --git a/lib/thinking_sphinx/configuration/consistent_ids.rb b/lib/thinking_sphinx/configuration/consistent_ids.rb index 61fd90447..afd7420a0 100644 --- a/lib/thinking_sphinx/configuration/consistent_ids.rb +++ b/lib/thinking_sphinx/configuration/consistent_ids.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Configuration::ConsistentIds def initialize(indices) @indices = indices diff --git a/lib/thinking_sphinx/configuration/defaults.rb b/lib/thinking_sphinx/configuration/defaults.rb index a495a6c30..b57dd9c30 100644 --- a/lib/thinking_sphinx/configuration/defaults.rb +++ b/lib/thinking_sphinx/configuration/defaults.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Configuration::Defaults ADDRESS = '127.0.0.1' PORT = 9306 diff --git a/lib/thinking_sphinx/configuration/distributed_indices.rb b/lib/thinking_sphinx/configuration/distributed_indices.rb index 13ea73f50..b4e4b08f2 100644 --- a/lib/thinking_sphinx/configuration/distributed_indices.rb +++ b/lib/thinking_sphinx/configuration/distributed_indices.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Configuration::DistributedIndices def initialize(indices) @indices = indices @@ -19,7 +21,7 @@ def append(index) def distributed_index(reference, indices) index = ThinkingSphinx::Distributed::Index.new reference - index.local_indices += indices.collect &:name + index.local_index_objects = indices index end diff --git a/lib/thinking_sphinx/configuration/duplicate_names.rb b/lib/thinking_sphinx/configuration/duplicate_names.rb new file mode 100644 index 000000000..8741d93b8 --- /dev/null +++ b/lib/thinking_sphinx/configuration/duplicate_names.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Configuration::DuplicateNames + def initialize(indices) + @indices = indices + end + + def reconcile + indices.each do |index| + return if index.distributed? + + counts_for(index).each do |name, count| + next if count <= 1 + + raise ThinkingSphinx::DuplicateNameError, + "Duplicate field/attribute name '#{name}' in index '#{index.name}'" + end + end + end + + private + + attr_reader :indices + + def counts_for(index) + names_for(index).inject({}) do |hash, name| + hash[name] ||= 0 + hash[name] += 1 + hash + end + end + + def names_for(index) + index.fields.collect(&:name) + index.attributes.collect(&:name) + end +end diff --git a/lib/thinking_sphinx/configuration/minimum_fields.rb b/lib/thinking_sphinx/configuration/minimum_fields.rb index 3563260bc..b96824caf 100644 --- a/lib/thinking_sphinx/configuration/minimum_fields.rb +++ b/lib/thinking_sphinx/configuration/minimum_fields.rb @@ -1,13 +1,13 @@ +# frozen_string_literal: true + class ThinkingSphinx::Configuration::MinimumFields def initialize(indices) @indices = indices end def reconcile - return unless no_inheritance_columns? - - sources.each do |source| - source.fields.delete_if do |field| + field_collections.each do |collection| + collection.fields.delete_if do |field| field.name == 'sphinx_internal_class_name' end end @@ -17,15 +17,20 @@ def reconcile attr_reader :indices - def no_inheritance_columns? - indices.select { |index| - index.model.column_names.include?(index.model.inheritance_column) - }.empty? + def field_collections + indices_without_inheritance_of_type('plain').collect(&:sources).flatten + + indices_without_inheritance_of_type('rt') + end + + def inheritance_columns?(index) + index.model.table_exists? && index.model.column_names.include?(index.model.inheritance_column) + end + + def indices_without_inheritance_of_type(type) + indices_without_inheritance.select { |index| index.type == type } end - def sources - @sources ||= @indices.select { |index| - index.respond_to?(:sources) - }.collect(&:sources).flatten + def indices_without_inheritance + indices.reject(&method(:inheritance_columns?)) end end diff --git a/lib/thinking_sphinx/connection.rb b/lib/thinking_sphinx/connection.rb index 76d8f0d50..b3e954174 100644 --- a/lib/thinking_sphinx/connection.rb +++ b/lib/thinking_sphinx/connection.rb @@ -1,22 +1,25 @@ +# frozen_string_literal: true + module ThinkingSphinx::Connection MAXIMUM_RETRIES = 3 def self.new configuration = ThinkingSphinx::Configuration.instance - # If you use localhost, MySQL insists on a socket connection, but Sphinx - # requires a TCP connection. Using 127.0.0.1 fixes that. - address = configuration.searchd.address || '127.0.0.1' - address = '127.0.0.1' if address == 'localhost' options = { - :host => address, + :host => configuration.searchd.address, :port => configuration.searchd.mysql41, + :socket => configuration.searchd.socket, :reconnect => true }.merge(configuration.settings['connection_options'] || {}) connection_class.new options end + def self.clear + @pool = nil + end + def self.connection_class return ThinkingSphinx::Connection::JRuby if RUBY_PLATFORM == 'java' @@ -26,7 +29,7 @@ def self.connection_class def self.pool @pool ||= Innertube::Pool.new( Proc.new { ThinkingSphinx::Connection.new }, - Proc.new { |connection| connection.close } + Proc.new { |connection| connection.close! } ) end @@ -63,112 +66,8 @@ def self.persistent=(persist) end @persistent = true - - class Client - def close - client.close unless ThinkingSphinx::Connection.persistent? - end - - def execute(statement) - query(statement).first - end - - def query_all(*statements) - query *statements - end - - private - - def close_and_clear - client.close - @client = nil - end - - def query(*statements) - results_for *statements - rescue => error - message = "#{error.message} - #{statements.join('; ')}" - wrapper = ThinkingSphinx::QueryExecutionError.new message - wrapper.statement = statements.join('; ') - raise wrapper - ensure - close_and_clear unless ThinkingSphinx::Connection.persistent? - end - end - - class MRI < Client - def initialize(options) - @options = options - end - - def base_error - Mysql2::Error - end - - private - - attr_reader :options - - def client - @client ||= Mysql2::Client.new({ - :flags => Mysql2::Client::MULTI_STATEMENTS - }.merge(options)) - rescue base_error => error - raise ThinkingSphinx::SphinxError.new_from_mysql error - end - - def results_for(*statements) - results = [client.query(statements.join('; '))] - results << client.store_result while client.next_result - results - end - end - - class JRuby < Client - attr_reader :address, :options - - def initialize(options) - @address = "jdbc:mysql://#{options[:host]}:#{options[:port]}/?allowMultiQueries=true" - @options = options - end - - def base_error - Java::JavaSql::SQLException - end - - private - - def client - @client ||= java.sql.DriverManager.getConnection address, - options[:username], options[:password] - rescue base_error => error - raise ThinkingSphinx::SphinxError.new_from_mysql error - end - - def results_for(*statements) - statement = client.createStatement - statement.execute statements.join('; ') - - results = [set_to_array(statement.getResultSet)] - results << set_to_array(statement.getResultSet) while statement.getMoreResults - results.compact - end - - def set_to_array(set) - return nil if set.nil? - - meta = set.meta_data - rows = [] - - while set.next - rows << (1..meta.column_count).inject({}) do |row, index| - name = meta.column_name index - row[name] = set.get_object(index) - row - end - end - - rows - end - end end + +require 'thinking_sphinx/connection/client' +require 'thinking_sphinx/connection/jruby' +require 'thinking_sphinx/connection/mri' diff --git a/lib/thinking_sphinx/connection/client.rb b/lib/thinking_sphinx/connection/client.rb new file mode 100644 index 000000000..03313cf96 --- /dev/null +++ b/lib/thinking_sphinx/connection/client.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Connection::Client + def initialize(options) + if options[:socket].present? + options[:socket] = options[:socket].remove /:mysql41$/ + + options.delete :host + options.delete :port + else + options.delete :socket + + # If you use localhost, MySQL insists on a socket connection, but in this + # situation we want a TCP connection. Using 127.0.0.1 fixes that. + if options[:host].nil? || options[:host] == "localhost" + options[:host] = "127.0.0.1" + end + end + + @options = options + end + + def close + close! unless ThinkingSphinx::Connection.persistent? + end + + def close! + client.close + end + + def execute(statement) + check_and_perform(statement).first + end + + def query_all(*statements) + check_and_perform statements.join('; ') + end + + private + + def check(statements) + if statements.length > maximum_statement_length + exception = ThinkingSphinx::QueryLengthError.new + exception.statement = statements + raise exception + end + end + + def check_and_perform(statements) + check statements + perform statements + end + + def close_and_clear + client.close + @client = nil + end + + def maximum_statement_length + @maximum_statement_length ||= ThinkingSphinx::Configuration.instance. + settings['maximum_statement_length'] + end + + def perform(statements) + results_for statements + rescue => error + message = "#{error.message} - #{statements}" + wrapper = ThinkingSphinx::QueryExecutionError.new message + wrapper.statement = statements + raise wrapper + ensure + close_and_clear unless ThinkingSphinx::Connection.persistent? + end +end diff --git a/lib/thinking_sphinx/connection/jruby.rb b/lib/thinking_sphinx/connection/jruby.rb new file mode 100644 index 000000000..6e0295cc5 --- /dev/null +++ b/lib/thinking_sphinx/connection/jruby.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Connection::JRuby < ThinkingSphinx::Connection::Client + attr_reader :address, :options + + def initialize(options) + options.delete :socket + + super + + @address = "jdbc:mysql://#{@options[:host]}:#{@options[:port]}/?allowMultiQueries=true" + end + + def base_error + Java::JavaSql::SQLException + end + + private + + def client + @client ||= Java::ComMysqlJdbc::Driver.new.connect address, properties + rescue base_error => error + raise ThinkingSphinx::SphinxError.new_from_mysql error + end + + def properties + object = Java::JavaUtil::Properties.new + object.setProperty "user", options[:username] if options[:username] + object.setProperty "password", options[:password] if options[:password] + object + end + + def results_for(statements) + statement = client.createStatement + statement.execute statements + + results = [set_to_array(statement.getResultSet)] + results << set_to_array(statement.getResultSet) while statement.getMoreResults + results.compact + end + + def set_to_array(set) + return nil if set.nil? + + meta = set.getMetaData + rows = [] + + while set.next + rows << (1..meta.getColumnCount).inject({}) do |row, index| + name = meta.getColumnName index + row[name] = set.getObject(index) + row + end + end + + rows + end +end diff --git a/lib/thinking_sphinx/connection/mri.rb b/lib/thinking_sphinx/connection/mri.rb new file mode 100644 index 000000000..323327f3c --- /dev/null +++ b/lib/thinking_sphinx/connection/mri.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Connection::MRI < ThinkingSphinx::Connection::Client + def base_error + Mysql2::Error + end + + private + + attr_reader :options + + def client + @client ||= Mysql2::Client.new({ + :flags => Mysql2::Client::MULTI_STATEMENTS, + :connect_timeout => 5 + }.merge(options)) + rescue base_error => error + raise ThinkingSphinx::SphinxError.new_from_mysql error + end + + def results_for(statements) + results = [client.query(statements)] + results << client.store_result while client.next_result + results + end +end diff --git a/lib/thinking_sphinx/controller.rb b/lib/thinking_sphinx/controller.rb deleted file mode 100644 index add386c47..000000000 --- a/lib/thinking_sphinx/controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -class ThinkingSphinx::Controller < Riddle::Controller - def index(*indices) - options = indices.extract_options! - indices << '--all' if indices.empty? - - ThinkingSphinx::Guard::Files.call(indices) do |names| - super(*(names + [options])) - end - end -end diff --git a/lib/thinking_sphinx/core.rb b/lib/thinking_sphinx/core.rb index 63ed190b2..24325a5da 100644 --- a/lib/thinking_sphinx/core.rb +++ b/lib/thinking_sphinx/core.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Core # end diff --git a/lib/thinking_sphinx/core/field.rb b/lib/thinking_sphinx/core/field.rb index dfd426704..b7d7c5cd2 100644 --- a/lib/thinking_sphinx/core/field.rb +++ b/lib/thinking_sphinx/core/field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Core::Field def infixing? options[:infixes] diff --git a/lib/thinking_sphinx/core/index.rb b/lib/thinking_sphinx/core/index.rb index 0a9d1ad22..469062cba 100644 --- a/lib/thinking_sphinx/core/index.rb +++ b/lib/thinking_sphinx/core/index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Core::Index extend ActiveSupport::Concern include ThinkingSphinx::Core::Settings @@ -9,8 +11,6 @@ module ThinkingSphinx::Core::Index def initialize(reference, options = {}) @reference = reference.to_sym - @docinfo = :extern - @charset_type = 'utf-8' @options = options @offset = config.next_offset(options[:offset_as] || reference) @type = 'plain' @@ -26,11 +26,22 @@ def distributed? false end + def document_id_for_instance(instance) + document_id_for_key instance.public_send(primary_key) + end + def document_id_for_key(key) + return nil if key.nil? + key * config.indices.count + offset end def interpret_definition! + table_exists = model.table_exists? + unless table_exists + Rails.logger.info "No table exists for #{model}. Index can not be created" + return + end return if @interpreted_definition apply_defaults! @@ -48,6 +59,11 @@ def options @options end + def primary_key + @primary_key ||= @options[:primary_key] || + config.settings['primary_key'] || model.primary_key || :id + end + def render pre_render set_path @@ -85,7 +101,10 @@ def pre_render end def set_path - FileUtils.mkdir_p path_prefix + unless config.settings['skip_directory_creation'] + FileUtils.mkdir_p path_prefix + end + @path = File.join path_prefix, name end end diff --git a/lib/thinking_sphinx/core/interpreter.rb b/lib/thinking_sphinx/core/interpreter.rb index 33f637188..3982f95d7 100644 --- a/lib/thinking_sphinx/core/interpreter.rb +++ b/lib/thinking_sphinx/core/interpreter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Core::Interpreter < BasicObject def self.translate!(index, block) new(index, block).translate! diff --git a/lib/thinking_sphinx/core/property.rb b/lib/thinking_sphinx/core/property.rb index 0ee722d4a..4012364d6 100644 --- a/lib/thinking_sphinx/core/property.rb +++ b/lib/thinking_sphinx/core/property.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Core::Property def facet? options[:facet] diff --git a/lib/thinking_sphinx/core/settings.rb b/lib/thinking_sphinx/core/settings.rb index 86e9d06c5..df759530b 100644 --- a/lib/thinking_sphinx/core/settings.rb +++ b/lib/thinking_sphinx/core/settings.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Core::Settings private def apply_defaults!(defaults = self.class.settings) diff --git a/lib/thinking_sphinx/deletion.rb b/lib/thinking_sphinx/deletion.rb index 61fc97cfd..02ae111bc 100644 --- a/lib/thinking_sphinx/deletion.rb +++ b/lib/thinking_sphinx/deletion.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Deletion delegate :name, :to => :index @@ -20,31 +22,49 @@ def initialize(index, ids) attr_reader :index, :ids - def document_ids_for_keys - ids.collect { |id| index.document_id_for_key id } - end - def execute(statement) - ThinkingSphinx::Connection.take do |connection| - connection.execute statement - end - end + statement = statement.gsub(/\s*\n\s*/, ' ').strip - class RealtimeDeletion < ThinkingSphinx::Deletion - def perform - execute Riddle::Query::Delete.new(name, document_ids_for_keys).to_sql + ThinkingSphinx::Logger.log :query, statement do + ThinkingSphinx::Connection.take do |connection| + connection.execute statement + end end end class PlainDeletion < ThinkingSphinx::Deletion def perform - document_ids_for_keys.each_slice(1000) do |document_ids| + ids.each_slice(1000) do |some_ids| execute <<-SQL UPDATE #{name} SET sphinx_deleted = 1 -WHERE id IN (#{document_ids.join(', ')}) +WHERE sphinx_internal_id IN (#{some_ids.join(', ')}) + SQL + end + end + end + + class RealtimeDeletion < ThinkingSphinx::Deletion + def perform + return unless callbacks_enabled? + + ids.each_slice(1000) do |some_ids| + execute <<-SQL +DELETE FROM #{name} +WHERE sphinx_internal_id IN (#{some_ids.join(', ')}) SQL end end + + private + + def callbacks_enabled? + setting = configuration.settings['real_time_callbacks'] + setting.nil? || setting + end + + def configuration + ThinkingSphinx::Configuration.instance + end end end diff --git a/lib/thinking_sphinx/deltas.rb b/lib/thinking_sphinx/deltas.rb index 3c5bf682a..198c604a3 100644 --- a/lib/thinking_sphinx/deltas.rb +++ b/lib/thinking_sphinx/deltas.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Deltas def self.config ThinkingSphinx::Configuration.instance diff --git a/lib/thinking_sphinx/deltas/default_delta.rb b/lib/thinking_sphinx/deltas/default_delta.rb index 08884e8bf..c2e06bba5 100644 --- a/lib/thinking_sphinx/deltas/default_delta.rb +++ b/lib/thinking_sphinx/deltas/default_delta.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Deltas::DefaultDelta attr_reader :adapter, :options @@ -13,7 +15,7 @@ def clause(delta_source = false) def delete(index, instance) ThinkingSphinx::Deltas::DeleteJob.new( - index.name, index.document_id_for_key(instance.id) + index.name, index.document_id_for_instance(instance) ).perform end diff --git a/lib/thinking_sphinx/deltas/delete_job.rb b/lib/thinking_sphinx/deltas/delete_job.rb index 4c8cb3a09..90378435b 100644 --- a/lib/thinking_sphinx/deltas/delete_job.rb +++ b/lib/thinking_sphinx/deltas/delete_job.rb @@ -1,15 +1,27 @@ +# frozen_string_literal: true + class ThinkingSphinx::Deltas::DeleteJob def initialize(index_name, document_id) @index_name, @document_id = index_name, document_id end def perform - ThinkingSphinx::Connection.take do |connection| - connection.execute Riddle::Query.update( - @index_name, @document_id, :sphinx_deleted => true - ) + return if @document_id.nil? + + ThinkingSphinx::Logger.log :query, statement do + ThinkingSphinx::Connection.take do |connection| + connection.execute statement + end end rescue ThinkingSphinx::ConnectionError => error # This isn't vital, so don't raise the error. end + + private + + def statement + @statement ||= Riddle::Query.update( + @index_name, @document_id, :sphinx_deleted => true + ) + end end diff --git a/lib/thinking_sphinx/deltas/index_job.rb b/lib/thinking_sphinx/deltas/index_job.rb index 7e698aaae..f0d1e720d 100644 --- a/lib/thinking_sphinx/deltas/index_job.rb +++ b/lib/thinking_sphinx/deltas/index_job.rb @@ -1,16 +1,28 @@ +# frozen_string_literal: true + class ThinkingSphinx::Deltas::IndexJob def initialize(index_name) @index_name = index_name end def perform - configuration.controller.index @index_name, - :verbose => !configuration.settings['quiet_deltas'] + ThinkingSphinx::Commander.call( + :index_sql, configuration, + :indices => [index_name], + :verbose => !quiet_deltas? + ) end private + attr_reader :index_name + def configuration @configuration ||= ThinkingSphinx::Configuration.instance end + + def quiet_deltas? + configuration.settings['quiet_deltas'].nil? || + configuration.settings['quiet_deltas'] + end end diff --git a/lib/thinking_sphinx/distributed.rb b/lib/thinking_sphinx/distributed.rb index 9a072286d..48b845518 100644 --- a/lib/thinking_sphinx/distributed.rb +++ b/lib/thinking_sphinx/distributed.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Distributed # end diff --git a/lib/thinking_sphinx/distributed/index.rb b/lib/thinking_sphinx/distributed/index.rb index 7720923c1..26142ec0b 100644 --- a/lib/thinking_sphinx/distributed/index.rb +++ b/lib/thinking_sphinx/distributed/index.rb @@ -1,11 +1,14 @@ +# frozen_string_literal: true + class ThinkingSphinx::Distributed::Index < Riddle::Configuration::DistributedIndex - attr_reader :reference, :options + attr_reader :reference, :options, :local_index_objects def initialize(reference) - @reference = reference - @options = {} + @reference = reference + @options = {} + @local_index_objects = [] super reference.to_s.gsub('/', '_') end @@ -18,7 +21,26 @@ def distributed? true end + def facets + local_index_objects.collect(&:facets).flatten + end + + def local_index_objects=(indices) + self.local_indices = indices.collect(&:name) + @local_index_objects = indices + end + def model @model ||= reference.to_s.camelize.constantize end + + def primary_key + @primary_key ||= configuration.settings['primary_key'] || :id + end + + private + + def configuration + ThinkingSphinx::Configuration.instance + end end diff --git a/lib/thinking_sphinx/errors.rb b/lib/thinking_sphinx/errors.rb index 2d37686ff..35d069a7d 100644 --- a/lib/thinking_sphinx/errors.rb +++ b/lib/thinking_sphinx/errors.rb @@ -1,18 +1,24 @@ +# frozen_string_literal: true + class ThinkingSphinx::SphinxError < StandardError attr_accessor :statement def self.new_from_mysql(error) case error.message - when /parse error/ + when /parse error/, /query is non-computable/ replacement = ThinkingSphinx::ParseError.new(error.message) when /syntax error/ replacement = ThinkingSphinx::SyntaxError.new(error.message) - when /query error/ + when /query error/, /unknown column/ replacement = ThinkingSphinx::QueryError.new(error.message) - when /Can't connect to MySQL server/, /Communications link failure/ + when /Can't connect to( MySQL)? server/, + /Communications link failure/, + /Lost connection to( MySQL)? server/ replacement = ThinkingSphinx::ConnectionError.new( "Error connecting to Sphinx via the MySQL protocol. #{error.message}" ) + when /offset out of bounds/ + replacement = ThinkingSphinx::OutOfBoundsError.new(error.message) else replacement = new(error.message) end @@ -29,12 +35,28 @@ class ThinkingSphinx::ConnectionError < ThinkingSphinx::SphinxError class ThinkingSphinx::QueryError < ThinkingSphinx::SphinxError end +class ThinkingSphinx::QueryLengthError < ThinkingSphinx::SphinxError + def message + <<-MESSAGE +The supplied SphinxQL statement is #{statement.length} characters long. The maximum allowed length is #{ThinkingSphinx::Configuration.instance.settings['maximum_statement_length']}. + +If this error has been raised during real-time index population, it's probably due to overly large batches of records being processed at once. The default is 1000, but you can lower it on a per-environment basis in config/thinking_sphinx.yml: + + development: + batch_size: 500 + MESSAGE + end +end + class ThinkingSphinx::SyntaxError < ThinkingSphinx::QueryError end class ThinkingSphinx::ParseError < ThinkingSphinx::QueryError end +class ThinkingSphinx::OutOfBoundsError < ThinkingSphinx::QueryError +end + class ThinkingSphinx::QueryExecutionError < StandardError attr_accessor :statement end @@ -50,3 +72,25 @@ class ThinkingSphinx::MissingColumnError < StandardError class ThinkingSphinx::PopulatedResultsError < StandardError end + +class ThinkingSphinx::DuplicateNameError < StandardError +end + +class ThinkingSphinx::InvalidDatabaseAdapter < StandardError +end + +class ThinkingSphinx::SphinxAlreadyRunning < StandardError +end + +class ThinkingSphinx::UnknownDatabaseAdapter < StandardError +end + +class ThinkingSphinx::UnknownAttributeType < StandardError +end + +class ThinkingSphinx::TranscriptionError < StandardError + attr_accessor :inner_exception, :instance, :property +end + +class ThinkingSphinx::UnknownCommand < StandardError +end diff --git a/lib/thinking_sphinx/excerpter.rb b/lib/thinking_sphinx/excerpter.rb index 65fc66505..7ec275aa5 100644 --- a/lib/thinking_sphinx/excerpter.rb +++ b/lib/thinking_sphinx/excerpter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Excerpter DefaultOptions = { :before_match => '', diff --git a/lib/thinking_sphinx/facet.rb b/lib/thinking_sphinx/facet.rb index 6df636f48..650a83a02 100644 --- a/lib/thinking_sphinx/facet.rb +++ b/lib/thinking_sphinx/facet.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Facet attr_reader :name @@ -11,7 +13,7 @@ def filter_type def results_from(raw) raw.inject({}) { |hash, row| - hash[row[group_column]] = row[ThinkingSphinx::SphinxQL.count[:column]] + hash[row[group_column]] = row["sphinx_internal_count"] hash } end @@ -19,8 +21,7 @@ def results_from(raw) private def group_column - @properties.any?(&:multi?) ? - ThinkingSphinx::SphinxQL.group_by[:column] : name + @properties.any?(&:multi?) ? "sphinx_internal_group" : name end def use_field? diff --git a/lib/thinking_sphinx/facet_search.rb b/lib/thinking_sphinx/facet_search.rb index bb1170fa2..9f82dd70b 100644 --- a/lib/thinking_sphinx/facet_search.rb +++ b/lib/thinking_sphinx/facet_search.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::FacetSearch include Enumerable @@ -112,8 +114,8 @@ def limit def options_for(facet) options.merge( :select => [(options[:select] || '*'), - "#{ThinkingSphinx::SphinxQL.group_by[:select]}", - "#{ThinkingSphinx::SphinxQL.count[:select]}" + "groupby() AS sphinx_internal_group", + "id AS sphinx_document_id, count(DISTINCT sphinx_document_id) AS sphinx_internal_count" ].join(', '), :group_by => facet.name, :indices => index_names_for(facet), diff --git a/lib/thinking_sphinx/float_formatter.rb b/lib/thinking_sphinx/float_formatter.rb index 68b0a3d72..58e5c9bef 100644 --- a/lib/thinking_sphinx/float_formatter.rb +++ b/lib/thinking_sphinx/float_formatter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::FloatFormatter PATTERN = /(\d+)e\-(\d+)$/ diff --git a/lib/thinking_sphinx/frameworks.rb b/lib/thinking_sphinx/frameworks.rb index b30d730a3..14656ae3f 100644 --- a/lib/thinking_sphinx/frameworks.rb +++ b/lib/thinking_sphinx/frameworks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Frameworks def self.current defined?(::Rails) ? ThinkingSphinx::Frameworks::Rails.new : diff --git a/lib/thinking_sphinx/frameworks/plain.rb b/lib/thinking_sphinx/frameworks/plain.rb index dff18a227..371fe85f7 100644 --- a/lib/thinking_sphinx/frameworks/plain.rb +++ b/lib/thinking_sphinx/frameworks/plain.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Frameworks::Plain attr_accessor :environment, :root diff --git a/lib/thinking_sphinx/frameworks/rails.rb b/lib/thinking_sphinx/frameworks/rails.rb index af17c3b6f..7ef4b53ea 100644 --- a/lib/thinking_sphinx/frameworks/rails.rb +++ b/lib/thinking_sphinx/frameworks/rails.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Frameworks::Rails def environment Rails.env diff --git a/lib/thinking_sphinx/guard.rb b/lib/thinking_sphinx/guard.rb index 83e8df204..2e2840d7a 100644 --- a/lib/thinking_sphinx/guard.rb +++ b/lib/thinking_sphinx/guard.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + module ThinkingSphinx::Guard # end require 'thinking_sphinx/guard/file' require 'thinking_sphinx/guard/files' +require 'thinking_sphinx/guard/none' diff --git a/lib/thinking_sphinx/guard/file.rb b/lib/thinking_sphinx/guard/file.rb index a1509571c..a4b5e5623 100644 --- a/lib/thinking_sphinx/guard/file.rb +++ b/lib/thinking_sphinx/guard/file.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Guard::File attr_reader :name @@ -10,7 +12,7 @@ def lock end def locked? - File.exists? path + File.exist? path end def path @@ -21,6 +23,6 @@ def path end def unlock - FileUtils.rm path + FileUtils.rm(path) if locked? end end diff --git a/lib/thinking_sphinx/guard/files.rb b/lib/thinking_sphinx/guard/files.rb index 49e9c1be4..233a646a6 100644 --- a/lib/thinking_sphinx/guard/files.rb +++ b/lib/thinking_sphinx/guard/files.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Guard::Files def self.call(names, &block) new(names).call(&block) diff --git a/lib/thinking_sphinx/guard/none.rb b/lib/thinking_sphinx/guard/none.rb new file mode 100644 index 000000000..3da65334e --- /dev/null +++ b/lib/thinking_sphinx/guard/none.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Guard::None + def self.call(names, &block) + block.call names + end +end diff --git a/lib/thinking_sphinx/hooks/guard_presence.rb b/lib/thinking_sphinx/hooks/guard_presence.rb new file mode 100644 index 000000000..72954d08d --- /dev/null +++ b/lib/thinking_sphinx/hooks/guard_presence.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Hooks::GuardPresence + def self.call(configuration = nil, stream = STDERR) + new(configuration, stream).call + end + + def initialize(configuration = nil, stream = STDERR) + @configuration = configuration || ThinkingSphinx::Configuration.instance + @stream = stream + end + + def call + return if files.empty? + + stream.puts "WARNING: The following indexing guard files exist:" + files.each do |file| + stream.puts " * #{file}" + end + stream.puts <<-TXT +These files indicate indexing is already happening. If that is not the case, +these files should be deleted to ensure all indices can be processed. + + TXT + end + + private + + attr_reader :configuration, :stream + + def files + @files ||= Dir["#{configuration.indices_location}/ts-*.tmp"] + end +end diff --git a/lib/thinking_sphinx/index.rb b/lib/thinking_sphinx/index.rb index af3c27482..c94189346 100644 --- a/lib/thinking_sphinx/index.rb +++ b/lib/thinking_sphinx/index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Index attr_reader :reference, :options, :block diff --git a/lib/thinking_sphinx/index_set.rb b/lib/thinking_sphinx/index_set.rb index 185f49bef..2e87a1643 100644 --- a/lib/thinking_sphinx/index_set.rb +++ b/lib/thinking_sphinx/index_set.rb @@ -1,6 +1,13 @@ +# frozen_string_literal: true + class ThinkingSphinx::IndexSet include Enumerable + def self.reference_name(klass) + @cached_results ||= {} + @cached_results[klass.name] ||= klass.name.underscore.to_sym + end + delegate :each, :empty?, :to => :indices def initialize(options = {}, configuration = nil) @@ -27,15 +34,15 @@ def all_indices end def classes - options[:classes] || [] + options[:classes] || instances.collect(&:class) end def classes_specified? - classes.any? || references_specified? + instances.any? || classes.any? || references_specified? end def classes_and_ancestors - @classes_and_ancestors ||= classes.collect { |model| + @classes_and_ancestors ||= mti_classes + sti_classes.collect { |model| model.ancestors.take_while { |klass| klass != ActiveRecord::Base }.select { |klass| @@ -61,13 +68,29 @@ def indices_for_references all_indices.select { |index| references.include? index.reference } end + def instances + options[:instances] || [] + end + + def mti_classes + classes.reject { |klass| + klass.column_names.include?(klass.inheritance_column) + } + end + def references options[:references] || classes_and_ancestors.collect { |klass| - klass.name.underscore.to_sym + self.class.reference_name(klass) } end def references_specified? options[:references] && options[:references].any? end + + def sti_classes + classes.select { |klass| + klass.column_names.include?(klass.inheritance_column) + } + end end diff --git a/lib/thinking_sphinx/indexing_strategies/all_at_once.rb b/lib/thinking_sphinx/indexing_strategies/all_at_once.rb new file mode 100644 index 000000000..d2abe3960 --- /dev/null +++ b/lib/thinking_sphinx/indexing_strategies/all_at_once.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class ThinkingSphinx::IndexingStrategies::AllAtOnce + def self.call(indices = [], &block) + indices << '--all' if indices.empty? + + block.call indices + end +end diff --git a/lib/thinking_sphinx/indexing_strategies/one_at_a_time.rb b/lib/thinking_sphinx/indexing_strategies/one_at_a_time.rb new file mode 100644 index 000000000..32f3083cc --- /dev/null +++ b/lib/thinking_sphinx/indexing_strategies/one_at_a_time.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ThinkingSphinx::IndexingStrategies::OneAtATime + def self.call(indices = [], &block) + if indices.empty? + configuration = ThinkingSphinx::Configuration.instance + configuration.preload_indices + + indices = configuration.indices.select { |index| + !(index.distributed? || index.type == 'rt') + }.collect &:name + end + + indices.each { |name| block.call [name] } + end +end diff --git a/lib/thinking_sphinx/interfaces.rb b/lib/thinking_sphinx/interfaces.rb new file mode 100644 index 000000000..37019df45 --- /dev/null +++ b/lib/thinking_sphinx/interfaces.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module ThinkingSphinx::Interfaces + # +end + +require 'thinking_sphinx/interfaces/base' +require 'thinking_sphinx/interfaces/daemon' +require 'thinking_sphinx/interfaces/real_time' +require 'thinking_sphinx/interfaces/sql' diff --git a/lib/thinking_sphinx/interfaces/base.rb b/lib/thinking_sphinx/interfaces/base.rb new file mode 100644 index 000000000..8a72773e2 --- /dev/null +++ b/lib/thinking_sphinx/interfaces/base.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Interfaces::Base + include ThinkingSphinx::WithOutput + + private + + def command(command, extra_options = {}) + ThinkingSphinx::Commander.call( + command, configuration, options.merge(extra_options), stream + ) + end +end diff --git a/lib/thinking_sphinx/interfaces/daemon.rb b/lib/thinking_sphinx/interfaces/daemon.rb new file mode 100644 index 000000000..57acaaa3c --- /dev/null +++ b/lib/thinking_sphinx/interfaces/daemon.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Interfaces::Daemon < ThinkingSphinx::Interfaces::Base + def start + if command :running + raise ThinkingSphinx::SphinxAlreadyRunning, 'searchd is already running' + end + + command(options[:nodetach] ? :start_attached : :start_detached) + end + + def status + if command :running + stream.puts "The Sphinx daemon searchd is currently running." + else + stream.puts "The Sphinx daemon searchd is not currently running." + end + end + + def stop + command :stop + end + + private + + delegate :controller, :to => :configuration +end diff --git a/lib/thinking_sphinx/interfaces/real_time.rb b/lib/thinking_sphinx/interfaces/real_time.rb new file mode 100644 index 000000000..c35a335b4 --- /dev/null +++ b/lib/thinking_sphinx/interfaces/real_time.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Interfaces::RealTime < ThinkingSphinx::Interfaces::Base + def initialize(configuration, options, stream = STDOUT) + super + + configuration.preload_indices + + command :prepare + end + + def clear + command :clear_real_time, :indices => indices + end + + def index + return if indices.empty? + if !command :running + stream.puts <<-TXT +The Sphinx daemon is not currently running. Real-time indices can only be +populated by sending commands to a running daemon. + TXT + return + end + + command :index_real_time, :indices => indices + end + + private + + def index_names + @index_names ||= options[:index_names] || [] + end + + def indices + @indices ||= begin + indices = configuration.indices.select { |index| index.type == 'rt' } + + if index_names.any? + indices.select! { |index| index_names.include? index.name } + end + + indices + end + end +end diff --git a/lib/thinking_sphinx/interfaces/sql.rb b/lib/thinking_sphinx/interfaces/sql.rb new file mode 100644 index 000000000..b682f5db3 --- /dev/null +++ b/lib/thinking_sphinx/interfaces/sql.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Interfaces::SQL < ThinkingSphinx::Interfaces::Base + def initialize(configuration, options, stream = STDOUT) + super + + configuration.preload_indices + + command :prepare + end + + def clear + command :clear_sql, :indices => (filtered? ? filtered_indices : indices) + end + + def index(reconfigure = true, verbose = nil) + stream.puts <<-TXT unless verbose.nil? +The verbose argument to the index method is now deprecated, and can instead be +managed by the :verbose option passed in when initialising RakeInterface. That +option is set automatically when invoked by rake, via rake's --silent and/or +--quiet arguments. + TXT + return if indices.empty? + + command :configure if reconfigure + command :index_sql, + :indices => (filtered? ? filtered_indices.collect(&:name) : nil) + end + + def merge + command :merge_and_update + end + + private + + def filtered? + index_names.any? + end + + def filtered_indices + indices.select { |index| index_names.include? index.name } + end + + def index_names + @index_names ||= options[:index_names] || [] + end + + def indices + @indices ||= configuration.indices.select do |index| + index.type == 'plain' || index.type.blank? + end + end +end diff --git a/lib/thinking_sphinx/logger.rb b/lib/thinking_sphinx/logger.rb index ce1243dde..8722bb826 100644 --- a/lib/thinking_sphinx/logger.rb +++ b/lib/thinking_sphinx/logger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Logger def self.log(notification, message, &block) ActiveSupport::Notifications.instrument( diff --git a/lib/thinking_sphinx/masks.rb b/lib/thinking_sphinx/masks.rb index c7c265434..89510cc88 100644 --- a/lib/thinking_sphinx/masks.rb +++ b/lib/thinking_sphinx/masks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Masks # end diff --git a/lib/thinking_sphinx/masks/group_enumerators_mask.rb b/lib/thinking_sphinx/masks/group_enumerators_mask.rb index 602024b02..8b6753259 100644 --- a/lib/thinking_sphinx/masks/group_enumerators_mask.rb +++ b/lib/thinking_sphinx/masks/group_enumerators_mask.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Masks::GroupEnumeratorsMask def initialize(search) @search = search @@ -9,20 +11,20 @@ def can_handle?(method) def each_with_count(&block) @search.raw.each_with_index do |row, index| - yield @search[index], row[ThinkingSphinx::SphinxQL.count[:column]] + yield @search[index], row["sphinx_internal_count"] end end def each_with_group(&block) @search.raw.each_with_index do |row, index| - yield @search[index], row[ThinkingSphinx::SphinxQL.group_by[:column]] + yield @search[index], row["sphinx_internal_group"] end end def each_with_group_and_count(&block) @search.raw.each_with_index do |row, index| - yield @search[index], row[ThinkingSphinx::SphinxQL.group_by[:column]], - row[ThinkingSphinx::SphinxQL.count[:column]] + yield @search[index], row["sphinx_internal_group"], + row["sphinx_internal_count"] end end end diff --git a/lib/thinking_sphinx/masks/pagination_mask.rb b/lib/thinking_sphinx/masks/pagination_mask.rb index c215e3d75..344d2d373 100644 --- a/lib/thinking_sphinx/masks/pagination_mask.rb +++ b/lib/thinking_sphinx/masks/pagination_mask.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Masks::PaginationMask def initialize(search) @search = search @@ -37,6 +39,8 @@ def previous_page search.current_page == 1 ? nil : search.current_page - 1 end + alias_method :prev_page, :previous_page + def total_entries search.meta['total_found'].to_i end diff --git a/lib/thinking_sphinx/masks/scopes_mask.rb b/lib/thinking_sphinx/masks/scopes_mask.rb index 04e9a6be7..2dd773d8b 100644 --- a/lib/thinking_sphinx/masks/scopes_mask.rb +++ b/lib/thinking_sphinx/masks/scopes_mask.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Masks::ScopesMask def initialize(search) @search = search @@ -24,6 +26,12 @@ def search_for_ids(query = nil, options = {}) search query, options.merge(:ids_only => true) end + def none + ThinkingSphinx::Search::Merger.new(@search).merge! nil, :none => true + end + + alias_method :search_none, :none + private def apply_scope(scope, *args) diff --git a/lib/thinking_sphinx/masks/weight_enumerator_mask.rb b/lib/thinking_sphinx/masks/weight_enumerator_mask.rb index 2998c86f2..e3aa8680e 100644 --- a/lib/thinking_sphinx/masks/weight_enumerator_mask.rb +++ b/lib/thinking_sphinx/masks/weight_enumerator_mask.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Masks::WeightEnumeratorMask def initialize(search) @search = search @@ -9,7 +11,7 @@ def can_handle?(method) def each_with_weight(&block) @search.raw.each_with_index do |row, index| - yield @search[index], row[ThinkingSphinx::SphinxQL.weight[:column]] + yield @search[index], row["weight()"] end end end diff --git a/lib/thinking_sphinx/middlewares.rb b/lib/thinking_sphinx/middlewares.rb index 5fddee3f4..958a750cd 100644 --- a/lib/thinking_sphinx/middlewares.rb +++ b/lib/thinking_sphinx/middlewares.rb @@ -1,7 +1,11 @@ +# frozen_string_literal: true + module ThinkingSphinx::Middlewares; end -%w[middleware active_record_translator geographer glazier ids_only inquirer - sphinxql stale_id_checker stale_id_filter utf8].each do |middleware| +%w[ + middleware active_record_translator geographer glazier ids_only inquirer + sphinxql stale_id_checker stale_id_filter valid_options +].each do |middleware| require "thinking_sphinx/middlewares/#{middleware}" end @@ -10,7 +14,7 @@ def self.use(builder, middlewares) middlewares.each { |m| builder.use m } end - BASE_MIDDLEWARES = [SphinxQL, Geographer, Inquirer] + BASE_MIDDLEWARES = [ValidOptions, SphinxQL, Geographer, Inquirer] DEFAULT = ::Middleware::Builder.new do use StaleIdFilter diff --git a/lib/thinking_sphinx/middlewares/active_record_translator.rb b/lib/thinking_sphinx/middlewares/active_record_translator.rb index 56e4a3c69..1986dba72 100644 --- a/lib/thinking_sphinx/middlewares/active_record_translator.rb +++ b/lib/thinking_sphinx/middlewares/active_record_translator.rb @@ -1,6 +1,11 @@ +# frozen_string_literal: true + class ThinkingSphinx::Middlewares::ActiveRecordTranslator < ThinkingSphinx::Middlewares::Middleware + NO_MODEL = Struct.new(:primary_key).new(:id).freeze + NO_INDEX = Struct.new(:primary_key).new(:id).freeze + def call(contexts) contexts.each do |context| Inner.new(context).call @@ -36,12 +41,25 @@ def ids_for_model(model_name) }.compact end + def index_for(model) + return NO_INDEX unless context[:indices] + + context[:indices].detect { |index| index.model == model } || NO_INDEX + end + def model_names @model_names ||= context[:results].collect { |row| row['sphinx_internal_class'] }.uniq end + def primary_key_for(model) + model = NO_MODEL unless model.respond_to?(:primary_key) + + @primary_keys ||= {} + @primary_keys[model] ||= index_for(model).primary_key + end + def reset_memos @model_names = nil @results_for_models = nil @@ -49,27 +67,32 @@ def reset_memos def result_for(row) results_for_models[row['sphinx_internal_class']].detect { |record| - record.id == row['sphinx_internal_id'] + record.public_send( + primary_key_for(record.class) + ) == row['sphinx_internal_id'] } end def results_for_models @results_for_models ||= model_names.inject({}) do |hash, name| model = name.constantize - hash[name] = model_relation_with_sql_options(model.unscoped).where( - model.primary_key => ids_for_model(name) + + model_sql_options = sql_options[name] || sql_options + + hash[name] = model_relation_with_sql_options(model.unscoped, model_sql_options).where( + primary_key_for(model) => ids_for_model(name) ) hash end end - def model_relation_with_sql_options(relation) - relation = relation.includes sql_options[:include] if sql_options[:include] - relation = relation.joins sql_options[:joins] if sql_options[:joins] - relation = relation.order sql_options[:order] if sql_options[:order] - relation = relation.select sql_options[:select] if sql_options[:select] - relation = relation.group sql_options[:group] if sql_options[:group] + def model_relation_with_sql_options(relation, model_sql_options) + relation = relation.includes model_sql_options[:include] if model_sql_options[:include] + relation = relation.joins model_sql_options[:joins] if model_sql_options[:joins] + relation = relation.order model_sql_options[:order] if model_sql_options[:order] + relation = relation.select model_sql_options[:select] if model_sql_options[:select] + relation = relation.group model_sql_options[:group] if model_sql_options[:group] relation end diff --git a/lib/thinking_sphinx/middlewares/geographer.rb b/lib/thinking_sphinx/middlewares/geographer.rb index 767f97e3d..d9e5e8cdc 100644 --- a/lib/thinking_sphinx/middlewares/geographer.rb +++ b/lib/thinking_sphinx/middlewares/geographer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support/core_ext/module/delegation' class ThinkingSphinx::Middlewares::Geographer < diff --git a/lib/thinking_sphinx/middlewares/glazier.rb b/lib/thinking_sphinx/middlewares/glazier.rb index f322ebc0e..4363811bc 100644 --- a/lib/thinking_sphinx/middlewares/glazier.rb +++ b/lib/thinking_sphinx/middlewares/glazier.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Middlewares::Glazier < ThinkingSphinx::Middlewares::Middleware @@ -14,6 +16,7 @@ def call(contexts) class Inner def initialize(context) @context = context + @indices = {} end def call @@ -29,10 +32,20 @@ def call attr_reader :context + def indices_for(model) + @indices[model] ||= context[:indices].select do |index| + index.model == model + end + end + def row_for(result) + ids = indices_for(result.class).collect do |index| + result.send index.primary_key + end + context[:raw].detect { |row| row['sphinx_internal_class'] == result.class.name && - row['sphinx_internal_id'] == result.id + ids.include?(row['sphinx_internal_id']) } end end diff --git a/lib/thinking_sphinx/middlewares/ids_only.rb b/lib/thinking_sphinx/middlewares/ids_only.rb index 0885af42f..f03f6a30b 100644 --- a/lib/thinking_sphinx/middlewares/ids_only.rb +++ b/lib/thinking_sphinx/middlewares/ids_only.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Middlewares::IdsOnly < ThinkingSphinx::Middlewares::Middleware diff --git a/lib/thinking_sphinx/middlewares/inquirer.rb b/lib/thinking_sphinx/middlewares/inquirer.rb index 75bd45dad..9c0151294 100644 --- a/lib/thinking_sphinx/middlewares/inquirer.rb +++ b/lib/thinking_sphinx/middlewares/inquirer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Middlewares::Inquirer < ThinkingSphinx::Middlewares::Middleware @@ -45,7 +47,7 @@ def initialize(context) def call(raw_results, meta_results) context[:results] = raw_results.to_a - context[:raw] = raw_results + context[:raw] = context[:results].dup context[:meta] = meta_results.inject({}) { |hash, row| hash[row['Variable_name']] = row['Value'] hash diff --git a/lib/thinking_sphinx/middlewares/middleware.rb b/lib/thinking_sphinx/middlewares/middleware.rb index 204714433..c5cdd96d5 100644 --- a/lib/thinking_sphinx/middlewares/middleware.rb +++ b/lib/thinking_sphinx/middlewares/middleware.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Middlewares::Middleware def initialize(app) @app = app diff --git a/lib/thinking_sphinx/middlewares/sphinxql.rb b/lib/thinking_sphinx/middlewares/sphinxql.rb index 21115e924..69e01fd96 100644 --- a/lib/thinking_sphinx/middlewares/sphinxql.rb +++ b/lib/thinking_sphinx/middlewares/sphinxql.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + class ThinkingSphinx::Middlewares::SphinxQL < ThinkingSphinx::Middlewares::Middleware SELECT_OPTIONS = [:agent_query_timeout, :boolean_simplify, :comment, :cutoff, :field_weights, :global_idf, :idf, :index_weights, :max_matches, :max_query_time, :max_predicted_time, :ranker, :retry_count, :retry_delay, - :reverse_scan, :sort_method] + :reverse_scan, :sort_method, :rand_seed] def call(contexts) contexts.each do |context| @@ -80,19 +82,6 @@ def descendants_from_tables end.flatten end - def indices_match_classes? - indices.collect(&:reference).uniq.sort == classes.collect { |klass| - klass.name.underscore.to_sym - }.sort - end - - def inheritance_column_select(klass) - <<-SQL -SELECT DISTINCT #{klass.inheritance_column} -FROM #{klass.table_name} -SQL - end - def exclusive_filters @exclusive_filters ||= (options[:without] || {}).tap do |without| without[:sphinx_internal_id] = options[:without_ids] if options[:without_ids].present? @@ -142,6 +131,19 @@ def indices end end + def indices_match_classes? + indices.collect(&:reference).uniq.sort == classes.collect { |klass| + configuration.index_set_class.reference_name(klass) + }.sort + end + + def inheritance_column_select(klass) + <<-SQL +SELECT DISTINCT #{klass.inheritance_column} +FROM #{klass.table_name} +SQL + end + def order_clause order_by = options[:order] order_by = "#{order_by} ASC" if order_by.is_a? Symbol @@ -159,8 +161,8 @@ def select_options def values options[:select] ||= ['*', - "#{ThinkingSphinx::SphinxQL.group_by[:select]}", - "#{ThinkingSphinx::SphinxQL.count[:select]}" + "groupby() AS sphinx_internal_group", + "id AS sphinx_document_id, count(DISTINCT sphinx_document_id) AS sphinx_internal_count" ].join(', ') if group_attribute.present? options[:select] end diff --git a/lib/thinking_sphinx/middlewares/stale_id_checker.rb b/lib/thinking_sphinx/middlewares/stale_id_checker.rb index aec680572..e58d94d8d 100644 --- a/lib/thinking_sphinx/middlewares/stale_id_checker.rb +++ b/lib/thinking_sphinx/middlewares/stale_id_checker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Middlewares::StaleIdChecker < ThinkingSphinx::Middlewares::Middleware @@ -33,7 +35,7 @@ def expected_ids end def raise_exception - raise ThinkingSphinx::Search::StaleIdsException, stale_ids + raise ThinkingSphinx::Search::StaleIdsException.new(stale_ids, context) end def stale_ids diff --git a/lib/thinking_sphinx/middlewares/stale_id_filter.rb b/lib/thinking_sphinx/middlewares/stale_id_filter.rb index 8c5c2ce45..431528361 100644 --- a/lib/thinking_sphinx/middlewares/stale_id_filter.rb +++ b/lib/thinking_sphinx/middlewares/stale_id_filter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Middlewares::StaleIdFilter < ThinkingSphinx::Middlewares::Middleware @@ -11,7 +13,7 @@ def call(contexts) rescue ThinkingSphinx::Search::StaleIdsException => error raise error if @retries <= 0 - append_stale_ids error.ids + append_stale_ids error.ids, error.context ThinkingSphinx::Logger.log :message, log_message @retries -= 1 and retry @@ -20,7 +22,7 @@ def call(contexts) private - def append_stale_ids(ids) + def append_stale_ids(ids, context) @stale_ids |= ids context.search.options[:without_ids] ||= [] diff --git a/lib/thinking_sphinx/middlewares/utf8.rb b/lib/thinking_sphinx/middlewares/utf8.rb deleted file mode 100644 index 086e86aa6..000000000 --- a/lib/thinking_sphinx/middlewares/utf8.rb +++ /dev/null @@ -1,27 +0,0 @@ -class ThinkingSphinx::Middlewares::UTF8 < - ThinkingSphinx::Middlewares::Middleware - - def call(contexts) - contexts.each do |context| - context[:results].each { |row| update_row row } - update_row context[:meta] - end unless encoded? - - app.call contexts - end - - private - - def encoded? - ThinkingSphinx::Configuration.instance.settings['utf8'].nil? || - ThinkingSphinx::Configuration.instance.settings['utf8'] - end - - def update_row(row) - row.each do |key, value| - next unless value.is_a?(String) - - row[key] = ThinkingSphinx::UTF8.encode value - end - end -end diff --git a/lib/thinking_sphinx/middlewares/valid_options.rb b/lib/thinking_sphinx/middlewares/valid_options.rb new file mode 100644 index 000000000..b666fccd1 --- /dev/null +++ b/lib/thinking_sphinx/middlewares/valid_options.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Middlewares::ValidOptions < + ThinkingSphinx::Middlewares::Middleware + + def call(contexts) + contexts.each { |context| check_options context.search.options } + + app.call contexts + end + + private + + def check_options(options) + unknown = invalid_keys options.keys + return if unknown.empty? + + ThinkingSphinx::Logger.log :caution, + "Unexpected search options: #{unknown.inspect}" + end + + def invalid_keys(keys) + keys - ThinkingSphinx::Search.valid_options + end +end diff --git a/lib/thinking_sphinx/panes.rb b/lib/thinking_sphinx/panes.rb index 94ba474e5..66f59b239 100644 --- a/lib/thinking_sphinx/panes.rb +++ b/lib/thinking_sphinx/panes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Panes # end diff --git a/lib/thinking_sphinx/panes/attributes_pane.rb b/lib/thinking_sphinx/panes/attributes_pane.rb index ab6e9a592..ad4ca8814 100644 --- a/lib/thinking_sphinx/panes/attributes_pane.rb +++ b/lib/thinking_sphinx/panes/attributes_pane.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Panes::AttributesPane def initialize(context, object, raw) @raw = raw diff --git a/lib/thinking_sphinx/panes/distance_pane.rb b/lib/thinking_sphinx/panes/distance_pane.rb index e8c03f7ed..31fe1b0e8 100644 --- a/lib/thinking_sphinx/panes/distance_pane.rb +++ b/lib/thinking_sphinx/panes/distance_pane.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Panes::DistancePane def initialize(context, object, raw) @raw = raw diff --git a/lib/thinking_sphinx/panes/excerpts_pane.rb b/lib/thinking_sphinx/panes/excerpts_pane.rb index bde976e86..c1806d95b 100644 --- a/lib/thinking_sphinx/panes/excerpts_pane.rb +++ b/lib/thinking_sphinx/panes/excerpts_pane.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Panes::ExcerptsPane def initialize(context, object, raw) @context, @object = context, object diff --git a/lib/thinking_sphinx/panes/weight_pane.rb b/lib/thinking_sphinx/panes/weight_pane.rb index 221d36492..6c3397b58 100644 --- a/lib/thinking_sphinx/panes/weight_pane.rb +++ b/lib/thinking_sphinx/panes/weight_pane.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + class ThinkingSphinx::Panes::WeightPane def initialize(context, object, raw) @raw = raw end def weight - @raw[ThinkingSphinx::SphinxQL.weight[:column]] + @raw["weight()"] end end diff --git a/lib/thinking_sphinx/processor.rb b/lib/thinking_sphinx/processor.rb new file mode 100644 index 000000000..db440ccf9 --- /dev/null +++ b/lib/thinking_sphinx/processor.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +class ThinkingSphinx::Processor + # @param instance [ActiveRecord::Base] an ActiveRecord object + # @param model [Class] the ActiveRecord model of the instance + # @param id [Integer] the instance indices primary key (might be different from model primary key) + def initialize(instance: nil, model: nil, id: nil) + raise ArgumentError if instance.nil? && (model.nil? || id.nil?) + + @instance = instance + @model = model || instance.class + @id = id + end + + def delete + return if instance&.new_record? + + indices.each { |index| perform_deletion(index) } + end + + # Will insert instance into all matching indices + def upsert + real_time_indices.each do |index| + found = loaded_instance(index) + ThinkingSphinx::RealTime::Transcriber.new(index).copy found if found + end + end + + # Will upsert or delete instance into all matching indices based on index scope + def sync + real_time_indices.each do |index| + found = find_in(index) + + if found + ThinkingSphinx::RealTime::Transcriber.new(index).copy found + else + ThinkingSphinx::Deletion.perform(index, index_id(index)) + end + end + end + + private + + attr_reader :instance, :model, :id + + def indices + ThinkingSphinx::Configuration.instance.index_set_class.new( + :instances => [instance].compact, :classes => [model] + ).to_a + end + + def find_in(index) + index.scope.find_by(index.primary_key => index_id(index)) + end + + def loaded_instance(index) + instance || find_in(index) + end + + def real_time_indices + indices.select { |index| index.is_a? ThinkingSphinx::RealTime::Index } + end + + def perform_deletion(index) + ThinkingSphinx::Deletion.perform(index, index_id(index)) + end + + def index_id(index) + id || instance.public_send(index.primary_key) + end +end diff --git a/lib/thinking_sphinx/query.rb b/lib/thinking_sphinx/query.rb index f21196f79..21fcc0992 100644 --- a/lib/thinking_sphinx/query.rb +++ b/lib/thinking_sphinx/query.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Query def self.escape(query) Riddle::Query.escape query diff --git a/lib/thinking_sphinx/railtie.rb b/lib/thinking_sphinx/railtie.rb index 1d8a17aa8..cac3ba898 100644 --- a/lib/thinking_sphinx/railtie.rb +++ b/lib/thinking_sphinx/railtie.rb @@ -1,11 +1,38 @@ +# frozen_string_literal: true + class ThinkingSphinx::Railtie < Rails::Railtie + config.to_prepare do + ThinkingSphinx::Configuration.reset + end + + config.after_initialize do + require 'thinking_sphinx/active_record' + end + initializer 'thinking_sphinx.initialisation' do - if defined?(ActiveRecord::Base) - ActiveRecord::Base.send :include, ThinkingSphinx::ActiveRecord::Base + ActiveSupport.on_load(:active_record) do + require 'thinking_sphinx/active_record' end + + if zeitwerk? + ActiveSupport::Dependencies.autoload_paths.delete( + Rails.root.join("app", "indices").to_s + ) + end + + Rails.application.config.eager_load_paths -= + ThinkingSphinx::Configuration.instance.index_paths + Rails.application.config.eager_load_paths.freeze end rake_tasks do load File.expand_path('../tasks.rb', __FILE__) end + + def zeitwerk? + return true if ActiveSupport::VERSION::MAJOR >= 7 + return false if ActiveSupport::VERSION::MAJOR <= 5 + + Rails.application.config.autoloader == :zeitwerk + end end diff --git a/lib/thinking_sphinx/rake_interface.rb b/lib/thinking_sphinx/rake_interface.rb index 1f020aa15..95a69dbba 100644 --- a/lib/thinking_sphinx/rake_interface.rb +++ b/lib/thinking_sphinx/rake_interface.rb @@ -1,87 +1,32 @@ -class ThinkingSphinx::RakeInterface - def clear_all - [ - configuration.indices_location, - configuration.searchd.binlog_path - ].each do |path| - FileUtils.rm_r(path) if File.exists?(path) - end - end +# frozen_string_literal: true - def clear_real_time - indices = configuration.indices.select { |index| index.type == 'rt' } - indices.each do |index| - index.render - Dir["#{index.path}.*"].each { |path| FileUtils.rm path } - end +class ThinkingSphinx::RakeInterface + DEFAULT_OPTIONS = {:verbose => true} - path = configuration.searchd.binlog_path - FileUtils.rm_r(path) if File.exists?(path) + def initialize(options = {}) + @options = DEFAULT_OPTIONS.merge options + @options[:verbose] = false if @options[:silent] end def configure - puts "Generating configuration to #{configuration.configuration_file}" - configuration.render_to_file - end - - def generate - indices = configuration.indices.select { |index| index.type == 'rt' } - indices.each do |index| - ThinkingSphinx::RealTime::Populator.populate index - end - end - - def index(reconfigure = true, verbose = true) - configure if reconfigure - FileUtils.mkdir_p configuration.indices_location - ThinkingSphinx.before_index_hooks.each { |hook| hook.call } - controller.index :verbose => verbose + ThinkingSphinx::Commander.call :configure, configuration, options end - def prepare - configuration.preload_indices - configuration.render - - FileUtils.mkdir_p configuration.indices_location + def daemon + @daemon ||= ThinkingSphinx::Interfaces::Daemon.new configuration, options end - def start - raise RuntimeError, 'searchd is already running' if controller.running? - - FileUtils.mkdir_p configuration.indices_location - controller.start - - if controller.running? - puts "Started searchd successfully (pid: #{controller.pid})." - else - puts "Failed to start searchd. Check the log files for more information." - end + def rt + @rt ||= ThinkingSphinx::Interfaces::RealTime.new configuration, options end - def status - if controller.running? - puts "The Sphinx daemon searchd is currently running." - else - puts "The Sphinx daemon searchd is not currently running." - end - end - - def stop - unless controller.running? - puts 'searchd is not currently running.' and return - end - - pid = controller.pid - until controller.stop do - sleep(0.5) - end - - puts "Stopped searchd daemon (pid: #{pid})." + def sql + @sql ||= ThinkingSphinx::Interfaces::SQL.new configuration, options end private - delegate :controller, :to => :configuration + attr_reader :options def configuration ThinkingSphinx::Configuration.instance diff --git a/lib/thinking_sphinx/real_time.rb b/lib/thinking_sphinx/real_time.rb index 95e016f23..3c6c5570c 100644 --- a/lib/thinking_sphinx/real_time.rb +++ b/lib/thinking_sphinx/real_time.rb @@ -1,10 +1,28 @@ +# frozen_string_literal: true + module ThinkingSphinx::RealTime module Callbacks # end def self.callback_for(reference, path = [], &block) - Callbacks::RealTimeCallbacks.new reference, path, &block + Callbacks::RealTimeCallbacks.new reference.to_sym, path, &block + end + + def self.populator + @populator ||= ThinkingSphinx::RealTime::Populator + end + + def self.populator=(value) + @populator = value + end + + def self.processor + @processor ||= ThinkingSphinx::RealTime::Processor + end + + def self.processor=(value) + @processor = value end end @@ -14,6 +32,9 @@ def self.callback_for(reference, path = [], &block) require 'thinking_sphinx/real_time/index' require 'thinking_sphinx/real_time/interpreter' require 'thinking_sphinx/real_time/populator' +require 'thinking_sphinx/real_time/processor' +require 'thinking_sphinx/real_time/transcribe_instance' require 'thinking_sphinx/real_time/transcriber' +require 'thinking_sphinx/real_time/translator' require 'thinking_sphinx/real_time/callbacks/real_time_callbacks' diff --git a/lib/thinking_sphinx/real_time/attribute.rb b/lib/thinking_sphinx/real_time/attribute.rb index adc970c9c..1a64bb582 100644 --- a/lib/thinking_sphinx/real_time/attribute.rb +++ b/lib/thinking_sphinx/real_time/attribute.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::RealTime::Attribute < ThinkingSphinx::RealTime::Property def multi? @options[:multi] @@ -8,7 +10,9 @@ def type end def translate(object) - super || default_value + output = super || default_value + + json? ? output.to_json : output end private @@ -16,4 +20,8 @@ def translate(object) def default_value type == :string ? '' : 0 end + + def json? + type == :json + end end diff --git a/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb b/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb index 8030cba0e..b88a26006 100644 --- a/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb +++ b/lib/thinking_sphinx/real_time/callbacks/real_time_callbacks.rb @@ -1,16 +1,16 @@ +# frozen_string_literal: true + class ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks def initialize(reference, path = [], &block) @reference, @path, @block = reference, path, block end - def after_save(instance) - return unless real_time_indices? && callbacks_enabled? + def after_commit(instance) + persist_changes instance + end - real_time_indices.each do |index| - objects_for(instance).each do |object| - ThinkingSphinx::RealTime::Transcriber.new(index).copy object - end - end + def after_save(instance) + persist_changes instance end private @@ -40,6 +40,16 @@ def objects_for(instance) Array results end + def persist_changes(instance) + return unless real_time_indices? && callbacks_enabled? + + real_time_indices.each do |index| + objects_for(instance).each do |object| + ThinkingSphinx::RealTime::Transcriber.new(index).copy object + end + end + end + def real_time_indices? real_time_indices.any? end diff --git a/lib/thinking_sphinx/real_time/field.rb b/lib/thinking_sphinx/real_time/field.rb index 684f99443..e2bf30b89 100644 --- a/lib/thinking_sphinx/real_time/field.rb +++ b/lib/thinking_sphinx/real_time/field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::RealTime::Field < ThinkingSphinx::RealTime::Property include ThinkingSphinx::Core::Field diff --git a/lib/thinking_sphinx/real_time/index.rb b/lib/thinking_sphinx/real_time/index.rb index 43ea17dfa..f5db245e8 100644 --- a/lib/thinking_sphinx/real_time/index.rb +++ b/lib/thinking_sphinx/real_time/index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::RealTime::Index < Riddle::Configuration::RealtimeIndex include ThinkingSphinx::Core::Index @@ -8,16 +10,20 @@ def initialize(reference, options = {}) @attributes = [] @conditions = [] - Template.new(self).apply - super reference, options + + Template.new(self).apply end def add_attribute(attribute) + @attributes.delete_if { |existing| existing.name == attribute.name } + @attributes << attribute end def add_field(field) + @fields.delete_if { |existing| existing.name == field.name } + @fields << field end @@ -59,16 +65,16 @@ def append_unique_attribute(collection, attribute) def collection_for(attribute) case attribute.type - when :integer, :boolean + when :integer, :boolean, :timestamp attribute.multi? ? @rt_attr_multi : @rt_attr_uint when :string @rt_attr_string - when :timestamp - @rt_attr_timestamp when :float @rt_attr_float when :bigint attribute.multi? ? @rt_attr_multi_64 : @rt_attr_bigint + when :json + @rt_attr_json else raise "Unknown attribute type '#{attribute.type}'" end diff --git a/lib/thinking_sphinx/real_time/index/template.rb b/lib/thinking_sphinx/real_time/index/template.rb index 89d0de79b..c8db1067d 100644 --- a/lib/thinking_sphinx/real_time/index/template.rb +++ b/lib/thinking_sphinx/real_time/index/template.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::RealTime::Index::Template attr_reader :index @@ -8,9 +10,13 @@ def initialize(index) def apply add_field class_column, :sphinx_internal_class_name - add_attribute :id, :sphinx_internal_id, :bigint + add_attribute primary_key, :sphinx_internal_id, :bigint add_attribute class_column, :sphinx_internal_class, :string, :facet => true add_attribute 0, :sphinx_deleted, :integer + + if tidying? + add_attribute -> (_) { Time.current.to_i }, :sphinx_updated_at, :timestamp + end end private @@ -31,4 +37,16 @@ def add_field(column, name) def class_column [:class, :name] end + + def config + ThinkingSphinx::Configuration.instance + end + + def primary_key + index.primary_key.to_sym + end + + def tidying? + config.settings["real_time_tidy"] + end end diff --git a/lib/thinking_sphinx/real_time/interpreter.rb b/lib/thinking_sphinx/real_time/interpreter.rb index bc11b12a8..ba9c11a1d 100644 --- a/lib/thinking_sphinx/real_time/interpreter.rb +++ b/lib/thinking_sphinx/real_time/interpreter.rb @@ -1,18 +1,22 @@ +# frozen_string_literal: true + class ThinkingSphinx::RealTime::Interpreter < ::ThinkingSphinx::Core::Interpreter def has(*columns) options = columns.extract_options! - @index.attributes += columns.collect { |column| + + columns.collect { |column| ::ThinkingSphinx::RealTime::Attribute.new column, options - } + }.each { |attribute| @index.add_attribute attribute } end def indexes(*columns) options = columns.extract_options! - @index.fields += columns.collect { |column| + + columns.collect { |column| ::ThinkingSphinx::RealTime::Field.new column, options - } + }.each { |field| @index.add_field field } append_sortable_attributes columns, options if options[:sortable] end @@ -37,7 +41,7 @@ def where(condition) def append_sortable_attributes(columns, options) options = options.except(:sortable).merge(:type => :string) - @index.attributes += columns.collect { |column| + columns.collect { |column| aliased_name = options[:as] aliased_name ||= column.__name.to_sym if column.respond_to?(:__name) aliased_name ||= column @@ -45,6 +49,6 @@ def append_sortable_attributes(columns, options) options[:as] = "#{aliased_name}_sort".to_sym ::ThinkingSphinx::RealTime::Attribute.new column, options - } + }.each { |attribute| @index.add_attribute attribute } end end diff --git a/lib/thinking_sphinx/real_time/populator.rb b/lib/thinking_sphinx/real_time/populator.rb index e1b8d24e7..b739445ea 100644 --- a/lib/thinking_sphinx/real_time/populator.rb +++ b/lib/thinking_sphinx/real_time/populator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::RealTime::Populator def self.populate(index) new(index).populate @@ -5,28 +7,28 @@ def self.populate(index) def initialize(index) @index = index + @started_at = Time.current end - def populate(&block) + def populate instrument 'start_populating' - remove_files - - scope.find_each do |instance| - transcriber.copy instance - instrument 'populated', :instance => instance + scope.find_in_batches(:batch_size => batch_size) do |instances| + transcriber.copy *instances + instrument 'populated', :instances => instances end - controller.rotate + transcriber.clear_before(started_at) if configuration.settings["real_time_tidy"] + instrument 'finish_populating' end private - attr_reader :index + attr_reader :index, :started_at - delegate :controller, :to => :configuration - delegate :scope, :to => :index + delegate :controller, :batch_size, :to => :configuration + delegate :scope, :to => :index def configuration ThinkingSphinx::Configuration.instance @@ -38,10 +40,6 @@ def instrument(message, options = {}) ) end - def remove_files - Dir["#{index.path}*"].each { |file| FileUtils.rm file } - end - def transcriber @transcriber ||= ThinkingSphinx::RealTime::Transcriber.new index end diff --git a/lib/thinking_sphinx/real_time/processor.rb b/lib/thinking_sphinx/real_time/processor.rb new file mode 100644 index 000000000..52c157739 --- /dev/null +++ b/lib/thinking_sphinx/real_time/processor.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class ThinkingSphinx::RealTime::Processor + def self.call(indices, &block) + new(indices).call(&block) + end + + def initialize(indices) + @indices = indices + end + + def call(&block) + subscribe_to_progress + + indices.each do |index| + ThinkingSphinx::RealTime.populator.populate index + + block.call + end + end + + private + + attr_reader :indices + + def command + ThinkingSphinx::Commander.call( + command, configuration, options, stream + ) + end + + def subscribe_to_progress + ThinkingSphinx::Subscribers::PopulatorSubscriber. + attach_to 'thinking_sphinx.real_time' + end +end diff --git a/lib/thinking_sphinx/real_time/property.rb b/lib/thinking_sphinx/real_time/property.rb index b2be10dd5..7f913b015 100644 --- a/lib/thinking_sphinx/real_time/property.rb +++ b/lib/thinking_sphinx/real_time/property.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::RealTime::Property include ThinkingSphinx::Core::Property @@ -14,10 +16,6 @@ def name end def translate(object) - return @column.__name unless @column.__name.is_a?(Symbol) - - base = @column.__stack.inject(object) { |base, node| base.try(node) } - base = base.try(@column.__name) - base.is_a?(String) ? base.gsub("\u0000", '') : base + ThinkingSphinx::RealTime::Translator.call(object, @column) end end diff --git a/lib/thinking_sphinx/real_time/transcribe_instance.rb b/lib/thinking_sphinx/real_time/transcribe_instance.rb new file mode 100644 index 000000000..ba29da794 --- /dev/null +++ b/lib/thinking_sphinx/real_time/transcribe_instance.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class ThinkingSphinx::RealTime::TranscribeInstance + def self.call(instance, index, properties) + new(instance, index, properties).call + end + + def initialize(instance, index, properties) + @instance, @index, @properties = instance, index, properties + end + + def call + properties.each_with_object([document_id]) do |property, instance_values| + begin + instance_values << property.translate(instance) + rescue StandardError => error + raise_wrapper error, property + end + end + end + + private + + attr_reader :instance, :index, :properties + + def document_id + index.document_id_for_key instance.public_send(index.primary_key) + end + + def raise_wrapper(error, property) + wrapper = ThinkingSphinx::TranscriptionError.new + wrapper.inner_exception = error + wrapper.instance = instance + wrapper.property = property + + raise wrapper + end +end diff --git a/lib/thinking_sphinx/real_time/transcriber.rb b/lib/thinking_sphinx/real_time/transcriber.rb index c11e79d24..4562bb3c5 100644 --- a/lib/thinking_sphinx/real_time/transcriber.rb +++ b/lib/thinking_sphinx/real_time/transcriber.rb @@ -1,31 +1,36 @@ +# frozen_string_literal: true + class ThinkingSphinx::RealTime::Transcriber def initialize(index) @index = index end - def copy(instance) - return unless instance.persisted? && copy?(instance) + def clear_before(time) + execute <<~SQL.strip + DELETE FROM #{@index.name} WHERE sphinx_updated_at < #{time.to_i} + SQL + end - columns, values = ['id'], [index.document_id_for_key(instance.id)] - (index.fields + index.attributes).each do |property| - columns << property.name - values << property.translate(instance) - end + def copy(*instances) + items = instances.select { |instance| + instance.persisted? && copy?(instance) + } + return unless items.present? - insert = Riddle::Query::Insert.new index.name, columns, values - sphinxql = insert.replace!.to_sql - - ThinkingSphinx::Logger.log :query, sphinxql do - ThinkingSphinx::Connection.take do |connection| - connection.execute sphinxql - end - end + delete_existing items + insert_replacements items end private attr_reader :index + def columns + @columns ||= properties.each_with_object(['id']) do |property, columns| + columns << property.name + end + end + def copy?(instance) index.conditions.empty? || index.conditions.all? { |condition| case condition @@ -38,4 +43,47 @@ def copy?(instance) end } end + + def delete_existing(instances) + ids = instances.collect(&index.primary_key.to_sym) + + execute <<~SQL.strip + DELETE FROM #{@index.name} WHERE sphinx_internal_id IN (#{ids.join(', ')}) + SQL + end + + def execute(sphinxql) + ThinkingSphinx::Logger.log :query, sphinxql do + ThinkingSphinx::Connection.take do |connection| + connection.execute sphinxql + end + end + end + + def insert_replacements(instances) + insert = Riddle::Query::Insert.new index.name, columns, values(instances) + execute insert.replace!.to_sql + end + + def instrument(message, options = {}) + ActiveSupport::Notifications.instrument( + "#{message}.thinking_sphinx.real_time", options.merge(:index => index) + ) + end + + def properties + @properties ||= index.fields + index.attributes + end + + def values(instances) + instances.each_with_object([]) do |instance, array| + begin + array << ThinkingSphinx::RealTime::TranscribeInstance.call( + instance, index, properties + ) + rescue ThinkingSphinx::TranscriptionError => error + instrument 'error', :error => error + end + end + end end diff --git a/lib/thinking_sphinx/real_time/translator.rb b/lib/thinking_sphinx/real_time/translator.rb new file mode 100644 index 000000000..f723c762d --- /dev/null +++ b/lib/thinking_sphinx/real_time/translator.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class ThinkingSphinx::RealTime::Translator + def self.call(object, column) + new(object, column).call + end + + def initialize(object, column) + @object, @column = object, column + end + + def call + return name.call(object) if name.is_a?(Proc) + return name unless name.is_a?(Symbol) + return result unless result.is_a?(String) + + result.gsub("\u0000", '').force_encoding "UTF-8" + end + + private + + attr_reader :object, :column + + def name + @column.__name + end + + def owner + stack.inject(object) { |previous, node| previous.try node } + end + + def result + @result ||= owner.try name + end + + def stack + @column.__stack + end +end diff --git a/lib/thinking_sphinx/scopes.rb b/lib/thinking_sphinx/scopes.rb index 7f415ddf2..e71c82fcc 100644 --- a/lib/thinking_sphinx/scopes.rb +++ b/lib/thinking_sphinx/scopes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx::Scopes extend ActiveSupport::Concern @@ -24,5 +26,9 @@ def method_missing(method, *args, &block) query, options = sphinx_scopes[method].call(*args) search query, (options || {}) end + + def respond_to_missing?(method, include_private = false) + super || sphinx_scopes.keys.include?(method) + end end end diff --git a/lib/thinking_sphinx/search.rb b/lib/thinking_sphinx/search.rb index 3743b898d..97b1799c9 100644 --- a/lib/thinking_sphinx/search.rb +++ b/lib/thinking_sphinx/search.rb @@ -1,11 +1,23 @@ +# frozen_string_literal: true + class ThinkingSphinx::Search < Array CORE_METHODS = %w( == class class_eval extend frozen? id instance_eval - instance_of? instance_values instance_variable_defined? + instance_exec instance_of? instance_values instance_variable_defined? instance_variable_get instance_variable_set instance_variables is_a? kind_of? member? method methods nil? object_id respond_to? respond_to_missing? send should should_not type ) SAFE_METHODS = %w( partition private_methods protected_methods public_methods send class ) + KNOWN_OPTIONS = ( + [ + :classes, :conditions, :excerpts, :geo, :group_by, :ids_only, + :ignore_scopes, :indices, :limit, :masks, :max_matches, :middleware, + :none, :offset, :order, :order_group_by, :page, :per_page, :populate, + :retry_stale, :select, :skip_sti, :sql, :star, :with, :with_all, :without, + :without_ids + ] + + ThinkingSphinx::Middlewares::SphinxQL::SELECT_OPTIONS + ).uniq DEFAULT_MASKS = [ ThinkingSphinx::Masks::PaginationMask, ThinkingSphinx::Masks::ScopesMask, @@ -21,6 +33,12 @@ class ThinkingSphinx::Search < Array attr_reader :options attr_accessor :query + def self.valid_options + @valid_options + end + + @valid_options = KNOWN_OPTIONS.dup + def initialize(query = nil, options = {}) query, options = nil, query if query.is_a?(Hash) @query, @options = query, options @@ -38,6 +56,16 @@ def current_page options[:page].to_i end + def marshal_dump + populate + + [@populated, @query, @options, @context] + end + + def marshal_load(array) + @populated, @query, @options, @context = array + end + def masks @masks ||= @options[:masks] || DEFAULT_MASKS.clone end @@ -64,7 +92,7 @@ def per_page(value = nil) def populate return self if @populated - middleware.call [context] + middleware.call [context] unless options[:none] @populated = true self diff --git a/lib/thinking_sphinx/search/batch_inquirer.rb b/lib/thinking_sphinx/search/batch_inquirer.rb index dc6da1879..9ca03cb4e 100644 --- a/lib/thinking_sphinx/search/batch_inquirer.rb +++ b/lib/thinking_sphinx/search/batch_inquirer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Search::BatchInquirer def initialize(&block) @queries = [] diff --git a/lib/thinking_sphinx/search/context.rb b/lib/thinking_sphinx/search/context.rb index 85a3df22f..b1f051b3a 100644 --- a/lib/thinking_sphinx/search/context.rb +++ b/lib/thinking_sphinx/search/context.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Search::Context attr_reader :search, :configuration @@ -5,6 +7,7 @@ def initialize(search, configuration = nil) @search = search @configuration = configuration || ThinkingSphinx::Configuration.instance @memory = { + :raw => [], :results => [], :panes => ThinkingSphinx::Configuration::Defaults::PANES.clone } @@ -17,4 +20,12 @@ def [](key) def []=(key, value) @memory[key] = value end + + def marshal_dump + [@memory.except(:raw, :indices)] + end + + def marshal_load(array) + @memory = array.first + end end diff --git a/lib/thinking_sphinx/search/glaze.rb b/lib/thinking_sphinx/search/glaze.rb index bc8167416..182c231c4 100644 --- a/lib/thinking_sphinx/search/glaze.rb +++ b/lib/thinking_sphinx/search/glaze.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Search::Glaze < BasicObject def initialize(context, object, raw = {}, pane_classes = []) @object, @raw = object, raw diff --git a/lib/thinking_sphinx/search/merger.rb b/lib/thinking_sphinx/search/merger.rb index 4592c411e..bff5e010c 100644 --- a/lib/thinking_sphinx/search/merger.rb +++ b/lib/thinking_sphinx/search/merger.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Search::Merger attr_reader :search diff --git a/lib/thinking_sphinx/search/query.rb b/lib/thinking_sphinx/search/query.rb index a385c31a9..5654442bf 100644 --- a/lib/thinking_sphinx/search/query.rb +++ b/lib/thinking_sphinx/search/query.rb @@ -1,4 +1,6 @@ # encoding: utf-8 +# frozen_string_literal: true + class ThinkingSphinx::Search::Query attr_reader :keywords, :conditions, :star @@ -10,12 +12,18 @@ def to_s (star_keyword(keywords || '') + ' ' + conditions.keys.collect { |key| next if conditions[key].blank? - "@#{key} #{star_keyword conditions[key], key}" + "#{expand_key key} #{star_keyword conditions[key], key}" }.join(' ')).strip end private + def expand_key(key) + return "@#{key}" unless key.is_a?(Array) + + "@(#{key.join(',')})" + end + def star_keyword(keyword, key = nil) return keyword.to_s unless star return keyword.to_s if key.to_s == 'sphinx_internal_class_name' diff --git a/lib/thinking_sphinx/search/stale_ids_exception.rb b/lib/thinking_sphinx/search/stale_ids_exception.rb index 928afa33e..484bea0dd 100644 --- a/lib/thinking_sphinx/search/stale_ids_exception.rb +++ b/lib/thinking_sphinx/search/stale_ids_exception.rb @@ -1,11 +1,15 @@ +# frozen_string_literal: true + class ThinkingSphinx::Search::StaleIdsException < StandardError - attr_reader :ids + attr_reader :ids, :context - def initialize(ids) + def initialize(ids, context) @ids = ids + @context = context end def message - "Record IDs found by Sphinx but not by ActiveRecord : #{ids.join(', ')}" + "Record IDs found by Sphinx but not by ActiveRecord : #{ids.join(', ')}\n" \ + "https://freelancing-gods.com/thinking-sphinx/v5/common_issues.html#record-ids" end end diff --git a/lib/thinking_sphinx/settings.rb b/lib/thinking_sphinx/settings.rb new file mode 100644 index 000000000..621cb3bda --- /dev/null +++ b/lib/thinking_sphinx/settings.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require "pathname" + +class ThinkingSphinx::Settings + ALWAYS_ABSOLUTE = %w[ socket ] + FILE_KEYS = %w[ + indices_location configuration_file bin_path log query_log pid_file + binlog_path snippets_file_prefix sphinxql_state path stopwords wordforms + exceptions global_idf rlp_context rlp_root rlp_environment plugin_dir + lemmatizer_base mysql_ssl_cert mysql_ssl_key mysql_ssl_ca + ].freeze + DEFAULTS = { + "configuration_file" => "config/ENVIRONMENT.sphinx.conf", + "indices_location" => "db/sphinx/ENVIRONMENT", + "pid_file" => "log/ENVIRONMENT.sphinx.pid", + "log" => "log/ENVIRONMENT.searchd.log", + "query_log" => "log/ENVIRONMENT.searchd.query.log", + "binlog_path" => "tmp/binlog/ENVIRONMENT", + "workers" => "threads", + "mysql_encoding" => "utf8", + "maximum_statement_length" => (2 ** 23) - 5, + "real_time_tidy" => false, + "cutoff" => 0 + }.freeze + YAML_SAFE_LOAD = YAML.method(:safe_load).parameters.any? do |parameter| + parameter == [:key, :aliases] + end + + def self.call(configuration) + new(configuration).call + end + + def initialize(configuration) + @configuration = configuration + end + + def call + return defaults unless File.exist? file + + merged.inject({}) do |hash, (key, value)| + if absolute_key?(key) + hash[key] = absolute value + else + hash[key] = value + end + hash + end + end + + private + + attr_reader :configuration + + delegate :framework, :to => :configuration + + def absolute(relative) + return relative if relative.nil? + + real_path File.absolute_path(relative, framework.root) + end + + def absolute_key?(key) + return true if ALWAYS_ABSOLUTE.include?(key) + + merged["absolute_paths"] && file_keys.include?(key) + end + + def defaults + DEFAULTS.inject({}) do |hash, (key, value)| + if value.is_a?(String) + value = value.gsub("ENVIRONMENT", framework.environment) + end + + if FILE_KEYS.include?(key) + hash[key] = absolute value + else + hash[key] = value + end + + hash + end + end + + def file + @file ||= Pathname.new(framework.root).join "config", "thinking_sphinx.yml" + end + + def file_keys + @file_keys ||= FILE_KEYS + (original["file_keys"] || []) + end + + def join(first, last) + return first if last.nil? + + File.join first, last + end + + def merged + @merged ||= defaults.merge original + end + + def original + yaml_contents && yaml_contents[framework.environment] || {} + end + + def real_path(base, nonexistent = nil) + if File.exist?(base) + join File.realpath(base), nonexistent + else + components = File.split base + real_path components.first, join(components.last, nonexistent) + end + end + + def yaml_contents + @yaml_contents ||= begin + input = File.read file + input = ERB.new(input).result if defined?(ERB) + + if YAML_SAFE_LOAD + YAML.safe_load(input, aliases: true) + else + YAML.load(input) + end + end + end +end diff --git a/lib/thinking_sphinx/sinatra.rb b/lib/thinking_sphinx/sinatra.rb index 3dba3c9ef..fe382c7ac 100644 --- a/lib/thinking_sphinx/sinatra.rb +++ b/lib/thinking_sphinx/sinatra.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require 'thinking_sphinx' ActiveSupport.on_load :active_record do - include ThinkingSphinx::ActiveRecord::Base + require 'thinking_sphinx/active_record' end diff --git a/lib/thinking_sphinx/sphinxql.rb b/lib/thinking_sphinx/sphinxql.rb deleted file mode 100644 index fe84e3011..000000000 --- a/lib/thinking_sphinx/sphinxql.rb +++ /dev/null @@ -1,23 +0,0 @@ -module ThinkingSphinx::SphinxQL - mattr_accessor :weight, :group_by, :count - - def self.functions! - self.weight = {:select => 'weight()', :column => 'weight()'} - self.group_by = { - :select => 'groupby() AS sphinx_internal_group', - :column => 'sphinx_internal_group' - } - self.count = { - :select => 'id AS sphinx_document_id, count(DISTINCT sphinx_document_id) AS sphinx_internal_count', - :column => 'sphinx_internal_count' - } - end - - def self.variables! - self.weight = {:select => '@weight', :column => '@weight'} - self.group_by = {:select => '@groupby', :column => '@groupby'} - self.count = {:select => '@count', :column => '@count'} - end - - self.functions! -end diff --git a/lib/thinking_sphinx/subscribers/populator_subscriber.rb b/lib/thinking_sphinx/subscribers/populator_subscriber.rb index 6fa806183..f113e373f 100644 --- a/lib/thinking_sphinx/subscribers/populator_subscriber.rb +++ b/lib/thinking_sphinx/subscribers/populator_subscriber.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Subscribers::PopulatorSubscriber def self.attach_to(namespace) subscriber = new @@ -16,19 +18,31 @@ def call(message, *args) ActiveSupport::Notifications::Event.new(message, *args) end + def error(event) + error = event.payload[:error].inner_exception + instance = event.payload[:error].instance + + puts <<-MESSAGE + +Error transcribing #{instance.class} #{instance.id}: +#{error.message} + MESSAGE + end + def start_populating(event) puts "Generating index files for #{event.payload[:index].name}" end def populated(event) - print '.' + print '.' * event.payload[:instances].length end def finish_populating(event) print "\n" end -end -ThinkingSphinx::Subscribers::PopulatorSubscriber.attach_to( - 'thinking_sphinx.real_time' -) + private + + delegate :output, :to => ThinkingSphinx + delegate :puts, :print, :to => :output +end diff --git a/lib/thinking_sphinx/tasks.rb b/lib/thinking_sphinx/tasks.rb index 1a59d9e66..2038eb710 100644 --- a/lib/thinking_sphinx/tasks.rb +++ b/lib/thinking_sphinx/tasks.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + namespace :ts do desc 'Generate the Sphinx configuration file' task :configure => :environment do @@ -5,54 +7,79 @@ end desc 'Generate the Sphinx configuration file and process all indices' - task :index => :environment do - interface.index( - ENV['INDEX_ONLY'] != 'true', - !Rake.application.options.silent - ) - end + task :index => ['ts:sql:index', 'ts:rt:index'] desc 'Clear out Sphinx files' - task :clear => :environment do - interface.clear_all - end + task :clear => ['ts:sql:clear', 'ts:rt:clear'] - desc 'Clear out real-time index files' - task :clear_rt => :environment do - interface.clear_real_time - end + desc "Merge all delta indices into their respective core indices" + task :merge => ["ts:sql:merge"] - desc 'Generate fresh index files for real-time indices' - task :generate => :environment do - interface.prepare - interface.generate - end - - desc 'Stop Sphinx, index and then restart Sphinx' - task :rebuild => [:stop, :clear, :index, :start] - - desc 'Stop Sphinx, clear files, reconfigure, start Sphinx, generate files' - task :regenerate => [:stop, :clear_rt, :configure, :start, :generate] + desc 'Delete and regenerate Sphinx files, restart the daemon' + task :rebuild => [ + :stop, :clear, :configure, 'ts:sql:index', :start, 'ts:rt:index' + ] desc 'Restart the Sphinx daemon' task :restart => [:stop, :start] desc 'Start the Sphinx daemon' task :start => :environment do - interface.start + interface.daemon.start end desc 'Stop the Sphinx daemon' task :stop => :environment do - interface.stop + interface.daemon.stop end desc 'Determine whether Sphinx is running' task :status => :environment do - interface.status + interface.daemon.status + end + + namespace :sql do + desc 'Delete SQL-backed Sphinx files' + task :clear => :environment do + interface.sql.clear + end + + desc 'Generate fresh index files for SQL-backed indices' + task :index => :environment do + interface.sql.index(ENV['INDEX_ONLY'] != 'true') + end + + task :merge => :environment do + interface.sql.merge + end + + desc 'Delete and regenerate SQL-backed Sphinx files, restart the daemon' + task :rebuild => ['ts:stop', 'ts:sql:clear', 'ts:sql:index', 'ts:start'] + end + + namespace :rt do + desc 'Delete real-time Sphinx files' + task :clear => :environment do + interface.rt.clear + end + + desc 'Generate fresh index files for real-time indices' + task :index => :environment do + interface.rt.index + end + + desc 'Delete and regenerate real-time Sphinx files, restart the daemon' + task :rebuild => [ + 'ts:stop', 'ts:rt:clear', 'ts:configure', 'ts:start', 'ts:rt:index' + ] end def interface - @interface ||= ThinkingSphinx::RakeInterface.new + @interface ||= ThinkingSphinx.rake_interface.new( + :verbose => Rake::FileUtilsExt.verbose_flag, + :silent => Rake.application.options.silent, + :nodetach => (ENV['NODETACH'] == 'true'), + :index_names => ENV.fetch('INDEX_FILTER', '').split(',') + ) end end diff --git a/lib/thinking_sphinx/test.rb b/lib/thinking_sphinx/test.rb index 0fd150e78..8e4abdcc4 100644 --- a/lib/thinking_sphinx/test.rb +++ b/lib/thinking_sphinx/test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Test def self.init(suppress_delta_output = true) FileUtils.mkdir_p config.indices_location @@ -40,7 +42,7 @@ def self.clear config.indices_location, config.searchd.binlog_path ].each do |path| - FileUtils.rm_r(path) if File.exists?(path) + FileUtils.rm_rf(path) if File.exist?(path) end end diff --git a/lib/thinking_sphinx/utf8.rb b/lib/thinking_sphinx/utf8.rb index 08f157016..6a6c4e260 100644 --- a/lib/thinking_sphinx/utf8.rb +++ b/lib/thinking_sphinx/utf8.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::UTF8 attr_reader :string diff --git a/lib/thinking_sphinx/wildcard.rb b/lib/thinking_sphinx/wildcard.rb index 57e6b87a2..2027e7ef9 100644 --- a/lib/thinking_sphinx/wildcard.rb +++ b/lib/thinking_sphinx/wildcard.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ThinkingSphinx::Wildcard DEFAULT_TOKEN = /\p{Word}+/ @@ -14,7 +16,7 @@ def call query.gsub(extended_pattern) do pre, proper, post = $`, $&, $' # E.g. "@foo", "/2", "~3", but not as part of a token pattern - is_operator = pre == '@' || + is_operator = pre.match(%r{@$}) || pre.match(%r{([^\\]+|\A)[~/]\Z}) || pre.match(%r{(\W|^)@\([^\)]*$}) # E.g. "foo bar", with quotes diff --git a/lib/thinking_sphinx/with_output.rb b/lib/thinking_sphinx/with_output.rb new file mode 100644 index 000000000..b253b9ce6 --- /dev/null +++ b/lib/thinking_sphinx/with_output.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module ThinkingSphinx::WithOutput + def initialize(configuration, options = {}, stream = STDOUT) + @configuration = configuration + @options = options + @stream = stream + end + + private + + attr_reader :configuration, :options, :stream +end diff --git a/spec/acceptance/association_scoping_spec.rb b/spec/acceptance/association_scoping_spec.rb index 1579bbfce..3b78861e8 100644 --- a/spec/acceptance/association_scoping_spec.rb +++ b/spec/acceptance/association_scoping_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Scoping association search calls by foreign keys', :live => true do @@ -9,7 +11,7 @@ dublin = Article.create :title => 'Guide to Dublin', :user => paul index - pat.articles.search('Guide').to_a.should == [melbourne] + expect(pat.articles.search('Guide').to_a).to eq([melbourne]) end it "limits id-only results to those matching the foreign key" do @@ -19,7 +21,7 @@ dublin = Article.create :title => 'Guide to Dublin', :user => paul index - pat.articles.search_for_ids('Guide').to_a.should == [melbourne.id] + expect(pat.articles.search_for_ids('Guide').to_a).to eq([melbourne.id]) end end @@ -31,7 +33,7 @@ audi = Manufacturer.create :name => 'Audi' r_eight = Car.create :name => 'R8 Spyder', :manufacturer => audi - porsche.cars.search('Spyder').to_a.should == [spyder] + expect(porsche.cars.search('Spyder').to_a).to eq([spyder]) end it "limits id-only results to those matching the foreign key" do @@ -41,7 +43,7 @@ audi = Manufacturer.create :name => 'Audi' r_eight = Car.create :name => 'R8 Spyder', :manufacturer => audi - porsche.cars.search_for_ids('Spyder').to_a.should == [spyder.id] + expect(porsche.cars.search_for_ids('Spyder').to_a).to eq([spyder.id]) end end @@ -57,7 +59,7 @@ pancakes.categories << flat waffles.categories << food - flat.products.search('Low').to_a.should == [pancakes] + expect(flat.products.search('Low').to_a).to eq([pancakes]) end end end diff --git a/spec/acceptance/attribute_access_spec.rb b/spec/acceptance/attribute_access_spec.rb index ae0c82ca5..10d4ef775 100644 --- a/spec/acceptance/attribute_access_spec.rb +++ b/spec/acceptance/attribute_access_spec.rb @@ -1,41 +1,58 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Accessing attributes directly via search results', :live => true do it "allows access to attribute values" do - Book.create! :title => 'American Gods', :year => 2001 + Book.create! :title => 'American Gods', :publishing_year => 2001 index search = Book.search('gods') search.context[:panes] << ThinkingSphinx::Panes::AttributesPane - search.first.sphinx_attributes['year'].should == 2001 + expect(search.first.sphinx_attributes['publishing_year']).to eq(2001) end it "provides direct access to the search weight/relevance scores" do - Book.create! :title => 'American Gods', :year => 2001 + Book.create! :title => 'American Gods', :publishing_year => 2001 index - search = Book.search 'gods', - :select => "*, #{ThinkingSphinx::SphinxQL.weight[:select]}" + search = Book.search 'gods', :select => "*, weight()" + search.context[:panes] << ThinkingSphinx::Panes::WeightPane + + if ENV["SPHINX_ENGINE"] == "sphinx" && ENV["SPHINX_VERSION"].to_f > 3.3 + expect(search.first.weight).to eq(20_000.0) + else + expect(search.first.weight).to eq(2500) + end + end + + it "provides direct access to the weight with alternative primary keys" do + album = Album.create! :name => 'Sing to the Moon', :artist => 'Laura Mvula' + + search = Album.search 'sing', :select => "*, weight()" search.context[:panes] << ThinkingSphinx::Panes::WeightPane - search.first.weight.should == 2500 + expect(search.first.weight).to be >= 1000 end it "can enumerate with the weight" do - gods = Book.create! :title => 'American Gods', :year => 2001 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 index - search = Book.search 'gods', - :select => "*, #{ThinkingSphinx::SphinxQL.weight[:select]}" + search = Book.search 'gods', :select => "*, weight()" search.masks << ThinkingSphinx::Masks::WeightEnumeratorMask - expectations = [[gods, 2500]] + if ENV["SPHINX_ENGINE"] == "sphinx" && ENV["SPHINX_VERSION"].to_f > 3.3 + expectations = [[gods, 20_000.0]] + else + expectations = [[gods, 2500]] + end search.each_with_weight do |result, weight| expectation = expectations.shift - result.should == expectation.first - weight.should == expectation.last + expect(result).to eq(expectation.first) + expect(weight).to eq(expectation.last) end end end diff --git a/spec/acceptance/attribute_updates_spec.rb b/spec/acceptance/attribute_updates_spec.rb index 1576e7706..830c25b47 100644 --- a/spec/acceptance/attribute_updates_spec.rb +++ b/spec/acceptance/attribute_updates_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Update attributes automatically where possible', :live => true do @@ -5,12 +7,12 @@ article = Article.create :title => 'Pancakes', :published => false index - Article.search('pancakes', :with => {:published => true}).should be_empty + expect(Article.search('pancakes', :with => {:published => true})).to be_empty article.published = true article.save - Article.search('pancakes', :with => {:published => true}).to_a - .should == [article] + expect(Article.search('pancakes', :with => {:published => true}).to_a) + .to eq([article]) end end diff --git a/spec/acceptance/batch_searching_spec.rb b/spec/acceptance/batch_searching_spec.rb index 731cc1f0e..e5a72acfe 100644 --- a/spec/acceptance/batch_searching_spec.rb +++ b/spec/acceptance/batch_searching_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Executing multiple searches in one Sphinx call', :live => true do @@ -12,10 +14,10 @@ batch.populate - batch.searches.first.should include(pancakes) - batch.searches.first.should_not include(waffles) + expect(batch.searches.first).to include(pancakes) + expect(batch.searches.first).not_to include(waffles) - batch.searches.last.should include(waffles) - batch.searches.last.should_not include(pancakes) + expect(batch.searches.last).to include(waffles) + expect(batch.searches.last).not_to include(pancakes) end end diff --git a/spec/acceptance/big_integers_spec.rb b/spec/acceptance/big_integers_spec.rb index 2106aaa98..3f097a515 100644 --- a/spec/acceptance/big_integers_spec.rb +++ b/spec/acceptance/big_integers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe '64 bit integer support' do @@ -21,17 +23,17 @@ [small_index, large_index, real_time_index] ).reconcile - large_index.sources.first.attributes.detect { |attribute| + expect(large_index.sources.first.attributes.detect { |attribute| attribute.name == 'sphinx_internal_id' - }.type.should == :bigint + }.type).to eq(:bigint) - small_index.sources.first.attributes.detect { |attribute| + expect(small_index.sources.first.attributes.detect { |attribute| attribute.name == 'sphinx_internal_id' - }.type.should == :bigint + }.type).to eq(:bigint) - real_time_index.attributes.detect { |attribute| + expect(real_time_index.attributes.detect { |attribute| attribute.name == 'sphinx_internal_id' - }.type.should == :bigint + }.type).to eq(:bigint) end end @@ -50,7 +52,7 @@ context 'with Real-Time' do it 'handles large 32 bit integers with an offset multiplier' do product = Product.create! :name => "Widget" - product.update_attribute :id, 980190962 + product.update :id => 980190962 expect( Product.search('widget', :indices => ['product_core']).to_a ).to eq([product]) diff --git a/spec/acceptance/excerpts_spec.rb b/spec/acceptance/excerpts_spec.rb index abc721fc0..10aa5b982 100644 --- a/spec/acceptance/excerpts_spec.rb +++ b/spec/acceptance/excerpts_spec.rb @@ -1,29 +1,30 @@ # encoding: utf-8 +# frozen_string_literal: true require 'acceptance/spec_helper' describe 'Accessing excerpts for methods on a search result', :live => true do it "returns excerpts for a given method" do - Book.create! :title => 'American Gods', :year => 2001 + Book.create! :title => 'American Gods', :publishing_year => 2001 index search = Book.search('gods') search.context[:panes] << ThinkingSphinx::Panes::ExcerptsPane - search.first.excerpts.title. - should == 'American Gods' + expect(search.first.excerpts.title). + to eq('American Gods') end it "handles UTF-8 text for excerpts" do - Book.create! :title => 'Война и миръ', :year => 1869 + Book.create! :title => 'Война и миръ', :publishing_year => 1869 index search = Book.search 'миръ' search.context[:panes] << ThinkingSphinx::Panes::ExcerptsPane - search.first.excerpts.title. - should == 'Война и миръ' - end + expect(search.first.excerpts.title). + to eq('Война и миръ') + end if ENV['SPHINX_VERSION'].try :[], /2.2.\d/ it "does not include class names in excerpts" do Book.create! :title => 'The Graveyard Book' @@ -32,8 +33,8 @@ search = Book.search('graveyard') search.context[:panes] << ThinkingSphinx::Panes::ExcerptsPane - search.first.excerpts.title. - should == 'The Graveyard Book' + expect(search.first.excerpts.title). + to eq('The Graveyard Book') end it "respects the star option with queries" do @@ -43,7 +44,7 @@ search = Article.search('thin', :star => true) search.context[:panes] << ThinkingSphinx::Panes::ExcerptsPane - search.first.excerpts.title. - should == 'Something' + expect(search.first.excerpts.title). + to eq('Something') end end diff --git a/spec/acceptance/facets_spec.rb b/spec/acceptance/facets_spec.rb index 5c49467c5..e2102833c 100644 --- a/spec/acceptance/facets_spec.rb +++ b/spec/acceptance/facets_spec.rb @@ -1,4 +1,5 @@ # encoding: utf-8 +# frozen_string_literal: true require 'acceptance/spec_helper' @@ -16,9 +17,9 @@ Tee.create! :colour => green index - Tee.facets.to_hash[:colour_id].should == { + expect(Tee.facets.to_hash[:colour_id]).to eq({ blue.id => 2, red.id => 1, green.id => 3 - } + }) end it "provides facet breakdowns across classes" do @@ -28,11 +29,9 @@ Article.create! index - article_count = ENV['SPHINX_VERSION'].try(:[], /2.0.\d/) ? 2 : 1 - - ThinkingSphinx.facets.to_hash[:class].should == { - 'Tee' => 2, 'City' => 1, 'Article' => article_count - } + expect(ThinkingSphinx.facets.to_hash[:class]).to eq({ + 'Tee' => 2, 'City' => 1, 'Article' => 1 + }) end it "handles field facets" do @@ -42,9 +41,9 @@ Book.create! :title => '1Q84', :author => '村上 春樹' index - Book.facets.to_hash[:author].should == { + expect(Book.facets.to_hash[:author]).to eq({ 'Neil Gaiman' => 2, 'Terry Pratchett' => 1, '村上 春樹' => 1 - } + }) end it "handles MVA facets" do @@ -62,9 +61,9 @@ :tag => pancakes index - User.facets.to_hash[:tag_ids].should == { + expect(User.facets.to_hash[:tag_ids]).to eq({ pancakes.id => 2, waffles.id => 1 - } + }) end it "can filter on integer facet results" do @@ -76,7 +75,7 @@ r1 = Tee.create! :colour => red index - Tee.facets.for(:colour_id => blue.id).to_a.should == [b1, b2] + expect(Tee.facets.for(:colour_id => blue.id).to_a).to eq([b1, b2]) end it "can filter on MVA facet results" do @@ -91,7 +90,7 @@ Tagging.create! :article => Article.create!(:user => u2), :tag => pancakes index - User.facets.for(:tag_ids => waffles.id).to_a.should == [u1] + expect(User.facets.for(:tag_ids => waffles.id).to_a).to eq([u1]) end it "can filter on string facet results" do @@ -100,7 +99,7 @@ snuff = Book.create! :title => 'Snuff', :author => 'Terry Pratchett' index - Book.facets.for(:author => 'Neil Gaiman').to_a.should == [gods, boys] + expect(Book.facets.for(:author => 'Neil Gaiman').to_a).to eq([gods, boys]) end it "allows enumeration" do @@ -119,10 +118,24 @@ [:class, {'Tee' => 3}] ] Tee.facets.each do |facet, hash| - facet.should == expectations[calls].first - hash.should == expectations[calls].last + expect(facet).to eq(expectations[calls].first) + expect(hash).to eq(expectations[calls].last) calls += 1 end end + + it "can be called on distributed indices" do + blue = Colour.create! :name => 'blue' + green = Colour.create! :name => 'green' + + Tee.create! :colour => blue + Tee.create! :colour => blue + Tee.create! :colour => green + index + + expect(Tee.facets(:indices => ["tee"]).to_hash[:colour_id]).to eq({ + blue.id => 2, green.id => 1 + }) + end end diff --git a/spec/acceptance/geosearching_spec.rb b/spec/acceptance/geosearching_spec.rb index a0438f5aa..9f68c2ce9 100644 --- a/spec/acceptance/geosearching_spec.rb +++ b/spec/acceptance/geosearching_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Searching by latitude and longitude', :live => true do @@ -7,8 +9,8 @@ bri = City.create :name => 'Brisbane', :lat => -0.4794031, :lng => 2.670838 index - City.search(:geo => [-0.616241, 2.602712], :order => 'geodist ASC'). - to_a.should == [syd, mel, bri] + expect(City.search(:geo => [-0.616241, 2.602712], :order => 'geodist ASC'). + to_a).to eq([syd, mel, bri]) end it "filters by distance" do @@ -17,10 +19,10 @@ bri = City.create :name => 'Brisbane', :lat => -0.4794031, :lng => 2.670838 index - City.search( + expect(City.search( :geo => [-0.616241, 2.602712], :with => {:geodist => 0.0..470_000.0} - ).to_a.should == [mel, syd] + ).to_a).to eq([mel, syd]) end it "provides the distance for each search result" do @@ -30,16 +32,26 @@ index cities = City.search(:geo => [-0.616241, 2.602712], :order => 'geodist ASC') - if ENV['SPHINX_VERSION'].try :[], /2.2.\d/ + if ENV.fetch('SPHINX_VERSION', '2.1.2').to_f > 2.1 expected = {:mysql => 249907.171875, :postgresql => 249912.03125} else expected = {:mysql => 250326.906250, :postgresql => 250331.234375} end - if ActiveRecord::Base.configurations['test']['adapter'][/postgres/] - cities.first.geodist.should == expected[:postgresql] + adapter = nil + + if ActiveRecord::VERSION::STRING.to_f > 6.0 + adapter = ActiveRecord::Base.configurations.configs_for.first.adapter + elsif ActiveRecord::VERSION::STRING.to_f > 5.2 + adapter = ActiveRecord::Base.configurations.configs_for.first.config["adapter"] + else + adapter = ActiveRecord::Base.configurations['test']['adapter'] + end + + if adapter[/postgres/] + expect(cities.first.geodist).to be_within(0.01).of(expected[:postgresql]) else # mysql - cities.first.geodist.should == expected[:mysql] + expect(cities.first.geodist).to be_within(0.01).of(expected[:mysql]) end end @@ -49,10 +61,10 @@ bri = City.create :name => 'Brisbane', :lat => -0.4794031, :lng => 2.670838 index - City.search( + expect(City.search( :geo => [-0.616241, 2.602712], :with => {:geodist => 0.0..470_000.0}, :select => "*, geodist as custom_weight" - ).to_a.should == [mel, syd] + ).to_a).to eq([mel, syd]) end end diff --git a/spec/acceptance/grouping_by_attributes_spec.rb b/spec/acceptance/grouping_by_attributes_spec.rb index 831517a04..8481f9dca 100644 --- a/spec/acceptance/grouping_by_attributes_spec.rb +++ b/spec/acceptance/grouping_by_attributes_spec.rb @@ -1,77 +1,79 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Grouping search results by attributes', :live => true do it "groups by the provided attribute" do - snuff = Book.create! :title => 'Snuff', :year => 2011 - earth = Book.create! :title => 'The Long Earth', :year => 2012 - dodger = Book.create! :title => 'Dodger', :year => 2012 + snuff = Book.create! :title => 'Snuff', :publishing_year => 2011 + earth = Book.create! :title => 'The Long Earth', :publishing_year => 2012 + dodger = Book.create! :title => 'Dodger', :publishing_year => 2012 index - Book.search(:group_by => :year).to_a.should == [snuff, earth] + expect(Book.search(:group_by => :publishing_year).to_a).to eq([snuff, earth]) end it "allows sorting within the group" do - snuff = Book.create! :title => 'Snuff', :year => 2011 - earth = Book.create! :title => 'The Long Earth', :year => 2012 - dodger = Book.create! :title => 'Dodger', :year => 2012 + snuff = Book.create! :title => 'Snuff', :publishing_year => 2011 + earth = Book.create! :title => 'The Long Earth', :publishing_year => 2012 + dodger = Book.create! :title => 'Dodger', :publishing_year => 2012 index - Book.search(:group_by => :year, :order_group_by => 'title ASC').to_a. - should == [snuff, dodger] + expect(Book.search(:group_by => :publishing_year, :order_group_by => 'title ASC').to_a). + to eq([snuff, dodger]) end it "allows enumerating by count" do - snuff = Book.create! :title => 'Snuff', :year => 2011 - earth = Book.create! :title => 'The Long Earth', :year => 2012 - dodger = Book.create! :title => 'Dodger', :year => 2012 + snuff = Book.create! :title => 'Snuff', :publishing_year => 2011 + earth = Book.create! :title => 'The Long Earth', :publishing_year => 2012 + dodger = Book.create! :title => 'Dodger', :publishing_year => 2012 index expectations = [[snuff, 1], [earth, 2]] - Book.search(:group_by => :year).each_with_count do |book, count| + Book.search(:group_by => :publishing_year).each_with_count do |book, count| expectation = expectations.shift - book.should == expectation.first - count.should == expectation.last + expect(book).to eq(expectation.first) + expect(count).to eq(expectation.last) end end it "allows enumerating by group" do - snuff = Book.create! :title => 'Snuff', :year => 2011 - earth = Book.create! :title => 'The Long Earth', :year => 2012 - dodger = Book.create! :title => 'Dodger', :year => 2012 + snuff = Book.create! :title => 'Snuff', :publishing_year => 2011 + earth = Book.create! :title => 'The Long Earth', :publishing_year => 2012 + dodger = Book.create! :title => 'Dodger', :publishing_year => 2012 index expectations = [[snuff, 2011], [earth, 2012]] - Book.search(:group_by => :year).each_with_group do |book, group| + Book.search(:group_by => :publishing_year).each_with_group do |book, group| expectation = expectations.shift - book.should == expectation.first - group.should == expectation.last + expect(book).to eq(expectation.first) + expect(group).to eq(expectation.last) end end it "allows enumerating by group and count" do - snuff = Book.create! :title => 'Snuff', :year => 2011 - earth = Book.create! :title => 'The Long Earth', :year => 2012 - dodger = Book.create! :title => 'Dodger', :year => 2012 + snuff = Book.create! :title => 'Snuff', :publishing_year => 2011 + earth = Book.create! :title => 'The Long Earth', :publishing_year => 2012 + dodger = Book.create! :title => 'Dodger', :publishing_year => 2012 index expectations = [[snuff, 2011, 1], [earth, 2012, 2]] - search = Book.search(:group_by => :year) + search = Book.search(:group_by => :publishing_year) search.each_with_group_and_count do |book, group, count| expectation = expectations.shift - book.should == expectation[0] - group.should == expectation[1] - count.should == expectation[2] + expect(book).to eq(expectation[0]) + expect(group).to eq(expectation[1]) + expect(count).to eq(expectation[2]) end end end diff --git a/spec/acceptance/index_options_spec.rb b/spec/acceptance/index_options_spec.rb index 1491453a9..1634f990e 100644 --- a/spec/acceptance/index_options_spec.rb +++ b/spec/acceptance/index_options_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Index options' do @@ -14,11 +16,11 @@ end it "keeps #{type}_fields blank" do - index.send("#{type}_fields").should be_nil + expect(index.send("#{type}_fields")).to be_nil end it "sets min_#{type}_len" do - index.send("min_#{type}_len").should == 3 + expect(index.send("min_#{type}_len")).to eq(3) end end @@ -33,11 +35,11 @@ end it "#{type}_fields should contain the field" do - index.send("#{type}_fields").should == 'title' + expect(index.send("#{type}_fields")).to eq('title') end it "sets min_#{type}_len" do - index.send("min_#{type}_len").should == 3 + expect(index.send("min_#{type}_len")).to eq(3) end end end @@ -57,12 +59,12 @@ end it "stores each source definition" do - index.sources.length.should == 2 + expect(index.sources.length).to eq(2) end it "treats each source as separate" do - index.sources.first.fields.length.should == 2 - index.sources.last.fields.length.should == 3 + expect(index.sources.first.fields.length).to eq(2) + expect(index.sources.last.fields.length).to eq(3) end end @@ -77,11 +79,11 @@ end it "declares wordcount fields" do - index.sources.first.sql_field_str2wordcount.should == ['title'] + expect(index.sources.first.sql_field_str2wordcount).to eq(['title']) end it "declares wordcount attributes" do - index.sources.first.sql_attr_str2wordcount.should == ['content'] + expect(index.sources.first.sql_attr_str2wordcount).to eq(['content']) end end @@ -98,15 +100,15 @@ end it "allows for core source settings" do - index.sources.first.sql_range_step.should == 5 + expect(index.sources.first.sql_range_step).to eq(5) end it "allows for source options" do - index.sources.first.disable_range?.should be_true + expect(index.sources.first.disable_range?).to be_truthy end it "respects sql_query_pre values" do - index.sources.first.sql_query_pre.should include("DO STUFF") + expect(index.sources.first.sql_query_pre).to include("DO STUFF") end end @@ -130,23 +132,23 @@ end it "prioritises index-level options over YAML options" do - index.min_infix_len.should == 1 + expect(index.min_infix_len).to eq(1) end it "prioritises index-level source options" do - index.sources.first.sql_range_step.should == 20 + expect(index.sources.first.sql_range_step).to eq(20) end it "keeps index-level options prioritised when rendered again" do index.render - index.min_infix_len.should == 1 + expect(index.min_infix_len).to eq(1) end it "keeps index-level options prioritised when rendered again" do index.render - index.sources.first.sql_range_step.should == 20 + expect(index.sources.first.sql_range_step).to eq(20) end end end diff --git a/spec/acceptance/indexing_spec.rb b/spec/acceptance/indexing_spec.rb index 09dd96455..d2182ee5a 100644 --- a/spec/acceptance/indexing_spec.rb +++ b/spec/acceptance/indexing_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Indexing', :live => true do @@ -8,7 +10,7 @@ article = Article.create! :title => 'Pancakes' index 'article_core' - Article.search.should be_empty + expect(Article.search).to be_empty FileUtils.rm path end @@ -20,7 +22,7 @@ article = Article.create! :title => 'Pancakes' index 'article_core' - Article.search.should_not be_empty + expect(Article.search).not_to be_empty FileUtils.rm path end @@ -31,6 +33,6 @@ index 'article_core' file = Rails.root.join('db/sphinx/test/ts-article_core.tmp') - File.exist?(file).should be_false + expect(File.exist?(file)).to be_falsey end end diff --git a/spec/acceptance/merging_spec.rb b/spec/acceptance/merging_spec.rb new file mode 100644 index 000000000..7e69e8a16 --- /dev/null +++ b/spec/acceptance/merging_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require "acceptance/spec_helper" + +describe "Merging deltas", :live => true do + it "merges in new records" do + guards = Book.create( + :title => "Guards! Guards!", :author => "Terry Pratchett" + ) + sleep 0.25 + + expect( + Book.search("Terry Pratchett", :indices => ["book_delta"]).to_a + ).to eq([guards]) + expect( + Book.search("Terry Pratchett", :indices => ["book_core"]).to_a + ).to be_empty + + merge + guards.reload + + expect( + Book.search("Terry Pratchett", :indices => ["book_core"]).to_a + ).to eq([guards]) + expect(guards.delta).to eq(false) + end + + it "merges in changed records" do + race = Book.create( + :title => "The Hate Space", :author => "Maxine Beneba Clarke" + ) + index + expect( + Book.search("Space", :indices => ["book_core"]).to_a + ).to eq([race]) + + race.reload.update :title => "The Hate Race" + sleep 0.25 + expect( + Book.search("Race", :indices => ["book_delta"]).to_a + ).to eq([race]) + expect( + Book.search("Race", :indices => ["book_core"]).to_a + ).to be_empty + + merge + race.reload + + expect( + Book.search("Race", :indices => ["book_core"]).to_a + ).to eq([race]) + expect( + Book.search("Race", :indices => ["book_delta"]).to_a + ).to eq([race]) + expect( + Book.search("Space", :indices => ["book_core"]).to_a + ).to be_empty + expect(race.delta).to eq(false) + end + + it "maintains existing records" do + race = Book.create( + :title => "The Hate Race", :author => "Maxine Beneba Clarke" + ) + index + + soil = Book.create( + :title => "Foreign Soil", :author => "Maxine Beneba Clarke" + ) + sleep 0.25 + expect( + Book.search("Soil", :indices => ["book_delta"]).to_a + ).to eq([soil]) + expect( + Book.search("Soil", :indices => ["book_core"]).to_a + ).to be_empty + expect( + Book.search("Race", :indices => ["book_core"]).to_a + ).to eq([race]) + + merge + + expect( + Book.search("Soil", :indices => ["book_core"]).to_a + ).to eq([soil]) + expect( + Book.search("Race", :indices => ["book_core"]).to_a + ).to eq([race]) + end +end diff --git a/spec/acceptance/paginating_search_results_spec.rb b/spec/acceptance/paginating_search_results_spec.rb index 87f1fc5d6..df085f432 100644 --- a/spec/acceptance/paginating_search_results_spec.rb +++ b/spec/acceptance/paginating_search_results_spec.rb @@ -1,24 +1,42 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Paginating search results', :live => true do it "tracks how many results there are in total" do + expect(Article.search.total_entries).to be_zero + 21.times { |number| Article.create :title => "Article #{number}" } index - Article.search.total_entries.should == 21 + if ENV["SPHINX_ENGINE"] == "manticore" && ENV["SPHINX_VERSION"].to_f >= 4.0 + # I suspect this is a bug in Manticore? + expect(Article.search.total_entries).to eq(22) + else + expect(Article.search.total_entries).to eq(21) + end end it "paginates the result set by default" do + expect(Article.search.total_entries).to be_zero + 21.times { |number| Article.create :title => "Article #{number}" } index - Article.search.length.should == 20 + expect(Article.search.length).to eq(20) end it "tracks the number of pages" do + expect(Article.search.total_entries).to be_zero + 21.times { |number| Article.create :title => "Article #{number}" } index - Article.search.total_pages.should == 2 + if ENV["SPHINX_ENGINE"] == "manticore" && ENV["SPHINX_VERSION"].to_f >= 4.0 + # I suspect this is a bug in Manticore? + expect(Article.search.total_pages).to eq(1) + else + expect(Article.search.total_pages).to eq(2) + end end end diff --git a/spec/acceptance/real_time_updates_spec.rb b/spec/acceptance/real_time_updates_spec.rb index 57d3d273a..8eca41b6c 100644 --- a/spec/acceptance/real_time_updates_spec.rb +++ b/spec/acceptance/real_time_updates_spec.rb @@ -1,17 +1,115 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Updates to records in real-time indices', :live => true do it "handles fields with unicode nulls" do product = Product.create! :name => "Widget \u0000" - Product.search.first.should == product - end + expect(Product.search.first).to eq(product) + end unless ENV['DATABASE'] == 'postgresql' it "handles attributes for sortable fields accordingly" do product = Product.create! :name => 'Red Fish' - product.update_attributes :name => 'Blue Fish' + product.update :name => 'Blue Fish' + + expect(Product.search('blue fish', :indices => ['product_core']).to_a). + to eq([product]) + end + + it "handles inserts and updates for namespaced models" do + person = Admin::Person.create :name => 'Death' + + expect(Admin::Person.search('Death').to_a).to eq([person]) + + person.update :name => 'Mort' + + expect(Admin::Person.search('Death').to_a).to be_empty + expect(Admin::Person.search('Mort').to_a).to eq([person]) + end + + it "can use direct interface for upserting records" do + Admin::Person.connection.execute <<~SQL + INSERT INTO admin_people (name, created_at, updated_at) + VALUES ('Pat', now(), now()); + SQL + + expect(Admin::Person.search('Pat').to_a).to be_empty + + instance = Admin::Person.find_by(:name => 'Pat') + ThinkingSphinx::Processor.new(instance: instance).upsert + + expect(Admin::Person.search('Pat').to_a).to eq([instance]) + + Admin::Person.connection.execute <<~SQL + UPDATE admin_people SET name = 'Patrick' WHERE name = 'Pat'; + SQL + + expect(Admin::Person.search('Patrick').to_a).to be_empty + + instance.reload + ThinkingSphinx::Processor.new(model: Admin::Person, id: instance.id).upsert + + expect(Admin::Person.search('Patrick').to_a).to eq([instance]) + end + + it "can use direct interface for processing records outside scope" do + Article.connection.execute <<~SQL + INSERT INTO articles (title, published, created_at, updated_at) + VALUES ('Nice Title', TRUE, now(), now()); + SQL + + article = Article.last + + ThinkingSphinx::Processor.new(model: article.class, id: article.id).sync + + expect(ThinkingSphinx.search('Nice', :indices => ["published_articles_core"])).to include(article) + + Article.connection.execute <<~SQL + UPDATE articles SET published = FALSE WHERE title = 'Nice Title'; + SQL + ThinkingSphinx::Processor.new(model: article.class, id: article.id).sync + + expect(ThinkingSphinx.search('Nice', :indices => ["published_articles_core"])).to be_empty + end + + it "can use direct interface for processing deleted records" do + Article.connection.execute <<~SQL + INSERT INTO articles (title, published, created_at, updated_at) + VALUES ('Nice Title', TRUE, now(), now()); + SQL + + article = Article.last + ThinkingSphinx::Processor.new(:instance => article).sync + + expect(ThinkingSphinx.search('Nice', :indices => ["published_articles_core"])).to include(article) + + Article.connection.execute <<~SQL + DELETE FROM articles where title = 'Nice Title'; + SQL + + ThinkingSphinx::Processor.new(:instance => article).sync + + expect(ThinkingSphinx.search('Nice', :indices => ["published_articles_core"])).to be_empty + end + + it "syncs records in real-time index with alternate ids" do + Album.connection.execute <<~SQL + INSERT INTO albums (id, name, artist, integer_id) + VALUES ('#{("a".."z").to_a.sample}', 'Sing to the Moon', 'Laura Mvula', #{rand(10000)}); + SQL + + album = Album.last + ThinkingSphinx::Processor.new(:model => Album, id: album.integer_id).sync + + expect(ThinkingSphinx.search('Laura', :indices => ["album_real_core"])).to include(album) + + Article.connection.execute <<~SQL + DELETE FROM albums where id = '#{album.id}'; + SQL + + ThinkingSphinx::Processor.new(:instance => album).sync - Product.search('blue fish', :indices => ['product_core']).to_a. - should == [product] + expect(ThinkingSphinx.search('Laura', :indices => ["album_real_core"])).to be_empty end end diff --git a/spec/acceptance/remove_deleted_records_spec.rb b/spec/acceptance/remove_deleted_records_spec.rb index 1101afd50..b4146751c 100644 --- a/spec/acceptance/remove_deleted_records_spec.rb +++ b/spec/acceptance/remove_deleted_records_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Hiding deleted records from search results', :live => true do @@ -5,32 +7,62 @@ pancakes = Article.create! :title => 'Pancakes' index - Article.search('pancakes').should_not be_empty + expect(Article.search('pancakes')).not_to be_empty pancakes.destroy - Article.search('pancakes').should be_empty + expect(Article.search('pancakes')).to be_empty end it "will catch stale records deleted without callbacks being fired" do pancakes = Article.create! :title => 'Pancakes' index - Article.search('pancakes').should_not be_empty + expect(Article.search('pancakes')).not_to be_empty Article.connection.execute "DELETE FROM articles WHERE id = #{pancakes.id}" - Article.search('pancakes').should be_empty + expect(Article.search('pancakes')).to be_empty end it "removes records from real-time index results" do product = Product.create! :name => 'Shiny' - Product.search('Shiny', :indices => ['product_core']).to_a. - should == [product] + expect(Product.search('Shiny', :indices => ['product_core']).to_a). + to eq([product]) + + product.destroy + + expect(Product.search_for_ids('Shiny', :indices => ['product_core'])). + to be_empty + end + + it "removes records from real-time index results with alternate ids" do + album = Album.create! :name => 'Sing to the Moon', :artist => 'Laura Mvula' + + expect(Album.search('Sing', :indices => ['album_real_core']).to_a). + to eq([album]) + + album.destroy + + expect(Album.search_for_ids('Sing', :indices => ['album_real_core'])). + to be_empty + end + + it "does not remove real-time results when callbacks are disabled" do + original = ThinkingSphinx::Configuration.instance. + settings['real_time_callbacks'] + product = Product.create! :name => 'Shiny' + expect(Product.search('Shiny', :indices => ['product_core']).to_a). + to eq([product]) + + ThinkingSphinx::Configuration.instance. + settings['real_time_callbacks'] = false product.destroy + expect(Product.search_for_ids('Shiny', :indices => ['product_core'])). + not_to be_empty - Product.search_for_ids('Shiny', :indices => ['product_core']). - should be_empty + ThinkingSphinx::Configuration.instance. + settings['real_time_callbacks'] = original end it "deletes STI child classes from parent indices" do @@ -40,4 +72,28 @@ expect(Bird.search_for_ids('duck')).to be_empty end + + it "can use a direct interface for processing records" do + pancakes = Article.create! :title => 'Pancakes' + index + expect(Article.search('pancakes')).not_to be_empty + + Article.connection.execute "DELETE FROM articles WHERE id = #{pancakes.id}" + expect(Article.search_for_ids('pancakes')).not_to be_empty + + ThinkingSphinx::Processor.new(instance: pancakes).delete + expect(Article.search_for_ids('pancakes')).to be_empty + end + + it "can use a direct interface for processing records without an instance" do + pancakes = Article.create! :title => 'Pancakes' + index + expect(Article.search('pancakes')).not_to be_empty + + Article.connection.execute "DELETE FROM articles WHERE id = #{pancakes.id}" + expect(Article.search_for_ids('pancakes')).not_to be_empty + + ThinkingSphinx::Processor.new(model: Article, id: pancakes.id).delete + expect(Article.search_for_ids('pancakes')).to be_empty + end end diff --git a/spec/acceptance/search_counts_spec.rb b/spec/acceptance/search_counts_spec.rb index 39798bb79..1fe7be3e8 100644 --- a/spec/acceptance/search_counts_spec.rb +++ b/spec/acceptance/search_counts_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Get search result counts', :live => true do @@ -5,7 +7,7 @@ 4.times { |i| Article.create :title => "Article #{i}" } index - Article.search_count.should == 4 + expect(Article.search_count).to eq(4) end it "returns counts across all models" do @@ -13,6 +15,6 @@ 2.times { |i| Book.create :title => "Book #{i}" } index - ThinkingSphinx.count.should == 5 + expect(ThinkingSphinx.count).to eq(5) end end diff --git a/spec/acceptance/search_for_just_ids_spec.rb b/spec/acceptance/search_for_just_ids_spec.rb index 8b9c69a6e..8862dbd30 100644 --- a/spec/acceptance/search_for_just_ids_spec.rb +++ b/spec/acceptance/search_for_just_ids_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Searching for just instance Ids', :live => true do @@ -6,7 +8,7 @@ waffles = Article.create! :title => 'Waffles' index - Article.search_for_ids('pancakes').to_a.should == [pancakes.id] + expect(Article.search_for_ids('pancakes').to_a).to eq([pancakes.id]) end it "works across the global context" do @@ -14,6 +16,6 @@ book = Book.create! :title => 'American Gods' index - ThinkingSphinx.search_for_ids.to_a.should =~ [article.id, book.id] + expect(ThinkingSphinx.search_for_ids.to_a).to match_array([article.id, book.id]) end end diff --git a/spec/acceptance/searching_across_models_spec.rb b/spec/acceptance/searching_across_models_spec.rb index 626a3f87a..e471fcd71 100644 --- a/spec/acceptance/searching_across_models_spec.rb +++ b/spec/acceptance/searching_across_models_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Searching across models', :live => true do @@ -5,7 +7,7 @@ article = Article.create! :title => 'Pancakes' index - ThinkingSphinx.search.first.should == article + expect(ThinkingSphinx.search.first).to eq(article) end it "returns results matching the given query" do @@ -14,8 +16,8 @@ index articles = ThinkingSphinx.search 'pancakes' - articles.should include(pancakes) - articles.should_not include(waffles) + expect(articles).to include(pancakes) + expect(articles).not_to include(waffles) end it "handles results from different models" do @@ -23,7 +25,7 @@ book = Book.create! :title => 'American Gods' index - ThinkingSphinx.search.to_a.should =~ [article, book] + expect(ThinkingSphinx.search.to_a).to match_array([article, book]) end it "filters by multiple classes" do @@ -32,7 +34,14 @@ user = User.create! :name => 'Pat' index - ThinkingSphinx.search(:classes => [User, Article]).to_a. - should =~ [article, user] + expect(ThinkingSphinx.search(:classes => [User, Article]).to_a). + to match_array([article, user]) + end + + it "has a 'none' default scope" do + article = Article.create! :title => 'Pancakes' + index + + expect(ThinkingSphinx.none).to be_empty end end diff --git a/spec/acceptance/searching_across_schemas_spec.rb b/spec/acceptance/searching_across_schemas_spec.rb index b50daed6b..68374b59b 100644 --- a/spec/acceptance/searching_across_schemas_spec.rb +++ b/spec/acceptance/searching_across_schemas_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' multi_schema = MultiSchema.new @@ -16,23 +18,23 @@ it 'can distinguish between objects with the same primary key' do multi_schema.switch :public jekyll = Product.create name: 'Doctor Jekyll' - Product.search('Jekyll', :retry_stale => false).to_a.should == [jekyll] - Product.search(:retry_stale => false).to_a.should == [jekyll] + expect(Product.search('Jekyll', :retry_stale => false).to_a).to eq([jekyll]) + expect(Product.search(:retry_stale => false).to_a).to eq([jekyll]) multi_schema.switch :thinking_sphinx hyde = Product.create name: 'Mister Hyde' - Product.search('Jekyll', :retry_stale => false).to_a.should == [] - Product.search('Hyde', :retry_stale => false).to_a.should == [hyde] - Product.search(:retry_stale => false).to_a.should == [hyde] + expect(Product.search('Jekyll', :retry_stale => false).to_a).to eq([]) + expect(Product.search('Hyde', :retry_stale => false).to_a).to eq([hyde]) + expect(Product.search(:retry_stale => false).to_a).to eq([hyde]) multi_schema.switch :public - Product.search('Jekyll', :retry_stale => false).to_a.should == [jekyll] - Product.search(:retry_stale => false).to_a.should == [jekyll] - Product.search('Hyde', :retry_stale => false).to_a.should == [] + expect(Product.search('Jekyll', :retry_stale => false).to_a).to eq([jekyll]) + expect(Product.search(:retry_stale => false).to_a).to eq([jekyll]) + expect(Product.search('Hyde', :retry_stale => false).to_a).to eq([]) - Product.search( + expect(Product.search( :middleware => ThinkingSphinx::Middlewares::RAW_ONLY, :indices => ['product_core', 'product_two_core'] - ).to_a.length.should == 2 + ).to_a.length).to eq(2) end end if multi_schema.active? diff --git a/spec/acceptance/searching_on_fields_spec.rb b/spec/acceptance/searching_on_fields_spec.rb index b6e53e93f..a04ab1439 100644 --- a/spec/acceptance/searching_on_fields_spec.rb +++ b/spec/acceptance/searching_on_fields_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Searching on fields', :live => true do @@ -8,8 +10,8 @@ index articles = Article.search :conditions => {:title => 'pancakes'} - articles.should include(pancakes) - articles.should_not include(waffles) + expect(articles).to include(pancakes) + expect(articles).not_to include(waffles) end it "limits results for a field from an association" do @@ -17,7 +19,7 @@ pancakes = Article.create! :title => 'Pancakes', :user => user index - Article.search(:conditions => {:user => 'pat'}).first.should == pancakes + expect(Article.search(:conditions => {:user => 'pat'}).first).to eq(pancakes) end it "returns results with matches from grouped fields" do @@ -26,23 +28,23 @@ waffles = Article.create! :title => 'Waffles', :user => user index - Article.search('waffles', :conditions => {:title => 'pancakes'}).to_a. - should == [pancakes] + expect(Article.search('waffles', :conditions => {:title => 'pancakes'}).to_a). + to eq([pancakes]) end it "returns results with matches from concatenated columns in a field" do book = Book.create! :title => 'Night Watch', :author => 'Terry Pratchett' index - Book.search(:conditions => {:info => 'Night Pratchett'}).to_a. - should == [book] + expect(Book.search(:conditions => {:info => 'Night Pratchett'}).to_a). + to eq([book]) end it "handles NULLs in concatenated fields" do book = Book.create! :title => 'Night Watch' index - Book.search(:conditions => {:info => 'Night Watch'}).to_a.should == [book] + expect(Book.search(:conditions => {:info => 'Night Watch'}).to_a).to eq([book]) end it "returns results with matches from file fields" do @@ -52,6 +54,6 @@ book = Book.create! :title => 'Accelerando', :blurb_file => file_path.to_s index - Book.search('cyberpunk').to_a.should == [book] + expect(Book.search('cyberpunk').to_a).to eq([book]) end end diff --git a/spec/acceptance/searching_with_filters_spec.rb b/spec/acceptance/searching_with_filters_spec.rb index b9c3da02d..199cce20f 100644 --- a/spec/acceptance/searching_with_filters_spec.rb +++ b/spec/acceptance/searching_with_filters_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Searching with filters', :live => true do @@ -6,16 +8,16 @@ waffles = Article.create! :title => 'Waffles', :published => false index - Article.search(:with => {:published => true}).to_a.should == [pancakes] + expect(Article.search(:with => {:published => true}).to_a).to eq([pancakes]) end it "limits results by an array of values" do - gods = Book.create! :title => 'American Gods', :year => 2001 - boys = Book.create! :title => 'Anansi Boys', :year => 2005 - grave = Book.create! :title => 'The Graveyard Book', :year => 2009 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 + boys = Book.create! :title => 'Anansi Boys', :publishing_year => 2005 + grave = Book.create! :title => 'The Graveyard Book', :publishing_year => 2009 index - Book.search(:with => {:year => [2001, 2005]}).to_a.should == [gods, boys] + expect(Book.search(:with => {:publishing_year => [2001, 2005]}).to_a).to match_array([gods, boys]) end it "limits results by a ranged filter" do @@ -28,8 +30,8 @@ grave.update_column :created_at, 1.day.ago index - Book.search(:with => {:created_at => 6.days.ago..2.days.ago}).to_a. - should == [gods, boys] + expect(Book.search(:with => {:created_at => 6.days.ago..2.days.ago}).to_a). + to match_array([gods, boys]) end it "limits results by exclusive filters on single values" do @@ -37,16 +39,16 @@ waffles = Article.create! :title => 'Waffles', :published => false index - Article.search(:without => {:published => true}).to_a.should == [waffles] + expect(Article.search(:without => {:published => true}).to_a).to eq([waffles]) end it "limits results by exclusive filters on arrays of values" do - gods = Book.create! :title => 'American Gods', :year => 2001 - boys = Book.create! :title => 'Anansi Boys', :year => 2005 - grave = Book.create! :title => 'The Graveyard Book', :year => 2009 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 + boys = Book.create! :title => 'Anansi Boys', :publishing_year => 2005 + grave = Book.create! :title => 'The Graveyard Book', :publishing_year => 2009 index - Book.search(:without => {:year => [2001, 2005]}).to_a.should == [grave] + expect(Book.search(:without => {:publishing_year => [2001, 2005]}).to_a).to eq([grave]) end it "limits results by ranged filters on timestamp MVAs" do @@ -64,9 +66,9 @@ index - Article.search( + expect(Article.search( :with => {:taggings_at => 1.days.ago..1.day.from_now} - ).to_a.should == [pancakes] + ).to_a).to eq([pancakes]) end it "takes into account local timezones for timestamps" do @@ -84,9 +86,9 @@ index - Article.search( + expect(Article.search( :with => {:taggings_at => 2.minutes.ago..Time.zone.now} - ).to_a.should == [pancakes] + ).to_a).to eq([pancakes]) end it "limits results with MVAs having all of the given values" do @@ -103,13 +105,13 @@ index articles = Article.search :with_all => {:tag_ids => [food.id, flat.id]} - articles.to_a.should == [pancakes] + expect(articles.to_a).to eq([pancakes]) end it "limits results with MVAs that don't contain all the given values" do # Matching results may have some of the given values, but cannot have all # of them. Certainly an edge case. - pending "SphinxQL doesn't yet support OR in its WHERE clause" + skip "SphinxQL doesn't yet support OR in its WHERE clause" pancakes = Article.create :title => 'Pancakes' waffles = Article.create :title => 'Waffles' @@ -124,7 +126,7 @@ index articles = Article.search :without_all => {:tag_ids => [food.id, flat.id]} - articles.to_a.should == [waffles] + expect(articles.to_a).to eq([waffles]) end it "limits results on real-time indices with multi-value integer attributes" do @@ -139,6 +141,19 @@ waffles.categories << food products = Product.search :with => {:category_ids => [flat.id]} - products.to_a.should == [pancakes] + expect(products.to_a).to eq([pancakes]) end + + it 'searches with real-time JSON attributes' do + pancakes = Product.create :name => 'Pancakes', + :options => {'lemon' => 1, 'sugar' => 1, :number => 3} + waffles = Product.create :name => 'Waffles', + :options => {'chocolate' => 1, 'sugar' => 1, :number => 1} + + products = Product.search :with => {"options.lemon" => 1} + expect(products.to_a).to eq([pancakes]) + + products = Product.search :with => {"options.sugar" => 1} + expect(products.to_a).to match_array([pancakes, waffles]) + end if JSONColumn.call end diff --git a/spec/acceptance/searching_with_sti_spec.rb b/spec/acceptance/searching_with_sti_spec.rb index da4a98170..a37bfe0c7 100644 --- a/spec/acceptance/searching_with_sti_spec.rb +++ b/spec/acceptance/searching_with_sti_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Searching across STI models', :live => true do @@ -6,7 +8,7 @@ duck = Bird.create :name => 'Duck' index - Animal.search(:indices => ['animal_core']).to_a.should == [platypus, duck] + expect(Animal.search(:indices => ['animal_core']).to_a).to eq([platypus, duck]) end it "limits results based on subclasses" do @@ -14,7 +16,7 @@ duck = Bird.create :name => 'Duck' index - Bird.search(:indices => ['animal_core']).to_a.should == [duck] + expect(Bird.search(:indices => ['animal_core']).to_a).to eq([duck]) end it "returns results for deeper subclasses when searching on their parents" do @@ -23,7 +25,7 @@ emu = FlightlessBird.create :name => 'Emu' index - Bird.search(:indices => ['animal_core']).to_a.should == [duck, emu] + expect(Bird.search(:indices => ['animal_core']).to_a).to eq([duck, emu]) end it "returns results for deeper subclasses" do @@ -32,7 +34,7 @@ emu = FlightlessBird.create :name => 'Emu' index - FlightlessBird.search(:indices => ['animal_core']).to_a.should == [emu] + expect(FlightlessBird.search(:indices => ['animal_core']).to_a).to eq([emu]) end it "filters out sibling subclasses" do @@ -41,7 +43,7 @@ otter = Mammal.create :name => 'Otter' index - Bird.search(:indices => ['animal_core']).to_a.should == [duck] + expect(Bird.search(:indices => ['animal_core']).to_a).to eq([duck]) end it "obeys :classes if supplied" do @@ -50,18 +52,18 @@ emu = FlightlessBird.create :name => 'Emu' index - Bird.search( + expect(Bird.search( :indices => ['animal_core'], :skip_sti => true, :classes => [Bird, FlightlessBird] - ).to_a.should == [duck, emu] + ).to_a).to eq([duck, emu]) end it 'finds root objects when type is blank' do animal = Animal.create :name => 'Animal', type: '' index - Animal.search(:indices => ['animal_core']).to_a.should == [animal] + expect(Animal.search(:indices => ['animal_core']).to_a).to eq([animal]) end it 'allows for indices on mid-hierarchy classes' do @@ -69,6 +71,6 @@ emu = FlightlessBird.create :name => 'Emu' index - Bird.search(:indices => ['bird_core']).to_a.should == [duck, emu] + expect(Bird.search(:indices => ['bird_core']).to_a).to eq([duck, emu]) end end diff --git a/spec/acceptance/searching_within_a_model_spec.rb b/spec/acceptance/searching_within_a_model_spec.rb index 3bfb0c199..45cb79b74 100644 --- a/spec/acceptance/searching_within_a_model_spec.rb +++ b/spec/acceptance/searching_within_a_model_spec.rb @@ -1,4 +1,6 @@ # encoding: UTF-8 +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Searching within a model', :live => true do @@ -6,7 +8,7 @@ article = Article.create! :title => 'Pancakes' index - Article.search.first.should == article + expect(Article.search.first).to eq(article) end it "returns results matching the given query" do @@ -15,22 +17,22 @@ index articles = Article.search 'pancakes' - articles.should include(pancakes) - articles.should_not include(waffles) + expect(articles).to include(pancakes) + expect(articles).not_to include(waffles) end it "handles unicode characters" do istanbul = City.create! :name => 'İstanbul' index - City.search('İstanbul').to_a.should == [istanbul] + expect(City.search('İstanbul').to_a).to eq([istanbul]) end it "will star provided queries on request" do article = Article.create! :title => 'Pancakes' index - Article.search('cake', :star => true).first.should == article + expect(Article.search('cake', :star => true).first).to eq(article) end it "allows for searching on specific indices" do @@ -38,7 +40,7 @@ index articles = Article.search('pancake', :indices => ['stemmed_article_core']) - articles.to_a.should == [article] + expect(articles.to_a).to eq([article]) end it "allows for searching on distributed indices" do @@ -46,38 +48,63 @@ index articles = Article.search('pancake', :indices => ['article']) - articles.to_a.should == [article] + expect(articles.to_a).to eq([article]) end it "can search on namespaced models" do person = Admin::Person.create :name => 'James Bond' index - Admin::Person.search('Bond').to_a.should == [person] + expect(Admin::Person.search('Bond').to_a).to eq([person]) end it "raises an error if searching through an ActiveRecord scope" do - lambda { + expect { City.ordered.search - }.should raise_error(ThinkingSphinx::MixedScopesError) + }.to raise_error(ThinkingSphinx::MixedScopesError) end it "does not raise an error when searching with a default ActiveRecord scope" do - lambda { + expect { User.search - }.should_not raise_error(ThinkingSphinx::MixedScopesError) + }.not_to raise_error end it "raises an error when searching with default and applied AR scopes" do - lambda { + expect { User.recent.search - }.should raise_error(ThinkingSphinx::MixedScopesError) + }.to raise_error(ThinkingSphinx::MixedScopesError) end it "raises an error if the model has no indices defined" do - lambda { + expect { Category.search.to_a - }.should raise_error(ThinkingSphinx::NoIndicesError) + }.to raise_error(ThinkingSphinx::NoIndicesError) + end + + it "handles models with alternative id columns" do + album = Album.create! :name => 'The Seldom Seen Kid', :artist => 'Elbow' + index + + expect(Album.search(:indices => ['album_core', 'album_delta']).first). + to eq(album) + + expect(Album.search(:indices => ['album_real_core']).first). + to eq(album) + end + + it "is available via a sphinx-prefixed method" do + article = Article.create! :title => 'Pancakes' + index + + expect(Article.sphinx_search.first).to eq(article) + end + + it "has a 'none' default scope" do + article = Article.create! :title => 'Pancakes' + index + + expect(Article.search_none).to be_empty end end @@ -85,6 +112,6 @@ it "returns results" do product = Product.create! :name => 'Widget' - Product.search.first.should == product + expect(Product.search.first).to eq(product) end end diff --git a/spec/acceptance/sorting_search_results_spec.rb b/spec/acceptance/sorting_search_results_spec.rb index eecb74d7b..20f2799ac 100644 --- a/spec/acceptance/sorting_search_results_spec.rb +++ b/spec/acceptance/sorting_search_results_spec.rb @@ -1,48 +1,50 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Sorting search results', :live => true do it "sorts by a given clause" do - gods = Book.create! :title => 'American Gods', :year => 2001 - grave = Book.create! :title => 'The Graveyard Book', :year => 2009 - boys = Book.create! :title => 'Anansi Boys', :year => 2005 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 + grave = Book.create! :title => 'The Graveyard Book', :publishing_year => 2009 + boys = Book.create! :title => 'Anansi Boys', :publishing_year => 2005 index - Book.search(:order => 'year ASC').to_a.should == [gods, boys, grave] + expect(Book.search(:order => 'publishing_year ASC').to_a).to eq([gods, boys, grave]) end it "sorts by a given attribute in ascending order" do - gods = Book.create! :title => 'American Gods', :year => 2001 - grave = Book.create! :title => 'The Graveyard Book', :year => 2009 - boys = Book.create! :title => 'Anansi Boys', :year => 2005 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 + grave = Book.create! :title => 'The Graveyard Book', :publishing_year => 2009 + boys = Book.create! :title => 'Anansi Boys', :publishing_year => 2005 index - Book.search(:order => :year).to_a.should == [gods, boys, grave] + expect(Book.search(:order => :publishing_year).to_a).to eq([gods, boys, grave]) end it "sorts by a given sortable field" do - gods = Book.create! :title => 'American Gods', :year => 2001 - grave = Book.create! :title => 'The Graveyard Book', :year => 2009 - boys = Book.create! :title => 'Anansi Boys', :year => 2005 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 + grave = Book.create! :title => 'The Graveyard Book', :publishing_year => 2009 + boys = Book.create! :title => 'Anansi Boys', :publishing_year => 2005 index - Book.search(:order => :title).to_a.should == [gods, boys, grave] + expect(Book.search(:order => :title).to_a).to eq([gods, boys, grave]) end it "sorts by a given sortable field with real-time indices" do widgets = Product.create! :name => 'Widgets' gadgets = Product.create! :name => 'Gadgets' - Product.search(:order => "name_sort ASC").to_a.should == [gadgets, widgets] + expect(Product.search(:order => "name_sort ASC").to_a).to eq([gadgets, widgets]) end it "can sort with a provided expression" do - gods = Book.create! :title => 'American Gods', :year => 2001 - grave = Book.create! :title => 'The Graveyard Book', :year => 2009 - boys = Book.create! :title => 'Anansi Boys', :year => 2005 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 + grave = Book.create! :title => 'The Graveyard Book', :publishing_year => 2009 + boys = Book.create! :title => 'Anansi Boys', :publishing_year => 2005 index - Book.search( - :select => '*, year MOD 2004 as mod_year', :order => 'mod_year ASC' - ).to_a.should == [boys, grave, gods] + expect(Book.search( + :select => '*, publishing_year MOD 2004 as mod_year', :order => 'mod_year ASC' + ).to_a).to eq([boys, grave, gods]) end end diff --git a/spec/acceptance/spec_helper.rb b/spec/acceptance/spec_helper.rb index 9ad705842..f877bf30f 100644 --- a/spec/acceptance/spec_helper.rb +++ b/spec/acceptance/spec_helper.rb @@ -1,17 +1,6 @@ +# frozen_string_literal: true + require 'spec_helper' root = File.expand_path File.dirname(__FILE__) Dir["#{root}/support/**/*.rb"].each { |file| require file } - -if ENV['SPHINX_VERSION'].try :[], /2.0.\d/ - ThinkingSphinx::SphinxQL.variables! - - ThinkingSphinx::Middlewares::DEFAULT.insert_after( - ThinkingSphinx::Middlewares::Inquirer, - ThinkingSphinx::Middlewares::UTF8 - ) - ThinkingSphinx::Middlewares::RAW_ONLY.insert_after( - ThinkingSphinx::Middlewares::Inquirer, - ThinkingSphinx::Middlewares::UTF8 - ) -end diff --git a/spec/acceptance/specifying_sql_spec.rb b/spec/acceptance/specifying_sql_spec.rb index 234bb546a..2d790a148 100644 --- a/spec/acceptance/specifying_sql_spec.rb +++ b/spec/acceptance/specifying_sql_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'specifying SQL for index definitions' do @@ -8,7 +10,7 @@ join user } index.render - index.sources.first.sql_query.should match(/LEFT OUTER JOIN .users./) + expect(index.sources.first.sql_query).to match(/LEFT OUTER JOIN .users./) end it "handles deep joins" do @@ -20,8 +22,8 @@ index.render query = index.sources.first.sql_query - query.should match(/LEFT OUTER JOIN .users./) - query.should match(/LEFT OUTER JOIN .articles./) + expect(query).to match(/LEFT OUTER JOIN .users./) + expect(query).to match(/LEFT OUTER JOIN .articles./) end it "handles has-many :through joins" do @@ -32,8 +34,8 @@ index.render query = index.sources.first.sql_query - query.should match(/LEFT OUTER JOIN .taggings./) - query.should match(/LEFT OUTER JOIN .tags./) + expect(query).to match(/LEFT OUTER JOIN .taggings./) + expect(query).to match(/LEFT OUTER JOIN .tags./) end it "handles custom join SQL statements" do @@ -45,7 +47,7 @@ index.render query = index.sources.first.sql_query - query.should match(/INNER JOIN foo ON foo.x = bar.y/) + expect(query).to match(/INNER JOIN foo ON foo.x = bar.y/) end it "handles GROUP BY clauses" do @@ -57,7 +59,7 @@ index.render query = index.sources.first.sql_query - query.should match(/GROUP BY .articles.\..id., .?articles.?\..title., .?articles.?\..id., lat/) + expect(query).to match(/GROUP BY .articles.\..id., .?articles.?\..title., .?articles.?\..id., lat/) end it "handles WHERE clauses" do @@ -69,7 +71,7 @@ index.render query = index.sources.first.sql_query - query.should match(/WHERE .+title != 'secret'.+ GROUP BY/) + expect(query).to match(/WHERE .+title != 'secret'.+ GROUP BY/) end it "handles manual MVA declarations" do @@ -81,7 +83,7 @@ } index.render - index.sources.first.sql_attr_multi.should == ['uint tag_ids from field'] + expect(index.sources.first.sql_attr_multi).to eq(['uint tag_ids from field']) end it "provides the sanitize_sql helper within the index definition block" do @@ -93,7 +95,7 @@ index.render query = index.sources.first.sql_query - query.should match(/WHERE .+title != 'secret'.+ GROUP BY/) + expect(query).to match(/WHERE .+title != 'secret'.+ GROUP BY/) end it "escapes new lines in SQL snippets" do @@ -111,7 +113,7 @@ index.render query = index.sources.first.sql_query - query.should match(/\\\n/) + expect(query).to match(/\\\n/) end it "joins each polymorphic relation" do @@ -123,10 +125,10 @@ index.render query = index.sources.first.sql_query - query.should match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/) - query.should match(/LEFT OUTER JOIN .books. ON .books.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Book'/) - query.should match(/articles\..title., books\..title./) - end + expect(query).to match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/) + expect(query).to match(/LEFT OUTER JOIN .books. ON .books.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Book'/) + expect(query).to match(/.articles.\..title., .books.\..title./) + end if ActiveRecord::VERSION::MAJOR > 3 it "concatenates references that have column" do index = ThinkingSphinx::ActiveRecord::Index.new(:event) @@ -137,10 +139,10 @@ index.render query = index.sources.first.sql_query - query.should match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/) - query.should_not match(/articles\..title., users\..title./) - query.should match(/articles\..title./) - end + expect(query).to match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/) + expect(query).not_to match(/articles\..title., users\..title./) + expect(query).to match(/.articles.\..title./) + end if ActiveRecord::VERSION::MAJOR > 3 it "respects deeper associations through polymorphic joins" do index = ThinkingSphinx::ActiveRecord::Index.new(:event) @@ -151,13 +153,31 @@ index.render query = index.sources.first.sql_query - query.should match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/) - query.should match(/LEFT OUTER JOIN .users. ON .users.\..id. = .articles.\..user_id./) - query.should match(/users\..name./) + expect(query).to match(/LEFT OUTER JOIN .articles. ON .articles.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Article'/) + expect(query).to match(/LEFT OUTER JOIN .users. ON .users.\..id. = .articles.\..user_id./) + expect(query).to match(/.users.\..name./) end -end + + it "allows for STI mixed with polymorphic joins" do + index = ThinkingSphinx::ActiveRecord::Index.new(:event) + index.definition_block = Proc.new { + indexes eventable.name, :as => :name + polymorphs eventable, :to => %w(Bird Car) + } + index.render + + query = index.sources.first.sql_query + expect(query).to match(/LEFT OUTER JOIN .animals. ON .animals.\..id. = .events.\..eventable_id. .* AND .events.\..eventable_type. = 'Animal'/) + expect(query).to match(/LEFT OUTER JOIN .cars. ON .cars.\..id. = .events.\..eventable_id. AND .events.\..eventable_type. = 'Car'/) + expect(query).to match(/.animals.\..name., .cars.\..name./) + end +end if ActiveRecord::VERSION::MAJOR > 3 describe 'separate queries for MVAs' do + def id_type + ActiveRecord::VERSION::STRING.to_f > 5.0 ? 'bigint' : 'uint' + end + let(:index) { ThinkingSphinx::ActiveRecord::Index.new(:article) } let(:count) { ThinkingSphinx::Configuration.instance.indices.count } let(:source) { index.sources.first } @@ -174,8 +194,33 @@ } declaration, query = attribute.split(/;\s+/) - declaration.should == 'uint tag_ids from query' - query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .taggings.\..tag_id. AS .tag_ids. FROM .taggings.\s? WHERE \(.taggings.\..article_id. IS NOT NULL\)$/) + expect(declaration).to eq("uint tag_ids from query") + expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .taggings.\..tag_id. AS .tag_ids. FROM .taggings.\s? WHERE \(.taggings.\..article_id. IS NOT NULL\)$/) + end + + it "does not include attributes sourced via separate queries" do + index.definition_block = Proc.new { + indexes title + has taggings.tag_id, :as => :tag_ids, :source => :query + } + index.render + + # We don't want it in the SELECT, JOIN or GROUP clauses. This should catch + # them all. + expect(source.sql_query).not_to include('taggings') + end + + it "keeps the joins in for separately queried tables if they're used elsewhere" do + index.definition_block = Proc.new { + indexes taggings.tag.name, :as => :tag_names + has taggings.tag.created_at, :as => :tag_dates, :source => :query + } + index.render + + expect(source.sql_query).to include('taggings') + expect(source.sql_query).to include('tags') + expect(source.sql_query).to_not match(/.tags.\..created_at./) + expect(source.sql_query).to match(/.tags.\..name./) end it "generates a SQL query with joins when appropriate for MVAs" do @@ -190,8 +235,8 @@ } declaration, query = attribute.split(/;\s+/) - declaration.should == 'uint tag_ids from query' - query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. WHERE \(.taggings.\..article_id. IS NOT NULL\)\s?$/) + expect(declaration).to eq("#{id_type} tag_ids from query") + expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. WHERE \(.taggings.\..article_id. IS NOT NULL\)\s?$/) end it "respects has_many :through joins for MVA queries" do @@ -206,8 +251,8 @@ } declaration, query = attribute.split(/;\s+/) - declaration.should == 'uint tag_ids from query' - query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. WHERE \(.taggings.\..article_id. IS NOT NULL\)\s?$/) + expect(declaration).to eq("#{id_type} tag_ids from query") + expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. WHERE \(.taggings.\..article_id. IS NOT NULL\)\s?$/) end it "can handle multiple joins for MVA queries" do @@ -224,8 +269,8 @@ } declaration, query = attribute.split(/;\s+/) - declaration.should == 'uint tag_ids from query' - query.should match(/^SELECT .articles.\..user_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .articles. INNER JOIN .taggings. ON .taggings.\..article_id. = .articles.\..id. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. WHERE \(.articles.\..user_id. IS NOT NULL\)\s?$/) + expect(declaration).to eq("#{id_type} tag_ids from query") + expect(query).to match(/^SELECT .articles.\..user_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .articles. INNER JOIN .taggings. ON .taggings.\..article_id. = .articles.\..id. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. WHERE \(.articles.\..user_id. IS NOT NULL\)\s?$/) end it "can handle simple HABTM joins for MVA queries" do @@ -242,9 +287,9 @@ } declaration, query = attribute.split(/;\s+/) - declaration.should == 'uint genre_ids from query' - query.should match(/^SELECT .books_genres.\..book_id. \* #{count} \+ #{source.offset} AS .id., .books_genres.\..genre_id. AS .genre_ids. FROM .books_genres.\s?$/) - end + expect(declaration).to eq("#{id_type} genre_ids from query") + expect(query).to match(/^SELECT .books_genres.\..book_id. \* #{count} \+ #{source.offset} AS .id., .books_genres.\..genre_id. AS .genre_ids. FROM .books_genres.\s?$/) + end if ActiveRecord::VERSION::MAJOR > 3 it "generates an appropriate range SQL queries for an MVA" do index.definition_block = Proc.new { @@ -258,9 +303,9 @@ } declaration, query, range = attribute.split(/;\s+/) - declaration.should == 'uint tag_ids from ranged-query' - query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .taggings.\..tag_id. AS .tag_ids. FROM .taggings. \s?WHERE \(.taggings.\..article_id. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)$/) - range.should match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/) + expect(declaration).to eq("uint tag_ids from ranged-query") + expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .taggings.\..tag_id. AS .tag_ids. FROM .taggings. \s?WHERE \(.taggings.\..article_id. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)$/) + expect(range).to match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/) end it "generates a SQL query with joins when appropriate for MVAs" do @@ -275,9 +320,9 @@ } declaration, query, range = attribute.split(/;\s+/) - declaration.should == 'uint tag_ids from ranged-query' - query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. \s?WHERE \(.taggings.\..article_id. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)$/) - range.should match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/) + expect(declaration).to eq("#{id_type} tag_ids from ranged-query") + expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..id. AS .tag_ids. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. \s?WHERE \(.taggings.\..article_id. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)$/) + expect(range).to match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/) end it "can handle ranged queries for simple HABTM joins for MVA queries" do @@ -294,10 +339,10 @@ } declaration, query, range = attribute.split(/;\s+/) - declaration.should == 'uint genre_ids from ranged-query' - query.should match(/^SELECT .books_genres.\..book_id. \* #{count} \+ #{source.offset} AS .id., .books_genres.\..genre_id. AS .genre_ids. FROM .books_genres. WHERE \(.books_genres.\..book_id. BETWEEN \$start AND \$end\)$/) - range.should match(/^SELECT MIN\(.books_genres.\..book_id.\), MAX\(.books_genres.\..book_id.\) FROM .books_genres.$/) - end + expect(declaration).to eq("#{id_type} genre_ids from ranged-query") + expect(query).to match(/^SELECT .books_genres.\..book_id. \* #{count} \+ #{source.offset} AS .id., .books_genres.\..genre_id. AS .genre_ids. FROM .books_genres. WHERE \(.books_genres.\..book_id. BETWEEN \$start AND \$end\)$/) + expect(range).to match(/^SELECT MIN\(.books_genres.\..book_id.\), MAX\(.books_genres.\..book_id.\) FROM .books_genres.$/) + end if ActiveRecord::VERSION::MAJOR > 3 it "respects custom SQL snippets as the query value" do index.definition_block = Proc.new { @@ -312,8 +357,8 @@ } declaration, query = attribute.split(/;\s+/) - declaration.should == 'uint tag_ids from query' - query.should == 'My Custom SQL Query' + expect(declaration).to eq('uint tag_ids from query') + expect(query).to eq('My Custom SQL Query') end it "respects custom SQL snippets as the ranged query value" do @@ -329,9 +374,9 @@ } declaration, query, range = attribute.split(/;\s+/) - declaration.should == 'uint tag_ids from ranged-query' - query.should == 'My Custom SQL Query' - range.should == 'And a Range' + expect(declaration).to eq('uint tag_ids from ranged-query') + expect(query).to eq('My Custom SQL Query') + expect(range).to eq('And a Range') end it "escapes new lines in custom SQL snippets" do @@ -349,8 +394,8 @@ } declaration, query = attribute.split(/;\s+/) - declaration.should == 'uint tag_ids from query' - query.should == "My Custom\\\nSQL Query" + expect(declaration).to eq('uint tag_ids from query') + expect(query).to eq("My Custom\\\nSQL Query") end end @@ -368,8 +413,8 @@ field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query = field.split(/;\s+/) - declaration.should == 'tags from query' - query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s? WHERE \(.taggings.\..article_id. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC\s?$/) + expect(declaration).to eq('tags from query') + expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s? WHERE \(.taggings.\..article_id. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC\s?$/) end it "respects has_many :through joins for MVF queries" do @@ -381,8 +426,8 @@ field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query = field.split(/;\s+/) - declaration.should == 'tags from query' - query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s? WHERE \(.taggings.\..article_id. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC\s?$/) + expect(declaration).to eq('tags from query') + expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s? WHERE \(.taggings.\..article_id. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC\s?$/) end it "can handle multiple joins for MVF queries" do @@ -396,8 +441,8 @@ field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query = field.split(/;\s+/) - declaration.should == 'tags from query' - query.should match(/^SELECT .articles.\..user_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .articles. INNER JOIN .taggings. ON .taggings.\..article_id. = .articles.\..id. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s? WHERE \(.articles.\..user_id. IS NOT NULL\)\s? ORDER BY .articles.\..user_id. ASC\s?$/) + expect(declaration).to eq('tags from query') + expect(query).to match(/^SELECT .articles.\..user_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .articles. INNER JOIN .taggings. ON .taggings.\..article_id. = .articles.\..id. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id.\s? WHERE \(.articles.\..user_id. IS NOT NULL\)\s? ORDER BY .articles.\..user_id. ASC\s?$/) end it "generates a SQL query with joins when appropriate for MVFs" do @@ -409,9 +454,20 @@ field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query, range = field.split(/;\s+/) - declaration.should == 'tags from ranged-query' - query.should match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. \s?WHERE \(.taggings.\..article_id. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC$/) - range.should match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/) + expect(declaration).to eq('tags from ranged-query') + expect(query).to match(/^SELECT .taggings.\..article_id. \* #{count} \+ #{source.offset} AS .id., .tags.\..name. AS .tags. FROM .taggings. INNER JOIN .tags. ON .tags.\..id. = .taggings.\..tag_id. \s?WHERE \(.taggings.\..article_id. BETWEEN \$start AND \$end\) AND \(.taggings.\..article_id. IS NOT NULL\)\s? ORDER BY .taggings.\..article_id. ASC$/) + expect(range).to match(/^SELECT MIN\(.taggings.\..article_id.\), MAX\(.taggings.\..article_id.\) FROM .taggings.\s?$/) + end + + it "does not include fields sourced via separate queries" do + index.definition_block = Proc.new { + indexes taggings.tag.name, :as => :tags, :source => :query + } + index.render + + # We don't want it in the SELECT, JOIN or GROUP clauses. This should catch + # them all. + expect(source.sql_query).not_to include('tags') end it "respects custom SQL snippets as the query value" do @@ -423,8 +479,8 @@ field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query = field.split(/;\s+/) - declaration.should == 'tags from query' - query.should == 'My Custom SQL Query' + expect(declaration).to eq('tags from query') + expect(query).to eq('My Custom SQL Query') end it "respects custom SQL snippets as the ranged query value" do @@ -437,9 +493,9 @@ field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query, range = field.split(/;\s+/) - declaration.should == 'tags from ranged-query' - query.should == 'My Custom SQL Query' - range.should == 'And a Range' + expect(declaration).to eq('tags from ranged-query') + expect(query).to eq('My Custom SQL Query') + expect(range).to eq('And a Range') end it "escapes new lines in custom SQL snippets" do @@ -454,7 +510,7 @@ field = source.sql_joined_field.detect { |field| field[/tags/] } declaration, query = field.split(/;\s+/) - declaration.should == 'tags from query' - query.should == "My Custom\\\nSQL Query" + expect(declaration).to eq('tags from query') + expect(query).to eq("My Custom\\\nSQL Query") end end diff --git a/spec/acceptance/sphinx_scopes_spec.rb b/spec/acceptance/sphinx_scopes_spec.rb index 1141d73a3..c47d11ee0 100644 --- a/spec/acceptance/sphinx_scopes_spec.rb +++ b/spec/acceptance/sphinx_scopes_spec.rb @@ -1,41 +1,43 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Sphinx scopes', :live => true do it "allows calling sphinx scopes from models" do - gods = Book.create! :title => 'American Gods', :year => 2001 - boys = Book.create! :title => 'Anansi Boys', :year => 2005 - grave = Book.create! :title => 'The Graveyard Book', :year => 2009 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 + boys = Book.create! :title => 'Anansi Boys', :publishing_year => 2005 + grave = Book.create! :title => 'The Graveyard Book', :publishing_year => 2009 index - Book.by_year(2009).to_a.should == [grave] + expect(Book.by_publishing_year(2009).to_a).to eq([grave]) end it "allows scopes to return both query and options" do - gods = Book.create! :title => 'American Gods', :year => 2001 - boys = Book.create! :title => 'Anansi Boys', :year => 2005 - grave = Book.create! :title => 'The Graveyard Book', :year => 2009 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 + boys = Book.create! :title => 'Anansi Boys', :publishing_year => 2005 + grave = Book.create! :title => 'The Graveyard Book', :publishing_year => 2009 index - Book.by_query_and_year('Graveyard', 2009).to_a.should == [grave] + expect(Book.by_query_and_publishing_year('Graveyard', 2009).to_a).to eq([grave]) end it "allows chaining of scopes" do - gods = Book.create! :title => 'American Gods', :year => 2001 - boys = Book.create! :title => 'Anansi Boys', :year => 2005 - grave = Book.create! :title => 'The Graveyard Book', :year => 2009 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 + boys = Book.create! :title => 'Anansi Boys', :publishing_year => 2005 + grave = Book.create! :title => 'The Graveyard Book', :publishing_year => 2009 index - Book.by_year(2001..2005).ordered.to_a.should == [boys, gods] + expect(Book.by_publishing_year(2001..2005).ordered.to_a).to eq([boys, gods]) end it "allows chaining of scopes that include queries" do - gods = Book.create! :title => 'American Gods', :year => 2001 - boys = Book.create! :title => 'Anansi Boys', :year => 2005 - grave = Book.create! :title => 'The Graveyard Book', :year => 2009 + gods = Book.create! :title => 'American Gods', :publishing_year => 2001 + boys = Book.create! :title => 'Anansi Boys', :publishing_year => 2005 + grave = Book.create! :title => 'The Graveyard Book', :publishing_year => 2009 index - Book.by_year(2001).by_query_and_year('Graveyard', 2009).to_a. - should == [grave] + expect(Book.by_publishing_year(2001).by_query_and_publishing_year('Graveyard', 2009).to_a). + to eq([grave]) end it "allows further search calls on scopes" do @@ -43,7 +45,7 @@ pratchett = Book.create! :title => 'Small Gods' index - Book.by_query('Gods').search('Small').to_a.should == [pratchett] + expect(Book.by_query('Gods').search('Small').to_a).to eq([pratchett]) end it "allows facet calls on scopes" do @@ -52,9 +54,9 @@ Book.create! :title => 'Small Gods', :author => 'Terry Pratchett' index - Book.by_query('Gods').facets.to_hash[:author].should == { + expect(Book.by_query('Gods').facets.to_hash[:author]).to eq({ 'Neil Gaiman' => 1, 'Terry Pratchett' => 1 - } + }) end it "allows accessing counts on scopes" do @@ -64,7 +66,7 @@ Book.create! :title => 'Night Watch' index - Book.by_query('gods').count.should == 2 + expect(Book.by_query('gods').count).to eq(2) end it 'raises an exception when trying to modify a populated request' do @@ -75,4 +77,11 @@ ThinkingSphinx::PopulatedResultsError ) end + + it "handles a chainable 'none' scope and returns nothing" do + Book.create! :title => 'Small Gods' + index + + expect(Book.by_query('gods').none).to be_empty + end end diff --git a/spec/acceptance/sql_deltas_spec.rb b/spec/acceptance/sql_deltas_spec.rb index bb7935d83..e1808aa00 100644 --- a/spec/acceptance/sql_deltas_spec.rb +++ b/spec/acceptance/sql_deltas_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'SQL delta indexing', :live => true do @@ -7,38 +9,50 @@ ) index - Book.search('Terry Pratchett').to_a.should == [guards] + expect(Book.search('Terry Pratchett').to_a).to eq([guards]) men = Book.create( :title => 'Men At Arms', :author => 'Terry Pratchett' ) sleep 0.25 - Book.search('Terry Pratchett').to_a.should == [guards, men] + expect(Book.search('Terry Pratchett').to_a).to match_array([guards, men]) end it "automatically indexes updated records" do book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett' index - Book.search('Harry').to_a.should == [book] + expect(Book.search('Harry').to_a).to eq([book]) - book.reload.update_attributes(:author => 'Terry Pratchett') + book.reload.update(:author => 'Terry Pratchett') sleep 0.25 - Book.search('Terry').to_a.should == [book] + expect(Book.search('Terry').to_a).to eq([book]) end it "does not match on old values" do book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett' index - Book.search('Harry').to_a.should == [book] + expect(Book.search('Harry').to_a).to eq([book]) + + book.reload.update(:author => 'Terry Pratchett') + sleep 0.25 + + expect(Book.search('Harry')).to be_empty + end + + it "does not match on old values with alternative ids" do + album = Album.create :name => 'Eternal Nightcap', :artist => 'The Whitloms' + index + + expect(Album.search('Whitloms').to_a).to eq([album]) - book.reload.update_attributes(:author => 'Terry Pratchett') + album.reload.update(:artist => 'The Whitlams') sleep 0.25 - Book.search('Harry').should be_empty + expect(Book.search('Whitloms')).to be_empty end it "automatically indexes new records of subclasses" do @@ -47,6 +61,18 @@ ) sleep 0.25 - Book.search('Gaiman').to_a.should == [book] + expect(Book.search('Gaiman').to_a).to eq([book]) + end + + it "updates associated models" do + colour = Colour.create(:name => 'green') + sleep 0.25 + + expect(Colour.search('green').to_a).to eq([colour]) + + tee = colour.tees.create + sleep 0.25 + + expect(Colour.search(:with => {:tee_ids => tee.id}).to_a).to eq([colour]) end end diff --git a/spec/acceptance/support/database_cleaner.rb b/spec/acceptance/support/database_cleaner.rb index 30646eb93..7c460d284 100644 --- a/spec/acceptance/support/database_cleaner.rb +++ b/spec/acceptance/support/database_cleaner.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + RSpec.configure do |config| config.before(:suite) do DatabaseCleaner.strategy = :truncation end - config.after(:each) do + config.after(:each) do |example| if example.example_group_instance.class.metadata[:live] DatabaseCleaner.clean end diff --git a/spec/acceptance/support/sphinx_controller.rb b/spec/acceptance/support/sphinx_controller.rb index 806704c6b..9f5674391 100644 --- a/spec/acceptance/support/sphinx_controller.rb +++ b/spec/acceptance/support/sphinx_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SphinxController def initialize config.searchd.mysql41 = 9307 @@ -10,14 +12,12 @@ def setup ThinkingSphinx::Configuration.reset - ActiveSupport::Dependencies.loaded.each do |path| - $LOADED_FEATURES.delete "#{path}.rb" - end - - ActiveSupport::Dependencies.clear + if Rails::VERSION::MAJOR < 7 + ActiveSupport::Dependencies.loaded.each do |path| + $LOADED_FEATURES.delete "#{path}.rb" + end - if ENV['SPHINX_VERSION'].try :[], /2.0.\d/ - ThinkingSphinx::Configuration.instance.settings['utf8'] = false + ActiveSupport::Dependencies.clear end config.searchd.mysql41 = 9307 @@ -28,14 +28,30 @@ def setup def start config.controller.start + rescue Riddle::CommandFailedError => error + puts <<-TXT + +The Sphinx start command failed: + Command: #{error.command_result.command} + Status: #{error.command_result.status} + Output: #{error.command_result.output} + TXT + raise error end def stop - config.controller.stop + while config.controller.running? do + config.controller.stop + sleep(0.1) + end end def index(*indices) - config.controller.index *indices + ThinkingSphinx::Commander.call :index_sql, config, :indices => indices + end + + def merge + ThinkingSphinx::Commander.call(:merge_and_update, config, {}) end private diff --git a/spec/acceptance/support/sphinx_helpers.rb b/spec/acceptance/support/sphinx_helpers.rb index 93f87d14c..87935accb 100644 --- a/spec/acceptance/support/sphinx_helpers.rb +++ b/spec/acceptance/support/sphinx_helpers.rb @@ -1,16 +1,27 @@ +# frozen_string_literal: true + module SphinxHelpers def sphinx @sphinx ||= SphinxController.new end def index(*indices) - sleep 0.5 if ENV['TRAVIS'] + sleep 0.5 if ENV['CI'] yield if block_given? sphinx.index *indices sleep 0.25 - sleep 0.5 if ENV['TRAVIS'] + sleep 0.5 if ENV['CI'] + end + + def merge + sleep 0.5 if ENV['CI'] + sleep 0.5 + + sphinx.merge + sleep 1.5 + sleep 0.5 if ENV['CI'] end end @@ -27,4 +38,8 @@ def index(*indices) config.after :all do |group| sphinx.stop if group.class.metadata[:live] end + + config.after :suite do + SphinxController.new.stop + end end diff --git a/spec/acceptance/suspended_deltas_spec.rb b/spec/acceptance/suspended_deltas_spec.rb index 5829961ab..b5c8c603b 100644 --- a/spec/acceptance/suspended_deltas_spec.rb +++ b/spec/acceptance/suspended_deltas_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'acceptance/spec_helper' describe 'Suspend deltas for a given action', :live => true do @@ -5,50 +7,50 @@ book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett' index - Book.search('Harry').to_a.should == [book] + expect(Book.search('Harry').to_a).to eq([book]) ThinkingSphinx::Deltas.suspend :book do - book.reload.update_attributes(:author => 'Terry Pratchett') + book.reload.update(:author => 'Terry Pratchett') sleep 0.25 - Book.search('Terry').to_a.should == [] + expect(Book.search('Terry').to_a).to eq([]) end sleep 0.25 - Book.search('Terry').to_a.should == [book] + expect(Book.search('Terry').to_a).to eq([book]) end it "returns core records even though they are no longer valid" do book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett' index - Book.search('Harry').to_a.should == [book] + expect(Book.search('Harry').to_a).to eq([book]) ThinkingSphinx::Deltas.suspend :book do - book.reload.update_attributes(:author => 'Terry Pratchett') + book.reload.update(:author => 'Terry Pratchett') sleep 0.25 - Book.search('Terry').to_a.should == [] + expect(Book.search('Terry').to_a).to eq([]) end sleep 0.25 - Book.search('Harry').to_a.should == [book] + expect(Book.search('Harry').to_a).to eq([book]) end it "marks core records as deleted" do book = Book.create :title => 'Night Watch', :author => 'Harry Pritchett' index - Book.search('Harry').to_a.should == [book] + expect(Book.search('Harry').to_a).to eq([book]) ThinkingSphinx::Deltas.suspend_and_update :book do - book.reload.update_attributes(:author => 'Terry Pratchett') + book.reload.update(:author => 'Terry Pratchett') sleep 0.25 - Book.search('Terry').to_a.should == [] + expect(Book.search('Terry').to_a).to eq([]) end sleep 0.25 - Book.search('Harry').to_a.should be_empty + expect(Book.search('Harry').to_a).to be_empty end end diff --git a/spec/fixtures/database.yml b/spec/fixtures/database.yml index 10457672b..a65f6b0f0 100644 --- a/spec/fixtures/database.yml +++ b/spec/fixtures/database.yml @@ -1,4 +1,4 @@ username: root -password: +password: thinking_sphinx host: localhost database: thinking_sphinx diff --git a/spec/internal/app/indices/admin_person_index.rb b/spec/internal/app/indices/admin_person_index.rb index 94901e387..931448b0c 100644 --- a/spec/internal/app/indices/admin_person_index.rb +++ b/spec/internal/app/indices/admin_person_index.rb @@ -1,3 +1,9 @@ +# frozen_string_literal: true + ThinkingSphinx::Index.define 'admin/person', :with => :active_record do indexes name end + +ThinkingSphinx::Index.define 'admin/person', :with => :real_time, :name => 'admin_person_rt' do + indexes name +end diff --git a/spec/internal/app/indices/album_index.rb b/spec/internal/app/indices/album_index.rb new file mode 100644 index 000000000..21f9aeb1f --- /dev/null +++ b/spec/internal/app/indices/album_index.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +ThinkingSphinx::Index.define :album, :with => :active_record, :primary_key => :integer_id, :delta => true do + indexes name, artist +end + +ThinkingSphinx::Index.define :album, :with => :real_time, :primary_key => :integer_id, :name => :album_real do + indexes name, artist +end diff --git a/spec/internal/app/indices/animal_index.rb b/spec/internal/app/indices/animal_index.rb index e5be81a94..64fd15ea0 100644 --- a/spec/internal/app/indices/animal_index.rb +++ b/spec/internal/app/indices/animal_index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ThinkingSphinx::Index.define :animal, :with => :active_record do indexes name end diff --git a/spec/internal/app/indices/article_index.rb b/spec/internal/app/indices/article_index.rb index 6a655cc80..a0c456218 100644 --- a/spec/internal/app/indices/article_index.rb +++ b/spec/internal/app/indices/article_index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ThinkingSphinx::Index.define :article, :with => :active_record do indexes title, content indexes user.name, :as => :user @@ -5,10 +7,9 @@ has published, user_id has taggings.tag_id, :as => :tag_ids, :source => :query - has taggings.created_at, :as => :taggings_at + has taggings.created_at, :as => :taggings_at, :type => :timestamp set_property :min_infix_len => 4 - set_property :enable_star => true end ThinkingSphinx::Index.define :article, :with => :active_record, @@ -18,7 +19,13 @@ has published, user_id has taggings.tag_id, :as => :tag_ids - has taggings.created_at, :as => :taggings_at + has taggings.created_at, :as => :taggings_at, :type => :timestamp set_property :morphology => 'stem_en' end + +ThinkingSphinx::Index.define :article, :name => :published_articles, :with => :real_time do + indexes title, content + + scope { Article.where :published => true } +end diff --git a/spec/internal/app/indices/bird_index.rb b/spec/internal/app/indices/bird_index.rb index 6879ae1c3..c3bb7c276 100644 --- a/spec/internal/app/indices/bird_index.rb +++ b/spec/internal/app/indices/bird_index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + FlightlessBird ThinkingSphinx::Index.define :bird, :with => :active_record do indexes name diff --git a/spec/internal/app/indices/book_index.rb b/spec/internal/app/indices/book_index.rb index 30baaee6b..d1c2dc5de 100644 --- a/spec/internal/app/indices/book_index.rb +++ b/spec/internal/app/indices/book_index.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + ThinkingSphinx::Index.define :book, :with => :active_record, :delta => true do indexes title, :sortable => true indexes author, :facet => true indexes [title, author], :as => :info indexes blurb_file, :file => true - has year, created_at + has publishing_year + has created_at, :type => :timestamp end diff --git a/spec/internal/app/indices/car_index.rb b/spec/internal/app/indices/car_index.rb index f684c5c3a..6e0848a11 100644 --- a/spec/internal/app/indices/car_index.rb +++ b/spec/internal/app/indices/car_index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ThinkingSphinx::Index.define :car, :with => :real_time do indexes name, :sortable => true diff --git a/spec/internal/app/indices/city_index.rb b/spec/internal/app/indices/city_index.rb index e80ebcf81..12f8e2e47 100644 --- a/spec/internal/app/indices/city_index.rb +++ b/spec/internal/app/indices/city_index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ThinkingSphinx::Index.define :city, :with => :active_record do indexes name has lat, lng diff --git a/spec/internal/app/indices/colour_index.rb b/spec/internal/app/indices/colour_index.rb new file mode 100644 index 000000000..d427cc858 --- /dev/null +++ b/spec/internal/app/indices/colour_index.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +ThinkingSphinx::Index.define :colour, :with => :active_record, :delta => true do + indexes name + + has tees.id, :as => :tee_ids +end diff --git a/spec/internal/app/indices/product_index.rb b/spec/internal/app/indices/product_index.rb index 9cbef0d94..26fc95fe6 100644 --- a/spec/internal/app/indices/product_index.rb +++ b/spec/internal/app/indices/product_index.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true + multi_schema = MultiSchema.new ThinkingSphinx::Index.define :product, :with => :real_time do indexes name, :sortable => true has category_ids, :type => :integer, :multi => true + has options, :type => :json if JSONColumn.call end if multi_schema.active? - multi_schema.create 'thinking_sphinx' - ThinkingSphinx::Index.define(:product, :name => :product_two, :offset_as => :product_two, :with => :real_time ) do diff --git a/spec/internal/app/indices/tee_index.rb b/spec/internal/app/indices/tee_index.rb index cbb228c6f..2777ed7e2 100644 --- a/spec/internal/app/indices/tee_index.rb +++ b/spec/internal/app/indices/tee_index.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true + ThinkingSphinx::Index.define :tee, :with => :active_record do - index colour.name + indexes colour.name has colour_id, :facet => true end diff --git a/spec/internal/app/indices/user_index.rb b/spec/internal/app/indices/user_index.rb index 0af8840aa..719e8e9b5 100644 --- a/spec/internal/app/indices/user_index.rb +++ b/spec/internal/app/indices/user_index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ThinkingSphinx::Index.define :user, :with => :active_record do indexes name diff --git a/spec/internal/app/models/admin/person.rb b/spec/internal/app/models/admin/person.rb index 2c48d632e..eaa3935dc 100644 --- a/spec/internal/app/models/admin/person.rb +++ b/spec/internal/app/models/admin/person.rb @@ -1,3 +1,9 @@ +# frozen_string_literal: true + class Admin::Person < ActiveRecord::Base self.table_name = 'admin_people' + + ThinkingSphinx::Callbacks.append( + self, 'admin/person', :behaviours => [:sql, :real_time] + ) end diff --git a/spec/internal/app/models/album.rb b/spec/internal/app/models/album.rb new file mode 100644 index 000000000..7b6e953e4 --- /dev/null +++ b/spec/internal/app/models/album.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class Album < ActiveRecord::Base + self.primary_key = :id + + before_validation :set_id, :on => :create + before_validation :set_integer_id, :on => :create + + ThinkingSphinx::Callbacks.append( + self, :behaviours => [:sql, :real_time, :deltas] + ) + + validates :id, :presence => true, :uniqueness => true + validates :integer_id, :presence => true, :uniqueness => true + + private + + def set_id + self.id = (Album.maximum(:id) || "a").next + end + + def set_integer_id + self.integer_id = (Album.maximum(:integer_id) || 0) + 1 + end +end diff --git a/spec/internal/app/models/animal.rb b/spec/internal/app/models/animal.rb index e7d24fa22..4431ef86b 100644 --- a/spec/internal/app/models/animal.rb +++ b/spec/internal/app/models/animal.rb @@ -1,2 +1,5 @@ +# frozen_string_literal: true + class Animal < ActiveRecord::Base + ThinkingSphinx::Callbacks.append(self, :behaviours => [:sql]) end diff --git a/spec/internal/app/models/article.rb b/spec/internal/app/models/article.rb index 6664d400c..b2ebf186b 100644 --- a/spec/internal/app/models/article.rb +++ b/spec/internal/app/models/article.rb @@ -1,5 +1,9 @@ +# frozen_string_literal: true + class Article < ActiveRecord::Base belongs_to :user has_many :taggings has_many :tags, :through => :taggings + + ThinkingSphinx::Callbacks.append(self, :behaviours => [:sql, :updates]) end diff --git a/spec/internal/app/models/bird.rb b/spec/internal/app/models/bird.rb index 1bda40b8e..a2d11ddc3 100644 --- a/spec/internal/app/models/bird.rb +++ b/spec/internal/app/models/bird.rb @@ -1,2 +1,5 @@ +# frozen_string_literal: true + class Bird < Animal + ThinkingSphinx::Callbacks.append(self, :behaviours => [:sql]) end diff --git a/spec/internal/app/models/book.rb b/spec/internal/app/models/book.rb index 6a5fe58f9..a2cd0639e 100644 --- a/spec/internal/app/models/book.rb +++ b/spec/internal/app/models/book.rb @@ -1,14 +1,18 @@ +# frozen_string_literal: true + class Book < ActiveRecord::Base include ThinkingSphinx::Scopes has_and_belongs_to_many :genres + ThinkingSphinx::Callbacks.append(self, :behaviours => [:sql, :deltas]) + sphinx_scope(:by_query) { |query| query } - sphinx_scope(:by_year) do |year| - {:with => {:year => year}} + sphinx_scope(:by_publishing_year) do |year| + {:with => {:publishing_year => year}} end - sphinx_scope(:by_query_and_year) do |query, year| - [query, {:with => {:year =>year}}] + sphinx_scope(:by_query_and_publishing_year) do |query, year| + [query, {:with => {:publishing_year =>year}}] end - sphinx_scope(:ordered) { {:order => 'year DESC'} } + sphinx_scope(:ordered) { {:order => 'publishing_year DESC'} } end diff --git a/spec/internal/app/models/car.rb b/spec/internal/app/models/car.rb index 661fa506e..8651aca86 100644 --- a/spec/internal/app/models/car.rb +++ b/spec/internal/app/models/car.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class Car < ActiveRecord::Base belongs_to :manufacturer - after_save ThinkingSphinx::RealTime.callback_for(:car) + ThinkingSphinx::Callbacks.append(self, :behaviours => [:real_time]) end diff --git a/spec/internal/app/models/categorisation.rb b/spec/internal/app/models/categorisation.rb index 1af00b9c7..0553928ac 100644 --- a/spec/internal/app/models/categorisation.rb +++ b/spec/internal/app/models/categorisation.rb @@ -1,6 +1,15 @@ +# frozen_string_literal: true + class Categorisation < ActiveRecord::Base belongs_to :category belongs_to :product - after_save ThinkingSphinx::RealTime.callback_for(:product, [:product]) + after_commit :update_product + + private + + def update_product + product.reload + ThinkingSphinx::RealTime.callback_for(:product, [:product]).after_save self + end end diff --git a/spec/internal/app/models/category.rb b/spec/internal/app/models/category.rb index cdb4ed032..45405bd8a 100644 --- a/spec/internal/app/models/category.rb +++ b/spec/internal/app/models/category.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Category < ActiveRecord::Base has_many :categorisations has_many :products, :through => :categorisations diff --git a/spec/internal/app/models/city.rb b/spec/internal/app/models/city.rb index 1e39734cb..59c27041b 100644 --- a/spec/internal/app/models/city.rb +++ b/spec/internal/app/models/city.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + class City < ActiveRecord::Base + ThinkingSphinx::Callbacks.append(self, :behaviours => [:sql]) + scope :ordered, lambda { order(:name) } end diff --git a/spec/internal/app/models/colour.rb b/spec/internal/app/models/colour.rb index d20d60595..34bc8ea42 100644 --- a/spec/internal/app/models/colour.rb +++ b/spec/internal/app/models/colour.rb @@ -1,3 +1,7 @@ +# frozen_string_literal: true + class Colour < ActiveRecord::Base has_many :tees + + ThinkingSphinx::Callbacks.append(self, behaviours: [:sql, :deltas]) end diff --git a/spec/internal/app/models/event.rb b/spec/internal/app/models/event.rb index adabceef9..c22ce9a8d 100644 --- a/spec/internal/app/models/event.rb +++ b/spec/internal/app/models/event.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Event < ActiveRecord::Base belongs_to :eventable, :polymorphic => true end diff --git a/spec/internal/app/models/flightless_bird.rb b/spec/internal/app/models/flightless_bird.rb index 345a2fcdc..f7c749a7e 100644 --- a/spec/internal/app/models/flightless_bird.rb +++ b/spec/internal/app/models/flightless_bird.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class FlightlessBird < Bird end diff --git a/spec/internal/app/models/genre.rb b/spec/internal/app/models/genre.rb index d9f9fc908..da4cd7fbe 100644 --- a/spec/internal/app/models/genre.rb +++ b/spec/internal/app/models/genre.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Genre < ActiveRecord::Base # end diff --git a/spec/internal/app/models/hardcover.rb b/spec/internal/app/models/hardcover.rb index 88a04e3ed..45dc3c93a 100644 --- a/spec/internal/app/models/hardcover.rb +++ b/spec/internal/app/models/hardcover.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Hardcover < Book # end diff --git a/spec/internal/app/models/mammal.rb b/spec/internal/app/models/mammal.rb index 6cd52f02e..1291a1c39 100644 --- a/spec/internal/app/models/mammal.rb +++ b/spec/internal/app/models/mammal.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class Mammal < Animal end diff --git a/spec/internal/app/models/manufacturer.rb b/spec/internal/app/models/manufacturer.rb index 6ddeac8e8..aed9e5eb8 100644 --- a/spec/internal/app/models/manufacturer.rb +++ b/spec/internal/app/models/manufacturer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Manufacturer < ActiveRecord::Base has_many :cars end diff --git a/spec/internal/app/models/product.rb b/spec/internal/app/models/product.rb index d6cf201fc..2bc519f98 100644 --- a/spec/internal/app/models/product.rb +++ b/spec/internal/app/models/product.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + class Product < ActiveRecord::Base has_many :categorisations has_many :categories, :through => :categorisations - after_save ThinkingSphinx::RealTime.callback_for(:product) + ThinkingSphinx::Callbacks.append(self, :behaviours => [:real_time]) end diff --git a/spec/internal/app/models/tag.rb b/spec/internal/app/models/tag.rb index 9574ef9a3..376642bf2 100644 --- a/spec/internal/app/models/tag.rb +++ b/spec/internal/app/models/tag.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Tag < ActiveRecord::Base has_many :taggings has_many :articles, :through => :taggings diff --git a/spec/internal/app/models/tagging.rb b/spec/internal/app/models/tagging.rb index 7e6d0df12..fcfbc8c1b 100644 --- a/spec/internal/app/models/tagging.rb +++ b/spec/internal/app/models/tagging.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Tagging < ActiveRecord::Base belongs_to :tag belongs_to :article diff --git a/spec/internal/app/models/tee.rb b/spec/internal/app/models/tee.rb index 1cd013b25..39e2ea928 100644 --- a/spec/internal/app/models/tee.rb +++ b/spec/internal/app/models/tee.rb @@ -1,3 +1,10 @@ +# frozen_string_literal: true + class Tee < ActiveRecord::Base belongs_to :colour + + ThinkingSphinx::Callbacks.append(self, :behaviours => [:sql]) + ThinkingSphinx::Callbacks.append( + self, behaviours: [:sql, :deltas], :path => [:colour] + ) end diff --git a/spec/internal/app/models/tweet.rb b/spec/internal/app/models/tweet.rb index c4a20a64b..c6ea180a1 100644 --- a/spec/internal/app/models/tweet.rb +++ b/spec/internal/app/models/tweet.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Tweet < ActiveRecord::Base self.primary_key = :id end diff --git a/spec/internal/app/models/user.rb b/spec/internal/app/models/user.rb index 87b3670f4..26053bc50 100644 --- a/spec/internal/app/models/user.rb +++ b/spec/internal/app/models/user.rb @@ -1,6 +1,10 @@ +# frozen_string_literal: true + class User < ActiveRecord::Base has_many :articles + ThinkingSphinx::Callbacks.append(self, :behaviours => [:sql]) + default_scope { order(:id) } scope :recent, lambda { where('created_at > ?', 1.week.ago) } end diff --git a/spec/internal/config/database.yml b/spec/internal/config/database.yml index 76b44e8a7..222dba6e8 100644 --- a/spec/internal/config/database.yml +++ b/spec/internal/config/database.yml @@ -1,6 +1,17 @@ test: adapter: <%= ENV['DATABASE'] || 'mysql2' %> database: thinking_sphinx - username: <%= ENV['DATABASE'] == 'postgresql' ? ENV['USER'] : 'root' %> + username: <%= ENV['DATABASE'] == 'postgresql' ? 'postgres' : 'root' %> +<% if ENV["DATABASE_PASSWORD"] %> + password: <%= ENV["DATABASE_PASSWORD"] %> +<% end %> +<% if ENV["DATABASE_PORT"] %> + host: 127.0.0.1 + port: <%= ENV["DATABASE_PORT"] %> +<% elsif ENV["CI"] %> + password: thinking_sphinx + host: 127.0.0.1 + port: <%= ENV['DATABASE'] == 'postgresql' ? 5432 : 3306 %> +<% end %> min_messages: warning encoding: utf8 diff --git a/spec/internal/db/schema.rb b/spec/internal/db/schema.rb index 62e8936d6..3aede6ba5 100644 --- a/spec/internal/db/schema.rb +++ b/spec/internal/db/schema.rb @@ -1,9 +1,19 @@ +# frozen_string_literal: true + ActiveRecord::Schema.define do create_table(:admin_people, :force => true) do |t| t.string :name t.timestamps null: false end + create_table(:albums, :force => true, :id => false) do |t| + t.string :id + t.integer :integer_id + t.string :name + t.string :artist + t.boolean :delta, :default => true, :null => false + end + create_table(:animals, :force => true) do |t| t.string :name t.string :type @@ -29,7 +39,7 @@ create_table(:books, :force => true) do |t| t.string :title t.string :author - t.integer :year + t.integer :publishing_year t.string :blurb_file t.boolean :delta, :default => true, :null => false t.string :type, :default => 'Book', :null => false @@ -58,6 +68,7 @@ create_table(:colours, :force => true) do |t| t.string :name + t.boolean :delta, :null => false, :default => true t.timestamps null: false end @@ -72,6 +83,7 @@ create_table(:products, :force => true) do |t| t.string :name + t.json :options if ::JSONColumn.call end create_table(:taggings, :force => true) do |t| diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 00ddd84e0..1d4b30962 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rubygems' require 'bundler' @@ -5,14 +7,22 @@ root = File.expand_path File.dirname(__FILE__) require "#{root}/support/multi_schema" +require "#{root}/support/json_column" +require "#{root}/support/mysql" require 'thinking_sphinx/railtie' Combustion.initialize! :active_record -Dir["#{root}/support/**/*.rb"].each { |file| require file } +MultiSchema.new.create 'thinking_sphinx' + +require "#{root}/support/sphinx_yaml_helpers" RSpec.configure do |config| # enable filtering for examples config.filter_run :wip => nil config.run_all_when_everything_filtered = true + + config.around :each, :live do |example| + example.run_with_retry :retry => 3 + end end diff --git a/spec/support/json_column.rb b/spec/support/json_column.rb new file mode 100644 index 000000000..7b9d6c037 --- /dev/null +++ b/spec/support/json_column.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class JSONColumn + include ActiveRecord::ConnectionAdapters + + def self.call + new.call + end + + def call + ruby? && postgresql? && column? + end + + private + + def column? + ( + ActiveRecord::ConnectionAdapters.constants.include?(:PostgreSQLAdapter) && + PostgreSQLAdapter.constants.include?(:TableDefinition) && + PostgreSQLAdapter::TableDefinition.instance_methods.include?(:json) + ) || ( + ActiveRecord::ConnectionAdapters.constants.include?(:PostgreSQL) && + PostgreSQL.constants.include?(:ColumnMethods) && + PostgreSQL::ColumnMethods.instance_methods.include?(:json) + ) + end + + def postgresql? + ENV['DATABASE'] == 'postgresql' + end + + def ruby? + RUBY_PLATFORM != 'java' + end +end diff --git a/spec/support/multi_schema.rb b/spec/support/multi_schema.rb index d5cf6b6bb..69c653f1b 100644 --- a/spec/support/multi_schema.rb +++ b/spec/support/multi_schema.rb @@ -1,15 +1,19 @@ +# frozen_string_literal: true + class MultiSchema def active? ENV['DATABASE'] == 'postgresql' end def create(schema_name) + return unless active? + unless connection.schema_exists? schema_name connection.execute %Q{CREATE SCHEMA "#{schema_name}"} end switch schema_name - silence_stream(STDOUT) { load Rails.root.join('db', 'schema.rb') } + load Rails.root.join('db', 'schema.rb') end def current diff --git a/spec/support/mysql.rb b/spec/support/mysql.rb new file mode 100644 index 000000000..f44ec7e3b --- /dev/null +++ b/spec/support/mysql.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# New versions of MySQL don't allow NULL values for primary keys, but old +# versions of Rails do. To use both at the same time, we need to update Rails' +# default primary key type to no longer have a default NULL value. +# +class PatchAdapter + def call + return unless using_mysql? && using_rails_pre_4_1? + + require 'active_record/connection_adapters/abstract_mysql_adapter' + ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter:: + NATIVE_DATABASE_TYPES[:primary_key] = "int(11) auto_increment PRIMARY KEY" + end + + def using_mysql? + ENV.fetch('DATABASE', 'mysql2') == 'mysql2' + end + + def using_rails_pre_4_1? + ActiveRecord::VERSION::STRING.to_f < 4.1 + end +end + +PatchAdapter.new.call diff --git a/spec/support/sphinx_yaml_helpers.rb b/spec/support/sphinx_yaml_helpers.rb index 9f1d14ec5..442fd5c68 100644 --- a/spec/support/sphinx_yaml_helpers.rb +++ b/spec/support/sphinx_yaml_helpers.rb @@ -1,6 +1,13 @@ +# frozen_string_literal: true + module SphinxYamlHelpers def write_configuration(hash) - File.stub :read => {'test' => hash}.to_yaml, :exists? => true + allow(File).to receive(:read).and_return({'test' => hash}.to_yaml) + allow(File).to receive(:exist?).and_wrap_original do |original, path| + next true if path.to_s == File.absolute_path("config/thinking_sphinx.yml", Rails.root.to_s) + + original.call(path) + end end end diff --git a/spec/thinking_sphinx/active_record/association_spec.rb b/spec/thinking_sphinx/active_record/association_spec.rb index 7953d4594..b91a5afc1 100644 --- a/spec/thinking_sphinx/active_record/association_spec.rb +++ b/spec/thinking_sphinx/active_record/association_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Association do @@ -6,7 +8,7 @@ describe '#stack' do it "returns the column's stack and name" do - association.stack.should == [:users, :post] + expect(association.stack).to eq([:users, :post]) end end end diff --git a/spec/thinking_sphinx/active_record/attribute/type_spec.rb b/spec/thinking_sphinx/active_record/attribute/type_spec.rb index 6be290190..5e7aa8b70 100644 --- a/spec/thinking_sphinx/active_record/attribute/type_spec.rb +++ b/spec/thinking_sphinx/active_record/attribute/type_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module ActiveRecord class Attribute; end @@ -23,52 +25,55 @@ class Attribute; end before :each do column.__stack << :foo - model.stub :reflect_on_association => association + allow(model).to receive(:reflect_on_association).and_return(association) end it "returns true if there are has_many associations" do - association.stub :macro => :has_many + allow(association).to receive(:macro).and_return(:has_many) - type.should be_multi + expect(type).to be_multi end it "returns true if there are has_and_belongs_to_many associations" do - association.stub :macro => :has_and_belongs_to_many + allow(association).to receive(:macro).and_return(:has_and_belongs_to_many) - type.should be_multi + expect(type).to be_multi end it "returns false if there are no associations" do column.__stack.clear - type.should_not be_multi + expect(type).not_to be_multi end it "returns false if there are only belongs_to associations" do - association.stub :macro => :belongs_to + allow(association).to receive(:macro).and_return(:belongs_to) - type.should_not be_multi + expect(type).not_to be_multi end it "returns false if there are only has_one associations" do - association.stub :macro => :has_one + allow(association).to receive(:macro).and_return(:has_one) - type.should_not be_multi + expect(type).not_to be_multi end it "returns true if deeper associations have many" do column.__stack << :bar deep_association = double(:klass => double, :macro => :has_many) - association.stub :macro => :belongs_to, - :klass => double(:reflect_on_association => deep_association) - type.should be_multi + allow(association).to receive(:macro).and_return(:belongs_to) + allow(association).to receive(:klass).and_return( + double(:reflect_on_association => deep_association) + ) + + expect(type).to be_multi end it "respects the provided setting" do attribute.options[:multi] = true - type.should be_multi + expect(type).to be_multi end end @@ -76,73 +81,79 @@ class Attribute; end it "returns the type option provided" do attribute.options[:type] = :datetime - type.type.should == :datetime + expect(type.type).to eq(:datetime) end it "detects integer types from the database" do - db_column.stub!(:type => :integer, :sql_type => 'integer(11)') + allow(db_column).to receive_messages(:type => :integer, :sql_type => 'integer(11)') - type.type.should == :integer + expect(type.type).to eq(:integer) end it "detects boolean types from the database" do - db_column.stub!(:type => :boolean) + allow(db_column).to receive_messages(:type => :boolean) - type.type.should == :boolean + expect(type.type).to eq(:boolean) end it "detects datetime types from the database as timestamps" do - db_column.stub!(:type => :datetime) + allow(db_column).to receive_messages(:type => :datetime) - type.type.should == :timestamp + expect(type.type).to eq(:timestamp) end it "detects date types from the database as timestamps" do - db_column.stub!(:type => :date) + allow(db_column).to receive_messages(:type => :date) - type.type.should == :timestamp + expect(type.type).to eq(:timestamp) end it "detects string types from the database" do - db_column.stub!(:type => :string) + allow(db_column).to receive_messages(:type => :string) - type.type.should == :string + expect(type.type).to eq(:string) end it "detects text types from the database as strings" do - db_column.stub!(:type => :text) + allow(db_column).to receive_messages(:type => :text) - type.type.should == :string + expect(type.type).to eq(:string) end it "detects float types from the database" do - db_column.stub!(:type => :float) + allow(db_column).to receive_messages(:type => :float) - type.type.should == :float + expect(type.type).to eq(:float) end it "detects decimal types from the database as floats" do - db_column.stub!(:type => :decimal) + allow(db_column).to receive_messages(:type => :decimal) - type.type.should == :float + expect(type.type).to eq(:float) end it "detects big ints as big ints" do - db_column.stub :type => :bigint + allow(db_column).to receive_messages :type => :bigint - type.type.should == :bigint + expect(type.type).to eq(:bigint) end it "detects large integers as big ints" do - db_column.stub :type => :integer, :sql_type => 'bigint(20)' + allow(db_column).to receive_messages :type => :integer, :sql_type => 'bigint(20)' + + expect(type.type).to eq(:bigint) + end + + it "detects JSON" do + allow(db_column).to receive_messages :type => :json - type.type.should == :bigint + expect(type.type).to eq(:json) end it "respects provided type setting" do attribute.options[:type] = :timestamp - type.type.should == :timestamp + expect(type.type).to eq(:timestamp) end it 'raises an error if the database column does not exist' do diff --git a/spec/thinking_sphinx/active_record/base_spec.rb b/spec/thinking_sphinx/active_record/base_spec.rb index db9f98767..2e5886be3 100644 --- a/spec/thinking_sphinx/active_record/base_spec.rb +++ b/spec/thinking_sphinx/active_record/base_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Base do @@ -16,34 +18,34 @@ def self.name; 'SubModel'; end describe '.facets' do it "returns a new search object" do - model.facets.should be_a(ThinkingSphinx::FacetSearch) + expect(model.facets).to be_a(ThinkingSphinx::FacetSearch) end it "passes through arguments to the search object" do - model.facets('pancakes').query.should == 'pancakes' + expect(model.facets('pancakes').query).to eq('pancakes') end it "scopes the search to a given model" do - model.facets('pancakes').options[:classes].should == [model] + expect(model.facets('pancakes').options[:classes]).to eq([model]) end it "merges the :classes option with the model" do - model.facets('pancakes', :classes => [sub_model]). - options[:classes].should == [sub_model, model] + expect(model.facets('pancakes', :classes => [sub_model]). + options[:classes]).to eq([sub_model, model]) end it "applies the default scope if there is one" do - model.stub :default_sphinx_scope => :default, + allow(model).to receive_messages :default_sphinx_scope => :default, :sphinx_scopes => {:default => Proc.new { {:order => :created_at} }} - model.facets.options[:order].should == :created_at + expect(model.facets.options[:order]).to eq(:created_at) end it "does not apply a default scope if one is not set" do - model.stub :default_sphinx_scope => nil, + allow(model).to receive_messages :default_sphinx_scope => nil, :default => {:order => :created_at} - model.facets.options[:order].should be_nil + expect(model.facets.options[:order]).to be_nil end end @@ -55,55 +57,55 @@ def self.name; 'SubModel'; end end it "returns a new search object" do - model.search.should be_a(ThinkingSphinx::Search) + expect(model.search).to be_a(ThinkingSphinx::Search) end it "passes through arguments to the search object" do - model.search('pancakes').query.should == 'pancakes' + expect(model.search('pancakes').query).to eq('pancakes') end it "scopes the search to a given model" do - model.search('pancakes').options[:classes].should == [model] + expect(model.search('pancakes').options[:classes]).to eq([model]) end it "passes through options to the search object" do - model.search('pancakes', populate: true). - options[:populate].should be_true + expect(model.search('pancakes', populate: true). + options[:populate]).to be_truthy end it "should automatically populate when :populate is set to true" do - stack.should_receive(:call).and_return(true) + expect(stack).to receive(:call).and_return(true) model.search('pancakes', populate: true) end it "merges the :classes option with the model" do - model.search('pancakes', :classes => [sub_model]). - options[:classes].should == [sub_model, model] + expect(model.search('pancakes', :classes => [sub_model]). + options[:classes]).to eq([sub_model, model]) end it "respects provided middleware" do - model.search(:middleware => ThinkingSphinx::Middlewares::RAW_ONLY). - options[:middleware].should == ThinkingSphinx::Middlewares::RAW_ONLY + expect(model.search(:middleware => ThinkingSphinx::Middlewares::RAW_ONLY). + options[:middleware]).to eq(ThinkingSphinx::Middlewares::RAW_ONLY) end it "respects provided masks" do - model.search(:masks => [ThinkingSphinx::Masks::PaginationMask]). - masks.should == [ThinkingSphinx::Masks::PaginationMask] + expect(model.search(:masks => [ThinkingSphinx::Masks::PaginationMask]). + masks).to eq([ThinkingSphinx::Masks::PaginationMask]) end it "applies the default scope if there is one" do - model.stub :default_sphinx_scope => :default, + allow(model).to receive_messages :default_sphinx_scope => :default, :sphinx_scopes => {:default => Proc.new { {:order => :created_at} }} - model.search.options[:order].should == :created_at + expect(model.search.options[:order]).to eq(:created_at) end it "does not apply a default scope if one is not set" do - model.stub :default_sphinx_scope => nil, + allow(model).to receive_messages :default_sphinx_scope => nil, :default => {:order => :created_at} - model.search.options[:order].should be_nil + expect(model.search.options[:order]).to be_nil end end @@ -112,18 +114,18 @@ def self.name; 'SubModel'; end :populated? => false) } before :each do - ThinkingSphinx.stub :search => search - FileUtils.stub :mkdir_p => true + allow(ThinkingSphinx).to receive_messages :search => search + allow(FileUtils).to receive_messages :mkdir_p => true end it "returns the search object's total entries count" do - model.search_count.should == search.total_entries + expect(model.search_count).to eq(search.total_entries) end it "scopes the search to a given model" do model.search_count - search.options[:classes].should == [model] + expect(search.options[:classes]).to eq([model]) end end end diff --git a/spec/thinking_sphinx/active_record/callbacks/delete_callbacks_spec.rb b/spec/thinking_sphinx/active_record/callbacks/delete_callbacks_spec.rb index dd5eafbe1..91aa896ec 100644 --- a/spec/thinking_sphinx/active_record/callbacks/delete_callbacks_spec.rb +++ b/spec/thinking_sphinx/active_record/callbacks/delete_callbacks_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks do @@ -10,20 +12,20 @@ let(:callbacks) { double('callbacks', :after_destroy => nil) } before :each do - ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks. - stub :new => callbacks + allow(ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks). + to receive_messages :new => callbacks end it "builds an object from the instance" do - ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks. - should_receive(:new).with(instance).and_return(callbacks) + expect(ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks). + to receive(:new).with(instance).and_return(callbacks) ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks. after_destroy(instance) end it "invokes after_destroy on the object" do - callbacks.should_receive(:after_destroy) + expect(callbacks).to receive(:after_destroy) ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks. after_destroy(instance) @@ -32,26 +34,95 @@ describe '#after_destroy' do let(:index_set) { double 'index set', :to_a => [index] } - let(:index) { double('index', :name => 'foo_core', + let(:index) { double('index', :name => 'foo_core', :primary_key => :id, :document_id_for_key => 14, :type => 'plain', :distributed? => false) } let(:instance) { double('instance', :id => 7, :new_record? => false) } before :each do - ThinkingSphinx::IndexSet.stub :new => index_set + allow(ThinkingSphinx::IndexSet).to receive_messages :new => index_set end it "performs the deletion for the index and instance" do - ThinkingSphinx::Deletion.should_receive(:perform).with(index, 7) + expect(ThinkingSphinx::Deletion).to receive(:perform).with(index, 7) callbacks.after_destroy end it "doesn't do anything if the instance is a new record" do - instance.stub :new_record? => true + allow(instance).to receive_messages :new_record? => true + + expect(ThinkingSphinx::Deletion).not_to receive(:perform) + + callbacks.after_destroy + end + + it 'does nothing if callbacks are suspended' do + ThinkingSphinx::Callbacks.suspend! - ThinkingSphinx::Deletion.should_not_receive(:perform) + expect(ThinkingSphinx::Deletion).not_to receive(:perform) callbacks.after_destroy + + ThinkingSphinx::Callbacks.resume! + end + end + + describe '.after_rollback' do + let(:callbacks) { double('callbacks', :after_rollback => nil) } + + before :each do + allow(ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks). + to receive_messages :new => callbacks + end + + it "builds an object from the instance" do + expect(ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks). + to receive(:new).with(instance).and_return(callbacks) + + ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks. + after_rollback(instance) + end + + it "invokes after_rollback on the object" do + expect(callbacks).to receive(:after_rollback) + + ThinkingSphinx::ActiveRecord::Callbacks::DeleteCallbacks. + after_rollback(instance) + end + end + + describe '#after_rollback' do + let(:index_set) { double 'index set', :to_a => [index] } + let(:index) { double('index', :name => 'foo_core', :primary_key => :id, + :document_id_for_key => 14, :type => 'plain', :distributed? => false) } + let(:instance) { double('instance', :id => 7, :new_record? => false) } + + before :each do + allow(ThinkingSphinx::IndexSet).to receive_messages :new => index_set + end + + it "performs the deletion for the index and instance" do + expect(ThinkingSphinx::Deletion).to receive(:perform).with(index, 7) + + callbacks.after_rollback + end + + it "doesn't do anything if the instance is a new record" do + allow(instance).to receive_messages :new_record? => true + + expect(ThinkingSphinx::Deletion).not_to receive(:perform) + + callbacks.after_rollback + end + + it 'does nothing if callbacks are suspended' do + ThinkingSphinx::Callbacks.suspend! + + expect(ThinkingSphinx::Deletion).not_to receive(:perform) + + callbacks.after_rollback + + ThinkingSphinx::Callbacks.resume! end end end diff --git a/spec/thinking_sphinx/active_record/callbacks/delta_callbacks_spec.rb b/spec/thinking_sphinx/active_record/callbacks/delta_callbacks_spec.rb index d5f50f080..95ababee3 100644 --- a/spec/thinking_sphinx/active_record/callbacks/delta_callbacks_spec.rb +++ b/spec/thinking_sphinx/active_record/callbacks/delta_callbacks_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks do @@ -11,7 +13,7 @@ } before :each do - ThinkingSphinx::Configuration.stub :instance => config + allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end [:after_commit, :before_save].each do |callback| @@ -19,20 +21,20 @@ let(:callbacks) { double('callbacks', callback => nil) } before :each do - ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks. - stub :new => callbacks + allow(ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks). + to receive_messages :new => callbacks end it "builds an object from the instance" do - ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks. - should_receive(:new).with(instance).and_return(callbacks) + expect(ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks). + to receive(:new).with(instance).and_return(callbacks) ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks. send(callback, instance) end it "invokes #{callback} on the object" do - callbacks.should_receive(callback) + expect(callbacks).to receive(callback) ThinkingSphinx::ActiveRecord::Callbacks::DeltaCallbacks. send(callback, instance) @@ -42,22 +44,23 @@ describe '#after_commit' do let(:index) { - double('index', :delta? => false, :delta_processor => processor) + double('index', :delta? => false, :delta_processor => processor, + :type => 'plain') } before :each do - config.stub :index_set_class => double(:new => [index]) + allow(config).to receive_messages :index_set_class => double(:new => [index]) end context 'without delta indices' do it "does not fire a delta index when no delta indices" do - processor.should_not_receive(:index) + expect(processor).not_to receive(:index) callbacks.after_commit end it "does not delete the instance from any index" do - processor.should_not_receive(:delete) + expect(processor).not_to receive(:delete) callbacks.after_commit end @@ -65,58 +68,58 @@ context 'with delta indices' do let(:core_index) { double('index', :delta? => false, :name => 'foo_core', - :delta_processor => processor) } + :delta_processor => processor, :type => 'plain') } let(:delta_index) { double('index', :delta? => true, :name => 'foo_delta', - :delta_processor => processor) } + :delta_processor => processor, :type => 'plain') } before :each do - ThinkingSphinx::Deltas.stub :suspended? => false + allow(ThinkingSphinx::Deltas).to receive_messages :suspended? => false - config.stub :index_set_class => double( + allow(config).to receive_messages :index_set_class => double( :new => [core_index, delta_index] ) end it "only indexes delta indices" do - processor.should_receive(:index).with(delta_index) + expect(processor).to receive(:index).with(delta_index) callbacks.after_commit end it "does not process delta indices when deltas are suspended" do - ThinkingSphinx::Deltas.stub :suspended? => true + allow(ThinkingSphinx::Deltas).to receive_messages :suspended? => true - processor.should_not_receive(:index) + expect(processor).not_to receive(:index) callbacks.after_commit end it "deletes the instance from the core index" do - processor.should_receive(:delete).with(core_index, instance) + expect(processor).to receive(:delete).with(core_index, instance) callbacks.after_commit end it "does not index if model's delta flag is not true" do - processor.stub :toggled? => false + allow(processor).to receive_messages :toggled? => false - processor.should_not_receive(:index) + expect(processor).not_to receive(:index) callbacks.after_commit end it "does not delete if model's delta flag is not true" do - processor.stub :toggled? => false + allow(processor).to receive_messages :toggled? => false - processor.should_not_receive(:delete) + expect(processor).not_to receive(:delete) callbacks.after_commit end it "does not delete when deltas are suspended" do - ThinkingSphinx::Deltas.stub :suspended? => true + allow(ThinkingSphinx::Deltas).to receive_messages :suspended? => true - processor.should_not_receive(:delete) + expect(processor).not_to receive(:delete) callbacks.after_commit end @@ -125,23 +128,47 @@ describe '#before_save' do let(:index) { - double('index', :delta? => true, :delta_processor => processor) + double('index', :delta? => true, :delta_processor => processor, + :type => 'plain') } before :each do - config.stub :index_set_class => double(:new => [index]) + allow(config).to receive_messages :index_set_class => double(:new => [index]) + allow(instance).to receive_messages( + :changed? => true, + :new_record? => false + ) end it "sets delta to true if there are delta indices" do - processor.should_receive(:toggle).with(instance) + expect(processor).to receive(:toggle).with(instance) callbacks.before_save end it "does not try to set delta to true if there are no delta indices" do - index.stub :delta? => false + allow(index).to receive_messages :delta? => false + + expect(processor).not_to receive(:toggle) + + callbacks.before_save + end + + it "does not try to set delta to true if the instance is unchanged" do + allow(instance).to receive_messages :changed? => false + + expect(processor).not_to receive(:toggle) + + callbacks.before_save + end + + it "does set delta to true if the instance is unchanged but new" do + allow(instance).to receive_messages( + :changed? => false, + :new_record? => true + ) - processor.should_not_receive(:toggle) + expect(processor).to receive(:toggle) callbacks.before_save end diff --git a/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb b/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb index 050c48878..9186683b1 100644 --- a/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb +++ b/spec/thinking_sphinx/active_record/callbacks/update_callbacks_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module ActiveRecord module Callbacks; end @@ -17,12 +19,13 @@ module Callbacks; end let(:klass) { double(:name => 'Article') } let(:configuration) { double('configuration', :settings => {'attribute_updates' => true}, - :indices_for_references => [index]) } + :indices_for_references => [index], :index_set_class => set_class) } let(:connection) { double('connection', :execute => '') } let(:index) { double 'index', :name => 'article_core', :sources => [source], :document_id_for_key => 3, :distributed? => false, - :type => 'plain'} + :type => 'plain', :primary_key => :id} let(:source) { double('source', :attributes => []) } + let(:set_class) { double(:reference_name => :article) } before :each do stub_const 'ThinkingSphinx::Configuration', @@ -30,7 +33,7 @@ module Callbacks; end stub_const 'ThinkingSphinx::Connection', double stub_const 'Riddle::Query', double(:update => 'SphinxQL') - ThinkingSphinx::Connection.stub(:take).and_yield(connection) + allow(ThinkingSphinx::Connection).to receive(:take).and_yield(connection) source.attributes.replace([ double(:name => 'foo', :updateable? => true, @@ -40,35 +43,49 @@ module Callbacks; end double(:name => 'baz', :updateable? => false) ]) - instance.stub :changed => ['bar_column', 'baz'], :bar_column => 7 + allow(instance).to receive_messages( + :changed => ['bar_column', 'baz'], + :bar_column => 7, + :saved_changes => {'bar_column' => [1, 2], 'baz' => [3, 4]} + ) end it "does not send any updates to Sphinx if updates are disabled" do configuration.settings['attribute_updates'] = false - connection.should_not_receive(:execute) + expect(connection).not_to receive(:execute) callbacks.after_update end it "builds an update query with only updateable attributes that have changed" do - Riddle::Query.should_receive(:update). - with('article_core', 3, 'bar' => 7).and_return('SphinxQL') + expect(Riddle::Query).to receive(:update). + with('article_core', 3, { 'bar' => 7 }).and_return('SphinxQL') callbacks.after_update end it "sends the update query through to Sphinx" do - connection.should_receive(:execute).with('SphinxQL') + expect(connection).to receive(:execute).with('SphinxQL') callbacks.after_update end it "doesn't care if the update fails at Sphinx's end" do - connection.stub(:execute). + allow(connection).to receive(:execute). and_raise(ThinkingSphinx::ConnectionError.new('')) - lambda { callbacks.after_update }.should_not raise_error + expect { callbacks.after_update }.not_to raise_error + end + + it 'does nothing if callbacks are suspended' do + ThinkingSphinx::Callbacks.suspend! + + expect(connection).not_to receive(:execute) + + callbacks.after_update + + ThinkingSphinx::Callbacks.resume! end end end diff --git a/spec/thinking_sphinx/active_record/column_spec.rb b/spec/thinking_sphinx/active_record/column_spec.rb index 60765ddd4..af1f76bb9 100644 --- a/spec/thinking_sphinx/active_record/column_spec.rb +++ b/spec/thinking_sphinx/active_record/column_spec.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Column do describe '#__name' do it "returns the top item" do column = ThinkingSphinx::ActiveRecord::Column.new(:content) - column.__name.should == :content + expect(column.__name).to eq(:content) end end @@ -14,27 +16,27 @@ it "returns itself when it's a string column" do column = ThinkingSphinx::ActiveRecord::Column.new('foo') - column.__replace(base, replacements).collect(&:__path). - should == [['foo']] + expect(column.__replace(base, replacements).collect(&:__path)). + to eq([['foo']]) end it "returns itself when the base of the stack does not match" do column = ThinkingSphinx::ActiveRecord::Column.new(:b, :c) - column.__replace(base, replacements).collect(&:__path). - should == [[:b, :c]] + expect(column.__replace(base, replacements).collect(&:__path)). + to eq([[:b, :c]]) end it "returns an array of new columns " do column = ThinkingSphinx::ActiveRecord::Column.new(:a, :b, :e) - column.__replace(base, replacements).collect(&:__path). - should == [[:a, :c, :e], [:a, :d, :e]] + expect(column.__replace(base, replacements).collect(&:__path)). + to eq([[:a, :c, :e], [:a, :d, :e]]) end end describe '#__stack' do it "returns all but the top item" do column = ThinkingSphinx::ActiveRecord::Column.new(:users, :posts, :id) - column.__stack.should == [:users, :posts] + expect(column.__stack).to eq([:users, :posts]) end end @@ -43,28 +45,28 @@ it "shifts the current name to the stack" do column.email - column.__stack.should == [:user] + expect(column.__stack).to eq([:user]) end it "adds the new method call as the name" do column.email - column.__name.should == :email + expect(column.__name).to eq(:email) end it "returns itself" do - column.email.should == column + expect(column.email).to eq(column) end end describe '#string?' do it "is true when the name is a string" do column = ThinkingSphinx::ActiveRecord::Column.new('content') - column.should be_a_string + expect(column).to be_a_string end it "is false when the name is a symbol" do column = ThinkingSphinx::ActiveRecord::Column.new(:content) - column.should_not be_a_string + expect(column).not_to be_a_string end end end diff --git a/spec/thinking_sphinx/active_record/column_sql_presenter_spec.rb b/spec/thinking_sphinx/active_record/column_sql_presenter_spec.rb new file mode 100644 index 000000000..7f27ce65a --- /dev/null +++ b/spec/thinking_sphinx/active_record/column_sql_presenter_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ThinkingSphinx::ActiveRecord::ColumnSQLPresenter do + describe '#with_table' do + let(:model) { double 'Model' } + let(:column) { double 'Column', :__name => 'column_name', + :__stack => [], :string? => false } + let(:adapter) { double 'Adapter' } + let(:associations) { double 'Associations' } + let(:path) { double 'Path', + :model => double(:column_names => ['column_name']) } + let(:presenter) { ThinkingSphinx::ActiveRecord::ColumnSQLPresenter.new( + model, column, adapter, associations + ) } + + before do + stub_const 'Joiner::Path', double(:new => path) + allow(adapter).to receive(:quote) { |arg| "`#{arg}`" } + end + + context "when there's no explicit db name" do + before { allow(associations).to receive_messages(:alias_for => 'table_name') } + + it 'returns quoted table and column names' do + expect(presenter.with_table).to eq('`table_name`.`column_name`') + end + end + + context 'when an eplicit db name is provided' do + before { allow(associations).to receive_messages(:alias_for => 'db_name.table_name') } + + it 'returns properly quoted table name with column name' do + expect(presenter.with_table).to eq('`db_name`.`table_name`.`column_name`') + end + end + end +end diff --git a/spec/thinking_sphinx/active_record/database_adapters/abstract_adapter_spec.rb b/spec/thinking_sphinx/active_record/database_adapters/abstract_adapter_spec.rb index 5a3a45d2c..4b3820bc7 100644 --- a/spec/thinking_sphinx/active_record/database_adapters/abstract_adapter_spec.rb +++ b/spec/thinking_sphinx/active_record/database_adapters/abstract_adapter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::AbstractAdapter do @@ -9,23 +11,23 @@ describe '#quote' do it "uses the model's connection to quote columns" do - connection.should_receive(:quote_column_name).with('foo') + expect(connection).to receive(:quote_column_name).with('foo') adapter.quote 'foo' end it "returns the quoted value" do - connection.stub :quote_column_name => '"foo"' + allow(connection).to receive_messages :quote_column_name => '"foo"' - adapter.quote('foo').should == '"foo"' + expect(adapter.quote('foo')).to eq('"foo"') end end describe '#quoted_table_name' do it "passes the method through to the model" do - model.should_receive(:quoted_table_name).and_return('"articles"') + expect(model).to receive(:quoted_table_name).and_return('"articles"') - adapter.quoted_table_name.should == '"articles"' + expect(adapter.quoted_table_name).to eq('"articles"') end end end diff --git a/spec/thinking_sphinx/active_record/database_adapters/mysql_adapter_spec.rb b/spec/thinking_sphinx/active_record/database_adapters/mysql_adapter_spec.rb index 1b802b8ea..dc5bcf6f6 100644 --- a/spec/thinking_sphinx/active_record/database_adapters/mysql_adapter_spec.rb +++ b/spec/thinking_sphinx/active_record/database_adapters/mysql_adapter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter do @@ -7,50 +9,62 @@ let(:model) { double('model') } it "returns 1 for true" do - adapter.boolean_value(true).should == 1 + expect(adapter.boolean_value(true)).to eq(1) end it "returns 0 for false" do - adapter.boolean_value(false).should == 0 + expect(adapter.boolean_value(false)).to eq(0) end describe '#cast_to_string' do it "casts the clause to characters" do - adapter.cast_to_string('foo').should == "CAST(foo AS char)" + expect(adapter.cast_to_string('foo')).to eq("CAST(foo AS char)") end end describe '#cast_to_timestamp' do it "converts to unix timestamps" do - adapter.cast_to_timestamp('created_at'). - should == 'UNIX_TIMESTAMP(created_at)' + expect(adapter.cast_to_timestamp('created_at')). + to eq('UNIX_TIMESTAMP(created_at)') end end describe '#concatenate' do it "concatenates with the given separator" do - adapter.concatenate('foo, bar, baz', ','). - should == "CONCAT_WS(',', foo, bar, baz)" + expect(adapter.concatenate('foo, bar, baz', ',')). + to eq("CONCAT_WS(',', foo, bar, baz)") end end describe '#convert_nulls' do it "translates arguments to an IFNULL SQL call" do - adapter.convert_nulls('id', 5).should == 'IFNULL(id, 5)' + expect(adapter.convert_nulls('id', 5)).to eq('IFNULL(id, 5)') end end describe '#convert_blank' do it "translates arguments to a COALESCE NULLIF SQL call" do - adapter.convert_blank('id', 5).should == "COALESCE(NULLIF(id, ''), 5)" + expect(adapter.convert_blank('id', 5)).to eq("COALESCE(NULLIF(id, ''), 5)") end end - describe '#group_concatenate' do it "group concatenates the clause with the given separator" do - adapter.group_concatenate('foo', ','). - should == "GROUP_CONCAT(DISTINCT foo SEPARATOR ',')" + expect(adapter.group_concatenate('foo', ',')). + to eq("GROUP_CONCAT(DISTINCT foo SEPARATOR ',')") + end + end + + describe '#utf8_query_pre' do + it "defaults to using utf8" do + expect(adapter.utf8_query_pre).to eq(["SET NAMES utf8"]) + end + + it "allows custom values" do + ThinkingSphinx::Configuration.instance.settings['mysql_encoding'] = + 'utf8mb4' + + expect(adapter.utf8_query_pre).to eq(["SET NAMES utf8mb4"]) end end end diff --git a/spec/thinking_sphinx/active_record/database_adapters/postgresql_adapter_spec.rb b/spec/thinking_sphinx/active_record/database_adapters/postgresql_adapter_spec.rb index 4d0b967c6..03a63bd15 100644 --- a/spec/thinking_sphinx/active_record/database_adapters/postgresql_adapter_spec.rb +++ b/spec/thinking_sphinx/active_record/database_adapters/postgresql_adapter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter do @@ -8,57 +10,57 @@ describe '#boolean_value' do it "returns 'TRUE' for true" do - adapter.boolean_value(true).should == 'TRUE' + expect(adapter.boolean_value(true)).to eq('TRUE') end it "returns 'FALSE' for false" do - adapter.boolean_value(false).should == 'FALSE' + expect(adapter.boolean_value(false)).to eq('FALSE') end end describe '#cast_to_string' do it "casts the clause to characters" do - adapter.cast_to_string('foo').should == 'foo::varchar' + expect(adapter.cast_to_string('foo')).to eq('foo::varchar') end end describe '#cast_to_timestamp' do it "converts to int unix timestamps" do - adapter.cast_to_timestamp('created_at'). - should == 'extract(epoch from created_at)::int' + expect(adapter.cast_to_timestamp('created_at')). + to eq('extract(epoch from created_at)::int') end it "converts to bigint unix timestamps" do ThinkingSphinx::Configuration.instance.settings['64bit_timestamps'] = true - adapter.cast_to_timestamp('created_at'). - should == 'extract(epoch from created_at)::bigint' + expect(adapter.cast_to_timestamp('created_at')). + to eq('extract(epoch from created_at)::bigint') end end describe '#concatenate' do it "concatenates with the given separator" do - adapter.concatenate('foo, bar, baz', ','). - should == "COALESCE(foo, '') || ',' || COALESCE(bar, '') || ',' || COALESCE(baz, '')" + expect(adapter.concatenate('foo, bar, baz', ',')). + to eq("COALESCE(foo, '') || ',' || COALESCE(bar, '') || ',' || COALESCE(baz, '')") end end describe '#convert_nulls' do it "translates arguments to a COALESCE SQL call" do - adapter.convert_nulls('id', 5).should == 'COALESCE(id, 5)' + expect(adapter.convert_nulls('id', 5)).to eq('COALESCE(id, 5)') end end describe '#convert_blank' do it "translates arguments to a COALESCE NULLIF SQL call" do - adapter.convert_blank('id', 5).should == "COALESCE(NULLIF(id, ''), 5)" + expect(adapter.convert_blank('id', 5)).to eq("COALESCE(NULLIF(id, ''), 5)") end end describe '#group_concatenate' do it "group concatenates the clause with the given separator" do - adapter.group_concatenate('foo', ','). - should == "array_to_string(array_agg(DISTINCT foo), ',')" + expect(adapter.group_concatenate('foo', ',')). + to eq("array_to_string(array_agg(DISTINCT foo), ',')") end end end diff --git a/spec/thinking_sphinx/active_record/database_adapters_spec.rb b/spec/thinking_sphinx/active_record/database_adapters_spec.rb index b9b411561..9b0544c58 100644 --- a/spec/thinking_sphinx/active_record/database_adapters_spec.rb +++ b/spec/thinking_sphinx/active_record/database_adapters_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::DatabaseAdapters do @@ -5,21 +7,21 @@ describe '.adapter_for' do it "returns a MysqlAdapter object for :mysql" do - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - stub(:adapter_type_for => :mysql) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters). + to receive_messages(:adapter_type_for => :mysql) - ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model). - should be_a( + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model)). + to be_a( ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter ) end it "returns a PostgreSQLAdapter object for :postgresql" do - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - stub(:adapter_type_for => :postgresql) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters). + to receive_messages(:adapter_type_for => :postgresql) - ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model). - should be_a( + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model)). + to be_a( ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter ) end @@ -29,21 +31,21 @@ adapter_instance = double('adapter instance') ThinkingSphinx::ActiveRecord::DatabaseAdapters.default = adapter_class - adapter_class.stub!(:new => adapter_instance) + allow(adapter_class).to receive_messages(:new => adapter_instance) - ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model). - should == adapter_instance + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model)). + to eq(adapter_instance) ThinkingSphinx::ActiveRecord::DatabaseAdapters.default = nil end it "raises an exception for other responses" do - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - stub(:adapter_type_for => :sqlite) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters). + to receive_messages(:adapter_type_for => :sqlite) - lambda { + expect { ThinkingSphinx::ActiveRecord::DatabaseAdapters.adapter_for(model) - }.should raise_error + }.to raise_error(ThinkingSphinx::InvalidDatabaseAdapter) end end @@ -53,74 +55,74 @@ let(:model) { double('model', :connection => connection) } it "translates a normal MySQL adapter" do - klass.stub(:name => 'ActiveRecord::ConnectionAdapters::MysqlAdapter') + allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::MysqlAdapter') - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - adapter_type_for(model).should == :mysql + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. + adapter_type_for(model)).to eq(:mysql) end it "translates a MySQL2 adapter" do - klass.stub(:name => 'ActiveRecord::ConnectionAdapters::Mysql2Adapter') + allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::Mysql2Adapter') - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - adapter_type_for(model).should == :mysql + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. + adapter_type_for(model)).to eq(:mysql) end it "translates a normal PostgreSQL adapter" do - klass.stub(:name => 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter') + allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::PostgreSQLAdapter') - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - adapter_type_for(model).should == :postgresql + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. + adapter_type_for(model)).to eq(:postgresql) end it "translates a JDBC MySQL adapter to MySQL" do - klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') - connection.stub(:config => {:adapter => 'jdbcmysql'}) + allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') + allow(connection).to receive_messages(:config => {:adapter => 'jdbcmysql'}) - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - adapter_type_for(model).should == :mysql + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. + adapter_type_for(model)).to eq(:mysql) end it "translates a JDBC PostgreSQL adapter to PostgreSQL" do - klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') - connection.stub(:config => {:adapter => 'jdbcpostgresql'}) + allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') + allow(connection).to receive_messages(:config => {:adapter => 'jdbcpostgresql'}) - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - adapter_type_for(model).should == :postgresql + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. + adapter_type_for(model)).to eq(:postgresql) end it "translates a JDBC adapter with MySQL connection string to MySQL" do - klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') - connection.stub(:config => {:adapter => 'jdbc', + allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') + allow(connection).to receive_messages(:config => {:adapter => 'jdbc', :url => 'jdbc:mysql://127.0.0.1:3306/sphinx'}) - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - adapter_type_for(model).should == :mysql + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. + adapter_type_for(model)).to eq(:mysql) end it "translates a JDBC adapter with PostgresSQL connection string to PostgresSQL" do - klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') - connection.stub(:config => {:adapter => 'jdbc', + allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') + allow(connection).to receive_messages(:config => {:adapter => 'jdbc', :url => 'jdbc:postgresql://127.0.0.1:3306/sphinx'}) - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - adapter_type_for(model).should == :postgresql + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. + adapter_type_for(model)).to eq(:postgresql) end it "returns other JDBC adapters without translation" do - klass.stub(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') - connection.stub(:config => {:adapter => 'jdbcmssql'}) + allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::JdbcAdapter') + allow(connection).to receive_messages(:config => {:adapter => 'jdbcmssql'}) - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - adapter_type_for(model).should == 'jdbcmssql' + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. + adapter_type_for(model)).to eq('jdbcmssql') end it "returns other unknown adapters without translation" do - klass.stub(:name => 'ActiveRecord::ConnectionAdapters::FooAdapter') + allow(klass).to receive_messages(:name => 'ActiveRecord::ConnectionAdapters::FooAdapter') - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - adapter_type_for(model). - should == 'ActiveRecord::ConnectionAdapters::FooAdapter' + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters. + adapter_type_for(model)). + to eq('ActiveRecord::ConnectionAdapters::FooAdapter') end end end diff --git a/spec/thinking_sphinx/active_record/field_spec.rb b/spec/thinking_sphinx/active_record/field_spec.rb index 692da5e9b..8b6e20f99 100644 --- a/spec/thinking_sphinx/active_record/field_spec.rb +++ b/spec/thinking_sphinx/active_record/field_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Field do @@ -7,16 +9,16 @@ let(:model) { double('model') } before :each do - column.stub! :to_a => [column] + allow(column).to receive_messages :to_a => [column] end describe '#columns' do it 'returns the provided Column object' do - field.columns.should == [column] + expect(field.columns).to eq([column]) end it 'translates symbols to Column objects' do - ThinkingSphinx::ActiveRecord::Column.should_receive(:new).with(:title). + expect(ThinkingSphinx::ActiveRecord::Column).to receive(:new).with(:title). and_return(column) ThinkingSphinx::ActiveRecord::Field.new model, :title @@ -25,25 +27,25 @@ describe '#file?' do it "defaults to false" do - field.should_not be_file + expect(field).not_to be_file end it "is true if file option is set" do field = ThinkingSphinx::ActiveRecord::Field.new model, column, :file => true - field.should be_file + expect(field).to be_file end end describe '#with_attribute?' do it "defaults to false" do - field.should_not be_with_attribute + expect(field).not_to be_with_attribute end it "is true if the field is sortable" do field = ThinkingSphinx::ActiveRecord::Field.new model, column, :sortable => true - field.should be_with_attribute + expect(field).to be_with_attribute end end end diff --git a/spec/thinking_sphinx/active_record/filter_reflection_spec.rb b/spec/thinking_sphinx/active_record/filter_reflection_spec.rb index 0bef35593..600edf7f9 100644 --- a/spec/thinking_sphinx/active_record/filter_reflection_spec.rb +++ b/spec/thinking_sphinx/active_record/filter_reflection_spec.rb @@ -1,35 +1,67 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::FilterReflection do describe '.call' do let(:reflection) { double('Reflection', :macro => :has_some, :options => options, :active_record => double, :name => 'baz', - :foreign_type => :foo_type, :class => reflection_klass) } + :foreign_type => :foo_type, :class => original_klass, + :build_join_constraint => nil) } let(:options) { {:polymorphic => true} } let(:filtered_reflection) { double 'filtered reflection' } - let(:reflection_klass) { double :new => filtered_reflection } + let(:original_klass) { double } + let(:subclass) { double :include => true } before :each do - reflection.active_record.stub_chain(:connection, :quote_column_name). + allow(reflection.active_record).to receive_message_chain(:connection, :quote_column_name). and_return('"foo_type"') + + if ActiveRecord::VERSION::STRING.to_f < 5.2 + allow(original_klass).to receive(:new).and_return(filtered_reflection) + else + allow(Class).to receive(:new).with(original_klass).and_return(subclass) + allow(subclass).to receive(:new).and_return(filtered_reflection) + end + end + + class ArgumentsWrapper + attr_reader :macro, :name, :scope, :options, :parent + + def initialize(*arguments) + if ActiveRecord::VERSION::STRING.to_f < 4.0 + @macro, @name, @options, @parent = arguments + elsif ActiveRecord::VERSION::STRING.to_f < 4.2 + @macro, @name, @scope, @options, @parent = arguments + else + @name, @scope, @options, @parent = arguments + end + end + end + + def reflection_klass + ActiveRecord::VERSION::STRING.to_f < 5.2 ? original_klass : subclass + end + + def expected_reflection_arguments + expect(reflection_klass).to receive(:new) do |*arguments| + yield ArgumentsWrapper.new(*arguments) + end end it "uses the existing reflection's macro" do - reflection_klass.should_receive(:new). - with(:has_some, anything, anything, anything) + expect(reflection_klass).to receive(:new) do |macro, *args| + expect(macro).to eq(:has_some) + end ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) - end unless defined?(ActiveRecord::Reflection::MacroReflection) + end if ActiveRecord::VERSION::STRING.to_f < 4.2 it "uses the supplied name" do - if defined?(ActiveRecord::Reflection::MacroReflection) - reflection_klass.should_receive(:new). - with('foo_bar', anything, anything, anything) - else - reflection_klass.should_receive(:new). - with(anything, 'foo_bar', anything, anything) + expected_reflection_arguments do |wrapper| + expect(wrapper.name).to eq('foo_bar') end ThinkingSphinx::ActiveRecord::FilterReflection.call( @@ -38,12 +70,8 @@ end it "uses the existing reflection's parent" do - if defined?(ActiveRecord::Reflection::MacroReflection) - reflection_klass.should_receive(:new). - with(anything, anything, anything, reflection.active_record) - else - reflection_klass.should_receive(:new). - with(anything, anything, anything, reflection.active_record) + expected_reflection_arguments do |wrapper| + expect(wrapper.parent).to eq(reflection.active_record) end ThinkingSphinx::ActiveRecord::FilterReflection.call( @@ -52,14 +80,8 @@ end it "removes the polymorphic setting from the options" do - if defined?(ActiveRecord::Reflection::MacroReflection) - reflection_klass.should_receive(:new) do |name, scope, options, parent| - options[:polymorphic].should be_nil - end - else - reflection_klass.should_receive(:new) do |macro, name, options, parent| - options[:polymorphic].should be_nil - end + expected_reflection_arguments do |wrapper| + expect(wrapper.options[:polymorphic]).to be_nil end ThinkingSphinx::ActiveRecord::FilterReflection.call( @@ -68,14 +90,8 @@ end it "adds the class name option" do - if defined?(ActiveRecord::Reflection::MacroReflection) - reflection_klass.should_receive(:new) do |name, scope, options, parent| - options[:class_name].should == 'Bar' - end - else - reflection_klass.should_receive(:new) do |macro, name, options, parent| - options[:class_name].should == 'Bar' - end + expected_reflection_arguments do |wrapper| + expect(wrapper.options[:class_name]).to eq('Bar') end ThinkingSphinx::ActiveRecord::FilterReflection.call( @@ -84,14 +100,8 @@ end it "sets the foreign key if necessary" do - if defined?(ActiveRecord::Reflection::MacroReflection) - reflection_klass.should_receive(:new) do |name, scope, options, parent| - options[:foreign_key].should == 'baz_id' - end - else - reflection_klass.should_receive(:new) do |macro, name, options, parent| - options[:foreign_key].should == 'baz_id' - end + expected_reflection_arguments do |wrapper| + expect(wrapper.options[:foreign_key]).to eq('baz_id') end ThinkingSphinx::ActiveRecord::FilterReflection.call( @@ -102,14 +112,8 @@ it "respects supplied foreign keys" do options[:foreign_key] = 'qux_id' - if defined?(ActiveRecord::Reflection::MacroReflection) - reflection_klass.should_receive(:new) do |name, scope, options, parent| - options[:foreign_key].should == 'qux_id' - end - else - reflection_klass.should_receive(:new) do |macro, name, options, parent| - options[:foreign_key].should == 'qux_id' - end + expected_reflection_arguments do |wrapper| + expect(wrapper.options[:foreign_key]).to eq('qux_id') end ThinkingSphinx::ActiveRecord::FilterReflection.call( @@ -117,56 +121,87 @@ ) end - it "sets conditions if there are none" do - reflection_klass.should_receive(:new) do |macro, name, options, parent| - options[:conditions].should == "::ts_join_alias::.\"foo_type\" = 'Bar'" + if ActiveRecord::VERSION::STRING.to_f < 4.0 + it "sets conditions if there are none" do + expect(reflection_klass).to receive(:new) do |macro, name, options, parent| + expect(options[:conditions]).to eq("::ts_join_alias::.\"foo_type\" = 'Bar'") + end + + ThinkingSphinx::ActiveRecord::FilterReflection.call( + reflection, 'foo_bar', 'Bar' + ) end - ThinkingSphinx::ActiveRecord::FilterReflection.call( - reflection, 'foo_bar', 'Bar' - ) - end unless defined?(ActiveRecord::Reflection::MacroReflection) + it "appends to the conditions array" do + options[:conditions] = ['existing'] - it "appends to the conditions array" do - options[:conditions] = ['existing'] + expect(reflection_klass).to receive(:new) do |macro, name, options, parent| + expect(options[:conditions]).to eq(['existing', "::ts_join_alias::.\"foo_type\" = 'Bar'"]) + end - reflection_klass.should_receive(:new) do |macro, name, options, parent| - options[:conditions].should == ['existing', "::ts_join_alias::.\"foo_type\" = 'Bar'"] + ThinkingSphinx::ActiveRecord::FilterReflection.call( + reflection, 'foo_bar', 'Bar' + ) end - ThinkingSphinx::ActiveRecord::FilterReflection.call( - reflection, 'foo_bar', 'Bar' - ) - end unless defined?(ActiveRecord::Reflection::MacroReflection) + it "extends the conditions hash" do + options[:conditions] = {:x => :y} - it "extends the conditions hash" do - options[:conditions] = {:x => :y} + expect(reflection_klass).to receive(:new) do |macro, name, options, parent| + expect(options[:conditions]).to eq({:x => :y, :foo_type => 'Bar'}) + end - reflection_klass.should_receive(:new) do |macro, name, options, parent| - options[:conditions].should == {:x => :y, :foo_type => 'Bar'} + ThinkingSphinx::ActiveRecord::FilterReflection.call( + reflection, 'foo_bar', 'Bar' + ) end + it "appends to the conditions string" do + options[:conditions] = 'existing' + + expect(reflection_klass).to receive(:new) do |macro, name, options, parent| + expect(options[:conditions]).to eq("existing AND ::ts_join_alias::.\"foo_type\" = 'Bar'") + end + + ThinkingSphinx::ActiveRecord::FilterReflection.call( + reflection, 'foo_bar', 'Bar' + ) + end + else + it "does not add a conditions option" do + expected_reflection_arguments do |wrapper| + expect(wrapper.options.keys).not_to include(:conditions) + end + + ThinkingSphinx::ActiveRecord::FilterReflection.call( + reflection, 'foo_bar', 'Bar' + ) + end + end + + it "includes custom behaviour in the subclass" do + expect(subclass).to receive(:include).with(ThinkingSphinx::ActiveRecord::Depolymorph::OverriddenReflection::BuildJoinConstraint) + ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) - end unless defined?(ActiveRecord::Reflection::MacroReflection) + end if ActiveRecord::VERSION::STRING.to_f > 5.1 - it "appends to the conditions string" do - options[:conditions] = 'existing' + it "includes custom behaviour in the subclass" do + allow(reflection).to receive(:respond_to?).with(:build_join_constraint). + and_return(false) - reflection_klass.should_receive(:new) do |macro, name, options, parent| - options[:conditions].should == "existing AND ::ts_join_alias::.\"foo_type\" = 'Bar'" - end + expect(subclass).to receive(:include).with(ThinkingSphinx::ActiveRecord::Depolymorph::OverriddenReflection::JoinScope) ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' ) - end unless defined?(ActiveRecord::Reflection::MacroReflection) + end if ActiveRecord::VERSION::STRING.to_f >= 6.0 it "returns the new reflection" do - ThinkingSphinx::ActiveRecord::FilterReflection.call( + expect(ThinkingSphinx::ActiveRecord::FilterReflection.call( reflection, 'foo_bar', 'Bar' - ).should == filtered_reflection + )).to eq(filtered_reflection) end end end diff --git a/spec/thinking_sphinx/active_record/index_spec.rb b/spec/thinking_sphinx/active_record/index_spec.rb index 759e81a79..3448f2eb0 100644 --- a/spec/thinking_sphinx/active_record/index_spec.rb +++ b/spec/thinking_sphinx/active_record/index_spec.rb @@ -1,46 +1,50 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Index do let(:index) { ThinkingSphinx::ActiveRecord::Index.new :user } - let(:indices_path) { double('indices path', :join => '') } let(:config) { double('config', :settings => {}, - :indices_location => indices_path, :next_offset => 8) } + :indices_location => 'location', :next_offset => 8, + :index_set_class => index_set_class) } + let(:index_set_class) { double :reference_name => :user } before :each do - ThinkingSphinx::Configuration.stub :instance => config + allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end describe '#append_source' do - let(:model) { double('model', :primary_key => :id) } + let(:model) { double('model', :primary_key => :id, + :table_exists? => true) } let(:source) { double('source') } before :each do - ActiveSupport::Inflector.stub(:constantize => model) - ThinkingSphinx::ActiveRecord::SQLSource.stub :new => source - config.stub :next_offset => 17 + allow(ActiveSupport::Inflector).to receive_messages(:constantize => model) + allow(ThinkingSphinx::ActiveRecord::SQLSource).to receive_messages :new => source + allow(config).to receive_messages :next_offset => 17 end it "adds a source to the index" do - index.sources.should_receive(:<<).with(source) + expect(index.sources).to receive(:<<).with(source) index.append_source end it "creates the source with the index's offset" do - ThinkingSphinx::ActiveRecord::SQLSource.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::SQLSource).to receive(:new). with(model, hash_including(:offset => 17)).and_return(source) index.append_source end it "returns the new source" do - index.append_source.should == source + expect(index.append_source).to eq(source) end it "defaults to the model's primary key" do - model.stub :primary_key => :sphinx_id + allow(model).to receive_messages :primary_key => :sphinx_id - ThinkingSphinx::ActiveRecord::SQLSource.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::SQLSource).to receive(:new). with(model, hash_including(:primary_key => :sphinx_id)). and_return(source) @@ -48,9 +52,9 @@ end it "uses a custom column when set" do - model.stub :primary_key => :sphinx_id + allow(model).to receive_messages :primary_key => :sphinx_id - ThinkingSphinx::ActiveRecord::SQLSource.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::SQLSource).to receive(:new). with(model, hash_including(:primary_key => :custom_sphinx_id)). and_return(source) @@ -60,9 +64,9 @@ end it "defaults to id if no primary key is set" do - model.stub :primary_key => nil + allow(model).to receive_messages :primary_key => nil - ThinkingSphinx::ActiveRecord::SQLSource.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::SQLSource).to receive(:new). with(model, hash_including(:primary_key => :id)). and_return(source) @@ -72,12 +76,12 @@ describe '#delta?' do it "defaults to false" do - index.should_not be_delta + expect(index).not_to be_delta end it "reflects the delta? option" do index = ThinkingSphinx::ActiveRecord::Index.new :user, :delta? => true - index.should be_delta + expect(index).to be_delta end end @@ -88,16 +92,16 @@ index = ThinkingSphinx::ActiveRecord::Index.new :user, :delta_processor => processor_class - index.delta_processor.should == processor + expect(index.delta_processor).to eq(processor) end end describe '#document_id_for_key' do it "calculates the document id based on offset and number of indices" do - config.stub_chain(:indices, :count).and_return(5) - config.stub :next_offset => 7 + allow(config).to receive_message_chain(:indices, :count).and_return(5) + allow(config).to receive_messages :next_offset => 7 - index.document_id_for_key(123).should == 622 + expect(index.document_id_for_key(123)).to eq(622) end end @@ -109,14 +113,14 @@ end it "interprets the definition block" do - ThinkingSphinx::ActiveRecord::Interpreter.should_receive(:translate!). + expect(ThinkingSphinx::ActiveRecord::Interpreter).to receive(:translate!). with(index, block) index.interpret_definition! end it "only interprets the definition block once" do - ThinkingSphinx::ActiveRecord::Interpreter.should_receive(:translate!). + expect(ThinkingSphinx::ActiveRecord::Interpreter).to receive(:translate!). once index.interpret_definition! @@ -128,13 +132,13 @@ let(:model) { double('model') } it "translates symbol references to model class" do - ActiveSupport::Inflector.stub(:constantize => model) + allow(ActiveSupport::Inflector).to receive_messages(:constantize => model) - index.model.should == model + expect(index.model).to eq(model) end it "memoizes the result" do - ActiveSupport::Inflector.should_receive(:constantize).with('User').once. + expect(ActiveSupport::Inflector).to receive(:constantize).with('User').once. and_return(model) index.model @@ -145,7 +149,7 @@ describe '#morphology' do context 'with a render' do before :each do - FileUtils.stub :mkdir_p => true + allow(FileUtils).to receive_messages :mkdir_p => true end it "defaults to nil" do @@ -154,7 +158,7 @@ rescue Riddle::Configuration::ConfigurationError end - index.morphology.should be_nil + expect(index.morphology).to be_nil end it "reads from the settings file if provided" do @@ -165,7 +169,7 @@ rescue Riddle::Configuration::ConfigurationError end - index.morphology.should == 'stem_en' + expect(index.morphology).to eq('stem_en') end end end @@ -173,26 +177,26 @@ describe '#name' do it "uses the core suffix by default" do index = ThinkingSphinx::ActiveRecord::Index.new :user - index.name.should == 'user_core' + expect(index.name).to eq('user_core') end it "uses the delta suffix when delta? is true" do index = ThinkingSphinx::ActiveRecord::Index.new :user, :delta? => true - index.name.should == 'user_delta' + expect(index.name).to eq('user_delta') end end describe '#offset' do before :each do - config.stub :next_offset => 4 + allow(config).to receive_messages :next_offset => 4 end it "uses the next offset value from the configuration" do - index.offset.should == 4 + expect(index.offset).to eq(4) end it "uses the reference to get a unique offset" do - config.should_receive(:next_offset).with(:user).and_return(2) + expect(config).to receive(:next_offset).with(:user).and_return(2) index.offset end @@ -200,11 +204,11 @@ describe '#render' do before :each do - FileUtils.stub :mkdir_p => true + allow(FileUtils).to receive_messages :mkdir_p => true end it "interprets the provided definition" do - index.should_receive(:interpret_definition!).at_least(:once) + expect(index).to receive(:interpret_definition!).at_least(:once) begin index.render diff --git a/spec/thinking_sphinx/active_record/interpreter_spec.rb b/spec/thinking_sphinx/active_record/interpreter_spec.rb index 3b497ff74..68d61f19e 100644 --- a/spec/thinking_sphinx/active_record/interpreter_spec.rb +++ b/spec/thinking_sphinx/active_record/interpreter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Interpreter do @@ -13,23 +15,28 @@ let(:block) { Proc.new { } } before :each do - ThinkingSphinx::ActiveRecord::SQLSource.stub! :new => source - source.stub :model => model + allow(ThinkingSphinx::ActiveRecord::SQLSource).to receive_messages( + :new => source + ) + + allow(source).to receive_messages( + :model => model, :add_attribute => nil, :add_field => nil + ) end describe '.translate!' do let(:instance) { double('interpreter', :translate! => true) } it "creates a new interpreter instance with the given block and index" do - ThinkingSphinx::ActiveRecord::Interpreter.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::Interpreter).to receive(:new). with(index, block).and_return(instance) ThinkingSphinx::ActiveRecord::Interpreter.translate! index, block end it "calls translate! on the instance" do - ThinkingSphinx::ActiveRecord::Interpreter.stub!(:new => instance) - instance.should_receive(:translate!) + allow(ThinkingSphinx::ActiveRecord::Interpreter).to receive_messages(:new => instance) + expect(instance).to receive(:translate!) ThinkingSphinx::ActiveRecord::Interpreter.translate! index, block end @@ -37,13 +44,13 @@ describe '#group_by' do it "adds a source to the index" do - index.should_receive(:append_source).and_return(source) + expect(index).to receive(:append_source).and_return(source) instance.group_by 'lat' end it "only adds a single source for the given context" do - index.should_receive(:append_source).once.and_return(source) + expect(index).to receive(:append_source).once.and_return(source) instance.group_by 'lat' instance.group_by 'lng' @@ -52,7 +59,7 @@ it "appends a new grouping statement to the source" do instance.group_by 'lat' - source.groupings.should include('lat') + expect(source.groupings).to include('lat') end end @@ -61,48 +68,46 @@ let(:attribute) { double('attribute') } before :each do - ThinkingSphinx::ActiveRecord::Attribute.stub! :new => attribute + allow(ThinkingSphinx::ActiveRecord::Attribute).to receive_messages :new => attribute end it "adds a source to the index" do - index.should_receive(:append_source).and_return(source) + expect(index).to receive(:append_source).and_return(source) instance.has column end it "only adds a single source for the given context" do - index.should_receive(:append_source).once.and_return(source) + expect(index).to receive(:append_source).once.and_return(source) instance.has column instance.has column end it "creates a new attribute with the provided column" do - ThinkingSphinx::ActiveRecord::Attribute.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::Attribute).to receive(:new). with(model, column, {}).and_return(attribute) instance.has column end it "passes through options to the attribute" do - ThinkingSphinx::ActiveRecord::Attribute.should_receive(:new). - with(model, column, :as => :other_name).and_return(attribute) + expect(ThinkingSphinx::ActiveRecord::Attribute).to receive(:new). + with(model, column, { :as => :other_name }).and_return(attribute) instance.has column, :as => :other_name end it "adds an attribute to the source" do - instance.has column + expect(source).to receive(:add_attribute).with(attribute) - source.attributes.should include(attribute) + instance.has column end it "adds multiple attributes when passed multiple columns" do - instance.has column, column + expect(source).to receive(:add_attribute).with(attribute).twice - source.attributes.select { |saved_attribute| - saved_attribute == attribute - }.length.should == 2 + instance.has column, column end end @@ -111,48 +116,46 @@ let(:field) { double('field') } before :each do - ThinkingSphinx::ActiveRecord::Field.stub! :new => field + allow(ThinkingSphinx::ActiveRecord::Field).to receive_messages :new => field end it "adds a source to the index" do - index.should_receive(:append_source).and_return(source) + expect(index).to receive(:append_source).and_return(source) instance.indexes column end it "only adds a single source for the given context" do - index.should_receive(:append_source).once.and_return(source) + expect(index).to receive(:append_source).once.and_return(source) instance.indexes column instance.indexes column end it "creates a new field with the provided column" do - ThinkingSphinx::ActiveRecord::Field.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::Field).to receive(:new). with(model, column, {}).and_return(field) instance.indexes column end it "passes through options to the field" do - ThinkingSphinx::ActiveRecord::Field.should_receive(:new). - with(model, column, :as => :other_name).and_return(field) + expect(ThinkingSphinx::ActiveRecord::Field).to receive(:new). + with(model, column, { :as => :other_name }).and_return(field) instance.indexes column, :as => :other_name end it "adds a field to the source" do - instance.indexes column + expect(source).to receive(:add_field).with(field) - source.fields.should include(field) + instance.indexes column end it "adds multiple fields when passed multiple columns" do - instance.indexes column, column + expect(source).to receive(:add_field).with(field).twice - source.fields.select { |saved_field| - saved_field == field - }.length.should == 2 + instance.indexes column, column end end @@ -161,24 +164,24 @@ let(:association) { double('association') } before :each do - ThinkingSphinx::ActiveRecord::Association.stub! :new => association + allow(ThinkingSphinx::ActiveRecord::Association).to receive_messages :new => association end it "adds a source to the index" do - index.should_receive(:append_source).and_return(source) + expect(index).to receive(:append_source).and_return(source) instance.join column end it "only adds a single source for the given context" do - index.should_receive(:append_source).once.and_return(source) + expect(index).to receive(:append_source).once.and_return(source) instance.join column instance.join column end it "creates a new association with the provided column" do - ThinkingSphinx::ActiveRecord::Association.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::Association).to receive(:new). with(column).and_return(association) instance.join column @@ -187,15 +190,15 @@ it "adds an association to the source" do instance.join column - source.associations.should include(association) + expect(source.associations).to include(association) end it "adds multiple fields when passed multiple columns" do instance.join column, column - source.associations.select { |saved_assoc| + expect(source.associations.select { |saved_assoc| saved_assoc == association - }.length.should == 2 + }.length).to eq(2) end end @@ -203,15 +206,15 @@ let(:column) { double('column') } before :each do - ThinkingSphinx::ActiveRecord::Column.stub!(:new => column) + allow(ThinkingSphinx::ActiveRecord::Column).to receive_messages(:new => column) end it "returns a new column for the given method" do - instance.id.should == column + expect(instance.id).to eq(column) end it "should initialise the column with the method name and arguments" do - ThinkingSphinx::ActiveRecord::Column.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::Column).to receive(:new). with(:users, :posts, :subject).and_return(column) instance.users(:posts, :subject) @@ -220,26 +223,26 @@ describe '#set_database' do before :each do - source.stub :set_database_settings => true + allow(source).to receive_messages :set_database_settings => true stub_const 'ActiveRecord::Base', - double(:configurations => {'other' => {:baz => :qux}}) + double(:configurations => {'other' => {'baz' => 'qux'}}) end it "sends through a hash if provided" do - source.should_receive(:set_database_settings).with(:foo => :bar) + expect(source).to receive(:set_database_settings).with({ :foo => :bar }) instance.set_database :foo => :bar end it "finds the environment settings if given a string key" do - source.should_receive(:set_database_settings).with(:baz => :qux) + expect(source).to receive(:set_database_settings).with({ :baz => 'qux' }) instance.set_database 'other' end it "finds the environment settings if given a symbol key" do - source.should_receive(:set_database_settings).with(:baz => :qux) + expect(source).to receive(:set_database_settings).with({ :baz => 'qux' }) instance.set_database :other end @@ -247,19 +250,19 @@ describe '#set_property' do before :each do - index.class.stub :settings => [:morphology] - source.class.stub :settings => [:mysql_ssl_cert] + allow(index.class).to receive_messages :settings => [:morphology] + allow(source.class).to receive_messages :settings => [:mysql_ssl_cert] end it 'saves other settings as index options' do instance.set_property :field_weights => {:name => 10} - index.options[:field_weights].should == {:name => 10} + expect(index.options[:field_weights]).to eq({:name => 10}) end context 'index settings' do it "sets the provided setting" do - index.should_receive(:morphology=).with('stem_en') + expect(index).to receive(:morphology=).with('stem_en') instance.set_property :morphology => 'stem_en' end @@ -267,24 +270,24 @@ context 'source settings' do before :each do - source.stub :mysql_ssl_cert= => true + allow(source).to receive_messages :mysql_ssl_cert= => true end it "adds a source to the index" do - index.should_receive(:append_source).and_return(source) + expect(index).to receive(:append_source).and_return(source) instance.set_property :mysql_ssl_cert => 'private.cert' end it "only adds a single source for the given context" do - index.should_receive(:append_source).once.and_return(source) + expect(index).to receive(:append_source).once.and_return(source) instance.set_property :mysql_ssl_cert => 'private.cert' instance.set_property :mysql_ssl_cert => 'private.cert' end it "sets the provided setting" do - source.should_receive(:mysql_ssl_cert=).with('private.cert') + expect(source).to receive(:mysql_ssl_cert=).with('private.cert') instance.set_property :mysql_ssl_cert => 'private.cert' end @@ -298,20 +301,20 @@ } interpreter = ThinkingSphinx::ActiveRecord::Interpreter.new index, block - interpreter.translate!. - should == interpreter.__id__ + expect(interpreter.translate!). + to eq(interpreter.__id__) end end describe '#where' do it "adds a source to the index" do - index.should_receive(:append_source).and_return(source) + expect(index).to receive(:append_source).and_return(source) instance.where 'id > 100' end it "only adds a single source for the given context" do - index.should_receive(:append_source).once.and_return(source) + expect(index).to receive(:append_source).once.and_return(source) instance.where 'id > 100' instance.where 'id < 150' @@ -320,7 +323,7 @@ it "appends a new grouping statement to the source" do instance.where 'id > 100' - source.conditions.should include('id > 100') + expect(source.conditions).to include('id > 100') end end end diff --git a/spec/thinking_sphinx/active_record/polymorpher_spec.rb b/spec/thinking_sphinx/active_record/polymorpher_spec.rb index 646c1c0ef..671c6e73d 100644 --- a/spec/thinking_sphinx/active_record/polymorpher_spec.rb +++ b/spec/thinking_sphinx/active_record/polymorpher_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::Polymorpher do @@ -23,29 +25,29 @@ let(:animal_reflection) { double 'Animal Reflection' } before :each do - ThinkingSphinx::ActiveRecord::FilterReflection. - stub(:call). + allow(ThinkingSphinx::ActiveRecord::FilterReflection). + to receive(:call). and_return(article_reflection, animal_reflection) - model.stub(:reflect_on_association) do |name| + allow(model).to receive(:reflect_on_association) do |name| name == :foo ? reflection : nil end if ActiveRecord::Reflection.respond_to?(:add_reflection) - ActiveRecord::Reflection.stub :add_reflection + allow(ActiveRecord::Reflection).to receive :add_reflection end end it "creates a new reflection for each class" do - ThinkingSphinx::ActiveRecord::FilterReflection. - unstub :call + allow(ThinkingSphinx::ActiveRecord::FilterReflection). + to receive(:call).and_call_original - ThinkingSphinx::ActiveRecord::FilterReflection. - should_receive(:call). + expect(ThinkingSphinx::ActiveRecord::FilterReflection). + to receive(:call). with(reflection, :foo_article, 'Article'). and_return(article_reflection) - ThinkingSphinx::ActiveRecord::FilterReflection. - should_receive(:call). + expect(ThinkingSphinx::ActiveRecord::FilterReflection). + to receive(:call). with(reflection, :foo_animal, 'Animal'). and_return(animal_reflection) @@ -54,9 +56,9 @@ it "adds the new reflections to the end-of-stack model" do if ActiveRecord::Reflection.respond_to?(:add_reflection) - ActiveRecord::Reflection.should_receive(:add_reflection). + expect(ActiveRecord::Reflection).to receive(:add_reflection). with(model, :foo_article, article_reflection) - ActiveRecord::Reflection.should_receive(:add_reflection). + expect(ActiveRecord::Reflection).to receive(:add_reflection). with(model, :foo_animal, animal_reflection) polymorpher.morph! @@ -69,14 +71,14 @@ end it "rebases each field" do - field.should_receive(:rebase).with([:a, :b, :foo], + expect(field).to receive(:rebase).with([:a, :b, :foo], :to => [[:a, :b, :foo_article], [:a, :b, :foo_animal]]) polymorpher.morph! end it "rebases each attribute" do - attribute.should_receive(:rebase).with([:a, :b, :foo], + expect(attribute).to receive(:rebase).with([:a, :b, :foo], :to => [[:a, :b, :foo_article], [:a, :b, :foo_animal]]) polymorpher.morph! diff --git a/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb b/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb index 828fb3030..d5eb92067 100644 --- a/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb +++ b/spec/thinking_sphinx/active_record/property_sql_presenter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::PropertySQLPresenter do @@ -7,7 +9,7 @@ let(:path) { double :aggregate? => false, :model => model } before :each do - adapter.stub(:quote) { |column| column } + allow(adapter).to receive(:quote) { |column| column } stub_const 'Joiner::Path', double(:new => path) end @@ -25,95 +27,95 @@ describe '#to_group' do it "returns the column name as a string" do - presenter.to_group.should == 'articles.title' + expect(presenter.to_group).to eq('articles.title') end it "gets the column's table alias from the associations object" do - column.stub!(:__stack => [:users, :posts]) + allow(column).to receive_messages(:__stack => [:users, :posts]) - associations.should_receive(:alias_for).with([:users, :posts]). + expect(associations).to receive(:alias_for).with([:users, :posts]). and_return('posts') presenter.to_group end it "returns nil if the property is an aggregate" do - path.stub! :aggregate? => true + allow(path).to receive_messages :aggregate? => true - presenter.to_group.should be_nil + expect(presenter.to_group).to be_nil end it "returns nil if the field is sourced via a separate query" do - field.stub :source_type => 'query' + allow(field).to receive_messages :source_type => 'query' - presenter.to_group.should be_nil + expect(presenter.to_group).to be_nil end end describe '#to_select' do it "returns the column name as a string" do - presenter.to_select.should == 'articles.title AS title' + expect(presenter.to_select).to eq('articles.title AS title') end it "gets the column's table alias from the associations object" do - column.stub!(:__stack => [:users, :posts]) + allow(column).to receive_messages(:__stack => [:users, :posts]) - associations.should_receive(:alias_for).with([:users, :posts]). + expect(associations).to receive(:alias_for).with([:users, :posts]). and_return('posts') presenter.to_select end it "returns the column name with an alias when provided" do - field.stub!(:name => :subject) + allow(field).to receive_messages(:name => :subject) - presenter.to_select.should == 'articles.title AS subject' + expect(presenter.to_select).to eq('articles.title AS subject') end it "groups and concatenates aggregated columns" do - adapter.stub :group_concatenate do |clause, separator| + allow(adapter).to receive :group_concatenate do |clause, separator| "GROUP_CONCAT(#{clause} SEPARATOR '#{separator}')" end - path.stub! :aggregate? => true + allow(path).to receive_messages :aggregate? => true - presenter.to_select. - should == "GROUP_CONCAT(articles.title SEPARATOR ' ') AS title" + expect(presenter.to_select). + to eq("GROUP_CONCAT(articles.title SEPARATOR ' ') AS title") end it "concatenates multiple columns" do - adapter.stub :concatenate do |clause, separator| + allow(adapter).to receive :concatenate do |clause, separator| "CONCAT_WS('#{separator}', #{clause})" end - field.stub!(:columns => [column, column]) + allow(field).to receive_messages(:columns => [column, column]) - presenter.to_select. - should == "CONCAT_WS(' ', articles.title, articles.title) AS title" + expect(presenter.to_select). + to eq("CONCAT_WS(' ', articles.title, articles.title) AS title") end it "does not include columns that don't exist" do - adapter.stub :concatenate do |clause, separator| + allow(adapter).to receive :concatenate do |clause, separator| "CONCAT_WS('#{separator}', #{clause})" end - field.stub!(:columns => [column, double('column', :string? => false, + allow(field).to receive_messages(:columns => [column, double('column', :string? => false, :__stack => [], :__name => 'body')]) - presenter.to_select. - should == "CONCAT_WS(' ', articles.title) AS title" + expect(presenter.to_select). + to eq("CONCAT_WS(' ', articles.title) AS title") end it "returns nil for query sourced fields" do - field.stub :source_type => :query + allow(field).to receive_messages :source_type => :query - presenter.to_select.should be_nil + expect(presenter.to_select).to be_nil end it "returns nil for ranged query sourced fields" do - field.stub :source_type => :ranged_query + allow(field).to receive_messages :source_type => :ranged_query - presenter.to_select.should be_nil + expect(presenter.to_select).to be_nil end end end @@ -131,131 +133,131 @@ :__name => 'created_at') } before :each do - adapter.stub :cast_to_timestamp do |clause| + allow(adapter).to receive :cast_to_timestamp do |clause| "UNIX_TIMESTAMP(#{clause})" end end describe '#to_group' do it "returns the column name as a string" do - presenter.to_group.should == 'articles.created_at' + expect(presenter.to_group).to eq('articles.created_at') end it "gets the column's table alias from the associations object" do - column.stub!(:__stack => [:users, :posts]) + allow(column).to receive_messages(:__stack => [:users, :posts]) - associations.should_receive(:alias_for).with([:users, :posts]). + expect(associations).to receive(:alias_for).with([:users, :posts]). and_return('posts') presenter.to_group end it "returns nil if the column is a string" do - column.stub!(:string? => true) + allow(column).to receive_messages(:string? => true) - presenter.to_group.should be_nil + expect(presenter.to_group).to be_nil end it "returns nil if the property is an aggregate" do - path.stub! :aggregate? => true + allow(path).to receive_messages :aggregate? => true - presenter.to_group.should be_nil + expect(presenter.to_group).to be_nil end it "returns nil if the attribute is sourced via a separate query" do - attribute.stub :source_type => 'query' + allow(attribute).to receive_messages :source_type => 'query' - presenter.to_group.should be_nil + expect(presenter.to_group).to be_nil end end describe '#to_select' do it "returns the column name as a string" do - presenter.to_select.should == 'articles.created_at AS created_at' + expect(presenter.to_select).to eq('articles.created_at AS created_at') end it "gets the column's table alias from the associations object" do - column.stub!(:__stack => [:users, :posts]) + allow(column).to receive_messages(:__stack => [:users, :posts]) - associations.should_receive(:alias_for).with([:users, :posts]). + expect(associations).to receive(:alias_for).with([:users, :posts]). and_return('posts') presenter.to_select end it "returns the column name with an alias when provided" do - attribute.stub!(:name => :creation_timestamp) + allow(attribute).to receive_messages(:name => :creation_timestamp) - presenter.to_select. - should == 'articles.created_at AS creation_timestamp' + expect(presenter.to_select). + to eq('articles.created_at AS creation_timestamp') end it "ensures datetime attributes are converted to timestamps" do - attribute.stub :type => :timestamp + allow(attribute).to receive_messages :type => :timestamp - presenter.to_select. - should == 'UNIX_TIMESTAMP(articles.created_at) AS created_at' + expect(presenter.to_select). + to eq('UNIX_TIMESTAMP(articles.created_at) AS created_at') end it "does not include columns that don't exist" do - adapter.stub :concatenate do |clause, separator| + allow(adapter).to receive :concatenate do |clause, separator| "CONCAT_WS('#{separator}', #{clause})" end - adapter.stub :cast_to_string do |clause| + allow(adapter).to receive :cast_to_string do |clause| "CAST(#{clause} AS varchar)" end - attribute.stub!(:columns => [column, double('column', + allow(attribute).to receive_messages(:columns => [column, double('column', :string? => false, :__stack => [], :__name => 'updated_at')]) - presenter.to_select.should == "CONCAT_WS(',', CAST(articles.created_at AS varchar)) AS created_at" + expect(presenter.to_select).to eq("CONCAT_WS(',', CAST(articles.created_at AS varchar)) AS created_at") end it "casts and concatenates multiple columns for attributes" do - adapter.stub :concatenate do |clause, separator| + allow(adapter).to receive :concatenate do |clause, separator| "CONCAT_WS('#{separator}', #{clause})" end - adapter.stub :cast_to_string do |clause| + allow(adapter).to receive :cast_to_string do |clause| "CAST(#{clause} AS varchar)" end - attribute.stub!(:columns => [column, column]) + allow(attribute).to receive_messages(:columns => [column, column]) - presenter.to_select.should == "CONCAT_WS(',', CAST(articles.created_at AS varchar), CAST(articles.created_at AS varchar)) AS created_at" + expect(presenter.to_select).to eq("CONCAT_WS(',', CAST(articles.created_at AS varchar), CAST(articles.created_at AS varchar)) AS created_at") end it "double-casts and concatenates multiple columns for timestamp attributes" do - adapter.stub :concatenate do |clause, separator| + allow(adapter).to receive :concatenate do |clause, separator| "CONCAT_WS('#{separator}', #{clause})" end - adapter.stub :cast_to_string do |clause| + allow(adapter).to receive :cast_to_string do |clause| "CAST(#{clause} AS varchar)" end - attribute.stub :columns => [column, column], :type => :timestamp + allow(attribute).to receive_messages :columns => [column, column], :type => :timestamp - presenter.to_select.should == "CONCAT_WS(',', CAST(UNIX_TIMESTAMP(articles.created_at) AS varchar), CAST(UNIX_TIMESTAMP(articles.created_at) AS varchar)) AS created_at" + expect(presenter.to_select).to eq("CONCAT_WS(',', CAST(UNIX_TIMESTAMP(articles.created_at) AS varchar), CAST(UNIX_TIMESTAMP(articles.created_at) AS varchar)) AS created_at") end it "does not split attribute clause for timestamp casting if it looks like a function call" do - column.stub :__name => "COALESCE(articles.updated_at, articles.created_at)", :string? => true + allow(column).to receive_messages :__name => "COALESCE(articles.updated_at, articles.created_at)", :string? => true - attribute.stub :name => 'mod_date', :columns => [column], + allow(attribute).to receive_messages :name => 'mod_date', :columns => [column], :type => :timestamp - presenter.to_select.should == "UNIX_TIMESTAMP(COALESCE(articles.updated_at, articles.created_at)) AS mod_date" + expect(presenter.to_select).to eq("UNIX_TIMESTAMP(COALESCE(articles.updated_at, articles.created_at)) AS mod_date") end it "returns nil for query sourced attributes" do - attribute.stub :source_type => :query + allow(attribute).to receive_messages :source_type => :query - presenter.to_select.should be_nil + expect(presenter.to_select).to be_nil end it "returns nil for ranged query sourced attributes" do - attribute.stub :source_type => :ranged_query + allow(attribute).to receive_messages :source_type => :ranged_query - presenter.to_select.should be_nil + expect(presenter.to_select).to be_nil end end end diff --git a/spec/thinking_sphinx/active_record/sql_builder_spec.rb b/spec/thinking_sphinx/active_record/sql_builder_spec.rb index c7f853b0f..1c80f046a 100644 --- a/spec/thinking_sphinx/active_record/sql_builder_spec.rb +++ b/spec/thinking_sphinx/active_record/sql_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::SQLBuilder do @@ -5,7 +7,7 @@ :fields => [], :attributes => [], :disable_range? => false, :delta_processor => nil, :conditions => [], :groupings => [], :adapter => adapter, :associations => [], :primary_key => :id, - :options => {}) } + :options => {}, :properties => []) } let(:model) { double('model', :connection => connection, :descends_from_active_record? => true, :column_names => [], :inheritance_column => 'type', :unscoped => relation, @@ -22,24 +24,24 @@ let(:builder) { ThinkingSphinx::ActiveRecord::SQLBuilder.new source } before :each do - ThinkingSphinx::Configuration.stub! :instance => config - ThinkingSphinx::ActiveRecord::PropertySQLPresenter.stub! :new => presenter - Joiner::Joins.stub! :new => associations - relation.stub! :select => relation, :where => relation, :group => relation, + allow(ThinkingSphinx::Configuration).to receive_messages :instance => config + allow(ThinkingSphinx::ActiveRecord::PropertySQLPresenter).to receive_messages :new => presenter + allow(Joiner::Joins).to receive_messages :new => associations + allow(relation).to receive_messages :select => relation, :where => relation, :group => relation, :order => relation, :joins => relation, :to_sql => '' - connection.stub!(:quote_column_name) { |column| "`#{column}`"} + allow(connection).to receive(:quote_column_name) { |column| "`#{column}`"} end describe 'sql_query' do before :each do - source.stub! :type => 'mysql' + allow(source).to receive_messages :type => 'mysql' end it "adds source associations to the joins of the query" do source.associations << double('association', :stack => [:user, :posts], :string? => false) - associations.should_receive(:add_join_to).with([:user, :posts]) + expect(associations).to receive(:add_join_to).with([:user, :posts]) builder.sql_query end @@ -48,25 +50,25 @@ source.associations << double('association', :to_s => 'my string', :string? => true) - relation.should_receive(:joins).with(['my string']).and_return(relation) + expect(relation).to receive(:joins).with(['my string']).and_return(relation) builder.sql_query end context 'MySQL adapter' do before :each do - source.stub! :type => 'mysql' + allow(source).to receive_messages :type => 'mysql' end it "returns the relation's query" do - relation.stub! :to_sql => 'SELECT * FROM people' + allow(relation).to receive_messages :to_sql => 'SELECT * FROM people' - builder.sql_query.should == 'SELECT * FROM people' + expect(builder.sql_query).to eq('SELECT * FROM people') end it "ensures results aren't from cache" do - relation.should_receive(:select) do |string| - string.should match(/^SQL_NO_CACHE /) + expect(relation).to receive(:select) do |string| + expect(string).to match(/^SQL_NO_CACHE /) relation end @@ -74,8 +76,8 @@ end it "adds the document id using the offset and index count" do - relation.should_receive(:select) do |string| - string.should match(/`users`.`id` \* 5 \+ 3 AS `id`/) + expect(relation).to receive(:select) do |string| + expect(string).to match(/`users`.`id` \* 5 \+ 3 AS `id`/) relation end @@ -85,8 +87,8 @@ it "adds each field to the SELECT clause" do source.fields << double('field') - relation.should_receive(:select) do |string| - string.should match(/`name` AS `name`/) + expect(relation).to receive(:select) do |string| + expect(string).to match(/`name` AS `name`/) relation end @@ -95,10 +97,10 @@ it "adds each attribute to the SELECT clause" do source.attributes << double('attribute') - presenter.stub!(:to_select => '`created_at` AS `created_at`') + allow(presenter).to receive_messages(:to_select => '`created_at` AS `created_at`') - relation.should_receive(:select) do |string| - string.should match(/`created_at` AS `created_at`/) + expect(relation).to receive(:select) do |string| + expect(string).to match(/`created_at` AS `created_at`/) relation end @@ -106,8 +108,8 @@ end it "limits results to a set range" do - relation.should_receive(:where) do |string| - string.should match(/`users`.`id` BETWEEN \$start AND \$end/) + expect(relation).to receive(:where) do |string| + expect(string).to match(/`users`.`id` BETWEEN \$start AND \$end/) relation end @@ -115,10 +117,10 @@ end it "shouldn't limit results to a range if ranges are disabled" do - source.stub! :disable_range? => true + allow(source).to receive_messages :disable_range? => true - relation.should_receive(:where) do |string| - string.should_not match(/`users`.`id` BETWEEN \$start AND \$end/) + expect(relation).to receive(:where) do |string| + expect(string).not_to match(/`users`.`id` BETWEEN \$start AND \$end/) relation end @@ -128,8 +130,8 @@ it "adds source conditions" do source.conditions << 'created_at > NOW()' - relation.should_receive(:where) do |string| - string.should match(/created_at > NOW()/) + expect(relation).to receive(:where) do |string| + expect(string).to match(/created_at > NOW()/) relation end @@ -137,8 +139,8 @@ end it "groups by the primary key" do - relation.should_receive(:group) do |string| - string.should match(/`users`.`id`/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/`users`.`id`/) relation end @@ -148,8 +150,8 @@ it "groups each field" do source.fields << double('field') - relation.should_receive(:group) do |string| - string.should match(/`name`/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/`name`/) relation end @@ -158,10 +160,10 @@ it "groups each attribute" do source.attributes << double('attribute') - presenter.stub!(:to_group => '`created_at`') + allow(presenter).to receive_messages(:to_group => '`created_at`') - relation.should_receive(:group) do |string| - string.should match(/`created_at`/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/`created_at`/) relation end @@ -171,8 +173,8 @@ it "groups by source groupings" do source.groupings << '`latitude`' - relation.should_receive(:group) do |string| - string.should match(/`latitude`/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/`latitude`/) relation end @@ -180,7 +182,7 @@ end it "orders by NULL" do - relation.should_receive(:order).with('NULL').and_return(relation) + expect(relation).to receive(:order).with('NULL').and_return(relation) builder.sql_query end @@ -188,13 +190,13 @@ context 'STI model' do before :each do model.column_names << 'type' - model.stub! :descends_from_active_record? => false - model.stub! :store_full_sti_class => true + allow(model).to receive_messages :descends_from_active_record? => false + allow(model).to receive_messages :store_full_sti_class => true end it "groups by the inheritance column" do - relation.should_receive(:group) do |string| - string.should match(/`users`.`type`/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/`users`.`type`/) relation end @@ -204,12 +206,12 @@ context 'with a custom inheritance column' do before :each do model.column_names << 'custom_type' - model.stub :inheritance_column => 'custom_type' + allow(model).to receive_messages :inheritance_column => 'custom_type' end it "groups by the right column" do - relation.should_receive(:group) do |string| - string.should match(/`users`.`custom_type`/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/`users`.`custom_type`/) relation end @@ -222,14 +224,14 @@ let(:processor) { double('processor') } before :each do - source.stub! :delta_processor => processor - source.stub! :delta? => true + allow(source).to receive_messages :delta_processor => processor + allow(source).to receive_messages :delta? => true end it "filters by the provided clause" do - processor.should_receive(:clause).with(true).and_return('`delta` = 1') - relation.should_receive(:where) do |string| - string.should match(/`delta` = 1/) + expect(processor).to receive(:clause).with(true).and_return('`delta` = 1') + expect(relation).to receive(:where) do |string| + expect(string).to match(/`delta` = 1/) relation end @@ -243,20 +245,20 @@ :to_group => '"name"') } before :each do - source.stub! :type => 'pgsql' - model.stub! :quoted_table_name => '"users"' - connection.stub!(:quote_column_name) { |column| "\"#{column}\""} + allow(source).to receive_messages :type => 'pgsql' + allow(model).to receive_messages :quoted_table_name => '"users"' + allow(connection).to receive(:quote_column_name) { |column| "\"#{column}\""} end it "returns the relation's query" do - relation.stub! :to_sql => 'SELECT * FROM people' + allow(relation).to receive_messages :to_sql => 'SELECT * FROM people' - builder.sql_query.should == 'SELECT * FROM people' + expect(builder.sql_query).to eq('SELECT * FROM people') end it "adds the document id using the offset and index count" do - relation.should_receive(:select) do |string| - string.should match(/"users"."id" \* 5 \+ 3 AS "id"/) + expect(relation).to receive(:select) do |string| + expect(string).to match(/"users"."id" \* 5 \+ 3 AS "id"/) relation end @@ -266,8 +268,8 @@ it "adds each field to the SELECT clause" do source.fields << double('field') - relation.should_receive(:select) do |string| - string.should match(/"name" AS "name"/) + expect(relation).to receive(:select) do |string| + expect(string).to match(/"name" AS "name"/) relation end @@ -276,10 +278,10 @@ it "adds each attribute to the SELECT clause" do source.attributes << double('attribute') - presenter.stub!(:to_select => '"created_at" AS "created_at"') + allow(presenter).to receive_messages(:to_select => '"created_at" AS "created_at"') - relation.should_receive(:select) do |string| - string.should match(/"created_at" AS "created_at"/) + expect(relation).to receive(:select) do |string| + expect(string).to match(/"created_at" AS "created_at"/) relation end @@ -287,8 +289,8 @@ end it "limits results to a set range" do - relation.should_receive(:where) do |string| - string.should match(/"users"."id" BETWEEN \$start AND \$end/) + expect(relation).to receive(:where) do |string| + expect(string).to match(/"users"."id" BETWEEN \$start AND \$end/) relation end @@ -296,10 +298,10 @@ end it "shouldn't limit results to a range if ranges are disabled" do - source.stub! :disable_range? => true + allow(source).to receive_messages :disable_range? => true - relation.should_receive(:where) do |string| - string.should_not match(/"users"."id" BETWEEN \$start AND \$end/) + expect(relation).to receive(:where) do |string| + expect(string).not_to match(/"users"."id" BETWEEN \$start AND \$end/) relation end @@ -309,8 +311,8 @@ it "adds source conditions" do source.conditions << 'created_at > NOW()' - relation.should_receive(:where) do |string| - string.should match(/created_at > NOW()/) + expect(relation).to receive(:where) do |string| + expect(string).to match(/created_at > NOW()/) relation end @@ -318,8 +320,8 @@ end it "groups by the primary key" do - relation.should_receive(:group) do |string| - string.should match(/"users"."id"/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/"users"."id"/) relation end @@ -329,8 +331,8 @@ it "groups each field" do source.fields << double('field') - relation.should_receive(:group) do |string| - string.should match(/"name"/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/"name"/) relation end @@ -339,10 +341,10 @@ it "groups each attribute" do source.attributes << double('attribute') - presenter.stub!(:to_group => '"created_at"') + allow(presenter).to receive_messages(:to_group => '"created_at"') - relation.should_receive(:group) do |string| - string.should match(/"created_at"/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/"created_at"/) relation end @@ -352,8 +354,8 @@ it "groups by source groupings" do source.groupings << '"latitude"' - relation.should_receive(:group) do |string| - string.should match(/"latitude"/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/"latitude"/) relation end @@ -361,7 +363,7 @@ end it "has no ORDER clause" do - relation.should_not_receive(:order) + expect(relation).not_to receive(:order) builder.sql_query end @@ -372,8 +374,57 @@ end it "groups by the primary key" do - relation.should_receive(:group) do |string| - string.should match(/"users"."id"/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/"users"."id"/) + relation + end + + builder.sql_query + end + + it "does not group by fields" do + source.fields << double('field') + + expect(relation).to receive(:group) do |string| + expect(string).not_to match(/"name"/) + relation + end + + builder.sql_query + end + + it "does not group by attributes" do + source.attributes << double('attribute') + allow(presenter).to receive_messages(:to_group => '"created_at"') + + expect(relation).to receive(:group) do |string| + expect(string).not_to match(/"created_at"/) + relation + end + + builder.sql_query + end + + it "groups by source groupings" do + source.groupings << '"latitude"' + + expect(relation).to receive(:group) do |string| + expect(string).to match(/"latitude"/) + relation + end + + builder.sql_query + end + end + + context 'group by shortcut in global configuration' do + before :each do + config.settings['minimal_group_by'] = true + end + + it "groups by the primary key" do + expect(relation).to receive(:group) do |string| + expect(string).to match(/"users"."id"/) relation end @@ -383,8 +434,8 @@ it "does not group by fields" do source.fields << double('field') - relation.should_receive(:group) do |string| - string.should_not match(/"name"/) + expect(relation).to receive(:group) do |string| + expect(string).not_to match(/"name"/) relation end @@ -393,10 +444,10 @@ it "does not group by attributes" do source.attributes << double('attribute') - presenter.stub!(:to_group => '"created_at"') + allow(presenter).to receive_messages(:to_group => '"created_at"') - relation.should_receive(:group) do |string| - string.should_not match(/"created_at"/) + expect(relation).to receive(:group) do |string| + expect(string).not_to match(/"created_at"/) relation end @@ -406,8 +457,8 @@ it "groups by source groupings" do source.groupings << '"latitude"' - relation.should_receive(:group) do |string| - string.should match(/"latitude"/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/"latitude"/) relation end @@ -418,13 +469,13 @@ context 'STI model' do before :each do model.column_names << 'type' - model.stub! :descends_from_active_record? => false - model.stub! :store_full_sti_class => true + allow(model).to receive_messages :descends_from_active_record? => false + allow(model).to receive_messages :store_full_sti_class => true end it "groups by the inheritance column" do - relation.should_receive(:group) do |string| - string.should match(/"users"."type"/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/"users"."type"/) relation end @@ -434,12 +485,12 @@ context 'with a custom inheritance column' do before :each do model.column_names << 'custom_type' - model.stub :inheritance_column => 'custom_type' + allow(model).to receive_messages :inheritance_column => 'custom_type' end it "groups by the right column" do - relation.should_receive(:group) do |string| - string.should match(/"users"."custom_type"/) + expect(relation).to receive(:group) do |string| + expect(string).to match(/"users"."custom_type"/) relation end @@ -452,14 +503,14 @@ let(:processor) { double('processor') } before :each do - source.stub! :delta_processor => processor - source.stub! :delta? => true + allow(source).to receive_messages :delta_processor => processor + allow(source).to receive_messages :delta? => true end it "filters by the provided clause" do - processor.should_receive(:clause).with(true).and_return('"delta" = 1') - relation.should_receive(:where) do |string| - string.should match(/"delta" = 1/) + expect(processor).to receive(:clause).with(true).and_return('"delta" = 1') + expect(relation).to receive(:where) do |string| + expect(string).to match(/"delta" = 1/) relation end @@ -469,82 +520,56 @@ end end - describe 'sql_query_info' do - it "filters on the reversed document id" do - relation.should_receive(:where). - with("`users`.`id` = ($id - #{source.offset}) / #{indices.count}"). - and_return(relation) - - builder.sql_query_info - end - - it "returns the generated SQL query" do - relation.stub(:to_sql).and_return('SELECT * FROM people WHERE id = $id') - - builder.sql_query_info.should == 'SELECT * FROM people WHERE id = $id' - end - end - - describe 'sql_query_post_index' do + describe 'sql_query_pre' do let(:processor) { double('processor', :reset_query => 'RESET DELTAS') } - it "adds a reset delta query if there is a delta processor and this is the core source" do - source.stub :delta_processor => processor, :delta? => false - - builder.sql_query_post_index.should include('RESET DELTAS') - end - - it "adds no reset delta query if there is a delta processor and this is the delta source" do - source.stub :delta_processor => processor, :delta? => true - - builder.sql_query_post_index.should_not include('RESET DELTAS') + before :each do + allow(source).to receive_messages :options => {}, :delta_processor => nil, :delta? => false + allow(adapter).to receive_messages :utf8_query_pre => ['SET UTF8'] end - end - describe 'sql_query_pre' do - let(:processor) { double('processor', :reset_query => 'RESET DELTAS') } + it "adds a reset delta query if there is a delta processor and this is the core source" do + allow(source).to receive_messages :delta_processor => processor - before :each do - source.stub :options => {}, :delta_processor => nil, :delta? => false - adapter.stub :utf8_query_pre => ['SET UTF8'] + expect(builder.sql_query_pre).to include('RESET DELTAS') end it "does not add a reset query if there is no delta processor" do - builder.sql_query_pre.should_not include('RESET DELTAS') + expect(builder.sql_query_pre).not_to include('RESET DELTAS') end it "does not add a reset query if this is a delta source" do - source.stub :delta_processor => processor - source.stub :delta? => true + allow(source).to receive_messages :delta_processor => processor + allow(source).to receive_messages :delta? => true - builder.sql_query_pre.should_not include('RESET DELTAS') + expect(builder.sql_query_pre).not_to include('RESET DELTAS') end it "sets the group_concat_max_len value if set" do source.options[:group_concat_max_len] = 123 - builder.sql_query_pre. - should include('SET SESSION group_concat_max_len = 123') + expect(builder.sql_query_pre). + to include('SET SESSION group_concat_max_len = 123') end it "does not set the group_concat_max_len if not provided" do source.options[:group_concat_max_len] = nil - builder.sql_query_pre.select { |sql| + expect(builder.sql_query_pre.select { |sql| sql[/SET SESSION group_concat_max_len/] - }.should be_empty + }).to be_empty end it "sets the connection to use UTF-8 if required" do source.options[:utf8?] = true - builder.sql_query_pre.should include('SET UTF8') + expect(builder.sql_query_pre).to include('SET UTF8') end it "does not set the connection to use UTF-8 if not required" do source.options[:utf8?] = false - builder.sql_query_pre.should_not include('SET UTF8') + expect(builder.sql_query_pre).not_to include('SET UTF8') end it "adds a time-zone query by default" do @@ -560,26 +585,26 @@ describe 'sql_query_range' do before :each do - adapter.stub!(:convert_nulls) { |string, default| + allow(adapter).to receive(:convert_nulls) { |string, default| "ISNULL(#{string}, #{default})" } end it "returns the relation's query" do - relation.stub! :to_sql => 'SELECT * FROM people' + allow(relation).to receive_messages :to_sql => 'SELECT * FROM people' - builder.sql_query_range.should == 'SELECT * FROM people' + expect(builder.sql_query_range).to eq('SELECT * FROM people') end it "returns nil if ranges are disabled" do - source.stub! :disable_range? => true + allow(source).to receive_messages :disable_range? => true - builder.sql_query_range.should be_nil + expect(builder.sql_query_range).to be_nil end it "selects the minimum primary key value, allowing for nulls" do - relation.should_receive(:select) do |string| - string.should match(/ISNULL\(MIN\(`users`.`id`\), 1\)/) + expect(relation).to receive(:select) do |string| + expect(string).to match(/ISNULL\(MIN\(`users`.`id`\), 1\)/) relation end @@ -587,8 +612,8 @@ end it "selects the maximum primary key value, allowing for nulls" do - relation.should_receive(:select) do |string| - string.should match(/ISNULL\(MAX\(`users`.`id`\), 1\)/) + expect(relation).to receive(:select) do |string| + expect(string).to match(/ISNULL\(MAX\(`users`.`id`\), 1\)/) relation end @@ -596,8 +621,8 @@ end it "shouldn't limit results to a range" do - relation.should_receive(:where) do |string| - string.should_not match(/`users`.`id` BETWEEN \$start AND \$end/) + expect(relation).to receive(:where) do |string| + expect(string).not_to match(/`users`.`id` BETWEEN \$start AND \$end/) relation end @@ -607,8 +632,8 @@ it "does not add source conditions" do source.conditions << 'created_at > NOW()' - relation.should_receive(:where) do |string| - string.should_not match(/created_at > NOW()/) + expect(relation).to receive(:where) do |string| + expect(string).not_to match(/created_at > NOW()/) relation end @@ -619,14 +644,14 @@ let(:processor) { double('processor') } before :each do - source.stub! :delta_processor => processor - source.stub! :delta? => true + allow(source).to receive_messages :delta_processor => processor + allow(source).to receive_messages :delta? => true end it "filters by the provided clause" do - processor.should_receive(:clause).with(true).and_return('`delta` = 1') - relation.should_receive(:where) do |string| - string.should match(/`delta` = 1/) + expect(processor).to receive(:clause).with(true).and_return('`delta` = 1') + expect(relation).to receive(:where) do |string| + expect(string).to match(/`delta` = 1/) relation end diff --git a/spec/thinking_sphinx/active_record/sql_source_spec.rb b/spec/thinking_sphinx/active_record/sql_source_spec.rb index b131033a6..800e4cd29 100644 --- a/spec/thinking_sphinx/active_record/sql_source_spec.rb +++ b/spec/thinking_sphinx/active_record/sql_source_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::ActiveRecord::SQLSource do @@ -9,42 +11,80 @@ let(:db_config) { {:host => 'localhost', :user => 'root', :database => 'default'} } let(:source) { ThinkingSphinx::ActiveRecord::SQLSource.new(model, - :position => 3) } + :position => 3, :primary_key => model.primary_key || :id ) } let(:adapter) { double('adapter') } before :each do - ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter. - stub!(:=== => true) - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - stub!(:adapter_for => adapter) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter). + to receive_messages(:=== => true) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters). + to receive_messages(:adapter_for => adapter) end describe '#adapter' do it "returns a database adapter for the model" do - ThinkingSphinx::ActiveRecord::DatabaseAdapters. - should_receive(:adapter_for).with(model).and_return(adapter) + expect(ThinkingSphinx::ActiveRecord::DatabaseAdapters). + to receive(:adapter_for).with(model).and_return(adapter) + + expect(source.adapter).to eq(adapter) + end + end + + describe '#add_attribute' do + let(:attribute) { double('attribute', name: 'my_attribute') } + + it "appends attributes to the collection" do + source.add_attribute attribute + + expect(source.attributes.collect(&:name)).to include('my_attribute') + end + + it "replaces attributes with the same name" do + source.add_attribute double('attribute', name: 'my_attribute') + source.add_attribute attribute - source.adapter.should == adapter + matching = source.attributes.select { |attr| attr.name == attribute.name } + + expect(matching).to eq([attribute]) + end + end + + describe '#add_field' do + let(:field) { double('field', name: 'my_field') } + + it "appends fields to the collection" do + source.add_field field + + expect(source.fields.collect(&:name)).to include('my_field') + end + + it "replaces fields with the same name" do + source.add_field double('field', name: 'my_field') + source.add_field field + + matching = source.fields.select { |fld| fld.name == field.name } + + expect(matching).to eq([field]) end end describe '#attributes' do it "has the internal id attribute by default" do - source.attributes.collect(&:name).should include('sphinx_internal_id') + expect(source.attributes.collect(&:name)).to include('sphinx_internal_id') end it "has the class name attribute by default" do - source.attributes.collect(&:name).should include('sphinx_internal_class') + expect(source.attributes.collect(&:name)).to include('sphinx_internal_class') end it "has the internal deleted attribute by default" do - source.attributes.collect(&:name).should include('sphinx_deleted') + expect(source.attributes.collect(&:name)).to include('sphinx_deleted') end it "marks the internal class attribute as a facet" do - source.attributes.detect { |attribute| + expect(source.attributes.detect { |attribute| attribute.name == 'sphinx_internal_class' - }.options[:facet].should be_true + }.options[:facet]).to be_truthy end end @@ -53,27 +93,29 @@ let(:processor) { double('processor') } let(:source) { ThinkingSphinx::ActiveRecord::SQLSource.new model, - :delta_processor => processor_class + :delta_processor => processor_class, + :primary_key => model.primary_key || :id } let(:source_with_options) { ThinkingSphinx::ActiveRecord::SQLSource.new model, :delta_processor => processor_class, - :delta_options => { :opt_key => :opt_value } + :delta_options => { :opt_key => :opt_value }, + :primary_key => model.primary_key || :id } it "loads the processor with the adapter" do - processor_class.should_receive(:try).with(:new, adapter, {}). + expect(processor_class).to receive(:try).with(:new, adapter, {}). and_return processor source.delta_processor end it "returns the given processor" do - source.delta_processor.should == processor + expect(source.delta_processor).to eq(processor) end it "passes given options to the processor" do - processor_class.should_receive(:try).with(:new, adapter, {:opt_key => :opt_value}) + expect(processor_class).to receive(:try).with(:new, adapter, {:opt_key => :opt_value}) source_with_options.delta_processor end end @@ -81,77 +123,99 @@ describe '#delta?' do it "returns the given delta setting" do source = ThinkingSphinx::ActiveRecord::SQLSource.new model, - :delta? => true + :delta? => true, + :primary_key => model.primary_key || :id - source.should be_a_delta + expect(source).to be_a_delta end end describe '#disable_range?' do it "returns the given range setting" do source = ThinkingSphinx::ActiveRecord::SQLSource.new model, - :disable_range? => true + :disable_range? => true, + :primary_key => model.primary_key || :id - source.disable_range?.should be_true + expect(source.disable_range?).to be_truthy end end describe '#fields' do it "has the internal class field by default" do - source.fields.collect(&:name). - should include('sphinx_internal_class_name') + expect(source.fields.collect(&:name)). + to include('sphinx_internal_class_name') end it "sets the sphinx class field to use a string of the class name" do - source.fields.detect { |field| + expect(source.fields.detect { |field| field.name == 'sphinx_internal_class_name' - }.columns.first.__name.should == "'User'" + }.columns.first.__name).to eq("'User'") end it "uses the inheritance column if it exists for the sphinx class field" do - adapter.stub :quoted_table_name => '"users"', :quote => '"type"' - adapter.stub(:convert_blank) { |clause, default| + allow(adapter).to receive_messages :quoted_table_name => '"users"', :quote => '"type"' + allow(adapter).to receive(:convert_blank) { |clause, default| "coalesce(nullif(#{clause}, ''), #{default})" } - model.stub :column_names => ['type'], :sti_name => 'User' + allow(model).to receive_messages :column_names => ['type'], :sti_name => 'User' - source.fields.detect { |field| + expect(source.fields.detect { |field| field.name == 'sphinx_internal_class_name' - }.columns.first.__name. - should == "coalesce(nullif(\"users\".\"type\", ''), 'User')" + }.columns.first.__name). + to eq("coalesce(nullif(\"users\".\"type\", ''), 'User')") end end describe '#name' do it "defaults to the model name downcased with the given position" do - source.name.should == 'user_3' + expect(source.name).to eq('user_3') end it "allows for custom names, but adds the position suffix" do source = ThinkingSphinx::ActiveRecord::SQLSource.new model, - :name => 'people', :position => 2 + :name => 'people', :position => 2, :primary_key => model.primary_key || :id - source.name.should == 'people_2' + expect(source.name).to eq('people_2') end end describe '#offset' do it "returns the given offset" do - source = ThinkingSphinx::ActiveRecord::SQLSource.new model, :offset => 12 + source = ThinkingSphinx::ActiveRecord::SQLSource.new model, + :offset => 12, :primary_key => model.primary_key || :id - source.offset.should == 12 + expect(source.offset).to eq(12) end end describe '#options' do it "defaults to having utf8? set to false" do - source.options[:utf8?].should be_false + expect(source.options[:utf8?]).to be_falsey end it "sets utf8? to true if the database encoding is utf8" do db_config[:encoding] = 'utf8' - source.options[:utf8?].should be_true + expect(source.options[:utf8?]).to be_truthy + end + + it "sets utf8? to true if the database encoding starts with utf8" do + db_config[:encoding] = 'utf8mb4' + + expect(source.options[:utf8?]).to be_truthy + end + + describe "#primary key" do + let(:model) { double('model', :connection => connection, + :name => 'User', :column_names => [], :inheritance_column => 'type') } + let(:source) { ThinkingSphinx::ActiveRecord::SQLSource.new(model, + :position => 3, :primary_key => :custom_key) } + let(:template) { ThinkingSphinx::ActiveRecord::SQLSource::Template.new(source) } + + it 'template should allow primary key from options' do + template.apply + template.source.attributes.collect(&:columns) == :custom_key + end end end @@ -164,50 +228,34 @@ let(:template) { double('template', :apply => true) } before :each do - ThinkingSphinx::ActiveRecord::SQLBuilder.stub! :new => builder - ThinkingSphinx::ActiveRecord::Attribute::SphinxPresenter.stub :new => presenter - ThinkingSphinx::ActiveRecord::SQLSource::Template.stub :new => template - ThinkingSphinx::Configuration.stub :instance => config + allow(ThinkingSphinx::ActiveRecord::SQLBuilder).to receive_messages :new => builder + allow(ThinkingSphinx::ActiveRecord::Attribute::SphinxPresenter).to receive_messages :new => presenter + allow(ThinkingSphinx::ActiveRecord::SQLSource::Template).to receive_messages :new => template + allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end it "uses the builder's sql_query value" do - builder.stub! :sql_query => 'select * from table' + allow(builder).to receive_messages :sql_query => 'select * from table' source.render - source.sql_query.should == 'select * from table' + expect(source.sql_query).to eq('select * from table') end it "uses the builder's sql_query_range value" do - builder.stub! :sql_query_range => 'select 0, 10 from table' + allow(builder).to receive_messages :sql_query_range => 'select 0, 10 from table' source.render - source.sql_query_range.should == 'select 0, 10 from table' - end - - it "uses the builder's sql_query_info value" do - builder.stub! :sql_query_info => 'select * from table where id = ?' - - source.render - - source.sql_query_info.should == 'select * from table where id = ?' + expect(source.sql_query_range).to eq('select 0, 10 from table') end it "appends the builder's sql_query_pre value" do - builder.stub! :sql_query_pre => ['Change Setting'] + allow(builder).to receive_messages :sql_query_pre => ['Change Setting'] source.render - source.sql_query_pre.should == ['Change Setting'] - end - - it "appends the builder's sql_query_post_index value" do - builder.stub! :sql_query_post_index => ['RESET DELTAS'] - - source.render - - source.sql_query_post_index.should include('RESET DELTAS') + expect(source.sql_query_pre).to eq(['Change Setting']) end it "adds fields with attributes to sql_field_string" do @@ -216,7 +264,7 @@ source.render - source.sql_field_string.should include('title') + expect(source.sql_field_string).to include('title') end it "adds any joined or file fields" do @@ -225,7 +273,7 @@ source.render - source.sql_file_field.should include('title') + expect(source.sql_file_field).to include('title') end it "adds wordcounted fields to sql_field_str2wordcount" do @@ -234,11 +282,11 @@ source.render - source.sql_field_str2wordcount.should include('title') + expect(source.sql_field_str2wordcount).to include('title') end it "adds any joined fields" do - ThinkingSphinx::ActiveRecord::PropertyQuery.stub( + allow(ThinkingSphinx::ActiveRecord::PropertyQuery).to receive_messages( :new => double(:to_s => 'query for title') ) source.fields << double('field', :name => 'title', @@ -247,90 +295,99 @@ source.render - source.sql_joined_field.should include('query for title') + expect(source.sql_joined_field).to include('query for title') end it "adds integer attributes to sql_attr_uint" do source.attributes << double('attribute') - presenter.stub :declaration => 'count', :collection_type => :uint + allow(presenter).to receive_messages :declaration => 'count', :collection_type => :uint source.render - source.sql_attr_uint.should include('count') + expect(source.sql_attr_uint).to include('count') end it "adds boolean attributes to sql_attr_bool" do source.attributes << double('attribute') - presenter.stub :declaration => 'published', :collection_type => :bool + allow(presenter).to receive_messages :declaration => 'published', :collection_type => :bool source.render - source.sql_attr_bool.should include('published') + expect(source.sql_attr_bool).to include('published') end it "adds string attributes to sql_attr_string" do source.attributes << double('attribute') - presenter.stub :declaration => 'name', :collection_type => :string + allow(presenter).to receive_messages :declaration => 'name', :collection_type => :string source.render - source.sql_attr_string.should include('name') + expect(source.sql_attr_string).to include('name') end - it "adds timestamp attributes to sql_attr_timestamp" do + it "adds timestamp attributes to sql_attr_uint" do source.attributes << double('attribute') - presenter.stub :declaration => 'created_at', - :collection_type => :timestamp + allow(presenter).to receive_messages :declaration => 'created_at', + :collection_type => :uint source.render - source.sql_attr_timestamp.should include('created_at') + expect(source.sql_attr_uint).to include('created_at') end it "adds float attributes to sql_attr_float" do source.attributes << double('attribute') - presenter.stub :declaration => 'rating', :collection_type => :float + allow(presenter).to receive_messages :declaration => 'rating', :collection_type => :float source.render - source.sql_attr_float.should include('rating') + expect(source.sql_attr_float).to include('rating') end it "adds bigint attributes to sql_attr_bigint" do source.attributes << double('attribute') - presenter.stub :declaration => 'super_id', :collection_type => :bigint + allow(presenter).to receive_messages :declaration => 'super_id', :collection_type => :bigint source.render - source.sql_attr_bigint.should include('super_id') + expect(source.sql_attr_bigint).to include('super_id') end it "adds ordinal strings to sql_attr_str2ordinal" do source.attributes << double('attribute') - presenter.stub :declaration => 'name', :collection_type => :str2ordinal + allow(presenter).to receive_messages :declaration => 'name', :collection_type => :str2ordinal source.render - source.sql_attr_str2ordinal.should include('name') + expect(source.sql_attr_str2ordinal).to include('name') end it "adds multi-value attributes to sql_attr_multi" do source.attributes << double('attribute') - presenter.stub :declaration => 'uint tag_ids from field', + allow(presenter).to receive_messages :declaration => 'uint tag_ids from field', :collection_type => :multi source.render - source.sql_attr_multi.should include('uint tag_ids from field') + expect(source.sql_attr_multi).to include('uint tag_ids from field') end it "adds word count attributes to sql_attr_str2wordcount" do source.attributes << double('attribute') - presenter.stub :declaration => 'name', :collection_type => :str2wordcount + allow(presenter).to receive_messages :declaration => 'name', :collection_type => :str2wordcount + + source.render + + expect(source.sql_attr_str2wordcount).to include('name') + end + + it "adds json attributes to sql_attr_json" do + source.attributes << double('attribute') + allow(presenter).to receive_messages :declaration => 'json', :collection_type => :json source.render - source.sql_attr_str2wordcount.should include('name') + expect(source.sql_attr_json).to include('json') end it "adds relevant settings from thinking_sphinx.yml" do @@ -339,7 +396,7 @@ source.render - source.mysql_ssl_cert.should == 'foo.cert' + expect(source.mysql_ssl_cert).to eq('foo.cert') end end @@ -347,84 +404,104 @@ it "sets the sql_host setting from the model's database settings" do source.set_database_settings :host => '12.34.56.78' - source.sql_host.should == '12.34.56.78' + expect(source.sql_host).to eq('12.34.56.78') end it "defaults sql_host to localhost if the model has no host" do source.set_database_settings :host => nil - source.sql_host.should == 'localhost' + expect(source.sql_host).to eq('localhost') end it "sets the sql_user setting from the model's database settings" do source.set_database_settings :username => 'pat' - source.sql_user.should == 'pat' + expect(source.sql_user).to eq('pat') end it "uses the user setting if username is not set in the model" do source.set_database_settings :username => nil, :user => 'pat' - source.sql_user.should == 'pat' + expect(source.sql_user).to eq('pat') end it "sets the sql_pass setting from the model's database settings" do source.set_database_settings :password => 'swordfish' - source.sql_pass.should == 'swordfish' + expect(source.sql_pass).to eq('swordfish') end it "escapes hashes in the password for sql_pass" do source.set_database_settings :password => 'sword#fish' - source.sql_pass.should == 'sword\#fish' + expect(source.sql_pass).to eq('sword\#fish') end it "sets the sql_db setting from the model's database settings" do source.set_database_settings :database => 'rails_app' - source.sql_db.should == 'rails_app' + expect(source.sql_db).to eq('rails_app') end it "sets the sql_port setting from the model's database settings" do source.set_database_settings :port => 5432 - source.sql_port.should == 5432 + expect(source.sql_port).to eq(5432) end it "sets the sql_sock setting from the model's database settings" do source.set_database_settings :socket => '/unix/socket' - source.sql_sock.should == '/unix/socket' + expect(source.sql_sock).to eq('/unix/socket') + end + + it "sets the mysql_ssl_cert from the model's database settings" do + source.set_database_settings :sslcert => '/path/to/cert.pem' + + expect(source.mysql_ssl_cert).to eq '/path/to/cert.pem' + end + + it "sets the mysql_ssl_key from the model's database settings" do + source.set_database_settings :sslkey => '/path/to/key.pem' + + expect(source.mysql_ssl_key).to eq '/path/to/key.pem' + end + + it "sets the mysql_ssl_ca from the model's database settings" do + source.set_database_settings :sslca => '/path/to/ca.pem' + + expect(source.mysql_ssl_ca).to eq '/path/to/ca.pem' end end describe '#type' do it "is mysql when using the MySQL Adapter" do - ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter. - stub!(:=== => true) - ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter. - stub!(:=== => false) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter). + to receive_messages(:=== => true) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter). + to receive_messages(:=== => false) - source.type.should == 'mysql' + expect(source.type).to eq('mysql') end it "is pgsql when using the PostgreSQL Adapter" do - ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter. - stub!(:=== => false) - ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter. - stub!(:=== => true) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter). + to receive_messages(:=== => false) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter). + to receive_messages(:=== => true) - source.type.should == 'pgsql' + expect(source.type).to eq('pgsql') end it "raises an exception for any other adapter" do - ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter. - stub!(:=== => false) - ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter. - stub!(:=== => false) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::MySQLAdapter). + to receive_messages(:=== => false) + allow(ThinkingSphinx::ActiveRecord::DatabaseAdapters::PostgreSQLAdapter). + to receive_messages(:=== => false) - lambda { source.type }.should raise_error + expect { source.type }.to raise_error( + ThinkingSphinx::UnknownDatabaseAdapter + ) end end end diff --git a/spec/thinking_sphinx/attribute_types_spec.rb b/spec/thinking_sphinx/attribute_types_spec.rb new file mode 100644 index 000000000..7c0c0c63b --- /dev/null +++ b/spec/thinking_sphinx/attribute_types_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::AttributeTypes do + let(:configuration) { + double('configuration', :configuration_file => 'sphinx.conf') + } + + before :each do + allow(ThinkingSphinx::Configuration).to receive(:instance). + and_return(configuration) + + allow(File).to receive(:exist?).with('sphinx.conf').and_return(true) + allow(File).to receive(:read).with('sphinx.conf').and_return(<<-CONF) +index plain_index +{ + source = plain_source +} + +source plain_source +{ + type = mysql + sql_attr_uint = customer_id + sql_attr_float = price + sql_attr_multi = uint comment_ids from field +} + +index rt_index +{ + type = rt + rt_attr_uint = user_id + rt_attr_multi = comment_ids +} + CONF + end + + it 'returns an empty hash if no configuration file exists' do + allow(File).to receive(:exist?).with('sphinx.conf').and_return(false) + + expect(ThinkingSphinx::AttributeTypes.new.call).to eq({}) + end + + it 'returns all known attributes' do + expect(ThinkingSphinx::AttributeTypes.new.call).to eq({ + 'customer_id' => [:uint], + 'price' => [:float], + 'comment_ids' => [:uint], + 'user_id' => [:uint] + }) + end +end diff --git a/spec/thinking_sphinx/commands/clear_real_time_spec.rb b/spec/thinking_sphinx/commands/clear_real_time_spec.rb new file mode 100644 index 000000000..df20c5840 --- /dev/null +++ b/spec/thinking_sphinx/commands/clear_real_time_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::ClearRealTime do + let(:command) { ThinkingSphinx::Commands::ClearRealTime.new( + configuration, {:indices => [users_index, parts_index]}, stream + ) } + let(:configuration) { double 'configuration', :searchd => double(:binlog_path => '/path/to/binlog') } + let(:stream) { double :puts => nil } + let(:users_index) { double :path => '/path/to/my/index/users', :render => true } + let(:parts_index) { double :path => '/path/to/my/index/parts', :render => true } + + before :each do + allow(Dir).to receive(:[]).with('/path/to/my/index/users.*'). + and_return(['users.a', 'users.b']) + allow(Dir).to receive(:[]).with('/path/to/my/index/parts.*'). + and_return(['parts.a', 'parts.b']) + + allow(FileUtils).to receive_messages :rm_rf => true, + :rm => true + allow(File).to receive_messages :exist? => true + end + + it 'finds each file for real-time indices' do + expect(Dir).to receive(:[]).with('/path/to/my/index/users.*'). + and_return([]) + + command.call + end + + it "removes the directory for the binlog files" do + expect(FileUtils).to receive(:rm_rf).with('/path/to/binlog') + + command.call + end + + it "removes each file for real-time indices" do + expect(FileUtils).to receive(:rm).with('users.a') + expect(FileUtils).to receive(:rm).with('users.b') + expect(FileUtils).to receive(:rm).with('parts.a') + expect(FileUtils).to receive(:rm).with('parts.b') + + command.call + end +end diff --git a/spec/thinking_sphinx/commands/clear_sql_spec.rb b/spec/thinking_sphinx/commands/clear_sql_spec.rb new file mode 100644 index 000000000..a5e0a83d8 --- /dev/null +++ b/spec/thinking_sphinx/commands/clear_sql_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::ClearSQL do + let(:command) { ThinkingSphinx::Commands::ClearSQL.new( + configuration, {:indices => [users_index, parts_index]}, stream + ) } + let(:configuration) { double 'configuration', :preload_indices => true, + :render => true, :indices => [users_index, parts_index], + :indices_location => '/path/to/indices' } + let(:stream) { double :puts => nil } + + let(:users_index) { double(:name => 'users', :type => 'plain', + :render => true, :path => '/path/to/my/index/users') } + let(:parts_index) { double(:name => 'users', :type => 'plain', + :render => true, :path => '/path/to/my/index/parts') } + + before :each do + allow(Dir).to receive(:[]).with('/path/to/my/index/users.*'). + and_return(['users.a', 'users.b']) + allow(Dir).to receive(:[]).with('/path/to/my/index/parts.*'). + and_return(['parts.a', 'parts.b']) + allow(Dir).to receive(:[]).with('/path/to/indices/ts-*.tmp'). + and_return(['/path/to/indices/ts-foo.tmp']) + + allow(FileUtils).to receive_messages :rm_rf => true, :rm => true + allow(File).to receive_messages :exist? => true + end + + it 'finds each file for sql-backed indices' do + expect(Dir).to receive(:[]).with('/path/to/my/index/users.*'). + and_return([]) + + command.call + end + + it "removes each file for real-time indices" do + expect(FileUtils).to receive(:rm).with('users.a') + expect(FileUtils).to receive(:rm).with('users.b') + expect(FileUtils).to receive(:rm).with('parts.a') + expect(FileUtils).to receive(:rm).with('parts.b') + + command.call + end + + it "removes any indexing guard files" do + expect(FileUtils).to receive(:rm_rf).with(["/path/to/indices/ts-foo.tmp"]) + + command.call + end +end diff --git a/spec/thinking_sphinx/commands/configure_spec.rb b/spec/thinking_sphinx/commands/configure_spec.rb new file mode 100644 index 000000000..99f254cfd --- /dev/null +++ b/spec/thinking_sphinx/commands/configure_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::Configure do + let(:command) { ThinkingSphinx::Commands::Configure.new( + configuration, {}, stream + ) } + let(:configuration) { double 'configuration' } + let(:stream) { double :puts => nil } + + before :each do + allow(configuration).to receive_messages( + :configuration_file => '/path/to/foo.conf', + :render_to_file => true + ) + end + + it "renders the configuration to a file" do + expect(configuration).to receive(:render_to_file) + + command.call + end + + it "prints a message stating the file is being generated" do + expect(stream).to receive(:puts). + with('Generating configuration to /path/to/foo.conf') + + command.call + end +end diff --git a/spec/thinking_sphinx/commands/index_real_time_spec.rb b/spec/thinking_sphinx/commands/index_real_time_spec.rb new file mode 100644 index 000000000..98d171998 --- /dev/null +++ b/spec/thinking_sphinx/commands/index_real_time_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::IndexRealTime do + let(:command) { ThinkingSphinx::Commands::IndexRealTime.new( + configuration, {:indices => [users_index, parts_index]}, stream + ) } + let(:configuration) { double 'configuration', :controller => controller } + let(:controller) { double 'controller', :rotate => nil } + let(:stream) { double :puts => nil } + let(:users_index) { double(name: 'users') } + let(:parts_index) { double(name: 'parts') } + + before :each do + allow(ThinkingSphinx::RealTime::Populator).to receive(:populate) + end + + it 'populates each real-index' do + expect(ThinkingSphinx::RealTime::Populator).to receive(:populate). + with(users_index) + expect(ThinkingSphinx::RealTime::Populator).to receive(:populate). + with(parts_index) + + command.call + end + + it "rotates the daemon for each index" do + expect(controller).to receive(:rotate).twice + + command.call + end +end diff --git a/spec/thinking_sphinx/commands/index_sql_spec.rb b/spec/thinking_sphinx/commands/index_sql_spec.rb new file mode 100644 index 000000000..eba36df47 --- /dev/null +++ b/spec/thinking_sphinx/commands/index_sql_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::IndexSQL do + let(:command) { ThinkingSphinx::Commands::IndexSQL.new( + configuration, {:verbose => true}, stream + ) } + let(:configuration) { double 'configuration', :controller => controller, + :indexing_strategy => indexing_strategy, + :guarding_strategy => guarding_strategy } + let(:controller) { double 'controller', :index => true } + let(:stream) { double :puts => nil } + let(:indexing_strategy) { Proc.new { |names, &block| block.call names } } + let(:guarding_strategy) { Proc.new { |names, &block| block.call names } } + + before :each do + allow(ThinkingSphinx).to receive_messages :before_index_hooks => [] + end + + it "calls all registered hooks" do + called = false + ThinkingSphinx.before_index_hooks << Proc.new { called = true } + + command.call + + expect(called).to eq(true) + end + + it "indexes all indices verbosely" do + expect(controller).to receive(:index).with(:verbose => true) + + command.call + end + + it "does not index verbosely if requested" do + command = ThinkingSphinx::Commands::IndexSQL.new( + configuration, {:verbose => false}, stream + ) + + expect(controller).to receive(:index).with(:verbose => false) + + command.call + end + + it "ignores a nil indices filter" do + command = ThinkingSphinx::Commands::IndexSQL.new( + configuration, {:verbose => false, :indices => nil}, stream + ) + + expect(controller).to receive(:index).with(:verbose => false) + + command.call + end + + it "ignores an empty indices filter" do + command = ThinkingSphinx::Commands::IndexSQL.new( + configuration, {:verbose => false, :indices => []}, stream + ) + + expect(controller).to receive(:index).with(:verbose => false) + + command.call + end + + it "uses filtered index names" do + command = ThinkingSphinx::Commands::IndexSQL.new( + configuration, {:verbose => false, :indices => ['foo_bar']}, stream + ) + + expect(controller).to receive(:index).with('foo_bar', :verbose => false) + + command.call + end + + it "does not call hooks when filtering by index" do + called = false + ThinkingSphinx.before_index_hooks << Proc.new { called = true } + + ThinkingSphinx::Commands::IndexSQL.new( + configuration, {:verbose => false, :indices => ['foo_bar']}, stream + ).call + + expect(called).to eq(false) + end +end diff --git a/spec/thinking_sphinx/commands/merge_and_update_spec.rb b/spec/thinking_sphinx/commands/merge_and_update_spec.rb new file mode 100644 index 000000000..7fd7fdbc8 --- /dev/null +++ b/spec/thinking_sphinx/commands/merge_and_update_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::MergeAndUpdate do + let(:command) { ThinkingSphinx::Commands::MergeAndUpdate.new( + configuration, {}, stream + ) } + let(:configuration) { double "configuration", :preload_indices => nil, + :render => "", :indices => [core_index_a, delta_index_a, rt_index, + plain_index, core_index_b, delta_index_b] } + let(:stream) { double :puts => nil } + let(:commander) { double :call => true } + let(:core_index_a) { double "index", :type => "plain", :options => {:delta_processor => true}, :delta? => false, :name => "index_a_core", :model => model_a, :path => "index_a_core" } + let(:delta_index_a) { double "index", :type => "plain", :options => {:delta_processor => true}, :delta? => true, :name => "index_a_delta", :path => "index_a_delta" } + let(:core_index_b) { double "index", :type => "plain", :options => {:delta_processor => true}, :delta? => false, :name => "index_b_core", :model => model_b, :path => "index_b_core" } + let(:delta_index_b) { double "index", :type => "plain", :options => {:delta_processor => true}, :delta? => true, :name => "index_b_delta", :path => "index_b_delta" } + let(:rt_index) { double "index", :type => "rt", :name => "rt_index" } + let(:plain_index) { double "index", :type => "plain", :name => "plain_index", :options => {:delta_processor => nil} } + let(:model_a) { double "model", :where => where_a } + let(:model_b) { double "model", :where => where_b } + let(:where_a) { double "where", :update_all => nil } + let(:where_b) { double "where", :update_all => nil } + + before :each do + stub_const 'ThinkingSphinx::Commander', commander + end + + it "merges core/delta pairs" do + expect(commander).to receive(:call).with( + :merge, configuration, hash_including( + :core_index => core_index_a, + :delta_index => delta_index_a, + :filters => {:sphinx_deleted => 0} + ), stream + ) + expect(commander).to receive(:call).with( + :merge, configuration, hash_including( + :core_index => core_index_b, + :delta_index => delta_index_b, + :filters => {:sphinx_deleted => 0} + ), stream + ) + + command.call + end + + it "unflags delta records" do + expect(model_a).to receive(:where).with(:delta => true).and_return(where_a) + expect(where_a).to receive(:update_all).with(:delta => false) + + expect(model_b).to receive(:where).with(:delta => true).and_return(where_b) + expect(where_b).to receive(:update_all).with(:delta => false) + + command.call + end + + it "ignores real-time indices" do + expect(commander).to_not receive(:call).with( + :merge, configuration, hash_including(:core_index => rt_index), stream + ) + expect(commander).to_not receive(:call).with( + :merge, configuration, hash_including(:delta_index => rt_index), stream + ) + + command.call + end + + it "ignores non-delta SQL indices" do + expect(commander).to_not receive(:call).with( + :merge, configuration, hash_including(:core_index => plain_index), + stream + ) + expect(commander).to_not receive(:call).with( + :merge, configuration, hash_including(:delta_index => plain_index), + stream + ) + + command.call + end + + context "with index name filter" do + let(:command) { ThinkingSphinx::Commands::MergeAndUpdate.new( + configuration, {:index_names => ["index_a"]}, stream + ) } + + it "only processes matching indices" do + expect(commander).to receive(:call).with( + :merge, configuration, hash_including( + :core_index => core_index_a, + :delta_index => delta_index_a, + :filters => {:sphinx_deleted => 0} + ), stream + ) + expect(commander).to_not receive(:call).with( + :merge, configuration, hash_including( + :core_index => core_index_b, + :delta_index => delta_index_b, + :filters => {:sphinx_deleted => 0} + ), stream + ) + + command.call + end + end +end diff --git a/spec/thinking_sphinx/commands/merge_spec.rb b/spec/thinking_sphinx/commands/merge_spec.rb new file mode 100644 index 000000000..159161246 --- /dev/null +++ b/spec/thinking_sphinx/commands/merge_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::Merge do + let(:command) { ThinkingSphinx::Commands::Merge.new( + configuration, {:core_index => core_index, :delta_index => delta_index, + :filters => {:sphinx_deleted => 0}}, stream + ) } + let(:configuration) { double "configuration", :controller => controller } + let(:stream) { double :puts => nil } + let(:controller) { double "controller", :merge => nil } + let(:core_index) { double "index", :path => "index_a_core", + :name => "index_a_core" } + let(:delta_index) { double "index", :path => "index_a_delta", + :name => "index_a_delta" } + + before :each do + allow(File).to receive(:exist?).and_return(true) + end + + it "merges core/delta pairs" do + expect(controller).to receive(:merge).with( + "index_a_core", + "index_a_delta", + :filters => {:sphinx_deleted => 0}, + :verbose => nil + ) + + command.call + end + + it "does not merge if just the core does not exist" do + allow(File).to receive(:exist?).with("index_a_core.spi").and_return(false) + + expect(controller).to_not receive(:merge) + + command.call + end + + it "does not merge if just the delta does not exist" do + allow(File).to receive(:exist?).with("index_a_delta.spi").and_return(false) + + expect(controller).to_not receive(:merge) + + command.call + end +end diff --git a/spec/thinking_sphinx/commands/prepare_spec.rb b/spec/thinking_sphinx/commands/prepare_spec.rb new file mode 100644 index 000000000..2a99c337b --- /dev/null +++ b/spec/thinking_sphinx/commands/prepare_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::Prepare do + let(:command) { ThinkingSphinx::Commands::Prepare.new( + configuration, {}, stream + ) } + let(:configuration) { double 'configuration', + :indices_location => '/path/to/indices', :settings => {} + } + let(:stream) { double :puts => nil } + + before :each do + allow(FileUtils).to receive_messages :mkdir_p => true + end + + it "creates the directory for the index files" do + expect(FileUtils).to receive(:mkdir_p).with('/path/to/indices') + + command.call + end + + it "skips directory creation if flag is set" do + configuration.settings['skip_directory_creation'] = true + + expect(FileUtils).to_not receive(:mkdir_p) + + command.call + end +end diff --git a/spec/thinking_sphinx/commands/running_spec.rb b/spec/thinking_sphinx/commands/running_spec.rb new file mode 100644 index 000000000..a82a99794 --- /dev/null +++ b/spec/thinking_sphinx/commands/running_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::Running do + let(:command) { ThinkingSphinx::Commands::Running.new( + configuration, {}, stream + ) } + let(:configuration) { + double 'configuration', :controller => controller, :settings => {} + } + let(:stream) { double :puts => nil } + let(:controller) { double 'controller', :running? => false } + + it 'returns true when Sphinx is running' do + allow(controller).to receive(:running?).and_return(true) + + expect(command.call).to eq(true) + end + + it 'returns false when Sphinx is not running' do + expect(command.call).to eq(false) + end + + it 'returns true if the flag is set' do + configuration.settings['skip_running_check'] = true + + expect(command.call).to eq(true) + end +end diff --git a/spec/thinking_sphinx/commands/start_detached_spec.rb b/spec/thinking_sphinx/commands/start_detached_spec.rb new file mode 100644 index 000000000..29e14d76e --- /dev/null +++ b/spec/thinking_sphinx/commands/start_detached_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::StartDetached do + let(:command) { + ThinkingSphinx::Commands::StartDetached.new(configuration, {}, stream) + } + let(:configuration) { + double 'configuration', :controller => controller, :settings => {} + } + let(:controller) { double 'controller', :start => result, :pid => 101 } + let(:result) { double 'result', :command => 'start', :status => 1, + :output => '' } + let(:stream) { double :puts => nil } + + before :each do + allow(controller).to receive(:running?).and_return(true) + allow(configuration).to receive_messages( + :indices_location => 'my/index/files', + :searchd => double(:log => '/path/to/log') + ) + allow(command).to receive(:exit).and_return(true) + + allow(FileUtils).to receive_messages :mkdir_p => true + end + + it "creates the index files directory" do + expect(FileUtils).to receive(:mkdir_p).with('my/index/files') + + command.call + end + + it "skips directory creation if flag is set" do + configuration.settings['skip_directory_creation'] = true + + expect(FileUtils).to_not receive(:mkdir_p) + + command.call + end + + it "starts the daemon" do + expect(controller).to receive(:start) + + command.call + end + + it "prints a success message if the daemon has started" do + allow(controller).to receive(:running?).and_return(true) + + expect(stream).to receive(:puts). + with('Started searchd successfully (pid: 101).') + + command.call + end + + it "prints a failure message if the daemon does not start" do + allow(controller).to receive(:running?).and_return(false) + allow(command).to receive(:exit) + + expect(stream).to receive(:puts) do |string| + expect(string).to match('The Sphinx start command failed') + end + + command.call + end +end diff --git a/spec/thinking_sphinx/commands/stop_spec.rb b/spec/thinking_sphinx/commands/stop_spec.rb new file mode 100644 index 000000000..d1bd037e9 --- /dev/null +++ b/spec/thinking_sphinx/commands/stop_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Commands::Stop do + let(:command) { + ThinkingSphinx::Commands::Stop.new(configuration, {}, stream) + } + let(:configuration) { double 'configuration', :controller => controller } + let(:controller) { double 'controller', :stop => true, :pid => 101 } + let(:stream) { double :puts => nil } + let(:commander) { double :call => nil } + + before :each do + stub_const 'ThinkingSphinx::Commander', commander + + allow(commander).to receive(:call). + with(:running, configuration, {}, stream).and_return(true, true, false) + end + + it "prints a message if the daemon is not already running" do + allow(commander).to receive(:call). + with(:running, configuration, {}, stream).and_return(false) + + expect(stream).to receive(:puts).with('searchd is not currently running.'). + and_return(nil) + expect(stream).to_not receive(:puts). + with('"Stopped searchd daemon (pid: ).') + + command.call + end + + it "does not try to stop the daemon if it's not running" do + allow(commander).to receive(:call). + with(:running, configuration, {}, stream).and_return(false) + + expect(controller).to_not receive(:stop) + + command.call + end + + it "stops the daemon" do + expect(controller).to receive(:stop) + + command.call + end + + it "prints a message informing the daemon has stopped" do + expect(stream).to receive(:puts).with('Stopped searchd daemon (pid: 101).') + + command.call + end + + it "should retry stopping the daemon until it stops" do + allow(commander).to receive(:call). + with(:running, configuration, {}, stream). + and_return(true, true, true, false) + + expect(controller).to receive(:stop).twice + + command.call + end +end diff --git a/spec/thinking_sphinx/configuration/minimum_fields_spec.rb b/spec/thinking_sphinx/configuration/minimum_fields_spec.rb new file mode 100644 index 000000000..9c85db788 --- /dev/null +++ b/spec/thinking_sphinx/configuration/minimum_fields_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Configuration::MinimumFields do + let(:indices) { [index_a, index_b] } + let(:index_a) { double 'Index A', :model => model_a, :type => 'plain', + :sources => [double(:fields => [field_a1, field_a2])] } + let(:index_b) { double 'Index B', :model => model_b, :type => 'rt', + :fields => [field_b1, field_b2] } + let(:field_a1) { double :name => 'sphinx_internal_class_name' } + let(:field_a2) { double :name => 'name' } + let(:field_b1) { double :name => 'sphinx_internal_class_name' } + let(:field_b2) { double :name => 'name' } + let(:model_a) { double :inheritance_column => 'type', + :table_exists? => true } + let(:model_b) { double :inheritance_column => 'type', + :table_exists? => true } + let(:subject) { ThinkingSphinx::Configuration::MinimumFields.new indices } + + it 'removes the class name fields when no index models have type columns' do + allow(model_a).to receive(:column_names).and_return(['id', 'name']) + allow(model_b).to receive(:column_names).and_return(['id', 'name']) + + subject.reconcile + + expect(index_a.sources.first.fields).to eq([field_a2]) + expect(index_b.fields).to eq([field_b2]) + end + + it 'removes the class name fields when models have no tables' do + allow(model_a).to receive(:table_exists?).and_return(false) + allow(model_b).to receive(:table_exists?).and_return(false) + + subject.reconcile + + expect(index_a.sources.first.fields).to eq([field_a2]) + expect(index_b.fields).to eq([field_b2]) + end + + it 'removes the class name fields only for the rt indices without type column' do + allow(model_a).to receive(:column_names).and_return(['id', 'name', 'type']) + allow(model_b).to receive(:column_names).and_return(['id', 'name']) + + subject.reconcile + + expect(index_a.sources.first.fields).to eq([field_a1, field_a2]) + expect(index_b.fields).to eq([field_b2]) + end + + it 'removes the class name fields only for the plain indices without type column' do + allow(model_a).to receive(:column_names).and_return(['id', 'name']) + allow(model_b).to receive(:column_names).and_return(['id', 'name', 'type']) + + subject.reconcile + + expect(index_a.sources.first.fields).to eq([field_a2]) + expect(index_b.fields).to eq([field_b1, field_b2]) + end +end diff --git a/spec/thinking_sphinx/configuration_spec.rb b/spec/thinking_sphinx/configuration_spec.rb index 4a190cab0..b7c9df672 100644 --- a/spec/thinking_sphinx/configuration_spec.rb +++ b/spec/thinking_sphinx/configuration_spec.rb @@ -1,22 +1,31 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::Configuration do let(:config) { ThinkingSphinx::Configuration.instance } + let(:use_load?) { ActiveRecord::VERSION::MAJOR > 5 } + let(:loading_object) { use_load? ? config : ActiveSupport::Dependencies } + let(:loading_method) { use_load? ? :load : :require_or_load } after :each do ThinkingSphinx::Configuration.reset end + def expect_loading_of(file) + expect(loading_object).to receive(loading_method).with(file).once + end + describe '.instance' do it "returns an instance of ThinkingSphinx::Configuration" do - ThinkingSphinx::Configuration.instance. - should be_a(ThinkingSphinx::Configuration) + expect(ThinkingSphinx::Configuration.instance). + to be_a(ThinkingSphinx::Configuration) end it "memoizes the instance" do config = double('configuration') - ThinkingSphinx::Configuration.should_receive(:new).once.and_return(config) + expect(ThinkingSphinx::Configuration).to receive(:new).once.and_return(config) ThinkingSphinx::Configuration.instance ThinkingSphinx::Configuration.instance @@ -29,8 +38,13 @@ end it 'does not cache settings after reset' do - File.stub :exists? => true - File.stub :read => { + allow(File).to receive(:exist?).and_wrap_original do |original, path| + next true if path.to_s == File.absolute_path("config/thinking_sphinx.yml", Rails.root) + + original.call(path) + end + + allow(File).to receive_messages :read => { 'test' => {'foo' => 'bugs'}, 'production' => {'foo' => 'bar'} }.to_yaml @@ -38,33 +52,33 @@ ThinkingSphinx::Configuration.reset # Grab a new copy of the instance. config = ThinkingSphinx::Configuration.instance - config.settings['foo'].should == 'bugs' + expect(config.settings['foo']).to eq('bugs') config.framework = double :environment => 'production', :root => Pathname.new(__FILE__).join('..', '..', 'internal') - config.settings['foo'].should == 'bar' + expect(config.settings['foo']).to eq('bar') end end describe '#configuration_file' do it "uses the Rails environment in the configuration file name" do - config.configuration_file. - should == File.join(Rails.root, 'config', 'test.sphinx.conf') + expect(config.configuration_file). + to eq(File.join(Rails.root, 'config', 'test.sphinx.conf')) end it "respects provided settings" do write_configuration 'configuration_file' => '/path/to/foo.conf' - config.configuration_file.should == '/path/to/foo.conf' + expect(config.configuration_file).to eq('/path/to/foo.conf') end end describe '#controller' do it "returns an instance of Riddle::Controller" do - config.controller.should be_a(Riddle::Controller) + expect(config.controller).to be_a(Riddle::Controller) end it "memoizes the instance" do - Riddle::Controller.should_receive(:new).once. + expect(Riddle::Controller).to receive(:new).once. and_return(double('controller')) config.controller @@ -74,19 +88,19 @@ it "sets the bin path from the thinking_sphinx.yml file" do write_configuration('bin_path' => '/foo/bar/bin/') - config.controller.bin_path.should == '/foo/bar/bin/' + expect(config.controller.bin_path).to eq('/foo/bar/bin/') end it "appends a backslash to the bin_path if appropriate" do write_configuration('bin_path' => '/foo/bar/bin') - config.controller.bin_path.should == '/foo/bar/bin/' + expect(config.controller.bin_path).to eq('/foo/bar/bin/') end end describe '#index_paths' do it "uses app/indices in the Rails app by default" do - config.index_paths.should include(File.join(Rails.root, 'app', 'indices')) + expect(config.index_paths).to include(File.join(Rails.root, 'app', 'indices')) end it "uses app/indices in the Rails engines" do @@ -95,22 +109,69 @@ } } engine_class = double :instance => engine - Rails::Engine.should_receive(:subclasses).and_return([ engine_class ]) + expect(Rails::Engine).to receive(:subclasses).and_return([ engine_class ]) - config.index_paths.should include('/engine/app/indices') + expect(config.index_paths).to include('/engine/app/indices') end end describe '#indices_location' do it "stores index files in db/sphinx/ENVIRONMENT" do - config.indices_location. - should == File.join(Rails.root, 'db', 'sphinx', 'test') + expect(config.indices_location). + to eq(File.join(Rails.root, 'db', 'sphinx', 'test')) end it "respects provided settings" do write_configuration 'indices_location' => '/my/index/files' - config.indices_location.should == '/my/index/files' + expect(config.indices_location).to eq('/my/index/files') + end + + it "respects relative paths" do + write_configuration 'indices_location' => 'my/index/files' + + expect(config.indices_location).to eq('my/index/files') + end + + it "translates relative paths to absolute if config requests it" do + write_configuration( + 'indices_location' => 'my/index/files', + 'absolute_paths' => true + ) + + expect(config.indices_location).to eq( + File.join(config.framework.root, 'my/index/files') + ) + end + + it "respects paths that are already absolute" do + write_configuration( + 'indices_location' => '/my/index/files', + 'absolute_paths' => true + ) + + expect(config.indices_location).to eq('/my/index/files') + end + + it "translates linked directories" do + write_configuration( + 'indices_location' => 'mine/index/files', + 'absolute_paths' => true + ) + + framework = ThinkingSphinx::Frameworks.current + local_path = File.join framework.root, "mine" + linked_path = File.join framework.root, "my" + + FileUtils.mkdir_p linked_path + `ln -s #{linked_path} #{local_path}` + + expect(config.indices_location).to eq( + File.join(config.framework.root, "my/index/files") + ) + + FileUtils.rm_rf local_path + FileUtils.rm_rf linked_path end end @@ -120,30 +181,30 @@ end it "sets the daemon pid file within log for the Rails app" do - config.searchd.pid_file. - should == File.join(Rails.root, 'log', 'test.sphinx.pid') + expect(config.searchd.pid_file). + to eq(File.join(Rails.root, 'log', 'test.sphinx.pid')) end it "sets the daemon log within log for the Rails app" do - config.searchd.log. - should == File.join(Rails.root, 'log', 'test.searchd.log') + expect(config.searchd.log). + to eq(File.join(Rails.root, 'log', 'test.searchd.log')) end it "sets the query log within log for the Rails app" do - config.searchd.query_log. - should == File.join(Rails.root, 'log', 'test.searchd.query.log') + expect(config.searchd.query_log). + to eq(File.join(Rails.root, 'log', 'test.searchd.query.log')) end it "sets indexer settings if within thinking_sphinx.yml" do write_configuration 'mem_limit' => '128M' - config.indexer.mem_limit.should == '128M' + expect(config.indexer.mem_limit).to eq('128M') end it "sets searchd settings if within thinking_sphinx.yml" do write_configuration 'workers' => 'none' - config.searchd.workers.should == 'none' + expect(config.searchd.workers).to eq('none') end it 'adds settings to indexer without common section' do @@ -164,18 +225,18 @@ let(:reference) { double('reference') } it "starts at 0" do - config.next_offset(reference).should == 0 + expect(config.next_offset(reference)).to eq(0) end it "increments for each new reference" do - config.next_offset(double('reference')).should == 0 - config.next_offset(double('reference')).should == 1 - config.next_offset(double('reference')).should == 2 + expect(config.next_offset(double('reference'))).to eq(0) + expect(config.next_offset(double('reference'))).to eq(1) + expect(config.next_offset(double('reference'))).to eq(2) end it "doesn't increment for recorded references" do - config.next_offset(reference).should == 0 - config.next_offset(reference).should == 0 + expect(config.next_offset(reference)).to eq(0) + expect(config.next_offset(reference)).to eq(0) end end @@ -190,9 +251,9 @@ it "searches each index path for ruby files" do config.index_paths.replace ['/path/to/indices', '/path/to/other/indices'] - Dir.should_receive(:[]).with('/path/to/indices/**/*.rb').once. + expect(Dir).to receive(:[]).with('/path/to/indices/**/*.rb').once. and_return([]) - Dir.should_receive(:[]).with('/path/to/other/indices/**/*.rb').once. + expect(Dir).to receive(:[]).with('/path/to/other/indices/**/*.rb').once. and_return([]) config.preload_indices @@ -200,37 +261,33 @@ it "loads each file returned" do config.index_paths.replace ['/path/to/indices'] - Dir.stub! :[] => [ + allow(Dir).to receive_messages :[] => [ '/path/to/indices/foo_index.rb', '/path/to/indices/bar_index.rb' ] - ActiveSupport::Dependencies.should_receive(:require_or_load). - with('/path/to/indices/foo_index.rb').once - ActiveSupport::Dependencies.should_receive(:require_or_load). - with('/path/to/indices/bar_index.rb').once + expect_loading_of('/path/to/indices/foo_index.rb') + expect_loading_of('/path/to/indices/bar_index.rb') config.preload_indices end it "does not double-load indices" do config.index_paths.replace ['/path/to/indices'] - Dir.stub! :[] => [ + allow(Dir).to receive_messages :[] => [ '/path/to/indices/foo_index.rb', '/path/to/indices/bar_index.rb' ] - ActiveSupport::Dependencies.should_receive(:require_or_load). - with('/path/to/indices/foo_index.rb').once - ActiveSupport::Dependencies.should_receive(:require_or_load). - with('/path/to/indices/bar_index.rb').once + expect_loading_of('/path/to/indices/foo_index.rb') + expect_loading_of('/path/to/indices/bar_index.rb') config.preload_indices config.preload_indices end it 'adds distributed indices' do - distributor.should_receive(:reconcile) + expect(distributor).to receive(:reconcile) config.preload_indices end @@ -238,7 +295,7 @@ it 'does not add distributed indices if disabled' do write_configuration('distributed_indices' => false) - distributor.should_not_receive(:reconcile) + expect(distributor).not_to receive(:reconcile) config.preload_indices end @@ -246,15 +303,15 @@ describe '#render' do before :each do - config.searchd.stub! :render => 'searchd { }' + allow(config.searchd).to receive_messages :render => 'searchd { }' end it "searches each index path for ruby files" do config.index_paths.replace ['/path/to/indices', '/path/to/other/indices'] - Dir.should_receive(:[]).with('/path/to/indices/**/*.rb').once. + expect(Dir).to receive(:[]).with('/path/to/indices/**/*.rb').once. and_return([]) - Dir.should_receive(:[]).with('/path/to/other/indices/**/*.rb').once. + expect(Dir).to receive(:[]).with('/path/to/other/indices/**/*.rb').once. and_return([]) config.render @@ -262,30 +319,26 @@ it "loads each file returned" do config.index_paths.replace ['/path/to/indices'] - Dir.stub! :[] => [ + allow(Dir).to receive_messages :[] => [ '/path/to/indices/foo_index.rb', '/path/to/indices/bar_index.rb' ] - ActiveSupport::Dependencies.should_receive(:require_or_load). - with('/path/to/indices/foo_index.rb').once - ActiveSupport::Dependencies.should_receive(:require_or_load). - with('/path/to/indices/bar_index.rb').once + expect_loading_of('/path/to/indices/foo_index.rb') + expect_loading_of('/path/to/indices/bar_index.rb') config.render end it "does not double-load indices" do config.index_paths.replace ['/path/to/indices'] - Dir.stub! :[] => [ + allow(Dir).to receive_messages :[] => [ '/path/to/indices/foo_index.rb', '/path/to/indices/bar_index.rb' ] - ActiveSupport::Dependencies.should_receive(:require_or_load). - with('/path/to/indices/foo_index.rb').once - ActiveSupport::Dependencies.should_receive(:require_or_load). - with('/path/to/indices/bar_index.rb').once + expect_loading_of('/path/to/indices/foo_index.rb') + expect_loading_of('/path/to/indices/bar_index.rb') config.preload_indices config.preload_indices @@ -293,70 +346,162 @@ end describe '#render_to_file' do - let(:file) { double('file') } - let(:output) { config.render } + let(:file) { double('file') } + let(:output) { config.render } + let(:skip_directories) { false } before :each do - config.searchd.stub! :render => 'searchd { }' + write_configuration('skip_directory_creation' => skip_directories) + + allow(config.searchd).to receive_messages :render => 'searchd { }' end it "writes the rendered configuration to the file" do config.configuration_file = '/path/to/file.config' - config.should_receive(:open).with('/path/to/file.config', 'w'). + expect(config).to receive(:open).with('/path/to/file.config', 'w'). and_yield(file) - file.should_receive(:write).with(output) + expect(file).to receive(:write).with(output) config.render_to_file end it "creates a directory at the binlog_path" do - FileUtils.stub :mkdir_p => true - config.stub :searchd => double(:binlog_path => '/path/to/binlog') + allow(FileUtils).to receive_messages :mkdir_p => true + allow(config).to receive_messages :searchd => double(:binlog_path => '/path/to/binlog') - FileUtils.should_receive(:mkdir_p).with('/path/to/binlog') + expect(FileUtils).to receive(:mkdir_p).with('/path/to/binlog') config.render_to_file end it "skips creating a directory when the binlog_path is blank" do - FileUtils.stub :mkdir_p => true - config.stub :searchd => double(:binlog_path => '') + allow(loading_object).to receive(loading_method) + + allow(FileUtils).to receive_messages :mkdir_p => true + allow(config).to receive_messages :searchd => double(:binlog_path => '') - FileUtils.should_not_receive(:mkdir_p) + expect(FileUtils).not_to receive(:mkdir_p) config.render_to_file end + + context 'skipping directory creation' do + let(:skip_directories) { true } + + it "skips creating a directory when flag is set" do + expect(FileUtils).not_to receive(:mkdir_p) + + config.render_to_file + end + end end describe '#searchd' do describe '#address' do it "defaults to 127.0.0.1" do - config.searchd.address.should == '127.0.0.1' + expect(config.searchd.address).to eq('127.0.0.1') end it "respects the address setting" do write_configuration('address' => '10.11.12.13') - config.searchd.address.should == '10.11.12.13' + expect(config.searchd.address).to eq('10.11.12.13') end end + describe '#log' do + it "defaults to an environment-specific file" do + expect(config.searchd.log).to eq( + File.join(config.framework.root, "log/test.searchd.log") + ) + end + + it "translates linked directories" do + framework = ThinkingSphinx::Frameworks.current + log_path = File.join framework.root, "log" + linked_path = File.join framework.root, "logging" + log_exists = File.exist? log_path + + FileUtils.mv log_path, "#{log_path}-tmp" if log_exists + FileUtils.mkdir_p linked_path + `ln -s #{linked_path} #{log_path}` + + expect(config.searchd.log).to eq( + File.join(config.framework.root, "logging/test.searchd.log") + ) + + FileUtils.rm log_path + FileUtils.rmdir linked_path + FileUtils.mv "#{log_path}-tmp", log_path if log_exists + end unless RUBY_PLATFORM == "java" + end + describe '#mysql41' do it "defaults to 9306" do - config.searchd.mysql41.should == 9306 + expect(config.searchd.mysql41).to eq(9306) end it "respects the port setting" do write_configuration('port' => 9313) - config.searchd.mysql41.should == 9313 + expect(config.searchd.mysql41).to eq(9313) end it "respects the mysql41 setting" do write_configuration('mysql41' => 9307) - config.searchd.mysql41.should == 9307 + expect(config.searchd.mysql41).to eq(9307) + end + end + + describe "#socket" do + it "does not set anything by default" do + expect(config.searchd.socket).to be_nil + end + + it "ignores unspecified address and port when socket is set" do + write_configuration("socket" => "/my/socket") + + expect(config.searchd.socket).to eq("/my/socket:mysql41") + expect(config.searchd.address).to be_nil + expect(config.searchd.mysql41).to be_nil + end + + it "allows address and socket settings" do + write_configuration("socket" => "/my/socket", "address" => "1.1.1.1") + + expect(config.searchd.socket).to eq("/my/socket:mysql41") + expect(config.searchd.address).to eq("1.1.1.1") + expect(config.searchd.mysql41).to eq(9306) + end + + it "allows mysql41 and socket settings" do + write_configuration("socket" => "/my/socket", "mysql41" => 9307) + + expect(config.searchd.socket).to eq("/my/socket:mysql41") + expect(config.searchd.address).to eq("127.0.0.1") + expect(config.searchd.mysql41).to eq(9307) + end + + it "allows port and socket settings" do + write_configuration("socket" => "/my/socket", "port" => 9307) + + expect(config.searchd.socket).to eq("/my/socket:mysql41") + expect(config.searchd.address).to eq("127.0.0.1") + expect(config.searchd.mysql41).to eq(9307) + end + + it "allows address, mysql41 and socket settings" do + write_configuration( + "socket" => "/my/socket", + "address" => "1.2.3.4", + "mysql41" => 9307 + ) + + expect(config.searchd.socket).to eq("/my/socket:mysql41") + expect(config.searchd.address).to eq("1.2.3.4") + expect(config.searchd.mysql41).to eq(9307) end end end @@ -364,66 +509,74 @@ describe '#settings' do context 'YAML file exists' do before :each do - File.stub :exists? => true + allow(File).to receive(:exist?).and_wrap_original do |original, path| + next true if path.to_s == File.absolute_path("config/thinking_sphinx.yml", Rails.root) + + original.call(path) + end end it "reads from the YAML file" do - File.should_receive(:read).and_return('') + expect(File).to receive(:read).and_return('') config.settings end it "uses the settings for the given environment" do - File.stub :read => { + allow(File).to receive_messages :read => { 'test' => {'foo' => 'bar'}, 'staging' => {'baz' => 'qux'} }.to_yaml - Rails.stub :env => 'staging' + allow(Rails).to receive_messages :env => 'staging' - config.settings['baz'].should == 'qux' + expect(config.settings['baz']).to eq('qux') end it "remembers the file contents" do - File.should_receive(:read).and_return('') + expect(File).to receive(:read).and_return('') config.settings config.settings end - it "returns an empty hash when no settings for the environment exist" do - File.stub :read => {'test' => {'foo' => 'bar'}}.to_yaml - Rails.stub :env => 'staging' + it "returns the default hash when no settings for the environment exist" do + allow(File).to receive_messages :read => {'test' => {'foo' => 'bar'}}.to_yaml + allow(Rails).to receive_messages :env => 'staging' - config.settings.should == {} + expect(config.settings.class).to eq(Hash) end end context 'YAML file does not exist' do before :each do - File.stub :exists? => false + allow(File).to receive(:exist?).and_wrap_original do |original, path| + next false if path.to_s == File.absolute_path("config/thinking_sphinx.yml", Rails.root) + + original.call(path) + end end it "does not read the file" do - File.should_not_receive(:read) + expect(File).not_to receive(:read) config.settings end - it "returns an empty hash" do - config.settings.should == {} + it "returns a hash" do + expect(config.settings.class).to eq(Hash) end end end describe '#version' do - it "defaults to 2.1.4" do - config.version.should == '2.1.4' + it "defaults to 2.2.11" do + expect(config.version).to eq('2.2.11') end it "respects supplied YAML versions" do write_configuration 'version' => '2.0.4' - config.version.should == '2.0.4' + expect(config.version).to eq('2.0.4') end end end diff --git a/spec/thinking_sphinx/connection/mri_spec.rb b/spec/thinking_sphinx/connection/mri_spec.rb new file mode 100644 index 000000000..a54926ea7 --- /dev/null +++ b/spec/thinking_sphinx/connection/mri_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.describe ThinkingSphinx::Connection::MRI do + subject { described_class.new :host => "127.0.0.1", :port => 9306 } + + let(:client) { double :client, :query => "result", :next_result => false } + + before :each do + allow(Mysql2::Client).to receive(:new).and_return(client) + end + + after :each do + ThinkingSphinx::Configuration.reset + end + + describe "#execute" do + it "sends the query to the client" do + subject.execute "SELECT QUERY" + + expect(client).to have_received(:query).with("SELECT QUERY") + end + + it "returns a result" do + expect(subject.execute("SELECT QUERY")).to eq("result") + end + + context "with long queries" do + let(:maximum) { (2 ** 23) - 5 } + let(:query) { String.new "SELECT * FROM book_core WHERE MATCH('')" } + let(:difference) { maximum - query.length } + + it 'does not allow overly long queries' do + expect { + subject.execute(query.insert(-3, 'a' * (difference + 5))) + }.to raise_error(ThinkingSphinx::QueryLengthError) + end + + it 'does not allow queries longer than specified in the settings' do + ThinkingSphinx::Configuration.reset + + write_configuration('maximum_statement_length' => maximum - 5) + + expect { + subject.execute(query.insert(-3, 'a' * (difference))) + }.to raise_error(ThinkingSphinx::QueryLengthError) + end + end + end +end if RUBY_PLATFORM != 'java' diff --git a/spec/thinking_sphinx/connection_spec.rb b/spec/thinking_sphinx/connection_spec.rb index 91d4c22ec..fbe14f51f 100644 --- a/spec/thinking_sphinx/connection_spec.rb +++ b/spec/thinking_sphinx/connection_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::Connection do @@ -8,9 +10,9 @@ let(:translated_error) { ThinkingSphinx::SphinxError.new } before :each do - ThinkingSphinx::Connection.stub :pool => pool - ThinkingSphinx::SphinxError.stub :new_from_mysql => translated_error - pool.stub(:take).and_yield(connection) + allow(ThinkingSphinx::Connection).to receive_messages :pool => pool + allow(ThinkingSphinx::SphinxError).to receive_messages :new_from_mysql => translated_error + allow(pool).to receive(:take).and_yield(connection) error.statement = 'SELECT * FROM article_core' translated_error.statement = 'SELECT * FROM article_core' @@ -18,41 +20,41 @@ it "yields a connection from the pool" do ThinkingSphinx::Connection.take do |c| - c.should == connection + expect(c).to eq(connection) end end it "retries errors once" do tries = 0 - lambda { + expect { ThinkingSphinx::Connection.take do |c| tries += 1 raise error if tries < 2 end - }.should_not raise_error + }.not_to raise_error end it "retries errors twice" do tries = 0 - lambda { + expect { ThinkingSphinx::Connection.take do |c| tries += 1 raise error if tries < 3 end - }.should_not raise_error + }.not_to raise_error end it "raises a translated error if it fails three times" do tries = 0 - lambda { + expect { ThinkingSphinx::Connection.take do |c| tries += 1 raise error if tries < 4 end - }.should raise_error(ThinkingSphinx::SphinxError) + }.to raise_error(ThinkingSphinx::SphinxError) end [ThinkingSphinx::SyntaxError, ThinkingSphinx::ParseError].each do |klass| @@ -60,9 +62,9 @@ let(:translated_error) { klass.new } it "raises the error" do - lambda { + expect { ThinkingSphinx::Connection.take { |c| raise error } - }.should raise_error(klass) + }.to raise_error(klass) end it "does not yield the connection more than once" do @@ -77,7 +79,7 @@ # end - yields.should == 1 + expect(yields).to eq(1) end end end diff --git a/spec/thinking_sphinx/deletion_spec.rb b/spec/thinking_sphinx/deletion_spec.rb index 1fd54501a..d3efd2bd9 100644 --- a/spec/thinking_sphinx/deletion_spec.rb +++ b/spec/thinking_sphinx/deletion_spec.rb @@ -1,56 +1,56 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::Deletion do describe '.perform' do let(:connection) { double('connection', :execute => nil) } let(:index) { double('index', :name => 'foo_core', - :document_id_for_key => 14, :type => 'plain', :distributed? => false) } + :type => 'plain', :distributed? => false) } before :each do - ThinkingSphinx::Connection.stub(:take).and_yield(connection) - Riddle::Query.stub :update => 'UPDATE STATEMENT' + allow(ThinkingSphinx::Connection).to receive(:take).and_yield(connection) + allow(Riddle::Query).to receive_messages :update => 'UPDATE STATEMENT' end context 'index is SQL-backed' do it "updates the deleted flag to false" do - connection.should_receive(:execute).with <<-SQL -UPDATE foo_core -SET sphinx_deleted = 1 -WHERE id IN (14) - SQL + expect(connection).to receive(:execute).with( + 'UPDATE foo_core SET sphinx_deleted = 1 WHERE sphinx_internal_id IN (7)' + ) ThinkingSphinx::Deletion.perform index, 7 end it "doesn't care about Sphinx errors" do - connection.stub(:execute). + allow(connection).to receive(:execute). and_raise(ThinkingSphinx::ConnectionError.new('')) - lambda { + expect { ThinkingSphinx::Deletion.perform index, 7 - }.should_not raise_error + }.not_to raise_error end end context "index is real-time" do before :each do - index.stub :type => 'rt' + allow(index).to receive_messages :type => 'rt' end it "deletes the record to false" do - connection.should_receive(:execute). - with('DELETE FROM foo_core WHERE id = 14') + expect(connection).to receive(:execute). + with('DELETE FROM foo_core WHERE sphinx_internal_id IN (7)') ThinkingSphinx::Deletion.perform index, 7 end it "doesn't care about Sphinx errors" do - connection.stub(:execute). + allow(connection).to receive(:execute). and_raise(ThinkingSphinx::ConnectionError.new('')) - lambda { + expect { ThinkingSphinx::Deletion.perform index, 7 - }.should_not raise_error + }.not_to raise_error end end end diff --git a/spec/thinking_sphinx/deltas/default_delta_spec.rb b/spec/thinking_sphinx/deltas/default_delta_spec.rb index 5fa341fc0..f5088c3f4 100644 --- a/spec/thinking_sphinx/deltas/default_delta_spec.rb +++ b/spec/thinking_sphinx/deltas/default_delta_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::Deltas::DefaultDelta do @@ -9,11 +11,11 @@ describe '#clause' do context 'for a delta source' do before :each do - adapter.stub :boolean_value => 't' + allow(adapter).to receive_messages :boolean_value => 't' end it "limits results to those flagged as deltas" do - delta.clause(true).should == "articles.delta = t" + expect(delta.clause(true)).to eq("articles.delta = t") end end end @@ -21,46 +23,46 @@ describe '#delete' do let(:connection) { double('connection', :execute => nil) } let(:index) { double('index', :name => 'foo_core', - :document_id_for_key => 14) } + :document_id_for_instance => 14) } let(:instance) { double('instance', :id => 7) } before :each do - ThinkingSphinx::Connection.stub(:take).and_yield(connection) - Riddle::Query.stub :update => 'UPDATE STATEMENT' + allow(ThinkingSphinx::Connection).to receive(:take).and_yield(connection) + allow(Riddle::Query).to receive_messages :update => 'UPDATE STATEMENT' end it "updates the deleted flag to false" do - connection.should_receive(:execute).with('UPDATE STATEMENT') + expect(connection).to receive(:execute).with('UPDATE STATEMENT') delta.delete index, instance end it "builds the update query for the given index" do - Riddle::Query.should_receive(:update). + expect(Riddle::Query).to receive(:update). with('foo_core', anything, anything).and_return('') delta.delete index, instance end it "builds the update query for the sphinx document id" do - Riddle::Query.should_receive(:update). + expect(Riddle::Query).to receive(:update). with(anything, 14, anything).and_return('') delta.delete index, instance end it "builds the update query for setting sphinx_deleted to true" do - Riddle::Query.should_receive(:update). + expect(Riddle::Query).to receive(:update). with(anything, anything, :sphinx_deleted => true).and_return('') delta.delete index, instance end it "doesn't care about Sphinx errors" do - connection.stub(:execute). + allow(connection).to receive(:execute). and_raise(ThinkingSphinx::ConnectionError.new('')) - lambda { delta.delete index, instance }.should_not raise_error + expect { delta.delete index, instance }.not_to raise_error end end @@ -68,13 +70,18 @@ let(:config) { double('config', :controller => controller, :settings => {}) } let(:controller) { double('controller') } + let(:commander) { double('commander', :call => true) } before :each do - ThinkingSphinx::Configuration.stub :instance => config + stub_const 'ThinkingSphinx::Commander', commander + + allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end it "indexes the given index" do - controller.should_receive(:index).with('foo_delta', :verbose => true) + expect(commander).to receive(:call).with( + :index_sql, config, :indices => ['foo_delta'], :verbose => false + ) delta.index double('index', :name => 'foo_delta') end @@ -82,9 +89,9 @@ describe '#reset_query' do it "updates the table to set delta flags to false" do - adapter.stub(:boolean_value) { |value| value ? 't' : 'f' } - delta.reset_query. - should == 'UPDATE articles SET delta = f WHERE delta = t' + allow(adapter).to receive(:boolean_value) { |value| value ? 't' : 'f' } + expect(delta.reset_query). + to eq('UPDATE articles SET delta = f WHERE delta = t') end end @@ -92,7 +99,7 @@ let(:instance) { double('instance') } it "sets instance's delta flag to true" do - instance.should_receive(:delta=).with(true) + expect(instance).to receive(:delta=).with(true) delta.toggle(instance) end @@ -102,15 +109,15 @@ let(:instance) { double('instance') } it "returns the delta flag value when true" do - instance.stub! :delta? => true + allow(instance).to receive_messages :delta? => true - delta.toggled?(instance).should be_true + expect(delta.toggled?(instance)).to be_truthy end it "returns the delta flag value when false" do - instance.stub! :delta? => false + allow(instance).to receive_messages :delta? => false - delta.toggled?(instance).should be_false + expect(delta.toggled?(instance)).to be_falsey end end end diff --git a/spec/thinking_sphinx/deltas_spec.rb b/spec/thinking_sphinx/deltas_spec.rb index be55bf7f2..4e62fdcfd 100644 --- a/spec/thinking_sphinx/deltas_spec.rb +++ b/spec/thinking_sphinx/deltas_spec.rb @@ -1,21 +1,23 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::Deltas do describe '.processor_for' do it "returns the default processor class when given true" do - ThinkingSphinx::Deltas.processor_for(true). - should == ThinkingSphinx::Deltas::DefaultDelta + expect(ThinkingSphinx::Deltas.processor_for(true)). + to eq(ThinkingSphinx::Deltas::DefaultDelta) end it "returns the class when given one" do klass = Class.new - ThinkingSphinx::Deltas.processor_for(klass).should == klass + expect(ThinkingSphinx::Deltas.processor_for(klass)).to eq(klass) end it "instantiates a class from the name as a string" do - ThinkingSphinx::Deltas. - processor_for('ThinkingSphinx::Deltas::DefaultDelta'). - should == ThinkingSphinx::Deltas::DefaultDelta + expect(ThinkingSphinx::Deltas. + processor_for('ThinkingSphinx::Deltas::DefaultDelta')). + to eq(ThinkingSphinx::Deltas::DefaultDelta) end end @@ -29,7 +31,7 @@ let(:processor) { double('processor', :index => true) } before :each do - ThinkingSphinx::Configuration.stub :instance => config + allow(ThinkingSphinx::Configuration).to receive_messages :instance => config end it "executes the given block" do @@ -39,12 +41,12 @@ variable = :bar end - variable.should == :bar + expect(variable).to eq(:bar) end it "suspends deltas within the block" do ThinkingSphinx::Deltas.suspend :user do - ThinkingSphinx::Deltas.should be_suspended + expect(ThinkingSphinx::Deltas).to be_suspended end end @@ -53,11 +55,11 @@ # end - ThinkingSphinx::Deltas.should_not be_suspended + expect(ThinkingSphinx::Deltas).not_to be_suspended end it "processes the delta indices for the given reference" do - processor.should_receive(:index).with(delta_index) + expect(processor).to receive(:index).with(delta_index) ThinkingSphinx::Deltas.suspend :user do # @@ -65,7 +67,7 @@ end it "does not process the core indices for the given reference" do - processor.should_not_receive(:index).with(core_index) + expect(processor).not_to receive(:index).with(core_index) ThinkingSphinx::Deltas.suspend :user do # diff --git a/spec/thinking_sphinx/errors_spec.rb b/spec/thinking_sphinx/errors_spec.rb index c40b01b7b..99ea2a2b0 100644 --- a/spec/thinking_sphinx/errors_spec.rb +++ b/spec/thinking_sphinx/errors_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::SphinxError do @@ -6,66 +8,96 @@ :backtrace => ['foo', 'bar'] } it "translates syntax errors" do - error.stub :message => 'index foo: syntax error: something is wrong' + allow(error).to receive_messages :message => 'index foo: syntax error: something is wrong' - ThinkingSphinx::SphinxError.new_from_mysql(error). - should be_a(ThinkingSphinx::SyntaxError) + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::SyntaxError) end it "translates parse errors" do - error.stub :message => 'index foo: parse error: something is wrong' + allow(error).to receive_messages :message => 'index foo: parse error: something is wrong' + + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::ParseError) + end - ThinkingSphinx::SphinxError.new_from_mysql(error). - should be_a(ThinkingSphinx::ParseError) + it "translates 'query is non-computable' errors" do + allow(error).to receive_messages :message => 'index model_core: query is non-computable (single NOT operator)' + + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::ParseError) end it "translates query errors" do - error.stub :message => 'index foo: query error: something is wrong' + allow(error).to receive_messages :message => 'index foo: query error: something is wrong' - ThinkingSphinx::SphinxError.new_from_mysql(error). - should be_a(ThinkingSphinx::QueryError) + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::QueryError) end it "translates connection errors" do - error.stub :message => "Can't connect to MySQL server on '127.0.0.1' (61)" + allow(error).to receive_messages :message => "Can't connect to MySQL server on '127.0.0.1' (61)" + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::ConnectionError) + + allow(error).to receive_messages :message => "Communications link failure" + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::ConnectionError) + + allow(error).to receive_messages :message => "Lost connection to MySQL server" + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::ConnectionError) + + # MariaDB has removed mention of MySQL in error messages: + allow(error).to receive_messages :message => "Can't connect to server on '127.0.0.1' (61)" + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::ConnectionError) + + allow(error).to receive_messages :message => "Lost connection to server" + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::ConnectionError) + end + + it 'translates out-of-bounds errors' do + allow(error).to receive_messages :message => "offset out of bounds (offset=1001, max_matches=1000)" - ThinkingSphinx::SphinxError.new_from_mysql(error). - should be_a(ThinkingSphinx::ConnectionError) + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::OutOfBoundsError) end it 'prefixes the connection error message' do - error.stub :message => "Can't connect to MySQL server on '127.0.0.1' (61)" + allow(error).to receive_messages :message => "Can't connect to MySQL server on '127.0.0.1' (61)" - ThinkingSphinx::SphinxError.new_from_mysql(error).message. - should == "Error connecting to Sphinx via the MySQL protocol. Can't connect to MySQL server on '127.0.0.1' (61)" + expect(ThinkingSphinx::SphinxError.new_from_mysql(error).message). + to eq("Error connecting to Sphinx via the MySQL protocol. Can't connect to MySQL server on '127.0.0.1' (61)") end it "translates jdbc connection errors" do - error.stub :message => "Communications link failure" + allow(error).to receive_messages :message => "Communications link failure" - ThinkingSphinx::SphinxError.new_from_mysql(error). - should be_a(ThinkingSphinx::ConnectionError) + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::ConnectionError) end it 'prefixes the jdbc connection error message' do - error.stub :message => "Communications link failure" + allow(error).to receive_messages :message => "Communications link failure" - ThinkingSphinx::SphinxError.new_from_mysql(error).message. - should == "Error connecting to Sphinx via the MySQL protocol. Communications link failure" + expect(ThinkingSphinx::SphinxError.new_from_mysql(error).message). + to eq("Error connecting to Sphinx via the MySQL protocol. Communications link failure") end it "defaults to sphinx errors" do - error.stub :message => 'index foo: unknown error: something is wrong' + allow(error).to receive_messages :message => 'index foo: unknown error: something is wrong' - ThinkingSphinx::SphinxError.new_from_mysql(error). - should be_a(ThinkingSphinx::SphinxError) + expect(ThinkingSphinx::SphinxError.new_from_mysql(error)). + to be_a(ThinkingSphinx::SphinxError) end it "keeps the original error's backtrace" do - error.stub :message => 'index foo: unknown error: something is wrong' + allow(error).to receive_messages :message => 'index foo: unknown error: something is wrong' - ThinkingSphinx::SphinxError.new_from_mysql(error). - backtrace.should == error.backtrace + expect(ThinkingSphinx::SphinxError.new_from_mysql(error). + backtrace).to eq(error.backtrace) end end end diff --git a/spec/thinking_sphinx/excerpter_spec.rb b/spec/thinking_sphinx/excerpter_spec.rb index 6ad095a30..abb6e0ae7 100644 --- a/spec/thinking_sphinx/excerpter_spec.rb +++ b/spec/thinking_sphinx/excerpter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::Excerpter do @@ -7,13 +9,13 @@ } before :each do - ThinkingSphinx::Connection.stub(:take).and_yield(connection) - Riddle::Query.stub :snippets => 'CALL SNIPPETS' + allow(ThinkingSphinx::Connection).to receive(:take).and_yield(connection) + allow(Riddle::Query).to receive_messages :snippets => 'CALL SNIPPETS' end describe '#excerpt!' do it "generates a snippets call" do - Riddle::Query.should_receive(:snippets). + expect(Riddle::Query).to receive(:snippets). with('all of the words', 'index', 'all words', ThinkingSphinx::Excerpter::DefaultOptions). and_return('CALL SNIPPETS') @@ -25,26 +27,27 @@ excerpter = ThinkingSphinx::Excerpter.new('index', 'all words', :before_match => '', :chunk_separator => ' -- ') - Riddle::Query.should_receive(:snippets). - with('all of the words', 'index', 'all words', + expect(Riddle::Query).to receive(:snippets). + with('all of the words', 'index', 'all words', { :before_match => '', :after_match => '', - :chunk_separator => ' -- '). + :chunk_separator => ' -- ' + }). and_return('CALL SNIPPETS') excerpter.excerpt!('all of the words') end it "sends the snippets call to Sphinx" do - connection.should_receive(:execute).with('CALL SNIPPETS'). + expect(connection).to receive(:execute).with('CALL SNIPPETS'). and_return([{'snippet' => ''}]) excerpter.excerpt!('all of the words') end it "returns the first value returned by Sphinx" do - connection.stub :execute => [{'snippet' => 'some highlighted words'}] + allow(connection).to receive_messages :execute => [{'snippet' => 'some highlighted words'}] - excerpter.excerpt!('all of the words').should == 'some highlighted words' + expect(excerpter.excerpt!('all of the words')).to eq('some highlighted words') end end end diff --git a/spec/thinking_sphinx/facet_search_spec.rb b/spec/thinking_sphinx/facet_search_spec.rb index bab2f32c3..e91fa48f3 100644 --- a/spec/thinking_sphinx/facet_search_spec.rb +++ b/spec/thinking_sphinx/facet_search_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::FacetSearch do @@ -25,19 +27,19 @@ DumbSearch = ::Struct.new(:query, :options) do def raw [{ - 'sphinx_internal_class' => 'Foo', - 'price_bracket' => 3, - 'tag_ids' => '1,2', - 'category_id' => 11, - ThinkingSphinx::SphinxQL.count[:column] => 5, - ThinkingSphinx::SphinxQL.group_by[:column] => 2 + 'sphinx_internal_class' => 'Foo', + 'price_bracket' => 3, + 'tag_ids' => '1,2', + 'category_id' => 11, + "sphinx_internal_count" => 5, + "sphinx_internal_group" => 2 }] end end describe '#[]' do it "populates facet results" do - facet_search[:price_bracket].should == {3 => 5} + expect(facet_search[:price_bracket]).to eq({3 => 5}) end end @@ -45,17 +47,17 @@ def raw it "queries on each facet with a grouped search in a batch" do facet_search.populate - batch.searches.detect { |search| + expect(batch.searches.detect { |search| search.options[:group_by] == 'price_bracket' - }.should_not be_nil + }).not_to be_nil end it "limits query for a facet to just indices that have that facet" do facet_search.populate - batch.searches.detect { |search| + expect(batch.searches.detect { |search| search.options[:indices] == ['foo_core'] - }.should_not be_nil + }).not_to be_nil end it "limits facets to the specified set" do @@ -63,25 +65,25 @@ def raw facet_search.populate - batch.searches.collect { |search| + expect(batch.searches.collect { |search| search.options[:group_by] - }.should == ['category_id'] + }).to eq(['category_id']) end it "aliases the class facet from sphinx_internal_class" do - property_a.stub :name => 'sphinx_internal_class' + allow(property_a).to receive_messages :name => 'sphinx_internal_class' facet_search.populate - facet_search[:class].should == {'Foo' => 5} + expect(facet_search[:class]).to eq({'Foo' => 5}) end it "uses the @groupby value for MVAs" do - property_a.stub :name => 'tag_ids', :multi? => true + allow(property_a).to receive_messages :name => 'tag_ids', :multi? => true facet_search.populate - facet_search[:tag_ids].should == {2 => 5} + expect(facet_search[:tag_ids]).to eq({2 => 5}) end [:max_matches, :limit].each do |setting| @@ -89,7 +91,7 @@ def raw facet_search.populate batch.searches.each { |search| - search.options[setting].should == 1000 + expect(search.options[setting]).to eq(1000) } end @@ -99,7 +101,7 @@ def raw facet_search.populate batch.searches.each { |search| - search.options[setting].should == 1234 + expect(search.options[setting]).to eq(1234) } end end @@ -111,7 +113,7 @@ def raw facet_search.populate batch.searches.each { |search| - search.options[setting].should == 42 + expect(search.options[setting]).to eq(42) } end @@ -122,8 +124,8 @@ def raw facet_search.populate batch.searches.each do |search| - search.options[setting].should == 10 - search.options[:max_matches].should == 500 + expect(search.options[setting]).to eq(10) + expect(search.options[:max_matches]).to eq(500) end end end diff --git a/spec/thinking_sphinx/hooks/guard_presence_spec.rb b/spec/thinking_sphinx/hooks/guard_presence_spec.rb new file mode 100644 index 000000000..c144b6342 --- /dev/null +++ b/spec/thinking_sphinx/hooks/guard_presence_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe ThinkingSphinx::Hooks::GuardPresence do + let(:subject) do + ThinkingSphinx::Hooks::GuardPresence.new configuration, stream + end + let(:configuration) { double "configuration", :indices_location => "/path" } + let(:stream) { double "stream", :puts => nil } + + describe "#call" do + it "outputs nothing if no guard files exist" do + allow(Dir).to receive(:[]).with('/path/ts-*.tmp').and_return([]) + + expect(stream).not_to receive(:puts) + + subject.call + end + + it "outputs a warning if a guard file exists" do + allow(Dir).to receive(:[]).with('/path/ts-*.tmp'). + and_return(['/path/ts-foo.tmp']) + + expect(stream).to receive(:puts) + + subject.call + end + end +end diff --git a/spec/thinking_sphinx/index_set_spec.rb b/spec/thinking_sphinx/index_set_spec.rb index 5c58a7cfd..3197009bd 100644 --- a/spec/thinking_sphinx/index_set_spec.rb +++ b/spec/thinking_sphinx/index_set_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx; end require 'active_support/core_ext/string/inflections' @@ -15,15 +17,31 @@ module ThinkingSphinx; end stub_const 'ActiveRecord::Base', ar_base end - def class_double(name, *superclasses) + def class_double(name, methods = {}, *superclasses) klass = double 'class', :name => name, :class => Class - klass.stub :ancestors => ([klass] + superclasses + [ar_base]) + + allow(klass).to receive_messages( + :ancestors => ([klass] + superclasses + [ar_base]), + :inheritance_column => :type + ) + allow(klass).to receive_messages(methods) + klass end describe '#to_a' do + let(:article_index) do + double(:reference => :article, :distributed? => false) + end + let(:opinion_article_index) do + double(:reference => :opinion_article, :distributed? => false) + end + let(:page_index) do + double(:reference => :page, :distributed? => false) + end + it "ensures the indices are loaded" do - configuration.should_receive(:preload_indices) + expect(configuration).to receive(:preload_indices) set.to_a end @@ -37,33 +55,56 @@ def class_double(name, *superclasses) configuration.indices.replace [article_core, user_core, distributed] - set.to_a.should == [article_core, user_core] + expect(set.to_a).to eq([article_core, user_core]) end it "uses indices for the given classes" do configuration.indices.replace [ - double(:reference => :article, :distributed? => false), - double(:reference => :opinion_article, :distributed? => false), - double(:reference => :page, :distributed? => false) + article_index, opinion_article_index, page_index ] - options[:classes] = [class_double('Article')] + options[:classes] = [class_double('Article', :column_names => [])] - set.to_a.length.should == 1 + expect(set.to_a).to eq([article_index]) end - it "requests indices for any superclasses" do + it "uses indices for the given instance's class" do configuration.indices.replace [ - double(:reference => :article, :distributed? => false), - double(:reference => :opinion_article, :distributed? => false), - double(:reference => :page, :distributed? => false) + article_index, opinion_article_index, page_index ] - options[:classes] = [ - class_double('OpinionArticle', class_double('Article')) + instance_class = class_double('Article', :column_names => []) + + options[:instances] = [double(:instance, :class => instance_class)] + + expect(set.to_a).to eq([article_index]) + end + + it "requests indices for any STI superclasses" do + configuration.indices.replace [ + article_index, opinion_article_index, page_index + ] + + article = class_double('Article', :column_names => [:type]) + opinion = class_double('OpinionArticle', {:column_names => [:type]}, + article) + + options[:classes] = [opinion] + + expect(set.to_a).to eq([article_index, opinion_article_index]) + end + + it "does not use MTI superclasses" do + configuration.indices.replace [ + article_index, opinion_article_index, page_index ] - set.to_a.length.should == 2 + article = class_double('Article', :column_names => []) + opinion = class_double('OpinionArticle', {:column_names => []}, article) + + options[:classes] = [opinion] + + expect(set.to_a).to eq([opinion_article_index]) end it "uses named indices if names are provided" do @@ -73,7 +114,7 @@ def class_double(name, *superclasses) options[:indices] = ['article_core'] - set.to_a.should == [article_core] + expect(set.to_a).to eq([article_core]) end it "selects from the full index set those with matching references" do @@ -85,7 +126,7 @@ def class_double(name, *superclasses) options[:references] = [:book, :article] - set.to_a.length.should == 2 + expect(set.to_a.length).to eq(2) end end end diff --git a/spec/thinking_sphinx/index_spec.rb b/spec/thinking_sphinx/index_spec.rb index 129201a95..7d16e2767 100644 --- a/spec/thinking_sphinx/index_spec.rb +++ b/spec/thinking_sphinx/index_spec.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::Index do let(:configuration) { Struct.new(:indices, :settings).new([], {}) } before :each do - ThinkingSphinx::Configuration.stub :instance => configuration + allow(ThinkingSphinx::Configuration).to receive_messages :instance => configuration end describe '.define' do @@ -12,29 +14,29 @@ context 'with ActiveRecord' do before :each do - ThinkingSphinx::ActiveRecord::Index.stub :new => index + allow(ThinkingSphinx::ActiveRecord::Index).to receive_messages :new => index end it "creates an ActiveRecord index" do - ThinkingSphinx::ActiveRecord::Index.should_receive(:new). - with(:user, :with => :active_record).and_return index + expect(ThinkingSphinx::ActiveRecord::Index).to receive(:new). + with(:user, { :with => :active_record }).and_return index ThinkingSphinx::Index.define(:user, :with => :active_record) end it "returns the ActiveRecord index" do - ThinkingSphinx::Index.define(:user, :with => :active_record). - should == [index] + expect(ThinkingSphinx::Index.define(:user, :with => :active_record)). + to eq([index]) end it "adds the index to the collection of indices" do ThinkingSphinx::Index.define(:user, :with => :active_record) - configuration.indices.should include(index) + expect(configuration.indices).to include(index) end it "sets the block in the index" do - index.should_receive(:definition_block=).with instance_of(Proc) + expect(index).to receive(:definition_block=).with instance_of(Proc) ThinkingSphinx::Index.define(:user, :with => :active_record) do indexes name @@ -46,19 +48,19 @@ let(:processor) { double('delta processor') } before :each do - ThinkingSphinx::Deltas.stub :processor_for => processor - ThinkingSphinx::ActiveRecord::Index.stub(:new). + allow(ThinkingSphinx::Deltas).to receive_messages :processor_for => processor + allow(ThinkingSphinx::ActiveRecord::Index).to receive(:new). and_return(index, delta_index) end it "creates two indices with delta settings" do - ThinkingSphinx::ActiveRecord::Index.unstub :new - ThinkingSphinx::ActiveRecord::Index.should_receive(:new). + allow(ThinkingSphinx::ActiveRecord::Index).to receive(:new).and_call_original + expect(ThinkingSphinx::ActiveRecord::Index).to receive(:new). with(:user, hash_including(:delta? => false, :delta_processor => processor) ).once. and_return index - ThinkingSphinx::ActiveRecord::Index.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::Index).to receive(:new). with(:user, hash_including(:delta? => true, :delta_processor => processor) ).once. @@ -74,13 +76,13 @@ :with => :active_record, :delta => true - configuration.indices.should include(index) - configuration.indices.should include(delta_index) + expect(configuration.indices).to include(index) + expect(configuration.indices).to include(delta_index) end it "sets the block in the index" do - index.should_receive(:definition_block=).with instance_of(Proc) - delta_index.should_receive(:definition_block=).with instance_of(Proc) + expect(index).to receive(:definition_block=).with instance_of(Proc) + expect(delta_index).to receive(:definition_block=).with instance_of(Proc) ThinkingSphinx::Index.define(:user, :with => :active_record, @@ -93,29 +95,29 @@ context 'with Real-Time' do before :each do - ThinkingSphinx::RealTime::Index.stub :new => index + allow(ThinkingSphinx::RealTime::Index).to receive_messages :new => index end it "creates a real-time index" do - ThinkingSphinx::RealTime::Index.should_receive(:new). - with(:user, :with => :real_time).and_return index + expect(ThinkingSphinx::RealTime::Index).to receive(:new). + with(:user, { :with => :real_time }).and_return index ThinkingSphinx::Index.define(:user, :with => :real_time) end it "returns the ActiveRecord index" do - ThinkingSphinx::Index.define(:user, :with => :real_time). - should == [index] + expect(ThinkingSphinx::Index.define(:user, :with => :real_time)). + to eq([index]) end it "adds the index to the collection of indices" do ThinkingSphinx::Index.define(:user, :with => :real_time) - configuration.indices.should include(index) + expect(configuration.indices).to include(index) end it "sets the block in the index" do - index.should_receive(:definition_block=).with instance_of(Proc) + expect(index).to receive(:definition_block=).with instance_of(Proc) ThinkingSphinx::Index.define(:user, :with => :real_time) do indexes name @@ -126,13 +128,13 @@ describe '#initialize' do it "is fine with no defaults from settings" do - ThinkingSphinx::Index.new(:user, {}).options.should == {} + expect(ThinkingSphinx::Index.new(:user, {}).options).to eq({}) end it "respects defaults from settings" do configuration.settings['index_options'] = {'delta' => true} - ThinkingSphinx::Index.new(:user, {}).options.should == {:delta => true} + expect(ThinkingSphinx::Index.new(:user, {}).options).to eq({:delta => true}) end end end diff --git a/spec/thinking_sphinx/interfaces/daemon_spec.rb b/spec/thinking_sphinx/interfaces/daemon_spec.rb new file mode 100644 index 000000000..d2f5e8fc0 --- /dev/null +++ b/spec/thinking_sphinx/interfaces/daemon_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Interfaces::Daemon do + let(:configuration) { double 'configuration' } + let(:stream) { double 'stream', :puts => true } + let(:commander) { double :call => nil } + let(:interface) { + ThinkingSphinx::Interfaces::Daemon.new(configuration, {}, stream) + } + + before :each do + stub_const 'ThinkingSphinx::Commander', commander + + allow(commander).to receive(:call). + with(:running, configuration, {}, stream).and_return(false) + end + + describe '#start' do + it "starts the daemon" do + expect(commander).to receive(:call).with( + :start_detached, configuration, {}, stream + ) + + interface.start + end + + it "raises an error if the daemon is already running" do + allow(commander).to receive(:call). + with(:running, configuration, {}, stream).and_return(true) + + expect { + interface.start + }.to raise_error(ThinkingSphinx::SphinxAlreadyRunning) + end + end + + describe '#status' do + it "reports when the daemon is running" do + allow(commander).to receive(:call). + with(:running, configuration, {}, stream).and_return(true) + + expect(stream).to receive(:puts). + with('The Sphinx daemon searchd is currently running.') + + interface.status + end + + it "reports when the daemon is not running" do + allow(commander).to receive(:call). + with(:running, configuration, {}, stream).and_return(false) + + expect(stream).to receive(:puts). + with('The Sphinx daemon searchd is not currently running.') + + interface.status + end + end +end diff --git a/spec/thinking_sphinx/interfaces/real_time_spec.rb b/spec/thinking_sphinx/interfaces/real_time_spec.rb new file mode 100644 index 000000000..09a1a39ea --- /dev/null +++ b/spec/thinking_sphinx/interfaces/real_time_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Interfaces::SQL do + let(:interface) { ThinkingSphinx::Interfaces::RealTime.new( + configuration, {}, stream + ) } + let(:configuration) { double 'configuration', :controller => controller, + :render => true, :indices_location => '/path/to/indices', + :preload_indices => true } + let(:controller) { double 'controller', :running? => true } + let(:commander) { double :call => true } + let(:stream) { double :puts => nil } + + before :each do + stub_const "ThinkingSphinx::Commander", commander + end + + describe '#clear' do + let(:plain_index) { double(:type => 'plain') } + let(:users_index) { double(:name => 'users', :type => 'rt', :render => true, + :path => '/path/to/my/index/users') } + let(:parts_index) { double(:name => 'parts', :type => 'rt', :render => true, + :path => '/path/to/my/index/parts') } + + before :each do + allow(configuration).to receive_messages( + :indices => [plain_index, users_index, parts_index] + ) + end + + it 'prepares the indices' do + expect(commander).to receive(:call).with( + :prepare, configuration, {}, stream + ) + + interface.clear + end + + it 'invokes the clear command' do + expect(commander).to receive(:call).with( + :clear_real_time, + configuration, + {:indices => [users_index, parts_index]}, + stream + ) + + interface.clear + end + + context "with options[:index_names]" do + let(:interface) { ThinkingSphinx::Interfaces::RealTime.new( + configuration, {:index_names => ['users']}, stream + ) } + + it "removes each file for real-time indices that match :index_filter" do + expect(commander).to receive(:call).with( + :clear_real_time, + configuration, + {:index_names => ['users'], :indices => [users_index]}, + stream + ) + + interface.clear + end + end + end + + describe '#index' do + let(:plain_index) { double(:type => 'plain') } + let(:users_index) { double(name: 'users', :type => 'rt') } + let(:parts_index) { double(name: 'parts', :type => 'rt') } + + before :each do + allow(configuration).to receive_messages( + :indices => [plain_index, users_index, parts_index] + ) + end + + it 'invokes the index command with real-time indices' do + expect(commander).to receive(:call).with( + :index_real_time, + configuration, + {:indices => [users_index, parts_index]}, + stream + ) + + interface.index + end + + context "with options[:index_names]" do + let(:interface) { ThinkingSphinx::Interfaces::RealTime.new( + configuration, {:index_names => ['users']}, stream + ) } + + it 'invokes the index command for matching indices' do + expect(commander).to receive(:call).with( + :index_real_time, + configuration, + {:index_names => ['users'], :indices => [users_index]}, + stream + ) + + interface.index + end + end + end +end diff --git a/spec/thinking_sphinx/interfaces/sql_spec.rb b/spec/thinking_sphinx/interfaces/sql_spec.rb new file mode 100644 index 000000000..9a6293c07 --- /dev/null +++ b/spec/thinking_sphinx/interfaces/sql_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Interfaces::SQL do + let(:interface) { ThinkingSphinx::Interfaces::SQL.new( + configuration, {:verbose => true}, stream + ) } + let(:commander) { double :call => true } + let(:configuration) { double 'configuration', :preload_indices => true, + :render => true, :indices => [double(:index, :type => 'plain')] } + let(:stream) { double :puts => nil } + + before :each do + stub_const 'ThinkingSphinx::Commander', commander + end + + describe '#clear' do + let(:users_index) { double(:type => 'plain') } + let(:parts_index) { double(:type => 'plain') } + let(:rt_index) { double(:type => 'rt') } + + before :each do + allow(configuration).to receive(:indices). + and_return([users_index, parts_index, rt_index]) + end + + it "invokes the clear_sql command" do + expect(commander).to receive(:call).with( + :clear_sql, + configuration, + {:verbose => true, :indices => [users_index, parts_index]}, + stream + ) + + interface.clear + end + end + + describe '#index' do + it "invokes the prepare command" do + expect(commander).to receive(:call).with( + :prepare, configuration, {:verbose => true}, stream + ) + + interface.index + end + + it "renders the configuration to a file by default" do + expect(commander).to receive(:call).with( + :configure, configuration, {:verbose => true}, stream + ) + + interface.index + end + + it "does not render the configuration if requested" do + expect(commander).not_to receive(:call).with( + :configure, configuration, {:verbose => true}, stream + ) + + interface.index false + end + + it "executes the index command" do + expect(commander).to receive(:call).with( + :index_sql, configuration, {:verbose => true, :indices => nil}, stream + ) + + interface.index + end + + context "with options[:index_names]" do + let(:users_index) { double(:name => 'users', :type => 'plain') } + let(:parts_index) { double(:name => 'parts', :type => 'plain') } + let(:rt_index) { double(:type => 'rt') } + let(:interface) { ThinkingSphinx::Interfaces::SQL.new( + configuration, {:index_names => ['users']}, stream + ) } + + before :each do + allow(configuration).to receive(:indices). + and_return([users_index, parts_index, rt_index]) + end + + it 'invokes the index command for matching indices' do + expect(commander).to receive(:call).with( + :index_sql, + configuration, + {:index_names => ['users'], :indices => ['users']}, + stream + ) + + interface.index + end + end + end + + describe '#merge' do + it "invokes the merge command" do + expect(commander).to receive(:call).with( + :merge_and_update, configuration, {:verbose => true}, stream + ) + + interface.merge + end + + context "with options[:index_names]" do + let(:interface) { ThinkingSphinx::Interfaces::SQL.new( + configuration, {:index_names => ['users']}, stream + ) } + + it 'invokes the merge command with the index_names option' do + expect(commander).to receive(:call).with( + :merge_and_update, configuration, {:index_names => ['users']}, stream + ) + + interface.merge + end + end + end +end diff --git a/spec/thinking_sphinx/masks/pagination_mask_spec.rb b/spec/thinking_sphinx/masks/pagination_mask_spec.rb index 433cbe7f4..1c8ed3afc 100644 --- a/spec/thinking_sphinx/masks/pagination_mask_spec.rb +++ b/spec/thinking_sphinx/masks/pagination_mask_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Masks; end end @@ -12,13 +14,13 @@ module Masks; end describe '#first_page?' do it "returns true when on the first page" do - mask.should be_first_page + expect(mask).to be_first_page end it "returns false on other pages" do - search.stub :current_page => 2 + allow(search).to receive_messages :current_page => 2 - mask.should_not be_first_page + expect(mask).not_to be_first_page end end @@ -28,13 +30,13 @@ module Masks; end end it "is true when there's no more pages" do - search.stub :current_page => 3 + allow(search).to receive_messages :current_page => 3 - mask.should be_last_page + expect(mask).to be_last_page end it "is false when there's still more pages" do - mask.should_not be_last_page + expect(mask).not_to be_last_page end end @@ -44,13 +46,13 @@ module Masks; end end it "should return one more than the current page" do - mask.next_page.should == 2 + expect(mask.next_page).to eq(2) end it "should return nil if on the last page" do - search.stub :current_page => 3 + allow(search).to receive_messages :current_page => 3 - mask.next_page.should be_nil + expect(mask.next_page).to be_nil end end @@ -60,13 +62,13 @@ module Masks; end end it "is true when there is a second page" do - mask.next_page?.should be_true + expect(mask.next_page?).to be_truthy end it "is false when there's no more pages" do - search.stub :current_page => 3 + allow(search).to receive_messages :current_page => 3 - mask.next_page?.should be_false + expect(mask.next_page?).to be_falsey end end @@ -76,13 +78,13 @@ module Masks; end end it "should return one less than the current page" do - search.stub :current_page => 2 + allow(search).to receive_messages :current_page => 2 - mask.previous_page.should == 1 + expect(mask.previous_page).to eq(1) end it "should return nil if on the first page" do - mask.previous_page.should be_nil + expect(mask.previous_page).to be_nil end end @@ -92,7 +94,7 @@ module Masks; end end it "returns the total found from the search request metadata" do - mask.total_entries.should == 12 + expect(mask.total_entries).to eq(12) end end @@ -103,19 +105,19 @@ module Masks; end end it "uses the total available from the search request metadata" do - mask.total_pages.should == 2 + expect(mask.total_pages).to eq(2) end it "should allow for custom per_page values" do - search.stub :per_page => 40 + allow(search).to receive_messages :per_page => 40 - mask.total_pages.should == 1 + expect(mask.total_pages).to eq(1) end it "should return 0 if there is no index and therefore no results" do search.meta.clear - mask.total_pages.should == 0 + expect(mask.total_pages).to eq(0) end end end diff --git a/spec/thinking_sphinx/masks/scopes_mask_spec.rb b/spec/thinking_sphinx/masks/scopes_mask_spec.rb index 334b1d2be..7f4db502e 100644 --- a/spec/thinking_sphinx/masks/scopes_mask_spec.rb +++ b/spec/thinking_sphinx/masks/scopes_mask_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Masks; end end @@ -10,18 +12,18 @@ module Masks; end let(:mask) { ThinkingSphinx::Masks::ScopesMask.new search } before :each do - FileUtils.stub :mkdir_p => true + allow(FileUtils).to receive_messages :mkdir_p => true end describe '#search' do it "replaces the query if one is supplied" do - search.should_receive(:query=).with('bar') + expect(search).to receive(:query=).with('bar') mask.search('bar') end it "keeps the existing query when only options are offered" do - search.should_not_receive(:query=) + expect(search).not_to receive(:query=) mask.search :with => {:foo => :bar} end @@ -31,7 +33,7 @@ module Masks; end mask.search :conditions => {:baz => 'qux'} - search.options[:conditions].should == {:foo => 'bar', :baz => 'qux'} + expect(search.options[:conditions]).to eq({:foo => 'bar', :baz => 'qux'}) end it "merges filters" do @@ -39,7 +41,7 @@ module Masks; end mask.search :with => {:baz => :qux} - search.options[:with].should == {:foo => :bar, :baz => :qux} + expect(search.options[:with]).to eq({:foo => :bar, :baz => :qux}) end it "merges exclusive filters" do @@ -47,7 +49,7 @@ module Masks; end mask.search :without => {:baz => :qux} - search.options[:without].should == {:foo => :bar, :baz => :qux} + expect(search.options[:without]).to eq({:foo => :bar, :baz => :qux}) end it "appends excluded ids" do @@ -55,7 +57,7 @@ module Masks; end mask.search :without_ids => [5, 7] - search.options[:without_ids].should == [1, 3, 5, 7] + expect(search.options[:without_ids]).to eq([1, 3, 5, 7]) end it "replaces the retry_stale option" do @@ -63,23 +65,23 @@ module Masks; end mask.search :retry_stale => 6 - search.options[:retry_stale].should == 6 + expect(search.options[:retry_stale]).to eq(6) end it "returns the original search object" do - mask.search.object_id.should == search.object_id + expect(mask.search.object_id).to eq(search.object_id) end end describe '#search_for_ids' do it "replaces the query if one is supplied" do - search.should_receive(:query=).with('bar') + expect(search).to receive(:query=).with('bar') mask.search_for_ids('bar') end it "keeps the existing query when only options are offered" do - search.should_not_receive(:query=) + expect(search).not_to receive(:query=) mask.search_for_ids :with => {:foo => :bar} end @@ -89,7 +91,7 @@ module Masks; end mask.search_for_ids :conditions => {:baz => 'qux'} - search.options[:conditions].should == {:foo => 'bar', :baz => 'qux'} + expect(search.options[:conditions]).to eq({:foo => 'bar', :baz => 'qux'}) end it "merges filters" do @@ -97,7 +99,7 @@ module Masks; end mask.search_for_ids :with => {:baz => :qux} - search.options[:with].should == {:foo => :bar, :baz => :qux} + expect(search.options[:with]).to eq({:foo => :bar, :baz => :qux}) end it "merges exclusive filters" do @@ -105,7 +107,7 @@ module Masks; end mask.search_for_ids :without => {:baz => :qux} - search.options[:without].should == {:foo => :bar, :baz => :qux} + expect(search.options[:without]).to eq({:foo => :bar, :baz => :qux}) end it "appends excluded ids" do @@ -113,7 +115,7 @@ module Masks; end mask.search_for_ids :without_ids => [5, 7] - search.options[:without_ids].should == [1, 3, 5, 7] + expect(search.options[:without_ids]).to eq([1, 3, 5, 7]) end it "replaces the retry_stale option" do @@ -121,17 +123,17 @@ module Masks; end mask.search_for_ids :retry_stale => 6 - search.options[:retry_stale].should == 6 + expect(search.options[:retry_stale]).to eq(6) end it "adds the ids_only option" do mask.search_for_ids - search.options[:ids_only].should be_true + expect(search.options[:ids_only]).to be_truthy end it "returns the original search object" do - mask.search_for_ids.object_id.should == search.object_id + expect(mask.search_for_ids.object_id).to eq(search.object_id) end end end diff --git a/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb b/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb index 366a77de0..127a8cbb5 100644 --- a/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb +++ b/spec/thinking_sphinx/middlewares/active_record_translator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Middlewares; end class Search; end @@ -11,9 +13,10 @@ class Search; end let(:app) { double('app', :call => true) } let(:middleware) { ThinkingSphinx::Middlewares::ActiveRecordTranslator.new app } - let(:context) { {:raw => [], :results => []} } + let(:context) { {:raw => [], :results => [] } } let(:model) { double('model', :primary_key => :id) } let(:search) { double('search', :options => {}) } + let(:configuration) { double('configuration', :settings => {:primary_key => :id}) } def raw_result(id, model_name) {'sphinx_internal_id' => id, 'sphinx_internal_class' => model_name} @@ -21,20 +24,21 @@ def raw_result(id, model_name) describe '#call' do before :each do - context.stub :search => search - model.stub :unscoped => model + allow(context).to receive_messages :search => search + allow(context).to receive_messages :configuration => configuration + allow(model).to receive_messages :unscoped => model end it "translates records to ActiveRecord objects" do model_name = double('article', :constantize => model) instance = double('instance', :id => 24) - model.stub :where => [instance] + allow(model).to receive_messages :where => [instance] context[:results] << raw_result(24, model_name) middleware.call [context] - context[:results].should == [instance] + expect(context[:results]).to eq([instance]) end it "only queries the model once for the given search results" do @@ -44,7 +48,7 @@ def raw_result(id, model_name) context[:results] << raw_result(24, model_name) context[:results] << raw_result(42, model_name) - model.should_receive(:where).once.and_return([instance_a, instance_b]) + expect(model).to receive(:where).once.and_return([instance_a, instance_b]) middleware.call [context] end @@ -58,14 +62,14 @@ def raw_result(id, model_name) user_name = double('user name', :constantize => user_model) user = double('user instance', :id => 12) - article_model.stub :unscoped => article_model - user_model.stub :unscoped => user_model + allow(article_model).to receive_messages :unscoped => article_model + allow(user_model).to receive_messages :unscoped => user_model context[:results] << raw_result(24, article_name) context[:results] << raw_result(12, user_name) - article_model.should_receive(:where).once.and_return([article]) - user_model.should_receive(:where).once.and_return([user]) + expect(article_model).to receive(:where).once.and_return([article]) + expect(user_model).to receive(:where).once.and_return([user]) middleware.call [context] end @@ -78,11 +82,11 @@ def raw_result(id, model_name) context[:results] << raw_result(2, model_name) context[:results] << raw_result(1, model_name) - model.stub(:where => [instance_1, instance_2]) + allow(model).to receive_messages(:where => [instance_1, instance_2]) middleware.call [context] - context[:results].should == [instance_2, instance_1] + expect(context[:results]).to eq([instance_2, instance_1]) end it "returns objects in database order if a SQL order clause is supplied" do @@ -93,28 +97,40 @@ def raw_result(id, model_name) context[:results] << raw_result(2, model_name) context[:results] << raw_result(1, model_name) - model.stub(:order => model, :where => [instance_1, instance_2]) + allow(model).to receive_messages(:order => model, :where => [instance_1, instance_2]) search.options[:sql] = {:order => 'name DESC'} middleware.call [context] - context[:results].should == [instance_1, instance_2] + expect(context[:results]).to eq([instance_1, instance_2]) + end + + it "handles model without primary key" do + no_primary_key_model = double('no primary key model') + allow(no_primary_key_model).to receive_messages :unscoped => no_primary_key_model + model_name = double('article', :constantize => no_primary_key_model) + instance = double('instance', :id => 1) + allow(no_primary_key_model).to receive_messages :where => [instance] + + context[:results] << raw_result(1, model_name) + + middleware.call [context] end context 'SQL options' do let(:relation) { double('relation', :where => []) } + let(:model_name) { double('article', :constantize => model) } before :each do - model.stub :unscoped => relation + allow(model).to receive_messages :unscoped => relation - model_name = double('article', :constantize => model) context[:results] << raw_result(1, model_name) end it "passes through SQL include options to the relation" do search.options[:sql] = {:include => :association} - relation.should_receive(:includes).with(:association). + expect(relation).to receive(:includes).with(:association). and_return(relation) middleware.call [context] @@ -123,7 +139,7 @@ def raw_result(id, model_name) it "passes through SQL join options to the relation" do search.options[:sql] = {:joins => :association} - relation.should_receive(:joins).with(:association).and_return(relation) + expect(relation).to receive(:joins).with(:association).and_return(relation) middleware.call [context] end @@ -131,7 +147,7 @@ def raw_result(id, model_name) it "passes through SQL order options to the relation" do search.options[:sql] = {:order => 'name DESC'} - relation.should_receive(:order).with('name DESC').and_return(relation) + expect(relation).to receive(:order).with('name DESC').and_return(relation) middleware.call [context] end @@ -139,7 +155,7 @@ def raw_result(id, model_name) it "passes through SQL select options to the relation" do search.options[:sql] = {:select => :column} - relation.should_receive(:select).with(:column).and_return(relation) + expect(relation).to receive(:select).with(:column).and_return(relation) middleware.call [context] end @@ -147,7 +163,15 @@ def raw_result(id, model_name) it "passes through SQL group options to the relation" do search.options[:sql] = {:group => :column} - relation.should_receive(:group).with(:column).and_return(relation) + expect(relation).to receive(:group).with(:column).and_return(relation) + + middleware.call [context] + end + + it "passes through SQL options defined by model to the relation" do + search.options[:sql] = {model_name => {:joins => :association}} + + expect(relation).to receive(:joins).with(:association).and_return(relation) middleware.call [context] end diff --git a/spec/thinking_sphinx/middlewares/geographer_spec.rb b/spec/thinking_sphinx/middlewares/geographer_spec.rb index 78c5fc20a..d4bdd6b5e 100644 --- a/spec/thinking_sphinx/middlewares/geographer_spec.rb +++ b/spec/thinking_sphinx/middlewares/geographer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Middlewares; end end @@ -16,7 +18,7 @@ module Middlewares; end before :each do stub_const 'ThinkingSphinx::Panes::DistancePane', double - context.stub :search => search + allow(context).to receive_messages :search => search end describe '#call' do @@ -26,7 +28,7 @@ module Middlewares; end end it "doesn't add anything if :geo is nil" do - sphinx_sql.should_not_receive(:prepend_values) + expect(sphinx_sql).not_to receive(:prepend_values) middleware.call [context] end @@ -38,7 +40,7 @@ module Middlewares; end end it "adds the geodist function when given a :geo option" do - sphinx_sql.should_receive(:prepend_values). + expect(sphinx_sql).to receive(:prepend_values). with('GEODIST(0.1, 0.2, lat, lng) AS geodist'). and_return(sphinx_sql) @@ -46,18 +48,18 @@ module Middlewares; end end it "adds the distance pane" do - sphinx_sql.stub :prepend_values => sphinx_sql + allow(sphinx_sql).to receive_messages :prepend_values => sphinx_sql middleware.call [context] - context[:panes].should include(ThinkingSphinx::Panes::DistancePane) + expect(context[:panes]).to include(ThinkingSphinx::Panes::DistancePane) end it "respects :latitude_attr and :longitude_attr options" do search.options[:latitude_attr] = 'side_to_side' search.options[:longitude_attr] = 'up_or_down' - sphinx_sql.should_receive(:prepend_values). + expect(sphinx_sql).to receive(:prepend_values). with('GEODIST(0.1, 0.2, side_to_side, up_or_down) AS geodist'). and_return(sphinx_sql) @@ -68,7 +70,7 @@ module Middlewares; end context[:indices] << double('index', :unique_attribute_names => ['latitude'], :name => 'an_index') - sphinx_sql.should_receive(:prepend_values). + expect(sphinx_sql).to receive(:prepend_values). with('GEODIST(0.1, 0.2, latitude, lng) AS geodist'). and_return(sphinx_sql) @@ -79,7 +81,7 @@ module Middlewares; end context[:indices] << double('index', :unique_attribute_names => ['longitude'], :name => 'an_index') - sphinx_sql.should_receive(:prepend_values). + expect(sphinx_sql).to receive(:prepend_values). with('GEODIST(0.1, 0.2, lat, longitude) AS geodist'). and_return(sphinx_sql) @@ -89,7 +91,7 @@ module Middlewares; end it "handles very small values" do search.options[:geo] = [0.0000001, 0.00000000002] - sphinx_sql.should_receive(:prepend_values). + expect(sphinx_sql).to receive(:prepend_values). with('GEODIST(0.0000001, 0.00000000002, lat, lng) AS geodist'). and_return(sphinx_sql) diff --git a/spec/thinking_sphinx/middlewares/glazier_spec.rb b/spec/thinking_sphinx/middlewares/glazier_spec.rb index 9d7a1804d..f7c31a551 100644 --- a/spec/thinking_sphinx/middlewares/glazier_spec.rb +++ b/spec/thinking_sphinx/middlewares/glazier_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Middlewares; end end @@ -10,11 +12,12 @@ module Middlewares; end let(:middleware) { ThinkingSphinx::Middlewares::Glazier.new app } let(:context) { {:results => [result], :indices => [index], :meta => {}, :raw => [raw_result], :panes => []} } - let(:result) { double('result', :id => 10, - :class => double(:name => 'Article')) } - let(:index) { double('index', :name => 'foo_core') } - let(:search) { double('search', :options => {}) } - let(:glazed_result) { double('glazed result') } + let(:result) { double 'result', :id => 10, :class => model } + let(:model) { double 'model', :name => 'Article' } + let(:index) { double 'index', :name => 'foo_core', :model => model, + :primary_key => :id } + let(:search) { double 'search', :options => {} } + let(:glazed_result) { double 'glazed result' } let(:raw_result) { {'sphinx_internal_class' => 'Article', 'sphinx_internal_id' => 10} } @@ -22,7 +25,7 @@ module Middlewares; end before :each do stub_const 'ThinkingSphinx::Search::Glaze', double(:new => glazed_result) - context.stub :search => search + allow(context).to receive_messages :search => search end context 'No panes provided' do @@ -33,7 +36,7 @@ module Middlewares; end it "leaves the results as they are" do middleware.call [context] - context[:results].should == [result] + expect(context[:results]).to eq([result]) end end @@ -47,11 +50,11 @@ module Middlewares; end it "replaces each result with a glazed version" do middleware.call [context] - context[:results].should == [glazed_result] + expect(context[:results]).to eq([glazed_result]) end it "creates a glazed result for each result" do - ThinkingSphinx::Search::Glaze.should_receive(:new). + expect(ThinkingSphinx::Search::Glaze).to receive(:new). with(context, result, raw_result, [pane_class]). and_return(glazed_result) diff --git a/spec/thinking_sphinx/middlewares/inquirer_spec.rb b/spec/thinking_sphinx/middlewares/inquirer_spec.rb index 4867f0f33..5f54c040c 100644 --- a/spec/thinking_sphinx/middlewares/inquirer_spec.rb +++ b/spec/thinking_sphinx/middlewares/inquirer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Middlewares; end end @@ -16,7 +18,7 @@ module Middlewares; end before :each do batch_class = double - batch_class.stub(:new).and_return(batch_inquirer) + allow(batch_class).to receive(:new).and_return(batch_inquirer) stub_const 'Riddle::Query', double(:meta => 'SHOW META') stub_const 'ThinkingSphinx::Search::BatchInquirer', batch_class @@ -24,8 +26,8 @@ module Middlewares; end describe '#call' do it "passes through the SphinxQL from a Riddle::Query::Select object" do - batch_inquirer.should_receive(:append_query).with('SELECT * FROM index') - batch_inquirer.should_receive(:append_query).with('SHOW META') + expect(batch_inquirer).to receive(:append_query).with('SELECT * FROM index') + expect(batch_inquirer).to receive(:append_query).with('SHOW META') middleware.call [context] end @@ -33,19 +35,19 @@ module Middlewares; end it "sets up the raw results" do middleware.call [context] - context[:raw].should == [:raw] + expect(context[:raw]).to eq([:raw]) end it "sets up the meta results as a hash" do middleware.call [context] - context[:meta].should == {'meta' => 'value'} + expect(context[:meta]).to eq({'meta' => 'value'}) end it "uses the raw values as the initial results" do middleware.call [context] - context[:results].should == [:raw] + expect(context[:results]).to eq([:raw]) end context "with mysql2 result" do @@ -63,7 +65,7 @@ def each; [{"fake" => "value"}].each { |m| yield m }; end it "converts the results into an array" do middleware.call [context] - context[:results].should be_a Array + expect(context[:results]).to be_a Array end end end diff --git a/spec/thinking_sphinx/middlewares/sphinxql_spec.rb b/spec/thinking_sphinx/middlewares/sphinxql_spec.rb index 1c448c426..96f1d7605 100644 --- a/spec/thinking_sphinx/middlewares/sphinxql_spec.rb +++ b/spec/thinking_sphinx/middlewares/sphinxql_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Middlewares; end end @@ -16,7 +18,6 @@ class SphinxQLSubclass require 'thinking_sphinx/middlewares/middleware' require 'thinking_sphinx/middlewares/sphinxql' require 'thinking_sphinx/errors' -require 'thinking_sphinx/sphinxql' describe ThinkingSphinx::Middlewares::SphinxQL do let(:app) { double('app', :call => true) } @@ -30,13 +31,13 @@ class SphinxQLSubclass let(:query) { double('query') } let(:configuration) { double('configuration', :settings => {}, index_set_class: set_class) } - let(:set_class) { double(:new => index_set) } + let(:set_class) { double(:new => index_set, :reference_name => :article) } before :each do stub_const 'Riddle::Query::Select', double(:new => sphinx_sql) stub_const 'ThinkingSphinx::Search::Query', double(:new => query) - context.stub :search => search, :configuration => configuration + allow(context).to receive_messages :search => search, :configuration => configuration end describe '#call' do @@ -46,7 +47,7 @@ class SphinxQLSubclass double('index', :name => 'user_core', :options => {}) ] - sphinx_sql.should_receive(:from).with('`article_core`', '`user_core`'). + expect(sphinx_sql).to receive(:from).with('`article_core`', '`user_core`'). and_return(sphinx_sql) middleware.call [context] @@ -57,10 +58,10 @@ class SphinxQLSubclass :name => 'User') search.options[:classes] = [klass] search.options[:indices] = ['user_core'] - index_set.first.stub :reference => :user + allow(index_set.first).to receive_messages :reference => :user - set_class.should_receive(:new). - with(:classes => [klass], :indices => ['user_core']). + expect(set_class).to receive(:new). + with({ :classes => [klass], :indices => ['user_core'] }). and_return(index_set) middleware.call [context] @@ -75,19 +76,19 @@ class SphinxQLSubclass end it "generates a Sphinx query from the provided keyword and conditions" do - search.stub :query => 'tasty' + allow(search).to receive_messages :query => 'tasty' search.options[:conditions] = {:title => 'pancakes'} - ThinkingSphinx::Search::Query.should_receive(:new). + expect(ThinkingSphinx::Search::Query).to receive(:new). with('tasty', {:title => 'pancakes'}, anything).and_return(query) middleware.call [context] end it "matches on the generated query" do - query.stub :to_s => 'waffles' + allow(query).to receive_messages :to_s => 'waffles' - sphinx_sql.should_receive(:matching).with('waffles') + expect(sphinx_sql).to receive(:matching).with('waffles') middleware.call [context] end @@ -95,15 +96,15 @@ class SphinxQLSubclass it "requests a starred query if the :star option is set to true" do search.options[:star] = true - ThinkingSphinx::Search::Query.should_receive(:new). + expect(ThinkingSphinx::Search::Query).to receive(:new). with(anything, anything, true).and_return(query) middleware.call [context] end it "doesn't append a field condition by default" do - ThinkingSphinx::Search::Query.should_receive(:new) do |query, conditions, star| - conditions[:sphinx_internal_class_name].should be_nil + expect(ThinkingSphinx::Search::Query).to receive(:new) do |query, conditions, star| + expect(conditions[:sphinx_internal_class_name]).to be_nil query end @@ -113,12 +114,13 @@ class SphinxQLSubclass it "doesn't append a field condition if all classes match index references" do model = double('model', :connection => double, :ancestors => [ActiveRecord::Base], :name => 'Animal') - index_set.first.stub :reference => :animal + allow(index_set.first).to receive_messages :reference => :animal + allow(set_class).to receive_messages(:reference_name => :animal) search.options[:classes] = [model] - ThinkingSphinx::Search::Query.should_receive(:new) do |query, conditions, star| - conditions[:sphinx_internal_class_name].should be_nil + expect(ThinkingSphinx::Search::Query).to receive(:new) do |query, conditions, star| + expect(conditions[:sphinx_internal_class_name]).to be_nil query end @@ -132,19 +134,19 @@ class SphinxQLSubclass def self.name; 'Cat'; end def self.inheritance_column; 'type'; end end - supermodel.stub :connection => db_connection, :column_names => ['type'] + allow(supermodel).to receive_messages :connection => db_connection, :column_names => ['type'] submodel = Class.new(supermodel) do def self.name; 'Lion'; end def self.inheritance_column; 'type'; end def self.table_name; 'cats'; end end - submodel.stub :connection => db_connection, :column_names => ['type'], + allow(submodel).to receive_messages :connection => db_connection, :column_names => ['type'], :descendants => [] - index_set.first.stub :reference => :cat + allow(index_set.first).to receive_messages :reference => :cat search.options[:classes] = [submodel] - ThinkingSphinx::Search::Query.should_receive(:new).with(anything, + expect(ThinkingSphinx::Search::Query).to receive(:new).with(anything, hash_including(:sphinx_internal_class_name => '(Lion)'), anything). and_return(query) @@ -158,19 +160,19 @@ def self.table_name; 'cats'; end def self.name; 'Animals::Cat'; end def self.inheritance_column; 'type'; end end - supermodel.stub :connection => db_connection, :column_names => ['type'] + allow(supermodel).to receive_messages :connection => db_connection, :column_names => ['type'] submodel = Class.new(supermodel) do def self.name; 'Animals::Lion'; end def self.inheritance_column; 'type'; end def self.table_name; 'cats'; end end - submodel.stub :connection => db_connection, :column_names => ['type'], + allow(submodel).to receive_messages :connection => db_connection, :column_names => ['type'], :descendants => [] - index_set.first.stub :reference => :"animals/cat" + allow(index_set.first).to receive_messages :reference => :"animals/cat" search.options[:classes] = [submodel] - ThinkingSphinx::Search::Query.should_receive(:new).with(anything, + expect(ThinkingSphinx::Search::Query).to receive(:new).with(anything, hash_including(:sphinx_internal_class_name => '("Animals::Lion")'), anything). and_return(query) @@ -180,12 +182,12 @@ def self.table_name; 'cats'; end it "does not query the database for subclasses if :skip_sti is set to true" do model = double('model', :connection => double, :ancestors => [ActiveRecord::Base], :name => 'Animal') - index_set.first.stub :reference => :animal + allow(index_set.first).to receive_messages :reference => :animal search.options[:classes] = [model] search.options[:skip_sti] = true - model.connection.should_not_receive(:select_values) + expect(model.connection).not_to receive(:select_values) middleware.call [context] end @@ -197,15 +199,15 @@ def self.table_name; 'cats'; end def self.name; 'Cat'; end def self.inheritance_column; 'type'; end end - supermodel.stub :connection => db_connection, :column_names => ['type'] + allow(supermodel).to receive_messages :connection => db_connection, :column_names => ['type'] submodel = Class.new(supermodel) do def self.name; 'Lion'; end def self.inheritance_column; 'type'; end def self.table_name; 'cats'; end end - submodel.stub :connection => db_connection, :column_names => ['type'], + allow(submodel).to receive_messages :connection => db_connection, :column_names => ['type'], :descendants => [] - index_set.first.stub :reference => :cat + allow(index_set.first).to receive_messages :reference => :cat search.options[:classes] = [submodel] @@ -213,7 +215,7 @@ def self.table_name; 'cats'; end end it "filters out deleted values by default" do - sphinx_sql.should_receive(:where).with(:sphinx_deleted => false). + expect(sphinx_sql).to receive(:where).with({ :sphinx_deleted => false }). and_return(sphinx_sql) middleware.call [context] @@ -222,7 +224,7 @@ def self.table_name; 'cats'; end it "appends boolean attribute filters to the query" do search.options[:with] = {:visible => true} - sphinx_sql.should_receive(:where).with(hash_including(:visible => true)). + expect(sphinx_sql).to receive(:where).with(hash_including(:visible => true)). and_return(sphinx_sql) middleware.call [context] @@ -231,7 +233,7 @@ def self.table_name; 'cats'; end it "appends exclusive filters to the query" do search.options[:without] = {:tag_ids => [2, 4, 8]} - sphinx_sql.should_receive(:where_not). + expect(sphinx_sql).to receive(:where_not). with(hash_including(:tag_ids => [2, 4, 8])).and_return(sphinx_sql) middleware.call [context] @@ -240,7 +242,7 @@ def self.table_name; 'cats'; end it "appends the without_ids option as an exclusive filter" do search.options[:without_ids] = [1, 4, 9] - sphinx_sql.should_receive(:where_not). + expect(sphinx_sql).to receive(:where_not). with(hash_including(:sphinx_internal_id => [1, 4, 9])). and_return(sphinx_sql) @@ -250,8 +252,8 @@ def self.table_name; 'cats'; end it "appends MVA matches with all values" do search.options[:with_all] = {:tag_ids => [1, 7]} - sphinx_sql.should_receive(:where_all). - with(:tag_ids => [1, 7]).and_return(sphinx_sql) + expect(sphinx_sql).to receive(:where_all). + with({ :tag_ids => [1, 7] }).and_return(sphinx_sql) middleware.call [context] end @@ -259,8 +261,8 @@ def self.table_name; 'cats'; end it "appends MVA matches without all of the given values" do search.options[:without_all] = {:tag_ids => [1, 7]} - sphinx_sql.should_receive(:where_not_all). - with(:tag_ids => [1, 7]).and_return(sphinx_sql) + expect(sphinx_sql).to receive(:where_not_all). + with({ :tag_ids => [1, 7] }).and_return(sphinx_sql) middleware.call [context] end @@ -268,7 +270,7 @@ def self.table_name; 'cats'; end it "appends order clauses to the query" do search.options[:order] = 'created_at ASC' - sphinx_sql.should_receive(:order_by).with('created_at ASC'). + expect(sphinx_sql).to receive(:order_by).with('created_at ASC'). and_return(sphinx_sql) middleware.call [context] @@ -277,7 +279,7 @@ def self.table_name; 'cats'; end it "presumes attributes given as symbols should be sorted ascendingly" do search.options[:order] = :updated_at - sphinx_sql.should_receive(:order_by).with('updated_at ASC'). + expect(sphinx_sql).to receive(:order_by).with('updated_at ASC'). and_return(sphinx_sql) middleware.call [context] @@ -285,10 +287,10 @@ def self.table_name; 'cats'; end it "appends a group by clause to the query" do search.options[:group_by] = :foreign_id - search.stub :masks => [] - sphinx_sql.stub :values => sphinx_sql + allow(search).to receive_messages :masks => [] + allow(sphinx_sql).to receive_messages :values => sphinx_sql - sphinx_sql.should_receive(:group_by).with('foreign_id'). + expect(sphinx_sql).to receive(:group_by).with('foreign_id'). and_return(sphinx_sql) middleware.call [context] @@ -297,24 +299,24 @@ def self.table_name; 'cats'; end it "appends a sort within group clause to the query" do search.options[:order_group_by] = :title - sphinx_sql.should_receive(:order_within_group_by).with('title ASC'). + expect(sphinx_sql).to receive(:order_within_group_by).with('title ASC'). and_return(sphinx_sql) middleware.call [context] end it "uses the provided offset" do - search.stub :offset => 50 + allow(search).to receive_messages :offset => 50 - sphinx_sql.should_receive(:offset).with(50).and_return(sphinx_sql) + expect(sphinx_sql).to receive(:offset).with(50).and_return(sphinx_sql) middleware.call [context] end it "uses the provided limit" do - search.stub :per_page => 24 + allow(search).to receive_messages :per_page => 24 - sphinx_sql.should_receive(:limit).with(24).and_return(sphinx_sql) + expect(sphinx_sql).to receive(:limit).with(24).and_return(sphinx_sql) middleware.call [context] end @@ -322,7 +324,7 @@ def self.table_name; 'cats'; end it "adds the provided select statement" do search.options[:select] = 'foo as bar' - sphinx_sql.should_receive(:values).with('foo as bar'). + expect(sphinx_sql).to receive(:values).with('foo as bar'). and_return(sphinx_sql) middleware.call [context] @@ -331,7 +333,7 @@ def self.table_name; 'cats'; end it "adds the provided group-best count" do search.options[:group_best] = 5 - sphinx_sql.should_receive(:group_best).with(5).and_return(sphinx_sql) + expect(sphinx_sql).to receive(:group_best).with(5).and_return(sphinx_sql) middleware.call [context] end @@ -339,7 +341,7 @@ def self.table_name; 'cats'; end it "adds the provided having clause" do search.options[:having] = 'foo > 1' - sphinx_sql.should_receive(:having).with('foo > 1').and_return(sphinx_sql) + expect(sphinx_sql).to receive(:having).with('foo > 1').and_return(sphinx_sql) middleware.call [context] end @@ -347,8 +349,8 @@ def self.table_name; 'cats'; end it "uses any provided field weights" do search.options[:field_weights] = {:title => 3} - sphinx_sql.should_receive(:with_options) do |options| - options[:field_weights].should == {:title => 3} + expect(sphinx_sql).to receive(:with_options) do |options| + expect(options[:field_weights]).to eq({:title => 3}) sphinx_sql end @@ -358,7 +360,7 @@ def self.table_name; 'cats'; end it "uses index-defined field weights if they're available" do index_set.first.options[:field_weights] = {:title => 3} - sphinx_sql.should_receive(:with_options).with( + expect(sphinx_sql).to receive(:with_options).with( hash_including(:field_weights => {:title => 3}) ).and_return(sphinx_sql) @@ -368,7 +370,7 @@ def self.table_name; 'cats'; end it "uses index-defined max matches if it's available" do index_set.first.options[:max_matches] = 100 - sphinx_sql.should_receive(:with_options).with( + expect(sphinx_sql).to receive(:with_options).with( hash_including(:max_matches => 100) ).and_return(sphinx_sql) @@ -378,7 +380,7 @@ def self.table_name; 'cats'; end it "uses configuration-level max matches if set" do configuration.settings['max_matches'] = 120 - sphinx_sql.should_receive(:with_options).with( + expect(sphinx_sql).to receive(:with_options).with( hash_including(:max_matches => 120) ).and_return(sphinx_sql) @@ -388,8 +390,8 @@ def self.table_name; 'cats'; end it "uses any given ranker option" do search.options[:ranker] = 'proximity' - sphinx_sql.should_receive(:with_options) do |options| - options[:ranker].should == 'proximity' + expect(sphinx_sql).to receive(:with_options) do |options| + expect(options[:ranker]).to eq('proximity') sphinx_sql end diff --git a/spec/thinking_sphinx/middlewares/stale_id_checker_spec.rb b/spec/thinking_sphinx/middlewares/stale_id_checker_spec.rb index 8a9768afa..726cefbad 100644 --- a/spec/thinking_sphinx/middlewares/stale_id_checker_spec.rb +++ b/spec/thinking_sphinx/middlewares/stale_id_checker_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Middlewares; end class Search; end @@ -25,7 +27,7 @@ def raw_result(id, model_name) context[:results] << double('instance', :id => 24) context[:results] << double('instance', :id => 42) - app.should_receive(:call) + expect(app).to receive(:call) middleware.call [context] end @@ -37,10 +39,11 @@ def raw_result(id, model_name) context[:results] << double('instance', :id => 24) context[:results] << nil - lambda { + expect { middleware.call [context] - }.should raise_error(ThinkingSphinx::Search::StaleIdsException) { |err| - err.ids.should == [42] + }.to raise_error(ThinkingSphinx::Search::StaleIdsException) { |err| + expect(err.ids).to eq([42]) + expect(err.context).to eq(context) } end end diff --git a/spec/thinking_sphinx/middlewares/stale_id_filter_spec.rb b/spec/thinking_sphinx/middlewares/stale_id_filter_spec.rb index c272ba015..8ac6db2af 100644 --- a/spec/thinking_sphinx/middlewares/stale_id_filter_spec.rb +++ b/spec/thinking_sphinx/middlewares/stale_id_filter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Middlewares; end class Search; end @@ -15,22 +17,22 @@ class Search; end describe '#call' do before :each do - context.stub :search => search + allow(context).to receive_messages :search => search end context 'one stale ids exception' do before :each do - app.stub(:call) do + allow(app).to receive(:call) do @calls ||= 0 @calls += 1 - raise ThinkingSphinx::Search::StaleIdsException, [12] if @calls == 1 + raise ThinkingSphinx::Search::StaleIdsException.new([12], context) if @calls == 1 end end it "appends the ids to the without_ids filter" do middleware.call [context] - search.options[:without_ids].should == [12] + expect(search.options[:without_ids]).to eq([12]) end it "respects existing without_ids filters" do @@ -38,24 +40,24 @@ class Search; end middleware.call [context] - search.options[:without_ids].should == [11, 12] + expect(search.options[:without_ids]).to eq([11, 12]) end end context 'two stale ids exceptions' do before :each do - app.stub(:call) do + allow(app).to receive(:call) do @calls ||= 0 @calls += 1 - raise ThinkingSphinx::Search::StaleIdsException, [12] if @calls == 1 - raise ThinkingSphinx::Search::StaleIdsException, [13] if @calls == 2 + raise ThinkingSphinx::Search::StaleIdsException.new([12], context) if @calls == 1 + raise ThinkingSphinx::Search::StaleIdsException.new([13], context) if @calls == 2 end end it "appends the ids to the without_ids filter" do middleware.call [context] - search.options[:without_ids].should == [12, 13] + expect(search.options[:without_ids]).to eq([12, 13]) end it "respects existing without_ids filters" do @@ -63,29 +65,49 @@ class Search; end middleware.call [context] - search.options[:without_ids].should == [11, 12, 13] + expect(search.options[:without_ids]).to eq([11, 12, 13]) end end context 'three stale ids exceptions' do before :each do - app.stub(:call) do + allow(app).to receive(:call) do @calls ||= 0 @calls += 1 - raise ThinkingSphinx::Search::StaleIdsException, [12] if @calls == 1 - raise ThinkingSphinx::Search::StaleIdsException, [13] if @calls == 2 - raise ThinkingSphinx::Search::StaleIdsException, [14] if @calls == 3 + raise ThinkingSphinx::Search::StaleIdsException.new([12], context) if @calls == 1 + raise ThinkingSphinx::Search::StaleIdsException.new([13], context) if @calls == 2 + raise ThinkingSphinx::Search::StaleIdsException.new([14], context) if @calls == 3 end end it "raises the final stale ids exceptions" do - lambda { + expect { middleware.call [context] - }.should raise_error(ThinkingSphinx::Search::StaleIdsException) { |err| - err.ids.should == [14] + }.to raise_error(ThinkingSphinx::Search::StaleIdsException) { |err| + expect(err.ids).to eq([14]) } end end + + context 'stale ids exceptions with multiple contexts' do + let(:context2) { {:raw => [], :results => []} } + let(:search2) { double('search2', :options => {}) } + before :each do + allow(context2).to receive_messages :search => search2 + allow(app).to receive(:call) do + @calls ||= 0 + @calls += 1 + raise ThinkingSphinx::Search::StaleIdsException.new([12], context2) if @calls == 1 + end + end + + it "appends the ids to the without_ids filter in the correct context" do + middleware.call [context, context2] + expect(search.options[:without_ids]).to eq(nil) + expect(search2.options[:without_ids]).to eq([12]) + end + end + end end diff --git a/spec/thinking_sphinx/middlewares/valid_options_spec.rb b/spec/thinking_sphinx/middlewares/valid_options_spec.rb new file mode 100644 index 000000000..52930405b --- /dev/null +++ b/spec/thinking_sphinx/middlewares/valid_options_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::Middlewares::ValidOptions do + let(:app) { double 'app', :call => true } + let(:middleware) { ThinkingSphinx::Middlewares::ValidOptions.new app } + let(:context) { double 'context', :search => search } + let(:search) { double 'search', :options => {} } + + before :each do + allow(ThinkingSphinx::Logger).to receive(:log) + end + + context 'with unknown options' do + before :each do + search.options[:foo] = :bar + end + + it "adds a warning" do + expect(ThinkingSphinx::Logger).to receive(:log). + with(:caution, "Unexpected search options: [:foo]") + + middleware.call [context] + end + + it 'continues on' do + expect(app).to receive(:call).with([context]) + + middleware.call [context] + end + end + + context "with known options" do + before :each do + search.options[:ids_only] = true + end + + it "is silent" do + expect(ThinkingSphinx::Logger).to_not receive(:log) + + middleware.call [context] + end + + it 'continues on' do + expect(app).to receive(:call).with([context]) + + middleware.call [context] + end + end +end diff --git a/spec/thinking_sphinx/panes/attributes_pane_spec.rb b/spec/thinking_sphinx/panes/attributes_pane_spec.rb index 9a7c00cd6..a52273d79 100644 --- a/spec/thinking_sphinx/panes/attributes_pane_spec.rb +++ b/spec/thinking_sphinx/panes/attributes_pane_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Panes; end end @@ -15,7 +17,7 @@ module Panes; end it "returns the object's sphinx attributes by default" do raw['foo'] = 24 - pane.sphinx_attributes.should == {'foo' => 24} + expect(pane.sphinx_attributes).to eq({'foo' => 24}) end end end diff --git a/spec/thinking_sphinx/panes/distance_pane_spec.rb b/spec/thinking_sphinx/panes/distance_pane_spec.rb index 0f35e791b..6ea31caf7 100644 --- a/spec/thinking_sphinx/panes/distance_pane_spec.rb +++ b/spec/thinking_sphinx/panes/distance_pane_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Panes; end end @@ -15,13 +17,13 @@ module Panes; end it "returns the object's geodistance attribute by default" do raw['geodist'] = 123.45 - pane.distance.should == 123.45 + expect(pane.distance).to eq(123.45) end it "converts string geodistances to floats" do raw['geodist'] = '123.450' - pane.distance.should == 123.45 + expect(pane.distance).to eq(123.45) end end @@ -29,13 +31,13 @@ module Panes; end it "returns the object's geodistance attribute by default" do raw['geodist'] = 123.45 - pane.geodist.should == 123.45 + expect(pane.geodist).to eq(123.45) end it "converts string geodistances to floats" do raw['geodist'] = '123.450' - pane.geodist.should == 123.45 + expect(pane.geodist).to eq(123.45) end end end diff --git a/spec/thinking_sphinx/panes/excerpts_pane_spec.rb b/spec/thinking_sphinx/panes/excerpts_pane_spec.rb index 9425bde8e..3adf50b89 100644 --- a/spec/thinking_sphinx/panes/excerpts_pane_spec.rb +++ b/spec/thinking_sphinx/panes/excerpts_pane_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Panes; end end @@ -13,7 +15,7 @@ module Panes; end let(:search) { double('search', :query => 'foo', :options => {}) } before :each do - context.stub :search => search + allow(context).to receive_messages :search => search end describe '#excerpts' do @@ -22,18 +24,18 @@ module Panes; end before :each do stub_const 'ThinkingSphinx::Excerpter', double(:new => excerpter) - ThinkingSphinx::Panes::ExcerptsPane::Excerpts.stub :new => excerpts + allow(ThinkingSphinx::Panes::ExcerptsPane::Excerpts).to receive_messages :new => excerpts end it "returns an excerpt glazing" do - pane.excerpts.should == excerpts + expect(pane.excerpts).to eq(excerpts) end it "creates an excerpter with the first index and the query and conditions values" do context[:indices] = [double(:name => 'alpha'), double(:name => 'beta')] context.search.options[:conditions] = {:baz => 'bar'} - ThinkingSphinx::Excerpter.should_receive(:new). + expect(ThinkingSphinx::Excerpter).to receive(:new). with('alpha', 'foo bar', anything).and_return(excerpter) pane.excerpts @@ -42,8 +44,8 @@ module Panes; end it "passes through excerpts options" do search.options[:excerpts] = {:before_match => 'foo'} - ThinkingSphinx::Excerpter.should_receive(:new). - with(anything, anything, :before_match => 'foo').and_return(excerpter) + expect(ThinkingSphinx::Excerpter).to receive(:new). + with(anything, anything, { :before_match => 'foo' }).and_return(excerpter) pane.excerpts end diff --git a/spec/thinking_sphinx/panes/weight_pane_spec.rb b/spec/thinking_sphinx/panes/weight_pane_spec.rb index ca1f19ebb..42bb682d5 100644 --- a/spec/thinking_sphinx/panes/weight_pane_spec.rb +++ b/spec/thinking_sphinx/panes/weight_pane_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx module Panes; end end @@ -12,9 +14,9 @@ module Panes; end describe '#weight' do it "returns the object's weight by default" do - raw[ThinkingSphinx::SphinxQL.weight[:column]] = 101 + raw["weight()"] = 101 - pane.weight.should == 101 + expect(pane.weight).to eq(101) end end end diff --git a/spec/thinking_sphinx/rake_interface_spec.rb b/spec/thinking_sphinx/rake_interface_spec.rb index cde504840..0e04a01f6 100644 --- a/spec/thinking_sphinx/rake_interface_spec.rb +++ b/spec/thinking_sphinx/rake_interface_spec.rb @@ -1,257 +1,39 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::RakeInterface do - let(:configuration) { double('configuration', :controller => controller) } - let(:interface) { ThinkingSphinx::RakeInterface.new } + let(:interface) { ThinkingSphinx::RakeInterface.new } + let(:commander) { double :call => nil } before :each do - ThinkingSphinx::Configuration.stub :instance => configuration - interface.stub(:puts => nil) - end - - describe '#clear_all' do - let(:controller) { double 'controller' } - - before :each do - configuration.stub( - :indices_location => '/path/to/indices', - :searchd => double(:binlog_path => '/path/to/binlog') - ) - - FileUtils.stub :rm_r => true - File.stub :exists? => true - end - - it "removes the directory for the index files" do - FileUtils.should_receive(:rm_r).with('/path/to/indices') - - interface.clear_all - end - - it "removes the directory for the binlog files" do - FileUtils.should_receive(:rm_r).with('/path/to/binlog') - - interface.clear_all - end - end - - describe '#clear_real_time' do - let(:controller) { double 'controller' } - let(:index) { - double(:type => 'rt', :render => true, :path => '/path/to/my/index') - } - - before :each do - configuration.stub( - :indices => [double(:type => 'plain'), index], - :searchd => double(:binlog_path => '/path/to/binlog') - ) - - Dir.stub :[] => ['foo.a', 'foo.b'] - FileUtils.stub :rm_r => true, :rm => true - File.stub :exists? => true - end - - it 'finds each file for real-time indices' do - Dir.should_receive(:[]).with('/path/to/my/index.*').and_return([]) - - interface.clear_real_time - end - - it "removes each file for real-time indices" do - FileUtils.should_receive(:rm).with('foo.a') - FileUtils.should_receive(:rm).with('foo.b') - - interface.clear_real_time - end - - it "removes the directory for the binlog files" do - FileUtils.should_receive(:rm_r).with('/path/to/binlog') - - interface.clear_real_time - end + stub_const 'ThinkingSphinx::Commander', commander end describe '#configure' do - let(:controller) { double('controller') } - - before :each do - configuration.stub( - :configuration_file => '/path/to/foo.conf', - :render_to_file => true - ) - end - - it "renders the configuration to a file" do - configuration.should_receive(:render_to_file) - - interface.configure - end - - it "prints a message stating the file is being generated" do - interface.should_receive(:puts). - with('Generating configuration to /path/to/foo.conf') + it 'sends the configure command' do + expect(commander).to receive(:call). + with(:configure, anything, {:verbose => true}) interface.configure end end - describe '#index' do - let(:controller) { double('controller', :index => true) } - - before :each do - ThinkingSphinx.stub :before_index_hooks => [] - configuration.stub( - :configuration_file => '/path/to/foo.conf', - :render_to_file => true, - :indices_location => '/path/to/indices' - ) - - FileUtils.stub :mkdir_p => true - end - - it "renders the configuration to a file by default" do - configuration.should_receive(:render_to_file) - - interface.index - end - - it "does not render the configuration if requested" do - configuration.should_not_receive(:render_to_file) - - interface.index false - end - - it "creates the directory for the index files" do - FileUtils.should_receive(:mkdir_p).with('/path/to/indices') - - interface.index - end - - it "calls all registered hooks" do - called = false - ThinkingSphinx.before_index_hooks << Proc.new { called = true } - - interface.index - - called.should be_true - end - - it "indexes all indices verbosely" do - controller.should_receive(:index).with(:verbose => true) - - interface.index - end - - it "does not index verbosely if requested" do - controller.should_receive(:index).with(:verbose => false) - - interface.index true, false - end - end - - describe '#start' do - let(:controller) { double('controller', :start => true, :pid => 101) } - - before :each do - controller.stub(:running?).and_return(false, true) - configuration.stub :indices_location => 'my/index/files' - - FileUtils.stub :mkdir_p => true - end - - it "creates the index files directory" do - FileUtils.should_receive(:mkdir_p).with('my/index/files') - - interface.start - end - - it "starts the daemon" do - controller.should_receive(:start) - - interface.start - end - - it "raises an error if the daemon is already running" do - controller.stub :running? => true - - lambda { - interface.start - }.should raise_error(RuntimeError) - end - - it "prints a success message if the daemon has started" do - controller.stub(:running?).and_return(false, true) - - interface.should_receive(:puts). - with('Started searchd successfully (pid: 101).') - - interface.start - end - - it "prints a failure message if the daemon does not start" do - controller.stub(:running?).and_return(false, false) - - interface.should_receive(:puts). - with('Failed to start searchd. Check the log files for more information.') - - interface.start + describe '#daemon' do + it 'returns a daemon interface' do + expect(interface.daemon.class).to eq(ThinkingSphinx::Interfaces::Daemon) end end - describe '#stop' do - let(:controller) { double('controller', :stop => true, :pid => 101) } - - before :each do - controller.stub :running? => true - end - - it "prints a message if the daemon is not already running" do - controller.stub :running? => false - - interface.should_receive(:puts).with('searchd is not currently running.') - - interface.stop - end - - it "stops the daemon" do - controller.should_receive(:stop) - - interface.stop - end - - it "prints a message informing the daemon has stopped" do - interface.should_receive(:puts).with('Stopped searchd daemon (pid: 101).') - - interface.stop - end - - it "should retry stopping the daemon until it stops" do - controller.should_receive(:stop).twice.and_return(false, true) - - interface.stop + describe '#rt' do + it 'returns a real-time interface' do + expect(interface.rt.class).to eq(ThinkingSphinx::Interfaces::RealTime) end end - describe '#status' do - let(:controller) { double('controller') } - - it "reports when the daemon is running" do - controller.stub :running? => true - - interface.should_receive(:puts). - with('The Sphinx daemon searchd is currently running.') - - interface.status - end - - it "reports when the daemon is not running" do - controller.stub :running? => false - - interface.should_receive(:puts). - with('The Sphinx daemon searchd is not currently running.') - - interface.status + describe '#sql' do + it 'returns an SQL interface' do + expect(interface.sql.class).to eq(ThinkingSphinx::Interfaces::SQL) end end end diff --git a/spec/thinking_sphinx/real_time/attribute_spec.rb b/spec/thinking_sphinx/real_time/attribute_spec.rb index b8dd057a5..43cef1857 100644 --- a/spec/thinking_sphinx/real_time/attribute_spec.rb +++ b/spec/thinking_sphinx/real_time/attribute_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::RealTime::Attribute do @@ -7,11 +9,11 @@ describe '#name' do it "uses the provided option by default" do attribute = ThinkingSphinx::RealTime::Attribute.new column, :as => :foo - attribute.name.should == 'foo' + expect(attribute.name).to eq('foo') end it "falls back to the column's name" do - attribute.name.should == 'created_at' + expect(attribute.name).to eq('created_at') end end @@ -21,34 +23,34 @@ let(:parent) { klass.new 'the parent name', nil } it "returns the column's name if it's a string" do - column.stub :__name => 'value' + allow(column).to receive_messages :__name => 'value' - attribute.translate(object).should == 'value' + expect(attribute.translate(object)).to eq('value') end it "returns the column's name if it's an integer" do - column.stub :__name => 404 + allow(column).to receive_messages :__name => 404 - attribute.translate(object).should == 404 + expect(attribute.translate(object)).to eq(404) end it "returns the object's method matching the column's name" do - object.stub :created_at => 'a time' + allow(object).to receive_messages :created_at => 'a time' - attribute.translate(object).should == 'a time' + expect(attribute.translate(object)).to eq('a time') end it "uses the column's stack to navigate through the object tree" do - column.stub :__name => :name, :__stack => [:parent] + allow(column).to receive_messages :__name => :name, :__stack => [:parent] - attribute.translate(object).should == 'the parent name' + expect(attribute.translate(object)).to eq('the parent name') end it "returns zero if any element in the object tree is nil" do - column.stub :__name => :name, :__stack => [:parent] + allow(column).to receive_messages :__name => :name, :__stack => [:parent] object.parent = nil - attribute.translate(object).should be_zero + expect(attribute.translate(object)).to be_zero end end @@ -56,7 +58,7 @@ it "returns the given type option" do attribute = ThinkingSphinx::RealTime::Attribute.new column, :type => :string - attribute.type.should == :string + expect(attribute.type).to eq(:string) end end end diff --git a/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb b/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb index 71d35f5e7..c0be290e3 100644 --- a/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb +++ b/spec/thinking_sphinx/real_time/callbacks/real_time_callbacks_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks do @@ -9,15 +11,15 @@ :settings => {}) } let(:index) { double('index', :name => 'my_index', :is_a? => true, :document_id_for_key => 123, :fields => [], :attributes => [], - :conditions => []) } + :conditions => [], :primary_key => :id) } let(:connection) { double('connection', :execute => true) } before :each do - ThinkingSphinx::Configuration.stub :instance => config - ThinkingSphinx::Connection.stub_chain(:pool, :take).and_yield connection + allow(ThinkingSphinx::Configuration).to receive_messages :instance => config + allow(ThinkingSphinx::Connection).to receive_message_chain(:pool, :take).and_yield connection end - describe '#after_save' do + describe '#after_save, #after_commit' do let(:insert) { double('insert', :to_sql => 'REPLACE INTO my_index') } let(:time) { 1.day.ago } let(:field) { double('field', :name => 'name', :translate => 'Foo') } @@ -25,32 +27,52 @@ :translate => time) } before :each do - ThinkingSphinx::Configuration.stub :instance => config - Riddle::Query::Insert.stub :new => insert - insert.stub :replace! => insert - index.stub :fields => [field], :attributes => [attribute] + allow(ThinkingSphinx::Configuration).to receive_messages :instance => config + allow(Riddle::Query::Insert).to receive_messages :new => insert + allow(insert).to receive_messages :replace! => insert + allow(index).to receive_messages :fields => [field], :attributes => [attribute] end it "creates an insert statement with all fields and attributes" do - Riddle::Query::Insert.should_receive(:new). - with('my_index', ['id', 'name', 'created_at'], [123, 'Foo', time]). + expect(Riddle::Query::Insert).to receive(:new). + with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_save instance end it "switches the insert to a replace statement" do - insert.should_receive(:replace!).and_return(insert) + expect(insert).to receive(:replace!).and_return(insert) callbacks.after_save instance end it "sends the insert through to the server" do - connection.should_receive(:execute).with('REPLACE INTO my_index') + expect(connection).to receive(:execute).with('REPLACE INTO my_index') callbacks.after_save instance end + it "creates an insert statement with all fields and attributes" do + expect(Riddle::Query::Insert).to receive(:new). + with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). + and_return(insert) + + callbacks.after_commit instance + end + + it "switches the insert to a replace statement" do + expect(insert).to receive(:replace!).and_return(insert) + + callbacks.after_commit instance + end + + it "sends the insert through to the server" do + expect(connection).to receive(:execute).with('REPLACE INTO my_index') + + callbacks.after_commit instance + end + context 'with a given path' do let(:callbacks) { ThinkingSphinx::RealTime::Callbacks::RealTimeCallbacks.new( @@ -61,24 +83,44 @@ let(:user) { double('user', :id => 13, :persisted? => true) } it "creates an insert statement with all fields and attributes" do - Riddle::Query::Insert.should_receive(:new). - with('my_index', ['id', 'name', 'created_at'], [123, 'Foo', time]). + expect(Riddle::Query::Insert).to receive(:new). + with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_save instance end it "gets the document id for the user object" do - index.should_receive(:document_id_for_key).with(13).and_return(123) + expect(index).to receive(:document_id_for_key).with(13).and_return(123) callbacks.after_save instance end it "translates values for the user object" do - field.should_receive(:translate).with(user).and_return('Foo') + expect(field).to receive(:translate).with(user).and_return('Foo') callbacks.after_save instance end + + it "creates an insert statement with all fields and attributes" do + expect(Riddle::Query::Insert).to receive(:new). + with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). + and_return(insert) + + callbacks.after_commit instance + end + + it "gets the document id for the user object" do + expect(index).to receive(:document_id_for_key).with(13).and_return(123) + + callbacks.after_commit instance + end + + it "translates values for the user object" do + expect(field).to receive(:translate).with(user).and_return('Foo') + + callbacks.after_commit instance + end end context 'with a path returning multiple objects' do @@ -93,26 +135,48 @@ let(:user_b) { double('user', :id => 14, :persisted? => true) } it "creates insert statements with all fields and attributes" do - Riddle::Query::Insert.should_receive(:new).twice. - with('my_index', ['id', 'name', 'created_at'], [123, 'Foo', time]). + expect(Riddle::Query::Insert).to receive(:new).twice. + with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_save instance end it "gets the document id for each reader" do - index.should_receive(:document_id_for_key).with(13).and_return(123) - index.should_receive(:document_id_for_key).with(14).and_return(123) + expect(index).to receive(:document_id_for_key).with(13).and_return(123) + expect(index).to receive(:document_id_for_key).with(14).and_return(123) callbacks.after_save instance end it "translates values for each reader" do - field.should_receive(:translate).with(user_a).and_return('Foo') - field.should_receive(:translate).with(user_b).and_return('Foo') + expect(field).to receive(:translate).with(user_a).and_return('Foo') + expect(field).to receive(:translate).with(user_b).and_return('Foo') callbacks.after_save instance end + + it "creates insert statements with all fields and attributes" do + expect(Riddle::Query::Insert).to receive(:new).twice. + with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). + and_return(insert) + + callbacks.after_commit instance + end + + it "gets the document id for each reader" do + expect(index).to receive(:document_id_for_key).with(13).and_return(123) + expect(index).to receive(:document_id_for_key).with(14).and_return(123) + + callbacks.after_commit instance + end + + it "translates values for each reader" do + expect(field).to receive(:translate).with(user_a).and_return('Foo') + expect(field).to receive(:translate).with(user_b).and_return('Foo') + + callbacks.after_commit instance + end end context 'with a block instead of a path' do @@ -127,26 +191,48 @@ let(:user_b) { double('user', :id => 14, :persisted? => true) } it "creates insert statements with all fields and attributes" do - Riddle::Query::Insert.should_receive(:new).twice. - with('my_index', ['id', 'name', 'created_at'], [123, 'Foo', time]). + expect(Riddle::Query::Insert).to receive(:new).twice. + with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). and_return(insert) callbacks.after_save instance end it "gets the document id for each reader" do - index.should_receive(:document_id_for_key).with(13).and_return(123) - index.should_receive(:document_id_for_key).with(14).and_return(123) + expect(index).to receive(:document_id_for_key).with(13).and_return(123) + expect(index).to receive(:document_id_for_key).with(14).and_return(123) callbacks.after_save instance end it "translates values for each reader" do - field.should_receive(:translate).with(user_a).and_return('Foo') - field.should_receive(:translate).with(user_b).and_return('Foo') + expect(field).to receive(:translate).with(user_a).and_return('Foo') + expect(field).to receive(:translate).with(user_b).and_return('Foo') callbacks.after_save instance end + + it "creates insert statements with all fields and attributes" do + expect(Riddle::Query::Insert).to receive(:new).twice. + with('my_index', ['id', 'name', 'created_at'], [[123, 'Foo', time]]). + and_return(insert) + + callbacks.after_commit instance + end + + it "gets the document id for each reader" do + expect(index).to receive(:document_id_for_key).with(13).and_return(123) + expect(index).to receive(:document_id_for_key).with(14).and_return(123) + + callbacks.after_commit instance + end + + it "translates values for each reader" do + expect(field).to receive(:translate).with(user_a).and_return('Foo') + expect(field).to receive(:translate).with(user_b).and_return('Foo') + + callbacks.after_commit instance + end end end end diff --git a/spec/thinking_sphinx/real_time/field_spec.rb b/spec/thinking_sphinx/real_time/field_spec.rb index 6989dd2a1..4adbe0b0b 100644 --- a/spec/thinking_sphinx/real_time/field_spec.rb +++ b/spec/thinking_sphinx/real_time/field_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::RealTime::Field do @@ -6,11 +8,11 @@ describe '#column' do it 'returns the provided Column object' do - field.column.should == column + expect(field.column).to eq(column) end it 'translates symbols to Column objects' do - ThinkingSphinx::ActiveRecord::Column.should_receive(:new).with(:title). + expect(ThinkingSphinx::ActiveRecord::Column).to receive(:new).with(:title). and_return(column) ThinkingSphinx::RealTime::Field.new :title @@ -20,11 +22,11 @@ describe '#name' do it "uses the provided option by default" do field = ThinkingSphinx::RealTime::Field.new column, :as => :foo - field.name.should == 'foo' + expect(field.name).to eq('foo') end it "falls back to the column's name" do - field.name.should == 'created_at' + expect(field.name).to eq('created_at') end end @@ -34,34 +36,34 @@ let(:parent) { klass.new 'the parent name', nil } it "returns the column's name if it's a string" do - column.stub :__name => 'value' + allow(column).to receive_messages :__name => 'value' - field.translate(object).should == 'value' + expect(field.translate(object)).to eq('value') end it "returns the column's name as a string if it's an integer" do - column.stub :__name => 404 + allow(column).to receive_messages :__name => 404 - field.translate(object).should == '404' + expect(field.translate(object)).to eq('404') end it "returns the object's method matching the column's name" do - object.stub :created_at => 'a time' + allow(object).to receive_messages :created_at => 'a time' - field.translate(object).should == 'a time' + expect(field.translate(object)).to eq('a time') end it "uses the column's stack to navigate through the object tree" do - column.stub :__name => :name, :__stack => [:parent] + allow(column).to receive_messages :__name => :name, :__stack => [:parent] - field.translate(object).should == 'the parent name' + expect(field.translate(object)).to eq('the parent name') end it "returns a blank string if any element in the object tree is nil" do - column.stub :__name => :name, :__stack => [:parent] + allow(column).to receive_messages :__name => :name, :__stack => [:parent] object.parent = nil - field.translate(object).should == '' + expect(field.translate(object)).to eq('') end end end diff --git a/spec/thinking_sphinx/real_time/index_spec.rb b/spec/thinking_sphinx/real_time/index_spec.rb index 54eb2c573..06e1e152d 100644 --- a/spec/thinking_sphinx/real_time/index_spec.rb +++ b/spec/thinking_sphinx/real_time/index_spec.rb @@ -1,47 +1,98 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::RealTime::Index do let(:index) { ThinkingSphinx::RealTime::Index.new :user } - let(:indices_path) { double('indices path', :join => '') } let(:config) { double('config', :settings => {}, - :indices_location => indices_path, :next_offset => 8) } + :indices_location => 'location', :next_offset => 8, + :index_set_class => index_set_class) } + let(:index_set_class) { double(:index_set_class, :reference_name => :user) } before :each do - ThinkingSphinx::Configuration.stub :instance => config + allow(ThinkingSphinx::Configuration).to receive_messages :instance => config + end + + describe '#add_attribute' do + let(:attribute) { double('attribute', name: 'my_attribute') } + + it "appends attributes to the collection" do + index.add_attribute attribute + + expect(index.attributes.collect(&:name)).to include('my_attribute') + end + + it "replaces attributes with the same name" do + index.add_attribute double('attribute', name: 'my_attribute') + index.add_attribute attribute + + matching = index.attributes.select { |attr| attr.name == attribute.name } + + expect(matching).to eq([attribute]) + end + end + + describe '#add_field' do + let(:field) { double('field', name: 'my_field') } + + it "appends fields to the collection" do + index.add_field field + + expect(index.fields.collect(&:name)).to include('my_field') + end + + it "replaces fields with the same name" do + index.add_field double('field', name: 'my_field') + index.add_field field + + matching = index.fields.select { |fld| fld.name == field.name } + + expect(matching).to eq([field]) + end end describe '#attributes' do it "has the internal id attribute by default" do - index.attributes.collect(&:name).should include('sphinx_internal_id') + expect(index.attributes.collect(&:name)).to include('sphinx_internal_id') end it "has the class name attribute by default" do - index.attributes.collect(&:name).should include('sphinx_internal_class') + expect(index.attributes.collect(&:name)).to include('sphinx_internal_class') end it "has the internal deleted attribute by default" do - index.attributes.collect(&:name).should include('sphinx_deleted') + expect(index.attributes.collect(&:name)).to include('sphinx_deleted') + end + + it "does not have an internal updated_at attribute by default" do + expect(index.attributes.collect(&:name)).to_not include('sphinx_updated_at') + end + + it "has an internal updated_at attribute if real_time_tidy is true" do + config.settings["real_time_tidy"] = true + + expect(index.attributes.collect(&:name)).to include('sphinx_updated_at') end end describe '#delta?' do it "always returns false" do - index.should_not be_delta + expect(index).not_to be_delta end end describe '#document_id_for_key' do it "calculates the document id based on offset and number of indices" do - config.stub_chain(:indices, :count).and_return(5) - config.stub :next_offset => 7 + allow(config).to receive_message_chain(:indices, :count).and_return(5) + allow(config).to receive_messages :next_offset => 7 - index.document_id_for_key(123).should == 622 + expect(index.document_id_for_key(123)).to eq(622) end end describe '#fields' do it "has the internal class field by default" do - index.fields.collect(&:name).should include('sphinx_internal_class_name') + expect(index.fields.collect(&:name)).to include('sphinx_internal_class_name') end end @@ -53,14 +104,14 @@ end it "interprets the definition block" do - ThinkingSphinx::RealTime::Interpreter.should_receive(:translate!). + expect(ThinkingSphinx::RealTime::Interpreter).to receive(:translate!). with(index, block) index.interpret_definition! end it "only interprets the definition block once" do - ThinkingSphinx::RealTime::Interpreter.should_receive(:translate!). + expect(ThinkingSphinx::RealTime::Interpreter).to receive(:translate!). once index.interpret_definition! @@ -69,16 +120,16 @@ end describe '#model' do - let(:model) { double('model') } + let(:model) { double('model', :primary_key => :id) } it "translates symbol references to model class" do - ActiveSupport::Inflector.stub(:constantize => model) + allow(ActiveSupport::Inflector).to receive_messages(:constantize => model) - index.model.should == model + expect(index.model).to eq(model) end it "memoizes the result" do - ActiveSupport::Inflector.should_receive(:constantize).with('User').once. + expect(ActiveSupport::Inflector).to receive(:constantize).with('User').once. and_return(model) index.model @@ -88,7 +139,7 @@ describe '#morphology' do before :each do - pending + skip end context 'with a render' do @@ -98,7 +149,7 @@ rescue Riddle::Configuration::ConfigurationError end - index.morphology.should be_nil + expect(index.morphology).to be_nil end it "reads from the settings file if provided" do @@ -109,7 +160,7 @@ rescue Riddle::Configuration::ConfigurationError end - index.morphology.should == 'stem_en' + expect(index.morphology).to eq('stem_en') end end end @@ -117,21 +168,21 @@ describe '#name' do it "always uses the core suffix" do index = ThinkingSphinx::RealTime::Index.new :user - index.name.should == 'user_core' + expect(index.name).to eq('user_core') end end describe '#offset' do before :each do - config.stub :next_offset => 4 + allow(config).to receive_messages :next_offset => 4 end it "uses the next offset value from the configuration" do - index.offset.should == 4 + expect(index.offset).to eq(4) end it "uses the reference to get a unique offset" do - config.should_receive(:next_offset).with(:user).and_return(2) + expect(config).to receive(:next_offset).with(:user).and_return(2) index.offset end @@ -139,11 +190,11 @@ describe '#render' do before :each do - FileUtils.stub :mkdir_p => true + allow(FileUtils).to receive_messages :mkdir_p => true end it "interprets the provided definition" do - index.should_receive(:interpret_definition!).at_least(:once) + expect(index).to receive(:interpret_definition!).at_least(:once) begin index.render @@ -154,26 +205,26 @@ end describe '#scope' do - let(:model) { double('model') } + let(:model) { double('model', :primary_key => :id) } it "returns the model by default" do - ActiveSupport::Inflector.stub(:constantize => model) + allow(ActiveSupport::Inflector).to receive_messages(:constantize => model) - index.scope.should == model + expect(index.scope).to eq(model) end it "returns the evaluated scope if provided" do index.scope = lambda { :foo } - index.scope.should == :foo + expect(index.scope).to eq(:foo) end end describe '#unique_attribute_names' do it "returns all attribute names" do - index.unique_attribute_names.should == [ + expect(index.unique_attribute_names).to eq([ 'sphinx_internal_id', 'sphinx_internal_class', 'sphinx_deleted' - ] + ]) end end end diff --git a/spec/thinking_sphinx/real_time/interpreter_spec.rb b/spec/thinking_sphinx/real_time/interpreter_spec.rb index 1655e05e2..eb6f789dd 100644 --- a/spec/thinking_sphinx/real_time/interpreter_spec.rb +++ b/spec/thinking_sphinx/real_time/interpreter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::RealTime::Interpreter do @@ -8,19 +10,23 @@ let(:index) { Struct.new(:attributes, :fields, :options).new([], [], {}) } let(:block) { Proc.new { } } + before :each do + allow(index).to receive_messages(:add_attribute => nil, :add_field => nil) + end + describe '.translate!' do let(:instance) { double('interpreter', :translate! => true) } it "creates a new interpreter instance with the given block and index" do - ThinkingSphinx::RealTime::Interpreter.should_receive(:new). + expect(ThinkingSphinx::RealTime::Interpreter).to receive(:new). with(index, block).and_return(instance) ThinkingSphinx::RealTime::Interpreter.translate! index, block end it "calls translate! on the instance" do - ThinkingSphinx::RealTime::Interpreter.stub!(:new => instance) - instance.should_receive(:translate!) + allow(ThinkingSphinx::RealTime::Interpreter).to receive_messages(:new => instance) + expect(instance).to receive(:translate!) ThinkingSphinx::RealTime::Interpreter.translate! index, block end @@ -31,35 +37,33 @@ let(:attribute) { double('attribute') } before :each do - ThinkingSphinx::RealTime::Attribute.stub! :new => attribute + allow(ThinkingSphinx::RealTime::Attribute).to receive_messages :new => attribute end it "creates a new attribute with the provided column" do - ThinkingSphinx::RealTime::Attribute.should_receive(:new). + expect(ThinkingSphinx::RealTime::Attribute).to receive(:new). with(column, {}).and_return(attribute) instance.has column end it "passes through options to the attribute" do - ThinkingSphinx::RealTime::Attribute.should_receive(:new). - with(column, :as => :other_name).and_return(attribute) + expect(ThinkingSphinx::RealTime::Attribute).to receive(:new). + with(column, { :as => :other_name }).and_return(attribute) instance.has column, :as => :other_name end it "adds an attribute to the index" do - instance.has column + expect(index).to receive(:add_attribute).with(attribute) - index.attributes.should include(attribute) + instance.has column end it "adds multiple attributes when passed multiple columns" do - instance.has column, column + expect(index).to receive(:add_attribute).with(attribute).twice - index.attributes.select { |saved_attribute| - saved_attribute == attribute - }.length.should == 2 + instance.has column, column end end @@ -68,74 +72,72 @@ let(:field) { double('field') } before :each do - ThinkingSphinx::RealTime::Field.stub! :new => field + allow(ThinkingSphinx::RealTime::Field).to receive_messages :new => field end it "creates a new field with the provided column" do - ThinkingSphinx::RealTime::Field.should_receive(:new). + expect(ThinkingSphinx::RealTime::Field).to receive(:new). with(column, {}).and_return(field) instance.indexes column end it "passes through options to the field" do - ThinkingSphinx::RealTime::Field.should_receive(:new). - with(column, :as => :other_name).and_return(field) + expect(ThinkingSphinx::RealTime::Field).to receive(:new). + with(column, { :as => :other_name }).and_return(field) instance.indexes column, :as => :other_name end it "adds a field to the index" do - instance.indexes column + expect(index).to receive(:add_field).with(field) - index.fields.should include(field) + instance.indexes column end it "adds multiple fields when passed multiple columns" do - instance.indexes column, column + expect(index).to receive(:add_field).with(field).twice - index.fields.select { |saved_field| - saved_field == field - }.length.should == 2 + instance.indexes column, column end context 'sortable' do let(:attribute) { double('attribute') } before :each do - ThinkingSphinx::RealTime::Attribute.stub! :new => attribute + allow(ThinkingSphinx::RealTime::Attribute).to receive_messages :new => attribute - column.stub :__name => :col + allow(column).to receive_messages :__name => :col end it "adds the _sort suffix to the field's name" do - ThinkingSphinx::RealTime::Attribute.should_receive(:new). - with(column, :as => :col_sort, :type => :string). + expect(ThinkingSphinx::RealTime::Attribute).to receive(:new). + with(column, { :as => :col_sort, :type => :string }). and_return(attribute) instance.indexes column, :sortable => true end it "respects given aliases" do - ThinkingSphinx::RealTime::Attribute.should_receive(:new). - with(column, :as => :other_sort, :type => :string). + expect(ThinkingSphinx::RealTime::Attribute).to receive(:new). + with(column, { :as => :other_sort, :type => :string }). and_return(attribute) instance.indexes column, :sortable => true, :as => :other end it "respects symbols instead of columns" do - ThinkingSphinx::RealTime::Attribute.should_receive(:new). - with(:title, :as => :title_sort, :type => :string). + expect(ThinkingSphinx::RealTime::Attribute).to receive(:new). + with(:title, { :as => :title_sort, :type => :string }). and_return(attribute) instance.indexes :title, :sortable => true end it "adds an attribute to the index" do - instance.indexes column, :sortable => true + expect(index).to receive(:add_attribute).with(attribute) - index.attributes.should include(attribute) + instance.indexes column, :sortable => true end end end @@ -144,15 +146,15 @@ let(:column) { double('column') } before :each do - ThinkingSphinx::ActiveRecord::Column.stub!(:new => column) + allow(ThinkingSphinx::ActiveRecord::Column).to receive_messages(:new => column) end it "returns a new column for the given method" do - instance.id.should == column + expect(instance.id).to eq(column) end it "should initialise the column with the method name and arguments" do - ThinkingSphinx::ActiveRecord::Column.should_receive(:new). + expect(ThinkingSphinx::ActiveRecord::Column).to receive(:new). with(:users, :posts, :subject).and_return(column) instance.users(:posts, :subject) @@ -161,7 +163,7 @@ describe '#scope' do it "passes the scope block through to the index" do - index.should_receive(:scope=).with(instance_of(Proc)) + expect(index).to receive(:scope=).with(instance_of(Proc)) instance.scope { :foo } end @@ -169,18 +171,18 @@ describe '#set_property' do before :each do - index.class.stub :settings => [:morphology] + allow(index.class).to receive_messages :settings => [:morphology] end it 'saves other settings as index options' do instance.set_property :field_weights => {:name => 10} - index.options[:field_weights].should == {:name => 10} + expect(index.options[:field_weights]).to eq({:name => 10}) end context 'index settings' do it "sets the provided setting" do - index.should_receive(:morphology=).with('stem_en') + expect(index).to receive(:morphology=).with('stem_en') instance.set_property :morphology => 'stem_en' end @@ -194,8 +196,8 @@ } interpreter = ThinkingSphinx::RealTime::Interpreter.new index, block - interpreter.translate!. - should == interpreter.__id__ + expect(interpreter.translate!). + to eq(interpreter.__id__) end end end diff --git a/spec/thinking_sphinx/real_time/transcribe_instance_spec.rb b/spec/thinking_sphinx/real_time/transcribe_instance_spec.rb new file mode 100644 index 000000000..4351f45fd --- /dev/null +++ b/spec/thinking_sphinx/real_time/transcribe_instance_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::RealTime::TranscribeInstance do + let(:subject) do + ThinkingSphinx::RealTime::TranscribeInstance.call( + instance, index, [property_a, property_b, property_c] + ) + end + let(:instance) { double :id => 43 } + let(:index) { double :document_id_for_key => 46, :primary_key => :id } + let(:property_a) { double :translate => 'A' } + let(:property_b) { double :translate => 'B' } + let(:property_c) { double :translate => 'C' } + + it 'returns an array of each translated property, and the document id' do + expect(subject).to eq([46, 'A', 'B', 'C']) + end + + it 'raises an error if something goes wrong' do + allow(property_b).to receive(:translate).and_raise(StandardError) + + expect { subject }.to raise_error(ThinkingSphinx::TranscriptionError) + end + + it 'notes the instance and property in the wrapper error' do + allow(property_b).to receive(:translate).and_raise(StandardError) + + expect { subject }.to raise_error do |wrapper| + expect(wrapper.instance).to eq(instance) + expect(wrapper.property).to eq(property_b) + end + end +end diff --git a/spec/thinking_sphinx/real_time/transcriber_spec.rb b/spec/thinking_sphinx/real_time/transcriber_spec.rb new file mode 100644 index 000000000..e18a08114 --- /dev/null +++ b/spec/thinking_sphinx/real_time/transcriber_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ThinkingSphinx::RealTime::Transcriber do + let(:subject) { ThinkingSphinx::RealTime::Transcriber.new index } + let(:index) { double 'index', :name => 'foo_core', :conditions => [], + :fields => [double(:name => 'field_a'), double(:name => 'field_b')], + :attributes => [double(:name => 'attr_a'), double(:name => 'attr_b')], + :primary_key => :id } + let(:insert) { double :replace! => replace } + let(:replace) { double :to_sql => 'REPLACE QUERY' } + let(:connection) { double :execute => true } + let(:instance_a) { double :id => 48, :persisted? => true } + let(:instance_b) { double :id => 49, :persisted? => true } + let(:properties_a) { double } + let(:properties_b) { double } + + before :each do + allow(Riddle::Query::Insert).to receive(:new).and_return(insert) + allow(ThinkingSphinx::Connection).to receive(:take).and_yield(connection) + allow(ThinkingSphinx::RealTime::TranscribeInstance).to receive(:call). + with(instance_a, index, anything).and_return(properties_a) + allow(ThinkingSphinx::RealTime::TranscribeInstance).to receive(:call). + with(instance_b, index, anything).and_return(properties_b) + end + + it "generates a SphinxQL command" do + expect(Riddle::Query::Insert).to receive(:new).with( + 'foo_core', + ['id', 'field_a', 'field_b', 'attr_a', 'attr_b'], + [properties_a, properties_b] + ) + + subject.copy instance_a, instance_b + end + + it "executes the SphinxQL command" do + expect(connection).to receive(:execute).with('REPLACE QUERY') + + subject.copy instance_a, instance_b + end + + it "deletes previous records" do + expect(connection).to receive(:execute). + with('DELETE FROM foo_core WHERE sphinx_internal_id IN (48, 49)') + + subject.copy instance_a, instance_b + end + + it "skips instances that aren't in the database" do + allow(instance_a).to receive(:persisted?).and_return(false) + + expect(Riddle::Query::Insert).to receive(:new).with( + 'foo_core', + ['id', 'field_a', 'field_b', 'attr_a', 'attr_b'], + [properties_b] + ) + + subject.copy instance_a, instance_b + end + + it "skips instances that fail a symbol condition" do + index.conditions << :ok? + allow(instance_a).to receive(:ok?).and_return(true) + allow(instance_b).to receive(:ok?).and_return(false) + + expect(Riddle::Query::Insert).to receive(:new).with( + 'foo_core', + ['id', 'field_a', 'field_b', 'attr_a', 'attr_b'], + [properties_a] + ) + + subject.copy instance_a, instance_b + end + + it "skips instances that fail a Proc condition" do + index.conditions << Proc.new { |instance| instance.ok? } + allow(instance_a).to receive(:ok?).and_return(true) + allow(instance_b).to receive(:ok?).and_return(false) + + expect(Riddle::Query::Insert).to receive(:new).with( + 'foo_core', + ['id', 'field_a', 'field_b', 'attr_a', 'attr_b'], + [properties_a] + ) + + subject.copy instance_a, instance_b + end + + it "skips instances that throw an error while transcribing values" do + error = ThinkingSphinx::TranscriptionError.new + error.instance = instance_a + error.inner_exception = StandardError.new + + allow(ThinkingSphinx::RealTime::TranscribeInstance).to receive(:call). + with(instance_a, index, anything). + and_raise(error) + allow(ThinkingSphinx.output).to receive(:puts).and_return(nil) + + expect(Riddle::Query::Insert).to receive(:new).with( + 'foo_core', + ['id', 'field_a', 'field_b', 'attr_a', 'attr_b'], + [properties_b] + ) + + subject.copy instance_a, instance_b + end +end diff --git a/spec/thinking_sphinx/real_time/translator_spec.rb b/spec/thinking_sphinx/real_time/translator_spec.rb new file mode 100644 index 000000000..4ab7710df --- /dev/null +++ b/spec/thinking_sphinx/real_time/translator_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ThinkingSphinx::RealTime::Translator do + let(:subject) { ThinkingSphinx::RealTime::Translator.call object, column } + let(:object) { double } + let(:column) { double :__stack => [], :__name => :title } + + it "converts non-UTF-8 strings to UTF-8" do + allow(object).to receive(:title). + and_return "hello".dup.force_encoding("ASCII-8BIT") + + expect(subject).to eq("hello") + expect(subject.encoding.name).to eq("UTF-8") + end +end diff --git a/spec/thinking_sphinx/scopes_spec.rb b/spec/thinking_sphinx/scopes_spec.rb index 301f66bab..a2bd8ec62 100644 --- a/spec/thinking_sphinx/scopes_spec.rb +++ b/spec/thinking_sphinx/scopes_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::Scopes do @@ -16,30 +18,34 @@ def self.search(query = nil, options = {}) model.sphinx_scopes[:foo] = Proc.new { {:with => {:foo => :bar}} } end + it "implements respond_to" do + expect(model).to respond_to(:foo) + end + it "creates new search" do - model.foo.class.should == ThinkingSphinx::Search + expect(model.foo.class).to eq(ThinkingSphinx::Search) end it "passes block result to constructor" do - model.foo.options[:with].should == {:foo => :bar} + expect(model.foo.options[:with]).to eq({:foo => :bar}) end it "passes non-scopes through to the standard method error call" do - lambda { model.bar }.should raise_error(NoMethodError) + expect { model.bar }.to raise_error(NoMethodError) end end describe '#sphinx_scope' do it "saves the given block with a name" do model.sphinx_scope(:foo) { 27 } - model.sphinx_scopes[:foo].call.should == 27 + expect(model.sphinx_scopes[:foo].call).to eq(27) end end describe '#default_sphinx_scope' do it "gets and sets the default scope depending on the argument" do model.default_sphinx_scope :foo - model.default_sphinx_scope.should == :foo + expect(model.default_sphinx_scope).to eq(:foo) end end end diff --git a/spec/thinking_sphinx/search/glaze_spec.rb b/spec/thinking_sphinx/search/glaze_spec.rb index 43a8844f6..06734195e 100644 --- a/spec/thinking_sphinx/search/glaze_spec.rb +++ b/spec/thinking_sphinx/search/glaze_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx class Search; end end @@ -12,11 +14,11 @@ class Search; end describe '#!=' do it "is true for objects that don't match" do - (glaze != double('foo')).should be_true + expect(glaze != double('foo')).to be_truthy end it "is false when the underlying object is a match" do - (glaze != object).should be_false + expect(glaze != object).to be_falsey end end @@ -28,50 +30,50 @@ class Search; end let(:pane_two) { double('pane two', :foo => 'two', :bar => 'two') } before :each do - klass.stub(:new).and_return(pane_one, pane_two) + allow(klass).to receive(:new).and_return(pane_one, pane_two) end it "respects objects existing methods" do - object.stub :foo => 'original' + allow(object).to receive_messages :foo => 'original' - glaze.foo.should == 'original' + expect(glaze.foo).to eq('original') end it "uses the first pane that responds to the method" do - glaze.foo.should == 'one' - glaze.bar.should == 'two' + expect(glaze.foo).to eq('one') + expect(glaze.bar).to eq('two') end it "raises the method missing error otherwise" do - object.stub :respond_to? => false - object.stub(:baz).and_raise(NoMethodError) + allow(object).to receive_messages :respond_to? => false + allow(object).to receive(:baz).and_raise(NoMethodError) - lambda { glaze.baz }.should raise_error(NoMethodError) + expect { glaze.baz }.to raise_error(NoMethodError) end end describe '#respond_to?' do it "responds to underlying object methods" do - object.stub :foo => true + allow(object).to receive_messages :foo => true - glaze.respond_to?(:foo).should be_true + expect(glaze.respond_to?(:foo)).to be_truthy end it "responds to underlying pane methods" do pane = double('Pane Class', :new => double('pane', :bar => true)) glaze = ThinkingSphinx::Search::Glaze.new context, object, raw, [pane] - glaze.respond_to?(:bar).should be_true + expect(glaze.respond_to?(:bar)).to be_truthy end it "does not to respond to methods that don't exist" do - glaze.respond_to?(:something).should be_false + expect(glaze.respond_to?(:something)).to be_falsey end end describe '#unglazed' do it "returns the original object" do - glaze.unglazed.should == object + expect(glaze.unglazed).to eq(object) end end end diff --git a/spec/thinking_sphinx/search/query_spec.rb b/spec/thinking_sphinx/search/query_spec.rb index 28b8cc58e..55514f37a 100644 --- a/spec/thinking_sphinx/search/query_spec.rb +++ b/spec/thinking_sphinx/search/query_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ThinkingSphinx class Search; end end @@ -14,30 +16,30 @@ class Search; end it "passes through the keyword as provided" do query = ThinkingSphinx::Search::Query.new 'pancakes' - query.to_s.should == 'pancakes' + expect(query.to_s).to eq('pancakes') end it "pairs fields and keywords for given conditions" do query = ThinkingSphinx::Search::Query.new '', :title => 'pancakes' - query.to_s.should == '@title pancakes' + expect(query.to_s).to eq('@title pancakes') end it "combines both keywords and conditions" do query = ThinkingSphinx::Search::Query.new 'tasty', :title => 'pancakes' - query.to_s.should == 'tasty @title pancakes' + expect(query.to_s).to eq('tasty @title pancakes') end it "automatically stars keywords if requested" do - ThinkingSphinx::Query.should_receive(:wildcard).with('cake', true). + expect(ThinkingSphinx::Query).to receive(:wildcard).with('cake', true). and_return('*cake*') ThinkingSphinx::Search::Query.new('cake', {}, true).to_s end it "automatically stars condition keywords if requested" do - ThinkingSphinx::Query.should_receive(:wildcard).with('pan', true). + expect(ThinkingSphinx::Query).to receive(:wildcard).with('pan', true). and_return('*pan*') ThinkingSphinx::Search::Query.new('', {:title => 'pan'}, true).to_s @@ -47,32 +49,39 @@ class Search; end query = ThinkingSphinx::Search::Query.new '', {:sphinx_internal_class_name => 'article'}, true - query.to_s.should == '@sphinx_internal_class_name article' + expect(query.to_s).to eq('@sphinx_internal_class_name article') end it "handles null values by removing them from the conditions hash" do query = ThinkingSphinx::Search::Query.new '', :title => nil - query.to_s.should == '' + expect(query.to_s).to eq('') end it "handles empty string values by removing them from the conditions hash" do query = ThinkingSphinx::Search::Query.new '', :title => '' - query.to_s.should == '' + expect(query.to_s).to eq('') end it "handles nil queries" do query = ThinkingSphinx::Search::Query.new nil, {} - query.to_s.should == '' + expect(query.to_s).to eq('') end it "allows mixing of blank and non-blank conditions" do query = ThinkingSphinx::Search::Query.new 'tasty', :title => 'pancakes', :ingredients => nil - query.to_s.should == 'tasty @title pancakes' + expect(query.to_s).to eq('tasty @title pancakes') + end + + it "handles multiple fields for a single condition" do + query = ThinkingSphinx::Search::Query.new '', + [:title, :content] => 'pancakes' + + expect(query.to_s).to eq('@(title,content) pancakes') end end end diff --git a/spec/thinking_sphinx/search_spec.rb b/spec/thinking_sphinx/search_spec.rb index c1721e53f..a5224e35c 100644 --- a/spec/thinking_sphinx/search_spec.rb +++ b/spec/thinking_sphinx/search_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx::Search do @@ -34,26 +36,26 @@ end before :each do - ThinkingSphinx::Search::Context.stub :new => context + allow(ThinkingSphinx::Search::Context).to receive_messages :new => context stub_const 'ThinkingSphinx::Middlewares::DEFAULT', stack end describe '#current_page' do it "should return 1 by default" do - search.current_page.should == 1 + expect(search.current_page).to eq(1) end it "should handle string page values" do - ThinkingSphinx::Search.new(:page => '2').current_page.should == 2 + expect(ThinkingSphinx::Search.new(:page => '2').current_page).to eq(2) end it "should handle empty string page values" do - ThinkingSphinx::Search.new(:page => '').current_page.should == 1 + expect(ThinkingSphinx::Search.new(:page => '').current_page).to eq(1) end it "should return the requested page" do - ThinkingSphinx::Search.new(:page => 10).current_page.should == 10 + expect(ThinkingSphinx::Search.new(:page => 10).current_page).to eq(10) end end @@ -61,25 +63,25 @@ it "returns false if there is anything in the data set" do context[:results] << double - search.should_not be_empty + expect(search).not_to be_empty end it "returns true if the data set is empty" do context[:results].clear - search.should be_empty + expect(search).to be_empty end end describe '#initialize' do it "lazily loads by default" do - stack.should_not_receive(:call) + expect(stack).not_to receive(:call) ThinkingSphinx::Search.new end it "should automatically populate when :populate is set to true" do - stack.should_receive(:call).and_return(true) + expect(stack).to receive(:call).and_return(true) ThinkingSphinx::Search.new(:populate => true) end @@ -87,74 +89,74 @@ describe '#offset' do it "should default to 0" do - search.offset.should == 0 + expect(search.offset).to eq(0) end it "should increase by the per_page value for each page in" do - ThinkingSphinx::Search.new(:per_page => 25, :page => 2).offset. - should == 25 + expect(ThinkingSphinx::Search.new(:per_page => 25, :page => 2).offset). + to eq(25) end it "should prioritise explicit :offset over calculated if given" do - ThinkingSphinx::Search.new(:offset => 5).offset.should == 5 + expect(ThinkingSphinx::Search.new(:offset => 5).offset).to eq(5) end end describe '#page' do it "sets the current page" do search.page(3) - search.current_page.should == 3 + expect(search.current_page).to eq(3) end it "returns the search object" do - search.page(2).should == search + expect(search.page(2)).to eq(search) end end describe '#per' do it "sets the current per_page value" do search.per(29) - search.per_page.should == 29 + expect(search.per_page).to eq(29) end it "returns the search object" do - search.per(29).should == search + expect(search.per(29)).to eq(search) end end describe '#per_page' do it "defaults to 20" do - search.per_page.should == 20 + expect(search.per_page).to eq(20) end it "is set as part of the search options" do - ThinkingSphinx::Search.new(:per_page => 10).per_page.should == 10 + expect(ThinkingSphinx::Search.new(:per_page => 10).per_page).to eq(10) end it "should prioritise :limit over :per_page if given" do - ThinkingSphinx::Search.new(:per_page => 30, :limit => 40).per_page. - should == 40 + expect(ThinkingSphinx::Search.new(:per_page => 30, :limit => 40).per_page). + to eq(40) end it "should allow for string arguments" do - ThinkingSphinx::Search.new(:per_page => '10').per_page.should == 10 + expect(ThinkingSphinx::Search.new(:per_page => '10').per_page).to eq(10) end it "allows setting of the per_page value" do search.per_page(24) - search.per_page.should == 24 + expect(search.per_page).to eq(24) end end describe '#populate' do it "runs the middleware" do - stack.should_receive(:call).with([context]).and_return(true) + expect(stack).to receive(:call).with([context]).and_return(true) search.populate end it "does not retrieve results twice" do - stack.should_receive(:call).with([context]).once.and_return(true) + expect(stack).to receive(:call).with([context]).once.and_return(true) search.populate search.populate @@ -163,11 +165,11 @@ describe '#respond_to?' do it "should respond to Array methods" do - search.respond_to?(:each).should be_true + expect(search.respond_to?(:each)).to be_truthy end it "should respond to Search methods" do - search.respond_to?(:per_page).should be_true + expect(search.respond_to?(:per_page)).to be_truthy end it "should return true for methods delegated to pagination mask by method_missing" do @@ -196,7 +198,7 @@ context[:results] << glazed - search.to_a.first.__id__.should == unglazed.__id__ + expect(search.to_a.first.__id__).to eq(unglazed.__id__) end end diff --git a/spec/thinking_sphinx/wildcard_spec.rb b/spec/thinking_sphinx/wildcard_spec.rb index c9b72f1ef..238d68d7d 100644 --- a/spec/thinking_sphinx/wildcard_spec.rb +++ b/spec/thinking_sphinx/wildcard_spec.rb @@ -1,4 +1,6 @@ # encoding: utf-8 +# frozen_string_literal: true + module ThinkingSphinx; end require './lib/thinking_sphinx/wildcard' @@ -6,41 +8,46 @@ module ThinkingSphinx; end describe ThinkingSphinx::Wildcard do describe '.call' do it "does not star quorum operators" do - ThinkingSphinx::Wildcard.call("foo/3").should == "*foo*/3" + expect(ThinkingSphinx::Wildcard.call("foo/3")).to eq("*foo*/3") end it "does not star proximity operators or quoted strings" do - ThinkingSphinx::Wildcard.call(%q{"hello world"~3}). - should == %q{"hello world"~3} + expect(ThinkingSphinx::Wildcard.call(%q{"hello world"~3})). + to eq(%q{"hello world"~3}) end it "treats slashes as a separator when starring" do - ThinkingSphinx::Wildcard.call("a\\/c").should == "*a*\\/*c*" + expect(ThinkingSphinx::Wildcard.call("a\\/c")).to eq("*a*\\/*c*") end it "separates escaping from the end of words" do - ThinkingSphinx::Wildcard.call("\\(913\\)").should == "\\(*913*\\)" + expect(ThinkingSphinx::Wildcard.call("\\(913\\)")).to eq("\\(*913*\\)") end it "ignores escaped slashes" do - ThinkingSphinx::Wildcard.call("\\/\\/pan").should == "\\/\\/*pan*" + expect(ThinkingSphinx::Wildcard.call("\\/\\/pan")).to eq("\\/\\/*pan*") end it "does not star manually provided field tags" do - ThinkingSphinx::Wildcard.call("@title pan").should == "@title *pan*" + expect(ThinkingSphinx::Wildcard.call("@title pan")).to eq("@title *pan*") + end + + it 'does not star multiple field tags' do + expect(ThinkingSphinx::Wildcard.call("@title pan @tags food")). + to eq("@title *pan* @tags *food*") end it "does not star manually provided arrays of field tags" do - ThinkingSphinx::Wildcard.call("@(title, body) pan"). - should == "@(title, body) *pan*" + expect(ThinkingSphinx::Wildcard.call("@(title, body) pan")). + to eq("@(title, body) *pan*") end it "handles nil queries" do - ThinkingSphinx::Wildcard.call(nil).should == '' + expect(ThinkingSphinx::Wildcard.call(nil)).to eq('') end it "handles unicode values" do - ThinkingSphinx::Wildcard.call('älytön').should == '*älytön*' + expect(ThinkingSphinx::Wildcard.call('älytön')).to eq('*älytön*') end end end diff --git a/spec/thinking_sphinx_spec.rb b/spec/thinking_sphinx_spec.rb index 5b0e4b3f5..775966e77 100644 --- a/spec/thinking_sphinx_spec.rb +++ b/spec/thinking_sphinx_spec.rb @@ -1,19 +1,22 @@ +# frozen_string_literal: true + require 'spec_helper' describe ThinkingSphinx do describe '.count' do - let(:search) { double('search', :total_entries => 23) } + let(:search) { double('search', :total_entries => 23, :populated? => false, + :options => {}) } before :each do - ThinkingSphinx::Search.stub :new => search + allow(ThinkingSphinx::Search).to receive_messages :new => search end it "returns the total entries of the search object" do - ThinkingSphinx.count.should == search.total_entries + expect(ThinkingSphinx.count).to eq(search.total_entries) end it "passes through the given query and options" do - ThinkingSphinx::Search.should_receive(:new).with('foo', :bar => :baz). + expect(ThinkingSphinx::Search).to receive(:new).with('foo', { :bar => :baz }). and_return(search) ThinkingSphinx.count('foo', :bar => :baz) @@ -24,15 +27,15 @@ let(:search) { double('search') } before :each do - ThinkingSphinx::Search.stub :new => search + allow(ThinkingSphinx::Search).to receive_messages :new => search end it "returns a new search object" do - ThinkingSphinx.search.should == search + expect(ThinkingSphinx.search).to eq(search) end it "passes through the given query and options" do - ThinkingSphinx::Search.should_receive(:new).with('foo', :bar => :baz). + expect(ThinkingSphinx::Search).to receive(:new).with('foo', { :bar => :baz }). and_return(search) ThinkingSphinx.search('foo', :bar => :baz) diff --git a/thinking-sphinx.gemspec b/thinking-sphinx.gemspec index 06a43ec62..a4e0587bb 100644 --- a/thinking-sphinx.gemspec +++ b/thinking-sphinx.gemspec @@ -1,19 +1,19 @@ +# frozen_string_literal: true + # -*- encoding: utf-8 -*- $:.push File.expand_path('../lib', __FILE__) Gem::Specification.new do |s| s.name = 'thinking-sphinx' - s.version = '3.1.3' + s.version = '5.6.0' s.platform = Gem::Platform::RUBY s.authors = ["Pat Allan"] s.email = ["pat@freelancing-gods.com"] - s.homepage = 'http://pat.github.io/thinking-sphinx/' + s.homepage = 'https://pat.github.io/thinking-sphinx/' s.summary = 'A smart wrapper over Sphinx for ActiveRecord' s.description = %Q{An intelligent layer for ActiveRecord (via Rails and Sinatra) for the Sphinx full-text search tool.} s.license = 'MIT' - s.rubyforge_project = 'thinking-sphinx' - s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| @@ -21,15 +21,16 @@ Gem::Specification.new do |s| } s.require_paths = ['lib'] - s.add_runtime_dependency 'activerecord', '>= 3.1.0' + s.add_runtime_dependency 'activerecord', '>= 4.2.0' s.add_runtime_dependency 'builder', '>= 2.1.2' - s.add_runtime_dependency 'joiner', '>= 0.2.0' + s.add_runtime_dependency 'joiner', '>= 0.3.4' s.add_runtime_dependency 'middleware', '>= 0.1.0' s.add_runtime_dependency 'innertube', '>= 1.0.2' - s.add_runtime_dependency 'riddle', '>= 1.5.11' + s.add_runtime_dependency 'riddle', '~> 2.3' - s.add_development_dependency 'appraisal', '~> 0.5.2' - s.add_development_dependency 'combustion', '~> 0.4.0' - s.add_development_dependency 'database_cleaner', '~> 1.2.0' - s.add_development_dependency 'rspec', '~> 2.13.0' + s.add_development_dependency 'appraisal', '~> 1.0.2' + s.add_development_dependency 'combustion', '~> 1.1' + s.add_development_dependency 'database_cleaner', '~> 2.0.2' + s.add_development_dependency 'rspec', '~> 3.12.0' + s.add_development_dependency 'rspec-retry', '~> 0.5.6' end