litestack 0.3.0 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
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"