ruby query with function - ruby-on-rails-3

# search for a particular date (between two other dates)
a = self.where('created_at > ? AND created_at < ?', yes, tom)
# search for a particular location
a = a.each do |loc|
if coorDist(loc.lat, loc.lng, lat, lng) < dist
return loc
end
end
I'm trying to return a hash like the initial value of a here. How do I loop through the hash and collect the entities that meet the condition of this if statement?? The way it is there, it just returns the first result.

a.select! do |loc|
coorDist(loc.lat, loc.lng, lat, long) < dist
end
If you are just returning the hash, use select instead of select!.

Instead of #each and if, use #select.
# search for a particular location
a = a.select do |loc|
coorDist(loc.lat, loc.lng, lat, lng) < dist
end

Related

How to dynamic, handle nested WHERE AND/OR queries using Rails and SQL

I'm currently building a feature that requires me to loop over an hash, and for each key in the hash, dynamically modify an SQL query.
The actual SQL query should look something like this:
select * from space_dates d
inner join space_prices p on p.space_date_id = d.id
where d.space_id = ?
and d.date between ? and ?
and (
(p.price_type = 'monthly' and p.price_cents <> 9360) or
(p.price_type = 'daily' and p.price_cents <> 66198) or
(p.price_type = 'hourly' and p.price_cents <> 66198) # This part should be added in dynamically
)
The last and query is to be added dynamically, as you can see, I basically need only one of the conditions to be true but not all.
query = space.dates
.joins(:price)
.where('date between ? and ?', start_date, end_date)
# We are looping over the rails enum (hash) and getting the key for each key value pair, alongside the index
SpacePrice.price_types.each_with_index do |(price_type, _), index|
amount_cents = space.send("#{price_type}_price").price_cents
query = if index.positive? # It's not the first item so we want to chain it as an 'OR'
query.or(
space.dates
.joins(:price)
.where('space_prices.price_type = ?', price_type)
.where('space_prices.price_cents <> ?', amount_cents)
)
else
query # It's the first item, chain it as an and
.where('space_prices.price_type = ?', price_type)
.where('space_prices.price_cents <> ?', amount_cents)
end
end
The output of this in rails is:
SELECT "space_dates".* FROM "space_dates"
INNER JOIN "space_prices" ON "space_prices"."space_date_id" = "space_dates"."id"
WHERE "space_dates"."space_id" = $1 AND (
(
(date between '2020-06-11' and '2020-06-11') AND
(space_prices.price_type = 'hourly') AND (space_prices.price_cents <> 9360) OR
(space_prices.price_type = 'daily') AND (space_prices.price_cents <> 66198)) OR
(space_prices.price_type = 'monthly') AND (space_prices.price_cents <> 5500)
) LIMIT $2
Which isn't as expected. I need to wrap the last few lines in another set of round brackets in order to produce the same output. I'm not sure how to go about this using ActiveRecord.
It's not possible for me to use find_by_sql since this would be dynamically generated SQL too.
So, I managed to solve this in about an hour using Arel with rails
dt = SpaceDate.arel_table
pt = SpacePrice.arel_table
combined_clauses = SpacePrice.price_types.map do |price_type, _|
amount_cents = space.send("#{price_type}_price").price_cents
pt[:price_type]
.eq(price_type)
.and(pt[:price_cents].not_eq(amount_cents))
end.reduce(&:or)
space.dates
.joins(:price)
.where(dt[:date].between(start_date..end_date).and(combined_clauses))
end
And the SQL output is:
SELECT "space_dates".* FROM "space_dates"
INNER JOIN "space_prices" ON "space_prices"."space_date_id" = "space_dates"."id"
WHERE "space_dates"."space_id" = $1
AND "space_dates"."date" BETWEEN '2020-06-11' AND '2020-06-15'
AND (
("space_prices"."price_type" = 'hourly'
AND "space_prices"."price_cents" != 9360
OR "space_prices"."price_type" = 'daily'
AND "space_prices"."price_cents" != 66198)
OR "space_prices"."price_type" = 'monthly'
AND "space_prices"."price_cents" != 5500
) LIMIT $2
What I ended up doing was:
Creating an array of clauses based on the enum key and the price_cents
Reduced the clauses and joined them using or
Added this to the main query with an and operator and the combined_clauses

How to match row data against an array of values

Is there a good way to check for matches in a SQL column, using an array of data, without having to loop as shown? Assume the url array has 100+ links, the below is just an example.
url = ["www.site1.com", "www.site2.com"]
url.each do |url|
match = db.execute("SELECT 1 FROM ListData WHERE Link=? ", url)
if match[0][0] == 1
flag = true
end
end
Use WHERE IN clause like this:
SELECT 1 FROM ListData WHERE Link IN ('www.site1.com','www.site2.com')

Difference between postgresql time and rails 3 app time

Scenario: I want to get the nearest time in relation to my system time.
I have written the following method inside my model:
def self.nearest_class(day, teacher_id, hour_min)
day_of_week = '%' + day + '%'
if hour_min == "06:00:00"
where('frequency like ? AND teacher_id = ? AND (start_time >= ?)', day_of_week, teacher_id, hour_min).order('start_time ASC')
else
where('frequency like ? AND teacher_id = ? AND (start_time >= localtime(0))', day_of_week, teacher_id).order('start_time ASC')
end
end
When I tested the above method in rails console using Lecture.nearest_class('Monday', 1, 'localtime(0)'), the following query was returned together with some data:
SELECT "lectures".* FROM "lectures" WHERE (frequency like '%Monday%' AND teacher_id = 1 AND (start_time >= localtime(0))) ORDER BY start_time ASC
But I am expecting no record because my system time is greater than any start_time recorded in the database. I have used the query from the console to pgadmin3 to test if the results are same. However, pgadmin3 showed no results, as expected.
Are there differences in postgresql time and rails app time? How can I be able to check these differences? I tried Time.now in the console and it is the same as SELECT LOCALTIME(0) from pgadmin3. What should be the proper way to get the records for nearest system time?
To further set the context, here's my controller method:
def showlectures
#teacher = Teacher.login(params[:id]).first
#lecture_by_course = nil
if #teacher
WEEKDAY.each do |day|
hour_min = "06:00:00"
if day == Time.now.strftime("%A") # check if day of week is same e.g. 'Monday'
hour_min = "localtime(0)" # system time with format: HH:MM:SS
end
#lecture_by_course = Lecture.nearest_class(day, #teacher.id, hour_min).first
if #lecture_by_course
break
end
end
end
render json: #lecture_by_course, include: { course: { only: [:name, :id] }}
end
I solved this by just passing the server Time.now instead of having a SQL check. { :| d'oh }

How to make this SQL query more efficient and how to do more

I have a sqlite3 database in which I have corrupt data. I qualify "corrupt" with the following characteristics:
Data in name, telephone, latitude, longitude columns is corrupt if: The value is NULL or "" or length < 2
Data in address column is corrupt if The value is NULL or "" or number of words < 2 and length of word is <2
To test this I wrote the following script in Ruby:
require 'sqlite3'
db = SQLite3::Database.new('development.sqlite3')
db.results_as_hash = true;
#Checks for empty strings in name, address, telephone, latitude, longitude
#Also checks length of strings is valid
rows = db.execute(" SELECT * FROM listings WHERE LENGTH('telephone') < 2 OR LENGTH('fax') < 2 OR LENGTH('address') < 2 OR LENGTH('city') < 2 OR LENGTH('province') < 2 OR LENGTH('postal_code') < 2 OR LENGTH('latitude') < 2 OR LENGTH('longitude') < 2
OR name = '' OR address = '' OR telephone = '' OR latitude = '' OR longitude = '' ")
rows.each do |row|
=begin
db.execute("INSERT INTO missing (id, name, telephone, fax, suite, address, city, province, postal_code, latitude, longitude, url) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", row['id'], row['name'], row['telephone'], row['fax'], row['suite'], row['address'], row['city'], row['province'],
row['postal_code'], row['latitude'], row['longitude'], row['url'] )
=end
id_num = row['id']
puts "Id = #{id_num}"
corrupt_name = row['name']
puts "name = #{corrupt_name}"
corrupt_address = row['address']
puts "address = #{corrupt_address}"
corrupt_tel = row['telephone']
puts "tel = #{corrupt_tel}"
corrupt_lat = row['latitude']
puts "lat = #{corrupt_lat}"
corrupt_long = row['longitude']
puts "lat = #{corrupt_long}"
puts '===end===='
end
#After inserting the records into the new table delete them from the old table
=begin
db.execute(" DELETE * FROM listings WHERE LENGTH('telephone') < 2 OR LENGTH('fax') < 2 OR LENGTH('address') < 2 OR
LENGTH('city') < 2 OR LENGTH('province') < 2 OR LENGTH('postal_code') < 2 OR LENGTH('latitude') < 2 OR LENGTH('longitude') < 2
OR name = '' OR address = '' OR telephone = '' OR latitude = '' OR longitude = '' ")
=end
This works but Im new to Ruby and DB programming. So I would welcome any suggestions to make this query better.
The ultimate goal I have is to run a script on my database which tests the validity of data in it and if there are some data that are not valid they are copied to a different table and deleted from the 1st table.
Also, I would like to add to this query a test to check for duplicate entries.
I qualify an entry as duplicate if more than 1 rows share the same name and the same address and the same telephone and the same latitude and the same longitude
I came up with this query but Im not sure if its the most optimal:
SELECT *
FROM listings L1, listings L2
WHERE L1.name = L2.name
AND L1.telephone = L2.telephone
AND L1.address = L2.address
AND L1.latitude = L2.latitude
AND L1.longitude = L2.longitude
Any suggestions, links, help would be greatly appreciated
Your first query doesn't have any significant performance problem. It will run with a seq scan evaluating your "is corrupt" predicate. The check for == '' is redundant with length(foo) < 2 as length('') is < 2. You have a bug where you quoted the field names in your length() calls, so you'll be evaluating the length of the literal field name instead of the value of the field. You have also failed to test for NULL which is a value distinct from ''. You can use the coalesce function to convert NULL to '' and capture NULLS with the length check. You also don't seem to have addressed the special word based rule for address. This later is trouble unless you extend sqlite with a regexp function. I suggest approximating it with LIKE or GLOB.
Try this alternative:
SELECT * FROM listings
WHERE LENGTH(coalesce(telephone,'')) < 2
OR LENGTH(coalesce(fax,'')) < 2
OR LENGTH(coalesce(city,'')) < 2
OR LENGTH(coalesce(province,'')) < 2
OR LENGTH(coalesce(postal_code,'')) < 2
OR LENGTH(coalesce(latitude,'')) < 2
OR LENGTH(coalesce(longitude,'')) < 2
OR LENGTH(coalesce(name,'')) < 2
OR LENGTH(coalesce(address,'')) < 5
OR trim(address) not like '%__ __%'
You find duplicates query doesn't work, since there's always at least one record to match when self joining on equality. You need to exclude the record under test on one side of the join. Typically this can be done by excluding on primary key. You haven't mentioned if the table has a primary key, but IIRC sqllite can give you a proxy for one with ROWID. Something like this:
SELECT L1.*
FROM listings L1
where exists (
select null
from listings L2
where L1.ROWID <> L2.ROWID
AND L1.name = L2.name
AND L1.telephone = L2.telephone
AND L1.address = L2.address
AND L1.latitude = L2.latitude
AND L1.longitude = L2.longitude
)
BTW, while you stressed efficiency in your question, it's important to make your code correct before you worry about efficiency.
I think you're doing overprocessing. As the length of the string '' is 0 then it matches the condicion length('') < 2. So, you don't need to check if a field is equal to '' as it has already been filtered by the conditions on the length function.
However, I don't see how you're checking for null values. I'd replace all the aField = '' with aField is null.

Rails 3 query in multiple date ranges

Suppose we have some date ranges, for example:
ranges = [
[(12.months.ago)..(8.months.ago)],
[(7.months.ago)..(6.months.ago)],
[(5.months.ago)..(4.months.ago)],
[(3.months.ago)..(2.months.ago)],
[(1.month.ago)..(15.days.ago)]
]
and a Post model with :created_at attribute.
I want to find posts where created_at value is in this range, so the goal is to create a query like:
SELECT * FROM posts WHERE created_at
BETWEEN '2011-04-06' AND '2011-08-06' OR
BETWEEN '2011-09-06' AND '2011-10-06' OR
BETWEEN '2011-11-06' AND '2011-12-06' OR
BETWEEN '2012-01-06' AND '2012-02-06' OR
BETWEEN '2012-02-06' AND '2012-03-23';
If you have only one range like this:
range = (12.months.ago)..(8.months.ago)
we can do this query:
Post.where(:created_at => range)
and query should be:
SELECT * FROM posts WHERE created_at
BETWEEN '2011-04-06' AND '2011-08-06';
Is there a way to make this query using a notation like this Post.where(:created_at => range)?
And what is the correct way to build this query?
Thank you
It gets a little aggressive with paren, but prepare to dive down the arel rabbit hole
ranges = [
((12.months.ago)..(8.months.ago)),
((7.months.ago)..(6.months.ago)),
((5.months.ago)..(4.months.ago)),
((3.months.ago)..(2.months.ago)),
((1.month.ago)..(15.days.ago))
]
table = Post.arel_table
query = ranges.inject(table) do |sum, range|
condition = table[:created_at].in(range)
sum.class == Arel::Table ? condition : sum.or(condition)
end
Then, query.to_sql should equal
(((("sessions"."created_at" BETWEEN '2011-06-05 12:23:32.442238' AND '2011-10-05 12:23:32.442575' OR "sessions"."created_at" BETWEEN '2011-11-05 12:23:32.442772' AND '2011-12-05 12:23:32.442926') OR "sessions"."created_at" BETWEEN '2012-01-05 12:23:32.443112' AND '2012-02-05 12:23:32.443266') OR "sessions"."created_at" BETWEEN '2012-03-05 12:23:32.443449' AND '2012-04-05 12:23:32.443598') OR "sessions"."created_at" BETWEEN '2012-05-05 12:23:32.443783' AND '2012-05-21 12:23:32.443938')
And you should be able to just do Post.where(query)
EDIT
You could also do something like:
range_conditions = ranges.map{|r| table[:created_at].in(r)}
query = range_conditions.inject(range_conditions.shift, &:or)
to keep it a little more terse
I suggest you try the pure string form:
# e.g. querying those in (12.months.ago .. 8.months.ago) or in (7.months.ago .. 6.months.ago)
Post.where("(created_at <= #{12.months.ago} AND created_at >= #{8.months.ago} ) OR " +
"(created_at <= #{7.months.ago} AND created_at >= #{6.months.ago} )" )
In your case, I would suggest to use mysql IN clause
Model.where('created_at IN (?)', ranges)