searchcraft 0.4.2 → 0.5.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: 913320513d07b3447532e62d060b3ebc118dce10531d2f1d62587bdccc5c8c33
4
- data.tar.gz: f8b7d111101ece94baeffd41d5d4f42350f47a9b7db0d96e589ee2297e4ae196
3
+ metadata.gz: dcdc1a8d1072f3a64089e24aa0c6682876a00e0f0e0300a3b1552f299a5d4208
4
+ data.tar.gz: a3ced335b1a4042c43936d39af7efc678b3b0f4bfb6e8959260c1ed11da3be66
5
5
  SHA512:
6
- metadata.gz: baab9337ba89647bd3cf7819570d0de0ae32a066c07d33587ac33c157f8bd3014c20516c6a3d5a2bc77ed37fb9e53fa73eabacef8fbf784b240cc5046da036a0
7
- data.tar.gz: 4daaa7dcd5d3312740a83eb8f08efb6f86bc20179a0081e092207f7788673e9bb6ecc0b88ca47beeb33278406f96d4775653b24ccd4255282e9fd8eba16ba3b1
6
+ metadata.gz: 387710b8e2980b3f84539feef53ceec31dac0180f28a3bf12f77cbd3e9932d5d1bd971b4994e0f70719684922e011c2d7cce67b38f59667e02a14a778edb3713
7
+ data.tar.gz: 484faca6fe838694569d563a3df6f5849cf555db4e17191ca3e3d0647dbd9fa09a05cb6fff0231f9dd40a2128c23b52a96ea0c2aef0bddf08e003d55da110d3f
@@ -0,0 +1,27 @@
1
+ # Features of Scenic not available in v1.7.0
2
+ class Scenic::Adapters::Postgres
3
+ # True if supplied relation name is populated. Useful for checking the
4
+ # state of materialized views which may error if created `WITH NO DATA`
5
+ # and used before they are refreshed. True for all other relation types.
6
+ #
7
+ # @param name The name of the relation
8
+ #
9
+ # @raise [MaterializedViewsNotSupportedError] if the version of Postgres
10
+ # in use does not support materialized views.
11
+ #
12
+ # @return [boolean]
13
+ def populated?(name)
14
+ raise_unless_materialized_views_supported
15
+
16
+ schemaless_name = name.split(".").last
17
+
18
+ sql = "SELECT relispopulated FROM pg_class WHERE relname = '#{schemaless_name}'"
19
+ relations = execute(sql)
20
+
21
+ if relations.count.positive?
22
+ relations.first["relispopulated"].in?(["t", true])
23
+ else
24
+ false
25
+ end
26
+ end
27
+ end
@@ -24,8 +24,20 @@ class SearchCraft::Builder
24
24
  end
25
25
 
26
26
  class << self
27
+ def with_no_data
28
+ @with_no_data = true
29
+ end
30
+
31
+ def with_data
32
+ @with_no_data = false
33
+ end
34
+
35
+ def with_no_data?
36
+ @with_no_data
37
+ end
38
+
27
39
  # Iterate through subclasses, and invoke recreate_view_if_changed!
28
- def rebuild_any_if_changed!
40
+ def rebuild_any_if_changed!(skip_dump_schema: false)
29
41
  SearchCraft::ViewHashStore.setup_table_if_needed!
30
42
 
31
43
  sorted_builders = sort_builders_by_dependency
@@ -39,7 +51,10 @@ class SearchCraft::Builder
39
51
 
40
52
  builders_changed = []
41
53
  sorted_builders.each do |builder|
42
- changed = builder.new.recreate_view_if_changed!(builders_changed: builders_changed)
54
+ changed = builder.new.recreate_view_if_changed!(
55
+ builders_changed: builders_changed,
56
+ skip_dump_schema: skip_dump_schema
57
+ )
43
58
  builders_changed << builder if changed
44
59
  end
45
60
 
@@ -98,7 +113,9 @@ class SearchCraft::Builder
98
113
  def view_sql
99
114
  # remove trailing ; from view_sql
100
115
  inner_sql = view_select_sql.gsub(/;\s*$/, "")
101
- "CREATE MATERIALIZED VIEW #{view_name} AS (#{inner_sql}) WITH DATA;"
116
+
117
+ with_data = self.class.with_no_data? ? "WITH NO DATA" : "WITH DATA"
118
+ "CREATE MATERIALIZED VIEW #{view_name} AS (#{inner_sql}) #{with_data};"
102
119
  end
103
120
 
104
121
  # After materialized view created, do you need indexes on its columns?
@@ -114,7 +131,7 @@ class SearchCraft::Builder
114
131
 
115
132
  # If missing or changed, drop and create view
116
133
  # Returns false if no change required
117
- def recreate_view_if_changed!(builders_changed: [])
134
+ def recreate_view_if_changed!(builders_changed: [], skip_dump_schema: false)
118
135
  if SearchCraft.debug?
119
136
  warn "#{self.class.name}#recreate_view_if_changed!"
120
137
  warn " builders_changed: #{builders_changed.map(&:name).join(", ")}" if builders_changed.any?
@@ -139,12 +156,13 @@ class SearchCraft::Builder
139
156
  drop_view!
140
157
  create_view!
141
158
  update_hash_store!
142
- dump_schema!
159
+ dump_schema! unless skip_dump_schema
143
160
 
144
161
  true
145
162
  end
146
163
 
147
164
  def create_view!
165
+ warn "Creating view/sequence/indexes for #{view_name}..." if SearchCraft.debug?
148
166
  create_sequence!
149
167
  sql_execute(view_sql)
150
168
  create_indexes!
@@ -152,13 +170,16 @@ class SearchCraft::Builder
152
170
 
153
171
  # Finds and drops all indexes and sequences on view, and then drops view
154
172
  def drop_view!
173
+ puts "Dropping view/sequence for #{view_name}..." if SearchCraft.debug?
155
174
  sql_execute("DROP MATERIALIZED VIEW IF EXISTS #{view_name} CASCADE;")
156
175
 
157
176
  sql_execute("DROP SEQUENCE IF EXISTS #{view_id_sequence_name};")
158
177
 
178
+ warn "Updating ViewHashStore for #{self.class.name}" if SearchCraft.debug?
159
179
  SearchCraft::ViewHashStore.reset!(builder: self)
160
180
  end
161
181
 
182
+ # TODO: what if indexes didn't change?
162
183
  def recreate_indexes!
163
184
  drop_indexes!
164
185
  create_indexes!
@@ -1,4 +1,5 @@
1
1
  require "scenic"
2
+ require "ext/scenic/adapters/postgres"
2
3
 
3
4
  module SearchCraft::Model
4
5
  # Maintain a list of classes that include this module
@@ -21,13 +22,18 @@ module SearchCraft::Model
21
22
  # Runs .refresh! on all classes that include SearchCraft::Model
22
23
  def self.refresh_all!
23
24
  included_classes.each do |klass|
24
- warn "Refreshing materialized view #{klass.table_name}..." unless Rails.env.test?
25
25
  if klass.is_a?(ClassMethods)
26
26
  klass.refresh!
27
27
  end
28
28
  end
29
29
  end
30
30
 
31
+ def self.refresh_any_unpopulated!
32
+ included_classes.each do |klass|
33
+ klass.refresh! unless klass.populated?
34
+ end
35
+ end
36
+
31
37
  def self.included_classes
32
38
  @included_classes | if SearchCraft.config.explicit_model_class_names
33
39
  SearchCraft.config.explicit_model_class_names.map(&:constantize)
@@ -38,12 +44,53 @@ module SearchCraft::Model
38
44
 
39
45
  module ClassMethods
40
46
  def refresh!
41
- Scenic.database.refresh_materialized_view(table_name, concurrently: @refresh_concurrently, cascade: false)
47
+ refresh_concurrently = @refresh_concurrently && populated?
48
+ unless Rails.env.test?
49
+ if refresh_concurrently
50
+ warn "Refreshing materialized view concurrently #{table_name}..."
51
+ else
52
+ warn "Refreshing materialized view #{table_name}..."
53
+ end
54
+ end
55
+
56
+ Scenic.database.refresh_materialized_view(table_name, concurrently: refresh_concurrently, cascade: false)
57
+ rescue ActiveRecord::StatementInvalid
58
+ # If populated? lies and returns true; then might get error:
59
+ # PG::FeatureNotSupported: ERROR: CONCURRENTLY cannot be used when the materialized view is not populated (ActiveRecord::StatementInvalid)
60
+ Scenic.database.refresh_materialized_view(table_name, concurrently: false, cascade: false)
61
+ end
62
+
63
+ def populated?
64
+ Scenic.database.populated?(table_name)
42
65
  end
43
66
 
44
67
  def refresh_concurrently=(value)
45
68
  @refresh_concurrently = value
46
69
  end
70
+
71
+ # Checks the database server to see if the materialized view is currently being refreshed
72
+ def currently_refreshing?
73
+ # quoted_table_name is table_name, but with double quotes around each chunk
74
+ # e.g. "schema"."table" or "table"
75
+ quoted_table_name = Scenic.database.quote_table_name(table_name)
76
+ dbname = ActiveRecord::Base.connection_db_config.database
77
+ sql = <<~SQL
78
+ SELECT EXISTS (
79
+ SELECT 1
80
+ FROM pg_stat_activity
81
+ WHERE datname = '#{dbname}'
82
+ AND query LIKE '%REFRESH MATERIALIZED VIEW #{quoted_table_name}%'
83
+ AND pid <> pg_backend_pid()
84
+ ) AS is_refresh_running;
85
+ SQL
86
+
87
+ warn "Checking if #{table_name} is currently being refreshed..." if SearchCraft.debug?
88
+ if (result = ActiveRecord::Base.connection.execute(sql))
89
+ result.first["is_refresh_running"]
90
+ else
91
+ false
92
+ end
93
+ end
47
94
  end
48
95
 
49
96
  def read_only?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SearchCraft
4
- VERSION = "0.4.2"
4
+ VERSION = "0.5.1"
5
5
  end
@@ -5,7 +5,7 @@ namespace :searchcraft do
5
5
  require "benchmark"
6
6
  SearchCraft.config.explicit_model_class_names.each do |model_class_name|
7
7
  klass = model_class_name.constantize
8
- puts "Refreshing materialized views for #{klass.name}"
8
+ warn "Refreshing materialized views for #{klass.name}"
9
9
  puts Benchmark.measure { klass.refresh! }
10
10
  end
11
11
  end
data/sig/ext/misc.rbs CHANGED
@@ -10,6 +10,11 @@ module ActiveRecord
10
10
 
11
11
  class Base
12
12
  def self.configurations: () -> ActiveRecord::DatabaseConfigurations
13
+ def self.connection_db_config: () -> ActiveRecord::DatabaseConfig
14
+ end
15
+
16
+ class DatabaseConfig
17
+ def database: () -> String
13
18
  end
14
19
  end
15
20
 
@@ -26,6 +31,12 @@ module Scenic
26
31
  module Adapters
27
32
  class Postgres
28
33
  def refresh_materialized_view: (String, ?concurrently: bool, ?cascade: bool) -> nil
34
+ def populated?: (String) -> bool
35
+ def quote_table_name: (String) -> String
36
+
37
+ # The following are due to temporary ext/scenic/adapters/postgres.rb
38
+ def raise_unless_materialized_views_supported: () -> nil
39
+ def execute: (String) -> ActiveRecord::Result
29
40
  end
30
41
  end
31
42
 
@@ -12,11 +12,14 @@ module SearchCraft
12
12
  def self.find_subclasses_via_rails_eager_load_paths: (?known_subclass_names: Array[String]) -> Array[String]
13
13
 
14
14
  def self.rebuild_all!: () -> void
15
-
16
15
  def self.rebuild_any_if_changed!: () -> void
17
16
 
18
17
  def self.recreate_indexes!: () -> void
19
18
 
19
+ def self.with_no_data: () -> void
20
+ def self.with_data: () -> void
21
+ def self.with_no_data?: () -> bool
22
+
20
23
  def create_view!: () -> void
21
24
 
22
25
  def dependencies_ready?: () -> bool
@@ -5,6 +5,8 @@ module SearchCraft
5
5
 
6
6
  def refresh_all!: () -> void
7
7
 
8
+ def currently_refreshing?: () -> bool
9
+
8
10
  def table_name: () -> String
9
11
 
10
12
  def table_name=: (String) -> void
@@ -12,6 +14,8 @@ module SearchCraft
12
14
  def name: () -> String
13
15
 
14
16
  def refresh!: () -> void
17
+
18
+ def populated?: () -> bool
15
19
  end
16
20
 
17
21
  extend ClassMethods
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: searchcraft
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dr Nic Williams
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-06 00:00:00.000000000 Z
11
+ date: 2024-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -168,6 +168,7 @@ files:
168
168
  - CODE_OF_CONDUCT.md
169
169
  - LICENSE.txt
170
170
  - README.md
171
+ - lib/ext/scenic/adapters/postgres.rb
171
172
  - lib/search_craft.rb
172
173
  - lib/searchcraft.rb
173
174
  - lib/searchcraft/annotate.rb
@@ -217,7 +218,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
217
218
  - !ruby/object:Gem::Version
218
219
  version: '0'
219
220
  requirements: []
220
- rubygems_version: 3.4.20
221
+ rubygems_version: 3.5.5
221
222
  signing_key:
222
223
  specification_version: 4
223
224
  summary: Instant search for Rails and ActiveRecord