How to query timestamp with Frozen Record gem - ruby-on-rails

I have a new timestamp field added to my FrozenRecord table. When I try,
TableName.where("expires_at < ?", Time.now)
this is the error message I get. Link to the line in repo
ArgumentError: wrong number of arguments (given 2, expected 0..1) from
/Users/preethikumar/.gem/ruby/3.1.2/gems/frozen_record-0.26.2/lib/frozen_record/scope.rb:105:in
`where'
How can I query fields with timestamps?

Error Message:
For this gem where only expects a single argument criterias, moreover it appears to expect the symbol :chain or a Hash (or other object that responds to map and yields key, value).
The gem Docs show "Supported query interfaces" as where(region: 'Europe').
Notably it does not say supports ActiveRecord query interfaces.
Additionally it goes on to show "Non supported query interfaces" such as where('region = "Europe" AND language != "English"').
Proposed Solution:
How can I query fields with timestamps?
While this gem does not appear to support less than (greater than) as part of the natural DSL, in your particular instance you should be able to use a beginless exclusive range instead which will use a CoverMatcher e.g.
TableName.where(expires_at: ...Time.now)
CoverMatcher#match? just uses #value.cover?(other) so this should produce the desired result via ruby's Range#cover?. For example:
t = Time.now
r = ...t
r.cover?(t)
#=> false
r.cover?(t - 1)
#=> true
r.cover?(t + 1)
#=> false

Related

matches_any throws exception on empty array writing custom filter for Datatables using Arel

I have am working with Ruby on Rails using the ajax-datatables-rails gem for datatables for which I need a custom filter.
My current filter looks like this:
def filter_status_column
statuses = []
->(column, value) do
# Do stuff and put statuses in the array
::Arel::Nodes::SqlLiteral.new(column.field.to_s).matches_any(statuses)
end
end
This, when my array is not empty will generate some sql like this:
0> ::Arel::Nodes::SqlLiteral.new(column.field.to_s).matches_any(like_mapped_values).to_sql
=> "(status ILIKE 'Foo' OR status ILIKE 'Bar')"
If the array this causes an exception which my expectation is after running .where("status in ?", []) against the model is like this as that turns [] to null:
"(status ILIKE NULL)"
Calling
::Arel::Nodes::SqlLiteral.new(column.field.to_s).matches_any([]).to_sql
generates the error
Unsupported argument type: NilClass. Construct an Arel node instead.
What is the correct method to handle an empty array in matches_any? I could also do this without arel I suppose as well. Status is just a column on a model
EDIT: Further background, this datatable on the UI side has a search box and this field differs in text between what's displayed and what's actually in the database. This custom filter takes the distinct values from the database and maps the view text to the meaningful database text. It "likes" the viewed text, takes the match from the database side, and needs to apply filters as it goes. So, a partial match on the view text is matched to the actual database match. This means there could be no database matches and match_any? pukes on that.
Querying for "status matches an empty array" is ambiguous, depending on your use case it may mean several things, you may want to:
match all possible statuses
match no rows at all
match rows that have a NULL status
match all possible statuses except NULL
You should therefore check if statuses is empty and return a query that matches what you would like to do. Either:
not adding a filter (or 1=1)
filter out everything (or 1=0)
status IS NULL
status IS NOT NULL

Active Record - Chain Queries with OR

Rails: 4.1.2
Database: PostgreSQL
For one of my queries, I am using methods from both the textacular gem and Active Record. How can I chain some of the following queries with an "OR" instead of an "AND":
people = People.where(status: status_approved).fuzzy_search(first_name: "Test").where("last_name LIKE ?", "Test")
I want to chain the last two scopes (fuzzy_search and the where after it) together with an "OR" instead of an "AND." So I want to retrieve all People who are approved AND (whose first name is similar to "Test" OR whose last name contains "Test"). I've been struggling with this for quite a while, so any help would be greatly appreciated!
I digged into fuzzy_search and saw that it will be translated to something like:
SELECT "people".*, COALESCE(similarity("people"."first_name", 'test'), 0) AS "rankxxx"
FROM "people"
WHERE (("people"."first_name" % 'abc'))
ORDER BY "rankxxx" DESC
That says if you don't care about preserving order, it will just filter the result by WHERE (("people"."first_name" % 'abc'))
Knowing that and now you can simply write the query with similar functionality:
People.where(status: status_approved)
.where('(first_name % :key) OR (last_name LIKE :key)', key: 'Test')
In case you want order, please specify what would you like the order will be after joining 2 conditions.
After a few days, I came up with the solution! Here's what I did:
This is the query I wanted to chain together with an OR:
people = People.where(status: status_approved).fuzzy_search(first_name: "Test").where("last_name LIKE ?", "Test")
As Hoang Phan suggested, when you look in the console, this produces the following SQL:
SELECT "people".*, COALESCE(similarity("people"."first_name", 'test'), 0) AS "rank69146689305952314"
FROM "people"
WHERE "people"."status" = 1 AND (("people"."first_name" % 'Test')) AND (last_name LIKE 'Test') ORDER BY "rank69146689305952314" DESC
I then dug into the textacular gem and found out how the rank is generated. I found it in the textacular.rb file and then crafted the SQL query using it. I also replaced the "AND" that connected the last two conditions with an "OR":
# Generate a random number for the ordering
rank = rand(100000000000000000).to_s
# Create the SQL query
sql_query = "SELECT people.*, COALESCE(similarity(people.first_name, :query), 0)" +
" AS rank#{rank} FROM people" +
" WHERE (people.status = :status AND" +
" ((people.first_name % :query) OR (last_name LIKE :query_like)))" +
" ORDER BY rank#{rank} DESC"
I took out all of quotation marks in the SQL query when referring to tables and fields because it was giving me error messages when I kept them there and even if I used single quotes.
Then, I used the find_by_sql method to retrieve the People object IDs in an array. The symbols (:status, :query, :query_like) are used to protect against SQL injections, so I set their values accordingly:
# Retrieve all the IDs of People who are approved and whose first name and last name match the search query.
# The IDs are sorted in order of most relevant to the search query.
people_ids = People.find_by_sql([sql_query, query: "Test", query_like: "%Test%", status: 1]).map(&:id)
I get the IDs and not the People objects in an array because find_by_sql returns an Array object and not a CollectionProxy object, as would normally be returned, so I cannot use ActiveRecord query methods such as where on this array. Using the IDs, we can execute another query to get a CollectionProxy object. However, there's one problem: If we were to simply run People.where(id: people_ids), the order of the IDs would not be preserved, so all the relevance ranking we did was for nothing.
Fortunately, there's a nice gem called order_as_specified that will allow us to retrieve all People objects in the specific order of the IDs. Although the gem would work, I didn't use it and instead wrote a short line of code to craft conditions that would preserve the order.
order_by = people_ids.map { |id| "people.id='#{id}' DESC" }.join(", ")
If our people_ids array is [1, 12, 3], it would create the following ORDER statement:
"people.id='1' DESC, people.id='12' DESC, people.id='3' DESC"
I learned from this comment that writing an ORDER statement in this way would preserve the order.
Now, all that's left is to retrieve the People objects from ActiveRecord, making sure to specify the order.
people = People.where(id: people_ids).order(order_by)
And that did it! I didn't worry about removing any duplicate IDs because ActiveRecord does that automatically when you run the where command.
I understand that this code is not very portable and would require some changes if any of the people table's columns are modified, but it works perfectly and seems to execute only one query according to the console.

Rails Mongoid model query result returns wrong size/length/count info even when using limit

When querying on a certain model in my rails application, it returns the correct results, excerpt the size, length or count information, even using the limit criteria.
recipes = Recipe
.where(:bitly_url => /some.url/)
.order_by(:date => :asc)
.skip(10)
.limit(100)
recipes.size # => 57179
recipes.count # => 57179
recipes.length # => 57179
I can't understand why this is happening, it keeps showing the total count of the recipes collection, and the correct value should be 100 since I used limit.
count = 0
recipes.each do |recipe|
count += 1
end
# WAT
count # => 100
Can somebody help me?
Thanks!
--
Rails version: 3.2.3
Mongoid version: 2.4.10
MongoDB version: 1.8.4
From the fine manual:
- (Integer) length
Also known as: size
Get's the number of documents matching the query selector.
But .limit doesn't really alter the query selector as it doesn't change what the query matches, .offset and .limit alter what segment of the matches are returned. This doesn't match the behavior of ActiveRecord and the documentation isn't exactly explicit about this subtle point. However, Mongoid's behaviour does match what the MongoDB shell does:
> db.things.find().limit(2).count()
23
My things collection contains 23 documents and you can see that the count ignores the limit.
If you want to know how many results are returned then you could to_a it first:
recipes.to_a.length
As mentioned in one of the comments, in newer Mongoid versions (not sure which ones), you can simply use recipes.count(true) and this will include the limit, without needing to query the result set, as per the API here.
In the current version of mongoid (5.x), count(true) no longer works. Instead, count now accepts an options hash. Among them there's :limit option
criteria.count(limit: 10)
Or, to reuse whatever limit is already set on the criteria
criteria.count(criteria.options.slice(:limit))

Rails ActiveRecord Date Comparison Database Agnostic?

I'm trying to find a database agnostic way of comparing dates with active record queries. I've the following query:
UserRole.where("(effective_end_date - effective_start_date) > ?", 900.seconds)
This works fine on MySQL but produces an error on PG as the sql it generates doesn't contain the 'interval' syntax. From the console:
←[1m←[36mUserRole Load (2.0ms)←[0m ←[1mSELECT "user_roles".* FROM "user_roles" WHERE "user_roles"."effective_end_date" IS NULL AND ((effective_end_d
ate - effective_start_date) > '--- 900
...
')←[0m
ActiveRecord::StatementInvalid: PG::Error: ERROR: invalid input syntax for type interval: "--- 900
When I run this with the to_sql I option I get:
irb(main):001:0> UserRole.where("effective_end_date - effective_start_date) > ?", 900.seconds).to_sql
=> "SELECT \"user_roles\".* FROM \"user_roles\" WHERE \"user_roles\".\"effective_end_date\" IS NULL AND (effective_end_date - effective_start_date) >
'--- 900\n...\n')"
All help appreciated.
If your effective_end_date and effective_start_date columns really are dates then your query is pointless because dates have a minimum resolution of one day and 900s is quite a bit smaller than 86400s (AKA 25*60*60 or 1 day). So I'll assume that your "date" columns are actually datetime (AKA timestamp) columns; if this is true then you might want to rename the columns to avoid confusion during maintenance, effectively_starts_at and effectively_ends_at would probably be good matches for the usual Rails conventions. If this assumption is invalid then you should change your column types or stop using 900s.
Back to the real problem. ActiveRecord converts Ruby values to SQL values using the ActiveRecord::ConnectionAdapters::Quoting#quote method:
def quote(value, column = nil)
# records are quoted as their primary key
return value.quoted_id if value.respond_to?(:quoted_id)
case value
#...
else
"'#{quote_string(YAML.dump(value))}'"
end
end
So if you try to use something as a value for a placeholder and there isn't any specific handling built in for that type, then you get YAML (a bizarre choice of defaults IMO). Also, 900.seconds is an ActiveSupport::Duration object (despite what 900.seconds.class says) and the case value has no branch for ActiveSupport::Duration so 900.seconds will get YAMLified.
The PostgreSQL adapter provides its own quote in ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#quote but that doesn't know about ActiveSupport::Duration either. The MySQL adapter's quote is also ignorant of ActiveSupport::Duration. You could monkey patch some sense into these quote methods. Something like this in an initializer:
class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
# Grab an alias for the standard quote method
alias :std_quote :quote
# Bludgeon some sense into things
def quote(value, column = nil)
return "interval '#{value.to_i} seconds'" if(value.is_a?(ActiveSupport::Duration))
std_quote(value, column)
end
end
With that patch in place, you get intervals that PostgreSQL understands when you use an ActiveSupport::Duration:
> Model.where('a - b > ?', 900.seconds).to_sql
=> "SELECT \"models\".* FROM \"models\" WHERE (a - b > interval '900 seconds')"
> Model.where('a - b > ?', 11.days).to_sql
=> "SELECT \"models\".* FROM \"models\" WHERE (a - b > interval '950400 seconds')"
If you add a similar patch to the MySQL adapter's quote (which is left as an exercise for the reader), then things like:
UserRole.where("(effective_end_date - effective_start_date) > ?", 900.seconds)
will do The Right Thing in both PostgreSQL and MySQL and your code won't have to worry about it.
That said, developing and deploying on different databases is a really bad idea that will make Santa Claus cry and go looking for some coal (possibly laced with arsenic, possibly radioactive) for your stocking. So don't do that.
If on the other hand you're trying to build database-agnostic software, then you're in for some happy fun times! Database portability is largely a myth and database-agnostic software always means writing your own portability layer on top of the ORM and database interfaces that your platform provides. You will have to exhaustively test everything on each database you plan to support, everyone pays lip service to the SQL Standard but no one seems to fully support it and everyone has their own extensions and quirks to worry about. You will end up writing your own portability layer that will consist of a mixture of utility methods and monkey patches.

activerecord sum returns a string?

This seems very strange to me, an active record sum returns a string, not a number
basket_items.sum("price")
This seems to make it work, but i thought i may have missed something, as this seems like very strange behaviour.
basket_items.sum("price").to_i
According to the (rails 2.3.9) API:
The value is returned with the same data type of the column, 0 if there’s no row
Could your price column be a string or text?
https://github.com/rails/rails/pull/7439
There was a reason it returned a string - calling to_d on a Fixnum in Ruby 1.8 would give a NoMethodError. This is no longer the case in Ruby 1.9 so it's probably okay to change.
ActiveRecord sum:
Difference:
1) basket_items.sum("price")
It will also sum non integer also and it will return non integer type.
2) basket_items.sum("price").to_i
This above will convert into integer.
# File activerecord/lib/active_record/relation/calculations.rb, line 92
def sum(*args)
if block_given?
self.to_a.sum(*args) {|*block_args| yield(*block_args)}
else
calculate(:sum, *args)
end
end
Calculates the sum of values on a given column. The value is returned with the same data type of the column, 0 if there’s no row.
http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-sum
Github:
https://github.com/rails/rails/blob/f8f4ac91203506c94d547ee0ef530bd60faf97ed/activerecord/lib/active_record/relation/calculations.rb#L92
Also see, Advanced sum() usage in Rails.

Resources