lutaml-store 0.2.2 → 0.2.4
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 +4 -4
- data/.rubocop_todo.yml +22 -13
- data/lib/lutaml/store/adapter/base.rb +28 -0
- data/lib/lutaml/store/adapter/memory.rb +213 -121
- data/lib/lutaml/store/adapter/sqlite.rb +181 -7
- data/lib/lutaml/store/database_store.rb +302 -49
- data/lib/lutaml/store/predicate.rb +248 -0
- data/lib/lutaml/store/query.rb +259 -0
- data/lib/lutaml/store/version.rb +1 -1
- data/lib/lutaml/store.rb +2 -0
- metadata +12 -4
|
@@ -23,9 +23,11 @@ module Lutaml
|
|
|
23
23
|
setup_database
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# ── Key-value operations ──
|
|
27
|
+
|
|
26
28
|
def get(key)
|
|
27
29
|
result = nil
|
|
28
|
-
|
|
30
|
+
execute_query_raw("SELECT value FROM #{@table_name} WHERE key = ?", [key]) do |row|
|
|
29
31
|
value = row[0]
|
|
30
32
|
begin
|
|
31
33
|
result = JSON.parse(value)
|
|
@@ -55,7 +57,7 @@ module Lutaml
|
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
def exists?(key)
|
|
58
|
-
|
|
60
|
+
execute_query_raw("SELECT 1 FROM #{@table_name} WHERE key = ? LIMIT 1", [key]) do |_row|
|
|
59
61
|
return true
|
|
60
62
|
end
|
|
61
63
|
false
|
|
@@ -63,7 +65,7 @@ module Lutaml
|
|
|
63
65
|
|
|
64
66
|
def all
|
|
65
67
|
result = {}
|
|
66
|
-
|
|
68
|
+
execute_query_raw("SELECT key, value FROM #{@table_name}") do |row|
|
|
67
69
|
result[row[0]] = row[1]
|
|
68
70
|
end
|
|
69
71
|
result
|
|
@@ -76,7 +78,7 @@ module Lutaml
|
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
def size
|
|
79
|
-
|
|
81
|
+
execute_query_raw("SELECT COUNT(*) FROM #{@table_name}") do |row|
|
|
80
82
|
return row[0]
|
|
81
83
|
end
|
|
82
84
|
0
|
|
@@ -84,7 +86,7 @@ module Lutaml
|
|
|
84
86
|
|
|
85
87
|
def keys
|
|
86
88
|
result = []
|
|
87
|
-
|
|
89
|
+
execute_query_raw("SELECT key FROM #{@table_name}") do |row|
|
|
88
90
|
result << row[0]
|
|
89
91
|
end
|
|
90
92
|
result
|
|
@@ -132,6 +134,56 @@ module Lutaml
|
|
|
132
134
|
result
|
|
133
135
|
end
|
|
134
136
|
|
|
137
|
+
# ── Query operations ──
|
|
138
|
+
|
|
139
|
+
def execute_query(query)
|
|
140
|
+
sql, params = build_select_sql(query)
|
|
141
|
+
results = []
|
|
142
|
+
execute_query_raw(sql, params) do |row|
|
|
143
|
+
key = row[0]
|
|
144
|
+
value = parse_json_value(row[1])
|
|
145
|
+
results << [key, value]
|
|
146
|
+
end
|
|
147
|
+
results
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def count_query(query)
|
|
151
|
+
sql, params = build_count_sql(query)
|
|
152
|
+
execute_query_raw(sql, params) do |row|
|
|
153
|
+
return row[0]
|
|
154
|
+
end
|
|
155
|
+
0
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def batch_query(query, after: nil, limit: 1000)
|
|
159
|
+
conditions, params = build_conditions(query)
|
|
160
|
+
|
|
161
|
+
if after
|
|
162
|
+
conditions << "key > ?"
|
|
163
|
+
params << after
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
sql = "SELECT key, value FROM #{@table_name}"
|
|
167
|
+
sql += " WHERE #{conditions.join(" AND ")}" unless conditions.empty?
|
|
168
|
+
sql += " ORDER BY key ASC"
|
|
169
|
+
sql += " LIMIT ?"
|
|
170
|
+
params << limit
|
|
171
|
+
|
|
172
|
+
results = []
|
|
173
|
+
execute_query_raw(sql, params) do |row|
|
|
174
|
+
key = row[0]
|
|
175
|
+
value = parse_json_value(row[1])
|
|
176
|
+
results << [key, value]
|
|
177
|
+
end
|
|
178
|
+
results
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def transaction(&block)
|
|
182
|
+
@db.transaction(&block)
|
|
183
|
+
rescue SQLite3::Exception => e
|
|
184
|
+
raise BackendError, "Transaction failed: #{e.message}"
|
|
185
|
+
end
|
|
186
|
+
|
|
135
187
|
private
|
|
136
188
|
|
|
137
189
|
def setup_database
|
|
@@ -161,7 +213,129 @@ module Lutaml
|
|
|
161
213
|
@db.execute("CREATE INDEX IF NOT EXISTS idx_#{@table_name}_updated_at ON #{@table_name} (updated_at)")
|
|
162
214
|
end
|
|
163
215
|
|
|
164
|
-
|
|
216
|
+
# ── SQL generation ──
|
|
217
|
+
|
|
218
|
+
def build_select_sql(query)
|
|
219
|
+
conditions, params = build_conditions(query)
|
|
220
|
+
|
|
221
|
+
sql = "SELECT key, value FROM #{@table_name}"
|
|
222
|
+
sql += " WHERE #{conditions.join(" AND ")}" unless conditions.empty?
|
|
223
|
+
sql += build_order_clause(query.orders)
|
|
224
|
+
sql += build_limit_offset(query.limit_value, query.offset_value, params)
|
|
225
|
+
|
|
226
|
+
[sql, params]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def build_count_sql(query)
|
|
230
|
+
conditions, params = build_conditions(query)
|
|
231
|
+
|
|
232
|
+
sql = "SELECT COUNT(*) FROM #{@table_name}"
|
|
233
|
+
sql += " WHERE #{conditions.join(" AND ")}" unless conditions.empty?
|
|
234
|
+
|
|
235
|
+
[sql, params]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def build_conditions(query)
|
|
239
|
+
conditions = ["key LIKE ?"]
|
|
240
|
+
params = ["#{query.model_class.name}:%"]
|
|
241
|
+
|
|
242
|
+
query.predicates.each do |pred|
|
|
243
|
+
clause, bind = translate_predicate(pred)
|
|
244
|
+
next unless clause
|
|
245
|
+
|
|
246
|
+
conditions << clause
|
|
247
|
+
params.concat(bind)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
[conditions, params]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
SIMPLE_PREDICATES = {
|
|
254
|
+
Predicate::Equal => "=",
|
|
255
|
+
Predicate::NotEqual => "!=",
|
|
256
|
+
Predicate::GreaterThan => ">",
|
|
257
|
+
Predicate::LessThan => "<",
|
|
258
|
+
Predicate::GreaterThanOrEqual => ">=",
|
|
259
|
+
Predicate::LessThanOrEqual => "<="
|
|
260
|
+
}.freeze
|
|
261
|
+
|
|
262
|
+
def translate_predicate(pred)
|
|
263
|
+
field_json = "json_extract(value, '$.#{pred.field}')"
|
|
264
|
+
|
|
265
|
+
op = SIMPLE_PREDICATES[pred.class]
|
|
266
|
+
return ["#{field_json} #{op} ?", [pred.value]] if op
|
|
267
|
+
|
|
268
|
+
case pred
|
|
269
|
+
when Predicate::Between
|
|
270
|
+
["#{field_json} BETWEEN ? AND ?", [pred.value.first, pred.value.last]]
|
|
271
|
+
when Predicate::NotBetween
|
|
272
|
+
["#{field_json} NOT BETWEEN ? AND ?", [pred.value.first, pred.value.last]]
|
|
273
|
+
when Predicate::In
|
|
274
|
+
["#{field_json} IN (#{in_placeholders(pred)})", pred.value]
|
|
275
|
+
when Predicate::NotIn
|
|
276
|
+
["#{field_json} NOT IN (#{in_placeholders(pred)})", pred.value]
|
|
277
|
+
when Predicate::Matches
|
|
278
|
+
translate_matches(field_json, pred, "LIKE")
|
|
279
|
+
when Predicate::NotMatches
|
|
280
|
+
translate_matches(field_json, pred, "NOT LIKE")
|
|
281
|
+
when Predicate::Nil
|
|
282
|
+
["#{field_json} IS NULL", []]
|
|
283
|
+
when Predicate::NotNil
|
|
284
|
+
["#{field_json} IS NOT NULL", []]
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def in_placeholders(pred)
|
|
289
|
+
pred.value.map { "?" }.join(", ")
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def translate_matches(field_json, pred, op)
|
|
293
|
+
pattern = pred.value.is_a?(Regexp) ? regex_to_like(pred.value) : "%#{pred.value}%"
|
|
294
|
+
["#{field_json} #{op} ?", [pattern]]
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def pred_value(pred) # :nodoc:
|
|
298
|
+
pred.value
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def build_order_clause(orders)
|
|
302
|
+
return "" if orders.empty?
|
|
303
|
+
|
|
304
|
+
clauses = orders.map do |o|
|
|
305
|
+
dir = o.direction == :desc ? "DESC" : "ASC"
|
|
306
|
+
"json_extract(value, '$.#{o.field}') #{dir} NULLS LAST"
|
|
307
|
+
end
|
|
308
|
+
" ORDER BY #{clauses.join(", ")}"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def build_limit_offset(limit, offset, params)
|
|
312
|
+
sql = ""
|
|
313
|
+
if limit
|
|
314
|
+
sql += " LIMIT ?"
|
|
315
|
+
params << limit.to_i
|
|
316
|
+
end
|
|
317
|
+
if offset
|
|
318
|
+
sql += " OFFSET ?"
|
|
319
|
+
params << offset.to_i
|
|
320
|
+
end
|
|
321
|
+
sql
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def regex_to_like(regex)
|
|
325
|
+
source = regex.source
|
|
326
|
+
pattern = source.gsub(".+", "%").gsub(".*", "%").gsub(".", "_").gsub("^", "").gsub("$", "")
|
|
327
|
+
"%#{pattern}%"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def parse_json_value(raw)
|
|
331
|
+
JSON.parse(raw)
|
|
332
|
+
rescue JSON::ParserError
|
|
333
|
+
raw
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# ── Raw query helpers ──
|
|
337
|
+
|
|
338
|
+
def execute_query_raw(sql, params = [])
|
|
165
339
|
@db.execute(sql, params) do |row|
|
|
166
340
|
yield row if block_given?
|
|
167
341
|
end
|
|
@@ -182,7 +356,7 @@ module Lutaml
|
|
|
182
356
|
end
|
|
183
357
|
|
|
184
358
|
def get_schema_version
|
|
185
|
-
|
|
359
|
+
execute_query_raw("PRAGMA user_version") do |row|
|
|
186
360
|
return row[0]
|
|
187
361
|
end
|
|
188
362
|
0
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Store
|
|
5
|
-
# Store-centric API with model registry and database-style operations
|
|
5
|
+
# Store-centric API with model registry and database-style operations.
|
|
6
|
+
# Provides CRUD, querying, file I/O, transactions, and events.
|
|
6
7
|
class DatabaseStore
|
|
7
|
-
attr_reader :store, :registry, :composite_handler, :attribute_updater
|
|
8
|
+
attr_reader :store, :registry, :composite_handler, :attribute_updater, :serializer
|
|
8
9
|
|
|
9
10
|
def initialize(adapter:, models: [], **options)
|
|
10
11
|
@store = BasicStore.new(adapter_type: adapter, **options)
|
|
@@ -12,11 +13,13 @@ module Lutaml
|
|
|
12
13
|
@serializer = ModelSerializer.new
|
|
13
14
|
@composite_handler = CompositeModelHandler.new(@registry, @store, self, serializer: @serializer)
|
|
14
15
|
@attribute_updater = AttributeUpdater.new(@registry, @composite_handler)
|
|
16
|
+
@scopes = {}
|
|
15
17
|
|
|
16
18
|
validate_configuration!
|
|
17
19
|
end
|
|
18
20
|
|
|
19
|
-
#
|
|
21
|
+
# ── CRUD ──
|
|
22
|
+
|
|
20
23
|
def save(models)
|
|
21
24
|
models_array = Array(models)
|
|
22
25
|
saved_models = models_array.map { |model| save_single_model(model) }
|
|
@@ -25,7 +28,6 @@ module Lutaml
|
|
|
25
28
|
models.is_a?(Array) ? saved_models : saved_models.first
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
# Fetch model by class and key field
|
|
29
31
|
def fetch(model:, **key_params)
|
|
30
32
|
registration = @registry.registration_for(model)
|
|
31
33
|
key_field = registration.key_field
|
|
@@ -35,20 +37,12 @@ module Lutaml
|
|
|
35
37
|
stored_data = find_stored_data(registration, model, key_value)
|
|
36
38
|
return nil unless stored_data
|
|
37
39
|
|
|
38
|
-
model_instance =
|
|
39
|
-
|
|
40
|
-
composite_references = stored_data["_composite_models"]
|
|
41
|
-
if composite_references
|
|
42
|
-
model_instance = @composite_handler.restore_composite_models(
|
|
43
|
-
model_instance, composite_references
|
|
44
|
-
)
|
|
45
|
-
end
|
|
40
|
+
model_instance = deserialize_record(stored_data, model, registration)
|
|
46
41
|
|
|
47
42
|
@store.emit_event(:model_fetch, model: model_instance, key: key_value, source: :backend)
|
|
48
43
|
model_instance
|
|
49
44
|
end
|
|
50
45
|
|
|
51
|
-
# Update model with attributes array or block
|
|
52
46
|
def update(model:, attributes: nil, **key_params, &block)
|
|
53
47
|
current_model = fetch(model: model, **key_params)
|
|
54
48
|
raise ModelNotRegisteredError, "Model not found" unless current_model
|
|
@@ -75,7 +69,6 @@ module Lutaml
|
|
|
75
69
|
updated_model
|
|
76
70
|
end
|
|
77
71
|
|
|
78
|
-
# Destroy model by class and key field
|
|
79
72
|
def destroy(model:, **key_params)
|
|
80
73
|
registration = @registry.registration_for(model)
|
|
81
74
|
key_field = registration.key_field
|
|
@@ -94,54 +87,96 @@ module Lutaml
|
|
|
94
87
|
deleted
|
|
95
88
|
end
|
|
96
89
|
|
|
97
|
-
# Query
|
|
90
|
+
# ── Query API ──
|
|
91
|
+
|
|
92
|
+
def query(model_class)
|
|
93
|
+
Query.new(self, model_class)
|
|
94
|
+
end
|
|
95
|
+
|
|
98
96
|
def where(model:, **conditions)
|
|
99
|
-
|
|
100
|
-
conditions.all? { |field, value| model_instance.public_send(field) == value }
|
|
101
|
-
end
|
|
97
|
+
query(model).where(**conditions)
|
|
102
98
|
end
|
|
103
99
|
|
|
104
|
-
# Get all models of a specific type
|
|
105
100
|
def all(model:)
|
|
106
|
-
|
|
101
|
+
query(model)
|
|
102
|
+
end
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
next unless parsed.class_name == model.name
|
|
104
|
+
def count(model:)
|
|
105
|
+
query(model).count
|
|
106
|
+
end
|
|
112
107
|
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
def exists?(model:, **key_params)
|
|
109
|
+
!fetch(model: model, **key_params).nil?
|
|
110
|
+
end
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
def find_by(model:, **conditions)
|
|
113
|
+
query(model).find_by(**conditions)
|
|
114
|
+
end
|
|
118
115
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
model_instance, composite_references
|
|
123
|
-
)
|
|
124
|
-
end
|
|
116
|
+
def find_by!(model:, **conditions)
|
|
117
|
+
query(model).find_by!(**conditions)
|
|
118
|
+
end
|
|
125
119
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
120
|
+
# ── Transactions ──
|
|
121
|
+
|
|
122
|
+
def transaction(&block)
|
|
123
|
+
adapter.transaction(&block)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# ── Scopes ──
|
|
127
|
+
|
|
128
|
+
def scope(name, body)
|
|
129
|
+
@scopes[name.to_sym] = body
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def scope_for(name)
|
|
133
|
+
@scopes[name.to_sym] || raise(ArgumentError, "Unknown scope: #{name}")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# ── Query execution (called by Query) ──
|
|
137
|
+
|
|
138
|
+
def execute_query(query)
|
|
139
|
+
registration = @registry.registration_for(query.model_class)
|
|
140
|
+
|
|
141
|
+
if custom_serializer?(registration)
|
|
142
|
+
execute_with_post_filter(query, registration)
|
|
143
|
+
else
|
|
144
|
+
execute_with_pre_filter(query, registration)
|
|
130
145
|
end
|
|
146
|
+
end
|
|
131
147
|
|
|
132
|
-
|
|
148
|
+
def count_query(query)
|
|
149
|
+
registration = @registry.registration_for(query.model_class)
|
|
150
|
+
|
|
151
|
+
if custom_serializer?(registration)
|
|
152
|
+
execute_with_post_filter(query, registration).size
|
|
153
|
+
else
|
|
154
|
+
count_with_pre_filter(query, registration)
|
|
155
|
+
end
|
|
133
156
|
end
|
|
134
157
|
|
|
135
|
-
def
|
|
136
|
-
|
|
158
|
+
def fetch_batch(query, after: nil, limit: 1000)
|
|
159
|
+
scan_query = Query.new(self, query.model_class)
|
|
160
|
+
raw_batch = adapter.batch_query(scan_query, after: after, limit: limit)
|
|
161
|
+
if raw_batch
|
|
162
|
+
models = deserialize_results(raw_batch, query.model_class)
|
|
163
|
+
query.predicates.empty? ? models : models.select { |m| query.predicates.all? { |p| p.match?(m) } }
|
|
164
|
+
else
|
|
165
|
+
fallback_batch(query, after: after, limit: limit)
|
|
166
|
+
end
|
|
137
167
|
end
|
|
138
168
|
|
|
139
|
-
def
|
|
140
|
-
|
|
169
|
+
def last_storage_key_from(models, model_class)
|
|
170
|
+
return nil if models.empty?
|
|
171
|
+
|
|
172
|
+
registration = @registry.registration_for(model_class)
|
|
173
|
+
last_model = models.last
|
|
174
|
+
key_value = registration.extract_key(last_model)
|
|
175
|
+
registration.generate_storage_key_from_value(key_value).to_s
|
|
141
176
|
end
|
|
142
177
|
|
|
143
|
-
#
|
|
144
|
-
|
|
178
|
+
# ── File I/O ──
|
|
179
|
+
|
|
145
180
|
def load_all(model_class, path: nil, format: :yaml, layout: :separate)
|
|
146
181
|
fmt = Format.resolve(format)
|
|
147
182
|
dir = resolve_model_dir(model_class, path)
|
|
@@ -161,8 +196,6 @@ module Lutaml
|
|
|
161
196
|
end
|
|
162
197
|
end
|
|
163
198
|
|
|
164
|
-
# Load from directory and store into the key-value backend.
|
|
165
|
-
# Returns the loaded models and makes them available via fetch/where/all.
|
|
166
199
|
def import_all(model_class, path: nil, format: :yaml, layout: :separate)
|
|
167
200
|
models = load_all(model_class, path: path, format: format, layout: layout)
|
|
168
201
|
models.each { |model| save(model) }
|
|
@@ -170,7 +203,6 @@ module Lutaml
|
|
|
170
203
|
models
|
|
171
204
|
end
|
|
172
205
|
|
|
173
|
-
# Save all models to a directory using format-specific serialization.
|
|
174
206
|
def save_all(models, path: nil, format: :yaml, layout: :separate)
|
|
175
207
|
fmt = Format.resolve(format)
|
|
176
208
|
models_array = Array(models)
|
|
@@ -198,7 +230,6 @@ module Lutaml
|
|
|
198
230
|
saved
|
|
199
231
|
end
|
|
200
232
|
|
|
201
|
-
# Export models to a single file or directory.
|
|
202
233
|
def export(models, path:, format: :yaml)
|
|
203
234
|
fmt = Format.resolve(format)
|
|
204
235
|
models_array = Array(models)
|
|
@@ -212,6 +243,8 @@ module Lutaml
|
|
|
212
243
|
path
|
|
213
244
|
end
|
|
214
245
|
|
|
246
|
+
# ── Events ──
|
|
247
|
+
|
|
215
248
|
def on(event, &block)
|
|
216
249
|
@store.on(event, &block)
|
|
217
250
|
end
|
|
@@ -235,6 +268,226 @@ module Lutaml
|
|
|
235
268
|
|
|
236
269
|
private
|
|
237
270
|
|
|
271
|
+
def adapter
|
|
272
|
+
@store.adapter
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def custom_serializer?(registration)
|
|
276
|
+
registration&.serializer
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Pre-deserialization filtering: predicates evaluated on raw hash data.
|
|
280
|
+
# Used when the serializer stores model fields as direct hash keys.
|
|
281
|
+
def execute_with_pre_filter(query, registration)
|
|
282
|
+
adapter_results = adapter.execute_query(query)
|
|
283
|
+
|
|
284
|
+
if adapter_results
|
|
285
|
+
deserialize_results(adapter_results, query.model_class)
|
|
286
|
+
else
|
|
287
|
+
fallback_execute(query, registration)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def count_with_pre_filter(query, registration)
|
|
292
|
+
adapter_count = adapter.count_query(query)
|
|
293
|
+
return adapter_count unless adapter_count.nil?
|
|
294
|
+
|
|
295
|
+
adapter_results = adapter.execute_query(query)
|
|
296
|
+
if adapter_results
|
|
297
|
+
adapter_results.size
|
|
298
|
+
else
|
|
299
|
+
fallback_count(query, registration)
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Post-deserialization filtering: scan by class name only, deserialize
|
|
304
|
+
# everything, then filter predicates on model instances.
|
|
305
|
+
# Used when custom serializers store data in non-standard hash format.
|
|
306
|
+
def execute_with_post_filter(query, _registration)
|
|
307
|
+
scan_query = Query.new(self, query.model_class)
|
|
308
|
+
adapter_results = adapter.execute_query(scan_query)
|
|
309
|
+
|
|
310
|
+
raw_results = adapter_results || scan_all_by_class(query.model_class)
|
|
311
|
+
|
|
312
|
+
models = deserialize_results(raw_results, query.model_class)
|
|
313
|
+
results = apply_model_predicates(models, query.predicates)
|
|
314
|
+
results = apply_model_sort(results, query.orders)
|
|
315
|
+
apply_model_pagination(results, query.limit_value, query.offset_value)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def scan_all_by_class(model_class)
|
|
319
|
+
results = []
|
|
320
|
+
@store.each_key do |storage_key|
|
|
321
|
+
parsed = StorageKey.parse(storage_key.to_s)
|
|
322
|
+
next unless parsed.class_name == model_class.name
|
|
323
|
+
|
|
324
|
+
stored_data = @store.get(storage_key)
|
|
325
|
+
results << [storage_key, stored_data] if stored_data
|
|
326
|
+
rescue StandardError
|
|
327
|
+
next
|
|
328
|
+
end
|
|
329
|
+
results
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def apply_model_predicates(models, predicates)
|
|
333
|
+
return models if predicates.empty?
|
|
334
|
+
|
|
335
|
+
models.select { |m| predicates.all? { |p| p.match?(m) } }
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def apply_model_sort(models, orders)
|
|
339
|
+
return models if orders.empty?
|
|
340
|
+
|
|
341
|
+
models.sort do |a, b|
|
|
342
|
+
orders.reduce(0) do |cmp, o|
|
|
343
|
+
break cmp unless cmp.zero?
|
|
344
|
+
|
|
345
|
+
va = a.public_send(o.field)
|
|
346
|
+
vb = b.public_send(o.field)
|
|
347
|
+
|
|
348
|
+
cmp_val = if va.nil? && vb.nil?
|
|
349
|
+
0
|
|
350
|
+
elsif va.nil?
|
|
351
|
+
1
|
|
352
|
+
elsif vb.nil?
|
|
353
|
+
-1
|
|
354
|
+
else
|
|
355
|
+
va <=> vb || 0
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
o.direction == :desc ? -cmp_val : cmp_val
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def apply_model_pagination(models, limit, offset)
|
|
364
|
+
start = offset || 0
|
|
365
|
+
models = models[start..] || []
|
|
366
|
+
models = models.first(limit) if limit
|
|
367
|
+
models
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def deserialize_record(hash_data, model_class, registration)
|
|
371
|
+
model_instance = @serializer.deserialize(hash_data, model_class, registration)
|
|
372
|
+
|
|
373
|
+
composite_references = hash_data["_composite_models"]
|
|
374
|
+
if composite_references
|
|
375
|
+
model_instance = @composite_handler.restore_composite_models(
|
|
376
|
+
model_instance, composite_references
|
|
377
|
+
)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
model_instance
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def deserialize_results(raw_results, model_class)
|
|
384
|
+
registration = @registry.registration_for(model_class)
|
|
385
|
+
|
|
386
|
+
raw_results.filter_map do |(storage_key, hash_data)|
|
|
387
|
+
deserialize_record(hash_data, model_class, registration)
|
|
388
|
+
rescue StandardError => e
|
|
389
|
+
@store.emit_event(:deserialization_error, key: storage_key, error: e)
|
|
390
|
+
nil
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# ── Fallback query execution (no adapter query support) ──
|
|
395
|
+
|
|
396
|
+
def fallback_execute(query, _registration)
|
|
397
|
+
results = []
|
|
398
|
+
|
|
399
|
+
@store.each_key do |storage_key|
|
|
400
|
+
parsed = StorageKey.parse(storage_key.to_s)
|
|
401
|
+
next unless parsed.class_name == query.model_class.name
|
|
402
|
+
|
|
403
|
+
stored_data = @store.get(storage_key)
|
|
404
|
+
next unless stored_data
|
|
405
|
+
next unless query.predicates.all? { |p| p.match?(stored_data) }
|
|
406
|
+
|
|
407
|
+
results << [storage_key, stored_data]
|
|
408
|
+
rescue StandardError
|
|
409
|
+
next
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
results = apply_sort(results, query.orders)
|
|
413
|
+
results = apply_pagination(results, query.limit_value, query.offset_value)
|
|
414
|
+
deserialize_results(results, query.model_class)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def fallback_count(query, _registration)
|
|
418
|
+
count = 0
|
|
419
|
+
|
|
420
|
+
@store.each_key do |storage_key|
|
|
421
|
+
parsed = StorageKey.parse(storage_key.to_s)
|
|
422
|
+
next unless parsed.class_name == query.model_class.name
|
|
423
|
+
|
|
424
|
+
stored_data = @store.get(storage_key)
|
|
425
|
+
next unless stored_data
|
|
426
|
+
next unless query.predicates.all? { |p| p.match?(stored_data) }
|
|
427
|
+
|
|
428
|
+
count += 1
|
|
429
|
+
rescue StandardError
|
|
430
|
+
next
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
count
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def fallback_batch(query, after: nil, limit: 1000)
|
|
437
|
+
results = []
|
|
438
|
+
|
|
439
|
+
@store.each_key do |storage_key|
|
|
440
|
+
parsed = StorageKey.parse(storage_key.to_s)
|
|
441
|
+
next unless parsed.class_name == query.model_class.name
|
|
442
|
+
next if after && storage_key.to_s <= after
|
|
443
|
+
|
|
444
|
+
stored_data = @store.get(storage_key)
|
|
445
|
+
next unless stored_data
|
|
446
|
+
next unless query.predicates.all? { |p| p.match?(stored_data) }
|
|
447
|
+
|
|
448
|
+
results << [storage_key, stored_data]
|
|
449
|
+
break if results.size >= limit
|
|
450
|
+
rescue StandardError
|
|
451
|
+
next
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
deserialize_results(results, query.model_class)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
def apply_sort(results, orders)
|
|
458
|
+
return results if orders.empty?
|
|
459
|
+
|
|
460
|
+
results.sort do |a, b|
|
|
461
|
+
orders.reduce(0) do |cmp, o|
|
|
462
|
+
break cmp unless cmp.zero?
|
|
463
|
+
|
|
464
|
+
va = a.last.is_a?(Hash) ? a.last[o.field.to_s] : nil
|
|
465
|
+
vb = b.last.is_a?(Hash) ? b.last[o.field.to_s] : nil
|
|
466
|
+
|
|
467
|
+
cmp_val = if va.nil? && vb.nil?
|
|
468
|
+
0
|
|
469
|
+
elsif va.nil?
|
|
470
|
+
1
|
|
471
|
+
elsif vb.nil?
|
|
472
|
+
-1
|
|
473
|
+
else
|
|
474
|
+
va <=> vb || 0
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
o.direction == :desc ? -cmp_val : cmp_val
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def apply_pagination(results, limit, offset)
|
|
483
|
+
start = offset || 0
|
|
484
|
+
results = results[start..] || []
|
|
485
|
+
results = results.first(limit) if limit
|
|
486
|
+
results
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
# ── CRUD internals ──
|
|
490
|
+
|
|
238
491
|
def find_stored_data(registration, model, key_value)
|
|
239
492
|
if registration.polymorphic?
|
|
240
493
|
find_polymorphic_data(model, key_value)
|