litestack 0.3.0 → 0.4.2

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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +3 -0
  3. data/BENCHMARKS.md +34 -7
  4. data/CHANGELOG.md +21 -0
  5. data/Gemfile +1 -5
  6. data/Gemfile.lock +92 -0
  7. data/README.md +120 -6
  8. data/ROADMAP.md +45 -0
  9. data/Rakefile +3 -1
  10. data/WHYLITESTACK.md +1 -1
  11. data/assets/litecache_metrics.png +0 -0
  12. data/assets/litedb_metrics.png +0 -0
  13. data/assets/litemetric_logo_teal.png +0 -0
  14. data/assets/litesearch_logo_teal.png +0 -0
  15. data/bench/bench.rb +17 -10
  16. data/bench/bench_cache_rails.rb +10 -13
  17. data/bench/bench_cache_raw.rb +17 -22
  18. data/bench/bench_jobs_rails.rb +19 -13
  19. data/bench/bench_jobs_raw.rb +17 -10
  20. data/bench/bench_queue.rb +4 -6
  21. data/bench/rails_job.rb +5 -7
  22. data/bench/skjob.rb +4 -4
  23. data/bench/uljob.rb +6 -6
  24. data/lib/action_cable/subscription_adapter/litecable.rb +5 -8
  25. data/lib/active_job/queue_adapters/litejob_adapter.rb +6 -8
  26. data/lib/active_record/connection_adapters/litedb_adapter.rb +65 -75
  27. data/lib/active_support/cache/litecache.rb +38 -41
  28. data/lib/generators/litestack/install/install_generator.rb +3 -3
  29. data/lib/generators/litestack/install/templates/database.yml +7 -1
  30. data/lib/litestack/liteboard/liteboard.rb +269 -149
  31. data/lib/litestack/litecable.rb +44 -40
  32. data/lib/litestack/litecable.sql.yml +22 -11
  33. data/lib/litestack/litecache.rb +80 -89
  34. data/lib/litestack/litecache.sql.yml +81 -22
  35. data/lib/litestack/litecache.yml +1 -1
  36. data/lib/litestack/litedb.rb +39 -38
  37. data/lib/litestack/litejob.rb +31 -31
  38. data/lib/litestack/litejobqueue.rb +107 -106
  39. data/lib/litestack/litemetric.rb +83 -95
  40. data/lib/litestack/litemetric.sql.yml +244 -234
  41. data/lib/litestack/litemetric_collector.sql.yml +38 -41
  42. data/lib/litestack/litequeue.rb +39 -41
  43. data/lib/litestack/litequeue.sql.yml +39 -31
  44. data/lib/litestack/litescheduler.rb +84 -0
  45. data/lib/litestack/litesearch/index.rb +260 -0
  46. data/lib/litestack/litesearch/model.rb +179 -0
  47. data/lib/litestack/litesearch/schema.rb +190 -0
  48. data/lib/litestack/litesearch/schema_adapters/backed_adapter.rb +143 -0
  49. data/lib/litestack/litesearch/schema_adapters/basic_adapter.rb +137 -0
  50. data/lib/litestack/litesearch/schema_adapters/contentless_adapter.rb +14 -0
  51. data/lib/litestack/litesearch/schema_adapters/standalone_adapter.rb +31 -0
  52. data/lib/litestack/litesearch/schema_adapters.rb +4 -0
  53. data/lib/litestack/litesearch.rb +34 -0
  54. data/lib/litestack/litesupport.rb +85 -186
  55. data/lib/litestack/railtie.rb +1 -1
  56. data/lib/litestack/version.rb +2 -2
  57. data/lib/litestack.rb +7 -4
  58. data/lib/railties/rails/commands/dbconsole.rb +11 -15
  59. data/lib/sequel/adapters/litedb.rb +18 -22
  60. data/lib/sequel/adapters/shared/litedb.rb +168 -168
  61. data/scripts/build_metrics.rb +91 -0
  62. data/scripts/test_cable.rb +30 -0
  63. data/scripts/test_job_retry.rb +33 -0
  64. data/scripts/test_metrics.rb +60 -0
  65. data/template.rb +2 -2
  66. metadata +112 -7
@@ -0,0 +1,179 @@
1
+ module Litesearch::Model
2
+ def self.included(klass)
3
+ klass.include InstanceMethods
4
+ klass.extend ClassMethods
5
+ klass.attribute :search_rank, :float if klass.respond_to? :attribute
6
+ if !defined?(Sequel::Model).nil? && klass.ancestors.include?(Sequel::Model)
7
+ klass.include Litesearch::Model::SequelInstanceMethods
8
+ klass.extend Litesearch::Model::SequelClassMethods
9
+ Sequel::Model.extend Litesearch::Model::BaseClassMethods
10
+ elsif !defined?(ActiveRecord::Base).nil? && klass.ancestors.include?(ActiveRecord::Base)
11
+ klass.include Litesearch::Model::ActiveRecordInstanceMethods
12
+ klass.extend Litesearch::Model::ActiveRecordClassMethods
13
+ ActiveRecord::Base.extend Litesearch::Model::BaseClassMethods
14
+ end
15
+ end
16
+
17
+ module BaseClassMethods
18
+ def search_models
19
+ @@models ||= {}
20
+ end
21
+ end
22
+
23
+ module InstanceMethods
24
+ def similar(limit=10)
25
+ conn = self.class.get_connection
26
+ idx = conn.search_index(self.class.send(:index_name))
27
+ r_a_h = conn.results_as_hash
28
+ conn.results_as_hash = true
29
+ rs = idx.similar(id, limit)
30
+ conn.results_as_hash = r_a_h
31
+ result = []
32
+ rs.each do |row|
33
+ obj = self.class.fetch_row(row["id"])
34
+ obj.search_rank = row["search_rank"]
35
+ result << obj
36
+ end
37
+ result
38
+ end
39
+
40
+ end
41
+
42
+ module ClassMethods
43
+ def litesearch
44
+ idx = get_connection.search_index(index_name) do |schema|
45
+ schema.type :backed
46
+ schema.table table_name.to_sym
47
+ yield schema
48
+ schema.post_init
49
+ @schema = schema # save the schema
50
+ end
51
+ if !defined?(Sequel::Model).nil? && ancestors.include?(Sequel::Model)
52
+ Sequel::Model.search_models[name] = self
53
+ elsif !defined?(ActiveRecord::Base).nil? && ancestors.include?(ActiveRecord::Base)
54
+ ActiveRecord::Base.search_models[name] = self
55
+ end
56
+ idx
57
+ end
58
+
59
+ def rebuild_index!
60
+ get_connection.search_index(index_name).rebuild!
61
+ end
62
+
63
+ def drop_index!
64
+ get_connection.search_index(index_name).drop!
65
+ end
66
+
67
+ def search_all(term, options = {})
68
+ options[:offset] ||= 0
69
+ options[:limit] ||= 25
70
+ options[:term] = term
71
+ selects = []
72
+ if (models = options[:models])
73
+ models_hash = {}
74
+ models.each do |model|
75
+ models_hash[model.name] = model
76
+ end
77
+ else
78
+ models_hash = search_models
79
+ end
80
+ # remove the models from the options hash before passing it ot the query
81
+ options.delete(:models)
82
+ models_hash.each do |name, klass|
83
+ selects << "SELECT '#{name}' AS model, rowid, -rank AS search_rank FROM #{index_name_for_table(klass.table_name)}(:term)"
84
+ end
85
+ conn = get_connection
86
+ sql = selects.join(" UNION ") << " ORDER BY search_rank DESC LIMIT :limit OFFSET :offset"
87
+ result = []
88
+ rs = conn.query(sql, options) # , options[:limit], options[:offset])
89
+ rs.each_hash do |row|
90
+ obj = models_hash[row["model"]].fetch_row(row["rowid"])
91
+ obj.search_rank = row["search_rank"]
92
+ result << obj
93
+ end
94
+ rs.close
95
+ result
96
+ end
97
+
98
+ def index_name
99
+ "#{table_name}_search_idx"
100
+ end
101
+
102
+ def index_name_for_table(table)
103
+ "#{table}_search_idx"
104
+ end
105
+
106
+ # create a new instance of self with the row as an argument
107
+ def create_instance(row)
108
+ new(row)
109
+ end
110
+ end
111
+
112
+ module ActiveRecordInstanceMethods; end
113
+
114
+ module ActiveRecordClassMethods
115
+ def get_connection
116
+ connection.raw_connection
117
+ end
118
+
119
+ def fetch_row(id)
120
+ find(id)
121
+ end
122
+
123
+ def search(term)
124
+ self.select(
125
+ "#{table_name}.*"
126
+ ).joins(
127
+ "INNER JOIN #{index_name} ON #{table_name}.id = #{index_name}.rowid AND rank != 0 AND #{index_name} MATCH ", Arel.sql("'#{term}'")
128
+ ).select(
129
+ "-#{index_name}.rank AS search_rank"
130
+ ).order(
131
+ Arel.sql("#{index_name}.rank")
132
+ )
133
+ end
134
+
135
+ def create_instance(row)
136
+ instantiate(row)
137
+ end
138
+ end
139
+
140
+ module SequelInstanceMethods
141
+ def search_rank
142
+ @values[:search_rank]
143
+ end
144
+
145
+ def search_rank=(rank)
146
+ @values[:search_rank] = rank
147
+ end
148
+ end
149
+
150
+ module SequelClassMethods
151
+ def fetch_row(id)
152
+ self[id]
153
+ end
154
+
155
+ def get_connection
156
+ db.instance_variable_get(:@raw_db)
157
+ end
158
+
159
+ def search(term)
160
+ dataset.select(
161
+ Sequel.lit("#{table_name}.*, -#{index_name}.rank AS search_rank")
162
+ ).inner_join(
163
+ Sequel.lit("#{index_name}(:term) ON #{table_name}.id = #{index_name}.rowid AND rank != 0", {term: term})
164
+ ).order(
165
+ Sequel.lit("rank")
166
+ )
167
+ end
168
+
169
+ def create_instance(row)
170
+ # we need to convert keys to symbols first!
171
+ row.keys.each do |k|
172
+ next if k.is_a? Symbol
173
+ row[k.to_sym] = row[k]
174
+ row.delete(k)
175
+ end
176
+ call(row)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,190 @@
1
+ require_relative "./schema_adapters"
2
+
3
+ class Litesearch::Schema
4
+ TOKENIZERS = {
5
+ porter: "porter unicode61 remove_diacritics 2",
6
+ unicode: "unicode61 remove_diacritics 2",
7
+ ascii: "ascii",
8
+ trigram: "trigram"
9
+ }
10
+
11
+ INDEX_TYPES = {
12
+ standalone: Litesearch::Schema::StandaloneAdapter,
13
+ contentless: Litesearch::Schema::ContentlessAdapter,
14
+ backed: Litesearch::Schema::BackedAdapter
15
+ }
16
+
17
+ DEFAULT_SCHEMA = {
18
+ name: nil,
19
+ type: :standalone,
20
+ fields: nil,
21
+ table: nil,
22
+ filter_column: nil,
23
+ tokenizer: :porter,
24
+ auto_create: true,
25
+ auto_modify: true,
26
+ rebuild_on_create: false,
27
+ rebuild_on_modify: false
28
+ }
29
+
30
+ attr_accessor :schema
31
+
32
+ def initialize(schema = {})
33
+ @schema = schema # DEFAULT_SCHEMA.merge(schema)
34
+ @schema[:fields] = {} unless @schema[:fields]
35
+ end
36
+
37
+ # schema definition API
38
+ def name(new_name)
39
+ @schema[:name] = new_name
40
+ end
41
+
42
+ def type(new_type)
43
+ raise "Unknown index type" if INDEX_TYPES[new_type].nil?
44
+ @schema[:type] = new_type
45
+ end
46
+
47
+ def table(table_name)
48
+ @schema[:table] = table_name
49
+ end
50
+
51
+ def fields(field_names)
52
+ field_names.each { |f| field f }
53
+ end
54
+
55
+ def field(name, attributes = {})
56
+ name = name.to_s.downcase.to_sym
57
+ attributes = {weight: 1}.merge(attributes).select { |k, v| allowed_attributes.include?(k) } # only allow attributes we know, to ease schema comparison later
58
+ @schema[:fields][name] = attributes
59
+ end
60
+
61
+ def tokenizer(new_tokenizer)
62
+ raise "Unknown tokenizer" if TOKENIZERS[new_tokenizer].nil?
63
+ @schema[:tokenizer] = new_tokenizer
64
+ end
65
+
66
+ def filter_column(filter_column)
67
+ @schema[:filter_column] = filter_column
68
+ end
69
+
70
+ def auto_create(boolean)
71
+ @schema[:auto_create] = boolean
72
+ end
73
+
74
+ def auto_modify(boolean)
75
+ @schema[:auto_modify] = boolean
76
+ end
77
+
78
+ def rebuild_on_create(boolean)
79
+ @schema[:rebuild_on_create] = boolean
80
+ end
81
+
82
+ def rebuild_on_modify(boolean)
83
+ @schema[:rebuild_on_modify] = boolean
84
+ end
85
+
86
+ def post_init
87
+ @schema = DEFAULT_SCHEMA.merge(@schema)
88
+ end
89
+
90
+ # schema sql generation API
91
+
92
+ def sql_for(method, *args)
93
+ adapter.sql_for(method, *args)
94
+ end
95
+
96
+ # schema data structure API
97
+ def get(key)
98
+ @schema[key]
99
+ end
100
+
101
+ def get_field(name)
102
+ @schema[:fields][name]
103
+ end
104
+
105
+ def adapter
106
+ @adapter ||= INDEX_TYPES[@schema[:type]].new(@schema)
107
+ end
108
+
109
+ def reset_sql
110
+ adapter.generate_sql
111
+ end
112
+
113
+ def order_fields(old_schema)
114
+ adapter.order_fields(old_schema)
115
+ end
116
+
117
+ # should we do this at the schema objects level?
118
+ def compare(other_schema)
119
+ other_schema = other_schema.schema
120
+ # are the schemas identical?
121
+ # 1 - same fields?
122
+ [:type, :tokenizer, :name, :table].each do |key|
123
+ other_schema[key] = @schema[key] if other_schema[key].nil?
124
+ end
125
+ if @schema[:type] != other_schema[:type]
126
+ raise Litesearch::SchemaChangeException.new "Cannot change the index type, please drop the index before creating it again with the new type"
127
+ end
128
+ changes = {tokenizer: @schema[:tokenizer] != other_schema[:tokenizer], table: @schema[:table] != other_schema[:table], removed_fields_count: 0, filter_column: @schema[:filter_column] != other_schema[:filter_column]}
129
+ # check tokenizer changes
130
+ if changes[:tokenizer] && !other_schema[:rebuild_on_modify]
131
+ raise Litesearch::SchemaChangeException.new "Cannot change the tokenizer without an index rebuild!"
132
+ end
133
+
134
+ # check field changes
135
+ keys = @schema[:fields].keys.sort
136
+ other_keys = other_schema[:fields].keys.sort
137
+
138
+ extra_keys = other_keys - keys
139
+ extra_keys.each do |key|
140
+ if other_schema[:fields][key][:weight] == 0
141
+ other_schema[:fields].delete(key)
142
+ end
143
+ end
144
+
145
+ other_keys = other_schema[:fields].keys.sort
146
+
147
+ changes[:fields] = keys != other_keys # only acceptable change is adding extra fields
148
+ changes[:extra_fields_count] = other_keys.count - keys.count
149
+ # check for missing fields (please note that adding fields can work without a rebuild)
150
+ if keys - other_keys != []
151
+ raise Litesearch::SchemaChangeException.new "Missing fields from existing schema, they have to exist with weight zero until the next rebuild!"
152
+ end
153
+
154
+ # check field weights
155
+ weights = keys.collect { |key| @schema[:fields][key][:weight] }
156
+ other_weights = other_keys.collect { |key| other_schema[:fields][key][:weight] }
157
+ changes[:weights] = weights != other_weights # will always be true if fields are added
158
+ if (removed_count = other_weights.count { |w| w == 0 }) > 0
159
+ changes[:removed_fields_count] = removed_count
160
+ end
161
+ # check field attributes, only backed tables have attributes
162
+ attrs = keys.collect do |key|
163
+ f = @schema[:fields][key].dup
164
+ f.delete(:weight)
165
+ f.select { |k, v| allowed_attributes.include? k }
166
+ end
167
+ other_attrs = other_keys.collect do |key|
168
+ f = other_schema[:fields][key].dup
169
+ f.delete(:weight)
170
+ f.select { |k, v| allowed_attributes.include? k }
171
+ end
172
+ changes[:attributes] if other_attrs != attrs # this means that we will need to redefine the triggers if any are there and also the table definition if needed
173
+
174
+ # return the changes
175
+ changes
176
+ end
177
+
178
+ def clean
179
+ removable = @schema[:fields].select { |name, f| f[:weight] == 0 }.collect { |name, f| name }
180
+ removable.each { |name| @schema[:fields].delete(name) }
181
+ end
182
+
183
+ def allowed_attributes
184
+ [:weight, :col, :target]
185
+ end
186
+ end
187
+
188
+ class Litesearch::SchemaException < StandardError; end
189
+
190
+ class Litesearch::SchemaChangeException < StandardError; end
@@ -0,0 +1,143 @@
1
+ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
2
+ private
3
+
4
+ def table
5
+ @schema[:table]
6
+ end
7
+
8
+ def generate_sql
9
+ super
10
+ @sql[:rebuild] = :rebuild_sql
11
+ @sql[:drop_primary_triggers] = :drop_primary_triggers_sql
12
+ @sql[:drop_secondary_triggers] = :drop_secondary_triggers_sql
13
+ @sql[:create_primary_triggers] = :create_primary_triggers_sql
14
+ @sql[:create_secondary_triggers] = :create_secondary_triggers_sql
15
+ end
16
+
17
+ def drop_primary_triggers_sql
18
+ <<~SQL
19
+ DROP TRIGGER IF EXISTS #{name}_insert;
20
+ DROP TRIGGER IF EXISTS #{name}_update;
21
+ DROP TRIGGER IF EXISTS #{name}_update_not;
22
+ DROP TRIGGER IF EXISTS #{name}_delete;
23
+ SQL
24
+ end
25
+
26
+ def create_primary_triggers_sql(active = false)
27
+ when_stmt = "TRUE"
28
+ cols = active_cols_names
29
+ if (filter = @schema[:filter_column])
30
+ when_stmt = "NEW.#{filter} = TRUE"
31
+ cols << filter
32
+ end
33
+
34
+ <<-SQL
35
+ CREATE TRIGGER #{name}_insert AFTER INSERT ON #{table} WHEN #{when_stmt} BEGIN
36
+ INSERT OR REPLACE INTO #{name}(rowid, #{active_field_names.join(", ")}) VALUES (NEW.rowid, #{trigger_cols_sql});
37
+ END;
38
+ CREATE TRIGGER #{name}_update AFTER UPDATE OF #{cols.join(", ")} ON #{table} WHEN #{when_stmt} BEGIN
39
+ INSERT OR REPLACE INTO #{name}(rowid, #{active_field_names.join(", ")}) VALUES (NEW.rowid, #{trigger_cols_sql});
40
+ END;
41
+ CREATE TRIGGER #{name}_update_not AFTER UPDATE OF #{cols.join(", ")} ON #{table} WHEN NOT #{when_stmt} BEGIN
42
+ DELETE FROM #{name} WHERE rowid = NEW.rowid;
43
+ END;
44
+ CREATE TRIGGER #{name}_delete AFTER DELETE ON #{table} BEGIN
45
+ DELETE FROM #{name} WHERE rowid = OLD.id;
46
+ END;
47
+ SQL
48
+ end
49
+
50
+ def drop_secondary_trigger_sql(target_table, target_col, col)
51
+ "DROP TRIGGER IF EXISTS #{target_table}_#{target_col}_#{col}_#{name}_update;"
52
+ end
53
+
54
+ def create_secondary_trigger_sql(target_table, target_col, col)
55
+ <<~SQL
56
+ CREATE TRIGGER #{target_table}_#{target_col}_#{col}_#{name}_update AFTER UPDATE OF #{target_col} ON #{target_table} BEGIN
57
+ #{rebuild_sql} AND #{table}.#{col} = NEW.id;
58
+ END;
59
+ SQL
60
+ end
61
+
62
+ def drop_secondary_triggers_sql
63
+ sql = ""
64
+ @schema[:fields].each do |name, field|
65
+ if field[:trigger_sql]
66
+ sql << drop_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
67
+ end
68
+ end
69
+ sql.empty? ? nil : sql
70
+ end
71
+
72
+ def create_secondary_triggers_sql
73
+ sql = ""
74
+ @schema[:fields].each do |name, field|
75
+ if field[:trigger_sql]
76
+ sql << create_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
77
+ end
78
+ end
79
+ sql.empty? ? nil : sql
80
+ end
81
+
82
+ def rebuild_sql
83
+ conditions = ""
84
+ jcs = join_conditions_sql
85
+ fs = filter_sql
86
+ conditions = " ON #{jcs} #{fs}" unless jcs.empty? && fs.empty?
87
+ "INSERT OR REPLACE INTO #{name}(rowid, #{active_field_names.join(", ")}) SELECT #{table}.id, #{select_cols_sql} FROM #{join_tables_sql} #{conditions}"
88
+ end
89
+
90
+ def enrich_schema
91
+ @schema[:fields].each do |name, field|
92
+ if field[:target] && !field[:target].start_with?("#{table}.")
93
+ field[:target] = field[:target].downcase
94
+ target_table, target_col = field[:target].split(".")
95
+ field[:col] = "#{name}_id".to_sym unless field[:col]
96
+ field[:target_table] = target_table.to_sym
97
+ field[:target_col] = target_col.to_sym
98
+ field[:sql] = "(SELECT #{field[:target_col]} FROM #{field[:target_table]} WHERE id = NEW.#{field[:col]})"
99
+ field[:trigger_sql] = true # create_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
100
+ field[:target_table_alias] = "#{field[:target_table]}_#{name}"
101
+ else
102
+ field[:col] = name unless field[:col]
103
+ field[:sql] = field[:col]
104
+ field[:target_table] = @schema[:table]
105
+ field[:target] = "#{@schema[:table]}.#{field[:sql]}"
106
+ end
107
+ end
108
+ end
109
+
110
+ def filter_sql
111
+ sql = ""
112
+ sql << " AND #{@schema[:filter_column]} = TRUE " if @schema[:filter_column]
113
+ sql
114
+ end
115
+
116
+ def trigger_cols_sql
117
+ active_fields.collect do |name, field|
118
+ field[:trigger_sql] ? field[:sql] : "NEW.#{field[:sql]}"
119
+ end.join(", ")
120
+ end
121
+
122
+ def select_cols_sql
123
+ active_fields.collect do |name, field|
124
+ (!field[:trigger_sql].nil?) ? "#{field[:target_table_alias]}.#{field[:target_col]}" : field[:target]
125
+ end.join(", ")
126
+ end
127
+
128
+ def join_tables_sql
129
+ tables = [@schema[:table]]
130
+ active_fields.each do |name, field|
131
+ tables << "#{field[:target_table]} AS #{field[:target_table_alias]}" if field[:trigger_sql]
132
+ end
133
+ tables.uniq.join(", ")
134
+ end
135
+
136
+ def join_conditions_sql
137
+ conditions = []
138
+ active_fields.each do |name, field|
139
+ conditions << "#{field[:target_table_alias]}.id = #{@schema[:table]}.#{field[:col]}" if field[:trigger_sql]
140
+ end
141
+ conditions.join(" AND ")
142
+ end
143
+ end
@@ -0,0 +1,137 @@
1
+ class Litesearch::Schema::BasicAdapter
2
+ def initialize(schema)
3
+ @schema = schema
4
+ @sql = {}
5
+ enrich_schema
6
+ generate_sql
7
+ end
8
+
9
+ def name
10
+ @schema[:name]
11
+ end
12
+
13
+ def table
14
+ @schema[:table]
15
+ end
16
+
17
+ def fields
18
+ @schema[:fields]
19
+ end
20
+
21
+ def field_names
22
+ @schema[:fields].keys
23
+ end
24
+
25
+ def active_fields
26
+ @schema[:fields].select { |k, v| v[:weight] != 0 }
27
+ end
28
+
29
+ def active_field_names
30
+ active_fields.keys
31
+ end
32
+
33
+ def active_cols_names
34
+ active_fields.collect { |k, v| v[:col] }
35
+ end
36
+
37
+ def weights
38
+ @schema[:fields].values.collect { |v| v[:weight].to_f }
39
+ end
40
+
41
+ def active_weights
42
+ active_fields.values.collect { |v| v[:weight].to_f }
43
+ end
44
+
45
+ def tokenizer_sql
46
+ Litesearch::Schema::TOKENIZERS[@schema[:tokenizer]]
47
+ end
48
+
49
+ def order_fields(old_schema)
50
+ new_fields = {}
51
+ old_field_names = old_schema.schema[:fields].keys
52
+ old_field_names.each do |name|
53
+ new_fields[name] = @schema[:fields].delete(name)
54
+ end
55
+ missing_field_names = field_names - old_field_names
56
+ missing_field_names.each do |name|
57
+ new_fields[name] = @schema[:fields].delete(name)
58
+ end
59
+ @schema[:fields] = new_fields # this should be in order now
60
+ generate_sql
61
+ enrich_schema
62
+ end
63
+
64
+ def sql_for(method, *args)
65
+ if (sql = @sql[method])
66
+ if sql.is_a? String
67
+ sql
68
+ elsif sql.is_a? Proc
69
+ sql.call(*args)
70
+ elsif sql.is_a? Symbol
71
+ send(sql, *args)
72
+ elsif sql.is_a? Litesearch::SchemaChangeException
73
+ raise sql
74
+ end
75
+ end
76
+ end
77
+
78
+ def generate_sql
79
+ @sql[:create_index] = :create_index_sql
80
+ @sql[:create_vocab_tables] = :create_vocab_tables_sql
81
+ @sql[:insert] = "INSERT OR REPLACE INTO #{name}(rowid, #{active_col_names_sql}) VALUES (:id, #{active_col_names_var_sql}) RETURNING rowid"
82
+ @sql[:delete] = "DELETE FROM #{name} WHERE rowid = :id"
83
+ @sql[:count] = "SELECT count(*) FROM #{name}(:term)"
84
+ @sql[:count_all] = "SELECT count(*) FROM #{name}"
85
+ @sql[:delete_all] = "DELETE FROM #{name}"
86
+ @sql[:drop] = "DROP TABLE #{name}"
87
+ @sql[:expand_data] = "UPDATE #{name}_data SET block = block || zeroblob(:length) WHERE id = 1"
88
+ @sql[:expand_docsize] = "UPDATE #{name}_docsize SET sz = sz || zeroblob(:length)"
89
+ @sql[:ranks] = :ranks_sql
90
+ @sql[:set_config_value] = "INSERT OR REPLACE INTO #{name}_config(k, v) VALUES (:key, :value)"
91
+ @sql[:get_config_value] = "SELECT v FROM #{name}_config WHERE k = :key"
92
+ @sql[:search] = "SELECT rowid AS id, -rank AS search_rank FROM #{name}(:term) WHERE rank !=0 ORDER BY rank LIMIT :limit OFFSET :offset"
93
+ @sql[:similarity_terms] = "SELECT DISTINCT term FROM #{name}_instance WHERE doc = :id AND FLOOR(term) IS NULL AND LENGTH(term) > 2 AND NOT instr(term, ' ') AND NOT instr(term, '-') AND NOT instr(term, ':') AND NOT instr(term, '#') AND NOT instr(term, '_') LIMIT 15"
94
+ @sql[:similarity_query] = "SELECT group_concat('\"' || term || '\"', ' OR ') FROM #{name}_row WHERE term IN (#{@sql[:similarity_terms]})"
95
+ @sql[:similarity_search] = "SELECT rowid AS id, -rank AS search_rank FROM #{name}(:term) WHERE rowid != :id ORDER BY rank LIMIT :limit"
96
+ @sql[:similar] = "SELECT rowid AS id, -rank AS search_rank FROM #{name} WHERE #{name} = (#{@sql[:similarity_query]}) AND rowid != :id ORDER BY rank LIMIT :limit"
97
+ @sql[:update_index] = "UPDATE sqlite_schema SET sql = :sql WHERE name = '#{name}'"
98
+ @sql[:update_content_table] = "UPDATE sqlite_schema SET sql = :sql WHERE name = '#{name}_content'"
99
+ end
100
+
101
+ private
102
+
103
+ def create_vocab_tables_sql
104
+ <<~SQL
105
+ CREATE VIRTUAL TABLE IF NOT EXISTS #{name}_row USING fts5vocab(#{name}, row);
106
+ CREATE VIRTUAL TABLE IF NOT EXISTS #{name}_instance USING fts5vocab(#{name}, instance);
107
+ SQL
108
+ end
109
+
110
+ def ranks_sql(active = false)
111
+ weights_sql = if active
112
+ weights.join(", ")
113
+ else
114
+ active_weights.join(", ")
115
+ end
116
+ "INSERT INTO #{name}(#{name}, rank) VALUES ('rank', 'bm25(#{weights_sql})')"
117
+ end
118
+
119
+ def active_col_names_sql
120
+ active_field_names.join(", ")
121
+ end
122
+
123
+ def active_col_names_var_sql
124
+ ":#{active_field_names.join(", :")}"
125
+ end
126
+
127
+ def col_names_sql
128
+ field_names.join(", ")
129
+ end
130
+
131
+ def col_names_var_sql
132
+ ":#{field_names.join(", :")}"
133
+ end
134
+
135
+ def enrich_schema
136
+ end
137
+ end
@@ -0,0 +1,14 @@
1
+ class Litesearch::Schema::ContentlessAdapter < Litesearch::Schema::BasicAdapter
2
+ private
3
+
4
+ def generate_sql
5
+ super
6
+ # @sql[:rebuild_index] = Litesearch::SchemaChangeException.new("You cannot rebuild a contentless index")
7
+ # @sql[:rebuild] = Litesearch::SchemaChangeException.new("You cannot rebuild a contentless index")
8
+ end
9
+
10
+ def create_index_sql(active = false)
11
+ col_names = active ? active_col_names_sql : col_names_sql
12
+ "CREATE VIRTUAL TABLE #{name} USING FTS5(#{col_names}, content='', contentless_delete=1, tokenize='#{tokenizer_sql}')"
13
+ end
14
+ end
@@ -0,0 +1,31 @@
1
+ class Litesearch::Schema::StandaloneAdapter < Litesearch::Schema::BasicAdapter
2
+ def generate_sql
3
+ super
4
+ @sql[:move_content] = "ALTER TABLE #{name}_content RENAME TO #{name}_content_temp"
5
+ @sql[:adjust_temp_content] = "UPDATE sqlite_schema SET sql (SELECT sql FROM sqlite_schema WHERE name = '#{name}_content') WHERE name = #{name}_content_temp"
6
+ @sql[:restore_content] = "ALTER TABLE #{name}_content_temp RENAME TO #{name}_content"
7
+ @sql[:rebuild] = "INSERT INTO #{name}(#{name}) VALUES ('rebuild')"
8
+ @sql[:similar] = "SELECT rowid AS id, *, -rank AS search_rank FROM #{name} WHERE #{name} = (#{@sql[:similarity_query]}) AND rowid != :id ORDER BY rank LIMIT :limit"
9
+ @sql[:drop_content_table] = "DROP TABLE #{name}_content"
10
+ @sql[:drop_content_col] = :drop_content_col_sql
11
+ @sql[:create_content_table] = :create_content_table_sql
12
+ @sql[:search] = "SELECT rowid AS id, *, -rank AS search_rank FROM #{name}(:term) WHERE rank !=0 ORDER BY rank LIMIT :limit OFFSET :offset"
13
+ end
14
+
15
+ private
16
+
17
+ def create_index_sql(active = false)
18
+ col_names = active ? active_col_names_sql : col_names_sql
19
+ "CREATE VIRTUAL TABLE #{name} USING FTS5(#{col_names}, tokenize='#{tokenizer_sql}')"
20
+ end
21
+
22
+ def drop_content_col_sql(col_index)
23
+ "ALTER TABLE #{name}_content DROP COLUMN c#{col_index}"
24
+ end
25
+
26
+ def create_content_table_sql(count)
27
+ cols = []
28
+ count.times { |i| cols << "c#{i}" }
29
+ "CREATE TABLE #{name}_content(id INTEGER PRIMARY KEY, #{cols.join(", ")})"
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ require_relative "./schema_adapters/basic_adapter"
2
+ require_relative "./schema_adapters/standalone_adapter"
3
+ require_relative "./schema_adapters/contentless_adapter"
4
+ require_relative "./schema_adapters/backed_adapter"