Create Rails SQL Function + PostgreSQL - ruby-on-rails

I want to know the correct syntax for writing down function in rails.
This needs to accept parameters.
Code I have managed is:
def up
self.connection.execute %Q( CREATE OR REPLACE FUNCTION business_objective_function RETURNS INT AS $$
BEGIN
RETURN 1;
END
)
end
While running migration : syntax error at or near "RETURNS"

Related

How to access raw SQL statement generated by update_all (ActiveRecord method)

I'm just wondering if there's a way to access the raw SQL that's executed for an update_all ActiveRecord request. As an example, take the simple example below:
Something.update_all( ["to_update = ?"], ["id = ?" my_id] )
In the rails console I can see the raw SQL statement so I'm guessing it's available for me to access in some way?
PS - I'm specifically interested in update_all and can't change it to anything else.
Thanks!
If you look at the way update_all is implemented you can't call to_sql on it like you can on relations since it executes directly and returns an integer (the number of rows executed).
There is no way to tap into the flow or get the desired result except by duplicating the entire method and changing the last line:
module ActiveRecord
# = Active Record \Relation
class Relation
def update_all_to_sql(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
if eager_loading?
relation = apply_join_dependency
return relation.update_all(updates)
end
stmt = Arel::UpdateManager.new
stmt.set Arel.sql(#klass.sanitize_sql_for_assignment(updates))
stmt.table(table)
if has_join_values? || offset_value
#klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key))
else
stmt.key = arel_attribute(primary_key)
stmt.take(arel.limit)
stmt.order(*arel.orders)
stmt.wheres = arel.constraints
end
#- #klass.connection.update stmt, "#{#klass} Update All"
stmt.to_sql
end
end
end
The reason you see the log statements is that they are logged by the connection when it executes the statements. While you can override the logging its not really possible to do it for calls from a single AR method.
If you have set RAILS_LOG_LEVEL=debug Rails shows you which SQL statement it executed.
# Start Rails console in debug mode
$ RAILS_LOG_LEVEL=debug rails c
# Run your query
[1] pry(main)> Something.update_all( ["to_update = ?"], ["id = ?" my_id] )
SQL (619.8ms) UPDATE "somethings" WHERE id = 123 SET to_update = my_id;
# ^it prints out the query it executed

How does Rails type cast the result of array_agg() function

I'm trying to apply this in-line view trick https://gist.github.com/jturkel/7917985#file-inline_view_model-rb in Rails 4. but there is one column in my view will returns an array generated from postgresql's build function array_agg().
class Product < ActiveRecord::Base
defult_scope { set_from_clause }
def self.set_from_clause
query = Product.joins(:tags)
.group("products.id")
.select("products.id", "Array_agg('tags.id') AS tag_ids")
from(query, table_name)
end
def self.columns
[
ActiveRecord::ConnectionAdapters::Column.new('product_id', nil, ActiveRecord::Type::String.new),
ActiveRecord::ConnectionAdapters::Column.new('tag_ids', nil, ActiveRecord::Type::Integer.new),
]
end
end
This example code doesn't work, because the value returned by Array_agg cannot be typcasted properly.
I wonder is there is a way to make this trick works with the tag_ids in the example.
use json_agg instead of array_agg and then parse it using JSON.parse method.
This work for me.

undefined method 'exec_prepared' on Rails 4 postgresql query

Every time, I submit a form supposed to create a Deal and sending a very high nb of Prizes (>200K) to the Prize table using a transaction and raw postgresql, I have first the error 'undefined method exec_prepared' then if I reload the form then I get a new error 'ERROR: prepared statement 'xixie' already exists'.
I used this question wrong number of arguments (1 for 2..3) for Active Record postgresql query (Rails 4/postgresql 9.4) and Prepared Statement on Postgresql in Rails to create the following Postgresql query:
models deals.rb
CONNEXION = ActiveRecord::Base.connection.raw_connection
def create_prizes
Deal.transaction do
self.prize_number.times do |i|
st = CONNEXION.prepare('xixie', 'INSERT INTO prizes (deal_id) values ($1)')
values = [ { value: self.id} ]
st.exec_prepared('xixie', values )
st.close()
end
end
end
I have this problem in Local (not production) and I am not using any puma/unicorn. I do use Zeus and Guard.
Is it impossible with Rails4/postgresql prepared_statements to insert multiple rows at a time ?
How can I change the query to make it work ?
Also as Rails gives me ' ERROR: prepared statement 'xixie' already exists', I had to change multiple times the name of the prepared_statements but will they "live" forever? how can I "kill" them after I do all theses iterations trying to find the appropriate query.
EDIT
Updated the code after some proposed answer:
CONNECTION = ActiveRecord::Base.connection.raw_connection
def create_prizes
Deal.transaction do
self.prize_number.times do |i|
CONNECTION.prepare('mimiku', 'INSERT INTO deal_prizes (deal_id, created_at, updated_at) values ($1, $2, $3)')
CONNECTION.exec_prepared('mimiku', [ { value: self.id}, { value: '2009-01-23 20:21:13' }, { value: '2009-01-23 20:21:13' } ] )
end
# CONNECTION.close()
end
end
(added '2009-01-23 20:21:13' as Rails required created_at and updated_at for some reason).
I get this error:
ERROR: prepared statement "mimiku" already exists
Even if I change the name from 'mimiku' to 'somethingelse', I still get this type of error.
The prepare method returns a result according to the docs:
http://deveiate.org/code/pg/PG/Connection.html#method-i-prepare
Maybe try call exec_prepared on the connection object
connection = ActiveRecord::Base.connection.raw_connection
def create_prizes
begin
connection.describe_prepared('xixie')
rescue PG::InvalidSqlStatementName
connection.prepare('xixie', 'INSERT INTO prizes (deal_id) values ($1)')
end
Deal.transaction do
self.prize_number.times do |i|
connection.exec_prepared('xixie', [ { value: self.id} ] )
end
end
end
UPDATE: I reworked the code above to first check if a prepared statement exists. If it doesn't exist it creates it. Sorry I haven't realized it in the first place but you don't need to prepare a statement more than once. This is the actual benefit of such a statement, since it has to be only parsed once and can than be executed with different values, which is then much faster than a regular query.
As prepared statements last for the duration of the AR connection you only need to prepare it once.

Try/Catch fails to catch exception in stored procedure

I'm struggling to get my try/catch to work with my stored procedure, it seems to not catch the error. I've been searching around, but no luck. What I'm trying to accomplish is to display the error number if an invalid value, no value, or null is given when the stored procedure is called. I've tried moving the try and catch it feels like everywhere, but it seems like the error may be on the "#SomeID as int" line. I appreciate any help I can get.
alter procedure DeleteAcct
#SomeID as int
as
begin try
declare #counter int
set #counter = (select count(SomeDate) from Table1 where SomeID = #SomeID group by SomeID)
begin
if(#counter > 0)
begin
Print 'Testing'
end
else
begin
delete from Table2
where SomeID = #SomeID
Print 'Delete successful'
end
end
end try
begin catch
print error_number()
end catch
<!-- calling the stored procedure -->
exec DeleteAcct '1231231231221'
<!-- Error received -->
Msg 8114, Level 16, State 5, Procedure DeleteAcct, Line 0
Error converting data type varchar to int.
You declare param #SomeID as an int then try to pass a string!
Make call like so:
exec DeleteAcct 1231231231221
In fact I suspect #SomeID should be declared as a varchar
Well this answer is late, but we do this at work all the time.
You should try/catch the launcher and not the SP that is being executed.
I believe the explanation was quite obvious, but lets develop:
The called procedure does not need any try/catch:
alter procedure DeleteAcct
#SomeID as int
as
declare #counter int
set #counter = (select count(SomeDate) from Table1 where SomeID = #SomeID group by SomeID)
begin
if(#counter > 0)
begin
Print 'Testing'
end
else
begin
delete from Table2
where SomeID = #SomeID
Print 'Delete successful'
end
end
The launcher or the calling procedure needs to be put into try/catch block so that the error from the executed procedure is displayed at the launcher level.
begin try
exec DeleteAcct '1231231231221'
end try
begin catch
print error_number()
end catch

How to use ST_GeoHash and ST_MakePoint postgis functions in rails before save?

How to build in rails postgis point, then geohash and save them into database before send response to client? I would like to make it through ST_MakePoint and ST_GeoHash function, I prefer to avoid execute SQL and extracting data by [0]["st_makepoint"], if it is possible how to insert this functions to execute them automatically when inserting all attributes? I've installed squeel gem, maybe can I merge this functions to the query?
My current rails code:
before_save :set_geopoint
def set_geopoint
#attributes -> {"latitude" => 51.90,"longitude" => 16.42,"geopoint" => nil}
#self.geopoint = "ST_MakePoint(#{latitude}, #{longitude})")" #not working
#self.geopoint = ActiveRecord::Base.connection.execute("SELECT ST_MakePoint(#{latitude}, #{longitude})")[0]["st_makepoint"]
#self.geohash = "ST_GeoHash(#{self.geopoint})"
#self.geohash = ActiveRecord::Base.connection.execute("SELECT ST_GeoHash(ST_SetSRID(#{self.geopoint},4326),5);").first["st_geohash"]
end
I did it through SQL before trigger function but I'm still looking for rails approach.
CREATE FUNCTION geopoint_trigger() RETURNS TRIGGER AS $$
BEGIN
INSERT INTO posts( geopoint ) VALUES( ST_GeomFromText('POINT(' || NEW.latitude || ' ' || NEW.longitude || ')') );
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql';
To generate a point that you can save in a postgis enabled database, you need to generate that point with a factory. What you're doing here:
self.geopoint = "ST_MakePoint(#{latitude}, #{longitude})")" #not working
is just setting self.geopoint to a string and trying to save it. As your database geopoint field is (I assume) set to accept a point, it fails.
I suggest you use the rgeo gem to add geo factories to your models. Add it to your Gemfile.
https://github.com/rgeo/rgeo
You need to make sure that in your migration, you are using a point as column type like this:
t.point :geopoint, geographic: true
In your Post model, you then need to specify a factory like this:
RGEO_FACTORY = RGeo::Geographic.spherical_factory(srid: 4326)
And you also need to tell rgeo what factory to use on your geopoint column.
set_rgeo_factory_for_column :geopoint, RGEO_FACTORY
Now in your before_save, simply do:
self.geopoint = RGEO_FACTORY.point(latitude, longitude)
and it should work...
EDIT
If you want to use Postgis functions in your Rails models to get the GeoHash for instance, then you could do something like this:
post_geohash = Post.select("ST_GeoHash(geopoint) as geohash").where(id: some_post_id).geohash
You could also create an instance method that does that on your Post model:
def geohash
Post.select("ST_GeoHash(geopoint) as geohash").where(id: id).geohash
end
Not sure this works as it's not tested but you get the idea.

Resources