runestone 2.0.0 → 2.1

Sign up to get free protection for your applications and to get access to all the features.
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