I am trying to construct a SQL "having" clause via Sequel, in Ruby (Ruby 2.5.7, Sequel 5.28.0), but having issues with using an equality comparison.
All of the documentation and examples show that constructing a having block works for operators like <, >, <=, and >=. However, = throws a SyntaxError.
If I construct a having statement using >, I get the SQL out correctly:
query.having do sum(Sequel.case({{column => nil} => 1}, 0)) > count.function.* end
=> #<Sequel::Postgres::Dataset: "SELECT * FROM \"schema\".\"table\" GROUP BY \"id\" HAVING (sum((CASE WHEN (\"column\" IS NULL) THEN 1 ELSE 0 END)) > count(*))">
But if I change that > to =, I get a SyntaxError:
query.having do sum(Sequel.case({{column => nil} => 1}, 0)) = count.function.* end
SyntaxError: unexpected '=', expecting keyword_end
...nil} => 1}, 0)) = count.function.* end
... ^
SyntaxError: unexpected keyword_end, expecting end-of-input
... 1}, 0)) = count.function.* end
... ^~~
What is a valid = operator in this case? I can't find mention in the Sequel documentation or specs that show this usage. If I've missed this, apologies, and am happy to re-read if someone points me to where that information is.
== and <=> both lead to undesired and inaccurate SQL, and I also can't hack it via a combination of <= and >= since the having block doesn't seem to recognize && as and-ing multiple conditions (it just takes the last condition).
query.having do sum(Sequel.case({{column => nil} => 1}, 0)) == count.function.* end
=> #<Sequel::Postgres::Dataset: "SELECT * FROM \"schema\".\"table\" GROUP BY \"id\" HAVING false">
query.having do sum(Sequel.case({{column => nil} => 1}, 0)) <=> count.function.* end
=> #<Sequel::Postgres::Dataset: "SELECT * FROM \"schema\".\"table\" GROUP BY \"id\" HAVING false">
query.having do sum(Sequel.case({{column => nil} => 1}, 0)) >= count.function.* && sum(Sequel.case({{column => nil} => 1}, 0)) <= count.function.* end
=> #<Sequel::Postgres::Dataset: "SELECT * FROM \"schema\".\"table\" GROUP BY \"id\" HAVING (sum((CASE WHEN (\"column\" IS NULL) THEN 1 ELSE 0 END)) <= count(*))">
According to this this discussion, this should work, wrapping it in a virtual row and using a hash with =>
query.having do
{ sum(Sequel.case({{column => nil} => 1}, 0)) => count.function.* }
end
or using =~
query.having do
sum(Sequel.case({{column => nil} =~ 1}, 0)) == count.function.*
end
Related
I have a mongodb collection like this
{"assigneeId" => 1000, "status" => 3, "starttime" => "2014 Feb 25", "numofdays => 6}
{"assigneeId" => 1000, "status" => 2, "starttime" => "2014 Jan 10", "numofdays => 6}
{"assigneeId" => 1000, "status" => 3, "starttime" => "2014 Jan 1", "numofdays => 20}
I wrote a MongoDB query to group the above collection with assigneeId whose status is 3 and add the value of numofdays like this
db.events.group ( { key: {assigneeId:1}, cond: {status: {$gte: 3}}, reduce: function (curr, result) {result.total++; result.numofdays += curr.numofdays;}, initial: {total:0, numofdays:0} })
This gives the output as expected from the Mongodb cli.
When I write the ruby code for executing the same query, I could not make it work. Am getting lot of different hits for similar problem but none of the format seems to work. I could not go past the syntax error in ruby for grouping function. Please note I am not using mongoid. If I could get this work with Aggregation framework, that is also fine with me.
This is the ruby query I wrote and the error for the same.
#events_h.group (
:cond => {:status => { '$in' => ['3']}},
:key => 'assigneeId',
:initial => {count:0},
:reduce => "function(x, y) {y.count += x.count;}"
)
Error I am getting
analyze.rb:43: syntax error, unexpected ',', expecting ')'
out = #events_h.group (["assigneeId"], { }, { }, "function() { }")
^
analyze.rb:43: syntax error, unexpected ')', expecting keyword_end
analyze.rb:83: syntax error, unexpected end-of-input, expecting keyword_end
Any help is much appreciated.
Thanks
The following test includes solutions for using both methods, group and aggregate.
I recommend that you use aggregate, it is faster and avoids JavaScript and consuming a V8 engine.
Hope that this helps.
test.rb
require 'mongo'
require 'test/unit'
class MyTest < Test::Unit::TestCase
def setup
#events_h = Mongo::MongoClient.new['test']['events_h']
#docs = [
{"assigneeId" => 1000, "status" => 3, "starttime" => "2014 Feb 25", "numofdays" => 6},
{"assigneeId" => 1000, "status" => 2, "starttime" => "2014 Jan 10", "numofdays" => 6},
{"assigneeId" => 1000, "status" => 3, "starttime" => "2014 Jan 1", "numofdays" => 20}]
#events_h.remove
#events_h.insert(#docs)
end
test "group" do
result = #events_h.group(
:cond => {:status => {'$in' => [3]}},
:key => 'assigneeId',
:initial => {numofdays: 0},
:reduce => "function(x, y) {y.numofdays += x.numofdays;}"
)
assert_equal(26, result.first['numofdays'])
puts "group: #{result.inspect}"
end
test "aggregate" do
result = #events_h.aggregate([
{'$match' => {:status => {'$in' => [3]}}},
{'$group' => {'_id' => '$status', 'numofdays' => {'$sum' => '$numofdays'}}}
])
assert_equal(26, result.first['numofdays'])
puts "aggregate: #{result.inspect}"
end
end
$ ruby test.rb
Loaded suite test
Started
aggregate: [{"_id"=>3, "numofdays"=>26}]
.group: [{"assigneeId"=>1000.0, "numofdays"=>26.0}]
.
Finished in 0.010575 seconds.
2 tests, 2 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications
100% passed
189.13 tests/s, 189.13 assertions/s
I have a hash with the number of occurrences of cities from a model and I want to merge nil or blank keys and values into one and rename the new single key
for example
#raw_data = {nil => 2, "" => 2, "Berlin" => 1, "Paris" => 1}
# to
#corrected_data = {"undefined" => 4, "Berlin" => 1, "Paris" => 1}
I have looked into the merge method from the Rubydoc but it only merges if two keys are similar, but in this case nil and "" are not the same.
I could also create a new hash and add if nil or blank statements and add the values to a new undefined key, but it seems tedious.
I'd do :
#raw_data = {nil => 2, "" => 2, "Berlin" => 1, "Paris" => 1}
key_to_merge = [nil, ""]
#correct_data = #raw_data.each_with_object(Hash.new(0)) do |(k,v), out_hash|
next out_hash['undefined'] += v if key_to_merge.include? k
out_hash[k] = v
end
#correct_data # => {"undefined"=>4, "Berlin"=>1, "Paris"=>1}
Ruby cannot automatically determine what do you want to do with values represented by two "similar" keys. You have to define the mapping yourself. One approach would be:
def undefined?(key)
# write your definition of undefined here
key.nil? || key.length == 0
end
def merge_value(existing_value, new_value)
return new_value if existing_value.nil?
# your example seemed to add values
existing_value + new_value
end
def corrected_key(key)
return "undefined" if undefined?(key)
key
end
#raw_data = {nil => 2, "" => 2, "Berlin" => 1, "Paris" => 1}
#raw_data.reduce({}) do |acc, h|
k, v = h
acc[corrected_key(k)] = merge_value(acc[corrected_key(k)], v)
acc
end
# {"undefined" => 4, "Berlin" => 1, "Paris" => 1}
I have a helper module to generate an array hash data, which is something like:
[{:date => d, :total_amount => 31, :first_category => 1, :second_category => 2,...},
{:date => d+1, :total_amount => 31, :first_category => 1, :second_category => 2,...}]
So I make the method like:
def records_chart_data(category = nil, start = 3.weeks.ago)
total_by_day = Record.total_grouped_by_day(start)
category_sum_by_day = Record.sum_of_category_by_day(start)
(start.to_date..Time.zone.today).map do |date|
{
:date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0,
Category.find(1).title => category_sum_by_day[0][date].try(:first).try(:total_amount) || 0,
Category.find(2).title => category_sum_by_day[1][date].try(:first).try(:total_amount) || 0,
Category.find(3).title => category_sum_by_day[2][date].try(:first).try(:total_amount) || 0,
}
end
end
Since the Category will always change, I try to use loop in this method like:
def records_chart_data(category = nil, start = 3.weeks.ago)
total_by_day = Record.total_grouped_by_day(start)
category_sum_by_day = Record.sum_of_category_by_day(start)
(start.to_date..Time.zone.today).map do |date|
{
:date => date,
Category.all.each_with_index do |category, index|
category.title => category_sum_by_day[index][date].try(:first).try(:total_amount) || 0,
end
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}
end
end
But ruby alerts me with an error:
/Users/tsu/Code/CashNotes/app/helpers/records_helper.rb:10: syntax error, unexpected tASSOC, expecting keyword_end
category.title => category_sum_by_day[index][d...
Why does it say expecting keyword_end, and how should I fix it?
The method category_sum_by_day it calls looks like:
def self.sum_of_category_by_day(start)
records = where(date: start.beginning_of_day..Time.zone.today)
records = records.group('category_id, date(date)')
records = records.select('category_id, date, sum(amount) as total_amount')
records = records.group_by{ |r| r.category_id }
records.map do |category_id, value|
value.group_by {|r| r.date.to_date}
end
end
Or should I alter this method to generate a more friendly method for the helper above?
Category.all.each_with_index do |category, index|
category.title => category_sum_by_day # ...snip!
end
Unfortunately, this piece of code does not adhere to Ruby's grammar. The problem is the body of the block. x => y is not an expression and the syntax requires bodies of blocks to be expressions.
If you want to generate a hash by one key-value pair at a time try the following combination of Hash::[], Array#flatten and the splat operator (i.e. unary *):
Hash[*5.times.map { |i| [i * 3, - i * i] }.flatten]
As a result I'd rewrite the last expresion of records_chart_data more or less as follows
(start.to_date..Time.zone.today).map do |date|
categories = Hash[*Category.all.each_with_index do |category, index|
[ category.title, category_sum_by_day[...] ]
end .flatten]
{ :date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}.merge categories
end
If you consider it unreadable you can do it in a less sophisticated way, i.e.:
(start.to_date..Time.zone.today).map do |date|
hash = {
:date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}
Category.all.each_with_index do |category, index|
hash[category.title] = category_sum_by_day[...]
end
hash
end
Another idea is to use Array#reduce and adopt a more functional approach.
(start.to_date..Time.zone.today).map do |date|
Category.all.each_with_index.reduce({
:date => date,
:total_amount => total_by_day[date].try(:first).try(:total_amount) || 0
}) do |hash, (category, index)|
hash.merge category.title => category_sum_by_day[...]
end
hash
end
My method:
def my_method=(attributes, some_option = true, another_option = true)
puts hello
end
When i try to call this, i get such error:
my_method=({:one => 'one', :two => 'two'}, 1, 1)
#you_code.rb:4: syntax error, unexpected ',', expecting ')'
#my_method=({:one => 'one', :two => 'two'}, 1, 1)
^
What's the problem?
Method with suffix punctuation = can have only one argument.
Otherwise, you must use send to invoke with multiple parameters.
send :'my_method=', {:a => 1}, 1, 1
Don't use parenthesis when invoking a method using the = syntactic sugar.
Invoke it like this:
mymethod= {:one => 'one', :two => 'two'}, 1, 1
I want to use an equivalent of Oracle's nvl() function in Ruby. Is there a built in function or do I have to write one myself?
Edit:
I am using it to rewrite some sql to ruby:
INSERT INTO my_table (id, val)
VALUES (1, nvl(my_variable,'DEFAULT'));
becomes
plsql.my_table.insert {:id => 1, :val => ???my_variable???}
You could use Conditional assignment
x = find_something() #=>nil
x ||= "default" #=>"default" : value of x will be replaced with "default", but only if x is nil or false
x ||= "other" #=>"default" : value of x is not replaced if it already is other than nil or false
Operator ||= is a shorthand form of the expression
x = x || "default"
Some tests
irb(main):001:0> x=nil
=> nil
irb(main):003:0* x||=1
=> 1
irb(main):006:0> x=false
=> false
irb(main):008:0> x||=1
=> 1
irb(main):011:0* x||=2
=> 1
irb(main):012:0> x
=> 1
And yes, If you don't want false to be match, you could use if x.nil? as Nick Lewis mentioned
irb(main):024:0> x=nil
=> nil
irb(main):026:0* x = 1 if x.nil?
=> 1
Edit:
plsql.my_table.insert {:id => 1, :val => ???????}
would be
plsql.my_table.insert {:id => 1, :val => x || 'DEFAULT' }
where x is the variable name to set in :val, 'DEFAULT' will be insert into db, when x is nil or false
If you only want nil to 'DEFAULT', then use following way
{:id => 1, :val => ('DEFAULT' if x.nil?) }