litestack 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +3 -1
  3. data/BENCHMARKS.md +3 -3
  4. data/CAVEATS.md +20 -0
  5. data/CHANGELOG.md +19 -4
  6. data/FILESYSTEMS.md +55 -0
  7. data/Gemfile +2 -0
  8. data/README.md +2 -2
  9. data/ROADMAP.md +6 -6
  10. data/assets/litestack_advantage.png +0 -0
  11. data/bench/bench.rb +2 -0
  12. data/bench/bench_cache_rails.rb +14 -17
  13. data/bench/bench_cache_raw.rb +18 -15
  14. data/bench/bench_jobs_rails.rb +3 -3
  15. data/bench/bench_jobs_raw.rb +3 -3
  16. data/bin/liteboard +16 -14
  17. data/gemfiles/rails70.gemfile +5 -0
  18. data/gemfiles/rails71.gemfile +5 -0
  19. data/gemfiles/rails71.gemfile.lock +264 -0
  20. data/lib/active_job/queue_adapters/litejob_adapter.rb +11 -3
  21. data/lib/active_record/connection_adapters/litedb_adapter.rb +17 -7
  22. data/lib/active_support/cache/litecache.rb +25 -15
  23. data/lib/generators/litestack/install/install_generator.rb +2 -2
  24. data/lib/litestack/liteboard/liteboard.rb +15 -19
  25. data/lib/litestack/liteboard/views/litecable.erb +1 -1
  26. data/lib/litestack/litecable.rb +1 -1
  27. data/lib/litestack/litecache.rb +23 -25
  28. data/lib/litestack/litedb.rb +5 -1
  29. data/lib/litestack/litejob.rb +1 -1
  30. data/lib/litestack/litejobqueue.rb +24 -14
  31. data/lib/litestack/litemetric.rb +7 -6
  32. data/lib/litestack/litequeue.rb +17 -2
  33. data/lib/litestack/litequeue.sql.yml +38 -5
  34. data/lib/litestack/litescheduler.rb +1 -2
  35. data/lib/litestack/litesearch/index.rb +11 -10
  36. data/lib/litestack/litesearch/model.rb +61 -3
  37. data/lib/litestack/litesearch/schema.rb +7 -2
  38. data/lib/litestack/litesearch/schema_adapters/backed_adapter.rb +69 -25
  39. data/lib/litestack/litesearch/schema_adapters.rb +4 -4
  40. data/lib/litestack/litesearch.rb +2 -2
  41. data/lib/litestack/litesupport.rb +7 -6
  42. data/lib/litestack/railtie.rb +4 -2
  43. data/lib/litestack/version.rb +1 -1
  44. data/lib/litestack.rb +15 -15
  45. data/lib/sequel/adapters/litedb.rb +9 -1
  46. data/lib/sequel/adapters/shared/litedb.rb +2 -2
  47. data/scripts/build_metrics.rb +2 -2
  48. data/scripts/test_cable.rb +1 -1
  49. metadata +95 -59
@@ -1,6 +1,6 @@
1
1
  # frozen_stringe_literal: true
2
2
 
3
- require_relative "./litejobqueue"
3
+ require_relative "litejobqueue"
4
4
 
5
5
  ##
6
6
  # Litejob is a Ruby module that enables seamless integration of the Litejobqueue job queueing system into Ruby applications. By including the Litejob module in a class and implementing the #perform method, developers can easily enqueue and process jobs asynchronously.
@@ -1,7 +1,7 @@
1
1
  # frozen_stringe_literal: true
2
2
 
3
- require_relative "./litequeue"
4
- require_relative "./litemetric"
3
+ require_relative "litequeue"
4
+ require_relative "litemetric"
5
5
 
6
6
  ##
7
7
  # Litejobqueue is a job queueing and processing system designed for Ruby applications. It is built on top of SQLite, which is an embedded relational database management system that is #lightweight and fast.
@@ -15,7 +15,7 @@ class Litejobqueue < Litequeue
15
15
  include Litemetric::Measurable
16
16
 
17
17
  # the default options for the job queue
18
- # can be overriden by passing new options in a hash
18
+ # can be overridden by passing new options in a hash
19
19
  # to Litejobqueue.new, it will also be then passed to the underlying Litequeue object
20
20
  # config_path: "./litejob.yml" -> were to find the configuration file (if any)
21
21
  # path: "./db/queue.db"
@@ -136,20 +136,28 @@ class Litejobqueue < Litequeue
136
136
  # @@queue = nil
137
137
  close
138
138
  end
139
-
139
+
140
140
  private
141
+
142
+ def prepare_search_options(opts)
143
+ sql_opts = super(opts)
144
+ sql_opts[:klass] = opts[:klass]
145
+ sql_opts[:params] = opts[:params]
146
+ sql_opts
147
+ end
141
148
 
142
149
  def exit_callback
143
150
  @running = false # stop all workers
144
- return unless @jobs_in_flight > 0
145
- puts "--- Litejob detected an exit, cleaning up"
146
- index = 0
147
- while @jobs_in_flight > 0 && index < 30 # 3 seconds grace period for jobs to finish
148
- puts "--- Waiting for #{@jobs_in_flight} jobs to finish"
149
- sleep 0.1
150
- index += 1
151
+ if @jobs_in_flight > 0
152
+ puts "--- Litejob detected an exit, cleaning up"
153
+ index = 0
154
+ while @jobs_in_flight > 0 && index < 30 # 3 seconds grace period for jobs to finish
155
+ puts "--- Waiting for #{@jobs_in_flight} jobs to finish"
156
+ sleep 0.1
157
+ index += 1
158
+ end
159
+ puts " --- Exiting with #{@jobs_in_flight} jobs in flight"
151
160
  end
152
- puts " --- Exiting with #{@jobs_in_flight} jobs in flight"
153
161
  end
154
162
 
155
163
  def setup
@@ -179,6 +187,8 @@ class Litejobqueue < Litequeue
179
187
 
180
188
  # create a worker according to environment
181
189
  def create_worker
190
+ # temporarily stop this feature until a better solution is implemented
191
+ #return if defined?(Rails) && !defined?(Rails::Server)
182
192
  Litescheduler.spawn do
183
193
  worker_sleep_index = 0
184
194
  while @running
@@ -186,7 +196,7 @@ class Litejobqueue < Litequeue
186
196
  @queues.each do |priority, queues| # iterate through the levels
187
197
  queues.each do |queue, spawns| # iterate through the queues in the level
188
198
  batched = 0
189
-
199
+
190
200
  while (batched < priority) && (payload = pop(queue, 1)) # fearlessly use the same queue object
191
201
  capture(:dequeue, queue)
192
202
  processed += 1
@@ -201,7 +211,7 @@ class Litejobqueue < Litequeue
201
211
  end
202
212
  if processed == 0
203
213
  sleep @options[:sleep_intervals][worker_sleep_index]
204
- worker_sleep_index += 1 if worker_sleep_index < @options[:sleep_intervals].length - 1
214
+ worker_sleep_index += 1 if worker_sleep_index < (@options[:sleep_intervals].length - 1)
205
215
  else
206
216
  worker_sleep_index = 0 # reset the index
207
217
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "singleton"
4
4
 
5
- require_relative "./litesupport"
5
+ require_relative "litesupport"
6
6
 
7
7
  # this class is a singleton
8
8
  # and should remain so
@@ -148,10 +148,11 @@ class Litemetric
148
148
  end
149
149
 
150
150
  def exit_callback
151
- return unless @collector.count > 0
152
- warn "--- Litemetric detected an exit, flushing metrics"
153
151
  @running = false
154
- @collector.flush
152
+ if @collector.count > 0
153
+ warn "--- Litemetric detected an exit, flushing metrics"
154
+ @collector.flush
155
+ end
155
156
  end
156
157
 
157
158
  def setup
@@ -203,8 +204,8 @@ class Litemetric
203
204
  def create_snapshotter
204
205
  Litescheduler.spawn do
205
206
  while @running
206
- sleep @litemetric.options[:snapshot_interval]
207
207
  capture_snapshot
208
+ sleep @litemetric.options[:snapshot_interval]
208
209
  end
209
210
  end
210
211
  end
@@ -275,7 +276,7 @@ class Litemetric
275
276
  def capture_single_key(topic, event, key, value, time = nil)
276
277
  run_stmt(:capture_event, topic.to_s, event.to_s, key.to_s, time, 1, value)
277
278
  end
278
-
279
+
279
280
  def count
280
281
  run_stmt(:event_count)[0][0]
281
282
  end
@@ -13,7 +13,7 @@ require_relative "litesupport"
13
13
 
14
14
  class Litequeue
15
15
  # the default options for the queue
16
- # can be overriden by passing new options in a hash
16
+ # can be overridden by passing new options in a hash
17
17
  # to Litequeue.new
18
18
  # path: "./queue.db"
19
19
  # mmap_size: 128 * 1024 * 1024 -> 128MB to be held in memory
@@ -80,7 +80,7 @@ class Litequeue
80
80
  run_sql("DELETE FROM queue WHERE iif(?1 IS NOT NULL, name = ?1, TRUE)", queue)
81
81
  end
82
82
 
83
- # returns a count of entries in all queues, or if a queue name is given, reutrns the count of entries in that queue
83
+ # returns a count of entries in all queues, or if a queue name is given, returns the count of entries in that queue
84
84
  def count(queue = nil)
85
85
  run_sql("SELECT count(*) FROM queue WHERE iif(?1 IS NOT NULL, name = ?1, TRUE)", queue)[0][0]
86
86
  end
@@ -111,9 +111,24 @@ class Litequeue
111
111
  queues: queues
112
112
  }
113
113
  end
114
+
115
+ def find(opts = {})
116
+ run_stmt(:search, prepare_search_options(opts))
117
+ end
114
118
 
115
119
  private
116
120
 
121
+ def prepare_search_options(opts)
122
+ sql_opts = {}
123
+ sql_opts[:fire_at_from] = opts[:fire_at][0] rescue nil
124
+ sql_opts[:fire_at_to] = opts[:fire_at][1] rescue nil
125
+ sql_opts[:created_at_from] = opts[:created_at][0] rescue nil
126
+ sql_opts[:created_at_to] = opts[:created_at][1] rescue nil
127
+ sql_opts[:name] = opts[:queue]
128
+ sql_opts[:dir] = opts[:dir] == :desc ? -1 : 1
129
+ sql_opts
130
+ end
131
+
117
132
  def create_connection
118
133
  super("#{__dir__}/litequeue.sql.yml") do |conn|
119
134
  conn.wal_autocheckpoint = 10000
@@ -4,7 +4,7 @@ schema:
4
4
  CREATE TABLE IF NOT EXISTS queue(
5
5
  id TEXT PRIMARY KEY DEFAULT(hex(randomblob(32))) NOT NULL ON CONFLICT REPLACE,
6
6
  name TEXT DEFAULT('default') NOT NULL ON CONFLICT REPLACE,
7
- fire_at INTEGER DEFAULT(unixepoch()) NOT NULL ON CONFLICT REPLACE,
7
+ fire_at REAL DEFAULT(unixepoch('subsec')) NOT NULL ON CONFLICT REPLACE,
8
8
  value TEXT,
9
9
  created_at INTEGER DEFAULT(unixepoch()) NOT NULL ON CONFLICT REPLACE
10
10
  ) WITHOUT ROWID;
@@ -16,21 +16,36 @@ stmts:
16
16
 
17
17
  push: >
18
18
  INSERT INTO queue(id, name, fire_at, value)
19
- VALUES (hex(randomblob(32)), $1, (unixepoch() + $2), $3)
19
+ VALUES (hex(randomblob(32)), $1, (unixepoch('subsec') + $2), $3)
20
20
  RETURNING id, name;
21
21
 
22
22
  repush: >
23
23
  INSERT INTO queue(id, name, fire_at, value)
24
- VALUES (?, ?, (unixepoch() + ?), ?)
24
+ VALUES (?, ?, (unixepoch('subsec') + ?), ?)
25
25
  RETURNING name;
26
26
 
27
+ pop_in_place: >
28
+ UPDATE queue
29
+ SET
30
+ name = '->:' || name,
31
+ fire_at = unixepoch('subsec')
32
+ WHERE (name, fire_at, id)
33
+ IN (
34
+ SELECT name, fire_at, id FROM queue
35
+ WHERE name = ifnull($1, 'default')
36
+ AND fire_at <= (unixepoch('subsec'))
37
+ ORDER BY fire_at ASC
38
+ LIMIT ifnull($2, 1)
39
+ )
40
+ RETURNING id, value;
41
+
27
42
  pop: >
28
43
  DELETE FROM queue
29
44
  WHERE (name, fire_at, id)
30
45
  IN (
31
46
  SELECT name, fire_at, id FROM queue
32
47
  WHERE name = ifnull($1, 'default')
33
- AND fire_at <= (unixepoch())
48
+ AND fire_at <= (unixepoch('subsec'))
34
49
  ORDER BY fire_at ASC
35
50
  LIMIT ifnull($2, 1)
36
51
  )
@@ -40,7 +55,25 @@ stmts:
40
55
  DELETE FROM queue
41
56
  WHERE id = $1
42
57
  RETURNING value;
43
-
58
+
59
+ search: >
60
+ SELECT *, :params, value ->> '$.params', value ->> '$.params' LIKE '%'||'three_second'||'%' FROM queue
61
+ WHERE
62
+ name = ifnull(:name, name)
63
+ AND
64
+ iif(:fire_at_from, fire_at >= :fire_at_from, true)
65
+ AND
66
+ iif(:fire_at_to, fire_at <= :fire_at_to, true)
67
+ AND
68
+ iif(:created_at_from, created_at >= :created_at_from, true)
69
+ AND
70
+ iif(:created_at_to, created_at <= :created_at_to, true)
71
+ AND
72
+ iif(:klass IS NOT NULL, value ->> '$.klass' LIKE '%'||:klass||'%', true)
73
+ AND
74
+ iif(:params IS NOT NULL, value ->> '$.params' LIKE '%'||:params||'%', true)
75
+ ORDER BY created_at * :dir;
76
+
44
77
  info: >
45
78
  SELECT
46
79
  name,
@@ -82,9 +82,8 @@ module Litescheduler
82
82
  @@mutex ||= Mutex.new
83
83
  end
84
84
 
85
- private
86
-
87
85
  def self.fiber_backed?
88
86
  backend == :fiber || backend == :polyphony
89
87
  end
88
+ private_class_method :fiber_backed?
90
89
  end
@@ -1,5 +1,5 @@
1
1
  require "oj"
2
- require_relative "./schema"
2
+ require_relative "schema"
3
3
 
4
4
  class Litesearch::Index
5
5
  DEFAULT_SEARCH_OPTIONS = {limit: 25, offset: 0}
@@ -71,7 +71,7 @@ class Litesearch::Index
71
71
 
72
72
  def rebuild!
73
73
  if @db.transaction_active?
74
- do_rebuild
74
+ do_rebuild
75
75
  else
76
76
  @db.transaction(:immediate) { do_rebuild }
77
77
  end
@@ -102,19 +102,20 @@ class Litesearch::Index
102
102
  rs = @stmts[:search].execute(term, options[:limit], options[:offset])
103
103
  generate_results(rs)
104
104
  end
105
-
106
- def similar(id, limit=10)
105
+
106
+ def similar(id, limit = 10)
107
107
  # pp term = @db.execute(@schema.sql_for(:similarity_query), id)
108
- if @schema.schema[:tokenizer] == :trigram
108
+ rs = if @schema.schema[:tokenizer] == :trigram
109
109
  # just use the normal similarity approach for now
110
110
  # need to recondisder that for trigram indexes later
111
- rs = @stmts[:similar].execute(id, limit)
111
+ @stmts[:similar].execute(id, limit) # standard:disable Style/IdenticalConditionalBranches
112
112
  else
113
- rs = @stmts[:similar].execute(id, limit)
113
+ @stmts[:similar].execute(id, limit) # standard:disable Style/IdenticalConditionalBranches
114
114
  end
115
+
115
116
  generate_results(rs)
116
117
  end
117
-
118
+
118
119
  def clear!
119
120
  @stmts[:delete_all].execute!(id)
120
121
  end
@@ -140,7 +141,7 @@ class Litesearch::Index
140
141
  else
141
142
  result = rs.to_a
142
143
  end
143
- result
144
+ result
144
145
  end
145
146
 
146
147
  def exists?(name)
@@ -175,7 +176,7 @@ class Litesearch::Index
175
176
 
176
177
  def do_modify(new_schema)
177
178
  changes = @schema.compare(new_schema)
178
- # ensure the new schema maintains feild order
179
+ # ensure the new schema maintains field order
179
180
  new_schema.order_fields(@schema)
180
181
  # with the changes object decide what needs to be done to the schema
181
182
  requires_schema_change = false
@@ -11,6 +11,7 @@ module Litesearch::Model
11
11
  klass.include Litesearch::Model::ActiveRecordInstanceMethods
12
12
  klass.extend Litesearch::Model::ActiveRecordClassMethods
13
13
  ActiveRecord::Base.extend Litesearch::Model::BaseClassMethods
14
+ Litesearch::Schema.prepend Litesearch::Model::ActiveRecordSchemaMethods
14
15
  end
15
16
  end
16
17
 
@@ -21,7 +22,7 @@ module Litesearch::Model
21
22
  end
22
23
 
23
24
  module InstanceMethods
24
- def similar(limit=10)
25
+ def similar(limit = 10)
25
26
  conn = self.class.get_connection
26
27
  idx = conn.search_index(self.class.send(:index_name))
27
28
  r_a_h = conn.results_as_hash
@@ -36,14 +37,38 @@ module Litesearch::Model
36
37
  end
37
38
  result
38
39
  end
39
-
40
40
  end
41
41
 
42
42
  module ClassMethods
43
+
43
44
  def litesearch
45
+ # it is possible that this code is running when there is no table created yet
46
+ if !defined?(ActiveRecord::Base).nil? && ancestors.include?(ActiveRecord::Base)
47
+ unless table_exists?
48
+ # capture the schema block
49
+ @schema = ::Litesearch::Schema.new
50
+ @schema.model_class = self if @schema.respond_to? :model_class
51
+ @schema.type :backed
52
+ @schema.table table_name.to_sym
53
+ yield @schema
54
+ @schema.post_init
55
+ @schema_not_created = true
56
+ after_initialize do
57
+ if self.class.instance_variable_get(:@schema_not_created)
58
+ self.class.get_connection.search_index(self.class.index_name) do |schema|
59
+ @schema.model_class = self.class if @schema.respond_to? :model_class
60
+ schema.merge(self.class.instance_variable_get(:@schema))
61
+ end
62
+ self.class.instance_variable_set(:@schema_not_created, false)
63
+ end
64
+ end
65
+ return nil
66
+ end
67
+ end
44
68
  idx = get_connection.search_index(index_name) do |schema|
45
69
  schema.type :backed
46
70
  schema.table table_name.to_sym
71
+ schema.model_class = self if schema.respond_to? :model_class
47
72
  yield schema
48
73
  schema.post_init
49
74
  @schema = schema # save the schema
@@ -77,7 +102,7 @@ module Litesearch::Model
77
102
  else
78
103
  models_hash = search_models
79
104
  end
80
- # remove the models from the options hash before passing it ot the query
105
+ # remove the models from the options hash before passing it to the query
81
106
  options.delete(:models)
82
107
  models_hash.each do |name, klass|
83
108
  selects << "SELECT '#{name}' AS model, rowid, -rank AS search_rank FROM #{index_name_for_table(klass.table_name)}(:term)"
@@ -109,6 +134,32 @@ module Litesearch::Model
109
134
  end
110
135
  end
111
136
 
137
+ module ActiveRecordSchemaMethods
138
+
139
+ attr_accessor :model_class
140
+
141
+ def field(name, attributes = {})
142
+ keys = attributes.keys
143
+ if keys.include?(:action_text) || keys.include?(:rich_text)
144
+ attributes[:source] = "#{ActionText::RichText.table_name}.body" rescue "action_text_rich_texts.body"
145
+ attributes[:reference] = :record_id
146
+ attributes[:conditions] = { record_type: model_class.name }
147
+ attributes[:target] = nil
148
+ elsif keys.include? :as
149
+ attributes[:source] = attributes[:target] unless attributes[:source]
150
+ attributes[:reference] = "#{attributes[:as]}_id"
151
+ attributes[:conditions] = {"#{attributes[:as]}_type".to_sym => model_class.name }
152
+ attributes[:target] = nil
153
+ end
154
+ super(name, attributes)
155
+ end
156
+
157
+ def allowed_attributes
158
+ super + [:polymorphic, :as, :action_text]
159
+ end
160
+
161
+ end
162
+
112
163
  module ActiveRecordInstanceMethods; end
113
164
 
114
165
  module ActiveRecordClassMethods
@@ -121,6 +172,13 @@ module Litesearch::Model
121
172
  end
122
173
 
123
174
  def search(term)
175
+ if @schema_not_created
176
+ get_connection.search_index(index_name) do |schema|
177
+ schema.merge(@schema)
178
+ schema.model_class = self if schema.respond_to? :model_class
179
+ end
180
+ @schema_not_created = false
181
+ end
124
182
  self.select(
125
183
  "#{table_name}.*"
126
184
  ).joins(
@@ -1,6 +1,7 @@
1
- require_relative "./schema_adapters"
1
+ require_relative "schema_adapters"
2
2
 
3
3
  class Litesearch::Schema
4
+
4
5
  TOKENIZERS = {
5
6
  porter: "porter unicode61 remove_diacritics 2",
6
7
  unicode: "unicode61 remove_diacritics 2",
@@ -34,6 +35,10 @@ class Litesearch::Schema
34
35
  @schema[:fields] = {} unless @schema[:fields]
35
36
  end
36
37
 
38
+ def merge(other_schema)
39
+ @schema.merge!(other_schema.schema)
40
+ end
41
+
37
42
  # schema definition API
38
43
  def name(new_name)
39
44
  @schema[:name] = new_name
@@ -181,7 +186,7 @@ class Litesearch::Schema
181
186
  end
182
187
 
183
188
  def allowed_attributes
184
- [:weight, :col, :target]
189
+ [:weight, :col, :target, :source, :conditions, :reference]
185
190
  end
186
191
  end
187
192
 
@@ -25,20 +25,24 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
25
25
 
26
26
  def create_primary_triggers_sql(active = false)
27
27
  when_stmt = "TRUE"
28
- cols = active_cols_names
28
+ cols = active_cols_names.select{|n| !n.nil?}
29
29
  if (filter = @schema[:filter_column])
30
30
  when_stmt = "NEW.#{filter} = TRUE"
31
31
  cols << filter
32
32
  end
33
+ update_filter = ""
34
+ if cols.length > 0
35
+ " OF #{cols.join(', ')} "
36
+ end
33
37
 
34
38
  <<-SQL
35
39
  CREATE TRIGGER #{name}_insert AFTER INSERT ON #{table} WHEN #{when_stmt} BEGIN
36
40
  INSERT OR REPLACE INTO #{name}(rowid, #{active_field_names.join(", ")}) VALUES (NEW.rowid, #{trigger_cols_sql});
37
41
  END;
38
- CREATE TRIGGER #{name}_update AFTER UPDATE OF #{cols.join(", ")} ON #{table} WHEN #{when_stmt} BEGIN
42
+ CREATE TRIGGER #{name}_update AFTER UPDATE #{update_filter} ON #{table} WHEN #{when_stmt} BEGIN
39
43
  INSERT OR REPLACE INTO #{name}(rowid, #{active_field_names.join(", ")}) VALUES (NEW.rowid, #{trigger_cols_sql});
40
44
  END;
41
- CREATE TRIGGER #{name}_update_not AFTER UPDATE OF #{cols.join(", ")} ON #{table} WHEN NOT #{when_stmt} BEGIN
45
+ CREATE TRIGGER #{name}_update_not AFTER UPDATE #{update_filter} ON #{table} WHEN NOT #{when_stmt} BEGIN
42
46
  DELETE FROM #{name} WHERE rowid = NEW.rowid;
43
47
  END;
44
48
  CREATE TRIGGER #{name}_delete AFTER DELETE ON #{table} BEGIN
@@ -51,19 +55,41 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
51
55
  "DROP TRIGGER IF EXISTS #{target_table}_#{target_col}_#{col}_#{name}_update;"
52
56
  end
53
57
 
58
+ def drop_secondary_trigger_poly_sql(target_table, target_col, col)
59
+ "DROP TRIGGER IF EXISTS #{target_table}_#{target_col}_#{name}_update;"
60
+ end
61
+
54
62
  def create_secondary_trigger_sql(target_table, target_col, col)
55
63
  <<~SQL
56
- CREATE TRIGGER #{target_table}_#{target_col}_#{col}_#{name}_update AFTER UPDATE OF #{target_col} ON #{target_table} BEGIN
64
+ CREATE TRIGGER IF NOT EXISTS #{target_table}_#{target_col}_#{col}_#{name}_update AFTER UPDATE OF #{target_col} ON #{target_table} BEGIN
57
65
  #{rebuild_sql} AND #{table}.#{col} = NEW.id;
58
66
  END;
59
67
  SQL
60
68
  end
61
69
 
70
+ def create_secondary_trigger_poly_sql(target_table, target_col, col, conditions)
71
+ conditions_sql = conditions.collect{|k, v| "NEW.#{k} = '#{v}'"}.join(" AND ")
72
+ <<~SQL
73
+ CREATE TRIGGER IF NOT EXISTS #{target_table}_#{target_col}_#{name}_insert AFTER INSERT ON #{target_table} WHEN #{conditions_sql} BEGIN
74
+ #{rebuild_sql};
75
+ END;
76
+ CREATE TRIGGER IF NOT EXISTS #{target_table}_#{target_col}_#{name}_update AFTER UPDATE ON #{target_table} WHEN #{conditions_sql} BEGIN
77
+ #{rebuild_sql};
78
+ END;
79
+ SQL
80
+ end
81
+
82
+
83
+
62
84
  def drop_secondary_triggers_sql
63
85
  sql = ""
64
86
  @schema[:fields].each do |name, field|
65
87
  if field[:trigger_sql]
66
- sql << drop_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
88
+ if field[:col]
89
+ sql << drop_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
90
+ elsif field[:source]
91
+ sql << drop_secondary_trigger_poly_sql(field[:target_table], field[:target_col], name)
92
+ end
67
93
  end
68
94
  end
69
95
  sql.empty? ? nil : sql
@@ -73,18 +99,18 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
73
99
  sql = ""
74
100
  @schema[:fields].each do |name, field|
75
101
  if field[:trigger_sql]
76
- sql << create_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
102
+ if field[:col]
103
+ sql << create_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
104
+ elsif field[:source]
105
+ sql << create_secondary_trigger_poly_sql(field[:target_table], field[:target_col], name, field[:conditions])
106
+ end
77
107
  end
78
108
  end
79
109
  sql.empty? ? nil : sql
80
110
  end
81
111
 
82
112
  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}"
113
+ "INSERT OR REPLACE INTO #{name}(rowid, #{active_field_names.join(", ")}) SELECT #{table}.id, #{select_cols_sql} FROM #{joins_sql} #{filter_sql}"
88
114
  end
89
115
 
90
116
  def enrich_schema
@@ -92,12 +118,23 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
92
118
  if field[:target] && !field[:target].start_with?("#{table}.")
93
119
  field[:target] = field[:target].downcase
94
120
  target_table, target_col = field[:target].split(".")
95
- field[:col] = "#{name}_id".to_sym unless field[:col]
121
+ field[:col] = :"#{name}_id" unless field[:col]
96
122
  field[:target_table] = target_table.to_sym
97
123
  field[:target_col] = target_col.to_sym
98
124
  field[:sql] = "(SELECT #{field[:target_col]} FROM #{field[:target_table]} WHERE id = NEW.#{field[:col]})"
99
125
  field[:trigger_sql] = true # create_secondary_trigger_sql(field[:target_table], field[:target_col], field[:col])
100
126
  field[:target_table_alias] = "#{field[:target_table]}_#{name}"
127
+ elsif field[:source]
128
+ field[:source] = field[:source].downcase
129
+ target_table, target_col = field[:source].split(".")
130
+ field[:target_table] = target_table.to_sym
131
+ field[:target_col] = target_col.to_sym
132
+ field[:conditions_sql] = field[:conditions].collect{|k, v| "#{k} = '#{v}'"}.join(" AND ") if field[:conditions]
133
+ field[:sql] = "SELECT #{field[:target_col]} FROM #{field[:target_table]} WHERE #{field[:reference]} = NEW.id"
134
+ field[:sql] += " AND #{field[:conditions_sql]}" if field[:conditions_sql]
135
+ field[:sql] = "(#{field[:sql]})"
136
+ field[:trigger_sql] = true
137
+ field[:target_table_alias] = "#{field[:target_table]}_#{name}"
101
138
  else
102
139
  field[:col] = name unless field[:col]
103
140
  field[:sql] = field[:col]
@@ -109,7 +146,7 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
109
146
 
110
147
  def filter_sql
111
148
  sql = ""
112
- sql << " AND #{@schema[:filter_column]} = TRUE " if @schema[:filter_column]
149
+ sql << " WHERE #{@schema[:filter_column]} = TRUE " if @schema[:filter_column]
113
150
  sql
114
151
  end
115
152
 
@@ -124,20 +161,27 @@ class Litesearch::Schema::BackedAdapter < Litesearch::Schema::ContentlessAdapter
124
161
  (!field[:trigger_sql].nil?) ? "#{field[:target_table_alias]}.#{field[:target_col]}" : field[:target]
125
162
  end.join(", ")
126
163
  end
127
-
128
- def join_tables_sql
129
- tables = [@schema[:table]]
164
+
165
+ def joins_sql
166
+ joins = [@schema[:table]]
130
167
  active_fields.each do |name, field|
131
- tables << "#{field[:target_table]} AS #{field[:target_table_alias]}" if field[:trigger_sql]
168
+ if field[:trigger_sql]
169
+ join_table = ""
170
+ join_table << "#{field[:target_table]} AS #{field[:target_table_alias]} ON "
171
+ if field[:col]
172
+ join_table << "#{field[:target_table_alias]}.id = #{@schema[:table]}.#{field[:col]}" if field[:col]
173
+ elsif field[:source]
174
+ join_table << "#{field[:target_table_alias]}.#{field[:reference]} = #{@schema[:table]}.id"
175
+ if field[:conditions]
176
+ join_table << " AND "
177
+ join_table << field[:conditions].collect{|k, v| "#{field[:target_table_alias]}.#{k} = '#{v}'"}.join(" AND ")
178
+ end
179
+ end
180
+ joins << join_table
181
+ end
132
182
  end
133
- tables.uniq.join(", ")
183
+ joins.join(" LEFT JOIN ")
134
184
  end
135
185
 
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
186
+
143
187
  end
@@ -1,4 +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"
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"
@@ -4,8 +4,8 @@ module Litesearch
4
4
  class Schema; end
5
5
  end
6
6
 
7
- require_relative "./litesearch/index"
8
- require_relative "./litesearch/model"
7
+ require_relative "litesearch/index"
8
+ require_relative "litesearch/model"
9
9
 
10
10
  module Litesearch
11
11
  def litesearch_index_cache