diamond-orm 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/diamond/api_catalog.rb +676 -0
- data/lib/diamond/ast.rb +397 -0
- data/lib/diamond/changeset.rb +36 -0
- data/lib/diamond/compiler/base.rb +17 -0
- data/lib/diamond/compiler/ddl.rb +94 -0
- data/lib/diamond/compiler/dml.rb +109 -0
- data/lib/diamond/compiler/dql.rb +390 -0
- data/lib/diamond/compiler/registry.rb +45 -0
- data/lib/diamond/cursor.rb +52 -0
- data/lib/diamond/domains/cte.rb +25 -0
- data/lib/diamond/domains/ddl.rb +11 -0
- data/lib/diamond/domains/dml.rb +106 -0
- data/lib/diamond/domains/dql.rb +395 -0
- data/lib/diamond/domains/dynamic_finders.rb +64 -0
- data/lib/diamond/dsl/default.rb +419 -0
- data/lib/diamond/engine.rb +226 -0
- data/lib/diamond/json_string.rb +13 -0
- data/lib/diamond/null_table.rb +10 -0
- data/lib/diamond/operator.rb +31 -0
- data/lib/diamond/operators/like.rb +142 -0
- data/lib/diamond/parser/proxy.rb +469 -0
- data/lib/diamond/parser/registry.rb +74 -0
- data/lib/diamond/parser.rb +571 -0
- data/lib/diamond/query_object.rb +463 -0
- data/lib/diamond/struct_factory.rb +262 -0
- data/lib/diamond/table.rb +32 -0
- data/lib/diamond/version.rb +3 -0
- data/lib/diamond.rb +436 -0
- data/sig/diamond.rbs +693 -0
- metadata +95 -0
data/lib/diamond.rb
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# shareable_constant_value: literal
|
|
2
|
+
#
|
|
3
|
+
# makes every top-level constant declared below shareable across Ractors.
|
|
4
|
+
# `Users` etc. become references to shareable objects, so a Ractor can
|
|
5
|
+
# resolve `Users` without going through any global mutable state.
|
|
6
|
+
#
|
|
7
|
+
# After this file runs, every Diamond::* constant (and every public
|
|
8
|
+
# constant nested under Diamond::Parser / ::Compiler / ::Domains / ::DSL /
|
|
9
|
+
# ::Operators) is registered via Module#autoload — referencing one
|
|
10
|
+
# loads the corresponding file from gems/diamond/lib on first use.
|
|
11
|
+
# The eager top-half defines the error classes, RACTOR_KEYS, IDENT_RE
|
|
12
|
+
# and the global const_missing prepend, all of which must exist before
|
|
13
|
+
# any user code touches a Diamond table.
|
|
14
|
+
|
|
15
|
+
require 'extralite'
|
|
16
|
+
require 'prism'
|
|
17
|
+
require 'did_you_mean'
|
|
18
|
+
|
|
19
|
+
module Diamond
|
|
20
|
+
class Error < StandardError; end
|
|
21
|
+
class TableNotFound < Error; end
|
|
22
|
+
class RecordNotFound < Error; end
|
|
23
|
+
class InertObjectError < Error; end
|
|
24
|
+
|
|
25
|
+
class UnknownColumnError < StandardError
|
|
26
|
+
def self.build(schema, name)
|
|
27
|
+
cols = schema[:columns].map(&:to_s)
|
|
28
|
+
|
|
29
|
+
spell_checker = DidYouMean::SpellChecker.new(dictionary: cols)
|
|
30
|
+
suggestions = spell_checker.correct(name.to_s)
|
|
31
|
+
|
|
32
|
+
message = "Table has no column '#{name}'."
|
|
33
|
+
message += " Did you mean '#{suggestions.first}'?" unless suggestions.empty?
|
|
34
|
+
|
|
35
|
+
new(message)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Central registry of keys used to stash per-Ractor state in
|
|
40
|
+
# `Ractor.current[...]`. One place so the namespace stays grep-able and
|
|
41
|
+
# collisions are impossible. Each consumer module has a constant pointing
|
|
42
|
+
# here: `Diamond::RACTOR_KEYS[:engine]`, etc.
|
|
43
|
+
RACTOR_KEYS = {
|
|
44
|
+
engine: :_diamond_engine,
|
|
45
|
+
parser_caches: :_diamond_parser_caches,
|
|
46
|
+
struct_caches: :_diamond_struct_caches,
|
|
47
|
+
where_ops: :_diamond_where_ops,
|
|
48
|
+
derive_ops: :_diamond_derive_ops,
|
|
49
|
+
compiler_ops: :_diamond_compiler_ops,
|
|
50
|
+
finder_cols: :_diamond_finder_cols
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
IDENT_RE = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/.freeze
|
|
54
|
+
|
|
55
|
+
@db_path = nil
|
|
56
|
+
|
|
57
|
+
# Engine tuning inherited by lazily-booted worker-Ractor engines
|
|
58
|
+
# (see .engine). Engine-safe keys only: on_progress procs can never
|
|
59
|
+
# cross Ractors and auto_fiber_yield is wake_up-only, so neither is
|
|
60
|
+
# stored here. Always a frozen Hash of shareable values.
|
|
61
|
+
@diamond_opts = {}.freeze
|
|
62
|
+
|
|
63
|
+
# Autoloads — one entry per public constant. Loading :Parser / :Compiler
|
|
64
|
+
# / :Domains / :DSL / :Operators pulls in the namespace AND its sibling
|
|
65
|
+
# implementation files via require_relative inside each namespace-opener
|
|
66
|
+
# (see diamond/parser.rb, diamond/compiler/base.rb, diamond/domains/dql.rb).
|
|
67
|
+
autoload :VERSION, 'diamond/version'
|
|
68
|
+
autoload :Operator, 'diamond/operator'
|
|
69
|
+
autoload :Engine, 'diamond/engine'
|
|
70
|
+
autoload :Changeset, 'diamond/changeset'
|
|
71
|
+
autoload :AST, 'diamond/ast'
|
|
72
|
+
autoload :NullTable, 'diamond/null_table'
|
|
73
|
+
autoload :StructJSON, 'diamond/struct_factory'
|
|
74
|
+
autoload :StructFactory, 'diamond/struct_factory'
|
|
75
|
+
autoload :Parser, 'diamond/parser'
|
|
76
|
+
autoload :Compiler, 'diamond/compiler/base'
|
|
77
|
+
autoload :Domains, 'diamond/domains/dql'
|
|
78
|
+
autoload :DSL, 'diamond/dsl/default'
|
|
79
|
+
autoload :Operators, 'diamond/operators/like'
|
|
80
|
+
autoload :Table, 'diamond/table'
|
|
81
|
+
autoload :QueryObject, 'diamond/query_object'
|
|
82
|
+
autoload :Cursor, 'diamond/cursor'
|
|
83
|
+
autoload :JsonString, 'diamond/json_string'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# const_missing is kept as a fallback only. wake_up's bind_tables! already
|
|
87
|
+
# defines every schema table; this hook only fires for typo'd constants
|
|
88
|
+
# (where Diamond can't help) or in older code paths. Either way it raises
|
|
89
|
+
# NameError cleanly instead of silently inventing proxies.
|
|
90
|
+
#
|
|
91
|
+
# Prepended to Module — must run exactly once at load time so every
|
|
92
|
+
# later const lookup goes through it.
|
|
93
|
+
module DiamondConstMissing
|
|
94
|
+
def const_missing(name)
|
|
95
|
+
table_sym = name.to_s.downcase.to_sym
|
|
96
|
+
|
|
97
|
+
# This hook is prepended to every Module, so it fires for missing
|
|
98
|
+
# constants anywhere (not just table proxies). When Diamond was
|
|
99
|
+
# never woken, the engine cannot boot (nil path) — degrade to a
|
|
100
|
+
# clean NameError instead of leaking a TypeError from Extralite.
|
|
101
|
+
engine = begin
|
|
102
|
+
Diamond.engine
|
|
103
|
+
rescue StandardError
|
|
104
|
+
nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
if engine && engine.schema_cache.key?(table_sym)
|
|
108
|
+
# schema had this table but it wasn't eager-bound (e.g., bind_tables!
|
|
109
|
+
# ran before this table existed). bind it now.
|
|
110
|
+
proxy = Diamond::Table.new(table_sym).freeze
|
|
111
|
+
Object.const_set(name, proxy)
|
|
112
|
+
Diamond.note_bound_table(name)
|
|
113
|
+
proxy
|
|
114
|
+
else
|
|
115
|
+
super
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
Module.prepend(DiamondConstMissing)
|
|
120
|
+
|
|
121
|
+
module Diamond
|
|
122
|
+
def self.quote_ident(name)
|
|
123
|
+
"\"#{name.to_s.gsub('"', '""')}\""
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.validate_ident!(name, what = "identifier")
|
|
127
|
+
unless name.to_s.match?(IDENT_RE)
|
|
128
|
+
raise ArgumentError, "invalid #{what} #{name.inspect}: must match #{IDENT_RE.inspect}"
|
|
129
|
+
end
|
|
130
|
+
name
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Names we've bound as frozen constants, so a later wake_up can unbind them
|
|
134
|
+
# instead of leaving them pinned to the dead engine's schema.
|
|
135
|
+
@bound_tables = []
|
|
136
|
+
|
|
137
|
+
# Phase 7 + Fiber amendment: when waking up, auto-detect whether a
|
|
138
|
+
# Fiber::Scheduler is running and, if so, install an on_progress hook
|
|
139
|
+
# that yields to the scheduler while SQLite is waiting on I/O. This
|
|
140
|
+
# turns Diamond into a non-blocking database when used inside Async /
|
|
141
|
+
# Polyphony / any Fiber.scheduler-driven runtime.
|
|
142
|
+
def self.wake_up(db_path, busy_timeout: nil, gvl_release_threshold: nil,
|
|
143
|
+
on_progress: nil, extensions: [], auto_fiber_yield: true)
|
|
144
|
+
@db_path = db_path.freeze
|
|
145
|
+
# Stash engine-safe tuning for worker Ractors that boot lazily via
|
|
146
|
+
# .engine long after this returns. Deep-frozen (dup-freeze each
|
|
147
|
+
# extension path so caller-owned strings are never mutated) and
|
|
148
|
+
# shareable, so cross-Ractor reads never trip isolation.
|
|
149
|
+
@diamond_opts = {
|
|
150
|
+
busy_timeout: busy_timeout,
|
|
151
|
+
gvl_release_threshold: gvl_release_threshold,
|
|
152
|
+
extensions: Array(extensions).map { |p| p.dup.freeze }.freeze
|
|
153
|
+
}.freeze
|
|
154
|
+
|
|
155
|
+
# Phase 7: the caller may pass a custom on_progress; otherwise we
|
|
156
|
+
# build one that yields to the running Fiber::Scheduler when present.
|
|
157
|
+
# auto_fiber_yield=false opts out (e.g. for tests) — passed through
|
|
158
|
+
# as `false` so Engine#initialize can tell "explicitly none" apart
|
|
159
|
+
# from "absent" (absent + scheduler present would auto-install).
|
|
160
|
+
progress = on_progress
|
|
161
|
+
if progress.nil?
|
|
162
|
+
if auto_fiber_yield && Fiber.scheduler
|
|
163
|
+
progress = method(:fiber_yield_on_progress)
|
|
164
|
+
elsif !auto_fiber_yield
|
|
165
|
+
progress = false
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Main Ractor initializes its connection immediately for backward compat
|
|
170
|
+
# (so existing tests that touch `Diamond.engine` after wake_up still work).
|
|
171
|
+
Ractor.current[Diamond::RACTOR_KEYS[:engine]] = Engine.new(
|
|
172
|
+
@db_path,
|
|
173
|
+
busy_timeout: busy_timeout,
|
|
174
|
+
gvl_release_threshold: gvl_release_threshold,
|
|
175
|
+
on_progress: progress,
|
|
176
|
+
extensions: extensions
|
|
177
|
+
).freeze!
|
|
178
|
+
|
|
179
|
+
# including twice is a no-op for ancestors but still busts ruby's global
|
|
180
|
+
# method cache. guard it so per-test wake_up stays cheap.
|
|
181
|
+
unless Diamond::Table.include?(Diamond::DSL::Default)
|
|
182
|
+
Diamond::Table.include(Diamond::DSL::Default)
|
|
183
|
+
Diamond::Table.include(Diamond::Domains::DQL)
|
|
184
|
+
Diamond::Table.include(Diamond::Domains::DML)
|
|
185
|
+
Diamond::Table.include(Diamond::Domains::DynamicFinders)
|
|
186
|
+
|
|
187
|
+
Diamond::QueryObject.include(Diamond::DSL::Default)
|
|
188
|
+
Diamond::QueryObject.include(Diamond::Domains::DQL)
|
|
189
|
+
Diamond::QueryObject.include(Diamond::Domains::DML)
|
|
190
|
+
Diamond::QueryObject.include(Diamond::Domains::DynamicFinders)
|
|
191
|
+
|
|
192
|
+
Diamond.extend(Diamond::Domains::DDL)
|
|
193
|
+
Diamond.extend(Diamond::Domains::CTE)
|
|
194
|
+
Diamond.extend(Diamond::DSL::Default)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
rebind_tables!
|
|
198
|
+
bind_tables!
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Phase 7 Fiber-amendment hook. Called by Extralite while waiting on
|
|
202
|
+
# SQLite I/O. `Kernel.sleep(0)` dispatches to the running
|
|
203
|
+
# Fiber::Scheduler's kernel_sleep (a real yield) when inside a
|
|
204
|
+
# fiber, and is a fast no-op on plain threads — so one body covers
|
|
205
|
+
# both worlds with no scheduler sniffing here (presence was already
|
|
206
|
+
# checked at install time). Ends falsy; the return value is never
|
|
207
|
+
# an abort signal. `Kernel.` prefix is mandatory: bare `sleep`
|
|
208
|
+
# would resolve to Diamond.sleep itself and recurse forever.
|
|
209
|
+
# Defaulted arg: Extralite 3.0.1 invokes the handler with zero
|
|
210
|
+
# arguments (the documented busy flag is not passed).
|
|
211
|
+
def self.fiber_yield_on_progress(_busy = nil)
|
|
212
|
+
Kernel.sleep(0)
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# drop old constants so the next reference re-resolves (or raises
|
|
217
|
+
# NameError) instead of serving the dead engine's table.
|
|
218
|
+
def self.rebind_tables!
|
|
219
|
+
@bound_tables.each do |const_name|
|
|
220
|
+
Object.send(:remove_const, const_name) if Object.const_defined?(const_name, false)
|
|
221
|
+
end
|
|
222
|
+
@bound_tables.clear
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# eager-bind every schema table as a frozen top-level constant. Ractor-safe
|
|
226
|
+
# because the Table object is immutable after .freeze, and the engine's
|
|
227
|
+
# schema cache is frozen (see Engine#freeze!).
|
|
228
|
+
def self.bind_tables!
|
|
229
|
+
return unless Ractor.current[Diamond::RACTOR_KEYS[:engine]]
|
|
230
|
+
Ractor.current[Diamond::RACTOR_KEYS[:engine]].schema_cache.each_key do |table_sym|
|
|
231
|
+
const_name = table_sym.to_s.split('_').map(&:capitalize).join
|
|
232
|
+
next if Object.const_defined?(const_name, false)
|
|
233
|
+
proxy = Table.new(table_sym).freeze
|
|
234
|
+
Object.const_set(const_name, proxy)
|
|
235
|
+
@bound_tables << const_name unless @bound_tables.include?(const_name)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def self.note_bound_table(name)
|
|
240
|
+
@bound_tables << name unless @bound_tables.include?(name)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Per-Ractor engine. Each Ractor opens its own Extralite::Database connection
|
|
244
|
+
# lazily on first access. The shared schema/FK caches are loaded from the
|
|
245
|
+
# same DB file/connection-string, so every Ractor sees the same logical
|
|
246
|
+
# schema — but live query execution goes through each Ractor's own connection.
|
|
247
|
+
def self.engine
|
|
248
|
+
# Workers inherit the main Ractor's engine tuning (busy_timeout,
|
|
249
|
+
# gvl_release_threshold, extensions) stashed at wake_up. Hooks are
|
|
250
|
+
# deliberately NOT inherited: procs can't cross Ractors, and each
|
|
251
|
+
# engine re-runs presence detection for its own Ractor (which has
|
|
252
|
+
# no scheduler, so no hook — same as before).
|
|
253
|
+
engine = Ractor.current[Diamond::RACTOR_KEYS[:engine]] ||= Engine.new(@db_path, **@diamond_opts).freeze!
|
|
254
|
+
# Late-bind the yield hook if a scheduler appeared after this
|
|
255
|
+
# engine booted (no-op when already installed, opted out, or
|
|
256
|
+
# schedulerless — see Engine#sync_scheduler_hook!).
|
|
257
|
+
engine.sync_scheduler_hook!
|
|
258
|
+
engine
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Phase 7: backup to a file or another Engine. Block (if given) gets
|
|
262
|
+
# called with (remaining, total_pages) on each step.
|
|
263
|
+
def self.backup(target, &block)
|
|
264
|
+
engine.db.backup(target, &block)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Phase 7: track changes to the given tables inside a block. Returns
|
|
268
|
+
# a Diamond::Changeset wrapper around the underlying Extralite
|
|
269
|
+
# changeset. Use .apply(target_db) to replay on another connection,
|
|
270
|
+
# .invert to undo, .to_blob / .load(blob) to serialize over the wire.
|
|
271
|
+
#
|
|
272
|
+
# Requires the extralite-bundle build (sqlite3 session extension).
|
|
273
|
+
# Plain extralite 3.0.1 does NOT include track_changes — calling
|
|
274
|
+
# here raises FeatureNotAvailableError.
|
|
275
|
+
def self.track_changes(*tables, &block)
|
|
276
|
+
raise ArgumentError, "track_changes requires a block" unless block
|
|
277
|
+
unless defined?(Extralite::Changeset)
|
|
278
|
+
raise FeatureNotAvailableError,
|
|
279
|
+
"track_changes requires extralite-bundle (sqlite3 session extension). " \
|
|
280
|
+
"Install with `gem install extralite-bundle` and activate it with " \
|
|
281
|
+
"`gem \"extralite-bundle\"` before `require \"extralite\"`."
|
|
282
|
+
end
|
|
283
|
+
# NOTE: the block must run *inside* Changeset#track — that method
|
|
284
|
+
# creates the sqlite session, yields, materializes via
|
|
285
|
+
# sqlite3session_changeset, and tears down in ensure. The db-level
|
|
286
|
+
# track_changes never yields, so calling it blockless dies in
|
|
287
|
+
# rb_yield (LocalJumpError) and calling it first would leave the
|
|
288
|
+
# session closed before the block runs (empty changeset).
|
|
289
|
+
raw = Extralite::Changeset.new
|
|
290
|
+
raw.track(engine.db, tables, &block)
|
|
291
|
+
Diamond::Changeset.new(raw)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
RUNTIME_STATUS_CODES = {
|
|
295
|
+
memory_used: Extralite::SQLITE_STATUS_MEMORY_USED,
|
|
296
|
+
pagecache_used: Extralite::SQLITE_STATUS_PAGECACHE_USED,
|
|
297
|
+
pagecache_overflow: Extralite::SQLITE_STATUS_PAGECACHE_OVERFLOW,
|
|
298
|
+
scratch_used: Extralite::SQLITE_STATUS_SCRATCH_USED,
|
|
299
|
+
scratch_overflow: Extralite::SQLITE_STATUS_SCRATCH_OVERFLOW,
|
|
300
|
+
malloc_size: Extralite::SQLITE_STATUS_MALLOC_SIZE,
|
|
301
|
+
parser_stack: Extralite::SQLITE_STATUS_PARSER_STACK
|
|
302
|
+
}.freeze
|
|
303
|
+
|
|
304
|
+
# Phase 7: run a query with a wall-clock timeout. After `seconds`, a
|
|
305
|
+
# background timer calls Diamond.engine.db.interrupt; the query raises
|
|
306
|
+
# Extralite::InterruptError which we re-wrap as our own error class.
|
|
307
|
+
def self.query_with_timeout(seconds, &block)
|
|
308
|
+
raise ArgumentError, "query_with_timeout requires a block" unless block
|
|
309
|
+
return block.call if seconds.nil? || seconds <= 0
|
|
310
|
+
|
|
311
|
+
timer = Thread.new do
|
|
312
|
+
Kernel.sleep(seconds)
|
|
313
|
+
Diamond.engine.db.interrupt
|
|
314
|
+
end
|
|
315
|
+
begin
|
|
316
|
+
block.call
|
|
317
|
+
rescue Extralite::InterruptError => e
|
|
318
|
+
raise QueryInterruptedError, "Query interrupted after #{seconds}s: #{e.message}"
|
|
319
|
+
ensure
|
|
320
|
+
timer.kill rescue nil
|
|
321
|
+
timer.join rescue nil
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Raised by query_with_timeout when the wall-clock deadline fires.
|
|
326
|
+
class QueryInterruptedError < Error; end
|
|
327
|
+
|
|
328
|
+
# Feature gate: raises if the running Extralite build doesn't expose
|
|
329
|
+
# the requested feature. Use for changesets, etc. that require the
|
|
330
|
+
# extralite-bundle build.
|
|
331
|
+
class FeatureNotAvailableError < Error; end
|
|
332
|
+
|
|
333
|
+
# Phase 7 Fiber amendment + Kino integration. Prefer Kino.sleep when
|
|
334
|
+
# available — it uses the OS clock instead of MRI's coarse timer and
|
|
335
|
+
# is correct inside non-main Ractors. Otherwise Kernel.sleep, which
|
|
336
|
+
# dispatches to the running Fiber::Scheduler's kernel_sleep on its
|
|
337
|
+
# own when one is present (no explicit scheduler branch needed) and
|
|
338
|
+
# sleeps normally when not.
|
|
339
|
+
# `Kernel.sleep` explicitly: bare `sleep` here would resolve to
|
|
340
|
+
# Diamond.sleep itself (self == Diamond) and recurse forever.
|
|
341
|
+
def self.sleep(seconds)
|
|
342
|
+
if defined?(Kino) && Kino.respond_to?(:sleep)
|
|
343
|
+
Kino.sleep(seconds)
|
|
344
|
+
else
|
|
345
|
+
Kernel.sleep(seconds)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Wrap a block in BEGIN/COMMIT. Rolls back on exception.
|
|
350
|
+
#
|
|
351
|
+
# Diamond.transaction do
|
|
352
|
+
# Users.create(name: 'A')
|
|
353
|
+
# Posts.create(title: 'B', user_id: 1)
|
|
354
|
+
# end
|
|
355
|
+
#
|
|
356
|
+
# Pass a mode for explicit transaction type:
|
|
357
|
+
# Diamond.transaction(:deferred) { ... }
|
|
358
|
+
# Diamond.transaction(:immediate) { ... } # default
|
|
359
|
+
# Diamond.transaction(:exclusive) { ... }
|
|
360
|
+
#
|
|
361
|
+
# Returns the block's return value on commit, re-raises on rollback.
|
|
362
|
+
def self.transaction(mode = :immediate, &block)
|
|
363
|
+
raise ArgumentError, "transaction requires a block" unless block
|
|
364
|
+
engine.db.transaction(mode) { block.call }
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Phase 7: nested transactions via SQLite savepoints.
|
|
368
|
+
# Diamond.transaction do
|
|
369
|
+
# Users.create(name: 'A')
|
|
370
|
+
# Diamond.savepoint(:risky_op) do
|
|
371
|
+
# Users.create(name: 'Bad')
|
|
372
|
+
# raise 'oops'
|
|
373
|
+
# end # rolls back to savepoint
|
|
374
|
+
# end # commits Users.create(:A)
|
|
375
|
+
def self.savepoint(name, &block)
|
|
376
|
+
raise ArgumentError, "savepoint requires a block" unless block
|
|
377
|
+
db = engine.db
|
|
378
|
+
db.savepoint(name)
|
|
379
|
+
begin
|
|
380
|
+
result = block.call
|
|
381
|
+
db.release(name)
|
|
382
|
+
result
|
|
383
|
+
rescue StandardError
|
|
384
|
+
db.rollback_to(name)
|
|
385
|
+
raise
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# table names the calling Ractor's engine knows about, sorted.
|
|
390
|
+
def self.tables
|
|
391
|
+
engine.schema_cache.keys.sort
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Phase 7: concurrency tuning shortcuts
|
|
395
|
+
def self.busy_timeout=(val); engine.busy_timeout = val; end
|
|
396
|
+
def self.busy_timeout; engine.busy_timeout; end
|
|
397
|
+
def self.gvl_release_threshold=(val); engine.gvl_release_threshold = val; end
|
|
398
|
+
def self.gvl_release_threshold; engine.gvl_release_threshold; end
|
|
399
|
+
|
|
400
|
+
# Phase 7: progress hook
|
|
401
|
+
def self.on_progress(&block); engine.on_progress(&block); end
|
|
402
|
+
|
|
403
|
+
# Phase 7: load extension
|
|
404
|
+
def self.load_extension(path); engine.load_extension(path); end
|
|
405
|
+
|
|
406
|
+
# Phase 7: tracing
|
|
407
|
+
def self.trace(&block)
|
|
408
|
+
if block
|
|
409
|
+
engine.db.trace(&block)
|
|
410
|
+
else
|
|
411
|
+
engine.db.trace
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Phase 7: runtime status and limits
|
|
416
|
+
def self.runtime_status(code)
|
|
417
|
+
Extralite.runtime_status(RUNTIME_STATUS_CODES.fetch(code))
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def self.status(code)
|
|
421
|
+
engine.db.status(Engine::DBSTATUS_CODES.fetch(code))
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def self.limit(code, value = nil)
|
|
425
|
+
engine.db.limit(code, value)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# drop everything derived from the old schema. runs on reload_schema!.
|
|
429
|
+
# per-Ractor: clears only the calling Ractor's parser/struct/finder
|
|
430
|
+
# caches. operator registries are untouched (use their `clear!`).
|
|
431
|
+
def self.clear_caches!
|
|
432
|
+
Parser.clear_caches!
|
|
433
|
+
StructFactory.clear_caches!
|
|
434
|
+
Domains::DynamicFinders.clear_caches!
|
|
435
|
+
end
|
|
436
|
+
end
|