The real tactical question I am facing is all categories are set as 'default' therefore if I make options[:category] = 'default' it only adds the points that have no category. Therefore if i add points to cateogry 'arin' it will not be counted to the 'default' total. So I tried to grab all tables if NOT NULL or by category but it keeps grabbing the same amount for 'arin'.
default: 20
arin: 20
Should be total of 40 if category not supplied or at 'default', if params category 'arin' then it should be 20.
Can someone help me understand the concept behind the correct SQL to get the results I am looking for?
New to rails and SQL.
def self.top_scored(options = {})
options[:table_name] ||= :users
options[:since_date] ||= 4.months.ago
options[:end_date] ||= 1.month.from_now
options[:category] ||= nil
options[:limit] ||= 10
alias_id_column = "#{options[:table_name].to_s.singularize}_id"
if options[:table_name] == :sashes
sash_id_column = "#{options[:table_name]}.id"
else
sash_id_column = "#{options[:table_name]}.sash_id"
end
# MeritableModel - Sash -< Scores -< ScorePoints
sql_query = <<SQL
SELECT
#{options[:table_name]}.id AS #{alias_id_column},
SUM(num_points) as sum_points
FROM #{options[:table_name]}
LEFT JOIN merit_scores ON merit_scores.sash_id = #{sash_id_column}
LEFT JOIN merit_score_points ON merit_score_points.score_id = merit_scores.id
WHERE merit_score_points.created_at > '#{options[:since_date]}' AND merit_score_points.created_at < '#{options[:end_date]}' AND (merit_scores.category IS NOT NULL OR merit_scores.category = '#{options[:category]}')
GROUP BY #{options[:table_name]}.id, merit_scores.sash_id
ORDER BY sum_points DESC
LIMIT #{options[:limit]}
SQL
results = ActiveRecord::Base.connection.execute(sql_query)
results.map do |h|
h.keep_if { |k, v| (k == alias_id_column) || (k == 'sum_points') }
end
results
end
end
Seems no one answered and only down voted. Here is to anyone that questions this in the future. I figured out you can split sql statements and use an if statement in rails around the SQL.
sql_query = "SELECT
#{options[:table_name]}.id AS #{alias_id_column},
SUM(num_points) as sum_points
FROM #{options[:table_name]}
LEFT JOIN merit_scores ON merit_scores.sash_id = #{sash_id_column}
LEFT JOIN merit_score_points ON merit_score_points.score_id = merit_scores.id
WHERE merit_score_points.created_at > '#{options[:since_date]}' AND merit_score_points.created_at < '#{options[:end_date]}' "
if(options[:category] != nil)
sql_query += "AND merit_scores.category = \"#{options[:category]}\" "
end
sql_query += "GROUP BY #{options[:table_name]}.id, merit_scores.sash_id
ORDER BY sum_points DESC
LIMIT #{options[:limit]} "
results = ActiveRecord::Base.connection.execute(sql_query)
This is the query I'm trying to run. Screenshot.
#colleges = College.all
#colleges = #colleges.where(category: #university_type) unless #university_type.blank? or #university_type.all? &:blank?
#colleges = #colleges.where("us_news_ranking <= ?", #rank_low) unless #rank_low.blank?
#colleges = #colleges.where("(sat_math_25+sat_math_75+sat_reading_25+sat_reading_75)/2 >= ?", #sat_low) unless #sat_low.blank?
#colleges = #colleges.where("(sat_math_25+sat_math_75+sat_reading_25+sat_reading_75)/2 <= ?", #sat_high) unless #sat_high.blank?
#colleges = #colleges.where("(act_composite_25+act_composite_75)/2 >= ?", #act_low) unless #act_low.blank?
#colleges = #colleges.where("(act_composite_25+act_composite_75)/2 <= ?", #act_high) unless #act_high.blank?
if !#cost_low.blank?
if #in_state.blank?
#colleges = #colleges.where("out_of_state_tuition+room_and_board >= ?", #cost_low)
#colleges = #colleges.where("out_of_state_tuition+room_and_board <= ?", #cost_high)
else
#colleges = #colleges.where(state: #in_state).where("in_state_tuition+room_and_board >= ? AND in_state_tuition+room_and_board <= ?", #cost_low, #cost_high)
#colleges = #colleges.where("state != ? AND out_of_state_tuition+room_and_board >= ? AND out_of_state_tuition+room_and_board <= ?", #in_state, #cost_low, #cost_high)
end
end
I've tested it, and the problem is with the else statement. If I comment out one of the lines in the else statement, the other one behaves as you'd expect. However, when I leave them both uncommented, it never returns any colleges.
I don't know what the problem is, but I figure that it has something to do with me querying for state = A in one line, and state = B in the other. Is that the problem? Why? If not, what is the problem?
College.rb
def self.get_college(university_type, rank_low, sat_low, sat_high, act_low, act_high, in_state, cost_low, cost_high)
colleges = College.all
colleges = colleges.where(category: university_type) unless university_type.blank? or university_type.all? &:blank?
colleges = colleges.where("us_news_ranking <= ?", rank_low) unless rank_low.blank?
colleges = colleges.where("(sat_math_25+sat_math_75+sat_reading_25+sat_reading_75)/2 >= ?", sat_low) unless sat_low.blank?
colleges = colleges.where("(sat_math_25+sat_math_75+sat_reading_25+sat_reading_75)/2 <= ?", sat_high) unless sat_high.blank?
colleges = colleges.where("(act_composite_25+act_composite_75)/2 >= ?", act_low) unless act_low.blank?
colleges = colleges.where("(act_composite_25+act_composite_75)/2 <= ?", act_high) unless act_high.blank?
select_fields = sanitize_sql_array( [ <<-ENDSQL, in_state ] )
*,
IF( colleges.state = ?,
in_state_tuition,
out_of_state_tuition
) AS user_tuition,
user_tuition + room_and_board AS total_cost
ENDSQL
colleges = colleges.select(select_fields).where("total_cost BETWEEN ? AND ?", cost_low, cost_high)
return colleges
end
tools_controller.rb
#colleges = College.get_college(#university_type, #rank_low, #sat_low, #sat_high, #act_low, #act_high, #in_state, #cost_low, #cost_high) if #searched
Update following #AdamZerner's comment:
Say the user selects a price range and a state. I want to return colleges in the price range, and to do that I see if tuition + room_and_board are in that range. But I want to calculate tuition using in_state_tuition if the user is in-state, and out_of_state_tuition if the user is out-of-state.
Ah, good question. As I mentioned, you need to know SQL, not just Rails. A nice, clean query for getting what you want looks like this:
SELECT *,
IF( colleges.state = #user_state,
in_state_tuition,
out_of_state_tuition
) AS user_tuition,
user_tuition + room_and_board AS total_cost
FROM colleges
WHERE total_cost BETWEEN #cost_low AND #cost_high
We use an IF() expression to decide whether to use in_state_tuition or out_of_state_tuition and give it a name, user_tuition. Then we take that and add it to room_and_board to get total_cost. Then in our WHERE we use BETWEEN, because it's much more concise and readable than A <= B AND B <= C.
Once we know what our SQL looks like, it's easy to translate into ActiveRecord methods.
# This will give you the `SELECT` part of the query above
select_fields = sanitize_sql_array( [ <<-ENDSQL, #in_state ] )
*,
IF( colleges.state = ?,
in_state_tuition,
out_of_state_tuition
) AS user_tuition,
user_tuition + room_and_board AS total_cost
ENDSQL
#colleges = College.select(select_fields)
.where("total_cost BETWEEN ? AND ?", #cost_low, #cost_high)
Note that sanitize_sql_array is a protected method of ActiveRecord::Base, so it will only work inside your model. But querying logic like this belongs in the model anyway. Your use case is perfect for Rails scopes:
class College < ActiveRecord::Base
scope :ranked_at_least, ->(rank=nil) {
return self if rank.nil?
where("us_news_ranking <= ?", rank)
}
scope :in_state_with_cost_between, ->(state_name, cost_low, cost_high) {
select_fields = sanitize_sql_array( [ <<-ENDSQL, state_name ] )
*,
IF( colleges.state = ?,
in_state_tuition,
out_of_state_tuition
) AS user_tuition,
user_tuition + room_and_board AS total_cost
ENDSQL
select(select_fields)
.where("total_cost BETWEEN ? AND ?", cost_low, cost_high)
}
scope :with_sat_composite_between, ->(score_low, score_high) {
# ...
}
# ...and so on...
end
This would allow you to make nice, clean queries like this:
College.in_state_with_cost_between("New York", 10_000, 50_000).
ranked_at_least(20).
with_sat_composite_between(1_200, 1_500)
...which seems a lot nicer to me.
Update 2 - Simpler and works with SQLite
I didn't realize that SQLite doesn't have IF(). I think the sanitize_sql_array bit overcomplicated things a bit, too, so let's simplify. The below SQLite query is equivalent to the one above (which works in MySQL and others):
SELECT colleges.*,
CASE WHEN colleges.state = 'New York'
THEN colleges.in_state_tuition
ELSE colleges.out_of_state_tuition
END AS user_tuition,
user_tuition + room_and_board AS total_cost
FROM colleges
WHERE total_cost BETWEEN 15000 AND 60000
The only difference is that we used CASE WHEN x THEN y ELSE z END instead of IF(x, y, z).
Now let's turn it into an ActiveRecord query:
# Always sanitize values you get from the user!
safe_state_name = ActiveRecord::Base.sanitize(#in_state)
select_sql = <<-ENDSQL
colleges.*,
CASE WHEN colleges.state = #{safe_state_name}
THEN colleges.in_state_tuition
ELSE colleges.out_of_state_tuition
END AS user_tuition,
user_tuition + room_and_board AS total_cost
ENDSQL
College.select(select_sql).
where("total_cost BETWEEN ? AND ?", #cost_low, #cost_high)
When we use the "?" replacement in where() Rails automatically sanitizes #cost_low and #cost_high for us, but we have to do it manually for the select(). Don't skip this step, though--it's very important!
We could also have written the query this way:
where_sql = <<-ENDSQL
( CASE WHEN colleges.state = ?
THEN colleges.in_state_tuition
ELSE colleges.out_of_state_tuition
END
) BETWEEN ? AND ?
ENDSQL
College.where(where_sql, #in_state, #cost_low, #cost_high)
...but I think using select() makes for cleaner queries, and also let you use a calculated value (e.g. user_tuition, total_cost) multiple times.
Scopes are a core part of Rails and learning to use them will help you write maintainable code. If you don't know scopes, you don't know rails. They're also really easy. In this case we could write a scope like this:
class College < ActiveRecord::Base
scope :for_state_with_cost_between, ->(state_name, cost_low, cost_high) {
safe_state_name = ActiveRecord::Base.sanitize(state_name)
select_sql = <<-ENDSQL
colleges.*,
CASE WHEN colleges.state = #{safe_state_name}
THEN colleges.in_state_tuition
ELSE colleges.out_of_state_tuition
END AS user_tuition,
user_tuition + room_and_board AS total_cost
ENDSQL
select(select_sql).
where("total_cost BETWEEN ? AND ?", cost_low, cost_high)
}
# ...
This is basically equivalent to defining a class method like this:
class College < ActiveRecord::Base
def self.for_state_with_cost_between(state_name, cost_low, cost_high)
safe_state_name = ActiveRecord::Base.sanitize(state_name)
select_sql = # ...
self.select(select_sql).where("total_cost BETWEEN ? AND ?", cost_low, cost_high)
end
# ...
In both cases you would use it like this:
College.for_state_with_cost_between("New York", 10_000, 50_000)
Using scopes your code could be written to be much cleaner and readable with less room for bugs. I didn't want to paste the whole thing here, but take a look at this gist (untested, of course).
Original answer
Let's break it down. First you do this:
#colleges = College.all
# ...let's pretend you didn't do anything here...
#colleges = #colleges.where( state: #in_state )
.where( "in_state_tuition + room_and_board >= ? AND
in_state_tuition + room_and_board <= ?",
#cost_low, #cost_high )
This creates a ActiveRecord::Relation and assigns it to #colleges. If you called #colleges.all now, it would generate and execute SQL like this (more or less):
SELECT * FROM colleges
WHERE state = #in_state AND
in_state_tuition + room_and_board >= #cost_low AND
in_state_tuition + room_and_board <= #cost_high
Next you do this:
#colleges = #colleges.where( "state != ? AND
out_of_state_tuition + room_and_board >= ? AND
out_of_state_tuition+room_and_board <= ?",
#in_state, #cost_low, #cost_high )
This takes the ActiveRecord::Relation object you created above and adds more WHERE conditions on to it. If you did #colleges.all now, it would generate and execute SQL like this:
SELECT * FROM colleges
WHERE ( state = #in_state AND
in_state_tuition + room_and_board >= #cost_low AND
in_state_tuition + room_and_board <= #cost_high
) AND
( state != #in_state AND
out_of_state_tuition + room_and_board >= #cost_low AND
out_of_state_tuition + room_and_board <= #cost_high
)
This makes it pretty obvious what the problem is. You have state = #in_state and state != #in_state in the same query. A state can't be "New York" and not "New York" at the same time, so your result is empty.
ActiveRecord gives you some nice abstractions and convenience methods for doing database queries, but in the end it's still very important to know what kind of SQL it's generating and what it means.
Actually i have this search query from MrJoshi here is the associated question:
Search query for (name or forename) and (name forname) and (forname name)
def self.search(query)
return where('FALSE') if query.blank?
conditions = []
search_columns = [ :forname, :name ]
query.split(' ').each do |word|
search_columns.each do |column|
conditions << " lower(#{column}) LIKE lower(#{sanitize("%#{word}%")}) "
end
end
conditions = conditions.join('OR')
self.where(conditions)
end
The problem with this search query is that it returns way to much records. For example if somebody is searching for John Smith this search query returns all records wih the forename John and all records with the name Smith although there is only one person that exactly matches the search query means name is Smith and forename is John
So i changed the code a little bit:
def self.search(query)
return where('FALSE') if query.blank?
conditions = []
query2 = query.split(' ')
if query2.length == 2
conditions << " lower(:forname) AND lower(:name) LIKE ?', lower(#{sanitize("%#{query2.first}%")}) , lower(#{sanitize("%#{query2.last}%")})"
conditions << " lower(:forname) AND lower(:name) LIKE ?', lower(#{sanitize("%#{query2.last}%")}) , lower(#{sanitize("%#{query2.first}%")})"
else
search_columns = [ :forname, :name ]
query2.each do |word|
search_columns.each do |column|
conditions << " lower(#{column}) LIKE lower(#{sanitize("%#{word}%")}) "
end
end
end
conditions = conditions.join('OR')
self.where(conditions)
end
But now i get this error:
SQLite3::SQLException: near "', lower('": syntax error: SELECT "patients".* FROM "patients" WHERE ( lower(:forname) AND lower(:name) LIKE ?', lower('%John%') , lower('%Smith%')OR lower(:forname) AND lower(:name) LIKE ?', lower('%Smith%') , lower('%John%')) LIMIT 12 OFFSET 0
What did i wrong? Thanks!
In my view I send an ajax request to get the device_ports of a particular device.
Previously I used
def get_device_ports
if params[:id] != ''
#device_ports = Device.find(params[:id]).device_ports.all(:order => 'id ASC')
output = '<option value="">Select Device Port...</option>'
#device_ports.each do |device_port|
output = output + '<option value="' + device_port.id.to_s + '">' + device_port.name + '</option>'
end
render :text => output
else
render :text => '0'
end
end
Which worked one but now having changed my query I get an error undefined method 'name' for [268, "test-1"]:Array with 268 and test-1 being the id and name of the first row of results.
This is my updated code:
def get_device_ports
if params[:id] != '' and params[:device_id] != ''
# #device_ports = Device.find(params[:id]).device_ports.all(:order => 'id ASC')
device_id = params[:device_id]
# Need a list of ports that aren't in use or are multiuse
#device_ports = ActiveRecord::Base.connection.execute('SELECT DISTINCT d.id, d.name FROM device_ports d LEFT OUTER JOIN circuits c ON c.physical_port_id = d.id WHERE (c.physical_port_id IS NULL AND d.device_id = ' + device_id + ') OR (d.multiuse = 1 AND d.device_id = ' + device_id + ') ORDER BY d.id ')
output = '<option value="">Select Device Port...</option>'
#device_ports.each do |device_port|
output = output + '<option value="' + device_port.id.to_s + '">' + device_port.name + '</option>'
end
render :text => output
else
render :text => '0'
end
end
I'm just not sure why I'm getting the error, I imagine it's something trivial but due to the amount of different NoMethodError questions it's hard to find an answer.
You are having this problem because you aren't using ActiveRecord as an ORM to wrap the object, but rather executing a query and working on the resulting series of arrays. I would recommend changing your query like so:
#device_ports = Device.find(device_id).device_ports.includes(:circuits).
where('device_ports.multiuse = 1 OR circuits.id IS NULL').
order('device_ports.id').distinct
If you absolutely want to avoid ActiveRecord, then don't use id and name, but rather treat each record as an array:
output << %Q{<option value="#{device_port.first}">#{device_port.last}</option>}
UPDATE
I just noticed that you're using RoR-2. Although more painful, you can still use an ActiveRecord query like so:
#device_ports = DevicePort.all(
:joins => "LEFT JOIN circuits c ON device_ports.id = c.devic_port_id",
:conditions => ['device_ports.device_id = ? AND (device_ports.multiuse = 1 OR c.id IS NULL)', device_id],
:order => 'device_ports.id',
:select => 'DISTINCT device_ports.id, device_ports.name')
I have two models (Folder and Document) which I need to show in a single view together. However, to reduce the number of queries sent I am collecting the Documents only if the folders are less than 12 (my :per_page). While this is working fine, I am stuck in a particular case,
When my total documents are less than 12 and folders are less than 12 but together are more than 12, the pagination fails.
Below is the code to calculate which page to be shown where f_page returns the page for the Folder pagination and d_page returns the page number for the document collection.
def f_page(page_cnt, size)
page_cnt.present? and size.nonzero? ? page_cnt.to_i <= (size/12 + (size%12==0 ? 0 : 1)) ? page_cnt.to_i : (size / 12 ) + (size%12==0 ? 0 : 1) : 1
end
def d_page(page_cnt, fc, dc)
page_cnt = page_cnt.present? ? page_cnt : 1
puts page_cnt
dpg = 1
if (fc/12+1 == page_cnt.to_i)
dpg = 1
elsif ((fc/12+1) < page_cnt.to_i)
if (fc < 12)
unless (dc <= 12)
dpg = page_cnt
else
dpg = 1
end
else
(fc/12 == 0) ? (dpg = page_cnt.to_i - (fc/12+1)) : (dpg = page_cnt.to_i - (fc/12))
end
end
puts "dpg = #{dpg}"
return dpg
end
Both are together collected and paginated which is shown in the view.
f = Folder.action_folder_collection(#action, current_user).paginate(:page => params[:page], :per_page => 12)
if (f.count < 12)
d = Document.action_document_collection(#action, current_user).paginate(:page => d_page(params[:page], total_folders, total_documents), :per_page => per_page-f.count)
end
collection << f
collection << d
#collection = collection.flatten.paginate(:page => 1,:per_page => 12,:total_entries => total)
How do I solve it?
I have just solved the similar problem. My paginate_catalog_children helper receives either an AR collection or an array of collections as a parameter and returns WillPaginate::Collection object containing elements from all collections.
def paginate_catalog_children catalog_children, page
per_page = 20
if catalog_children.is_a? ActiveRecord::Relation
catalog_children.paginate(:per_page => per_page, :page => page)
else
# paginating array of collections
WillPaginate::Collection.create(page, per_page) do |pager|
catalog_children_counts = catalog_children.map(&:count)
result = []
offset = pager.offset
items_left = pager.per_page
catalog_children.each_with_index do |collection, index|
break if items_left == 0
if catalog_children_counts[index] <= offset
# skip this collection
offset -= catalog_children_counts[index]
else
collection_items = collection.limit(items_left).offset(offset)
result += collection_items
items_left -= collection_items.size
offset = 0
end
end
pager.replace(result)
pager.total_entries = catalog_children_counts.sum
result
end
end
end