full_search 0.4.8 → 0.4.9

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: 834fb641e471174fd832f6c0e825d2486dbb94985e35f5ab95f5fc068c08ece1
4
+ data.tar.gz: c5d19d2c11fdf7eaa95c2af1fdc2a39d2db9d170e248591e29bba107249f691e
5
5
  SHA512:
6
- metadata.gz: bf77188446fed43a0e9f69826f85ced628f5960d493cbd96d721730e9fd9d8c92ffe5c58412aee47e81feb01adca00e2e7b7641f85ea01fe2c98fcaaca05b5f1
7
- data.tar.gz: 89e0b8dc7ee2b44391deb5c339e2b048c836431a5997ab19c281285b1fef79fcfe5ad4fea02793a672643899e08e5276a12dbadacf51cb3411853d389c6fbbf7
6
+ metadata.gz: 7bd69680899b8b405d3de387d1c3eafbb3e98ce0b08f937f7a9cac5282e5cb32cd24b96e7c20019b470937c817f419d2094ca48e9e9dd2a1fc49b9a0502295d7
7
+ data.tar.gz: 50f29a99547ce19b2a3c5098a992476dbd5b0423e6d251768a4ec355db482f81bd94b0e23e13e0008c671c8d35911e0394cd716cf278f1808de52f94f5daaa71
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
 
@@ -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.9"
5
5
  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.9
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: []