Class: Mxrb::Runtime::SQLiteStore

Inherits:
Object
  • Object
show all
Defined in:
lib/mxrb/runtime/sqlite_store.rb

Overview

SQLite-backed replacement for Runtime::Native::Store. ObjectValue stays the public value type, so the native microflow interpreter can use this store without a persistence-specific object abstraction. rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity

Constant Summary collapse

EVENTS =
%i[
  before_create after_create before_update after_update
  before_commit after_commit before_delete after_delete
].freeze
SYSTEM_MEMBERS =
{
  owner: %w[Owner __owner_id], created_date: %w[createdDate __created_at],
  changed_date: %w[changedDate __changed_at], changed_by: %w[changedBy __changed_by_id]
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project, path: ':memory:', defaults: {}, hooks: {}, allow_destructive: false) ⇒ SQLiteStore

Returns a new instance of SQLiteStore.



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/mxrb/runtime/sqlite_store.rb', line 28

def initialize(project, path: ':memory:', defaults: {}, hooks: {}, allow_destructive: false)
  @database = SQLite3::Database.new(path.to_s)
  @database.results_as_hash = true
  @database.execute('PRAGMA foreign_keys = ON')
  @database.busy_timeout = 5_000
  @schema = SchemaMigrator.new(
    project, database: @database, allow_destructive:
  ).tap(&:migrate!).schema
  @defaults = defaults.transform_keys(&:to_s)
  transient_defaults = project.modules.flat_map do |mod|
    mod.entities.select { _1.persistable == false }.map do |entity|
      ["#{mod.name}.#{entity.name}", defaults.fetch("#{mod.name}.#{entity.name}", {})]
    end
  end.to_h
  @transient_entities = transient_defaults.keys.freeze
  @transient = Native::Store.new(defaults: transient_defaults)
  @hooks = Hash.new { |values, event| values[event] = [] }
  @identity = {}
  @persisted = {}
  @staged_new = {}
  @sequence_values = {}
  @manual_transaction = false
  hooks.each { |event, callbacks| Array(callbacks).each { on(event, &_1) } }
end

Instance Attribute Details

#databaseObject (readonly)

Returns the value of attribute database.



26
27
28
# File 'lib/mxrb/runtime/sqlite_store.rb', line 26

def database
  @database
end

#schemaObject (readonly)

Returns the value of attribute schema.



26
27
28
# File 'lib/mxrb/runtime/sqlite_store.rb', line 26

def schema
  @schema
end

Instance Method Details

#begin_transactionObject

Raises:

  • (SQLite3::SQLException)


177
178
179
180
181
182
183
184
# File 'lib/mxrb/runtime/sqlite_store.rb', line 177

def begin_transaction
  raise SQLite3::SQLException, 'transaction already active' if @manual_transaction

  @manual_snapshot = transaction_snapshot
  database.execute('BEGIN IMMEDIATE')
  @manual_transaction = true
  self
end

#closeObject



237
238
239
240
# File 'lib/mxrb/runtime/sqlite_store.rb', line 237

def close
  rollback if @manual_transaction
  database.close unless database.closed?
end

#commit(value = nil, events: true) ⇒ Object



168
169
170
171
172
173
174
175
# File 'lib/mxrb/runtime/sqlite_store.rb', line 168

def commit(value = nil, events: true)
  values = value.nil? ? dirty_values : Array(value)
  transient, persistent = values.compact.partition { transient?(_1.entity) }
  @transient.commit(transient, events:) unless transient.empty?
  atomic { persistent.each { persist_update(_1, events:) } }
  finish_manual_transaction if value.nil? && @manual_transaction
  value
end

#count(entity, predicate = nil) ⇒ Object



158
159
160
161
162
163
164
165
166
# File 'lib/mxrb/runtime/sqlite_store.rb', line 158

def count(entity, predicate = nil)
  return @transient.count(transient_name(entity), predicate) if transient?(entity)

  return retrieve(entity).count { predicate.call(_1) } if predicate

  definition = schema.entity(entity)
  durable = database.get_first_value("SELECT COUNT(*) FROM #{quote(definition.table)}").to_i
  durable + @staged_new.values.count { _1.entity == definition.name }
end

#create(entity, events: true) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/mxrb/runtime/sqlite_store.rb', line 70

def create(entity, events: true)
  return @transient.create(transient_name(entity), events:) if transient?(entity)

  definition = schema.entity(entity)
  value = nil
  atomic do
    members = defaults_for(definition)
    value = Native::ObjectValue.new(entity: definition.name, id: SecureRandom.uuid, members:)
    run_hooks(:before_create, value) if events
    stage(value)
    run_hooks(:after_create, value) if events
  end
  value
rescue StandardError
  @identity.delete(value&.id)
  @persisted.delete(value&.id)
  @staged_new.delete(value&.id)
  raise
end

#delete(value, events: true) ⇒ Object



151
152
153
154
155
156
# File 'lib/mxrb/runtime/sqlite_store.rb', line 151

def delete(value, events: true)
  transient, persistent = Array(value).compact.partition { transient?(_1.entity) }
  @transient.delete(transient, events:) unless transient.empty?
  persistent.each { delete_one(_1, events:) }
  nil
end

#find(entity, id) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/mxrb/runtime/sqlite_store.rb', line 101

def find(entity, id)
  return @transient.find(transient_name(entity), id) if transient?(entity)

  definition = schema.entity(entity)
  staged = @staged_new[id.to_s]
  return staged if staged&.entity == definition.name

  row = database.get_first_row(
    "SELECT * FROM #{quote(definition.table)} WHERE id = ?", id.to_s
  )
  return unless row

  materialize(definition, row).tap { load_direct_associations([_1]) }
end

#on(event_or_entity, positional_event = nil, entity: nil, &block) ⇒ Object

Raises:

  • (ArgumentError)


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/mxrb/runtime/sqlite_store.rb', line 53

def on(event_or_entity, positional_event = nil, entity: nil, &block)
  entity ||= event_or_entity if positional_event
  event = positional_event || event_or_entity
  key = event.to_sym
  raise ArgumentError, "unsupported lifecycle event #{event}" unless EVENTS.include?(key)
  raise ArgumentError, 'lifecycle hook requires a block' unless block

  @hooks[key] << [entity&.to_s, block]
  @transient.on(transient_name(entity), key) { |value| block.call(value, self) } if entity && transient?(entity)
  unless entity
    @transient_entities.each do |name|
      @transient.on(name, key) { |value| block.call(value, self) }
    end
  end
  self
end

#release_cache!Object



116
117
118
119
120
# File 'lib/mxrb/runtime/sqlite_store.rb', line 116

def release_cache!
  @identity = @staged_new.dup
  @persisted = {}
  self
end

#restore(snapshot) ⇒ Object



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/mxrb/runtime/sqlite_store.rb', line 221

def restore(snapshot)
  unit_of_work = snapshot['__mxrb_uow__']
  atomic do
    schema.associations.each { database.execute("DELETE FROM #{quote(_1.table)}") }
    schema.entities.each { database.execute("DELETE FROM #{quote(_1.table)}") }
    clear_cache
    if unit_of_work
      restore_unit_of_work(snapshot, unit_of_work)
    else
      restore_legacy_snapshot(snapshot)
    end
  end
  @transient.restore(snapshot.fetch('__mxrb_transient__', @transient.snapshot))
  self
end

#retrieve(entity) ⇒ Object



90
91
92
93
94
95
96
97
98
99
# File 'lib/mxrb/runtime/sqlite_store.rb', line 90

def retrieve(entity)
  return @transient.retrieve(transient_name(entity)) if transient?(entity)

  definition = schema.entity(entity)
  rows = database.execute("SELECT * FROM #{quote(definition.table)} ORDER BY rowid")
  values = rows.map { materialize(definition, _1) }
  values.concat(@staged_new.values.select { _1.entity == definition.name })
  load_direct_associations(values)
  values.uniq(&:id)
end

#retrieve_association(association, start) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/mxrb/runtime/sqlite_store.rb', line 122

def retrieve_association(association, start)
  return [] unless start

  definition = begin
    schema.association(association)
  rescue ArgumentError
    return @transient.retrieve_association(association, start) if transient?(start.entity)

    raise
  end
  return volatile_association_values(definition, association, start) if hybrid_association?(definition)

  return @transient.retrieve_association(association, start) if transient?(start.entity)

  if start.entity == definition.from_entity
    ids = database.execute(
      "SELECT target_id FROM #{quote(definition.table)} WHERE source_id = ? ORDER BY rowid", [start.id]
    ).map { _1['target_id'] }
    materialize_ids(definition.to_entity, ids)
  elsif start.entity == definition.to_entity
    ids = database.execute(
      "SELECT source_id FROM #{quote(definition.table)} WHERE target_id = ? ORDER BY rowid", [start.id]
    ).map { _1['source_id'] }
    materialize_ids(definition.from_entity, ids)
  else
    []
  end
end

#rollback(value = nil) ⇒ Object



197
198
199
200
201
202
203
204
205
206
# File 'lib/mxrb/runtime/sqlite_store.rb', line 197

def rollback(value = nil)
  return rollback_values(value) unless value.nil?

  state = @manual_snapshot
  database.execute('ROLLBACK') if database.transaction_active?
  @manual_transaction = false
  @manual_snapshot = nil
  restore_transaction_snapshot(state) if state
  self
end

#snapshotObject



208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/mxrb/runtime/sqlite_store.rb', line 208

def snapshot
  result = schema.entities.to_h do |entity|
    [entity.name, retrieve(entity.name).map { duplicate_value(_1) }]
  end
  result['__mxrb_transient__'] = @transient.snapshot
  result['__mxrb_uow__'] = {
    persisted: @persisted.transform_values(&:dup),
    staged_ids: @staged_new.keys,
    associations: association_snapshot
  }
  result
end

#transactionObject



186
187
188
189
190
191
192
193
194
195
# File 'lib/mxrb/runtime/sqlite_store.rb', line 186

def transaction
  begin_transaction
  result = yield self
  finish_manual_transaction
  detach_uncommitted
  result
rescue Exception # rubocop:disable Lint/RescueException
  rollback if @manual_transaction
  raise
end