full_search 0.4.8 → 0.4.10

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: f50c2f24f4b4929d3da0744d93e33a0b6c5a6ba3743c4542612c6e1007687691
4
- data.tar.gz: d497857811078992322ad4cd081e41aa476878c0950b71a2ec5bcd7a76ff8db2
3
+ metadata.gz: b21fb40775574ba4e404c2409fb2b4a4a57095baeb2f4867ca52812391d31348
4
+ data.tar.gz: baf95666180a4702197d41deb90ea31a423c5f33a3aa90dc52d3709d5df44a3a
5
5
  SHA512:
6
- metadata.gz: bf77188446fed43a0e9f69826f85ced628f5960d493cbd96d721730e9fd9d8c92ffe5c58412aee47e81feb01adca00e2e7b7641f85ea01fe2c98fcaaca05b5f1
7
- data.tar.gz: 89e0b8dc7ee2b44391deb5c339e2b048c836431a5997ab19c281285b1fef79fcfe5ad4fea02793a672643899e08e5276a12dbadacf51cb3411853d389c6fbbf7
6
+ metadata.gz: a52f22fffe5a0751a1be3178e65d8ea51c08b789e8240017d9f036e8d407ff68c45deffd7dc99e1e3a5c68813448eab82f06ee38b103838f24dbb42df182485c
7
+ data.tar.gz: 4b791aea84de3a56fda5674e4a3e74af7cec5d53ca4e24d241c54c9f368bbc9c579cc8a22a62c46e4956f5a2892515a6be0b0d4f3aab77cb4cc5d34e55145fbd
data/README.md CHANGED
@@ -150,7 +150,9 @@ FTS indexes are SQLite virtual tables (`customers_fts`, `vehicles_fts`, etc.) th
150
150
  These two operations are often confused:
151
151
 
152
152
  - **Rebuild** (`full_search:rebuild` / `Customer.rebuild!`) — drops and recreates the FTS virtual table. Needed when the DSL changes (fields added/removed, tokenizer changed, etc.). The table is re-created from scratch, backfilled, triggers re-installed, and the index is optimized.
153
- - **Reindex** (`Customer.reindex!` / `FullSearch::Index.reindex_source_fields!`) — updates existing FTS rows with fresh values from computed `source:` fields only. The table structure is untouched. Database triggers cover regular column changes automatically; only Ruby-evaluated `source:` blocks need an explicit reindex.
153
+ - **Reindex** (`Customer.reindex!` / `Customer.reindex_ids([ids])` / `FullSearch::Index.reindex_source_fields!`) — updates existing FTS rows with fresh values from computed `source:` fields only. The table structure is untouched. Database triggers cover regular column changes automatically, and since v0.4.9 they **preserve** existing `source:` field values rather than wiping them to `''` on UPDATE. Only Ruby-evaluated `source:` blocks need an explicit reindex.
154
+
155
+ **Important:** raw SQL operations that bypass Rails callbacks (`update_all`, `update_column`, `insert_all`, `upsert_all`, `counter_cache`, `touch`) will not run the `after_save_commit` reindex hook. Use `Model.reindex_ids(ids)` after a callback-free `update_all` that changes data backing a `source:` field, or `Model.reindex!` after a bulk insert, to keep the FTS source fields in sync.
154
156
 
155
157
  ### Setup lifecycle
156
158
 
@@ -235,6 +237,21 @@ end
235
237
 
236
238
  When disabled, you must run `bin/rails full_search:prepare` after every `db:schema:load` to recreate the indexes.
237
239
 
240
+ `full_search` also ensures FTS tables exist before `db:schema:dump` reads the database and after `db:schema:load` finishes, so virtual tables are never silently dropped from `schema.rb` when models are registered but their tables haven't been created yet (e.g. development with `eager_load = false`). These hooks are gated by two config options, both on by default:
241
+
242
+ ```ruby
243
+ FullSearch.configure do |config|
244
+ # Run full_search:prepare before db:schema:dump so every registered
245
+ # FTS table is present in sqlite_master when the dumper reads it.
246
+ config.auto_prepare_on_schema_dump = true
247
+
248
+ # Create any FTS tables missing from schema.rb after db:schema:load.
249
+ config.auto_prepare_after_schema_load = true
250
+ end
251
+ ```
252
+
253
+ Set either to `false` if you prefer to run `bin/rails full_search:prepare` manually.
254
+
238
255
  ### Rake tasks
239
256
 
240
257
  | Task | Description |
@@ -5,7 +5,8 @@ module FullSearch
5
5
  attr_accessor :auto_rebuild_schema, :stale_query_behavior, :lock_rebuilds,
6
6
  :default_async_reindex, :default_async_source_reindex,
7
7
  :default_tokenizer, :auto_rebuild_on_stale_query, :min_like_prefix_length,
8
- :dump_schema_virtual_tables, :auto_rebuild_missing_tables
8
+ :dump_schema_virtual_tables, :auto_rebuild_missing_tables,
9
+ :auto_prepare_on_schema_dump, :auto_prepare_after_schema_load
9
10
 
10
11
  def initialize
11
12
  @auto_rebuild_schema = false
@@ -18,6 +19,8 @@ module FullSearch
18
19
  @min_like_prefix_length = FullSearch::Constants::DEFAULT_MIN_LIKE_PREFIX_LENGTH
19
20
  @dump_schema_virtual_tables = true
20
21
  @auto_rebuild_missing_tables = false
22
+ @auto_prepare_on_schema_dump = true
23
+ @auto_prepare_after_schema_load = true
21
24
  end
22
25
  end
23
26
 
@@ -452,24 +452,27 @@ module FullSearch
452
452
 
453
453
  when_clause = SoftDelete.active_update_clause(model)
454
454
 
455
+ update_assignments = update_assignments_sql(cols)
456
+
455
457
  if dsl.conditional_index?
456
458
  select_parts = cols.map { |c| column_ref(c, prefix: "new") }.join(", ")
457
459
  <<~SQL
458
460
  CREATE TRIGGER #{qt(trigger_names(model)[2])} AFTER UPDATE ON #{qt(model.table_name)} #{when_clause}
459
461
  BEGIN
460
- DELETE FROM #{fts_table} WHERE rowid = old.id;
462
+ DELETE FROM #{fts_table} WHERE rowid = old.id
463
+ AND NOT EXISTS (SELECT 1 FROM #{tbl} WHERE #{tbl}.id = old.id AND (#{dsl.index_if_sql}));
464
+ UPDATE #{fts_table} SET #{update_assignments} WHERE rowid = new.id
465
+ AND EXISTS (SELECT 1 FROM #{tbl} WHERE #{tbl}.id = new.id AND (#{dsl.index_if_sql}));
461
466
  INSERT INTO #{fts_table}(rowid, #{cols_str})
462
- SELECT new.id, #{select_parts} FROM #{tbl} WHERE #{tbl}.id = new.id AND (#{dsl.index_if_sql});
467
+ SELECT new.id, #{select_parts} FROM #{tbl} WHERE #{tbl}.id = new.id AND (#{dsl.index_if_sql})
468
+ AND NOT EXISTS (SELECT 1 FROM #{fts_table} WHERE #{fts_table}.rowid = new.id);
463
469
  END;
464
470
  SQL
465
471
  else
466
- values = cols.map { |c| column_ref(c, prefix: "new") }.join(", ")
467
472
  <<~SQL
468
473
  CREATE TRIGGER #{qt(trigger_names(model)[2])} AFTER UPDATE ON #{qt(model.table_name)} #{when_clause}
469
474
  BEGIN
470
- DELETE FROM #{fts_table} WHERE rowid = old.id;
471
- INSERT INTO #{fts_table}(rowid, #{cols_str})
472
- VALUES (new.id, #{values});
475
+ UPDATE #{fts_table} SET #{update_assignments} WHERE rowid = new.id;
473
476
  END;
474
477
  SQL
475
478
  end
@@ -562,24 +565,27 @@ module FullSearch
562
565
 
563
566
  when_clause = SoftDelete.active_update_clause(model)
564
567
 
568
+ update_assignments = update_assignments_sql(cols)
569
+
565
570
  if dsl.conditional_index?
566
571
  select_parts = cols.map { |c| column_ref(c, prefix: "new") }.join(", ")
567
572
  <<~SQL
568
573
  CREATE TRIGGER #{qt(trigram_trigger_names(model)[2])} AFTER UPDATE ON #{qt(model.table_name)} #{when_clause}
569
574
  BEGIN
570
- DELETE FROM #{trigram_table} WHERE rowid = old.id;
575
+ DELETE FROM #{trigram_table} WHERE rowid = old.id
576
+ AND NOT EXISTS (SELECT 1 FROM #{tbl} WHERE #{tbl}.id = old.id AND (#{dsl.index_if_sql}));
577
+ UPDATE #{trigram_table} SET #{update_assignments} WHERE rowid = new.id
578
+ AND EXISTS (SELECT 1 FROM #{tbl} WHERE #{tbl}.id = new.id AND (#{dsl.index_if_sql}));
571
579
  INSERT INTO #{trigram_table}(rowid, #{cols_str})
572
- SELECT new.id, #{select_parts} FROM #{tbl} WHERE #{tbl}.id = new.id AND (#{dsl.index_if_sql});
580
+ SELECT new.id, #{select_parts} FROM #{tbl} WHERE #{tbl}.id = new.id AND (#{dsl.index_if_sql})
581
+ AND NOT EXISTS (SELECT 1 FROM #{trigram_table} WHERE #{trigram_table}.rowid = new.id);
573
582
  END;
574
583
  SQL
575
584
  else
576
- values = cols.map { |c| column_ref(c, prefix: "new") }.join(", ")
577
585
  <<~SQL
578
586
  CREATE TRIGGER #{qt(trigram_trigger_names(model)[2])} AFTER UPDATE ON #{qt(model.table_name)} #{when_clause}
579
587
  BEGIN
580
- DELETE FROM #{trigram_table} WHERE rowid = old.id;
581
- INSERT INTO #{trigram_table}(rowid, #{cols_str})
582
- VALUES (new.id, #{values});
588
+ UPDATE #{trigram_table} SET #{update_assignments} WHERE rowid = new.id;
583
589
  END;
584
590
  SQL
585
591
  end
@@ -604,6 +610,28 @@ module FullSearch
604
610
  end
605
611
  end
606
612
 
613
+ # Builds `col = value` assignments for the AFTER UPDATE trigger. Source
614
+ # fields are deliberately excluded so a raw SQL UPDATE (e.g. a
615
+ # counter_cache or touch) does not wipe the previously computed source
616
+ # value back to an empty string. Source fields are refreshed later by the
617
+ # after_save_commit reindex callback on normal Rails saves.
618
+ #
619
+ # When every column is a source field (nothing to update), emit a valid
620
+ # no-op assignment so the generated trigger SQL stays syntactically valid.
621
+ def update_assignments_sql(cols)
622
+ assignments = cols.filter_map do |col|
623
+ if col.respond_to?(:unindexed?) && col.name == "indexed"
624
+ "#{qc(col.name)} = '1'"
625
+ elsif col.respond_to?(:source) && col.source
626
+ nil
627
+ else
628
+ "#{qc(column_name(col))} = new.#{qc(column_name(col))}"
629
+ end
630
+ end
631
+ assignments << "#{qc(column_name(cols.first))} = #{qc(column_name(cols.first))}" if assignments.empty?
632
+ assignments.join(", ")
633
+ end
634
+
607
635
  # NOTE: This lock prevents concurrent rebuilds within the same process/connection only.
608
636
  # For multi-process or multi-host deployments, run full_search:rebuild from a single
609
637
  # deployment step. See README.
@@ -40,6 +40,19 @@ module FullSearch
40
40
  FullSearch::Index.reindex_source_fields!(self)
41
41
  end
42
42
 
43
+ # Re-evaluates source: fields for a specific set of records and updates
44
+ # the FTS table in place. Use after bulk operations that bypass model
45
+ # callbacks (e.g. `update_all`, `insert_all`, `upsert_all`, `update_column`)
46
+ # so the source fields don't drift out of sync with the underlying rows.
47
+ def reindex_ids(ids)
48
+ source_ids = ids.to_a.map(&:to_i).reject(&:zero?)
49
+ return if source_ids.empty?
50
+
51
+ where(id: source_ids).find_each(batch_size: 500) do |record|
52
+ FullSearch::Callbacks.reindex_record!(record)
53
+ end
54
+ end
55
+
43
56
  def bulk_import(&block)
44
57
  FullSearch.bulk_import(self, &block)
45
58
  end
@@ -75,6 +88,13 @@ module FullSearch
75
88
 
76
89
  FullSearch::Model.evaluate_source(self, field)
77
90
  end
91
+
92
+ # Re-evaluates this record's source: fields and writes them to the FTS
93
+ # table immediately (synchronously). Use after a callback-free mutation
94
+ # (update_column, update_all, counter_cache/touch) to refresh the index.
95
+ def reindex_source_fields!
96
+ FullSearch::Callbacks.reindex_record!(self)
97
+ end
78
98
  end
79
99
  end
80
100
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FullSearch
4
- VERSION = "0.4.8"
4
+ VERSION = "0.4.10"
5
5
  end
@@ -16,6 +16,17 @@ FullSearch.configure do |config|
16
16
  # Bulk imports should use FullSearch.bulk_import(Model) { ... } to defer reindexing.
17
17
  config.default_async_source_reindex = true
18
18
 
19
+ # Ensure FTS tables exist before db:schema:dump reads the database, so
20
+ # virtual tables are never silently dropped from schema.rb. Set to false
21
+ # if you prefer to run `bin/rails full_search:prepare` manually before
22
+ # dumping the schema.
23
+ config.auto_prepare_on_schema_dump = true
24
+
25
+ # Create any FTS tables missing from schema.rb after db:schema:load.
26
+ # Set to false if you prefer to run `bin/rails full_search:prepare`
27
+ # manually after loading the schema.
28
+ config.auto_prepare_after_schema_load = true
29
+
19
30
  # Uncomment to exclude full_search virtual tables from db/schema.rb.
20
31
  # When false, you must run `bin/rails full_search:prepare` after db:schema:load.
21
32
  # config.dump_schema_virtual_tables = false
@@ -140,4 +140,43 @@ namespace :full_search do
140
140
  puts "[OK] All full_search indexes are healthy"
141
141
  end
142
142
  end
143
+
144
+ # Internal: ensure FTS tables exist before db:schema:dump reads
145
+ # sqlite_master. Gated by auto_prepare_on_schema_dump so apps that
146
+ # prefer manual control can opt out.
147
+ task auto_prepare_before_dump: :environment do
148
+ next unless FullSearch.config.auto_prepare_on_schema_dump
149
+
150
+ Rails.application.eager_load!
151
+ FullSearch.setup!
152
+ end
153
+
154
+ # Internal: create any FTS tables missing from schema.rb after
155
+ # db:schema:load. Gated by auto_prepare_after_schema_load.
156
+ task auto_prepare_after_load: :environment do
157
+ next unless FullSearch.config.auto_prepare_after_schema_load
158
+
159
+ Rails.application.eager_load!
160
+ FullSearch.setup!
161
+ end
162
+ end
163
+
164
+ # Ensure FTS tables exist before db:schema:dump reads sqlite_master,
165
+ # so virtual tables aren't silently dropped from schema.rb when models
166
+ # were registered but their FTS tables never got created (e.g. dev with
167
+ # eager_load off, or a fresh db:schema:load against an incomplete dump).
168
+ if Rake::Task.task_defined?("db:schema:dump")
169
+ Rake::Task["db:schema:dump"].enhance(["full_search:auto_prepare_before_dump"])
170
+ end
171
+
172
+ # Create any FTS tables missing from schema.rb after db:schema:load.
173
+ # schema.rb may be missing tables if it was dumped with
174
+ # auto_prepare_on_schema_dump = false, or if new searchable models were
175
+ # added since the last dump.
176
+ if Rake::Task.task_defined?("db:schema:load")
177
+ Rake::Task["db:schema:load"].enhance do
178
+ next unless FullSearch.config.auto_prepare_after_schema_load
179
+ Rails.application.eager_load!
180
+ FullSearch.setup!
181
+ end
143
182
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: full_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.8
4
+ version: 0.4.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben D'Angelo
@@ -189,7 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
189
  - !ruby/object:Gem::Version
190
190
  version: '2.0'
191
191
  requirements: []
192
- rubygems_version: 4.0.10
192
+ rubygems_version: 4.0.18
193
193
  specification_version: 4
194
194
  summary: SQLite FTS5 full-text search for Rails/ActiveRecord
195
195
  test_files: []