Class: SearchCraft::Builder

Inherits:
Object
  • Object
show all
Extended by:
Annotate, DependsOn::ClassMethods
Includes:
DependsOn, DumpSchema, TextSearch
Defined in:
lib/searchcraft/builder.rb

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Annotate

annotate_models!, capture_stdout

Methods included from DependsOn::ClassMethods

depends_on, sort_builders_by_dependency, visit

Methods included from TextSearch

#coalesce, #setweight_arel, #to_tsvector_arel

Methods included from DumpSchema

#dump_schema!

Class Method Details

.builders_to_rebuildObject



72
73
74
75
76
77
78
79
80
# File 'lib/searchcraft/builder.rb', line 72

def builders_to_rebuild
  if SearchCraft.config.explicit_builder_class_names
    SearchCraft.config.explicit_builder_class_names.map(&:constantize)
  elsif Object.const_defined?(:Rails) && Rails.application
    find_subclasses_via_rails_eager_load_paths.map(&:constantize)
  else
    subclasses
  end
end

.find_subclasses_via_rails_eager_load_paths(known_subclass_names: []) ⇒ Object

Looks for subclasses of SearchCraft::Builder in Rails eager load paths and then any subclasses of those. Returns an array of class names



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/searchcraft/builder.rb', line 85

def find_subclasses_via_rails_eager_load_paths(known_subclass_names: [])
  subclass_names = []

  potential_superclass_names = known_subclass_names + ["SearchCraft::Builder"]
  potential_superclass_regex = Regexp.new(potential_superclass_names.join("|"))

  Rails.configuration.eager_load_paths.each do |load_path|
    Dir.glob("#{load_path}/**/*.rb").each do |file|
      File.readlines(file).each do |line|
        if (match = line.match(/class\s+([\w:]+)\s*<\s*#{potential_superclass_regex}/))
          class_name = match[1].to_s
          warn "Found #{class_name} in #{file}" unless known_subclass_names.include?(class_name)
          subclass_names << class_name
        end
      end
    end
  end

  newly_found_subclass_names = subclass_names - known_subclass_names
  if newly_found_subclass_names.any?
    return find_subclasses_via_rails_eager_load_paths(known_subclass_names: subclass_names)
  end

  subclass_names
end

.rebuild_all!Object



64
65
# File 'lib/searchcraft/builder.rb', line 64

def rebuild_all!
end

.rebuild_any_if_changed!(skip_dump_schema: false) ⇒ Object

Iterate through subclasses, and invoke recreate_view_if_changed!



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/searchcraft/builder.rb', line 40

def rebuild_any_if_changed!(skip_dump_schema: false)
  SearchCraft::ViewHashStore.setup_table_if_needed!

  sorted_builders = sort_builders_by_dependency

  # If tests, and after rails db:schema:load, the ViewHashStore table is empty.
  # So just drop any views created from the schema.rb and we'll recreate them.

  unless SearchCraft::ViewHashStore.any?
    sorted_builders.each { |builder| builder.new.drop_view! }
  end

  builders_changed = []
  sorted_builders.each do |builder|
    changed = builder.new.recreate_view_if_changed!(
      builders_changed: builders_changed,
      skip_dump_schema: skip_dump_schema
    )
    builders_changed << builder if changed
  end

  annotate_models!
end

.recreate_indexes!Object



67
68
69
70
# File 'lib/searchcraft/builder.rb', line 67

def recreate_indexes!
  sorted_builders = sort_builders_by_dependency
  sorted_builders.each { |builder| builder.new.recreate_indexes! }
end

.with_dataObject



31
32
33
# File 'lib/searchcraft/builder.rb', line 31

def with_data
  @with_no_data = false
end

.with_no_dataObject



27
28
29
# File 'lib/searchcraft/builder.rb', line 27

def with_no_data
  @with_no_data = true
end

.with_no_data?Boolean

Returns:

  • (Boolean)


35
36
37
# File 'lib/searchcraft/builder.rb', line 35

def with_no_data?
  @with_no_data
end

Instance Method Details

#create_view!Object



164
165
166
167
168
169
# File 'lib/searchcraft/builder.rb', line 164

def create_view!
  warn "Creating view/sequence/indexes for #{view_name}..." if SearchCraft.debug?
  create_sequence!
  sql_execute(view_sql)
  create_indexes!
end

#dependencies_ready?Boolean

Override if a Builder SQL has dependencies, such as extensions or text search config that are required first.

Returns:

  • (Boolean)


22
23
24
# File 'lib/searchcraft/builder.rb', line 22

def dependencies_ready?
  true
end

#drop_view!Object

Finds and drops all indexes and sequences on view, and then drops view



172
173
174
175
176
177
178
179
180
# File 'lib/searchcraft/builder.rb', line 172

def drop_view!
  puts "Dropping view/sequence for #{view_name}..." if SearchCraft.debug?
  sql_execute("DROP MATERIALIZED VIEW IF EXISTS #{view_name} CASCADE;")

  sql_execute("DROP SEQUENCE IF EXISTS #{view_id_sequence_name};")

  warn "Updating ViewHashStore for #{self.class.name}" if SearchCraft.debug?
  SearchCraft::ViewHashStore.reset!(builder: self)
end

#recreate_indexes!Object

TODO: what if indexes didn’t change?



183
184
185
186
# File 'lib/searchcraft/builder.rb', line 183

def recreate_indexes!
  drop_indexes!
  create_indexes!
end

#recreate_view_if_changed!(builders_changed: [], skip_dump_schema: false) ⇒ Object

If missing or changed, drop and create view Returns false if no change required



134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/searchcraft/builder.rb', line 134

def recreate_view_if_changed!(builders_changed: [], skip_dump_schema: false)
  if SearchCraft.debug?
    warn "#{self.class.name}#recreate_view_if_changed!"
    warn "  builders_changed: #{builders_changed.map(&:name).join(", ")}" if builders_changed.any?
  end
  return unless dependencies_ready?

  @@dependencies ||= {}
  dependencies_changed = (@@dependencies[self.class.name] || []) & builders_changed.map(&:name)
  return false unless dependencies_changed.any? ||
    SearchCraft::ViewHashStore.changed?(builder: self)

  if SearchCraft.debug?
    if !SearchCraft::ViewHashStore.exists?(builder: self)
      warn "Creating #{view_name} because it doesn't yet exist"
    elsif dependencies_changed.any?
      warn "Recreating #{view_name} because dependencies changed: #{dependencies_changed.join(" ")}"
    else
      warn "Recreating #{view_name} because SQL changed"
    end
  end

  drop_view!
  create_view!
  update_hash_store!
  dump_schema! unless skip_dump_schema

  true
end

#view_indexesObject

After materialized view created, do you need indexes on its columns?



122
123
124
# File 'lib/searchcraft/builder.rb', line 122

def view_indexes
  {}
end

#view_nameObject

Pluralized table name of class



189
190
191
# File 'lib/searchcraft/builder.rb', line 189

def view_name
  base_sql_name
end

#view_scopeObject

Subclass must implement view_scope or view_select_sql

Raises:

  • (NotImplementedError)


9
10
11
# File 'lib/searchcraft/builder.rb', line 9

def view_scope
  raise NotImplementedError, "Subclass must implement view_scope or view_select_sql"
end

#view_select_sqlObject

By default, assumes subclass implements view_scope to return an ActiveRecord::Relation. Alternately, override view_select_sql to return a SQL string.



16
17
18
# File 'lib/searchcraft/builder.rb', line 16

def view_select_sql
  @_view_select_sql ||= view_scope.to_sql
end

#view_sqlObject

Produces the SQL that will create the materialized view



113
114
115
116
117
118
119
# File 'lib/searchcraft/builder.rb', line 113

def view_sql
  # remove trailing ; from view_sql
  inner_sql = view_select_sql.gsub(/;\s*$/, "")

  with_data = self.class.with_no_data? ? "WITH NO DATA" : "WITH DATA"
  "CREATE MATERIALIZED VIEW #{view_name} AS (#{inner_sql}) #{with_data};"
end

#view_sql_hashObject

To indicate if view has changed, we store a hash of the SQL used to create it TODO: include the indexes SQL too



128
129
130
# File 'lib/searchcraft/builder.rb', line 128

def view_sql_hash
  Digest::SHA256.hexdigest(view_sql)
end