Rails: ActiveRecord - Custom SQL - mysql

What is the best way, using ActiveRecord, to execute the following SQL:
SELECT parent.*
FROM sections AS node, sections AS parent
WHERE node.left BETWEEN parent.left AND parent.right
ORDER BY parent.left DESC
LIMIT 1
I know is possible to use .limit(), .where() and .order() but how do you deal with 'from'?
Or is it better just to execute the whole lot as a single statement?
Thanks for any help.

There's nothing wrong with using SQL in your application so long as you can verify it's working correctly and isn't exposing you to injection attacks. Since this statement is executed as-is you're okay in that regard.
ActiveRecord::Base.connection provides a method for executing arbitrary queries and retrieving the results in a variety of formats. select_all or select_rows may be what you're looking for.
The AREL query generator is not always as clever as we'd like, so when in doubt go with the simplest form of expression. In your case it seems to be that chunk of SQL.
To re-write it using AREL you'd have to use the join method to link it back on itself.

Parent.from('sections AS node, sections AS parent')
.where('node.left BETWEEN parent.left AND parent.right')
.order('parent.left DESC')
.limit(1)
And you get the benefit of chaining.

Related

(Spring JPA) Method name query

First of all, sorry for my poor english.
I want to change the following query to 'findBy~' method, but i don't know how to.
#Query(value = "SELECT t FROM Table t WHERE (b.num1 <= :variable or b.num1 IS NULL) AND (b.num2 >= :variable or b.num2 IS NULL)")
Or, is it impossible to get the result by using 'findby~' method name?
I would appreciate if anyone could reply.
Spring Data JPA does have support for all the conditions in your query and nesting of conditions. I'd argue that your query name will become unnecesarelly verbose. It would end up as
Table findByNum1LessThanEqualOrNum1IsNullAndNum2GreaterThanEqualOrNum2IsNull(Integer var0, Integer var1);
This should return the appropiate query, but you'd need to send the variable twice, once for each equals.
With #Query you have the freedom to call your query as you'd like and reuse the same variable.
Now, you CAN fix the downsides of using named methods by using a default method like
default Table myQuery (Integer var) {
return findByNum1LessThanEqualOrNum1IsNullAndNum2GreaterThanEqualOrNum2IsNull(var, var);
}
So you call this instead of the actual query, but then again, it would be much cleaner to use #Query with a proper, descriptive or even self-documenting name if you don't comment your code (you should comment your code). In any case, I suggest you use method names for simple queries and use #Query for anything more complex.
Please, refer to the following links for further reading:
Spring JPA Query Creation
Spring JPA Query Keyword Repository
LeafyJava article on Query Precedence Tricks, which also provides and example of how to change your query logic in case the conditions aren't arranged as you want.
This SO question also provides a bit of insight.

Knex.js Select Average and Round

I am switching an application from PHP/MYSQL to Express and am using knex to connect to the MYSQL database. In one of my queries I use a statement like such (I have shortened it for brevity.)
SELECT ROUND(AVG(Q1),2) AS Q1 FROM reviews WHERE id=? AND active='1'
I am able to use ROUND if I use knex.raw but I am wondering if there is a way to write this using query builder. Using query builder makes dealing with the output on the view side so much easier than trying to navigate the objects returned from the raw query.
Here is what I have so far in knex.
let id = req.params.id;
knex('reviews')
//Can you wrap a ROUND around the average? Or do a Round at all?
.avg('Q1 as Q1')
.where('id', '=', id)
Thanks so much!
You can use raw inside select. In this case:
knex('reviews')
.select(knex.raw('ROUND(AVG(Q1),2) AS Q1'))
Check the docs here for more examples and good practices when dealing with raw statements.

MySQLi PHP using OR and AND

Sorry about the title, I wasn't sure how to word it
I'm wanting to make a instant messaging system with PHP (I've done ajax for it) but I'm not sure how to get the query, I'm wanting something like this:
"SELECT * FROM messages WHERE user='$to' AND sender='$username' OR user='$username' AND sender='$to'"
Does anyone know if this is possible? Or a mysqli_fetch_array for two invididual queries on the same variable.
You can use parenthesis to use multiple operations to work as single operation in query. This is the typical approach anyway, and very useful for using multiple AND, OR operators in a query.
For you case, query should be like
"SELECT * FROM messages WHERE ( user='$to' AND sender='$username' ) OR ( user='$username' AND sender='$to' )"
Notice that tho we used 4 conditions, but with parenthesis we shrieked it into 2 separate conditions and ultimately one OR operation in the query.
Some good reading about this stuff at this article in case you want to dig it more

Creating an OR statement using existing conditions hash

I am working on a problem where I need to add an OR clause to a set of existing conditions. The current conditions are built in a hash in a method and at the end, they are used in the where clause. Here is a simplified example:
...
conds.merge!({:users => {:archived => false}})
Model.where(conds)
I am trying to add an OR clause to the current set of conditions so it would be something like '(conditions) OR new_condition'. I'd like to add the OR statement without converting each addition to the conds hash into a string. That would be my last option. I was hoping someone has done something like this before (without using Arel). I seem to recall in Rails 2 there was a way to parse a conditions hash using a method from the model (something like Model.some_method(conds) would produce the where clause string. Maybe that would be a good option to just add the OR clause on to that string. Any ideas are appreciated. Thank you for your help!
I found a way to do what I needed. Instead of changing all of the conditions that I am building, I am parsing the conditions to SQL using sanitize_sql_for_conditions. This is a private method in ActiveRecord, so I had to put a method on the model to allow me to access it. Here is my model method:
def self.convert_conditions_hash_to_sql(conditions)
self.sanitize_sql_for_conditions(conditions)
end
So, once I convert my conditions to text, I can add my OR clause (along with the appropriate parentheses) to the end of the original conditions. So, it would go something like this:
Model.where('(?) OR (model.type = ? AND model.id IN(?))', Model.convert_conditions_hash_to_sql(conds), model_type, model_id_array)

Why is Rails is adding `OR 1=0` to queries using the where clause hash syntax with a range?

The project that I'm working on is using MySQL on RDS (mysql2 gem specifically).
When I use a hash of conditions including a range in a where statement I'm getting a bit of an odd addition to my query.
User.where(id: [1..5])
and
User.where(id: [1...5])
Result in the following queries respectively:
SELECT `users`.* FROM `users` WHERE ((`users`.`id` BETWEEN 1 AND 5 OR 1=0))
SELECT `users`.* FROM `users` WHERE ((`users`.`id` >= 1 AND `users`.`id` < 5 OR 1=0))
The queries work perfectly fine since OR FALSE is effectively a no-op. I'm just wondering why Rails or ARel is adding this snippet into the query.
EDIT
It looks like the line that could explain this is line 26 in ActiveRecord::PredicateBuilder. Still no idea how the hash could be empty? at that point but maybe someone else does.
EDIT 2
This is intersting. I was looking into Filip's comment to see why he made it since it seems just like a clarification but he is correct that 1..5 != [1..5]. The former is an inclusive range from 1 to 5 where as the latter is an array whose first element is the former. I tried putting these into an ARel where call to see the SQL produced and the OR 1=0 is not there!
User.where(id: 1..5) #=> SELECT "users".* FROM "users" WHERE ("users"."id" BETWEEN 1 AND 5)
User.where(id: 1...5) #=> SELECT "users".* FROM "users" WHERE ("users"."id" >= 1 AND "users"."id" < 5)
While I still do not know why ARel is adding the OR 1=0 which will always be false and seemingly unnecessary. It may be due to how Arrays and Ranges are handled differently.
Building on the fact, which you've discovered, that [1..5] is not the correct way to specify the range... I have discovered why [1..5] behaves as it does. To get there, I first found that an empty array in a hash condition produces the 1=0 SQL condition:
User.where(id: []).to_sql
# => "SELECT \"users\".* FROM \"users\" WHERE 1=0"
And, if you check the ActiveRecord::PredicateBuilder::ArrayHandler code, you'll see that array values are always partitioned into ranges and other values.
ranges, values = values.partition { |v| v.is_a?(Range) }
This explains why you don't see the 1=0 when using non-range values. That is, the only way to get 1=0 from an array without including a range is to supply an empty array, which yields the 1=0 condition, as shown above. And when all the array has in it is a range you're going to get the range conditions (ranges) and, separately, an empty array condition (values) executed. My guess is that there isn't a good reason for this... it just simply is easier to let this be than to avoid it (since the result set is equivalent either way). If the partition code was a bit smarter then it wouldn't have to tack on the additional, empty values array and could skip the 1=0 condition.
As for where the 1=0 comes from in the first place... I think that comes from the database adapter, but I couldn't find exactly where. However, I would call it an attempt to fail to find a record. In other words, WHERE 1=0 isn't ever going to return any users, which makes sense over alternative SQL like WHERE id=null which will find any users whose id is null (realizing that this isn't really correct SQL syntax). And this is what I'd expect when attempting to find all Users whose id is in the empty set (i.e. we're not asking for nil ids or null ids or whatever). So, in my mind, leaving the bit about exactly where 1=0 comes from as a black box is OK. At least we now can reason about why the range inside of the array is causing it to show up!
UPDATE
I've also found that, even when using ARel directly, you can still get 1=0:
User.arel_table[:id].in([]).to_sql
# => "1=0"
This is strictly speaking a guess, since I did something similar in a project of my own (although I used AND 1).
For whatever reason, when generating a query, it is easier to always have a WHERE clause containing a no-op than it is to conditionally generate the WHERE clause at all. That is, if you don't include any where sections it will end up generating something still valid.
On the other hand, I'm not sure why it's taking this form: when I did it I use 1 [<AND (generated code)>...] it allowed arbitrary chaining, but I don't see how what you're seeing would allow it. None the less, I still think it likely to be a result of an algorithmic code generation scheme.
Check to see if you are using active_record-acts_as. That was the problem with me.
Add the line below to your Gemfile:
gem 'active_record-acts_as', :git => 'https://github.com/hzamani/active_record-acts_as.git'
This will just pull the latest version of the Gem that will hopefully be fixed. Worked for me.
I think you're seeing side effects of ruby personally.
I think the better way to do what you're doing would be with
2.0.0-p481#meri :008 > [*1..5]
=> [1, 2, 3, 4, 5]
User.where(id: [*1..5]).to_sql
"SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 2, 3, 4, 5)"
As this creates an Array vs an Array with element 1 of class Range.
OR
use an explicit Range to trigger the BETWEEN in AREL.
# with end element, i.e. exclude_end=false
2.0.0-p481#meri :013 > User.where(id: Range.new(1,5)).to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`id` BETWEEN 1 AND 5)"
# without end element, i.e. exclude_end=true
2.0.0-p481#meri :022 > User.where(id: Range.new(1, 5, true)).to_sql
=> "SELECT `users`.* FROM `users` WHERE (`users`.`id` >= 1 AND `users`.`id` < 5)"
If you care about having control of the queries you generate and the full power of the SQL language and database features then I would suggest moving from ActiveRecord/Arel to Sequel.
I can honestly say there are a lot more quirks and infuriating times ahead for you with ActiveRecord, especially when you move beyond simple crud like queries. When you start trying to query your data in anger, perhaps needing to join a few join tables here and there and realize you really do need join conditions or union all type queries.
It is also significantly faster and more reliable in its query generation and result handling and much easier to compose the queries you want. It also has real documentation you can actually read unlike arel.
I just wish I had discovered it much earlier rather than persisting with the rails default data access layer.