runestone 2.0.0 → 2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c069ec88ae02b52e3cb0a155efe7d17b0cb3d0f9b248e74deec97cb1ed752e0
4
- data.tar.gz: 4e627b4879f88ae65b8b56090604e7e8fe0379f93f5a72b4142d76d5eabf08f7
3
+ metadata.gz: 96c3c19ecdb5ba5e58fae758af825532f6dedd8a9ace5291a0e70ad3b28e2436
4
+ data.tar.gz: eab646d44fb2e24cd14ee1f98fcbbba7944fba4907cb2bb0de452dbdb21554cc
5
5
  SHA512:
6
- metadata.gz: 7f3f103de6f864ce9d8047232023f1c6db98867be4a7015d899c088875b830d9ee34e84c31d23b18bbdb88fc9058efe7a455891958d5adbb6b3600aed0ff4552
7
- data.tar.gz: 014ab3c81cb2c6df0473cd5ec0c65a35a6aa46a41c7f351962b26a97abc32eac74820738bcccddfa0d67b5a3b8c715d8ed5a6d1810d1099be1f134d85ac54679
6
+ metadata.gz: d728fbc86eedba51c8fc230ca6a49960d7e63a4f7f95af31a5c28cb619df55385c505ac874ca23f53c89b9bba7a2e23d4279ef5583580df05e866b55d145c001
7
+ data.tar.gz: 8a536c404142691627e26d6d6a74f3434df09a34ae326df390cfb5621e563433dae7f0123985ec0ec4f9168239e0a45b0d942dc12e0f50c59baa60a146a13f62
@@ -0,0 +1,45 @@
1
+ name: Test Suite
2
+ on: [push, pull_request]
3
+
4
+ jobs:
5
+ test:
6
+ runs-on: ubuntu-22.04
7
+ strategy:
8
+ matrix:
9
+ rails-version:
10
+ - 7.0.8
11
+ - 7.1.2
12
+ ruby-version:
13
+ - 3.0
14
+ - 3.1
15
+ - 3.2
16
+ postgres-version:
17
+ - 15
18
+
19
+ steps:
20
+ - name: Install Postgresql
21
+ run: |
22
+ sudo apt-get -y --purge remove $(sudo apt list --installed | grep postgresql | awk '{print $1}')
23
+ sudo apt-get install curl ca-certificates gnupg
24
+ curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
25
+ sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
26
+ sudo apt-get update
27
+ sudo apt-get -y install postgresql-${{ matrix.postgres-version }}-postgis-3
28
+ sudo systemctl start postgresql@${{ matrix.postgres-version }}-main.service
29
+ sudo systemctl status postgresql@${{ matrix.postgres-version }}-main.service
30
+ sudo pg_lsclusters
31
+ sudo -u postgres createuser runner --superuser
32
+ sudo -u postgres psql -c "ALTER USER runner WITH PASSWORD 'runner';"
33
+
34
+ - uses: actions/checkout@v4
35
+
36
+ - run: |
37
+ echo 'gem "activerecord", "${{ matrix.rails-version }}"' >> Gemfile
38
+
39
+ - uses: ruby/setup-ruby@v1
40
+ with:
41
+ ruby-version: ${{ matrix.ruby-version }}
42
+ bundler-cache: true # runs 'bundle install' and caches installed gems automatically
43
+
44
+ - name: Run Test Suite
45
+ run: bundle exec rake test
data/.gitignore CHANGED
@@ -2,3 +2,4 @@ coverage
2
2
  .byebug_history
3
3
  *.gem
4
4
  Gemfile.lock
5
+ .DS_Store
data/README.md CHANGED
@@ -51,7 +51,40 @@ class Building < ApplicationRecord
51
51
  end
52
52
  ```
53
53
 
54
- When searching the attribute(s) will be available in `data` on the result(s), but only the attributes specified by `index` will indexed and used for searching.
54
+ When searching the attribute(s) will be available in `data` on the result(s),
55
+ but only the attributes specified by `index` will indexed and used for searching.
56
+
57
+ Generally Runestone will automatically update the search index if changes are
58
+ made. This is done by seeing if the corresponding column or association has
59
+ changed. If your search attribute is generated you need to define on the columns
60
+ or associations it depends on.
61
+
62
+ ```ruby
63
+ class User < ApplicationRecord
64
+ runestone do
65
+ # The attribute `:name` is generated from the `name_en` column
66
+ attribute(:name, :name_en) { name_en }
67
+ end
68
+ end
69
+
70
+ class Building < ApplicationRecord
71
+ runestone do
72
+ # The attribute `:address_numbers` is generated from the association `addresses`
73
+ attribute(:address_numbers, :addresses) { addresses.map{ |a| a.number } }
74
+ end
75
+ end
76
+
77
+ class User < ActiveRecord::Base
78
+ runestone do
79
+ index 'name'
80
+
81
+ # The attribute `:name` is updated when the custom logic proc returns true
82
+ attribute :name, -> () { ...custom logic... } do
83
+ name
84
+ end
85
+ end
86
+ end
87
+ ```
55
88
 
56
89
  ### Searching
57
90
 
@@ -93,7 +126,17 @@ is the `simple` dictionary in PostgreSQL with `unaccent` to tranliterate some
93
126
  characters to the ASCII equivlent.
94
127
 
95
128
  ```ruby
96
- Runestone.dictionary = :runesonte
129
+ module RailsApplicationName
130
+ class Application < Rails::Application
131
+ config.runestone.dictionary = :runestone
132
+ end
133
+ end
134
+ ```
135
+
136
+ If you are not using Rails, you can use the following:
137
+
138
+ ```ruby
139
+ Runestone.dictionary = :runestone
97
140
  ```
98
141
 
99
142
  #### normalization for ranking
@@ -102,7 +145,17 @@ Ranking can be configured to use the `normalization` paramater as described
102
145
  in the [PostgreSQL documentation][3]. The default is `16`
103
146
 
104
147
  ```ruby
105
- Runestone.dictionary = 16
148
+ module RailsApplicationName
149
+ class Application < Rails::Application
150
+ config.runestone.normalization = 1|16
151
+ end
152
+ end
153
+ ```
154
+
155
+ If you are not using Rails, you can use the following:
156
+
157
+ ```ruby
158
+ Runestone.normalization = 16
106
159
  ```
107
160
 
108
161
  [1]: http://rachbelaid.com/postgres-full-text-search-is-good-enough/
@@ -51,13 +51,13 @@ module Runestone::ActiveRecord
51
51
  AND #{model_table}.id IS NULL;
52
52
  SQL
53
53
 
54
- find_each(&:update_runestones!)
54
+ find_each { |r| r.update_runestones!(false) }
55
55
  end
56
56
 
57
57
  def highlights(name: :default, dictionary: nil)
58
58
  dictionary ||= Runestone.dictionary
59
59
 
60
- rsettings = self.runestone_settings[name].find { |s| s.dictionary.to_s == dictionary.to_s}
60
+ rsettings = self.runestone_settings[name].find { |s| s.dictionary.to_s == dictionary.to_s }
61
61
  @highlights ||= highlight_indexes(rsettings.indexes.values.flatten.map{ |i| i.to_s.split('.') })
62
62
  end
63
63
 
@@ -106,10 +106,11 @@ module Runestone::ActiveRecord
106
106
  Runestone::IndexingJob.preform_later(self, :update_runestones!)
107
107
  end
108
108
 
109
- def update_runestones!
109
+ def update_runestones!(if_changed = true)
110
110
  conn = Runestone::Model.connection
111
111
  self.runestone_settings.each do |index_name, settings|
112
112
  settings.each do |setting|
113
+ next if if_changed && !setting.changed?(self)
113
114
  rdata = setting.extract_attributes(self)
114
115
 
115
116
  if conn.execute(<<-SQL).cmd_tuples == 0
@@ -1,11 +1,11 @@
1
1
  class Runestone::Engine < Rails::Engine
2
2
  config.runestone = ActiveSupport::OrderedOptions.new
3
3
 
4
- initializer :append_migrations do |app|
5
- unless app.root.to_s.match root.to_s
6
- config.paths["db/migrate"].expanded.each do |expanded_path|
7
- app.config.paths["db/migrate"] << expanded_path
8
- end
4
+ initializer :runestone do |app|
5
+ ActiveSupport.on_load(:active_record) do
6
+ require 'active_record/connection_adapters/postgresql/schema_dumper'
7
+ ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaDumper.prepend(Runestone::PsqlSchemaDumper)
8
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths << File.expand_path('../../../db/migrate', __FILE__)
9
9
  end
10
10
  end
11
11
 
@@ -14,8 +14,8 @@ class Runestone::Engine < Rails::Engine
14
14
 
15
15
  Runestone.runner = options.runner if options.runner
16
16
  Runestone.dictionary = options.dictionary if options.dictionary
17
+ Runestone.normalization = options.normalization if options.normalization
17
18
  Runestone.job_queue = options.job_queue if options.job_queue
18
19
  Runestone.typo_tolerances = options.typo_tolerances if options.typo_tolerances
19
20
  end
20
-
21
21
  end
@@ -0,0 +1,17 @@
1
+ module Runestone::PsqlSchemaDumper
2
+
3
+ def extensions(stream)
4
+ super(stream)
5
+ stream.puts <<-RB
6
+ ## Install the default dictionary for Runestone in the database
7
+ execute <<-SQL
8
+ CREATE TEXT SEARCH CONFIGURATION runestone (COPY = simple);
9
+ ALTER TEXT SEARCH CONFIGURATION runestone
10
+ ALTER MAPPING FOR hword, hword_part, word
11
+ WITH unaccent, simple;
12
+ SQL
13
+ RB
14
+ stream
15
+ end
16
+
17
+ end
@@ -14,11 +14,16 @@ class Runestone::Settings
14
14
  end
15
15
 
16
16
  def attribute(*names, &block)
17
- raise ArgumentError.new('Cannot pass multiple attribute names if block given') if block_given? and names.length > 1
17
+ deps = if block_given? and names.length > 2
18
+ raise ArgumentError.new('Cannot pass multiple attribute names if block given')
19
+ else
20
+ names.length > 1 ? names.pop : names.first
21
+ end
22
+ deps = deps.to_s if !deps.is_a?(Proc)
18
23
 
19
24
  @attributes ||= {}
20
25
  names.each do |name|
21
- @attributes[name.to_sym] = block ? block : nil
26
+ @attributes[name.to_sym] = [block ? block : nil, deps]
22
27
  end
23
28
  end
24
29
  alias :attributes :attribute
@@ -27,8 +32,8 @@ class Runestone::Settings
27
32
  attributes = {}
28
33
 
29
34
  @attributes.each do |name, value|
30
- attributes[name] = if value.is_a?(Proc)
31
- record.instance_exec(&value)
35
+ attributes[name] = if value[0].is_a?(Proc)
36
+ record.instance_exec(&value[0])
32
37
  else
33
38
  rv = record.send(name)
34
39
  end
@@ -37,6 +42,18 @@ class Runestone::Settings
37
42
  remove_nulls(attributes)
38
43
  end
39
44
 
45
+ def changed?(record)
46
+ @attributes.detect do |name, value|
47
+ if value[1].is_a?(Proc)
48
+ record.instance_exec(&value[1])
49
+ elsif record.attribute_names.include?(value[1])
50
+ record.previous_changes.has_key?(value[1])
51
+ elsif record._reflections[value[1]] && association = record.association(value[1])
52
+ association.loaded? && association.changed_for_autosave?
53
+ end
54
+ end
55
+ end
56
+
40
57
  def vectorize(data)
41
58
  conn = Runestone::Model.connection
42
59
  tsvector = []
@@ -1,3 +1,3 @@
1
1
  module Runestone
2
- VERSION = '2.0.0'
2
+ VERSION = '2.1'
3
3
  end
data/lib/runestone.rb CHANGED
@@ -5,6 +5,7 @@ module Runestone
5
5
  autoload :Settings, "#{File.dirname(__FILE__)}/runestone/settings"
6
6
  autoload :WebSearch, "#{File.dirname(__FILE__)}/runestone/web_search"
7
7
  autoload :IndexingJob, "#{File.dirname(__FILE__)}/runestone/indexing_job"
8
+ autoload :PsqlSchemaDumper, "#{File.dirname(__FILE__)}/runestone/psql_schema_dumper"
8
9
 
9
10
  mattr_accessor :dictionary, default: :runestone
10
11
  mattr_accessor :normalization, default: 16
@@ -16,37 +17,6 @@ module Runestone
16
17
  { }
17
18
  end
18
19
 
19
- DEFAULT_APPROXIMATIONS = {
20
- "À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE",
21
- "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I",
22
- "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O",
23
- "Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U",
24
- "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a",
25
- "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e",
26
- "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d",
27
- "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o",
28
- "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y",
29
- "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C",
30
- "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c",
31
- "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E",
32
- "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e",
33
- "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G",
34
- "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i",
35
- "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I",
36
- "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k",
37
- "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l",
38
- "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N",
39
- "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG", "ŋ"=>"ng",
40
- "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE",
41
- "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r",
42
- "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S",
43
- "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t",
44
- "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U",
45
- "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w",
46
- "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z",
47
- "Ž"=>"Z", "ž"=>"z"
48
- }.freeze
49
-
50
20
  def self.normalize(string)
51
21
  string = string.downcase
52
22
  string = string.unicode_normalize!
@@ -61,10 +31,6 @@ module Runestone
61
31
  rescue Encoding::CompatibilityError
62
32
  end
63
33
 
64
- def transliterate(string)
65
- string.gsub(/[^\x00-\x7f]/u) { |char| approximations[char] || char }
66
- end
67
-
68
34
  def self.add_synonyms(dictionary)
69
35
  dictionary.each do |k, v|
70
36
  add_synonym(k, *v)
data/runestone.gemspec CHANGED
@@ -24,9 +24,9 @@ Gem::Specification.new do |s|
24
24
  s.add_development_dependency 'byebug'
25
25
  s.add_development_dependency 'faker'
26
26
  s.add_development_dependency 'simplecov'
27
- s.add_development_dependency 'activejob', '>= 6.0'
27
+ s.add_development_dependency 'activejob', '>= 7.0'
28
28
 
29
29
  # Runtime
30
- s.add_runtime_dependency 'arel-extensions', '>= 6.0.0.9'
31
- s.add_runtime_dependency 'activerecord', '>= 6.0'
30
+ s.add_runtime_dependency 'arel-extensions', '>= 7.0.0'
31
+ s.add_runtime_dependency 'activerecord', '>= 7.0'
32
32
  end
data/test/database.rb CHANGED
@@ -1,18 +1,16 @@
1
1
  GlobalID.app = 'TestApp'
2
2
 
3
- task = ActiveRecord::Tasks::PostgreSQLDatabaseTasks.new({
4
- 'adapter' => 'postgresql',
5
- 'database' => "arel-extensions-test"
6
- })
7
- task.drop
8
- task.create
9
-
10
3
  ActiveRecord::Base.establish_connection({
11
4
  adapter: "postgresql",
12
5
  database: "arel-extensions-test",
13
6
  encoding: "utf8"
14
7
  })
15
8
 
9
+ db_config = ActiveRecord::Base.connection_db_config
10
+ task = ActiveRecord::Tasks::PostgreSQLDatabaseTasks.new(db_config)
11
+ task.drop
12
+ task.create
13
+
16
14
  ActiveRecord::Migration.suppress_messages do
17
15
  ActiveRecord::Schema.define do
18
16
  enable_extension 'pgcrypto'
@@ -22,21 +20,29 @@ ActiveRecord::Migration.suppress_messages do
22
20
 
23
21
  create_table :addresses, id: :uuid, force: :cascade do |t|
24
22
  t.string "name"
23
+ t.string "metadata"
25
24
  t.uuid "property_id"
26
25
  end
27
26
 
28
27
  create_table :properties, id: :uuid, force: :cascade do |t|
29
- t.string "name", limit: 255
28
+ t.string "name", limit: 255
29
+ t.string "metadata"
30
30
  end
31
31
 
32
32
  create_table :regions, id: :uuid, force: :cascade do |t|
33
33
  t.string "name", limit: 255
34
+ t.boolean "pooblic", default: true
34
35
  end
35
36
 
36
37
  create_table :buildings, id: :uuid, force: :cascade do |t|
37
38
  t.string "name_en", limit: 255
38
39
  t.string "name_ru", limit: 255
39
40
  end
41
+
42
+ create_table :people, id: :uuid, force: :cascade do |t|
43
+ t.string "name", limit: 255
44
+ t.boolean "pooblic", default: true
45
+ end
40
46
 
41
47
  create_table :runestones, id: :uuid, force: :cascade do |t|
42
48
  t.string :record_type, null: false
@@ -51,10 +57,9 @@ ActiveRecord::Migration.suppress_messages do
51
57
  add_index :runestones, :vector, using: :gin
52
58
 
53
59
  execute <<-SQL
54
- CREATE TABLE runestone_corpus ( word varchar, CONSTRAINT word UNIQUE(word) );
55
-
56
- CREATE INDEX runestone_corpus_trgm_idx ON runestone_corpus USING GIN (word gin_trgm_ops);
60
+ CREATE TABLE runestone_corpus ( word varchar, CONSTRAINT word UNIQUE(word) );
57
61
 
62
+ CREATE INDEX runestone_corpus_trgm_idx ON runestone_corpus USING GIN (word gin_trgm_ops);
58
63
 
59
64
  CREATE TEXT SEARCH CONFIGURATION runestone (COPY = simple);
60
65
  ALTER TEXT SEARCH CONFIGURATION runestone
@@ -90,7 +95,7 @@ end
90
95
 
91
96
  class Property < ActiveRecord::Base
92
97
 
93
- has_many :addresses
98
+ has_many :addresses, autosave: true
94
99
 
95
100
  runestone do
96
101
  index :name
@@ -107,7 +112,7 @@ class Building < ActiveRecord::Base
107
112
  runestone dictionary: 'english' do
108
113
  index :name
109
114
 
110
- attribute(:name) { name_en }
115
+ attribute(:name, :name_en) { name_en }
111
116
  end
112
117
 
113
118
  runestone dictionary: 'russian' do
@@ -116,4 +121,16 @@ class Building < ActiveRecord::Base
116
121
  attribute(:name) { name_ru }
117
122
  end
118
123
 
124
+ end
125
+
126
+ class Person < ActiveRecord::Base
127
+
128
+ runestone do
129
+ index 'name'
130
+
131
+ attribute :name, -> () { pooblic } do
132
+ name
133
+ end
134
+ end
135
+
119
136
  end
@@ -84,7 +84,7 @@ class IndexingTest < ActiveSupport::TestCase
84
84
  assert_corpus('address', 'name')
85
85
  end
86
86
 
87
- test 'index gets updated on Model.create' do
87
+ test 'index gets updated on Model.update' do
88
88
  address = Address.create(name: 'Address name')
89
89
  assert_no_difference 'Runestone::Model.count' do
90
90
  address.update!(name: 'Address name two')
@@ -106,6 +106,175 @@ class IndexingTest < ActiveSupport::TestCase
106
106
  assert_corpus('address', 'name', 'two')
107
107
  end
108
108
 
109
+ test 'index doesnt update on Model.update when updates dont affect index (column for attribute, depends on itself)' do
110
+ address = Address.create(name: 'Address name')
111
+ assert_no_difference 'Runestone::Model.count' do
112
+ assert_no_sql("UPDATE runestones SET data") do
113
+ address.update!(metadata: 'extra info not used in index')
114
+ end
115
+ end
116
+
117
+ assert_equal([[
118
+ 'Address', address.id,
119
+ {"name" => "Address name"},
120
+ "'address':1A 'name':2A"
121
+ ]], address.runestones.map { |runestone|
122
+ [
123
+ runestone.record_type,
124
+ runestone.record_id,
125
+ runestone.data,
126
+ runestone.vector
127
+ ]
128
+ })
129
+
130
+ assert_corpus('address', 'name')
131
+ end
132
+
133
+ test 'index doesnt update on Model.update when updates dont affect index (proc for attribute, depends on relation)' do
134
+ property = Property.create(name: 'Property name', addresses: [
135
+ Address.create(name: 'Address 1'),
136
+ Address.create(name: 'Address 2')
137
+ ])
138
+
139
+ assert_no_difference 'Runestone::Model.count' do
140
+ assert_no_sql("UPDATE runestones SET data") do
141
+ property.update!(metadata: 'extra info not used in index')
142
+ end
143
+ end
144
+
145
+ assert_equal([[
146
+ 'Property', property.id,
147
+ {"name"=>"Property name", "addresses"=>[
148
+ {"id"=> property.addresses[0].id, "name"=>"Address 1"},
149
+ {"id"=> property.addresses[1].id, "name"=>"Address 2"}
150
+ ]},
151
+ "'1':4C '2':6C 'address':3C,5C 'name':2A 'property':1A"
152
+ ]], property.runestones.map { |runestone|
153
+ [
154
+ runestone.record_type,
155
+ runestone.record_id,
156
+ runestone.data,
157
+ runestone.vector
158
+ ]
159
+ })
160
+
161
+ assert_corpus("1", "2", "address", "name", "property")
162
+ end
163
+
164
+ test 'index updates on Model.update when relation is load/changed (proc for attribute, depends on relation)' do
165
+ property = Property.create(name: 'Property name', addresses: [
166
+ Address.create(name: 'Address 1'),
167
+ Address.create(name: 'Address 2')
168
+ ])
169
+
170
+ assert_no_difference 'Runestone::Model.count' do
171
+ assert_sql("UPDATE runestones SET data") do
172
+ property.addresses.first.name = 'Address rename 1'
173
+ property.save
174
+ end
175
+ end
176
+
177
+ assert_equal([[
178
+ 'Property', property.id,
179
+ {"name"=>"Property name", "addresses"=>[
180
+ {"id"=> property.addresses[0].id, "name"=>"Address 1"},
181
+ {"id"=> property.addresses[1].id, "name"=>"Address 2"}
182
+ ]},
183
+ "'1':4C '2':6C 'address':3C,5C 'name':2A 'property':1A"
184
+ ]], property.runestones.map { |runestone|
185
+ [
186
+ runestone.record_type,
187
+ runestone.record_id,
188
+ runestone.data,
189
+ runestone.vector
190
+ ]
191
+ })
192
+
193
+ assert_corpus("1", "2", "address", "name", "property", "rename")
194
+ end
195
+
196
+ test 'index doesnt update on Model.update when updates dont affect index (block for attribute, depends on attribute)' do
197
+ building = Building.create(name_en: 'name', name_ru: 'имя')
198
+
199
+ assert_no_difference 'Runestone::Model.count' do
200
+ assert_no_sql(/UPDATE runestones\s+SET\s+data = '{"name":"имя"}/i) do
201
+ building.update!(name_en: 'name 2')
202
+ end
203
+ end
204
+
205
+ assert_equal([[
206
+ 'Building', building.id,
207
+ {"name"=>"name 2"},
208
+ "'2':2A 'name':1A"
209
+ ], [
210
+ 'Building', building.id,
211
+ {"name"=>"имя"},
212
+ "'имя':1A"
213
+ ]
214
+ ], building.runestones.map { |runestone|
215
+ [
216
+ runestone.record_type,
217
+ runestone.record_id,
218
+ runestone.data,
219
+ runestone.vector
220
+ ]
221
+ })
222
+
223
+ assert_corpus('2', 'name', 'м')
224
+ end
225
+
226
+ test 'index doesnt update on Model.update when updates dont affect index (block for attribute, block for depends)' do
227
+ record = Person.create(name: 'person')
228
+
229
+ assert_no_difference 'Runestone::Model.count' do
230
+ assert_no_sql("UPDATE runestones SET data") do
231
+ record.update!(name: 'ghost', pooblic: false)
232
+ end
233
+ end
234
+
235
+ assert_equal([[
236
+ 'Person', record.id,
237
+ {"name"=>"person"},
238
+ "'person':1A"
239
+ ]
240
+ ], record.runestones.map { |runestone|
241
+ [
242
+ runestone.record_type,
243
+ runestone.record_id,
244
+ runestone.data,
245
+ runestone.vector
246
+ ]
247
+ })
248
+
249
+ assert_corpus('person')
250
+ end
251
+
252
+ test 'index updates on Model.update when updates affect index (block for attribute, block for depends)' do
253
+ record = Person.create(name: 'person')
254
+
255
+ assert_no_difference 'Runestone::Model.count' do
256
+ assert_sql("UPDATE runestones SET data") do
257
+ record.update!(name: 'physical', pooblic: true)
258
+ end
259
+ end
260
+
261
+ assert_equal([[
262
+ 'Person', record.id,
263
+ {"name"=>"physical"},
264
+ "'physical':1A"
265
+ ]
266
+ ], record.runestones.map { |runestone|
267
+ [
268
+ runestone.record_type,
269
+ runestone.record_id,
270
+ runestone.data,
271
+ runestone.vector
272
+ ]
273
+ })
274
+
275
+ assert_corpus('person', 'physical')
276
+ end
277
+
109
278
  test 'index gets deleted on Model.destroy' do
110
279
  address = Address.create(name: 'Address name')
111
280
  assert_difference 'Runestone::Model.count', -1 do
data/test/test_helper.rb CHANGED
@@ -63,50 +63,70 @@ class ActiveSupport::TestCase
63
63
  ActiveRecord::Base.logger = nil
64
64
  $debugging = false
65
65
  end
66
+
67
+ def assert_sql(*expected)
68
+ return_value = nil
69
+
70
+ queries_ran = if block_given?
71
+ queries_ran = SQLLogger.log.size
72
+ return_value = yield if block_given?
73
+ SQLLogger.log[queries_ran...]
74
+ else
75
+ [expected.pop]
76
+ end
66
77
 
67
- def capture_sql
68
- # ActiveRecord::Base.connection.materialize_transactions
69
- SQLLogger.clear_log
70
- yield
71
- SQLLogger.log_all.dup
78
+ failed_patterns = []
79
+
80
+ expected.each do |pattern|
81
+ failed_patterns << pattern unless queries_ran.any?{ |sql| sql_equal(pattern, sql) }
82
+ end
83
+
84
+ assert failed_patterns.empty?, <<~MSG
85
+ Query pattern(s) not found:
86
+ - #{failed_patterns.map(&:inspect).join('\n - ')}
87
+
88
+ Queries Ran (queries_ran.size):
89
+ - #{queries_ran.map{|l| l.gsub(/\n\s*/, "\n ")}.join("\n - ")}
90
+ MSG
91
+
92
+ return_value
72
93
  end
73
94
 
74
- def assert_sql(*patterns_to_match)
75
- if patterns_to_match.all? { |s| s.is_a?(String) }
76
- assert_equal(*patterns_to_match.take(2).map { |sql| sql.gsub(/( +|\n\s*|\s+)/, ' ').strip })
77
- else
78
- begin
79
- ret_value = nil
80
- capture_sql { ret_value = yield }
81
- ret_value
82
- ensure
83
- failed_patterns = []
84
- patterns_to_match.each do |pattern|
85
- failed_patterns << pattern unless SQLLogger.log_all.any?{ |sql| pattern === sql }
86
- end
87
- assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{SQLLogger.log.size == 0 ? '' : "\nQueries:\n#{SQLLogger.log.join("\n")}"}"
88
- end
95
+ def assert_no_sql(*not_expected)
96
+ return_value = nil
97
+ queries_ran = block_given? ? SQLLogger.log.size : 0
98
+
99
+ return_value = yield if block_given?
100
+
101
+ ensure
102
+ failed_patterns = []
103
+ queries_ran = SQLLogger.log[queries_ran...]
104
+
105
+ not_expected.each do |pattern|
106
+ failed_patterns << pattern if queries_ran.any?{ |sql| sql_equal(pattern, sql) }
89
107
  end
108
+
109
+ assert failed_patterns.empty?, <<~MSG
110
+ Unexpected Query pattern(s) found:
111
+ - #{failed_patterns.map(&:inspect).join('\n - ')}
112
+
113
+ Queries Ran (queries_ran.size):
114
+ - #{queries_ran.map{|l| l.gsub(/\n\s*/, "\n ")}.join("\n - ")}
115
+ MSG
116
+
117
+ return_value
90
118
  end
91
119
 
92
- def assert_no_sql(*patterns_to_match)
93
- if patterns_to_match.all? { |s| s.is_a?(String) }
94
- assert_not_equal(*patterns_to_match.take(2).map { |sql| sql.gsub(/( +|\n\s*|\s+)/, ' ').strip })
95
- else
96
- begin
97
- ret_value = nil
98
- capture_sql { ret_value = yield }
99
- ret_value
100
- ensure
101
- failed_patterns = []
102
- patterns_to_match.each do |pattern|
103
- failed_patterns << pattern unless SQLLogger.log_all.any?{ |sql| pattern === sql }
104
- end
105
- assert !failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} found.#{SQLLogger.log.size == 0 ? '' : "\nQueries:\n#{SQLLogger.log.join("\n")}"}"
106
- end
120
+ def sql_equal(expected, sql)
121
+ sql = sql.strip.gsub(/"(\w+)"/, '\1').gsub(/\(\s+/, '(').gsub(/\s+\)/, ')').gsub(/[\s|\n]+/, ' ')
122
+ if expected.is_a?(String)
123
+ expected = Regexp.new(Regexp.escape(expected.strip.gsub(/"(\w+)"/, '\1').gsub(/\(\s+/, '(').gsub(/\s+\)/, ')').gsub(/[\s|\n]+/, ' ')), Regexp::IGNORECASE)
107
124
  end
125
+
126
+ expected.match(sql)
108
127
  end
109
128
 
129
+
110
130
  def corpus
111
131
  Runestone::Model.connection.execute('SELECT word FROM runestone_corpus ORDER BY word').values.flatten
112
132
  end
@@ -157,7 +177,7 @@ class ActiveSupport::TestCase
157
177
  self.class.log_all << sql
158
178
  unless ignore =~ sql
159
179
  if $debugging
160
- puts caller.select { |l| l.starts_with?(File.expand_path('../../lib', __FILE__)) }
180
+ puts caller.select { |l| l.start_with?(File.expand_path('../../lib', __FILE__)) }
161
181
  puts "\n\n"
162
182
  end
163
183
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: runestone
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: '2.1'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-10-25 00:00:00.000000000 Z
11
+ date: 2024-02-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -128,42 +128,42 @@ dependencies:
128
128
  requirements:
129
129
  - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: '6.0'
131
+ version: '7.0'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: '6.0'
138
+ version: '7.0'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: arel-extensions
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
143
  - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: 6.0.0.9
145
+ version: 7.0.0
146
146
  type: :runtime
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - ">="
151
151
  - !ruby/object:Gem::Version
152
- version: 6.0.0.9
152
+ version: 7.0.0
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: activerecord
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
157
  - - ">="
158
158
  - !ruby/object:Gem::Version
159
- version: '6.0'
159
+ version: '7.0'
160
160
  type: :runtime
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
- version: '6.0'
166
+ version: '7.0'
167
167
  description: PostgreSQL Full Text Search for Active Record and Rails
168
168
  email:
169
169
  - jonbracy@gmail.com
@@ -171,6 +171,7 @@ executables: []
171
171
  extensions: []
172
172
  extra_rdoc_files: []
173
173
  files:
174
+ - ".github/workflows/main.yml"
174
175
  - ".gitignore"
175
176
  - ".tm_properties"
176
177
  - Gemfile
@@ -185,6 +186,7 @@ files:
185
186
  - lib/runestone/engine.rb
186
187
  - lib/runestone/indexing_job.rb
187
188
  - lib/runestone/model.rb
189
+ - lib/runestone/psql_schema_dumper.rb
188
190
  - lib/runestone/settings.rb
189
191
  - lib/runestone/version.rb
190
192
  - lib/runestone/web_search.rb
@@ -223,7 +225,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
223
225
  - !ruby/object:Gem::Version
224
226
  version: '0'
225
227
  requirements: []
226
- rubygems_version: 3.1.4
228
+ rubygems_version: 3.5.4
227
229
  signing_key:
228
230
  specification_version: 4
229
231
  summary: Full Text Search for Active Record / Rails