activeitem 0.0.23 → 0.0.24
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/CHANGELOG.md +15 -0
- data/README.md +12 -0
- data/lib/active_item/begins_with.rb +18 -0
- data/lib/active_item/query_helpers.rb +22 -0
- data/lib/active_item/relation.rb +23 -50
- data/lib/active_item/version.rb +1 -1
- data/lib/activeitem.rb +6 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 296b80f6ec0f16823c99df6d3a041fe16d55eab4ce59118d95081566f13e01d4
|
|
4
|
+
data.tar.gz: 8ec0e73b20698346c67f96075c0ccc3070f2f6232c1e1b3607a61424c1a19f8b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8b5d8087d0cffede8296535868978d6dd24cddc70d1f6a1ba0741db31d5fc46caaea9635791f8984166aa8de92bfb50db000ecc935df4fe6498ecc2456fc1029
|
|
7
|
+
data.tar.gz: ef5ca8b2bb01a83a5673d0181d13627d5eb0b8bb01612f6e6ef146c5e4eb669cfd2ef0936c1aa09b15e6a5eed2367d7b1adb3c844c7e8aa4c72e7dc600201624
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`ActiveItem.begins_with` for prefix queries** — Pass it as a `where` value. On an index's sort key it goes into the key condition (`begins_with(#sk, :sk_val)`), so DynamoDB reads only the matching items instead of scanning. On any other attribute it becomes a `begins_with` filter, and `where.not` gives `NOT begins_with(...)`. Works with `limit`, `order`, `count`, `explain`, and array (fan-out) partition keys.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# GSI PrefixIndex: partition = first 2 letters, sort = whole name (both lowercased)
|
|
11
|
+
Username.where(search_prefix: 'ab', search_name: ActiveItem.begins_with('abe')).limit(10)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- The equality / Range / prefix sort-key condition is now built in one place (`build_sort_key_condition`) instead of five copies in `Relation`.
|
|
17
|
+
|
|
3
18
|
## 0.0.23
|
|
4
19
|
|
|
5
20
|
### Added
|
data/README.md
CHANGED
|
@@ -75,6 +75,18 @@ User.count
|
|
|
75
75
|
User.exists?('user-123')
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
Sort-key conditions on an index go into the key condition, not a filter:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
Event.where(customer_id: id, created_at: 1.week.ago..) # >=, <=, BETWEEN via Range
|
|
82
|
+
Username.where(search_prefix: 'ab', search_name: ActiveItem.begins_with('abe')) # prefix
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For prefix search (autocomplete), add a GSI whose partition key is the first few
|
|
86
|
+
lowercased characters and whose sort key is the whole lowercased value, then
|
|
87
|
+
`begins_with` on the sort key. `begins_with` on a non-key attribute works too, but
|
|
88
|
+
it's a filter, so DynamoDB still reads every item in the query or scan.
|
|
89
|
+
|
|
78
90
|
### Associations
|
|
79
91
|
|
|
80
92
|
```ruby
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveItem
|
|
4
|
+
# A prefix match for `where`, built with ActiveItem.begins_with.
|
|
5
|
+
#
|
|
6
|
+
# On an index's sort key it becomes part of the key condition, so DynamoDB
|
|
7
|
+
# reads only the matching items. On any other attribute it is a filter.
|
|
8
|
+
#
|
|
9
|
+
# @example Prefix search on a GSI (partition = first letters, sort = whole name)
|
|
10
|
+
# Username.where(search_prefix: 'ab', search_name: ActiveItem.begins_with('abc')).limit(10)
|
|
11
|
+
BeginsWith = Data.define(:prefix) do
|
|
12
|
+
def initialize(prefix:)
|
|
13
|
+
raise ArgumentError, 'begins_with needs a non-empty prefix' if prefix.to_s.empty?
|
|
14
|
+
|
|
15
|
+
super(prefix: prefix.to_s)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -411,6 +411,7 @@ module ActiveItem
|
|
|
411
411
|
# - Range values (BETWEEN): { date: Date.today..Date.today + 7 }
|
|
412
412
|
# - Beginless range (<=): { date: ..Date.today }
|
|
413
413
|
# - Endless range (>=): { date: Date.today.. }
|
|
414
|
+
# - Prefix match: { name: ActiveItem.begins_with('ab') }
|
|
414
415
|
# - Model objects: { parent_container: container } -> uses container.id
|
|
415
416
|
#
|
|
416
417
|
# Note: Attribute names are converted from Ruby snake_case to DynamoDB camelCase
|
|
@@ -452,6 +453,9 @@ module ActiveItem
|
|
|
452
453
|
# Handle Range values (BETWEEN, >=, <=)
|
|
453
454
|
return build_range_condition(dynamo_attr, val, idx) if val.is_a?(Range)
|
|
454
455
|
|
|
456
|
+
# Handle prefix match (begins_with)
|
|
457
|
+
return ["begins_with(#attr#{idx}, :val#{idx})", { "#attr#{idx}" => dynamo_attr }, { ":val#{idx}" => val.prefix }] if val.is_a?(ActiveItem::BeginsWith)
|
|
458
|
+
|
|
455
459
|
# Handle array values (IN clause)
|
|
456
460
|
return build_in_condition(dynamo_attr, val, idx) if val.is_a?(Array)
|
|
457
461
|
|
|
@@ -596,6 +600,24 @@ module ActiveItem
|
|
|
596
600
|
end
|
|
597
601
|
end
|
|
598
602
|
|
|
603
|
+
# Build the sort key part of a GSI key condition: equality, a Range
|
|
604
|
+
# (BETWEEN, >=, <=), or ActiveItem.begins_with.
|
|
605
|
+
def build_sort_key_condition(sort_key, value, placeholder_prefix = 'sk')
|
|
606
|
+
return build_sort_key_range_condition(sort_key, value, placeholder_prefix) if value.is_a?(Range)
|
|
607
|
+
|
|
608
|
+
expression = if value.is_a?(ActiveItem::BeginsWith)
|
|
609
|
+
value = value.prefix
|
|
610
|
+
"begins_with(##{placeholder_prefix}, :#{placeholder_prefix}_val)"
|
|
611
|
+
else
|
|
612
|
+
"##{placeholder_prefix} = :#{placeholder_prefix}_val"
|
|
613
|
+
end
|
|
614
|
+
{
|
|
615
|
+
expression: expression,
|
|
616
|
+
names: { "##{placeholder_prefix}" => sort_key },
|
|
617
|
+
values: { ":#{placeholder_prefix}_val" => value }
|
|
618
|
+
}
|
|
619
|
+
end
|
|
620
|
+
|
|
599
621
|
# Build sort key condition for Range values in GSI queries
|
|
600
622
|
# Returns the key condition expression part and values for sort key
|
|
601
623
|
def build_sort_key_range_condition(sort_key, range, placeholder_prefix = 'sk')
|
data/lib/active_item/relation.rb
CHANGED
|
@@ -660,16 +660,10 @@ module ActiveItem
|
|
|
660
660
|
resolved_model.to_dynamo_key(k.to_s) == sort_key || k.to_s == sort_key
|
|
661
661
|
end
|
|
662
662
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
params[:expression_attribute_values].merge!(range_condition[:values])
|
|
668
|
-
else
|
|
669
|
-
params[:key_condition_expression] += ' AND #sk = :sk_val'
|
|
670
|
-
params[:expression_attribute_names]['#sk'] = sort_key
|
|
671
|
-
params[:expression_attribute_values][':sk_val'] = sort_value
|
|
672
|
-
end
|
|
663
|
+
key_condition = resolved_model.send(:build_sort_key_condition, sort_key, sort_value)
|
|
664
|
+
params[:key_condition_expression] += " AND #{key_condition[:expression]}"
|
|
665
|
+
params[:expression_attribute_names].merge!(key_condition[:names])
|
|
666
|
+
params[:expression_attribute_values].merge!(key_condition[:values])
|
|
673
667
|
end
|
|
674
668
|
end
|
|
675
669
|
|
|
@@ -906,16 +900,10 @@ module ActiveItem
|
|
|
906
900
|
resolved_model.to_dynamo_key(k.to_s) == sort_key || k.to_s == sort_key
|
|
907
901
|
end
|
|
908
902
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
params[:expression_attribute_values].merge!(range_condition[:values])
|
|
914
|
-
else
|
|
915
|
-
params[:key_condition_expression] += ' AND #sk = :sk_val'
|
|
916
|
-
params[:expression_attribute_names]['#sk'] = sort_key
|
|
917
|
-
params[:expression_attribute_values][':sk_val'] = sort_value
|
|
918
|
-
end
|
|
903
|
+
key_condition = resolved_model.send(:build_sort_key_condition, sort_key, sort_value)
|
|
904
|
+
params[:key_condition_expression] += " AND #{key_condition[:expression]}"
|
|
905
|
+
params[:expression_attribute_names].merge!(key_condition[:names])
|
|
906
|
+
params[:expression_attribute_values].merge!(key_condition[:values])
|
|
919
907
|
end
|
|
920
908
|
end
|
|
921
909
|
|
|
@@ -1020,16 +1008,10 @@ module ActiveItem
|
|
|
1020
1008
|
resolved_model.to_dynamo_key(k.to_s) == sort_key || k.to_s == sort_key
|
|
1021
1009
|
end
|
|
1022
1010
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
params[:expression_attribute_values].merge!(range_condition[:values])
|
|
1028
|
-
else
|
|
1029
|
-
params[:key_condition_expression] += ' AND #sk = :sk_val'
|
|
1030
|
-
params[:expression_attribute_names]['#sk'] = sort_key
|
|
1031
|
-
params[:expression_attribute_values][':sk_val'] = sort_value
|
|
1032
|
-
end
|
|
1011
|
+
key_condition = resolved_model.send(:build_sort_key_condition, sort_key, sort_value)
|
|
1012
|
+
params[:key_condition_expression] += " AND #{key_condition[:expression]}"
|
|
1013
|
+
params[:expression_attribute_names].merge!(key_condition[:names])
|
|
1014
|
+
params[:expression_attribute_values].merge!(key_condition[:values])
|
|
1033
1015
|
end
|
|
1034
1016
|
end
|
|
1035
1017
|
|
|
@@ -1186,16 +1168,10 @@ module ActiveItem
|
|
|
1186
1168
|
resolved_model.to_dynamo_key(k.to_s) == sort_key || k.to_s == sort_key
|
|
1187
1169
|
end
|
|
1188
1170
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
params[:expression_attribute_values].merge!(range_condition[:values])
|
|
1194
|
-
else
|
|
1195
|
-
params[:key_condition_expression] += ' AND #sk = :sk_val'
|
|
1196
|
-
params[:expression_attribute_names]['#sk'] = sort_key
|
|
1197
|
-
params[:expression_attribute_values][':sk_val'] = sort_value
|
|
1198
|
-
end
|
|
1171
|
+
key_condition = resolved_model.send(:build_sort_key_condition, sort_key, sort_value)
|
|
1172
|
+
params[:key_condition_expression] += " AND #{key_condition[:expression]}"
|
|
1173
|
+
params[:expression_attribute_names].merge!(key_condition[:names])
|
|
1174
|
+
params[:expression_attribute_values].merge!(key_condition[:values])
|
|
1199
1175
|
end
|
|
1200
1176
|
end
|
|
1201
1177
|
|
|
@@ -1416,16 +1392,10 @@ module ActiveItem
|
|
|
1416
1392
|
return unless sort_condition
|
|
1417
1393
|
|
|
1418
1394
|
_, sort_value = sort_condition
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
params[:expression_attribute_values].merge!(range_condition[:values])
|
|
1424
|
-
else
|
|
1425
|
-
params[:key_condition_expression] += ' AND #sk = :sk_val'
|
|
1426
|
-
params[:expression_attribute_names]['#sk'] = sort_key
|
|
1427
|
-
params[:expression_attribute_values][':sk_val'] = sort_value
|
|
1428
|
-
end
|
|
1395
|
+
key_condition = resolved_model.send(:build_sort_key_condition, sort_key, sort_value)
|
|
1396
|
+
params[:key_condition_expression] += " AND #{key_condition[:expression]}"
|
|
1397
|
+
params[:expression_attribute_names].merge!(key_condition[:names])
|
|
1398
|
+
params[:expression_attribute_values].merge!(key_condition[:values])
|
|
1429
1399
|
end
|
|
1430
1400
|
|
|
1431
1401
|
def sort_fanout_results(items, sort_key)
|
|
@@ -1616,6 +1586,9 @@ module ActiveItem
|
|
|
1616
1586
|
return ["NOT (#attr#{idx} IN (#{placeholders.join(', ')}))", { "#attr#{idx}" => dynamo_attr }, values]
|
|
1617
1587
|
end
|
|
1618
1588
|
|
|
1589
|
+
# NOT begins_with
|
|
1590
|
+
return ["NOT begins_with(#attr#{idx}, :val#{idx})", { "#attr#{idx}" => dynamo_attr }, { ":val#{idx}" => val.prefix }] if val.is_a?(ActiveItem::BeginsWith)
|
|
1591
|
+
|
|
1619
1592
|
# Simple not equal
|
|
1620
1593
|
["#attr#{idx} <> :val#{idx}", { "#attr#{idx}" => dynamo_attr }, { ":val#{idx}" => val }]
|
|
1621
1594
|
end
|
data/lib/active_item/version.rb
CHANGED
data/lib/activeitem.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative 'active_item/configuration'
|
|
|
5
5
|
require_relative 'active_item/logging'
|
|
6
6
|
require_relative 'active_item/errors'
|
|
7
7
|
require_relative 'active_item/database_helpers'
|
|
8
|
+
require_relative 'active_item/begins_with'
|
|
8
9
|
require_relative 'active_item/query_helpers'
|
|
9
10
|
require_relative 'active_item/relation'
|
|
10
11
|
require_relative 'active_item/embedded_collection'
|
|
@@ -32,5 +33,10 @@ module ActiveItem
|
|
|
32
33
|
def logger
|
|
33
34
|
configuration.logger
|
|
34
35
|
end
|
|
36
|
+
|
|
37
|
+
# Prefix match for `where`; see ActiveItem::BeginsWith.
|
|
38
|
+
def begins_with(prefix)
|
|
39
|
+
BeginsWith.new(prefix)
|
|
40
|
+
end
|
|
35
41
|
end
|
|
36
42
|
end
|
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.24
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Davis
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2026-
|
|
12
|
+
date: 2026-10-30 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activemodel
|
|
@@ -157,6 +157,7 @@ files:
|
|
|
157
157
|
- README.md
|
|
158
158
|
- lib/active_item/associations.rb
|
|
159
159
|
- lib/active_item/base.rb
|
|
160
|
+
- lib/active_item/begins_with.rb
|
|
160
161
|
- lib/active_item/composed_of.rb
|
|
161
162
|
- lib/active_item/configuration.rb
|
|
162
163
|
- lib/active_item/database_helpers.rb
|