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 +4 -4
- data/lib/ext/scenic/adapters/postgres.rb +27 -0
- data/lib/searchcraft/builder.rb +26 -5
- data/lib/searchcraft/model.rb +49 -2
- data/lib/searchcraft/version.rb +1 -1
- data/lib/tasks/refresh.rake +1 -1
- data/sig/ext/misc.rbs +11 -0
- data/sig/searchcraft/builder.rbs +4 -1
- data/sig/searchcraft/model.rbs +4 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dcdc1a8d1072f3a64089e24aa0c6682876a00e0f0e0300a3b1552f299a5d4208
|
4
|
+
data.tar.gz: a3ced335b1a4042c43936d39af7efc678b3b0f4bfb6e8959260c1ed11da3be66
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/searchcraft/builder.rb
CHANGED
@@ -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!(
|
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
|
-
|
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!
|
data/lib/searchcraft/model.rb
CHANGED
@@ -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
|
-
|
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?
|
data/lib/searchcraft/version.rb
CHANGED
data/lib/tasks/refresh.rake
CHANGED
@@ -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
|
-
|
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
|
|
data/sig/searchcraft/builder.rbs
CHANGED
@@ -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
|
data/sig/searchcraft/model.rbs
CHANGED
@@ -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
|
+
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:
|
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.
|
221
|
+
rubygems_version: 3.5.5
|
221
222
|
signing_key:
|
222
223
|
specification_version: 4
|
223
224
|
summary: Instant search for Rails and ActiveRecord
|