activeitem 0.0.7 → 0.0.9
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
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +15 -0
- data/certs/stowzilla.pem +24 -29
- data/lib/active_item/relation.rb +303 -3
- data/lib/active_item/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +27 -32
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b243ed910a2d03058939e87540ae8db73306ce0b550497c9a2f7aab8cf4cb1b4
|
|
4
|
+
data.tar.gz: ebb28b886aa8f1e7fe45c0efe1dd760af15a729f434e5beb5075952bb059fee3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18250436726865a0397d3db991a911356f41a59b7d5b0542e56d8e171e27094d3c19d43fb73a6c42b6d1b705544432cc3f928f7bbf70afdd69ae3dbc2c8f78b3
|
|
7
|
+
data.tar.gz: 17677be007f18965b1206c1fb8274835dc86b923b54a0f43bd48f20750edd2eb052f38412d86357e28ef614207f2a24fb614e3c9ecd4878149bd8aa5ab7c61a0
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.9
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Pagination cursor loss in `paginated_query_with_index`** — when the 2x overfetch returned all remaining items in a single DynamoDB response (no `LastEvaluatedKey`), the method returned `has_more: false` despite having silently trimmed items. Now builds a synthetic `ExclusiveStartKey` from the last returned item's attributes (table PK + GSI partition/sort keys) to ensure correct pagination.
|
|
8
|
+
|
|
9
|
+
## 0.0.8
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Array partition key fan-out** — `where(partition_key: [val1, val2, ...])` now queries each partition in parallel and merges results, instead of raising `ArgumentError`
|
|
14
|
+
- Works with `.to_a`, `.page(cursor, per_page:)`, `.count`, `.order(:desc)`, `.not(...)`, and all other chainable methods
|
|
15
|
+
- Pagination uses a `sort_val|id` cursor format for cross-partition consistency
|
|
16
|
+
- Thread-pooled execution (max 10 concurrent queries) matching existing `preload_has_many_counts` pattern
|
|
17
|
+
|
|
3
18
|
## 0.0.7
|
|
4
19
|
|
|
5
20
|
### Added
|
data/certs/stowzilla.pem
CHANGED
|
@@ -1,31 +1,26 @@
|
|
|
1
1
|
-----BEGIN CERTIFICATE-----
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
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
-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
-
+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
-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
-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
|
|
31
26
|
-----END CERTIFICATE-----
|
data/lib/active_item/relation.rb
CHANGED
|
@@ -507,10 +507,20 @@ module ActiveItem
|
|
|
507
507
|
return [[], nil] if conditions[:_empty]
|
|
508
508
|
|
|
509
509
|
normalized_conditions = normalize_conditions(conditions)
|
|
510
|
-
exclusive_start_key = decode_cursor(cursor)
|
|
511
510
|
|
|
512
511
|
effective_index = index_name || resolved_model.send(:detect_index_for_conditions, normalized_conditions)
|
|
513
512
|
|
|
513
|
+
# Array partition key fan-out uses a simple string cursor (sort_val|id), not DynamoDB LastEvaluatedKey
|
|
514
|
+
if effective_index && normalized_conditions.any? && normalized_conditions.values.first.is_a?(Array)
|
|
515
|
+
index_config = resolved_model.indexes[effective_index] || {}
|
|
516
|
+
ruby_partition_key = normalized_conditions.keys.first.to_s
|
|
517
|
+
dynamo_partition_key = index_config[:partition_key]&.to_s || resolved_model.to_dynamo_key(ruby_partition_key)
|
|
518
|
+
return fanout_paginated_query(effective_index, normalized_conditions, index_config, dynamo_partition_key,
|
|
519
|
+
normalized_conditions.values.first, cursor, per_page)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
exclusive_start_key = decode_cursor(cursor)
|
|
523
|
+
|
|
514
524
|
if effective_index && normalized_conditions.any?
|
|
515
525
|
paginated_query_with_index(effective_index, normalized_conditions, exclusive_start_key, per_page)
|
|
516
526
|
else
|
|
@@ -651,12 +661,24 @@ module ActiveItem
|
|
|
651
661
|
|
|
652
662
|
# Trim to requested page size and determine next cursor
|
|
653
663
|
if collected_items.length > per_page
|
|
654
|
-
[
|
|
664
|
+
next_cursor = last_evaluated_key || build_synthetic_cursor(collected_items[per_page - 1], idx_name, index_config)
|
|
665
|
+
[collected_items.take(per_page), encode_cursor(next_cursor)]
|
|
655
666
|
else
|
|
656
667
|
[collected_items, encode_cursor(last_evaluated_key)]
|
|
657
668
|
end
|
|
658
669
|
end
|
|
659
670
|
|
|
671
|
+
# Build a synthetic ExclusiveStartKey from an item when DynamoDB didn't paginate
|
|
672
|
+
# but we have more items than per_page (overfetch returned everything in one shot).
|
|
673
|
+
# For GSI queries, DynamoDB requires: table PK + index partition key + index sort key (if any).
|
|
674
|
+
def build_synthetic_cursor(item, _idx_name, index_config)
|
|
675
|
+
pk_dynamo = resolved_model.to_dynamo_key(resolved_model.primary_key.to_s)
|
|
676
|
+
cursor = { pk_dynamo => item.id }
|
|
677
|
+
cursor[index_config[:partition_key].to_s] = item.send(resolved_model.from_dynamo_key(index_config[:partition_key].to_s)) if index_config[:partition_key]
|
|
678
|
+
cursor[index_config[:sort_key].to_s] = item.send(resolved_model.from_dynamo_key(index_config[:sort_key].to_s)) if index_config[:sort_key]
|
|
679
|
+
cursor
|
|
680
|
+
end
|
|
681
|
+
|
|
660
682
|
# Execute paginated scan
|
|
661
683
|
# Keeps fetching until we have per_page items that pass the filter
|
|
662
684
|
def paginated_scan_with_conditions(normalized_conditions, exclusive_start_key, per_page)
|
|
@@ -755,6 +777,9 @@ module ActiveItem
|
|
|
755
777
|
|
|
756
778
|
effective_index = (index_name || resolved_model.send(:detect_index_for_conditions, normalized_conditions) if normalized_conditions.any?)
|
|
757
779
|
|
|
780
|
+
# Array partition key: fan-out count queries in parallel
|
|
781
|
+
return fanout_count_query(effective_index, normalized_conditions) if effective_index && normalized_conditions.any? && normalized_conditions.values.first.is_a?(Array)
|
|
782
|
+
|
|
758
783
|
total = 0
|
|
759
784
|
exclusive_start_key = nil
|
|
760
785
|
|
|
@@ -1034,7 +1059,7 @@ module ActiveItem
|
|
|
1034
1059
|
index_config = resolved_model.indexes[idx_name] || {}
|
|
1035
1060
|
dynamo_partition_key = index_config[:partition_key]&.to_s || resolved_model.to_dynamo_key(ruby_partition_key)
|
|
1036
1061
|
|
|
1037
|
-
|
|
1062
|
+
return fanout_query(idx_name, normalized_conditions, index_config, dynamo_partition_key, partition_value) if partition_value.is_a?(Array)
|
|
1038
1063
|
|
|
1039
1064
|
raise ArgumentError, 'Range values not supported for partition key queries. Use scan instead.' if partition_value.is_a?(Range)
|
|
1040
1065
|
|
|
@@ -1120,6 +1145,281 @@ module ActiveItem
|
|
|
1120
1145
|
all_items
|
|
1121
1146
|
end
|
|
1122
1147
|
|
|
1148
|
+
# Fan-out parallel queries when partition key is an Array.
|
|
1149
|
+
# Queries each partition key in parallel, merges and optionally sorts results.
|
|
1150
|
+
def fanout_query(idx_name, _normalized_conditions, index_config, dynamo_partition_key, partition_values)
|
|
1151
|
+
return [] if partition_values.empty?
|
|
1152
|
+
|
|
1153
|
+
sort_key = index_config[:sort_key]&.to_s
|
|
1154
|
+
remaining_conditions = conditions.to_a[1..]
|
|
1155
|
+
filter_parts, filter_names, filter_values = build_fanout_filters(sort_key, remaining_conditions)
|
|
1156
|
+
|
|
1157
|
+
all_items = []
|
|
1158
|
+
mutex = Mutex.new
|
|
1159
|
+
max_concurrency = 10
|
|
1160
|
+
|
|
1161
|
+
partition_values.each_slice(max_concurrency) do |batch|
|
|
1162
|
+
threads = batch.map do |pk_val|
|
|
1163
|
+
Thread.new do
|
|
1164
|
+
params = {
|
|
1165
|
+
table_name: resolved_model.table_name,
|
|
1166
|
+
index_name: idx_name,
|
|
1167
|
+
key_condition_expression: '#pk = :pk_val',
|
|
1168
|
+
expression_attribute_names: { '#pk' => dynamo_partition_key }.merge(filter_names),
|
|
1169
|
+
expression_attribute_values: { ':pk_val' => pk_val }.merge(filter_values)
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
apply_sort_key_to_params!(params, sort_key, remaining_conditions)
|
|
1173
|
+
params[:filter_expression] = filter_parts.join(' AND ') if filter_parts.any?
|
|
1174
|
+
params[:limit] = limit_value if limit_value
|
|
1175
|
+
params[:scan_index_forward] = (order_direction != :desc) unless order_direction.nil?
|
|
1176
|
+
apply_projection_expression!(params)
|
|
1177
|
+
|
|
1178
|
+
items = []
|
|
1179
|
+
exclusive_start_key = nil
|
|
1180
|
+
loop do
|
|
1181
|
+
params[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
|
1182
|
+
response = resolved_model.dynamodb.query(params)
|
|
1183
|
+
items.concat(response.items.map { |item| resolved_model.instantiate(item) })
|
|
1184
|
+
exclusive_start_key = response.last_evaluated_key
|
|
1185
|
+
break unless exclusive_start_key
|
|
1186
|
+
break if limit_value && items.length >= limit_value
|
|
1187
|
+
end
|
|
1188
|
+
mutex.synchronize { all_items.concat(items) }
|
|
1189
|
+
end
|
|
1190
|
+
end
|
|
1191
|
+
threads.each(&:join)
|
|
1192
|
+
end
|
|
1193
|
+
|
|
1194
|
+
all_items = apply_ilike_filter(all_items)
|
|
1195
|
+
all_items = sort_fanout_results(all_items, sort_key)
|
|
1196
|
+
all_items = all_items.first(limit_value) if limit_value
|
|
1197
|
+
all_items
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1200
|
+
# Fan-out paginated query across multiple partition keys.
|
|
1201
|
+
# Uses a composite cursor that encodes sort_key|id position for cross-partition pagination.
|
|
1202
|
+
def fanout_paginated_query(idx_name, _normalized_conditions, index_config, dynamo_partition_key, partition_values,
|
|
1203
|
+
cursor, per_page)
|
|
1204
|
+
return [[], nil] if partition_values.empty?
|
|
1205
|
+
|
|
1206
|
+
sort_key = index_config[:sort_key]&.to_s
|
|
1207
|
+
remaining_conditions = conditions.to_a[1..]
|
|
1208
|
+
filter_parts, filter_names, filter_values = build_fanout_filters(sort_key, remaining_conditions)
|
|
1209
|
+
cursor_sort_val, cursor_id = decode_fanout_cursor(cursor)
|
|
1210
|
+
|
|
1211
|
+
all_items = []
|
|
1212
|
+
mutex = Mutex.new
|
|
1213
|
+
max_concurrency = 10
|
|
1214
|
+
|
|
1215
|
+
partition_values.each_slice(max_concurrency) do |batch|
|
|
1216
|
+
threads = batch.map do |pk_val|
|
|
1217
|
+
Thread.new do
|
|
1218
|
+
params = {
|
|
1219
|
+
table_name: resolved_model.table_name,
|
|
1220
|
+
index_name: idx_name,
|
|
1221
|
+
key_condition_expression: '#pk = :pk_val',
|
|
1222
|
+
expression_attribute_names: { '#pk' => dynamo_partition_key }.merge(filter_names),
|
|
1223
|
+
expression_attribute_values: { ':pk_val' => pk_val }.merge(filter_values)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
apply_sort_key_to_params!(params, sort_key, remaining_conditions)
|
|
1227
|
+
|
|
1228
|
+
# Apply cursor range condition to skip already-seen items
|
|
1229
|
+
if cursor_sort_val && sort_key
|
|
1230
|
+
sk_placeholder = '#fanout_sk'
|
|
1231
|
+
val_placeholder = ':fanout_sk_val'
|
|
1232
|
+
op = order_direction == :desc ? '<=' : '>='
|
|
1233
|
+
params[:key_condition_expression] += " AND #{sk_placeholder} #{op} #{val_placeholder}"
|
|
1234
|
+
params[:expression_attribute_names][sk_placeholder] = sort_key
|
|
1235
|
+
params[:expression_attribute_values][val_placeholder] = cursor_sort_val
|
|
1236
|
+
end
|
|
1237
|
+
|
|
1238
|
+
params[:filter_expression] = filter_parts.join(' AND ') if filter_parts.any?
|
|
1239
|
+
params[:scan_index_forward] = (order_direction != :desc) unless order_direction.nil?
|
|
1240
|
+
# Fetch enough to fill a page after merge
|
|
1241
|
+
params[:limit] = per_page + 1
|
|
1242
|
+
|
|
1243
|
+
items = []
|
|
1244
|
+
exclusive_start_key = nil
|
|
1245
|
+
loop do
|
|
1246
|
+
params[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
|
1247
|
+
response = resolved_model.dynamodb.query(params)
|
|
1248
|
+
items.concat(response.items.map { |item| resolved_model.instantiate(item) })
|
|
1249
|
+
exclusive_start_key = response.last_evaluated_key
|
|
1250
|
+
break unless exclusive_start_key
|
|
1251
|
+
break if items.length >= per_page + 1
|
|
1252
|
+
end
|
|
1253
|
+
mutex.synchronize { all_items.concat(items) }
|
|
1254
|
+
end
|
|
1255
|
+
end
|
|
1256
|
+
threads.each(&:join)
|
|
1257
|
+
end
|
|
1258
|
+
|
|
1259
|
+
all_items = apply_ilike_filter(all_items)
|
|
1260
|
+
all_items = sort_fanout_results(all_items, sort_key)
|
|
1261
|
+
|
|
1262
|
+
# Skip items at or before the cursor position
|
|
1263
|
+
if cursor_sort_val && cursor_id
|
|
1264
|
+
all_items = all_items.drop_while do |item|
|
|
1265
|
+
sk_val = if sort_key
|
|
1266
|
+
item.respond_to?(sort_key) ? item.send(sort_key) : item.send(underscore(sort_key))
|
|
1267
|
+
end
|
|
1268
|
+
sk_val ||= item.respond_to?(:created_at) ? item.created_at : nil
|
|
1269
|
+
cmp = compare_with_nil(sk_val, cursor_sort_val)
|
|
1270
|
+
if order_direction == :desc
|
|
1271
|
+
cmp.positive? || (cmp.zero? && item.id >= cursor_id)
|
|
1272
|
+
else
|
|
1273
|
+
cmp.negative? || (cmp.zero? && item.id <= cursor_id)
|
|
1274
|
+
end
|
|
1275
|
+
end
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1278
|
+
page_items = all_items.first(per_page)
|
|
1279
|
+
next_cursor = (encode_fanout_cursor(page_items.last, sort_key) if all_items.length > per_page && page_items.last)
|
|
1280
|
+
|
|
1281
|
+
[page_items, next_cursor]
|
|
1282
|
+
end
|
|
1283
|
+
|
|
1284
|
+
def build_fanout_filters(sort_key, remaining_conditions)
|
|
1285
|
+
filter_parts = []
|
|
1286
|
+
filter_names = {}
|
|
1287
|
+
filter_values = {}
|
|
1288
|
+
|
|
1289
|
+
# Exclude sort key conditions — those go into key_condition_expression
|
|
1290
|
+
non_sort_conditions = remaining_conditions.reject do |k, _|
|
|
1291
|
+
sort_key && (resolved_model.to_dynamo_key(k.to_s) == sort_key || k.to_s == sort_key)
|
|
1292
|
+
end
|
|
1293
|
+
|
|
1294
|
+
non_sort_conditions.each_with_index do |(attr, val), idx|
|
|
1295
|
+
expr, names, values = resolved_model.send(:build_condition_expression, attr, val, idx, ilike: ilike)
|
|
1296
|
+
filter_parts << expr
|
|
1297
|
+
filter_names.merge!(names)
|
|
1298
|
+
filter_values.merge!(values) if values.any?
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
not_conditions.each_with_index do |(attr, val), idx|
|
|
1302
|
+
expr, names, values = build_not_condition_expression(attr, val, "not#{idx}")
|
|
1303
|
+
filter_parts << expr
|
|
1304
|
+
filter_names.merge!(names)
|
|
1305
|
+
filter_values.merge!(values) if values.any?
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
[filter_parts, filter_names, filter_values]
|
|
1309
|
+
end
|
|
1310
|
+
|
|
1311
|
+
def apply_sort_key_to_params!(params, sort_key, remaining_conditions)
|
|
1312
|
+
return unless sort_key && remaining_conditions.any?
|
|
1313
|
+
|
|
1314
|
+
sort_condition = remaining_conditions.find do |k, _|
|
|
1315
|
+
resolved_model.to_dynamo_key(k.to_s) == sort_key || k.to_s == sort_key
|
|
1316
|
+
end
|
|
1317
|
+
return unless sort_condition
|
|
1318
|
+
|
|
1319
|
+
_, sort_value = sort_condition
|
|
1320
|
+
if sort_value.is_a?(Range)
|
|
1321
|
+
range_condition = resolved_model.send(:build_sort_key_range_condition, sort_key, sort_value)
|
|
1322
|
+
params[:key_condition_expression] += " AND #{range_condition[:expression]}"
|
|
1323
|
+
params[:expression_attribute_names].merge!(range_condition[:names])
|
|
1324
|
+
params[:expression_attribute_values].merge!(range_condition[:values])
|
|
1325
|
+
else
|
|
1326
|
+
params[:key_condition_expression] += ' AND #sk = :sk_val'
|
|
1327
|
+
params[:expression_attribute_names]['#sk'] = sort_key
|
|
1328
|
+
params[:expression_attribute_values][':sk_val'] = sort_value
|
|
1329
|
+
end
|
|
1330
|
+
end
|
|
1331
|
+
|
|
1332
|
+
def sort_fanout_results(items, sort_key)
|
|
1333
|
+
return items unless sort_key
|
|
1334
|
+
|
|
1335
|
+
sorted = items.sort_by do |item|
|
|
1336
|
+
sk_val = item.respond_to?(sort_key) ? item.send(sort_key) : item.send(underscore(sort_key))
|
|
1337
|
+
sk_val ||= ''
|
|
1338
|
+
if order_direction == :desc
|
|
1339
|
+
[sk_val, item.id].map { |v| v.is_a?(String) ? v : v.to_s }
|
|
1340
|
+
else
|
|
1341
|
+
[sk_val, item.id]
|
|
1342
|
+
end
|
|
1343
|
+
end
|
|
1344
|
+
|
|
1345
|
+
order_direction == :desc ? sorted.reverse : sorted
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
def decode_fanout_cursor(cursor)
|
|
1349
|
+
return [nil, nil] if cursor.nil? || cursor.empty?
|
|
1350
|
+
|
|
1351
|
+
sort_val, id = cursor.split('|', 2)
|
|
1352
|
+
[sort_val, id]
|
|
1353
|
+
rescue StandardError
|
|
1354
|
+
[nil, nil]
|
|
1355
|
+
end
|
|
1356
|
+
|
|
1357
|
+
def encode_fanout_cursor(item, sort_key)
|
|
1358
|
+
sk_val = if sort_key
|
|
1359
|
+
item.respond_to?(sort_key) ? item.send(sort_key) : item.send(underscore(sort_key))
|
|
1360
|
+
end
|
|
1361
|
+
sk_val ||= item.respond_to?(:created_at) ? item.created_at : nil
|
|
1362
|
+
"#{sk_val}|#{item.id}"
|
|
1363
|
+
end
|
|
1364
|
+
|
|
1365
|
+
def compare_with_nil(left, right)
|
|
1366
|
+
return 0 if left.nil? && right.nil?
|
|
1367
|
+
return -1 if left.nil?
|
|
1368
|
+
return 1 if right.nil?
|
|
1369
|
+
|
|
1370
|
+
left <=> right
|
|
1371
|
+
end
|
|
1372
|
+
|
|
1373
|
+
def underscore(str)
|
|
1374
|
+
str.gsub(/([A-Z])/, '_\1').sub(/^_/, '').downcase
|
|
1375
|
+
end
|
|
1376
|
+
|
|
1377
|
+
def fanout_count_query(idx_name, normalized_conditions)
|
|
1378
|
+
partition_values = normalized_conditions.values.first
|
|
1379
|
+
index_config = resolved_model.indexes[idx_name] || {}
|
|
1380
|
+
ruby_partition_key = normalized_conditions.keys.first.to_s
|
|
1381
|
+
dynamo_partition_key = index_config[:partition_key]&.to_s || resolved_model.to_dynamo_key(ruby_partition_key)
|
|
1382
|
+
sort_key = index_config[:sort_key]&.to_s
|
|
1383
|
+
remaining_conditions = conditions.to_a[1..]
|
|
1384
|
+
filter_parts, filter_names, filter_values = build_fanout_filters(sort_key, remaining_conditions)
|
|
1385
|
+
|
|
1386
|
+
total = 0
|
|
1387
|
+
mutex = Mutex.new
|
|
1388
|
+
max_concurrency = 10
|
|
1389
|
+
|
|
1390
|
+
partition_values.each_slice(max_concurrency) do |batch|
|
|
1391
|
+
threads = batch.map do |pk_val|
|
|
1392
|
+
Thread.new do
|
|
1393
|
+
params = {
|
|
1394
|
+
table_name: resolved_model.table_name,
|
|
1395
|
+
index_name: idx_name,
|
|
1396
|
+
select: 'COUNT',
|
|
1397
|
+
key_condition_expression: '#pk = :pk_val',
|
|
1398
|
+
expression_attribute_names: { '#pk' => dynamo_partition_key }.merge(filter_names),
|
|
1399
|
+
expression_attribute_values: { ':pk_val' => pk_val }.merge(filter_values)
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
apply_sort_key_to_params!(params, sort_key, remaining_conditions)
|
|
1403
|
+
params[:filter_expression] = filter_parts.join(' AND ') if filter_parts.any?
|
|
1404
|
+
|
|
1405
|
+
count = 0
|
|
1406
|
+
exclusive_start_key = nil
|
|
1407
|
+
loop do
|
|
1408
|
+
params[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
|
|
1409
|
+
response = resolved_model.dynamodb.query(params)
|
|
1410
|
+
count += response.count
|
|
1411
|
+
exclusive_start_key = response.last_evaluated_key
|
|
1412
|
+
break unless exclusive_start_key
|
|
1413
|
+
end
|
|
1414
|
+
mutex.synchronize { total += count }
|
|
1415
|
+
end
|
|
1416
|
+
end
|
|
1417
|
+
threads.each(&:join)
|
|
1418
|
+
end
|
|
1419
|
+
|
|
1420
|
+
limit_value ? [total, limit_value].min : total
|
|
1421
|
+
end
|
|
1422
|
+
|
|
1123
1423
|
def scan_with_conditions_normalized(normalized_conditions)
|
|
1124
1424
|
filter_parts = []
|
|
1125
1425
|
filter_values = {}
|
data/lib/active_item/version.rb
CHANGED
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activeitem
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -11,37 +11,32 @@ bindir: bin
|
|
|
11
11
|
cert_chain:
|
|
12
12
|
- |
|
|
13
13
|
-----BEGIN CERTIFICATE-----
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
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
-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
-
+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
-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
-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
|
|
43
38
|
-----END CERTIFICATE-----
|
|
44
|
-
date: 2026-06-
|
|
39
|
+
date: 2026-06-17 00:00:00.000000000 Z
|
|
45
40
|
dependencies:
|
|
46
41
|
- !ruby/object:Gem::Dependency
|
|
47
42
|
name: activemodel
|
|
@@ -188,7 +183,7 @@ licenses:
|
|
|
188
183
|
- MIT
|
|
189
184
|
metadata:
|
|
190
185
|
homepage_uri: https://github.com/stowzilla/activeitem
|
|
191
|
-
source_code_uri: https://github.com/stowzilla/activeitem
|
|
186
|
+
source_code_uri: https://github.com/stowzilla/activeitem/tree/master
|
|
192
187
|
changelog_uri: https://github.com/stowzilla/activeitem/blob/main/CHANGELOG.md
|
|
193
188
|
rubygems_mfa_required: 'true'
|
|
194
189
|
post_install_message:
|
metadata.gz.sig
CHANGED
|
Binary file
|