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
@@ -1,56 +1,53 @@
1
1
  schema:
2
2
  1:
3
-
4
3
  create_events: >
5
4
  CREATE TABLE IF NOT EXISTS local_events(
6
5
  topic TEXT NOT NULL,
7
- name TEXT DEFAULT('___') NOT NULL ON CONFLICT REPLACE,
8
- key TEXT DEFAULT('___') NOT NULL ON CONFLICT REPLACE,
9
- count INTEGER DEFAULT(0) NOT NULL ON CONFLICT REPLACE,
10
- value REAL,
11
- minimum REAL,
12
- maximum REAL,
13
- created_at INTEGER DEFAULT((unixepoch()/300*300)) NOT NULL ON CONFLICT REPLACE,
14
- resolution TEXT DEFAULT('minute') NOT NULL,
6
+ name TEXT DEFAULT('___') NOT NULL ON CONFLICT REPLACE,
7
+ key TEXT DEFAULT('___') NOT NULL ON CONFLICT REPLACE,
8
+ count INTEGER DEFAULT(0) NOT NULL ON CONFLICT REPLACE,
9
+ value REAL,
10
+ minimum REAL,
11
+ maximum REAL,
12
+ created_at INTEGER DEFAULT((unixepoch()/300*300)) NOT NULL ON CONFLICT REPLACE,
13
+ resolution TEXT DEFAULT('minute') NOT NULL,
15
14
  PRIMARY KEY(resolution, created_at, topic, name, key)
16
- ) STRICT;
15
+ ) STRICT;
17
16
 
18
17
  stmts:
19
-
20
18
  capture_event: >
21
- INSERT INTO local_events(topic, name, key, created_at, count, value, minimum, maximum)
22
- VALUES
23
- (?1, ?2, ?3, ?4, ?5, ?6, ?6, ?6),
19
+ INSERT INTO local_events(topic, name, key, created_at, count, value, minimum, maximum)
20
+ VALUES
21
+ (?1, ?2, ?3, ?4, ?5, ?6, ?6, ?6),
24
22
  (?1, ?2, '___', ?4, ?5, ?6, ?6, ?6),
25
- (?1, '___', '___', ?4, ?5, ?6, ?6, ?6)
26
- ON CONFLICT DO UPDATE
27
- SET
28
- count = count + EXCLUDED.count,
29
- value = value + EXCLUDED.value,
30
- minimum = min(minimum, EXCLUDED.minimum),
31
- maximum = max(maximum, EXCLUDED.maximum)
32
-
33
- migrate_events:
34
- INSERT INTO m.events(topic, name, key, created_at, count, value, minimum, maximum)
35
- SELECT topic, name, key, created_at, count, value, minimum, maximum
36
- FROM local_events
37
- ORDER BY resolution, created_at ASC, topic, name, key
23
+ (?1, '___', '___', ?4, ?5, ?6, ?6, ?6)
24
+ ON CONFLICT DO UPDATE
25
+ SET
26
+ count = count + EXCLUDED.count,
27
+ value = value + EXCLUDED.value,
28
+ minimum = min(minimum, EXCLUDED.minimum),
29
+ maximum = max(maximum, EXCLUDED.maximum);
30
+
31
+ migrate_events: >
32
+ INSERT INTO m.events(topic, name, key, created_at, count, value, minimum, maximum)
33
+ SELECT topic, name, key, created_at, count, value, minimum, maximum
34
+ FROM local_events
35
+ ORDER BY resolution, created_at ASC, topic, name, key
38
36
  LIMIT ?
39
- ON CONFLICT DO UPDATE
40
- SET
41
- count = count + EXCLUDED.count,
42
- value = value + EXCLUDED.value,
43
- minimum = min(minimum, EXCLUDED.minimum),
44
- maximum = max(maximum, EXCLUDED.maximum)
37
+ ON CONFLICT DO UPDATE
38
+ SET
39
+ count = count + EXCLUDED.count,
40
+ value = value + EXCLUDED.value,
41
+ minimum = min(minimum, EXCLUDED.minimum),
42
+ maximum = max(maximum, EXCLUDED.maximum);
45
43
 
46
- delete_migrated_events:
44
+ delete_migrated_events: >
47
45
  DELETE FROM local_events WHERE rowid IN (
48
- SELECT rowid
49
- FROM local_events
50
- ORDER BY resolution, created_at ASC, topic, name, key
46
+ SELECT rowid
47
+ FROM local_events
48
+ ORDER BY resolution, created_at ASC, topic, name, key
51
49
  LIMIT ?
52
- )
53
-
54
- event_count:
50
+ );
51
+
52
+ event_count: >
55
53
  SELECT count(*) FROM local_events;
56
-
@@ -1,27 +1,26 @@
1
1
  # frozen_stringe_literal: true
2
2
 
3
3
  # all components should require the support module
4
- require_relative 'litesupport'
4
+ require_relative "litesupport"
5
5
 
6
- #require 'securerandom'
6
+ # require 'securerandom'
7
7
 
8
8
  ##
9
- #Litequeue is a simple queueing system for Ruby applications that allows you to push and pop values from a queue. It provides a straightforward API for creating and managing named queues, and for adding and removing values from those queues. Additionally, it offers options for scheduling pops at a certain time in the future, which can be useful for delaying processing until a later time.
9
+ # Litequeue is a simple queueing system for Ruby applications that allows you to push and pop values from a queue. It provides a straightforward API for creating and managing named queues, and for adding and removing values from those queues. Additionally, it offers options for scheduling pops at a certain time in the future, which can be useful for delaying processing until a later time.
10
10
  #
11
- #Litequeue is built on top of SQLite, which makes it very fast and efficient, even when handling large volumes of data. This lightweight and easy-to-use queueing system serves as a good foundation for building more advanced job processing frameworks that require basic queuing capabilities.
11
+ # Litequeue is built on top of SQLite, which makes it very fast and efficient, even when handling large volumes of data. This lightweight and easy-to-use queueing system serves as a good foundation for building more advanced job processing frameworks that require basic queuing capabilities.
12
12
  #
13
-
14
- class Litequeue
15
13
 
14
+ class Litequeue
16
15
  # the default options for the queue
17
- # can be overriden by passing new options in a hash
16
+ # can be overriden by passing new options in a hash
18
17
  # to Litequeue.new
19
18
  # path: "./queue.db"
20
19
  # mmap_size: 128 * 1024 * 1024 -> 128MB to be held in memory
21
20
  # sync: 1 -> sync only when checkpointing
22
-
21
+
23
22
  include Litesupport::Liteconnection
24
-
23
+
25
24
  DEFAULT_OPTIONS = {
26
25
  path: Litesupport.root.join("queue.sqlite3"),
27
26
  mmap_size: 32 * 1024,
@@ -35,70 +34,70 @@ class Litequeue
35
34
  # queue.pop # => nil
36
35
  # sleep 2
37
36
  # queue.pop # => "somevalue"
38
-
37
+
39
38
  def initialize(options = {})
40
39
  init(options)
41
40
  end
42
-
41
+
43
42
  # push an item to the queue, optionally specifying the queue name (defaults to default) and after how many seconds it should be ready to pop (defaults to zero)
44
43
  # a unique job id is returned from this method, can be used later to delete it before it fires. You can push string, integer, float, true, false or nil values
45
44
  #
46
- def push(value, delay=0, queue='default')
45
+ def push(value, delay = 0, queue = "default")
47
46
  # @todo - check if queue is busy, back off if it is
48
47
  # also bring back the synchronize block, to prevent
49
48
  # a race condition if a thread hits the busy handler
50
49
  # before the current thread proceeds after a backoff
51
- #id = SecureRandom.uuid # this is somehow expensive, can we improve?
50
+ # id = SecureRandom.uuid # this is somehow expensive, can we improve?
52
51
  run_stmt(:push, queue, delay, value)[0]
53
52
  end
54
-
55
- def repush(id, value, delay=0, queue='default')
53
+
54
+ def repush(id, value, delay = 0, queue = "default")
56
55
  run_stmt(:repush, id, queue, delay, value)[0]
57
56
  end
58
-
59
- alias_method :"<<", :push
57
+
58
+ alias_method :<<, :push
60
59
  alias_method :"<<<", :repush
61
-
60
+
62
61
  # pop an item from the queue, optionally with a specific queue name (default queue name is 'default')
63
- def pop(queue='default', limit = 1)
64
- res = run_stmt(:pop, queue, limit)
65
- return res[0] if res.length == 1
66
- return nil if res.empty?
67
- res
62
+ def pop(queue = "default", limit = 1)
63
+ res = run_stmt(:pop, queue, limit)
64
+ return res[0] if res.length == 1
65
+ return nil if res.empty?
66
+ res
68
67
  end
69
-
68
+
70
69
  # delete an item from the queue
71
70
  # queue = Litequeue.new
72
71
  # id = queue.push("somevalue")
73
72
  # queue.delete(id) # => "somevalue"
74
73
  # queue.pop # => nil
75
74
  def delete(id)
76
- result = run_stmt(:delete, id)[0]
75
+ run_stmt(:delete, id)[0]
77
76
  end
78
-
77
+
79
78
  # deletes all the entries in all queues, or if a queue name is given, deletes all entries in that specific queue
80
- def clear(queue=nil)
79
+ def clear(queue = nil)
81
80
  run_sql("DELETE FROM queue WHERE iif(?1 IS NOT NULL, name = ?1, TRUE)", queue)
82
81
  end
83
82
 
84
83
  # returns a count of entries in all queues, or if a queue name is given, reutrns the count of entries in that queue
85
- def count(queue=nil)
84
+ def count(queue = nil)
86
85
  run_sql("SELECT count(*) FROM queue WHERE iif(?1 IS NOT NULL, name = ?1, TRUE)", queue)[0][0]
87
86
  end
88
-
87
+
89
88
  # return the size of the queue file on disk
90
- #def size
91
- # run_sql("SELECT size.page_size * count.page_count FROM pragma_page_size() AS size, pragma_page_count() AS count")[0][0]
92
- #end
93
-
89
+ # def size
90
+ # run_sql("SELECT size.page_size * count.page_count FROM pragma_page_size() AS size, pragma_page_count() AS count")[0][0]
91
+ # end
92
+
94
93
  def queues_info
95
94
  run_stmt(:info)
96
95
  end
97
-
96
+
98
97
  def snapshot
99
98
  queues = {}
100
99
  queues_info.each do |qc|
101
- #queues[qc[0]] = {count: qc[1], time_in_queue: {avg: qc[2], min: qc[3], max: qc[4]}}
100
+ # queues[qc[0]] = {count: qc[1], time_in_queue: {avg: qc[2], min: qc[3], max: qc[4]}}
102
101
  queues[qc[0]] = qc[1]
103
102
  end
104
103
  {
@@ -109,15 +108,15 @@ class Litequeue
109
108
  size: size,
110
109
  jobs: count
111
110
  },
112
- queues: queues
111
+ queues: queues
113
112
  }
114
113
  end
115
114
 
116
- private
117
-
115
+ private
116
+
118
117
  def create_connection
119
118
  super("#{__dir__}/litequeue.sql.yml") do |conn|
120
- conn.wal_autocheckpoint = 10000
119
+ conn.wal_autocheckpoint = 10000
121
120
  # check if there is an old database and convert entries to the new format
122
121
  if conn.get_first_value("select count(*) from sqlite_master where name = '_ul_queue_'") == 1
123
122
  conn.transaction(:immediate) do
@@ -127,5 +126,4 @@ class Litequeue
127
126
  end
128
127
  end
129
128
  end
130
-
131
- end
129
+ end
@@ -1,45 +1,53 @@
1
1
  schema:
2
- 1:
3
- create_table_queue: >
2
+ 1:
3
+ create_table_queue: >
4
4
  CREATE TABLE IF NOT EXISTS queue(
5
- id TEXT PRIMARY KEY DEFAULT(hex(randomblob(32))) NOT NULL ON CONFLICT REPLACE,
6
- name TEXT DEFAULT('default') NOT NULL ON CONFLICT REPLACE,
5
+ id TEXT PRIMARY KEY DEFAULT(hex(randomblob(32))) NOT NULL ON CONFLICT REPLACE,
6
+ name TEXT DEFAULT('default') NOT NULL ON CONFLICT REPLACE,
7
7
  fire_at INTEGER DEFAULT(unixepoch()) NOT NULL ON CONFLICT REPLACE,
8
8
  value TEXT,
9
9
  created_at INTEGER DEFAULT(unixepoch()) NOT NULL ON CONFLICT REPLACE
10
- ) WITHOUT ROWID
11
-
10
+ ) WITHOUT ROWID;
11
+
12
12
  create_index_queue_by_name: >
13
- CREATE INDEX IF NOT EXISTS idx_queue_by_name ON queue(name, fire_at ASC)
13
+ CREATE INDEX IF NOT EXISTS idx_queue_by_name ON queue(name, fire_at ASC);
14
14
 
15
15
  stmts:
16
16
 
17
- push: INSERT INTO queue(id, name, fire_at, value) VALUES (hex(randomblob(32)), $1, (unixepoch() + $2), $3) RETURNING id, name
18
-
19
- repush: INSERT INTO queue(id, name, fire_at, value) VALUES (?, ?, (unixepoch() + ?), ?) RETURNING name
17
+ push: >
18
+ INSERT INTO queue(id, name, fire_at, value)
19
+ VALUES (hex(randomblob(32)), $1, (unixepoch() + $2), $3)
20
+ RETURNING id, name;
21
+
22
+ repush: >
23
+ INSERT INTO queue(id, name, fire_at, value)
24
+ VALUES (?, ?, (unixepoch() + ?), ?)
25
+ RETURNING name;
20
26
 
21
27
  pop: >
22
- DELETE FROM queue
23
- WHERE (name, fire_at, id)
28
+ DELETE FROM queue
29
+ WHERE (name, fire_at, id)
24
30
  IN (
25
- SELECT name, fire_at, id FROM queue
26
- WHERE name = ifnull($1, 'default')
27
- AND fire_at <= (unixepoch())
28
- ORDER BY fire_at ASC
29
- LIMIT ifnull($2, 1)
30
- )
31
- RETURNING id, value
32
-
33
- delete: DELETE FROM queue WHERE id = $1 RETURNING value
34
-
31
+ SELECT name, fire_at, id FROM queue
32
+ WHERE name = ifnull($1, 'default')
33
+ AND fire_at <= (unixepoch())
34
+ ORDER BY fire_at ASC
35
+ LIMIT ifnull($2, 1)
36
+ )
37
+ RETURNING id, value;
38
+
39
+ delete: >
40
+ DELETE FROM queue
41
+ WHERE id = $1
42
+ RETURNING value;
43
+
35
44
  info: >
36
- SELECT
37
- name,
38
- count(*) AS count,
39
- avg(unixepoch() - created_at) AS avg,
40
- min(unixepoch() - created_at) AS min,
45
+ SELECT
46
+ name,
47
+ count(*) AS count,
48
+ avg(unixepoch() - created_at) AS avg,
49
+ min(unixepoch() - created_at) AS min,
41
50
  max(unixepoch() - created_at) AS max
42
- FROM queue
43
- GROUP BY name
44
- ORDER BY count DESC
45
-
51
+ FROM queue
52
+ GROUP BY name
53
+ ORDER BY count DESC;
@@ -0,0 +1,84 @@
1
+ # frozen_stringe_literal: true
2
+
3
+ module Litescheduler
4
+ # cache the scheduler we are running in
5
+ # it is an error to change the scheduler for a process
6
+ # or for a child forked from that process
7
+ def self.backend
8
+ @backend ||= if Fiber.scheduler
9
+ :fiber
10
+ elsif defined? Polyphony
11
+ :polyphony
12
+ elsif defined? Iodine
13
+ :iodine
14
+ else
15
+ :threaded
16
+ end
17
+ end
18
+
19
+ # spawn a new execution context
20
+ def self.spawn(&block)
21
+ if backend == :fiber
22
+ Fiber.schedule(&block)
23
+ elsif backend == :polyphony
24
+ spin(&block)
25
+ elsif (backend == :threaded) || (backend == :iodine)
26
+ Thread.new(&block)
27
+ end
28
+ # we should never reach here
29
+ end
30
+
31
+ def self.storage
32
+ if backend == :fiber || backend == :poylphony
33
+ Fiber.current.storage
34
+ else
35
+ Thread.current
36
+ end
37
+ end
38
+
39
+ def self.current
40
+ if backend == :fiber || backend == :poylphony
41
+ Fiber.current
42
+ else
43
+ Thread.current
44
+ end
45
+ end
46
+
47
+ # switch the execution context to allow others to run
48
+ def self.switch
49
+ if backend == :fiber
50
+ Fiber.scheduler.yield
51
+ true
52
+ elsif backend == :polyphony
53
+ Fiber.current.schedule
54
+ Thread.current.switch_fiber
55
+ true
56
+ else
57
+ # Thread.pass
58
+ false
59
+ end
60
+ end
61
+
62
+ # bold assumption, we will only synchronize threaded code!
63
+ # If some code explicitly wants to synchronize a fiber
64
+ # they must send (true) as a parameter to this method
65
+ # else it is a no-op for fibers
66
+ def self.synchronize(fiber_sync = false, &block)
67
+ if (backend == :fiber) || (backend == :polyphony)
68
+ yield # do nothing, just run the block as is
69
+ else
70
+ mutex.synchronize(&block)
71
+ end
72
+ end
73
+
74
+ def self.max_contexts
75
+ return 50 if backend == :fiber || backend == :polyphony
76
+ 5
77
+ end
78
+
79
+ # mutex initialization
80
+ def self.mutex
81
+ # a single mutex per process (is that ok?)
82
+ @@mutex ||= Mutex.new
83
+ end
84
+ end
@@ -0,0 +1,260 @@
1
+ require "oj"
2
+ require_relative "./schema"
3
+
4
+ class Litesearch::Index
5
+ DEFAULT_SEARCH_OPTIONS = {limit: 25, offset: 0}
6
+
7
+ def initialize(db, name)
8
+ @db = db # this index instance will always belong to this db instance
9
+ @stmts = {}
10
+ name = name.to_s.downcase.to_sym
11
+ # if in the db then put in cache and return if no schema is given
12
+ # if a schema is given then compare the new and the existing schema
13
+ # if they are the same put in cache and return
14
+ # if they differ only in weights then set the new weights, update the schema, put in cache and return
15
+ # if they differ in fields (added/removed/renamed) then update the structure, then rebuild if auto-rebuild is on
16
+ # if they differ in tokenizer then rebuild if auto-rebuild is on (error otherwise)
17
+ # if they differ in both then update the structure and rebuild if auto-rebuild is on (error otherwise)
18
+ load_index(name) if exists?(name)
19
+
20
+ if block_given?
21
+ schema = Litesearch::Schema.new
22
+ schema.schema[:name] = name
23
+ yield schema
24
+ schema.post_init
25
+ # now that we have a schema object we need to check if we need to create or modify and existing index
26
+ if @db.transaction_active?
27
+ if exists?(name)
28
+ load_index(name)
29
+ do_modify(schema)
30
+ else
31
+ do_create(schema)
32
+ end
33
+ prepare_statements
34
+ else
35
+ @db.transaction(:immediate) do
36
+ if exists?(name)
37
+ load_index(name)
38
+ do_modify(schema)
39
+ else
40
+ do_create(schema)
41
+ end
42
+ prepare_statements
43
+ end
44
+ end
45
+ elsif exists?(name)
46
+ load_index(name)
47
+ prepare_statements
48
+ # an index already exists, load it from the database and return the index instance to the caller
49
+ else
50
+ raise "index does not exist and no schema was supplied"
51
+ end
52
+ end
53
+
54
+ def load_index(name)
55
+ # we cannot use get_config_value here since the schema object is not created yet, should we allow something here?
56
+ @schema = begin
57
+ Litesearch::Schema.new(Oj.load(@db.get_first_value("SELECT v from #{name}_config where k = ?", :litesearch_schema.to_s)))
58
+ rescue
59
+ nil
60
+ end
61
+ raise "index configuration not found, either corrupted or not a litesearch index!" if @schema.nil?
62
+ self
63
+ end
64
+
65
+ def modify
66
+ schema = Litesearch::Schema.new
67
+ yield schema
68
+ schema.schema[:name] = @schema.schema[:name]
69
+ do_modify(schema)
70
+ end
71
+
72
+ def rebuild!
73
+ if @db.transaction_active?
74
+ do_rebuild
75
+ else
76
+ @db.transaction(:immediate) { do_rebuild }
77
+ end
78
+ end
79
+
80
+ def add(document)
81
+ @stmts[:insert].execute!(document)
82
+ @db.last_insert_row_id
83
+ end
84
+
85
+ def remove(id)
86
+ @stmts[:delete].execute!(id)
87
+ end
88
+
89
+ def count(term = nil)
90
+ if term
91
+ @stmts[:count].execute!(term)[0][0]
92
+ else
93
+ @stmts[:count_all].execute![0][0]
94
+ end
95
+ end
96
+
97
+ # search options include
98
+ # limit: how many records to return
99
+ # offset: start from which record
100
+ def search(term, options = {})
101
+ options = DEFAULT_SEARCH_OPTIONS.merge(options)
102
+ rs = @stmts[:search].execute(term, options[:limit], options[:offset])
103
+ generate_results(rs)
104
+ end
105
+
106
+ def similar(id, limit=10)
107
+ # pp term = @db.execute(@schema.sql_for(:similarity_query), id)
108
+ if @schema.schema[:tokenizer] == :trigram
109
+ # just use the normal similarity approach for now
110
+ # need to recondisder that for trigram indexes later
111
+ rs = @stmts[:similar].execute(id, limit)
112
+ else
113
+ rs = @stmts[:similar].execute(id, limit)
114
+ end
115
+ generate_results(rs)
116
+ end
117
+
118
+ def clear!
119
+ @stmts[:delete_all].execute!(id)
120
+ end
121
+
122
+ def drop!
123
+ if @schema.get(:type) == :backed
124
+ @db.execute_batch(@schema.sql_for(:drop_primary_triggers))
125
+ if @schema.sql_for(:create_secondary_triggers)
126
+ @db.execute_batch(@schema.sql_for(:drop_secondary_triggers))
127
+ end
128
+ end
129
+ @db.execute(@schema.sql_for(:drop))
130
+ end
131
+
132
+ private
133
+
134
+ def generate_results(rs)
135
+ result = []
136
+ if @db.results_as_hash
137
+ rs.each_hash do |hash|
138
+ result << hash
139
+ end
140
+ else
141
+ result = rs.to_a
142
+ end
143
+ result
144
+ end
145
+
146
+ def exists?(name)
147
+ @db.get_first_value("SELECT count(*) FROM SQLITE_MASTER WHERE name = ? AND type = 'table' AND (sql like '%fts5%' OR sql like '%FTS5%')", name.to_s) == 1
148
+ end
149
+
150
+ def prepare_statements
151
+ stmt_names = [:insert, :delete, :delete_all, :drop, :count, :count_all, :search, :similar]
152
+ stmt_names.each do |stmt_name|
153
+ @stmts[stmt_name] = @db.prepare(@schema.sql_for(stmt_name))
154
+ end
155
+ end
156
+
157
+ def do_create(schema)
158
+ @schema = schema
159
+ @schema.clean
160
+ # create index
161
+ @db.execute(schema.sql_for(:create_index, true))
162
+ @db.execute_batch(schema.sql_for(:create_vocab_tables))
163
+ # adjust ranking function
164
+ @db.execute(schema.sql_for(:ranks, true))
165
+ # create triggers (if any)
166
+ if @schema.get(:type) == :backed
167
+ @db.execute_batch(@schema.sql_for(:create_primary_triggers))
168
+ if (secondary_triggers_sql = @schema.sql_for(:create_secondary_triggers))
169
+ @db.execute_batch(secondary_triggers_sql)
170
+ end
171
+ @db.execute(@schema.sql_for(:rebuild)) if @schema.get(:rebuild_on_create)
172
+ end
173
+ set_config_value(:litesearch_schema, @schema.schema)
174
+ end
175
+
176
+ def do_modify(new_schema)
177
+ changes = @schema.compare(new_schema)
178
+ # ensure the new schema maintains feild order
179
+ new_schema.order_fields(@schema)
180
+ # with the changes object decide what needs to be done to the schema
181
+ requires_schema_change = false
182
+ requires_trigger_change = false
183
+ requires_rebuild = false
184
+ if changes[:fields] || changes[:table] || changes[:tokenizer] || changes[:filter_column] || changes[:removed_fields_count] > 0 # any change here will require a schema change
185
+ requires_schema_change = true
186
+ # only a change in tokenizer
187
+ requires_rebuild = changes[:tokenizer] || new_schema.get(:rebuild_on_modify)
188
+ requires_trigger_change = (changes[:table] || changes[:fields] || changes[:filter_column]) && @schema.get(:type) == :backed
189
+ end
190
+ if requires_schema_change
191
+ # 1. enable schema editing
192
+ @db.execute("PRAGMA WRITABLE_SCHEMA = TRUE")
193
+ # 2. update the index sql
194
+ @db.execute(new_schema.sql_for(:update_index), new_schema.sql_for(:create_index))
195
+ # 3. update the content table sql (if it exists)
196
+ @db.execute(new_schema.sql_for(:update_content_table), new_schema.sql_for(:create_content_table, new_schema.schema[:fields].count))
197
+ # adjust shadow tables
198
+ @db.execute(new_schema.sql_for(:expand_data), changes[:extra_fields_count])
199
+ @db.execute(new_schema.sql_for(:expand_docsize), changes[:extra_fields_count])
200
+ @db.execute("PRAGMA WRITABLE_SCHEMA = RESET")
201
+ # need to reprepare statements
202
+ end
203
+ if requires_trigger_change
204
+ @db.execute_batch(new_schema.sql_for(:drop_primary_triggers))
205
+ @db.execute_batch(new_schema.sql_for(:create_primary_triggers))
206
+ if (secondary_triggers_sql = new_schema.sql_for(:create_secondary_triggers))
207
+ @db.execute_batch(new_schema.sql_for(:drop_secondary_triggers))
208
+ @db.execute_batch(secondary_triggers_sql)
209
+ end
210
+ end
211
+ if changes[:fields] || changes[:table] || changes[:tokenizer] || changes[:weights] || changes[:filter_column]
212
+ @schema = new_schema
213
+ set_config_value(:litesearch_schema, @schema.schema)
214
+ prepare_statements
215
+ # save_schema
216
+ end
217
+ # update the weights if they changed
218
+ @db.execute(@schema.sql_for(:ranks, true)) if changes[:weights]
219
+ @db.execute_batch(@schema.sql_for(:create_vocab_tables))
220
+ do_rebuild if requires_rebuild
221
+ end
222
+
223
+ def do_rebuild
224
+ # remove any zero weight columns
225
+ if @schema.get(:type) == :backed
226
+ @db.execute_batch(@schema.sql_for(:drop_primary_triggers))
227
+ if (secondary_triggers_sql = @schema.sql_for(:create_secondary_triggers))
228
+ @db.execute_batch(@schema.sql_for(:drop_secondary_triggers))
229
+ end
230
+ @db.execute(@schema.sql_for(:drop))
231
+ @db.execute(@schema.sql_for(:create_index, true))
232
+ @db.execute_batch(@schema.sql_for(:create_primary_triggers))
233
+ @db.execute_batch(secondary_triggers_sql) if secondary_triggers_sql
234
+ @db.execute(@schema.sql_for(:rebuild))
235
+ elsif @schema.get(:type) == :standalone
236
+ removables = []
237
+ @schema.get(:fields).each_with_index { |f, i| removables << [f[0], i] if f[1][:weight] == 0 }
238
+ removables.each do |col|
239
+ @db.execute(@schema.sql_for(:drop_content_col, col[1]))
240
+ @schema.get(:fields).delete(col[0])
241
+ end
242
+ @db.execute("PRAGMA WRITABLE_SCHEMA = TRUE")
243
+ @db.execute(@schema.sql_for(:update_index), @schema.sql_for(:create_index, true))
244
+ @db.execute(@schema.sql_for(:update_content_table), @schema.sql_for(:create_content_table, @schema.schema[:fields].count))
245
+ @db.execute("PRAGMA WRITABLE_SCHEMA = RESET")
246
+ @db.execute(@schema.sql_for(:rebuild))
247
+ end
248
+ @db.execute_batch(@schema.sql_for(:create_vocab_tables))
249
+ set_config_value(:litesearch_schema, @schema.schema)
250
+ @db.execute(@schema.sql_for(:ranks, true))
251
+ end
252
+
253
+ def get_config_value(key)
254
+ Oj.load(@db.get_first_value(@schema.sql_for(:get_config_value), key.to_s)) # rescue nil
255
+ end
256
+
257
+ def set_config_value(key, value)
258
+ @db.execute(@schema.sql_for(:set_config_value), key.to_s, Oj.dump(value))
259
+ end
260
+ end