Rails 5 - using synthetic attributes in queries? - ruby-on-rails-5

I'm not sure if synthetic attributes are capable of being used this way, but I have an account model with two boolean fields, :is_class_accout and :is_expense_account.
I created a an attribute which is not persisted in teh db called :is_synthetic which return true if either :is_class_account is true or :is_expense_account is true.
What I want to do is write a query like:
Account.find_by_project_id(project_id).where(is_synthetic: true)
This returns a PG error because the resulting query is looking for the is_synthetic field in the db, and of course it's not there.
Am I doing something wrong, or is this expected behavior?
The code I am using is:
class Account < ApplicationRecord
attribute :is_synthetic, :boolean
def is_synthetic
self.is_class_account || self.is_expense_account
end

The behaviour is expected. Two things here:
The example query is wrong since find_by_project_id would return the first matching Account record and not a collection to call where on.
In your case all you have to do is retrieve the Account records and then filter them:
class Account < ApplicationRecord
def is_synthetic
self.is_class_account || self.is_expense_account
end
end
# Returns an Array of accounts
Account.where(project_id: <queried_project_id>).select(&:is_synthetic)

Related

How to toggle a boolean field for multiple records generating just one SQL query

I'm trying to write a migration to update a boolean field where the field will mean the exact opposite of what it means currently. Therefore I need to toggle every record to update the records where this field is true to false and vice-versa.
Example:
class ChangePostsVisibleToArchived < ActiveRecord::Migration[6.1]
def change
rename_column :posts, :visible, :archived
# Toggle all tracked topics
# What I DON'T want to do:
Post.all.each { |post| post.toggle! :archived }
end
end
The way I described above will generate one SQL command per Post record.
Is there a way I can toggle all records within a single SQL command using rails ActiveRecord syntax?
Post.update_all "archived = NOT archived"

Rails: How to use instance method in Active Record Query

I am having a filter query which should return all the records where either
attribute (column) "status" is not "done", or
instance method "completeness_status" does not return "done"
The query is something like that:
Studies.where("studies.status != ? OR studies.completeness_status != ?", "done", "done")
but I am getting error that column completeness_status does not exist.
Unfortunately, the column status is not continuously updated, so I cannot use it only. Also, the instance method "completeness_status" is based on records from other tables.
I try to add a scope to my model and use this instance method as scope but also I was not successful.
Also, I tried to used it as class method but then I do not know how to call it from where clause.
def self.completeness_status(study)
# code
end
or
def self.completeness_status_done?(study)
# code return true or false
end
Any Idea how to solve that.
Thanks.
You cannot use instance methods inside your query. But if you like to check the condition of completeness for only one row, then you can use it as instance method:
first_study = Study.first
first_study.completeness_status_done?
Also, if you provide more information about what is going on inside your completeness_status_done? then maybe I can give you some ideas to use scopes.
https://guides.rubyonrails.org/active_record_querying.html

Why does Postgres not accept my count column?

I am building a Rails app with the following models:
# vote.rb
class Vote < ApplicationRecord
belongs_to :person
belongs_to :show
scope :fulfilled, -> { where(fulfilled: true) }
scope :unfulfilled, -> { where(fulfilled: false) }
end
# person.rb
class Person < ApplicationRecord
has_many :votes, dependent: :destroy
def self.order_by_votes(show = nil)
count = 'nullif(votes.fulfilled, true)'
count = "case when votes.show_id = #{show.id} AND NOT votes.fulfilled then 1 else null end" if show
people = left_joins(:votes).group(:id).uniq!(:group)
people = people.select("people.*, COUNT(#{count}) AS people.vote_count")
people.order('people.vote_count DESC')
end
end
The idea behind order_by_votes is to sort People by the number of unfulfilled votes, either counting all votes, or counting only votes associated with a given Show.
This seem to work fine when I test against SQLite. But when I switch to Postgres I get this error:
Error:
PeopleControllerIndexTest#test_should_get_previously_on_show:
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column people.vote_count does not exist
LINE 1: ...s"."show_id" = $1 GROUP BY "people"."id" ORDER BY people.vot...
^
If I dump the SQL using #people.to_sql, this is what I get:
SELECT people.*, COUNT(nullif(votes.fulfilled, true)) AS people.vote_count FROM "people" LEFT OUTER JOIN "votes" ON "votes"."person_id" = "people"."id" GROUP BY "people"."id" ORDER BY people.vote_count DESC
Why is this failing on Postgres but working on SQLite? And what should I be doing instead to make it work on Postgres?
(PS: I named the field people.vote_count, with a dot, so I can access it in my view without having to do another SQL query to actually view the vote count for each person in the view (not sure if this works) but I get the same error even if I name the field simply vote_count.)
(PS2: I recently added the .uniq!(:group) because of some deprecation warning for Rails 6.2, but I couldn't find any documentation for it so I am not sure I am doing it right, still the error is there without that part.)
Are you sure you're not getting a syntax error from PostgreSQL somewhere? If you do something like this:
select count(*) as t.vote_count from t ... order by t.vote_count
I get a syntax error before PostgreSQL gets to complain about there being no t.vote_count column.
No matter, the solution is to not try to put your vote_count in the people table:
people = people.select("people.*, COUNT(#{count}) AS vote_count")
...
people.order(vote_count: :desc)
You don't need it there, you'll still be able to reference the vote_count just like any "normal" column in people. Anything in the select list will appear as an accessor in the resultant model instances whether they're columns or not, they won't show up in the #inspect output (since that's generated based on the table's columns) but you call the accessor methods nonetheless.
Historically there have been quite a few AR problems (and bugs) in getting the right count by just using count on a scope, and I am not sure they are actually all gone.
That depends on the scope (AR version, relations, group, sort, uniq, etc). A defaut count call that a gem has to generically use on a scope is not a one-fit-all solution. For that known reason Pagy allows you to pass the right count to its pagy method as explained in the Pagy documentation.
Your scope might become complex and the default pagy collection.count(:all) may not get the actual count. In that case you can get the right count with some custom statement, and pass it to pagy.
#pagy, #records = pagy(collection, count: your_count)
Notice: pagy will efficiently skip its internal count query and will just use the passed :count variable.
So... just get your own calculated count and pass it to pagy, and it will not even try to use the default.
EDIT: I forgot to mention: you may want to try the pagy arel extra that:
adds specialized pagination for collections from sql databases with GROUP BY clauses, by computing the total number of results with COUNT(*) OVER ().
Thanks to all the comments and answers I have finally found a solution which I think is the best way to solve this.
First of, the issue occurred when I called pagy which tried to count my scope by appending .count(:all). This is what caused the errors. The solution was to not create a "field" in select() and use it in .order().
So here is the proper code:
def self.order_by_votes(show = nil)
count = if show
"case when votes.show_id = #{show.id} AND NOT votes.fulfilled then 1 else null end"
else
'nullif(votes.fulfilled, true)'
end
left_joins(:votes).group(:id)
.uniq!(:group)
.select("people.*, COUNT(#{count}) as vote_count")
.order(Arel.sql("COUNT(#{count}) DESC"))
end
This sorts the number of people on the number of unfulfilled votes for them, with the ability to count only votes for a given show, and it works with pagy(), and pagy_arel() which in my case is a much better fit, so the results can be properly paginated.

How do you do retrieve records based on a child elements' condition?

I have a model AvailableSlot with attribute "max_attendees". AvailableSlot has_many BookedSlots.
I am looking for all AvailableSlots that are still available (where the booked_slots is less than max_attendees")
I tried
scope :with_capacity, -> { joins('LEFT OUTER JOIN booked_slots on booked_slots.available_slot_id = available_slots.id')
.group('available_slots.id').having("booked_slots.count < available_slots.max_attendees") }
and
#AvailableSlot.rb
def self.with_capacity
self.select{ |s| s.booked_slots.count < s.max_attendees }
end
but these return arrays, and the first one can't do a ".count", and the second solution can't do ".limit(3)" because the data that gets returned aren't active records.
One last question: for doing things like,
AvailableSlot.joins(:booked_slots).where("booked_slots.length < ?, max_attendees)
I can't use .length because that's not a column under booked_slots. What's a way to use this .joins.where format while using the activerecord methods like .count, .length, etc?
You need to create a virtual attribute for the having.
AvailableSlot.
joins('LEFT OUTER JOIN booked_slots on booked_slots.available_slot_id = available_slots.id').
select('available_slots.*, count(booked_slots) as booked_slots_count').
group('available_slots.id').
having('booked_slots_count < max_attendees')
This will also expose a the virtual attribute to the model.
Mind that this will make it so .count is more complicated and you'll have to pass jazz to it, check out the options available on the doc: http://apidock.com/rails/ActiveRecord/Calculations/ClassMethods/count

Rails Store Array in Database

I need to persist an array in my rails language learning app. Here is the situation/reason I need to do this (if there is a better way please let me know). The user is presented, one at a time, 10 words to learn. After the words are learned they get quized on the same set of 10 words.
For the 'quiz' portion I would like to randomize the order the words appear in (for example: currently if you learn the words 1.fish 2.cat 3.dog... you will be quized in the same order 1.fish 2.cat 3.dog... which can make the quiz easier.
I need to persist it in case the user were to log off or navigate away. In this instance I want to return them to the exact word they left off on the quiz.
Here is the controller code I currently have:
def index
.
.
#quiz = Lang.limit(10).offset(current_user.bookmark - 11)
exercise_bank
.
.
end
private
def exercise_bank
current_user.random_exercise_array = (0..9).step(1).shuffle
current_user.save
#index2 = current_user.random_exercise_array[#index]
##index starts at 0 and increments on each call to index action
##index2 grabs the random number and uses it to reference a word in #quiz
#something like this: #quiz[#index2].spanish_to_english---grabs a english word
end
end
The idea of the above is to pick a random number 0-9 and use it to reference a word in my DB. The above results in something like the following in my DB for the random_exercise_array attribute:
random_exercise_array: "---\n- 6\n- 0\n- 1\n- 7\n- 8\n- 5\n- 9\n- 3\n- 2\n- 4\n"
Here is the relevant User DB code:
class DeviseCreateUsers < ActiveRecord::Migration
serialize :random_exercise_array
end
Relevant migration file:
class AddRandomExerciseArrayToUsers < ActiveRecord::Migration
def change
add_column :users, :random_exercise_array, :text
end
end
Is this the best way to solve this problem? If so, can you explain how to get back an integer from random_exercise_array without all of the (-)'s and (\n')s?
If you want get an array back from DB text column, you should declare the type in your user model like this:
class DeviseCreateUsers < ActiveRecord::Base #why your original code inherit from ActiveRecord::Migration?
serialize :random_exercise_array, Array
end
Then you can use current_user.random_exercise_array as an array instead of a string.