Rails/SQL combine where conditions for rails scope - sql

I want to get total count of items with language_id other than [30, 54] and language_id: nil
LOCALES = {
non_arabic_languages: {
id: [30, 54]
}
}
scope :non_arabic_languages, -> { where.not(language_id: LOCALES[:non_arabic_languages][:id]) || where(language_id: nil) }
This example predictably returns first part, so I only get non arabic items. && works wrong as well. How may I combine it? We'll be thankful for the advice!

You're falling into a common trap where you confuse logical operations in Ruby with actually creating SQL via the ActiveRecord query interface.
Using || will return the first truthy value:
where.not(language_id: LOCALES[:non_arabic_languages][:id]) || where(language_id: nil)
Which is the ActiveRecord relation returned by where.not(language_id: LOCALES[:non_arabic_languages][:id]) since everything except false and nil are truthy in Ruby. || where(language_id: nil) is never actually evaluated.
Support for .or was added in Rails 5. In previous versions the most straight forward solution is to use Arel or a SQL string:
scope :non_arabic_languages, -> {
non_arabic_languages_ids = LOCALES[:non_arabic_languages].map { |h| h[:id] }
where(arel_table[:language_id].not_in(non_arabic_languages_ids ).or(arel_table[:language_id].eq(nil)))
}
I would leave a tagging comment (like #fixme) so that you fix this after upgrading to 5.0 or consider a better solution that doesn't involve hardcoding database ids in the first place like for example using a natural key.

Related

Slick plain sql query with pagination

I have something like this, using Akka, Alpakka + Slick
Slick
.source(
sql"""select #${onlyTheseColumns.mkString(",")} from #${dbSource.table}"""
.as[Map[String, String]]
.withStatementParameters(rsType = ResultSetType.ForwardOnly, rsConcurrency = ResultSetConcurrency.ReadOnly, fetchSize = batchSize)
.transactionally
).map( doSomething )...
I want to update this plain sql query with skipping the first N-th element.
But that is very DB specific.
Is is possible to get the pagination bit generated by Slick? [like for type-safe queries one just do a drop, filter, take?]
ps: I don't have the Schema, so I cannot go the type-safe way, just want all tables as Map, filter, drop etc on them.
ps2: at akka level, the flow.drop works, but it's not optimal/slow, coz it still consumes the rows.
Cheers
Since you are using the plain SQL, you have to provide a workable SQL in code snippet. Plain SQL may not type-safe, but agile.
BTW, the most optimal way is to skip N-th element by Database, such as limit in mysql.
depending on your database engine, you could use something like
val page = 1
val pageSize = 10
val query = sql"""
select #${onlyTheseColumns.mkString(",")}
from #${dbSource.table}
limit #${pageSize + 1}
offset #${pageSize * (page - 1)}
"""
the pageSize+1 part tells you whether the next page exists
I want to update this plain sql query with skipping the first N-th element. But that is very DB specific.
As you're concerned about changing the SQL for different databases, I suggest you abstract away that part of the SQL and decide what to do based on the Slick profile being used.
If you are working with multiple database product, you've probably already abstracted away from any specific profile, perhaps using JdbcProfile. In that case you could place your "skip N elements" helper in a class and use the active slickProfile to decide on the SQL to use. (As an alternative you could of course check via some other means, such as an environment value you set).
In practice that could be something like this:
case class Paginate(profile: slick.jdbc.JdbcProfile) {
// Return the correct LIMIT/OFFSET SQL for the current Slick profile
def page(size: Int, firstRow: Int): String =
if (profile.isInstanceOf[slick.jdbc.H2Profile]) {
s"LIMIT $size OFFSET $firstRow"
} else if (profile.isInstanceOf[slick.jdbc.MySQLProfile]) {
s"LIMIT $firstRow, $size"
} else {
// And so on... or a default
// Danger: I've no idea if the above SQL is correct - it's just placeholder
???
}
}
Which you could use as:
// Import your profile
import slick.jdbc.H2Profile.api._
val paginate = Paginate(slickProfile)
val action: DBIO[Seq[Int]] =
sql""" SELECT cols FROM table #${paginate.page(100, 10)}""".as[Int]
In this way, you get to isolate (and control) RDBMS-specific SQL in one place.
To make the helper more usable, and as slickProfile is implicit, you could instead write:
def page(size: Int, firstRow: Int)(implicit profile: slick.jdbc.JdbcProfile) =
// Logic for deciding on SQL goes here
I feel obliged to comment that using a splice (#$) in plain SQL opens you to SQL injection attacks if any of the values are provided by a user.

Querying attributes where value is not yet set

Any of you guys know how to query rally for a set of things where an string attribute value is currently not yet set?
I can’t query for the value equal to an empty string. That doesn’t parse. And I can’t use “null” either. Or rather, I can try “null” and it parses fine but it doesn’t result in finding anything.
query = #rally_api.find(:defect, :fetch =>true,
:project_scope_up => false, :project_scope_down => false,
:workspace => #workspace,
:project => #project) { equals :integration_i_d, "" }
This was followed up by telling the me to substitute "" with nil which didn't work. Null was tried to no success as well. I've tried "null" and null and "". None of them work.
I'm not familiar with our Ruby REST toolkit but directly hitting our WSAPI, you would say (<Field> = null). Notice that there are no quotes around "null".
Also, I'm wondering if the use of contains in your example above is what you wanted. You might want =.
try: { equal :integration_i_d, '""' }
Also, if rally_rest_api seems slow - we're working on something faster here:
http://developer.rallydev.com/help/ruby-rally-api

Rails scope that does nothing for NOT IN values

I have a Rails 3 scope that excludes an array of ids.
What is the best way to write the scope so that it does nothing when the array is empty and is still chainable? I currently have this, which works, but seems a little hokey:
scope :excluding_ids,
lambda {|ids| ids.empty? ? relation : where('id not in (?)', ids) }
If I do not have the "ids.empty? ? relation :" bit, when ids is empty the SQL generated is
... ID not in (NULL) ...
which will always return nothing. So something like:
Model.excluding_ids([]).where('id > 0')
returns no results.
If the ids array is empty then don't return anything.
scope :excluding_ids, lambda { |ids|
where(['id NOT IN (?)', ids]) if ids.any?
}
The query will run without any additional constraints on the query if there are no ids.
In Rails 4 you can use:
scope :excluding_ids, ->(ids) { where.not(id: ids) }
Here's a slight variation on Douglas' answer, using ruby 1.9 stabby lambda syntax and without the brackets in the where method.
scope :excluding_ids, ->(ids) {where("id NOT IN (?)", ids) if ids.any?}
How about the following? (It still checks for an empty array though, so if that's what you're trying to avoid it's not much of an improvement :)
scope :excluding_ids,
lambda {|ids| (ids.empty? && relation) || where('id not in (?)', ids) }

Why does this Rails named scope return empty (uninitialized?) objects?

In a Rails app, I have a model, Machine, that contains the following named scope:
named_scope :needs_updates, lambda {
{ :select => self.column_names.collect{|c| "\"machines\".\"#{c}\""}.join(','),
:group => self.column_names.collect{|c| "\"machines\".\"#{c}\""}.join(','),
:joins => 'LEFT JOIN "machine_updates" ON "machine_updates"."machine_id" = "machines"."id"',
:having => ['"machines"."manual_updates" = ? AND "machines"."in_use" = ? AND (MAX("machine_updates"."date") IS NULL OR MAX("machine_updates"."date") < ?)', true, true, UPDATE_THRESHOLD.days.ago]
}
}
This named scope works fine in development mode. In production mode, however, it returns the 2 models as expected, but the models are empty or uninitialized; that is, actual objects are returned (not nil), but all the fields are nil. For example, when inspecting the return value of the named scope in the console, the following is returned:
[#<Machine >, #<Machine >]
But, as you can see, all the fields of the objects returned are set to nil.
The production and development environments are essentially the same. Both are using a SQLite database. Here is the SQL statement that is generated for the query:
SELECT
"machines"."id",
"machines"."machine_name",
"machines"."hostname",
"machines"."mac_address",
"machines"."ip_address",
"machines"."hard_drive",
"machines"."ram",
"machines"."machine_type",
"machines"."use",
"machines"."comments",
"machines"."in_use",
"machines"."model",
"machines"."vendor_id",
"machines"."operating_system_id",
"machines"."location",
"machines"."acquisition_date",
"machines"."rpi_tag",
"machines"."processor",
"machines"."processor_speed",
"machines"."manual_updates",
"machines"."serial_number",
"machines"."owner"
FROM
"machines"
LEFT JOIN
"machine_updates" ON "machine_updates"."machine_id" = "machines"."id"
GROUP BY
"machines"."id",
"machines"."machine_name",
"machines"."hostname",
"machines"."mac_address",
"machines"."ip_address",
"machines"."hard_drive",
"machines"."ram",
"machines"."machine_type",
"machines"."use",
"machines"."comments",
"machines"."in_use",
"machines"."model",
"machines"."vendor_id",
"machines"."operating_system_id",
"machines"."location",
"machines"."acquisition_date",
"machines"."rpi_tag",
"machines"."processor",
"machines"."processor_speed",
"machines"."manual_updates",
"machines"."serial_number",
"machines"."owner"
HAVING
"machines"."manual_updates" = 't'
AND "machines"."in_use" = 't'
AND (MAX("machine_updates"."date") IS NULL
OR MAX("machine_updates"."date") < '2010-03-26 13:46:28')
Any ideas what's going wrong?
This might not be related to what is happening to you, but it sounds similar enough, so here it goes: are you using the rails cache for anything?
I got nearly the same results as you when I tried to cache the results of a query (as explained on railscast #115).
I tracked down the issue to a still open rails bug that makes cached ActiveRecords unusable - you have to choose between not using cached AR or applying a patch and getting memory leaks.
The cache works ok with non-AR objects, so I ended up "translating" the stuff I needed to integers and arrays, and cached that.
Hope this helps!
Seems like the grouping may be causing the problem. Is the data also identical in both dev & production?
Um, I'm not sure you're having the problem you think you're having.
[#<Machine >, #<Machine >]
implies that you have called "inspect" on the array... but not on each of the individual machine-objects inside it. This may be a silly question, but have you actually tried calling inspect on the individual Machine objects returned to really see if they have nil in the columns?
Machine.needs_updates.each do |m|
p m.inspect
end
?
If that does in fact result in nil-column data. My next suggestion is that you copy the generated SQL and go into the standard mysql interface and see what you get when you run that SQL... and then paste it into your question above so we can see.

Subsonic dynamic query expression

I am having a few issues with trying to live in a subsonic world and being a subsonic girl when it comes to subsonic expressions....
after reading Subsonic Query (ConditionA OR ConditionB) AND ConditionC it would appear i am not the only one with this sort of issue but hopefully someone (the almighty rob??) can answer this.
i am attempting to create an expression in my query based on a looping condition. what i want to achieve (in pseudo code) is something like this:
objQuery.andexpressionstart();
foreach (condition in conditions){
if (condition){
objQuery.and(conditionColumn).isequalto(X);
}
}
objQuery.expressionstop();
my main issue is that each condition that is inside the expression is a different column - otherwise i could just use .In() . i also have extra search criteria (read a fair bit) outside so it can't be outside an expression.
i REALLY don't want to leave the warm coseyness of the strongly-typed-subsonic womb however i think in this instance i might have too... if i DO have to is there a way to add to a subsonic query with a hand typed condition so i don't have to change all the other code in the query (alot of business logic living in subsonic land right now)
As always, thanks for any help
cheers
I haven't the time to test this right now, but I think if you do something like the following should work:
bool isFirstCondition = true;
foreach (condition in conditions){
if (condition)
{
if(isFirstCondition)
{
objQuery.AndExpression(conditionColumn).isequalto(X);
isFirstCondition = false;
}
else
{
objQuery.and(conditionColumn).isequalto(X);
}
}
}
Make sure all your other conditions have been added prior to the loop.