From 347910466bd4b5c8a8055e131dcc1a00832f2097 Mon Sep 17 00:00:00 2001 From: Stephen Ierodiaconou Date: Wed, 18 Sep 2024 23:21:28 +0200 Subject: [PATCH] First pass at migrating RBS sigs to inline RBS --- .standard.yml | 3 + Gemfile | 6 +- README.md | 1 - lib/quo.rb | 12 +- lib/quo/composed_query.rb | 113 ++++++++----- lib/quo/eager_query.rb | 19 ++- lib/quo/engine.rb | 2 + lib/quo/loaded_query.rb | 5 + lib/quo/query.rb | 233 ++++++++++++++++++-------- lib/quo/results.rb | 23 ++- lib/quo/utilities/callstack.rb | 21 --- lib/quo/utilities/compose.rb | 34 ---- lib/quo/utilities/sanitize.rb | 19 --- lib/quo/utilities/wrap.rb | 28 ---- lib/quo/version.rb | 2 + lib/quo/wrapped_query.rb | 6 + sig/generated/quo.rbs | 7 + sig/generated/quo/composed_query.rbs | 86 ++++++++++ sig/generated/quo/eager_query.rbs | 36 ++++ sig/generated/quo/engine.rbs | 6 + sig/generated/quo/loaded_query.rbs | 12 ++ sig/generated/quo/query.rbs | 201 ++++++++++++++++++++++ sig/generated/quo/results.rbs | 40 +++++ sig/generated/quo/version.rbs | 5 + sig/generated/quo/wrapped_query.rbs | 11 ++ sig/quo.rbs | 28 ---- sig/quo/eager_query.rbs | 15 -- sig/quo/loaded_query.rbs | 7 - sig/quo/merged_query.rbs | 19 --- sig/quo/query.rbs | 83 --------- sig/quo/query_composer.rbs | 32 ---- sig/quo/results.rbs | 22 --- sig/quo/utilities/callstack.rbs | 7 - sig/quo/utilities/compose.rbs | 8 - sig/quo/utilities/sanitize.rbs | 9 - sig/quo/utilities/wrap.rbs | 11 -- sig/quo/wrapped_query.rbs | 11 -- test/dummy/config/initializers/quo.rb | 4 - 38 files changed, 693 insertions(+), 494 deletions(-) delete mode 100644 lib/quo/utilities/callstack.rb delete mode 100644 lib/quo/utilities/compose.rb delete mode 100644 lib/quo/utilities/sanitize.rb delete mode 100644 lib/quo/utilities/wrap.rb create mode 100644 sig/generated/quo.rbs create mode 100644 sig/generated/quo/composed_query.rbs create mode 100644 sig/generated/quo/eager_query.rbs create mode 100644 sig/generated/quo/engine.rbs create mode 100644 sig/generated/quo/loaded_query.rbs create mode 100644 sig/generated/quo/query.rbs create mode 100644 sig/generated/quo/results.rbs create mode 100644 sig/generated/quo/version.rbs create mode 100644 sig/generated/quo/wrapped_query.rbs delete mode 100644 sig/quo.rbs delete mode 100644 sig/quo/eager_query.rbs delete mode 100644 sig/quo/loaded_query.rbs delete mode 100644 sig/quo/merged_query.rbs delete mode 100644 sig/quo/query.rbs delete mode 100644 sig/quo/query_composer.rbs delete mode 100644 sig/quo/results.rbs delete mode 100644 sig/quo/utilities/callstack.rbs delete mode 100644 sig/quo/utilities/compose.rbs delete mode 100644 sig/quo/utilities/sanitize.rbs delete mode 100644 sig/quo/utilities/wrap.rbs delete mode 100644 sig/quo/wrapped_query.rbs diff --git a/.standard.yml b/.standard.yml index a84ccc3..1b2d232 100644 --- a/.standard.yml +++ b/.standard.yml @@ -1,3 +1,6 @@ # For available configuration options, see: # https://github.com/testdouble/standard ruby_version: 3.1 +ignore: + - "**/*": + - "Layout/LeadingCommentSpace" diff --git a/Gemfile b/Gemfile index 0e8dd24..6361a80 100644 --- a/Gemfile +++ b/Gemfile @@ -12,7 +12,9 @@ group :development, :test do gem "minitest", "~> 5.0" - gem "standard" + gem "standard", require: false - gem "steep" + gem "steep", require: false + + gem "rbs-inline", "~> 0.8.0", require: false end diff --git a/README.md b/README.md index f5a6790..5203e3e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ The core implementation provides the following functionality: * provides a number of utility methods that operate on the underlying collection (eg `exists?`) * provides a `+` (`compose`) method which merges two query object instances (see section below for details!) * can specify a mapping or transform method to `transform` to perform on results -* in development outputs the callstack that led to the execution of the query * acts as a callable which executes the underlying query with `.first` * can return an `Enumerable` of results diff --git a/lib/quo.rb b/lib/quo.rb index 9272d36..1171c04 100644 --- a/lib/quo.rb +++ b/lib/quo.rb @@ -1,16 +1,13 @@ # frozen_string_literal: true +# rbs_inline: enabled + require_relative "quo/version" require "quo/engine" module Quo extend ActiveSupport::Autoload - autoload :Callstack, "quo/utilities/callstack" - autoload :Compose, "quo/utilities/compose" - autoload :Sanitize, "quo/utilities/sanitize" - autoload :Wrap, "quo/utilities/wrap" - autoload :Query autoload :Results autoload :ComposedQuery @@ -19,13 +16,10 @@ module Quo autoload :WrappedQuery mattr_accessor :base_query_class, default: "Quo::Query" - mattr_accessor :formatted_query_log, default: true - mattr_accessor :query_show_callstack_size, default: 10 - mattr_accessor :logger, default: nil mattr_accessor :max_page_size, default: 200 mattr_accessor :default_page_size, default: 20 - def self.base_query_class + def self.base_query_class #: Quo::Query @@base_query_class.constantize end end diff --git a/lib/quo/composed_query.rb b/lib/quo/composed_query.rb index 3d70901..2c48996 100644 --- a/lib/quo/composed_query.rb +++ b/lib/quo/composed_query.rb @@ -1,64 +1,74 @@ # frozen_string_literal: true +# rbs_inline: enabled + module Quo + # @rbs inherits Quo::Query class ComposedQuery < Quo.base_query_class - class << self - # Combine two Query classes into a new composed query class - def compose(left_query_class, right_query_class, joins: nil) - props = {} - props.merge!(left_query_class.literal_properties.properties_index) if left_query_class < Quo::Query - props.merge!(right_query_class.literal_properties.properties_index) if right_query_class < Quo::Query - - klass = Class.new(self) do - class << self - attr_reader :_composing_joins, :_left_query, :_right_query - end - - props.each do |name, property| - prop name, property.type, property.kind, reader: property.reader, writer: property.writer, default: property.default, shadow_check: false - end + # Combine two Query classes into a new composed query class + # Combine two query-like or composeable entities: + # These can be Quo::Query, Quo::MergedQuery, Quo::EagerQuery and ActiveRecord::Relations. + # See the `README.md` docs for more details. + # @rbs left_query_class: singleton(Quo::Query | ::ActiveRecord::Relation) + # @rbs right_query_class: singleton(Quo::Query | ::ActiveRecord::Relation) + # @rbs joins: untyped + # @rbs return: singleton(Quo::ComposedQuery) + def self.compose(left_query_class, right_query_class, joins: nil) + props = {} + props.merge!(left_query_class.literal_properties.properties_index) if left_query_class < Quo::Query + props.merge!(right_query_class.literal_properties.properties_index) if right_query_class < Quo::Query + + klass = Class.new(self) do + class << self + attr_reader :_composing_joins, :_left_query, :_right_query end - klass.instance_variable_set(:@_composing_joins, joins) - klass.instance_variable_set(:@_left_query, left_query_class) - klass.instance_variable_set(:@_right_query, right_query_class) - # klass.set_temporary_name = "quo::ComposedQuery" # Ruby 3.3+ - klass - end - # We can also merge instance of prepared queries - def merge_instances(left_instance, right_instance, joins: nil) - raise ArgumentError, "Cannot merge, left has incompatible type #{left_instance.class}" unless left_instance.is_a?(Quo::Query) || left_instance.is_a?(::ActiveRecord::Relation) - raise ArgumentError, "Cannot merge, right has incompatible type #{right_instance.class}" unless right_instance.is_a?(Quo::Query) || right_instance.is_a?(::ActiveRecord::Relation) - return compose(left_instance.class, right_instance, joins: joins).new(**left_instance.to_h) if left_instance.is_a?(Quo::Query) && right_instance.is_a?(::ActiveRecord::Relation) - return compose(left_instance, right_instance.class, joins: joins).new(**right_instance.to_h) if right_instance.is_a?(Quo::Query) && left_instance.is_a?(::ActiveRecord::Relation) - return compose(left_instance.class, right_instance.class, joins: joins).new(**left_instance.to_h.merge(right_instance.to_h)) if left_instance.is_a?(Quo::Query) && right_instance.is_a?(Quo::Query) - compose(left_instance, right_instance, joins: joins).new # Both are relations - end - - def inspect - "Quo::ComposedQuery[#{operand_desc(_left_query)}, #{operand_desc(_right_query)}]" + props.each do |name, property| + prop name, property.type, property.kind, reader: property.reader, writer: property.writer, default: property.default, shadow_check: false + end end + klass.instance_variable_set(:@_composing_joins, joins) + klass.instance_variable_set(:@_left_query, left_query_class) + klass.instance_variable_set(:@_right_query, right_query_class) + # klass.set_temporary_name = "quo::ComposedQuery" # Ruby 3.3+ + klass + end - private + # We can also merge instance of prepared queries + # @rbs left_instance: Quo::Query | ::ActiveRecord::Relation + # @rbs right_instance: Quo::Query | ::ActiveRecord::Relation + # @rbs joins: untyped + # @rbs return: Quo::ComposedQuery + def self.merge_instances(left_instance, right_instance, joins: nil) + raise ArgumentError, "Cannot merge, left has incompatible type #{left_instance.class}" unless left_instance.is_a?(Quo::Query) || left_instance.is_a?(::ActiveRecord::Relation) + raise ArgumentError, "Cannot merge, right has incompatible type #{right_instance.class}" unless right_instance.is_a?(Quo::Query) || right_instance.is_a?(::ActiveRecord::Relation) + return compose(left_instance.class, right_instance, joins: joins).new(**left_instance.to_h) if left_instance.is_a?(Quo::Query) && right_instance.is_a?(::ActiveRecord::Relation) + return compose(left_instance, right_instance.class, joins: joins).new(**right_instance.to_h) if right_instance.is_a?(Quo::Query) && left_instance.is_a?(::ActiveRecord::Relation) + return compose(left_instance.class, right_instance.class, joins: joins).new(**left_instance.to_h.merge(right_instance.to_h)) if left_instance.is_a?(Quo::Query) && right_instance.is_a?(Quo::Query) + compose(left_instance, right_instance, joins: joins).new # Both are relations + end - def operand_desc(operand) - if operand < Quo::ComposedQuery - operand.inspect - else - operand.name || "(anonymous)" - end - end + # @rbs override + def self.inspect + left = _left_query + left_desc = (left < Quo::ComposedQuery) ? left.inspect : (left.name || "(anonymous)") + right = _right_query + right_desc = (right < Quo::ComposedQuery) ? right.inspect : (right.name || "(anonymous)") + "Quo::ComposedQuery[#{left_desc}, #{right_desc}]" end + # @rbs override def query merge_left_and_right(left, right, _composing_joins) end + # @rbs return: Quo::Query | ::ActiveRecord::Relation def left return _left_query if relation?(_left_query) _left_query.new(**child_options(_left_query)) end + # @rbs return: Quo::Query | ::ActiveRecord::Relation def right return _right_query if relation?(_right_query) _right_query.new(**child_options(_right_query)) @@ -66,21 +76,25 @@ def right delegate :_composing_joins, :_left_query, :_right_query, to: :class + # @rbs override def inspect "Quo::ComposedQuery[#{operand_desc(left)}, #{operand_desc(right)}]" end private + # @rbs return: Hash[Symbol, untyped] def child_options(query_class) names = property_names(query_class) to_h.slice(*names) end + # @rbs return: Array[Symbol] def property_names(query_class) query_class.literal_properties.properties_index.keys end + # @rbs return: ActiveRecord::Relation | Object & Enumerable[untyped] def merge_left_and_right(left, right, joins) left_rel = unwrap_relation(left) right_rel = unwrap_relation(right) @@ -98,30 +112,47 @@ def merge_left_and_right(left, right, joins) end end + # @rbs left_rel: ActiveRecord::Relation + # @rbs joins: untyped + # @rbs return: ActiveRecord::Relation def apply_joins(left_rel, joins) joins.present? ? left_rel.joins(joins) : left_rel end + # @rbs rel: untyped + # @rbs return: bool def relation?(rel) rel.is_a?(::ActiveRecord::Relation) end + # @rbs left: untyped + # @rbs right: untyped + # @rbs return: bool def both_relations?(left, right) relation?(left) && relation?(right) end + # @rbs left: untyped + # @rbs right: untyped + # @rbs return: bool def left_relation_right_enumerable?(left, right) relation?(left) && !relation?(right) end + # @rbs left: untyped + # @rbs right: untyped + # @rbs return: bool def left_enumerable_right_relation?(left, right) !relation?(left) && relation?(right) end + # @rbs override def unwrap_relation(query) query.is_a?(Quo::Query) ? query.unwrap_unpaginated : query end + # @rbs operand: Quo::ComposedQuery | Quo::Query | ::ActiveRecord::Relation + # @rbs return: String def operand_desc(operand) if operand.is_a? Quo::ComposedQuery operand.inspect diff --git a/lib/quo/eager_query.rb b/lib/quo/eager_query.rb index 2198688..3ef1dc4 100644 --- a/lib/quo/eager_query.rb +++ b/lib/quo/eager_query.rb @@ -1,51 +1,60 @@ # frozen_string_literal: true +# rbs_inline: enabled + module Quo + # @rbs inherits Quo::Query class EagerQuery < Quo.base_query_class # Optionally return the `total_count` option if it has been set. # This is useful when the total count is known and not equal to size # of wrapped collection. + + # @rbs override def count options[:total_count] || underlying_query.count end + # @rbs override def page_count - query_with_logging.count + configured_query.count end # Is this query object paged? (when no total count) + # @rbs override def paged? options[:total_count].nil? && page_index.present? end + # @rbs return: Object & Enumerable[untyped] def collection raise NotImplementedError, "EagerQuery objects must define a 'collection' method" end + # @rbs return: Object & Enumerable[untyped] def query records = collection preload_includes(records) if options[:includes] records end + # @rbs override def relation? false end + # @rbs override def eager? true end private + # @rbs override def underlying_query unwrap_relation(query) end - def unwrap_relation(query) - query.is_a?(Quo::Query) ? query.unwrap : query - end - + # @rbs (untyped records, ?untyped? preload) -> untyped def preload_includes(records, preload = nil) ::ActiveRecord::Associations::Preloader.new( records: records, diff --git a/lib/quo/engine.rb b/lib/quo/engine.rb index 08773bc..6ea62fb 100644 --- a/lib/quo/engine.rb +++ b/lib/quo/engine.rb @@ -1,3 +1,5 @@ +# rbs_inline: enabled + module Quo class Engine < ::Rails::Engine isolate_namespace Quo diff --git a/lib/quo/loaded_query.rb b/lib/quo/loaded_query.rb index e480bb8..4549c59 100644 --- a/lib/quo/loaded_query.rb +++ b/lib/quo/loaded_query.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true +# rbs_inline: enabled + module Quo class LoadedQuery < Quo::EagerQuery + # @rbs data: untyped, props: Symbol => untyped, block: () -> untyped + # @rbs return: Quo::LoadedQuery def self.wrap(data = nil, props: {}, &block) klass = Class.new(self) if block @@ -15,6 +19,7 @@ def self.wrap(data = nil, props: {}, &block) klass end + # @rbs override def loaded? true end diff --git a/lib/quo/query.rb b/lib/quo/query.rb index 5b5475a..2b10304 100644 --- a/lib/quo/query.rb +++ b/lib/quo/query.rb @@ -1,45 +1,100 @@ # frozen_string_literal: true -require_relative "utilities/callstack" -require_relative "utilities/compose" -require_relative "utilities/sanitize" -require_relative "utilities/wrap" +# rbs_inline: enabled require "literal" module Quo class Query < Literal::Struct include Literal::Types - include Quo::Utilities::Callstack - include Quo::Utilities::Compose - extend Quo::Utilities::Sanitize - extend Quo::Utilities::Wrap - - class << self - def prop(name, type, kind = :keyword, reader: :public, writer: :public, default: nil, shadow_check: true) - if shadow_check && reader && instance_methods.include?(name.to_sym) - raise ArgumentError, "Property name '#{name}' shadows an existing method" - end - if shadow_check && writer && instance_methods.include?(:"#{name}=") - raise ArgumentError, "Property name '#{name}' shadows an existing writer method '#{name}='" - end - super(name, type, kind, reader: reader, writer: writer, default: default) + + # @rbs conditions: untyped? + # @rbs return: String + def self.sanitize_sql_for_conditions(conditions) + ActiveRecord::Base.sanitize_sql_for_conditions(conditions) + end + + # @rbs string: String + # @rbs return: String + def self.sanitize_sql_string(string) + sanitize_sql_for_conditions(["'%s'", string]) + end + + # @rbs value: untyped + # @rbs return: String + def self.sanitize_sql_parameter(value) + sanitize_sql_for_conditions(["?", value]) + end + + # 'Smart' wrap Query, ActiveRecord::Relation or a data collection in a Query. + # Calls out to Quo::WrappedQuery.wrap or Quo::LoadedQuery.wrap as appropriate. + def self.wrap(query_rel_or_data, **options) + if query_rel_or_data < Quo::Query + query_rel_or_data + elsif query_rel_or_data.is_a?(ActiveRecord::Relation) + Quo::WrappedQuery.wrap(query_rel_or_data, **options) + else + Quo::LoadedQuery.wrap(query_rel_or_data) end + end - def call(**options) - new(**options).first + def self.wrap_instance(query_rel_or_data) + if query_rel_or_data.is_a?(Quo::Query) + query_rel_or_data + elsif query_rel_or_data.is_a?(ActiveRecord::Relation) + Quo::WrappedQuery.wrap(query_rel_or_data).new + else + Quo::LoadedQuery.wrap(query_rel_or_data).new end + end - def call!(**options) - new(**options).first! + # @rbs query: untyped + # @rbs return: bool + def self.composable_with?(query) + query.is_a?(Quo::Query) || query.is_a?(ActiveRecord::Relation) + end + + # Compose is aliased as `+`. Can optionally take `joins` parameters to add joins on merged relation. + # @rbs right: Quo::Query | ActiveRecord::Relation | Object & Enumerable[untyped] + # @rbs joins: Symbol | Hash[Symbol, untyped] | Array[Symbol | Hash[Symbol, untyped]] + def self.compose(right, joins: nil) + ComposedQuery.compose(self, right, joins: joins) + end + singleton_class.alias_method :+, :compose + + # @rbs ovveride + def self.prop(name, type, kind = :keyword, reader: :public, writer: :public, default: nil, shadow_check: true) + if shadow_check && reader && instance_methods.include?(name.to_sym) + raise ArgumentError, "Property name '#{name}' shadows an existing method" + end + if shadow_check && writer && instance_methods.include?(:"#{name}=") + raise ArgumentError, "Property name '#{name}' shadows an existing writer method '#{name}='" end + super(name, type, kind, reader: reader, writer: writer, default: default) end - COERCE_TO_INT = ->(value) do + # @rbs **options: untyped + # @rbs return: untyped + def self.call(**options) + new(**options).first + end + + # @rbs **options: untyped + # @rbs return: untyped + def self.call!(**options) + new(**options).first! + end + + COERCE_TO_INT = ->(value) do #: (untyped value) -> Integer? return if value == Literal::Null value&.to_i end + # @rbs! + # attr_accessor page (): Integer? + # attr_accessor current_page (): Integer? + # attr_accessor page_size (): Integer? + prop :page, _Nilable(Integer), &COERCE_TO_INT prop :current_page, _Nilable(Integer), &COERCE_TO_INT prop(:page_size, _Nilable(Integer), default: -> { Quo.default_page_size || 20 }, &COERCE_TO_INT) @@ -57,47 +112,70 @@ def call!(**options) # @page_size = options[:page_size]&.to_i || Quo.default_page_size || 20 # end - def page_index + def page_index #: Integer page || current_page end # @deprecated - to be removed!! - def options + def options #: Hash[Symbol, untyped] @options ||= to_h.dup end # Returns a active record query, or a Quo::Query instance - def query + def query #: Quo::Query | ::ActiveRecord::Relation raise NotImplementedError, "Query objects must define a 'query' method" end + # @rbs **overrides: untyped + # @rbs return: Quo::Query def copy(**overrides) self.class.new(**to_h.merge(overrides)).tap do |q| q.instance_variable_set(:@__transformer, transformer) end end + # Compose is aliased as `+`. Can optionally take `joins` parameters to add joins on merged relation. + # @rbs right: Quo::Query | ::ActiveRecord::Relation + # @rbs joins: untyped + # @rbs return: Quo::ComposedQuery + def merge(right, joins: nil) + ComposedQuery.merge_instances(self, right, joins: joins) + end + alias_method :+, :merge + # Methods to prepare the query + # @rbs limit: untyped + # @rbs return: Quo::Query def limit(limit) copy(limit: limit) end + # @rbs options: untyped + # @rbs return: Quo::Query def order(options) copy(order: options) end + # @rbs *options: untyped + # @rbs return: Quo::Query def group(*options) copy(group: options) end + # @rbs *options: untyped + # @rbs return: Quo::Query def includes(*options) copy(includes: options) end + # @rbs *options: untyped + # @rbs return: Quo::Query def preload(*options) copy(preload: options) end + # @rbs *options: untyped + # @rbs return: Quo::Query def select(*options) copy(select: options) end @@ -105,10 +183,14 @@ def select(*options) # The following methods actually execute the underlying query # Delegate SQL calculation methods to the underlying query - delegate :sum, :average, :minimum, :maximum, to: :query_with_logging + # @rbs def sum: (?untyped column_name) -> Numeric + # @rbs def average: (untyped column_name) -> Numeric + # @rbs def minimum: (untyped column_name) -> Numeric + # @rbs def maximum: (untyped column_name) -> Numeric + delegate :sum, :average, :minimum, :maximum, to: :configured_query # Gets the count of all results ignoring the current page and page size (if set). - def count + def count #: Integer count_query(underlying_query) end @@ -117,30 +199,37 @@ def count # Gets the actual count of elements in the page of results (assuming paging is being used, otherwise the count of # all results) - def page_count - count_query(query_with_logging) + def page_count #: Integer + count_query(configured_query) end # Delegate methods that let us get the model class (available on AR relations) + # @rbs def model: () -> (untyped | nil) + # @rbs def klass: () -> (untyped | nil) delegate :model, :klass, to: :underlying_query # Get first elements + # @rbs limit: ?Integer + # @rbs return: untyped def first(limit = nil) if transform? - res = query_with_logging.first(limit) + res = configured_query.first(limit) if res.is_a? Array res.map.with_index { |r, i| transformer&.call(r, i) } elsif !res.nil? - transformer&.call(query_with_logging.first(limit)) + transformer&.call(configured_query.first(limit)) end elsif limit - query_with_logging.first(limit) + configured_query.first(limit) else # Array#first will not take nil as a limit - query_with_logging.first + configured_query.first end end + # Get first elements or raise an error if none are found + # @rbs limit: ?Integer + # @rbs return: untyped def first!(limit = nil) item = first(limit) raise ActiveRecord::RecordNotFound, "No item could be found!" unless item @@ -148,33 +237,37 @@ def first!(limit = nil) end # Get last elements + # @rbs limit: ?Integer + # @rbs return: untyped def last(limit = nil) if transform? - res = query_with_logging.last(limit) + res = configured_query.last(limit) if res.is_a? Array res.map.with_index { |r, i| transformer&.call(r, i) } elsif !res.nil? transformer&.call(res) end elsif limit - query_with_logging.last(limit) + configured_query.last(limit) else - query_with_logging.last + configured_query.last end end # Convert to array + # @rbs return: Array[untyped] def to_a - arr = query_with_logging.to_a + arr = configured_query.to_a transform? ? arr.map.with_index { |r, i| transformer&.call(r, i) } : arr end + # @rbs return: Quo::LoadedQuery def to_eager Quo::LoadedQuery.wrap(to_a).new end alias_method :load, :to_eager - def results + def results #: Quo::Results Quo::Results.new(self, transformer: transformer) end @@ -191,82 +284,73 @@ def results :each_with_object, to: :results + # @rbs @__transformer: nil | ^(untyped, ?Integer) -> untyped + # Set a block used to transform data after query fetching + # @rbs block: ^(untyped, ?Integer) -> untyped + # @rbs return: self def transform(&block) @__transformer = block self end # Are there any results for this query? - def exists? - return query_with_logging.exists? if relation? - query_with_logging.present? + def exists? #: bool + return configured_query.exists? if relation? + configured_query.present? end # Are there no results for this query? - def none? + def none? #: bool !exists? end alias_method :empty?, :none? # Is this query object a relation under the hood? (ie not eager loaded) - def relation? + def relation? #: bool test_relation(configured_query) end # Is this query object eager loaded data under the hood? (ie not a relation) - def eager? + def eager? #: bool test_eager(configured_query) end # Is this query object paged? (ie is paging enabled) - def paged? + def paged? #: bool page_index.present? end # Is this query object transforming results? - def transform? + def transform? #: bool transformer.present? end # Return the SQL string for this query if its a relation type query object - def to_sql + def to_sql #: String configured_query.to_sql if relation? end # Unwrap the paginated query - def unwrap + def unwrap #: ActiveRecord::Relation configured_query end # Unwrap the un-paginated query - def unwrap_unpaginated + def unwrap_unpaginated #: ActiveRecord::Relation underlying_query end + # @rbs! def distinct: () -> ActiveRecord::Relation delegate :distinct, to: :configured_query private - def formatted_queries? - !!Quo.formatted_query_log - end - - # 'trim' a query, ie remove comments and remove newlines - # This will remove dashes from inside strings too - def trim_query(sql) - sql.gsub(/--[^\n'"]*\n/m, " ").tr("\n", " ").strip - end - - def format_query(sql_str) - formatted_queries? ? sql_str : trim_query(sql_str) - end - def transformer @__transformer end - def offset + def offset #: Integer per_page = sanitised_page_size page = if page_index&.positive? page_index @@ -277,13 +361,13 @@ def offset end # The configured query is the underlying query with paging - def configured_query + def configured_query #: ActiveRecord::Relation q = underlying_query return q unless paged? && q.is_a?(ActiveRecord::Relation) q.offset(offset).limit(sanitised_page_size) end - def sanitised_page_size + def sanitised_page_size #: Integer if page_size&.positive? given_size = page_size.to_i max_page_size = Quo.max_page_size || 200 @@ -297,13 +381,8 @@ def sanitised_page_size end end - def query_with_logging - debug_callstack - configured_query - end - # The underlying query is essentially the configured query with optional extras setup - def underlying_query + def underlying_query #: ActiveRecord::Relation @underlying_query ||= begin rel = unwrap_relation(query) @@ -319,14 +398,20 @@ def underlying_query end end + # @rbs query: Quo::Query | ::ActiveRecord::Relation + # @rbs return: ActiveRecord::Relation def unwrap_relation(query) query.is_a?(Quo::Query) ? query.unwrap : query end + # @rbs rel: untyped + # @rbs return: bool def test_eager(rel) rel.is_a?(Quo::LoadedQuery) || (rel.is_a?(Enumerable) && !test_relation(rel)) end + # @rbs rel: untyped + # @rbs return: bool def test_relation(rel) rel.is_a?(ActiveRecord::Relation) end @@ -339,6 +424,8 @@ def test_relation(rel) # `SELECT COUNT(count_column) FROM (SELECT * AS count_column FROM ...) subquery_for_count` where the error is: # `ActiveRecord::StatementInvalid: SQLite3::SQLException: near "AS": syntax error` # Either way DB engines know how to count efficiently. + # @rbs query: ActiveRecord::Relation + # @rbs return: Integer def count_query(query) pk = query.model.primary_key if pk diff --git a/lib/quo/results.rb b/lib/quo/results.rb index e111b57..c8c7e57 100644 --- a/lib/quo/results.rb +++ b/lib/quo/results.rb @@ -1,19 +1,23 @@ # frozen_string_literal: true +# rbs_inline: enabled + require "forwardable" -require_relative "utilities/callstack" module Quo class Results extend Forwardable - include Quo::Utilities::Callstack + # @rbs query: Quo::Query + # @rbs transformer: (^(untyped, ?Integer) -> untyped)? + # @rbs return: void def initialize(query, transformer: nil) @query = query @unwrapped = query.unwrap @transformer = transformer end + # TODO: RBS for these, def_delegators :unwrapped, :include?, :member?, @@ -24,8 +28,9 @@ def initialize(query, transformer: nil) def_delegators :query, :count + # @rbs &block: (untyped, *untyped) -> untyped + # @rbs return: Hash[untyped, Array[untyped]] def group_by(&block) - debug_callstack grouped = unwrapped.group_by do |*block_args| x = block_args.first transformed = transformer ? transformer.call(x) : x @@ -40,9 +45,9 @@ def group_by(&block) end # Delegate other enumerable methods to underlying collection but also transform + # @rbs override def method_missing(method, *args, **kwargs, &block) if unwrapped.respond_to?(method) - debug_callstack if block unwrapped.send(method, *args, **kwargs) do |*block_args| x = block_args.first @@ -61,14 +66,21 @@ def method_missing(method, *args, **kwargs, &block) end end + # @rbs name: Symbol + # @rbs include_private: bool + # @rbs return: bool def respond_to_missing?(name, include_private = false) enumerable_methods_supported.include?(name) end private - attr_reader :query, :transformer, :unwrapped + attr_reader :query #: Quo::Query + attr_reader :transformer #: (^(untyped, ?Integer) -> untyped)? + attr_reader :unwrapped #: ActiveRecord::Relation | Object & Enumerable[untyped] + # @rbs results: untyped + # @rbs return: untyped def transform_results(results) return results unless transformer @@ -79,6 +91,7 @@ def transform_results(results) end end + # @rbs return: Array[Symbol] def enumerable_methods_supported [:find_each] + Enumerable.instance_methods end diff --git a/lib/quo/utilities/callstack.rb b/lib/quo/utilities/callstack.rb deleted file mode 100644 index 1a2e5da..0000000 --- a/lib/quo/utilities/callstack.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -module Quo - module Utilities - module Callstack - def debug_callstack - return unless Rails.env.development? - callstack_size = Quo.query_show_callstack_size - return unless callstack_size&.positive? - working_dir = Dir.pwd - exclude = %r{/(gems/|rubies/|query\.rb)} - stack = Kernel.caller.grep_v(exclude).map { |l| l.gsub(working_dir + "/", "") } - stack_to_display = stack[0..callstack_size] - message = "\n[Query stack]: -> #{stack_to_display&.join("\n &> ")}\n" - message += " (truncated to #{callstack_size} most recent)" if callstack_size && stack.size > callstack_size - logger = Quo.logger&.call - logger&.info(message) - end - end - end -end diff --git a/lib/quo/utilities/compose.rb b/lib/quo/utilities/compose.rb deleted file mode 100644 index 6042801..0000000 --- a/lib/quo/utilities/compose.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module Quo - module Utilities - # Combine two query-like or composeable entities: - # These can be Quo::Query, Quo::MergedQuery, Quo::EagerQuery and ActiveRecord::Relations. - # See the `README.md` docs for more details. - module Compose - def self.included(base) - base.extend ClassMethods - end - - # Compose is aliased as `+`. Can optionally take `joins` parameters to add joins on merged relation. - def merge(right, joins: nil) - ComposedQuery.merge_instances(self, right, joins: joins) - end - - alias_method :+, :merge - - module ClassMethods - def composable_with?(query) - query.is_a?(Quo::Query) || query.is_a?(ActiveRecord::Relation) - end - - # Compose is aliased as `+`. Can optionally take `joins` parameters to add joins on merged relation. - def compose(right, joins: nil) - ComposedQuery.compose(self, right, joins: joins) - end - - alias_method :+, :compose - end - end - end -end diff --git a/lib/quo/utilities/sanitize.rb b/lib/quo/utilities/sanitize.rb deleted file mode 100644 index 33bcf5c..0000000 --- a/lib/quo/utilities/sanitize.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -module Quo - module Utilities - module Sanitize - def sanitize_sql_for_conditions(conditions) - ActiveRecord::Base.sanitize_sql_for_conditions(conditions) - end - - def sanitize_sql_string(string) - sanitize_sql_for_conditions(["'%s'", string]) - end - - def sanitize_sql_parameter(value) - sanitize_sql_for_conditions(["?", value]) - end - end - end -end diff --git a/lib/quo/utilities/wrap.rb b/lib/quo/utilities/wrap.rb deleted file mode 100644 index a653b44..0000000 --- a/lib/quo/utilities/wrap.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module Quo - module Utilities - # 'Smart' wrap Query, ActiveRecord::Relation or a data collection in a Query. - module Wrap - def wrap(query_rel_or_data, **options) - if query_rel_or_data < Quo::Query - query_rel_or_data - elsif query_rel_or_data.is_a?(ActiveRecord::Relation) - Quo::WrappedQuery.wrap(query_rel_or_data, **options) - else - Quo::LoadedQuery.wrap(query_rel_or_data) - end - end - - def wrap_instance(query_rel_or_data) - if query_rel_or_data.is_a?(Quo::Query) - query_rel_or_data - elsif query_rel_or_data.is_a?(ActiveRecord::Relation) - Quo::WrappedQuery.wrap(query_rel_or_data).new - else - Quo::LoadedQuery.wrap(query_rel_or_data).new - end - end - end - end -end diff --git a/lib/quo/version.rb b/lib/quo/version.rb index 3e6a3cb..ede8ff5 100644 --- a/lib/quo/version.rb +++ b/lib/quo/version.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# rbs_inline: enabled + module Quo VERSION = "1.0.0.alpha1" end diff --git a/lib/quo/wrapped_query.rb b/lib/quo/wrapped_query.rb index 927f72c..112ef98 100644 --- a/lib/quo/wrapped_query.rb +++ b/lib/quo/wrapped_query.rb @@ -1,7 +1,13 @@ # frozen_string_literal: true +# rbs_inline: enabled + module Quo class WrappedQuery < Quo.base_query_class + # @rbs query: ActiveRecord::Relation | Quo::Query + # @rbs props: Hash[Symbol, untyped] + # @rbs &block: () -> ActiveRecord::Relation | Quo::Query | Object & Enumerable[untyped] + # @rbs return: Quo::WrappedQuery def self.wrap(query = nil, props: {}, &block) klass = Class.new(self) do props.each do |name, type| diff --git a/sig/generated/quo.rbs b/sig/generated/quo.rbs new file mode 100644 index 0000000..01ae787 --- /dev/null +++ b/sig/generated/quo.rbs @@ -0,0 +1,7 @@ +# Generated from lib/quo.rb with RBS::Inline + +module Quo + extend ActiveSupport::Autoload + + def self.base_query_class: () -> Quo::Query +end diff --git a/sig/generated/quo/composed_query.rbs b/sig/generated/quo/composed_query.rbs new file mode 100644 index 0000000..0c7f5dd --- /dev/null +++ b/sig/generated/quo/composed_query.rbs @@ -0,0 +1,86 @@ +# Generated from lib/quo/composed_query.rb with RBS::Inline + +module Quo + # @rbs inherits Quo::Query + class ComposedQuery < Quo::Query + # Combine two Query classes into a new composed query class + # Combine two query-like or composeable entities: + # These can be Quo::Query, Quo::MergedQuery, Quo::EagerQuery and ActiveRecord::Relations. + # See the `README.md` docs for more details. + # @rbs left_query_class: singleton(Quo::Query | ::ActiveRecord::Relation) + # @rbs right_query_class: singleton(Quo::Query | ::ActiveRecord::Relation) + # @rbs joins: untyped + # @rbs return: singleton(Quo::ComposedQuery) + def self.compose: (untyped left_query_class, untyped right_query_class, ?joins: untyped) -> singleton(Quo::ComposedQuery) + + attr_reader _composing_joins: untyped + + attr_reader _left_query: untyped + + attr_reader _right_query: untyped + + # We can also merge instance of prepared queries + # @rbs left_instance: Quo::Query | ::ActiveRecord::Relation + # @rbs right_instance: Quo::Query | ::ActiveRecord::Relation + # @rbs joins: untyped + # @rbs return: Quo::ComposedQuery + def self.merge_instances: (Quo::Query | ::ActiveRecord::Relation left_instance, Quo::Query | ::ActiveRecord::Relation right_instance, ?joins: untyped) -> Quo::ComposedQuery + + # @rbs override + def self.inspect: ... + + # @rbs override + def query: ... + + # @rbs return: Quo::Query | ::ActiveRecord::Relation + def left: () -> (Quo::Query | ::ActiveRecord::Relation) + + # @rbs return: Quo::Query | ::ActiveRecord::Relation + def right: () -> (Quo::Query | ::ActiveRecord::Relation) + + # @rbs override + def inspect: ... + + private + + # @rbs return: Hash[Symbol, untyped] + def child_options: (untyped query_class) -> Hash[Symbol, untyped] + + # @rbs return: Array[Symbol] + def property_names: (untyped query_class) -> Array[Symbol] + + # @rbs return: ActiveRecord::Relation | Object & Enumerable[untyped] + def merge_left_and_right: (untyped left, untyped right, untyped joins) -> (ActiveRecord::Relation | Object & Enumerable[untyped]) + + # @rbs left_rel: ActiveRecord::Relation + # @rbs joins: untyped + # @rbs return: ActiveRecord::Relation + def apply_joins: (ActiveRecord::Relation left_rel, untyped joins) -> ActiveRecord::Relation + + # @rbs rel: untyped + # @rbs return: bool + def relation?: (untyped rel) -> bool + + # @rbs left: untyped + # @rbs right: untyped + # @rbs return: bool + def both_relations?: (untyped left, untyped right) -> bool + + # @rbs left: untyped + # @rbs right: untyped + # @rbs return: bool + def left_relation_right_enumerable?: (untyped left, untyped right) -> bool + + # @rbs left: untyped + # @rbs right: untyped + # @rbs return: bool + def left_enumerable_right_relation?: (untyped left, untyped right) -> bool + + # @rbs override + def unwrap_relation: ... + + # @rbs operand: Quo::ComposedQuery | Quo::Query | ::ActiveRecord::Relation + # @rbs return: String + def operand_desc: (Quo::ComposedQuery | Quo::Query | ::ActiveRecord::Relation operand) -> String + end +end diff --git a/sig/generated/quo/eager_query.rbs b/sig/generated/quo/eager_query.rbs new file mode 100644 index 0000000..c164d10 --- /dev/null +++ b/sig/generated/quo/eager_query.rbs @@ -0,0 +1,36 @@ +# Generated from lib/quo/eager_query.rb with RBS::Inline + +module Quo + # @rbs inherits Quo::Query + class EagerQuery < Quo::Query + # @rbs override + def count: ... + + # @rbs override + def page_count: ... + + # Is this query object paged? (when no total count) + # @rbs override + def paged?: ... + + # @rbs return: Object & Enumerable[untyped] + def collection: () -> (Object & Enumerable[untyped]) + + # @rbs return: Object & Enumerable[untyped] + def query: () -> (Object & Enumerable[untyped]) + + # @rbs override + def relation?: ... + + # @rbs override + def eager?: ... + + private + + # @rbs override + def underlying_query: ... + + # @rbs (untyped records, ?untyped? preload) -> untyped + def preload_includes: (untyped records, ?untyped? preload) -> untyped + end +end diff --git a/sig/generated/quo/engine.rbs b/sig/generated/quo/engine.rbs new file mode 100644 index 0000000..b6f80a1 --- /dev/null +++ b/sig/generated/quo/engine.rbs @@ -0,0 +1,6 @@ +# Generated from lib/quo/engine.rb with RBS::Inline + +module Quo + class Engine < ::Rails::Engine + end +end diff --git a/sig/generated/quo/loaded_query.rbs b/sig/generated/quo/loaded_query.rbs new file mode 100644 index 0000000..90f6cee --- /dev/null +++ b/sig/generated/quo/loaded_query.rbs @@ -0,0 +1,12 @@ +# Generated from lib/quo/loaded_query.rb with RBS::Inline + +module Quo + class LoadedQuery < Quo::EagerQuery + # @rbs data: untyped, props: Symbol => untyped, block: () -> untyped + # @rbs return: Quo::LoadedQuery + def self.wrap: (?untyped data, ?props: untyped) ?{ (?) -> untyped } -> Quo::LoadedQuery + + # @rbs override + def loaded?: ... + end +end diff --git a/sig/generated/quo/query.rbs b/sig/generated/quo/query.rbs new file mode 100644 index 0000000..8005073 --- /dev/null +++ b/sig/generated/quo/query.rbs @@ -0,0 +1,201 @@ +# Generated from lib/quo/query.rb with RBS::Inline + +module Quo + class Query < Literal::Struct + include Literal::Types + + # @rbs conditions: untyped? + # @rbs return: String + def self.sanitize_sql_for_conditions: (untyped? conditions) -> String + + # @rbs string: String + # @rbs return: String + def self.sanitize_sql_string: (String string) -> String + + # @rbs value: untyped + # @rbs return: String + def self.sanitize_sql_parameter: (untyped value) -> String + + # 'Smart' wrap Query, ActiveRecord::Relation or a data collection in a Query. + # Calls out to Quo::WrappedQuery.wrap or Quo::LoadedQuery.wrap as appropriate. + def self.wrap: (untyped query_rel_or_data, **untyped options) -> untyped + + def self.wrap_instance: (untyped query_rel_or_data) -> untyped + + # @rbs query: untyped + # @rbs return: bool + def self.composable_with?: (untyped query) -> bool + + # Compose is aliased as `+`. Can optionally take `joins` parameters to add joins on merged relation. + # @rbs right: Quo::Query | ActiveRecord::Relation | Object & Enumerable[untyped] + # @rbs joins: Symbol | Hash[Symbol, untyped] | Array[Symbol | Hash[Symbol, untyped]] + def self.compose: (Quo::Query | ActiveRecord::Relation | Object & Enumerable[untyped] right, ?joins: Symbol | Hash[Symbol, untyped] | Array[Symbol | Hash[Symbol, untyped]]) -> untyped + + # @rbs ovveride + def self.prop: (untyped name, untyped type, ?untyped kind, ?reader: untyped, ?writer: untyped, ?default: untyped, ?shadow_check: untyped) -> untyped + + # @rbs **options: untyped + # @rbs return: untyped + def self.call: (**untyped options) -> untyped + + # @rbs **options: untyped + # @rbs return: untyped + def self.call!: (**untyped options) -> untyped + + COERCE_TO_INT: untyped + + attr_accessor page(): Integer? + + attr_accessor current_page(): Integer? + + attr_accessor page_size(): Integer? + + def page_index: () -> Integer + + # @deprecated - to be removed!! + def options: () -> Hash[Symbol, untyped] + + # Returns a active record query, or a Quo::Query instance + def query: () -> (Quo::Query | ::ActiveRecord::Relation) + + # @rbs **overrides: untyped + # @rbs return: Quo::Query + def copy: (**untyped overrides) -> Quo::Query + + # Compose is aliased as `+`. Can optionally take `joins` parameters to add joins on merged relation. + # @rbs right: Quo::Query | ::ActiveRecord::Relation + # @rbs joins: untyped + # @rbs return: Quo::ComposedQuery + def merge: (Quo::Query | ::ActiveRecord::Relation right, ?joins: untyped) -> Quo::ComposedQuery + + # Methods to prepare the query + # @rbs limit: untyped + # @rbs return: Quo::Query + def limit: (untyped limit) -> Quo::Query + + # @rbs options: untyped + # @rbs return: Quo::Query + def order: (untyped options) -> Quo::Query + + # @rbs *options: untyped + # @rbs return: Quo::Query + def group: (*untyped options) -> Quo::Query + + # @rbs *options: untyped + # @rbs return: Quo::Query + def includes: (*untyped options) -> Quo::Query + + # @rbs *options: untyped + # @rbs return: Quo::Query + def preload: (*untyped options) -> Quo::Query + + # @rbs *options: untyped + # @rbs return: Quo::Query + def select: (*untyped options) -> Quo::Query + + # Gets the count of all results ignoring the current page and page size (if set). + def count: () -> Integer + + # Gets the actual count of elements in the page of results (assuming paging is being used, otherwise the count of + # all results) + def page_count: () -> Integer + + # Get first elements + # @rbs limit: ?Integer + # @rbs return: untyped + def first: (?untyped limit) -> untyped + + # Get first elements or raise an error if none are found + # @rbs limit: ?Integer + # @rbs return: untyped + def first!: (?untyped limit) -> untyped + + # Get last elements + # @rbs limit: ?Integer + # @rbs return: untyped + def last: (?untyped limit) -> untyped + + # Convert to array + # @rbs return: Array[untyped] + def to_a: () -> Array[untyped] + + # @rbs return: Quo::LoadedQuery + def to_eager: () -> Quo::LoadedQuery + + def results: () -> Quo::Results + + @__transformer: nil | ^(untyped, ?Integer) -> untyped + + # Set a block used to transform data after query fetching + # @rbs block: ^(untyped, ?Integer) -> untyped + # @rbs return: self + def transform: () ?{ (?) -> untyped } -> self + + # Are there any results for this query? + def exists?: () -> bool + + # Are there no results for this query? + def none?: () -> bool + + # Is this query object a relation under the hood? (ie not eager loaded) + def relation?: () -> bool + + # Is this query object eager loaded data under the hood? (ie not a relation) + def eager?: () -> bool + + # Is this query object paged? (ie is paging enabled) + def paged?: () -> bool + + # Is this query object transforming results? + def transform?: () -> bool + + # Return the SQL string for this query if its a relation type query object + def to_sql: () -> String + + # Unwrap the paginated query + def unwrap: () -> ActiveRecord::Relation + + # Unwrap the un-paginated query + def unwrap_unpaginated: () -> ActiveRecord::Relation + + def distinct: () -> ActiveRecord::Relation + + private + + def transformer: () -> untyped + + def offset: () -> Integer + + # The configured query is the underlying query with paging + def configured_query: () -> ActiveRecord::Relation + + def sanitised_page_size: () -> Integer + + # The underlying query is essentially the configured query with optional extras setup + def underlying_query: () -> ActiveRecord::Relation + + # @rbs query: Quo::Query | ::ActiveRecord::Relation + # @rbs return: ActiveRecord::Relation + def unwrap_relation: (Quo::Query | ::ActiveRecord::Relation query) -> ActiveRecord::Relation + + # @rbs rel: untyped + # @rbs return: bool + def test_eager: (untyped rel) -> bool + + # @rbs rel: untyped + # @rbs return: bool + def test_relation: (untyped rel) -> bool + + # Note we reselect the query as this prevents query errors if the SELECT clause is not compatible with COUNT + # (SQLException: wrong number of arguments to function COUNT()). We do this in two ways, either with the primary key + # or with Arel.star. The primary key is the most compatible way to count, but if the query does not have a primary + # we fallback. The fallback "*" wont work in certain situations though, specifically if we have a limit() on the query + # which Arel constructs as a subquery. In this case we will get a SQL error as the generated SQL contains + # `SELECT COUNT(count_column) FROM (SELECT * AS count_column FROM ...) subquery_for_count` where the error is: + # `ActiveRecord::StatementInvalid: SQLite3::SQLException: near "AS": syntax error` + # Either way DB engines know how to count efficiently. + # @rbs query: ActiveRecord::Relation + # @rbs return: Integer + def count_query: (ActiveRecord::Relation query) -> Integer + end +end diff --git a/sig/generated/quo/results.rbs b/sig/generated/quo/results.rbs new file mode 100644 index 0000000..9d1cdaa --- /dev/null +++ b/sig/generated/quo/results.rbs @@ -0,0 +1,40 @@ +# Generated from lib/quo/results.rb with RBS::Inline + +module Quo + class Results + extend Forwardable + + # @rbs query: Quo::Query + # @rbs transformer: (^(untyped, ?Integer) -> untyped)? + # @rbs return: void + def initialize: (Quo::Query query, ?transformer: (^(untyped, ?Integer) -> untyped)?) -> void + + # @rbs &block: (untyped, *untyped) -> untyped + # @rbs return: Hash[untyped, Array[untyped]] + def group_by: () { (untyped, *untyped) -> untyped } -> Hash[untyped, Array[untyped]] + + # Delegate other enumerable methods to underlying collection but also transform + # @rbs override + def method_missing: ... + + # @rbs name: Symbol + # @rbs include_private: bool + # @rbs return: bool + def respond_to_missing?: (Symbol name, ?bool include_private) -> bool + + private + + attr_reader query: Quo::Query + + attr_reader transformer: (^(untyped, ?Integer) -> untyped)? + + attr_reader unwrapped: ActiveRecord::Relation | Object & Enumerable[untyped] + + # @rbs results: untyped + # @rbs return: untyped + def transform_results: (untyped results) -> untyped + + # @rbs return: Array[Symbol] + def enumerable_methods_supported: () -> Array[Symbol] + end +end diff --git a/sig/generated/quo/version.rbs b/sig/generated/quo/version.rbs new file mode 100644 index 0000000..2a93e94 --- /dev/null +++ b/sig/generated/quo/version.rbs @@ -0,0 +1,5 @@ +# Generated from lib/quo/version.rb with RBS::Inline + +module Quo + VERSION: ::String +end diff --git a/sig/generated/quo/wrapped_query.rbs b/sig/generated/quo/wrapped_query.rbs new file mode 100644 index 0000000..0b16184 --- /dev/null +++ b/sig/generated/quo/wrapped_query.rbs @@ -0,0 +1,11 @@ +# Generated from lib/quo/wrapped_query.rb with RBS::Inline + +module Quo + class WrappedQuery + # @rbs query: ActiveRecord::Relation | Quo::Query + # @rbs props: Hash[Symbol, untyped] + # @rbs &block: () -> ActiveRecord::Relation | Quo::Query | Object & Enumerable[untyped] + # @rbs return: Quo::WrappedQuery + def self.wrap: (?ActiveRecord::Relation | Quo::Query query, ?props: Hash[Symbol, untyped]) ?{ (?) -> untyped } -> Quo::WrappedQuery + end +end diff --git a/sig/quo.rbs b/sig/quo.rbs deleted file mode 100644 index 938df5d..0000000 --- a/sig/quo.rbs +++ /dev/null @@ -1,28 +0,0 @@ -module ActiveRecord - module Associations - class Preloader - def initialize: (records: untyped, associations: untyped, ?scope: untyped, ?available_records: Array[untyped], ?associate_by_default: bool) -> void - end - end -end - -module Quo - VERSION: String - - type query = Quo::Query - type queryOrRel = query | ActiveRecord::Relation - type enumerable = Object & Enumerable[untyped] - type relOrEnumerable = ActiveRecord::Relation | enumerable - type loadedQueryOrEnumerable = LoadedQuery | EagerQuery | enumerable - type composable = query | relOrEnumerable - - # TODO: how can we do the known options, eg `page` and then allow anything else? - # Maybe we should separate out the known options from the unknown options - type queryOptions = Hash[Symbol, untyped] - - interface _Logger - def info: (String) -> void - def error: (String) -> void - def debug: (String) -> void - end -end diff --git a/sig/quo/eager_query.rbs b/sig/quo/eager_query.rbs deleted file mode 100644 index d241552..0000000 --- a/sig/quo/eager_query.rbs +++ /dev/null @@ -1,15 +0,0 @@ -module Quo - class EagerQuery < Quo::Query - def collection: () -> loadedQueryOrEnumerable - def query: () -> loadedQueryOrEnumerable - - def relation?: () -> false - def eager?: () -> true - - private - - def preload_includes: (untyped records, ?untyped? preload) -> untyped - def underlying_query: () -> enumerable - def unwrap_relation: (loadedQueryOrEnumerable collection) -> enumerable - end -end diff --git a/sig/quo/loaded_query.rbs b/sig/quo/loaded_query.rbs deleted file mode 100644 index 79b303b..0000000 --- a/sig/quo/loaded_query.rbs +++ /dev/null @@ -1,7 +0,0 @@ -module Quo - class LoadedQuery < Quo::EagerQuery - @collection: enumerable - - def initialize: (enumerable, **untyped options) -> void - end -end diff --git a/sig/quo/merged_query.rbs b/sig/quo/merged_query.rbs deleted file mode 100644 index 9fb457b..0000000 --- a/sig/quo/merged_query.rbs +++ /dev/null @@ -1,19 +0,0 @@ -module Quo - class MergedQuery < Quo::Query - def self.build_from_options: (queryOptions) -> MergedQuery - - def initialize: (relOrEnumerable merged, composable left, composable right, **untyped options) -> void - - @merged_query: relOrEnumerable - - def query: () -> relOrEnumerable - - def inspect: () -> ::String - - private - - attr_reader left: composable - attr_reader right: composable - def operand_desc: (composable operand) -> String - end -end diff --git a/sig/quo/query.rbs b/sig/quo/query.rbs deleted file mode 100644 index f1292a4..0000000 --- a/sig/quo/query.rbs +++ /dev/null @@ -1,83 +0,0 @@ -module Quo - class Query - include Quo::Utilities::Callstack - extend Quo::Utilities::Compose - extend Quo::Utilities::Sanitize - extend Quo::Utilities::Wrap - - @underlying_query: ActiveRecord::Relation - - def self.call: (**untyped options) -> untyped - def self.call!: (**untyped options) -> untyped - - @scope: ActiveRecord::Relation | nil - - attr_reader current_page: Integer? - attr_reader page_size: Integer? - attr_reader options: Hash[untyped, untyped] - - def initialize: (**untyped options) -> void - def query: () -> queryOrRel - def compose: (composable right, ?joins: untyped?) -> Quo::MergedQuery - alias + compose - - def copy: (**untyped options) -> Quo::Query - - def limit: (untyped limit) -> Quo::Query - def order: (untyped options) -> Quo::Query - def group: (*untyped options) -> Quo::Query - def includes: (*untyped options) -> Quo::Query - def preload: (*untyped options) -> Quo::Query - def select: (*untyped options) -> Quo::Query - - def sum: (?untyped column_name) -> Numeric - def average: (untyped column_name) -> Numeric - def minimum: (untyped column_name) -> Numeric - def maximum: (untyped column_name) -> Numeric - def count: () -> Integer - - alias total_count count - - alias size count - def page_count: () -> Integer - def first: (?Integer? limit) -> untyped - def first!: (?Integer? limit) -> untyped - def last: (?Integer? limit) -> untyped - def to_a: () -> Array[untyped] - def to_eager: (?::Hash[untyped, untyped] more_opts) -> Quo::EagerQuery - alias load to_eager - def results: () -> Quo::Results - - # Set a block used to transform data after query fetching - def transform: () ?{ () -> untyped } -> self - - def exists?: () -> bool - def none?: () -> bool - alias empty? none? - def relation?: () -> bool - def eager?: () -> bool - def paged?: () -> bool - - def model: () -> (untyped | nil) - def klass: () -> (untyped | nil) - - def transform?: () -> bool - def to_sql: () -> (String | nil) - def unwrap: () -> ActiveRecord::Relation - - private - - def formatted_queries?: () -> bool - def trim_query: (String sql) -> String - def format_query: (String sql_str) -> String - def transformer: () -> (nil | ^(untyped, ?Integer) -> untyped) - def offset: () -> Integer - def configured_query: () -> ActiveRecord::Relation - def sanitised_page_size: () -> Integer - def query_with_logging: () -> ActiveRecord::Relation - def underlying_query: () -> ActiveRecord::Relation - def unwrap_relation: (queryOrRel query) -> ActiveRecord::Relation - def test_eager: (composable rel) -> bool - def test_relation: (composable rel) -> bool - end -end diff --git a/sig/quo/query_composer.rbs b/sig/quo/query_composer.rbs deleted file mode 100644 index a83bfb5..0000000 --- a/sig/quo/query_composer.rbs +++ /dev/null @@ -1,32 +0,0 @@ -module Quo - class QueryComposer - @left_relation: bool - @right_relation: bool - - def initialize: (composable left, composable right, ?untyped? joins) -> void - def compose: () -> Quo::MergedQuery - - private - - attr_reader left: composable - attr_reader right: composable - attr_reader joins: untyped - - attr_reader unwrapped_left: relOrEnumerable - attr_reader unwrapped_right: relOrEnumerable - - def left_relation?: -> bool - - def merge_left_and_right: () -> relOrEnumerable - def merged_options: () -> ::Hash[untyped, untyped] - - def right_relation?: -> bool - - def unwrap_relation: (composable) -> relOrEnumerable - def relation_type?: (relOrEnumerable) -> bool - def apply_joins: (ActiveRecord::Relation left_rel, untyped joins) -> ActiveRecord::Relation - def both_relations?: () -> bool - def left_relation_right_enumerable?: () -> bool - def left_enumerable_right_relation?: () -> bool - end -end diff --git a/sig/quo/results.rbs b/sig/quo/results.rbs deleted file mode 100644 index 178b1fe..0000000 --- a/sig/quo/results.rbs +++ /dev/null @@ -1,22 +0,0 @@ -module Quo - class Results - extend Forwardable - - include Quo::Utilities::Callstack - - def initialize: (Quo::Query query, ?transformer: (^(untyped, ?Integer) -> untyped)?) -> void - - @query: Quo::Query - - def group_by: () { (untyped, *untyped) -> untyped } -> Hash[untyped, Array[untyped]] - - def respond_to_missing?: (Symbol name, ?bool include_private) -> bool - - private - - attr_reader transformer: (^(untyped, ?Integer) -> untyped)? - attr_reader unwrapped: relOrEnumerable - - def transform_results: (untyped) -> untyped - end -end diff --git a/sig/quo/utilities/callstack.rbs b/sig/quo/utilities/callstack.rbs deleted file mode 100644 index 84ba22e..0000000 --- a/sig/quo/utilities/callstack.rbs +++ /dev/null @@ -1,7 +0,0 @@ -module Quo - module Utilities - module Callstack - def debug_callstack: () -> void - end - end -end diff --git a/sig/quo/utilities/compose.rbs b/sig/quo/utilities/compose.rbs deleted file mode 100644 index a469e08..0000000 --- a/sig/quo/utilities/compose.rbs +++ /dev/null @@ -1,8 +0,0 @@ -module Quo - module Utilities - module Compose - def compose: (composable query1, composable query2, ?joins: untyped?) -> Quo::MergedQuery - def composable_with?: (queryOrRel query) -> bool - end - end -end diff --git a/sig/quo/utilities/sanitize.rbs b/sig/quo/utilities/sanitize.rbs deleted file mode 100644 index 775b0b7..0000000 --- a/sig/quo/utilities/sanitize.rbs +++ /dev/null @@ -1,9 +0,0 @@ -module Quo - module Utilities - module Sanitize - def sanitize_sql_for_conditions: (untyped conditions) -> untyped? - def sanitize_sql_string: (untyped string) -> untyped? - def sanitize_sql_parameter: (untyped value) -> untyped? - end - end -end diff --git a/sig/quo/utilities/wrap.rbs b/sig/quo/utilities/wrap.rbs deleted file mode 100644 index d74a46f..0000000 --- a/sig/quo/utilities/wrap.rbs +++ /dev/null @@ -1,11 +0,0 @@ -module Quo - module Utilities - interface _Wrapable - def new: (**untyped options) -> query - end - - module Wrap : _Wrapable - def wrap: (composable query_rel_or_data, **untyped options) -> query - end - end -end diff --git a/sig/quo/wrapped_query.rbs b/sig/quo/wrapped_query.rbs deleted file mode 100644 index e63086a..0000000 --- a/sig/quo/wrapped_query.rbs +++ /dev/null @@ -1,11 +0,0 @@ -module Quo - class WrappedQuery < Quo::Query - def self.build_from_options: (**untyped options) -> WrappedQuery - - @wrapped_query: ActiveRecord::Relation - - def initialize: (ActiveRecord::Relation query, **untyped options) -> void - - def query: () -> ActiveRecord::Relation - end -end diff --git a/test/dummy/config/initializers/quo.rb b/test/dummy/config/initializers/quo.rb index 4b7fa9e..6efcbf6 100644 --- a/test/dummy/config/initializers/quo.rb +++ b/test/dummy/config/initializers/quo.rb @@ -1,5 +1 @@ -Quo.formatted_query_log = true -Quo.query_show_callstack_size = 5 -Quo.logger = -> { Rails.logger } Quo.base_query_class = "ApplicationQuery" -