searchcraft 0.4.2 → 0.5.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: 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