Why does .order doesn't get executed in my query? - ruby-on-rails

I have slightly modified version for Kaminari to find on which page my record is (original code is here: https://github.com/kaminari/kaminari/wiki/FAQ#how-can-i-know-which-page-a-record-is-on):
module KaminariHelper
extend ActiveSupport::Concern
# Detecting page number in pagination. This method allows you to specify column name, order,
# items per page and collection ids to filter specific collection.
#
# Options:
# * :by - specifies the column name. Defaults to :id.
# * :order - specifies the order in DB. Defaults to :asc.
# * :per - specifies amount of items per page. Defaults to object model's default_per_page.
# * :nulls_last - if set to true, will add "nulls last" into the order.
# * :collection_ids - array of ids for the collection of current class to search position in specific collection.
# If it's ommited then search is done across all objects of current object's model.
#
def page_num(options = {})
column = options[:by] || :id
order = options[:order] || :asc
per = options[:per] || self.class.default_per_page
nulls_last = options[:nulls_last] == true ? "nulls last" : ""
operator = (order == :asc ? "<=" : ">=")
data = if options[:collection_ids].present?
self.class.where(id: options[:collection_ids])
else
self.class
end
# 1. Get a count number of all the results that are listed before the given record/value.
# 2. Divide the count by default_per_page or per number given as an option.
# 3. Ceil the number to get a proper page number that the record/value is on.
(
data.where("#{column} #{operator} ?", read_attribute(column))
.order("#{column} #{order} #{nulls_last}").count.to_f / per
).ceil
end
end
However when I test it for some weird reasons .order doesn't seem to be executed. Here is the sql output in rails console:
2.3.1 :005 > email.page_num(by: :sent_at, order: :desc, per: 25, collection_ids: user.emails.ids, nulls_last: true)
(1.1ms) SELECT "emails"."id" FROM "emails" WHERE "emails"."deleted_at" IS NULL
AND "emails"."user_id" = $1 [["user_id", 648]]
(1.5ms) SELECT COUNT(*) FROM "emails" WHERE "emails"."deleted_at" IS NULL AND
"emails"."id" IN (35946, 41741) AND (sent_at >= '2016-01-22 14:04:26.700352')
=> 13
Why does .order is not applied in the final SQL query? Is there a way to execute it? Otherwise the code doesn't make sense since there are no guarantee it'll give me proper page number.

Rails does ignore the order clause when counting.
Instead of relying on Rails' count method, try counting manually:
data.where("#{column} #{operator} ?", read_attribute(column))
.order("#{column} #{order} #{nulls_last}")
.select('COUNT(*) c').first.c.to_f / per

Related

ActiveRecord select with OR and exclusive limit

I have the need to query the database and retrieve the last 10 objects that are either active or declined. We use the following:
User.where(status: [:active, :declined]).limit(10)
Now we need to get the last 10 of each status (total of 20 users)
I've tried the following:
User.where(status: :active).limit(10).or(User.where(status: : declined).limit(10))
# SELECT "users".* FROM "users" WHERE ("users"."status" = $1 OR "users"."status" = $2) LIMIT $3
This does the same as the previous query and returns only 10 users, of mixed statuses.
How can I get the last 10 active users and the last 10 declined users with a single query?
I'm not sure that SQL allows doing what you want. First thing I would try would be to use a subquery, something like this:
class User < ApplicationRecord
scope :active, -> { where status: :active }
scope :declined, -> { where status: :declined }
scope :last_active_or_declined, -> {
where(id: active.limit(10).pluck(:id))
.or(where(id: declined.limit(10).pluck(:id))
}
end
Then somewhere else you could just do
User.last_active_or_declined()
What this does is to perform 2 different subqueries asking separately for each of the group of users and then getting the ones in the propper group ids. I would say you could even forget about the pluck(:id) parts since ActiveRecord is smart enough to add the proper select clause to your SQL, but I'm not 100% sure and I don't have any Rails project at hand where I can try this.
limit is not a permitted value for #or relationship. If you check the Rails code, the Error raised come from here:
def or!(other) # :nodoc:
incompatible_values = structurally_incompatible_values_for_or(other)
unless incompatible_values.empty?
raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}"
end
# more code
end
You can check which methods are restricted further down in the code here:
STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope, :references]
def structurally_incompatible_values_for_or(other)
STRUCTURAL_OR_METHODS.reject do |method|
get_value(method) == other.get_value(method)
end
end
You can see in the Relation class here that limit is restricted:
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache,
:skip_preloading]
So you will have to resort to raw SQL I'm afraid
I don't think you can do it with a single query, but you can do it with two queries, get the record ids, and then build a query using those record ids.
It's not ideal but as you're just plucking ids the impact isn't too bad.
user_ids = User.where(status: :active).limit(10).pluck(:id) + User.where(status: :declined).limit(10).pluck(id)
users = User.where(id: user_ids)
I think you can use UNION. Install active_record_union and replace or with union:
User.where(status: :active).limit(10).union(User.where(status: :declined).limit(10))

Dynamic where clause in active record avoiding sql injection

do you know how to build a dynamic query avoiding sql injection
?
property = 'foo'
value = 'bar'
SomeObject.where("#{property} > ?", value)
# works but permit sql inj
SomeObject.where(":property > :value", property: property, value: value)
# create select * from some_object where 'foo' > 'bar'
# and the 'foo' I need without the quotes
SomeObject.where(
"#{SomeObject.connection.quote_column_name(property)} > :value",
value: value
)
UPDATE
Example 1 (trying to end the statement and inject a new one):
property = '; DROP TABLE users; --'
User.where("#{User.connection.quote_column_name(property)} > :value", value: 3)
# => SELECT "users".* FROM "users" WHERE ("; DROP TABLE users; --" > 3)
Example 2 (trying to end the column name quote):
property = '"; DELETE FROM users;--'
User.where("#{User.connection.quote_column_name(property)} > :value", value: 3)
# => SELECT "users".* FROM "users" WHERE ("""; DELETE FROM users;--" > 3)
Not sure of your use case but arel can help you with this like so
some_object_table = SomeObject.arel_table
SomeObject.where(some_object_table[property.intern].gt(value))
This will execute the query appropriately with all the escaping you have come to love with rails.
This works because arel is the underlying query assembler used by rails so ActiveRecord where clauses can understand Arel::Nodes without issue (its actually how they are assembled to begin with)
Also given the dynamic nature you may want to check that property is a valid column to avoid SQL level errors something like
raise AgrumentError unless some_object_table.engine.columns.map {|c| c.name.intern}.include?(property.intern)
# or
raise AgrumentError unless SomeObject.column_names.map(&:to_sym).include?(property.to_sym)
A simple but secure way would be to whitelist the allowed property names:
PROPERTIES = ["foo", "bar", "baz"].freeze
def find_greater_than(property, value)
raise "'#{property}' is not a valid property, only #{PROPERTIES.join(", ")} are allowed!" if !PROPERTIES.include?(property)
SomeObject.where("#{property} > ?", value)
end
You can (as #engineersmnky pointed out) dynamically check for available columns:
raise "Some Message" if SomeObject.column_names.include?(property)
but I don't like this aproach as having columns searchable should be a decission, not automated.
Another aproach is to use the sanitizing provided by Rails.
def find_greater_than(property, value)
sanitized_property = ActiveRecord::Base.connection.quote_column_name(property)
SomeObject.where("#{sanitized_property} > ?", value)
end
The quoting logic is implemented by the DB specific connection adapters.
In my case, I ended by using
ActiveRecord::Base.connection.quote_table_name function to sanitize the column name before queries.
property = 'foo'
value = 'bar'
sanitized_property = ActiveRecord::Base.connection.quote_table_name(property)
SomeObject.where("#{sanitized_property} > ?", value)
that function can handle table and column name definition.
property = 'table.column'
will produce
where `table`.`column` > `bar`

how to select distinct fields grouped by one field and concat one field with delimiter in ruby

I have an table that includes these field :
table phonebook
id
user_id
number
name
added
card_id
speeddial
updated_at
sms_group_name
some records have same sms_group_name and number,but there is some duplicate number for same sms_group_name. First I want to take distinct number for each sms_group_name and group_concat with , delimiter.
query result must be like this :
user_id, number,number,number,number,sms_group_name
select where condition is user_id
I tried all of them :
# #a = Phonebook.select(["DISTINCT number","sms_group_name"]).where(user_id: session[:user_id]).order(:sms_group_name).distinct
# #a = Phonebook.where(user_id: session[:user_id])
# Product.where.not(restaurant_id: nil).select("DISTINCT ON(name) name, restaurant_id, price, updated_at").order("name, updated_at")
# #a = Phonebook.where(user_id: session[:user_id]).select("DISTINCT ON(number) number, added, user_id, speeddial, updated_at,sms_group_name").order("sms_group_name")
# Phonebook.select("DISTINCT(number), *").where("user_id = ?", session[:user_id]).order("sms_group_name ASC").group_by(&:sms_group_name)
# Location.where("calendar_account_id = ?", current_user.calendar_accounts.first).group(:alias).order("alias ASC").group_by(&:category)
# #a = Phonebook.where("user_id = ?", session[:user_id]).order("sms_group_name ASC").group(:sms_group_name)
# #a = Phonebook.select("DISTINCT(number), sms_group_name").where("user_id = ?", session[:user_id]).order("sms_group_name ASC").group_by(&:sms_group_name)
# #a = Phonebook.select(:number)distinct.where("user_id = ?", session[:user_id]).order("sms_group_name ASC").group_by(&:sms_group_name)
# #a = Phonebook.select("DISTINCT(number), sms_group_name").group("sms_group_name")
most of them give error or does not work.
how can achive this ?
I tried select all values into object array ,after that I tried to eliminate them, but there is no suitable solution for now.
what can be solutions for both:
1- solution by using query and may be added one block
2- solution by using hash or array
best regards,

Rails find range in console

Rails 3.2
How do you do find a range to last record in the console?
Distributor.find_by_id(2..last)
Thank you
You can simply do:
Distributor.where('id >= 2')
This also works, but it performs 2 queries:
Distributor.where(id: 2..Distributor.last.id)
last(limit = nil)
Find the last record (or last N records if a parameter is supplied). If no order is defined it will order by primary key.
Person.last # returns the last object fetched by SELECT * FROM people
Person.where(["user_name = ?", user_name]).last
Person.order("created_on DESC").offset(5).last
Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html
def last(limit = nil)
if limit
if order_values.empty? && primary_key
order(arel_table[primary_key].desc).limit(limit).reverse
else
to_a.last(limit)
end
else
find_last
end
end
https://github.com/rails/rails/blob/4e823b61190388219868744a34dcfe926bad511c/activerecord/lib/active_record/relation/finder_methods.rb#L157

Ordering in a select clause, to support many columns

I want to select users from the users table based on:
user.created
user.sales_count
So I want to fetch all users, sometimes ordered by created date, and sometimes based on sales_count. And I want to be able to switch between ASC or DESC order.
All queries need to have this WHERE clause:
WHERE region = 123
How can I build my active record query to support these order by conditions?
def get_users(options={})
options[:order_col] ||= "created"
options[:order_type] ||= ""
User.where(:region=>123).order("#{options[:order_col]} #{options[:order_type]}")
end
options[:order_col] ||= is really saying:
options[:order_col] = options[:order_col] || ""
which in english is saying set options[:order_col] to options[:order_col] if set, other wise "". We can set the order_type to "" because SQL will by default order results ASC.
Example:
get_users #=> return ordered by created ASC
get_users(:order_col => "sales_count") #=> return order by sales count ASC
get_users(:order_col => "sales_count", :order_type => "DESC") #=> sales_count, DESC
# etc

Resources