diff --git a/lib/tapioca/dsl/compilers/identity_cache.rb b/lib/tapioca/dsl/compilers/identity_cache.rb index 5c1629f28..4a2dfbe81 100644 --- a/lib/tapioca/dsl/compilers/identity_cache.rb +++ b/lib/tapioca/dsl/compilers/identity_cache.rb @@ -131,7 +131,7 @@ def create_fetch_by_methods(field, klass) is_cache_index = field.instance_variable_defined?(:@attribute_proc) # Both `cache_index` and `cache_attribute` generate aliased methods - create_aliased_fetch_by_methods(field, klass) + create_aliased_fetch_by_methods(field, klass, is_cache_index: is_cache_index) # If the method used was `cache_index` a few extra methods are created create_index_fetch_by_methods(field, klass) if is_cache_index @@ -182,8 +182,8 @@ def create_index_fetch_by_methods(field, klass) ) end - #: (untyped field, RBI::Scope klass) -> void - def create_aliased_fetch_by_methods(field, klass) + #: (untyped field, RBI::Scope klass, is_cache_index: bool) -> void + def create_aliased_fetch_by_methods(field, klass, is_cache_index:) type, _ = Helpers::ActiveRecordColumnTypeHelper.new( constant, column_type_option: Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption::Nilable, @@ -195,6 +195,15 @@ def create_aliased_fetch_by_methods(field, klass) create_param(arg.to_s, type: "T.untyped") end + multi_value_type = + if field.unique + type + elsif is_cache_index + COLLECTION_TYPE.call(multi_type) + else + COLLECTION_TYPE.call(type) + end + klass.create_method( "fetch_#{suffix}", class_method: true, @@ -206,7 +215,7 @@ def create_aliased_fetch_by_methods(field, klass) "fetch_multi_#{suffix}", class_method: true, parameters: [create_param("keys", type: "T::Enumerable[T.untyped]")], - return_type: COLLECTION_TYPE.call(multi_type), + return_type: "T::Hash[T.untyped, #{multi_value_type}]", ) end end diff --git a/spec/tapioca/dsl/compilers/identity_cache_spec.rb b/spec/tapioca/dsl/compilers/identity_cache_spec.rb index 1acc4b826..b3292a963 100644 --- a/spec/tapioca/dsl/compilers/identity_cache_spec.rb +++ b/spec/tapioca/dsl/compilers/identity_cache_spec.rb @@ -102,10 +102,10 @@ def fetch_multi_by_blog_id(index_values, includes: nil); end sig { params(index_values: T::Enumerable[T.untyped], includes: T.untyped).returns(T::Array[::Post]) } def fetch_multi_by_title(index_values, includes: nil); end - sig { params(keys: T::Enumerable[T.untyped]).returns(T::Array[::Integer]) } + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T::Array[::Integer]]) } def fetch_multi_id_by_blog_id(keys); end - sig { params(keys: T::Enumerable[T.untyped]).returns(T::Array[::Integer]) } + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T::Array[::Integer]]) } def fetch_multi_id_by_title(keys); end end end @@ -160,10 +160,10 @@ def fetch_multi_by_blog_id(index_values, includes: nil); end sig { params(index_values: T::Enumerable[T.untyped], includes: T.untyped).returns(T::Array[::Post]) } def fetch_multi_by_title(index_values, includes: nil); end - sig { params(keys: T::Enumerable[T.untyped]).returns(T::Array[::Integer]) } + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T::Array[::Integer]]) } def fetch_multi_id_by_blog_id(keys); end - sig { params(keys: T::Enumerable[T.untyped]).returns(T::Array[::Integer]) } + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T.nilable(::Integer)]) } def fetch_multi_id_by_title(keys); end end end @@ -205,7 +205,7 @@ def fetch_id_by_blog_id_and_title(blog_id, title); end sig { params(index_values: T::Enumerable[T.untyped], includes: T.untyped).returns(T::Array[::Post]) } def fetch_multi_by_blog_id_and_title(index_values, includes: nil); end - sig { params(keys: T::Enumerable[T.untyped]).returns(T::Array[::Integer]) } + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T::Array[::Integer]]) } def fetch_multi_id_by_blog_id_and_title(keys); end end end @@ -260,10 +260,10 @@ def fetch_multi_by_title(index_values, includes: nil); end sig { params(index_values: T::Enumerable[T.untyped], includes: T.untyped).returns(T::Array[::Post]) } def fetch_multi_by_title_and_review_date(index_values, includes: nil); end - sig { params(keys: T::Enumerable[T.untyped]).returns(T::Array[::Integer]) } + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T::Array[::Integer]]) } def fetch_multi_id_by_title(keys); end - sig { params(keys: T::Enumerable[T.untyped]).returns(T::Array[::Integer]) } + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T.nilable(::Integer)]) } def fetch_multi_id_by_title_and_review_date(keys); end end end @@ -401,6 +401,7 @@ def fetch_user; end ActiveRecord::Schema.define do create_table :posts do |t| t.string :author + t.string :name end end end @@ -412,6 +413,7 @@ class Post < ActiveRecord::Base cache_attribute :author, by: :id cache_attribute :author, by: [:id, :author] + cache_attribute :name, by: :author, unique: false end RUBY @@ -426,11 +428,53 @@ def fetch_author_by_id(id); end sig { params(id: T.untyped, author: T.untyped).returns(T.nilable(::String)) } def fetch_author_by_id_and_author(id, author); end - sig { params(keys: T::Enumerable[T.untyped]).returns(T::Array[::String]) } + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T.nilable(::String)]) } def fetch_multi_author_by_id(keys); end - sig { params(keys: T::Enumerable[T.untyped]).returns(T::Array[::String]) } + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T.nilable(::String)]) } def fetch_multi_author_by_id_and_author(keys); end + + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T::Array[::T.nilable(::String)]]) } + def fetch_multi_name_by_author(keys); end + + sig { params(author: T.untyped).returns(T::Array[::T.nilable(::String)]) } + def fetch_name_by_author(author); end + end + end + RBI + + assert_equal(expected, rbi_for(:Post)) + end + + it "allows missing unique cache attributes for non-nullable columns" do + add_ruby_file("schema.rb", <<~RUBY) + ActiveRecord::Migration.suppress_messages do + ActiveRecord::Schema.define do + create_table :posts do |t| + t.string :author, null: false + end + end + end + RUBY + + add_ruby_file("post.rb", <<~RUBY) + class Post < ActiveRecord::Base + include IdentityCache + + cache_attribute :author, by: :id + end + RUBY + + expected = <<~RBI + # typed: strong + + class Post + class << self + sig { params(id: T.untyped).returns(T.nilable(::String)) } + def fetch_author_by_id(id); end + + sig { params(keys: T::Enumerable[T.untyped]).returns(T::Hash[T.untyped, T.nilable(::String)]) } + def fetch_multi_author_by_id(keys); end end end RBI