Rails: where condition with "OR is NULL" inside - ruby-on-rails

I want to get all the records where a datetime attribute is either NULL or within a timeframe. How can I add a or where disbursed_at IS NULL to the condition inside the where?
My actual query is way longer, I wouldn't want to repeat it all using .or
Investor.left_join(:disbursements).where(disbursements: { disbursed_at: year_period })

Generally speaking you can add an OR column_X IS NULL condition by providing an Array, containing at least one nil element, as part of the Hash arguments.
For Example:
Investor
.left_join(:disbursements)
.where(disbursements: { disbursed_at: [year_period,nil] })
Assuming year_period is a Range this should result in
SELECT
investors.*
FROM
investors
LEFT OUTER JOIN disbursements ON disbursements.investor_id = investors.id
WHERE
disbursements.disbursed_at BETWEEN xxxxx AND xxxxx
OR disbursements.disbursed_at IS NULL

Related

Ruby - Return record with longest string in column

I just need the record with the longest string in the product_description column.
A record could have nil in the product_description column
Which is why this won't work:
Product.where(parent_product_id: 22033).pluck(:product_description).max_by(&:length)
Then I try SQL and get:
ActiveRecord::UnknownAttributeReference (Query method called with non-attribute argument(s): "max(length(product_description))")
From this query:
Product.where(parent_product_id: 22033).pluck("max(length(product_description))")
This returns the same:
Product.where(parent_product_id: 22033).order("MAX(CHAR_LENGTH(product_description)) desc").limit(1)
But product_description is definitely a column on the Products table.. that's not the issue
You can use the length function of your RDBMS to calculate the length, then order by it.
Ties
There might be many products with the same description length. In order to have consistent results, you will need a tie breaker as otherwise the order within the products with same description length is not defined. You could add an order by id clause.
NULL
Be aware that
select length(null)
will return null and not 0.
null might be sorted before actual values or after (depending on your RDBMS and its config).
If you always need a numeric value you can do
select length(coalesce(null, ''))
which will return 0.
coalesce returns the first non-null argument and therefore ensures that we always pass at least an empty string to length.
You can also use the null last option for the order clause.
You can also exclude records with a null value for the description:
products = Product.where.not(product_description: nil)
to avoid dealing with null values altogether.
If the column is not nullable, then there is no problem either.
Now if you just use this:
products = Product.all # or whatever conditions you need
products
.order("length(coalesce(product_description, '')) desc")
.order(id: :asc)
.first
then Rails might complain (depends on the Version you are using) for security reasons with something like ActiveRecord::UnknownAttributeReference: Dangerous query method
which means you need to wrap the whole thing in Arel.sql
products = Product.all # or whatever conditions you need
products
.order(Arel.sql("length(coalesce(product_description, '')) desc"))
.order(id: :asc)
.first
Index
If you have many records, you might want to add an index on the column length. See https://sqlfordevs.com/function-based-index for how to create a function based index.
You can order by length and take first like this
Product
.where(parent_product_id: 22033)
.where.not(product_description: nil)
.order("LENGTH(product_description) DESC")
.first
LENGTH is RDBMS function and depends on specific system and may differ therefore

Can I force the execution of an active record query chain?

I have an edge case where I want to use .first only after my SQL query has been executed.
My case is the next one:
User.select("sum((type = 'foo')::int) as foo_count",
"sum((type = 'bar')::int) as bar_count")
.first
.yield_self { |r| r.bar_count / r.foo_count.to_f }
However, this would throw an SQL error saying that I should include my user_id in the GROUP BY clause. I've already found a hacky solution using to_a, but I really wonder if there is a proper way to force execution before my call to .first.
The error is because first uses an order by statement to order by id.
"Find the first record (or first N records if a parameter is supplied). If no order is defined it will order by primary key."
Instead try take
"Gives a record (or N records if a parameter is supplied) without any implied order. The order will depend on the database implementation. If an order is supplied it will be respected."
So
User.select("sum((type = 'foo')::int) as foo_count",
"sum((type = 'bar')::int) as bar_count")
.take
.yield_self { |r| r.bar_count / r.foo_count.to_f }
should work appropriately however as stated the order is indeterminate.
You may want to use pluck which retrieves only the data instead of select which just alters which fields get loaded into models:
User.pluck(
"sum((type = 'foo')::int) as foo_count",
"sum((type = 'bar')::int) as bar_count"
).map do |foo_count, bar_count|
bar_count / foo_count.to_f
end
You can probably do the division in the query as well if necessary.

Change search query after adding a new column to table rails

Right now I use this search to find items for certain category in my database
#items=Item.where(:category_id => #active_category_id).order(:price)
But recently I added a column to Item table called detail. It can be 0 or 1 and it is integer or it can be empty cause I added it just now and I had already some items in my db.
So now I need two searches: I need search that returns items with detail=1
And where detail is not 1.
So I do it like this:
#for items with detail = 1
#items=Item.where(:category_id => #active_category_id)
.where(:detail=> 1).order(:price)
It is working.
But now I need to find items with detail != 1
So I write
#items=Item.where(:category_id => #active_category_id)
.where.not(:detail=> 1).order(:price)
And it is not working. What do I do?
NULL values will not match an equality or inequality. You have to explicity compare to NULL. Try Item.where(detail: nil).
If you need 0 OR NULL you might need to write raw SQL: Item.where("detail = 0 OR detail IS NULL")
You might also consider backfilling your db to eliminate the NULL values, then you can just compare with 1 and 0.

How do I use TADOQuery.Parameters with integer parameter types that have to be put in two or more places in a query?

I have a complex query that contains more than one place where the same primary key value must be substituted. It looks like this:
select Foo.Id,
Foo.BearBaitId,
Foo.LinkType,
Foo.BugId,
Foo.GooNum,
Foo.WorkOrderId,
(case when Goo.ZenID is null or Goo.ZenID=0 then
IsNull(dbo.EmptyToNull(Bar.FanName),dbo.EmptyToNull(Bar.BazName))+' '+Bar.Strength else
'#'+BarZen.Description end) as Description,
Foo.Init,
Foo.DateCreated,
Foo.DateChanged,
Bug.LastName,
Bug.FirstName,
Goo.BarID,
(case when Goo.ZenID is null or Goo.ZenID=0 then
IsNull(dbo.EmptyToNull(Bar.BazName),dbo.EmptyToNull(Bar.FanName))+' '+Bar.Strength else
'#'+BarZen.Description end) as BazName,
GooTracking.Status as GooTrackingStatus
from
Foo
inner join Bug on (Foo.BugId=Bug.Id)
inner join Goo on (Foo.GooNum=Goo.GooNum)
left join Bar on (Bar.Id=Goo.BarID)
left join BarZen on (Goo.ZenID=BarZen.ID)
inner join GooTracking on(Goo.GooNum=GooTracking.GooNum )
where (BearBaitId = :aBaitid)
UNION
select Foo.Id,
Foo.BearBaitId,
Foo.LinkType,
Foo.BugId,
Foo.GooNum,
Foo.WorkOrderId,
Foo.Description,
Foo.Init,
Foo.DateCreated,
Foo.DateChanged,
Bug.LastName,
Bug.FirstName,
0,
NULL,
0
from Foo
inner join Bug on (Foo.BugId=Bug.Id)
where (LinkType=0) and (BearBaitId= :aBaitid )
order by BearBaitId,LinkType desc, GooNum
When I try to use an integer parameter on this non-trivial query, it seems impossible to me. I get this error:
Error
Incorrect syntax near ':'.
The query works fine if I take out the :aBaitid and substitute a literal 1.
Is there something else I can do to this query above? When I test with simple tests like this:
select * from foo where id = :anid
These simple cases work fine. The component is TADOQuery, and it works fine until you add any :parameters to the SQL string.
Update: when I use the following code at runtime, the parameter substitutions are actually done (some glitch in the ADO components is worked around) and a different error surfaces:
adoFooContentQuery.Parameters.FindParam('aBaitId').Value := 1;
adoFooContentQuery.Active := true;
Now the error changes to:
Incorrect syntax near the keyword 'inner''.
Note again, that this error goes away if I simply stop using the parameter substitution feature.
Update2: The accepted answer suggests I have to find two different copies of the parameter with the same name, which bothered me so I reworked the query like this:
DECLARE #aVar int;
SET #aVar = :aBaitid;
SELECT ....(long query here)
Then I used #aVar throughout the script where needed, to avoid the repeated use of :aBaitId. (If the number of times the parameter value is used changes, I don't want to have to find all parameters matching a name, and replace them).
I suppose a helper-function like this would be fine too: SetAllParamsNamed(aQuery:TAdoQuery; aName:String;aValue:Variant)
FindParam only finds one parameter, while you have two with the same name. Delphi dataset adds each parameter as a separate one to its collection of parameters.
It should work if you loop through all parameters, check if the name matches, and set the value of each one that matches, although I normally choose to give each same parameter a follow-up number to distingish between them.

SQL Query: Using IF statement in defining new field

I have a table with many fields and additionally several boolean fields (ex: BField1, BField2, BField3 etc.).
I need to make a Select Query, which will select all fields except for boolean ones, and a new virtual field (ex: FirstTrueBool) whose value will equal to the name of the first TRUE Boolean Field.
For ex: Say I have BField1 = False, BField2 = True, BField3 = true, BField4=false, in that case SQL Query should set [FirstTrueBool] to "BField2". Is that possible?
Thank you in advance.
P.S. I use Microsoft Access (MDB) Database and Jet Engine.
If you want to keep the current architecture (mixed 'x' non-null status and 'y' non-status fields) you have (AFAIS now) only the option to use IIF:
Select MyNonStatusField1, /* other non-status fields here */
IIF([BField1], "BField1",
IIF([BField2], "BField2",
...
IIF([BFieldLast], "BFieldLast", "#No Flag#")
))))) -- put as many parenthesis as it needs to close the imbricated IIFs
From
MyTable
Of course you can add any Where clause you like.
EDIT:
Alternatively you can use the following trick:
Set the fields to null when the flag is false and put the order number (iow, "1" for BField1, "2" for BField2 etc.) when the flag is true. Be sure that the status fields are strings (ie. Varchar(2) or, better, Char(2) in SQL terminology)
Then you can use the COALESCE function in order to return the first non-value from the status fields which will be the index number as string. Then you can add in front of this string any text you like (for example "BField"). Then you will end with something like:
Select "BField" || Coalesce(BField1, BField2, BField3, BField4) /*etc. (add as many fields you like) */
From MyTable
Much clearer IMHO.
HTH
You would be better using a single 'int' column as a bitset (provided you have up to 32 columns) to represent the columns.
e.g. see SQL Server: Updating Integer Status Columns (it's sql server, but the same technique applies equally well to MS Access)

Resources