Class: Mistri::Workspace::ActiveRecord
- Inherits:
-
Object
- Object
- Mistri::Workspace::ActiveRecord
- Defined in:
- lib/mistri/workspace/active_record.rb
Overview
Documents in the host's own database, through a model class the host supplies, optionally scoped (per session, per tenant). Not auto-required; load it with require "mistri/workspace/active_record".
The model needs a non-null primary key plus non-null path, content, and scope columns, with a unique index whose columns are exactly the scope plus path. Atomic edits use that index plus a short row-locking transaction; no lock_version column is required. A migration to copy:
create_table :mistri_documents do |t|
t.string :session_id, null: false
t.string :path, null: false, limit: 512
t.text :content, size: :medium, null: false
t.
end
add_index :mistri_documents, [:session_id, :path], unique: true
Reads run uncached: other processes write documents by design, and a cached read inside a job would serve the job's first snapshot forever. Uncached only defeats Rails' cache; an open REPEATABLE READ transaction still pins what reads see, so do not read from inside one.
Instance Method Summary collapse
- #atomic_writes? ⇒ Boolean
- #compare_and_write(path, content, expected_revision:) ⇒ Object
- #delete(path) ⇒ Object
-
#initialize(model, scope: {}) ⇒ ActiveRecord
constructor
A new instance of ActiveRecord.
- #list(prefix = nil) ⇒ Object
- #read(path) ⇒ Object
- #snapshot(path) ⇒ Object
- #write(path, content) ⇒ Object
Constructor Details
#initialize(model, scope: {}) ⇒ ActiveRecord
Returns a new instance of ActiveRecord.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/mistri/workspace/active_record.rb', line 32 def initialize(model, scope: {}) raise ArgumentError, "scope must be a Hash" unless scope.is_a?(Hash) names = scope.keys.map(&:to_s) if names.uniq.length != names.length raise ArgumentError, "scope keys must name distinct columns" end reserved = names & %w[path content] unless reserved.empty? raise ArgumentError, "scope cannot contain #{reserved.map(&:inspect).join(" or ")}" end @model = model @scope = own_scope(scope) end |
Instance Method Details
#atomic_writes? ⇒ Boolean
60 61 62 63 64 |
# File 'lib/mistri/workspace/active_record.rb', line 60 def atomic_writes? return @atomic_writes if defined?(@atomic_writes) @atomic_writes = atomic_storage? end |
#compare_and_write(path, content, expected_revision:) ⇒ Object
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/mistri/workspace/active_record.rb', line 72 def compare_and_write(path, content, expected_revision:) verify_atomic_context! key = path.to_s return create_if_absent(key, content) if expected_revision.nil? @model.transaction do record = @model.lock.find_by(**@scope, path: key) actual = record && Snapshot.for(record.content.to_s).revision unless actual == expected_revision raise WorkspaceConflictError.new( key, expected_revision:, actual_revision: actual ) end record.content = content.to_s record.save! committed_snapshot(record, key) end end |
#delete(path) ⇒ Object
92 93 94 95 |
# File 'lib/mistri/workspace/active_record.rb', line 92 def delete(path) @model.where(**@scope, path: path.to_s).delete_all nil end |
#list(prefix = nil) ⇒ Object
97 98 99 100 |
# File 'lib/mistri/workspace/active_record.rb', line 97 def list(prefix = nil) paths = @model.uncached { @model.where(**@scope).pluck(:path) }.sort prefix ? paths.select { |p| p.start_with?(prefix.to_s) } : paths end |
#read(path) ⇒ Object
49 50 51 |
# File 'lib/mistri/workspace/active_record.rb', line 49 def read(path) @model.uncached { @model.where(**@scope, path: path.to_s).pick(:content) } end |
#snapshot(path) ⇒ Object
66 67 68 69 70 |
# File 'lib/mistri/workspace/active_record.rb', line 66 def snapshot(path) verify_atomic_context! content = @model.uncached { relation(path).pick(:content) } content.nil? ? nil : Snapshot.for(content.to_s) end |
#write(path, content) ⇒ Object
53 54 55 56 57 58 |
# File 'lib/mistri/workspace/active_record.rb', line 53 def write(path, content) record = @model.find_or_initialize_by(**@scope, path: path.to_s) record.content = content.to_s record.save! nil end |