activeitem 0.0.14 → 0.0.16

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54985c6329129ceac142d8f176c14dfcc68548992f7e00d54a116592ee2a9276
4
- data.tar.gz: abee89f1984d42b3dd72991c5db573370ed356d4272be0bb0d4d21163a233f50
3
+ metadata.gz: 6460db8a74ca7bcb5d28c0bfdb7c4a6c1eba5c5d68318245612dad78d1d1d282
4
+ data.tar.gz: 9fe2d9707efa0a41b74fa1c07c5c24453605baa5d24e3f1880c248c92c5a03b0
5
5
  SHA512:
6
- metadata.gz: 456eb037d79395d8a63916245a681af7ad4a98a933cb294226658bf532886a7feb4fa2f90a2593cd1df34d4b5abd6d9299b206af2ad72cb96a5f1aa4ce1d3334
7
- data.tar.gz: a0626e95bf4d42f4b1f1c0c453223e50214cbad0be8de67e76539f42ff2c3449027bf78f62a6708303db14195c10558972b9715e7c2f9bbf95c080b5d4bcf509
6
+ metadata.gz: 61ad96dc3dd1a28bda5a9af288bd17e1eef4f11b450f08efae26c1ea8f8060819727dde8d855d562e0fbbf2e6012096e159952d44c0d22255abc2384d85d80a9
7
+ data.tar.gz: 741d0e3a257199f62373c16960bea2d90fe26e37e9f4dcb8516cfb049170caac8a237c90ef1ba17dde88b356218b12f7e5e7d5f4a9d96abee7f64225639762f8
@@ -374,6 +374,39 @@ module ActiveItem
374
374
  record
375
375
  end
376
376
 
377
+ # Build a new record with the foreign key pre-set (does not save).
378
+ # Usage: conversation.messages.build(role: "user", body: "hello")
379
+ # @param attributes [Hash] Attributes for the new record
380
+ # @return [ActiveItem::Base] The unsaved record
381
+ def build(attributes = {})
382
+ foreign_key, foreign_value = conditions.first
383
+ resolved_model.new(attributes.merge(foreign_key => foreign_value))
384
+ end
385
+
386
+ # Build and save a new record with the foreign key pre-set.
387
+ # Returns the record (may be invalid — check .persisted? or .errors).
388
+ # Usage: conversation.messages.create(role: "user", body: "hello")
389
+ # @param attributes [Hash] Attributes for the new record
390
+ # @return [ActiveItem::Base] The record (saved if valid)
391
+ def create(attributes = {})
392
+ record = build(attributes)
393
+ record.save
394
+ reload
395
+ record
396
+ end
397
+
398
+ # Build and save a new record with the foreign key pre-set.
399
+ # Raises ActiveItem::RecordInvalid if validations fail.
400
+ # Usage: conversation.messages.create!(role: "user", body: "hello")
401
+ # @param attributes [Hash] Attributes for the new record
402
+ # @return [ActiveItem::Base] The persisted record
403
+ def create!(attributes = {})
404
+ record = build(attributes)
405
+ record.save!
406
+ reload
407
+ record
408
+ end
409
+
377
410
  # Returns the DynamoDB operation that would be executed, without running it.
378
411
  # Analogous to ActiveRecord's .to_sql — shows the operation type, table, index,
379
412
  # key conditions, filters, and limits in DynamoDB terms.
@@ -514,7 +547,8 @@ module ActiveItem
514
547
  if effective_index && normalized_conditions.any? && normalized_conditions.values.first.is_a?(Array)
515
548
  index_config = resolved_model.indexes[effective_index] || {}
516
549
  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)
550
+ raw_pk = index_config[:partition_key]&.to_s || ruby_partition_key
551
+ dynamo_partition_key = resolved_model.to_dynamo_key(raw_pk)
518
552
  return fanout_paginated_query(effective_index, normalized_conditions, index_config, dynamo_partition_key,
519
553
  normalized_conditions.values.first, cursor, per_page)
520
554
  end
@@ -526,6 +560,12 @@ module ActiveItem
526
560
  else
527
561
  paginated_scan_with_conditions(normalized_conditions, exclusive_start_key, per_page)
528
562
  end
563
+ rescue Aws::DynamoDB::Errors::ValidationException => e
564
+ raise unless e.message.include?('specified index')
565
+
566
+ warn "[ActiveItem] WARNING: Index not found on table '#{resolved_model.table_name}'. " \
567
+ 'Falling back to table scan. Create the index: belt setup tables && belt deploy'
568
+ paginated_scan_with_conditions(normalized_conditions, exclusive_start_key, per_page)
529
569
  rescue Aws::DynamoDB::Errors::AccessDeniedException => e
530
570
  raise ActiveItem::AccessDeniedError.new(model_name: resolved_model.name, table: resolved_model.table_name,
531
571
  operation: 'PaginatedQuery', original_error: e)
@@ -573,7 +613,8 @@ module ActiveItem
573
613
  partition_value = normalized_conditions.values.first
574
614
 
575
615
  index_config = resolved_model.indexes[idx_name] || {}
576
- dynamo_partition_key = index_config[:partition_key]&.to_s || resolved_model.to_dynamo_key(ruby_partition_key)
616
+ raw_pk = index_config[:partition_key]&.to_s || ruby_partition_key
617
+ dynamo_partition_key = resolved_model.to_dynamo_key(raw_pk)
577
618
 
578
619
  params = {
579
620
  table_name: resolved_model.table_name,
@@ -755,7 +796,7 @@ module ActiveItem
755
796
  effective_index = index_name || resolved_model.send(:detect_index_for_conditions, normalized_conditions)
756
797
 
757
798
  if effective_index && normalized_conditions.any?
758
- query_with_index_normalized(effective_index, normalized_conditions)
799
+ query_with_index_or_fallback(effective_index, normalized_conditions)
759
800
  else
760
801
  scan_with_conditions_normalized(normalized_conditions)
761
802
  end
@@ -805,6 +846,13 @@ module ActiveItem
805
846
  end
806
847
 
807
848
  limit_value ? [total, limit_value].min : total
849
+ rescue Aws::DynamoDB::Errors::ValidationException => e
850
+ raise unless e.message.include?('specified index')
851
+
852
+ warn "[ActiveItem] WARNING: Index not found on table '#{resolved_model.table_name}'. " \
853
+ 'Falling back to table scan for count. Create the index: belt setup tables && belt deploy'
854
+ # Retry without the index
855
+ count_via_scan(normalized_conditions)
808
856
  rescue Aws::DynamoDB::Errors::AccessDeniedException => e
809
857
  raise ActiveItem::AccessDeniedError.new(model_name: resolved_model.name, table: resolved_model.table_name,
810
858
  operation: 'Count', original_error: e)
@@ -815,7 +863,8 @@ module ActiveItem
815
863
  partition_value = normalized_conditions.values.first
816
864
 
817
865
  index_config = resolved_model.indexes[idx_name] || {}
818
- dynamo_partition_key = index_config[:partition_key]&.to_s || resolved_model.to_dynamo_key(ruby_partition_key)
866
+ raw_partition_key = index_config[:partition_key]&.to_s || ruby_partition_key
867
+ dynamo_partition_key = resolved_model.to_dynamo_key(raw_partition_key)
819
868
 
820
869
  params = {
821
870
  table_name: resolved_model.table_name,
@@ -878,6 +927,26 @@ module ActiveItem
878
927
  params
879
928
  end
880
929
 
930
+ # Fallback count via scan when an index is missing
931
+ def count_via_scan(normalized_conditions)
932
+ total = 0
933
+ exclusive_start_key = nil
934
+
935
+ loop do
936
+ params = build_count_scan_params(normalized_conditions)
937
+ params[:exclusive_start_key] = exclusive_start_key if exclusive_start_key
938
+ params[:limit] = limit_value if limit_value
939
+
940
+ response = resolved_model.dynamodb.scan(params)
941
+ total += response.count
942
+ exclusive_start_key = response.last_evaluated_key
943
+ break unless exclusive_start_key
944
+ break if limit_value && total >= limit_value
945
+ end
946
+
947
+ limit_value ? [total, limit_value].min : total
948
+ end
949
+
881
950
  # Build scan params for a count-only scan
882
951
  def build_count_scan_params(normalized_conditions)
883
952
  filter_parts = []
@@ -911,7 +980,8 @@ module ActiveItem
911
980
  partition_value = normalized_conditions.values.first
912
981
 
913
982
  index_config = resolved_model.indexes[idx_name] || {}
914
- dynamo_partition_key = index_config[:partition_key]&.to_s || resolved_model.to_dynamo_key(ruby_partition_key)
983
+ raw_pk = index_config[:partition_key]&.to_s || ruby_partition_key
984
+ dynamo_partition_key = resolved_model.to_dynamo_key(raw_pk)
915
985
 
916
986
  params = {
917
987
  table_name: resolved_model.table_name,
@@ -1049,6 +1119,18 @@ module ActiveItem
1049
1119
  foreign_key.to_s == attr_name ? nil : foreign_key.to_s
1050
1120
  end
1051
1121
 
1122
+ # Attempt a GSI query, falling back to a filtered scan if the index doesn't exist.
1123
+ # Logs a warning when falling back so the developer knows to create the index.
1124
+ def query_with_index_or_fallback(idx_name, normalized_conditions)
1125
+ query_with_index_normalized(idx_name, normalized_conditions)
1126
+ rescue Aws::DynamoDB::Errors::ValidationException => e
1127
+ raise unless e.message.include?('specified index')
1128
+
1129
+ warn "[ActiveItem] WARNING: Index '#{idx_name}' not found on table '#{resolved_model.table_name}'. " \
1130
+ 'Falling back to table scan. Create the index for better performance: belt setup tables && belt deploy'
1131
+ scan_with_conditions_normalized(normalized_conditions)
1132
+ end
1133
+
1052
1134
  def query_with_index_normalized(idx_name, normalized_conditions)
1053
1135
  # Get the partition key from conditions (Ruby snake_case)
1054
1136
  ruby_partition_key = normalized_conditions.keys.first.to_s
@@ -1057,7 +1139,9 @@ module ActiveItem
1057
1139
  # Get the actual DynamoDB partition key name from the index definition
1058
1140
  # The index definition stores the DynamoDB key name (which may be camelCase)
1059
1141
  index_config = resolved_model.indexes[idx_name] || {}
1060
- dynamo_partition_key = index_config[:partition_key]&.to_s || resolved_model.to_dynamo_key(ruby_partition_key)
1142
+ raw_partition_key = index_config[:partition_key]&.to_s || ruby_partition_key
1143
+ # Always convert to DynamoDB key format (camelCase) for the expression
1144
+ dynamo_partition_key = resolved_model.to_dynamo_key(raw_partition_key)
1061
1145
 
1062
1146
  return fanout_query(idx_name, normalized_conditions, index_config, dynamo_partition_key, partition_value) if partition_value.is_a?(Array)
1063
1147
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveItem
4
- VERSION = '0.0.14'
4
+ VERSION = '0.0.16'
5
5
  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.14
4
+ version: 0.0.16
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-08-03 00:00:00.000000000 Z
12
+ date: 2026-08-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activemodel