Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- JsonDateRangeResolver fixed for custom store column

## [1.1.1] - 2025-12-16

### Fixed
Expand Down
3 changes: 2 additions & 1 deletion app/models/concerns/structured_store/storable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ def property_resolvers(column_name)
@property_resolvers ||= {}
@property_resolvers[column_name] ||= json_schema_properties(column_name).keys.index_with do |property_name|
StructuredStore::RefResolvers::Registry.matching_resolver(schema_inspector(column_name),
property_name)
property_name,
{ column_name: column_name })
end
end

Expand Down
19 changes: 11 additions & 8 deletions lib/structured_store/ref_resolvers/json_date_range_resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,24 @@ def self.matching_ref_pattern
# @return [Proc] a lambda that defines the attribute on the singleton class
# @raise [RuntimeError] if the property type is unsupported
def define_attribute
# Capture the property name in a local variable for closure
# Capture the property name and store column name in local variables for closure.
# context[:column_name] is set by Storable#property_resolvers; fall back to 'store'
# for any resolver instantiated without that context (e.g. in isolation in tests).
prop_name = property_name
resolver = self
col_name = (context[:column_name] || 'store').to_s
resolver = self

# Define the attribute on the singleton class of the object
lambda do |object|
converter = object.date_range_converter

# Define custom getter and setter methods
object.singleton_class.define_method(prop_name) do
resolver.send(:cast_stored_value, store, prop_name, converter)
resolver.send(:cast_stored_value, send(col_name), prop_name, converter)
end

object.singleton_class.define_method("#{prop_name}=") do |value|
resolver.send(:serialize_value_to_store, self, prop_name, value, converter)
resolver.send(:serialize_value_to_store, self, col_name, prop_name, value, converter)
end
end
end
Expand Down Expand Up @@ -67,13 +70,13 @@ def cast_hash_to_string(stored_value, converter)
end

# Serializes an input value to the store as a hash
def serialize_value_to_store(object, prop_name, value, converter)
def serialize_value_to_store(object, col_name, prop_name, value, converter)
# Initialize store as empty hash if nil
object.store ||= {}
return object.store[prop_name] = nil if value.blank?
object.send("#{col_name}=", {}) unless object.send(col_name)
return object.send(col_name)[prop_name] = nil if value.blank?

date1, date2 = converter.convert_to_dates(value)
object.store[prop_name] = {
object.send(col_name)[prop_name] = {
'date1' => date1&.to_fs(:db),
'date2' => date2&.to_fs(:db)
}
Expand Down
16 changes: 16 additions & 0 deletions test/dummy/app/models/audit_store_record.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

# Regression model: exercises structured_store when the column is NOT called "store".
# Reproduces the bug where JsonDateRangeResolver hardcoded the `store` accessor name.
class AuditStoreRecord < ApplicationRecord
include StructuredStore::Storable

structured_store :audit_store

# Returns a memoized ChronicDateRangeConverter for use by JsonDateRangeResolver.
#
# @return [StructuredStore::Converters::ChronicDateRangeConverter]
def date_range_converter
@date_range_converter ||= StructuredStore::Converters::ChronicDateRangeConverter.new
end
end
17 changes: 17 additions & 0 deletions test/dummy/db/migrate/20260223234721_create_audit_store_records.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

# Creates the audit_store_records table used by AuditStoreRecord.
# This table is intentionally named with "audit_store" (not "store") as the JSON column
# to serve as a regression fixture for JsonDateRangeResolver column-name handling.
class CreateAuditStoreRecords < ActiveRecord::Migration[7.2]
def change
create_table :audit_store_records do |t|
t.references :structured_store_audit_store_versioned_schema,
null: false,
foreign_key: { to_table: :structured_store_versioned_schemas }
t.json :audit_store

t.timestamps
end
end
end
11 changes: 10 additions & 1 deletion test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.2].define(version: 2025_07_09_170439) do
ActiveRecord::Schema[7.2].define(version: 2026_02_23_234721) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

create_table "audit_store_records", force: :cascade do |t|
t.bigint "structured_store_audit_store_versioned_schema_id", null: false
t.json "audit_store"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["structured_store_audit_store_versioned_schema_id"], name: "idx_on_structured_store_audit_store_versioned_schem_18464f9cd1"
end

create_table "binary_json_store_records", force: :cascade do |t|
t.bigint "structured_store_store_versioned_schema_id", null: false
t.jsonb "store"
Expand Down Expand Up @@ -113,6 +121,7 @@
t.index ["structured_store_warehouse_schema_id"], name: "idx_on_structured_store_warehouse_schema_id_78d0cbf551"
end

add_foreign_key "audit_store_records", "structured_store_versioned_schemas", column: "structured_store_audit_store_versioned_schema_id"
add_foreign_key "binary_json_store_records", "structured_store_versioned_schemas", column: "structured_store_store_versioned_schema_id"
add_foreign_key "binary_store_records", "structured_store_versioned_schemas", column: "structured_store_store_versioned_schema_id"
add_foreign_key "custom_foreign_key_records", "structured_store_versioned_schemas", column: "my_custom_schemaid"
Expand Down
27 changes: 27 additions & 0 deletions test/dummy/test/ref_resolvers/json_date_range_resolver_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,33 @@ class JsonDateRangeResolverTest < ActiveSupport::TestCase
assert_equal 'Jan 2024', store_record.foo
end

# Regression tests: JsonDateRangeResolver previously hardcoded `store` as the
# store accessor method name, breaking any model whose column is not called "store".

test 'define_attribute with non-default column name (string input)' do
versioned_schema = VersionedSchema.new(name: 'DateRangeSchema',
version: '0.1.0',
json_schema: simple_foo_date_range_schema)
record = AuditStoreRecord.new(audit_store_versioned_schema: versioned_schema)

assert_nil record.foo
record.foo = 'January 2024'
assert_equal({ 'date1' => '2024-01-01 00:00:00', 'date2' => '2024-01-31 00:00:00' },
record.audit_store['foo'])
assert_equal 'Jan 2024', record.foo
end

test 'define_attribute with non-default column name (blank value)' do
versioned_schema = VersionedSchema.new(name: 'DateRangeSchema',
version: '0.1.0',
json_schema: simple_foo_date_range_schema)
record = AuditStoreRecord.new(audit_store_versioned_schema: versioned_schema)

record.foo = ''
assert_nil record.audit_store['foo']
assert_nil record.foo
end

test 'options_array' do
schema = {
'$schema' => 'https://json-schema.org/draft/2019-09/schema',
Expand Down
Loading