Why Changeset.change is skipping validation in Elixir? - validation

This is a simple function used to insert or update some data.
If user data is already in db i simply update it, otherwise i insert a new row with data. Everything works fine but i have a problem with validation.
Changeset definition:
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name, :surname, :user_id])
|> validate_required([:name, :surname, :user_id])
|> unique_constraint(:user_id)
end
validate_required is currently working only during insert and not during update.
def add_or_change(user_id, new_data) do
data_from_db = data_by_user_id (user_id)
case data_from_db do
nil ->
Data.changeset(%Data{}, new_data)
|> Repo.insert()
_ ->
Changeset.change(data_from_db, new_data)
|> Repo.update()
end
end
If i try to insert "" as :name value, i get an error (can't be blank) as expected. However if i'm updating an existing row with "" as :name value, changeset does not pass through validation and my db is updated improperly. How to force validation also on change, before Repo.update()??

According to the doc: Ecto.Changeset/2 is meant for internal data changes, so it bypasses validations:
The function is meant for working with data internal to the application. Because of that neither validation nor casting is performed. This means change/2 expects the keys in the changes map or keyword to be atoms.
You should use Ecto.Changeset.cast/4 to apply the validations, and then update if it is valid.

Don't use this:
Changeset.change(data_from_db, new_data)
Just run the same function you were already using:
Data.changeset(data_from_db, new_data)
By the way, you can actually simplify this function a lot:
def add_or_change(user_id, new_data) do
(data_by_user_id(user_id) || %Data{})
|> Data.changeset(new_data)
|> Repo.insert_or_update()
end

Related

PostgreSQL "ERROR: could not determine data type of parameter" Ruby exec_params

I am trying to execute a query that selects recipes that match a search term from user input stored in the query variable. This is the portion of relevant code:
class DatabasePersistence
def initialize(logger)
#db = if Sinatra::Base.production?
PG.connect(ENV['DATABASE_URL'])
else
PG.connect(dbname: "recipes")
end
#logger = logger
end
def search_recipes(query)
p "Query parameter is:"
p query
p query.class
sql = <<~SQL
SELECT * FROM recipes
WHERE labels ILIKE '%$1::text%'
SQL
results = query(sql, query)
# ... more code
end
def query(statement, *params)
#logger.info "#{statement}: #{params}"
#db.exec_params(statement, params)
end
end
The following error is raised on when this line results = query(sql, query) is executed.
PG::IndeterminateDatatype at /search
ERROR: could not determine data type of parameter $1
Another post suggested adding an explicit type cast which is why I added the type cast. I could be doing it incorrectly. I also tried it like the following:
WHERE labels ILIKE '%text($1)%'
WHERE labels ILIKE '%cast($1 as text)%'
WHERE labels ~ '$1::text'
WHERE labels ~ 'cast($1 as text'
In all of the above cases it returned the same error "could not determine the datatype of parameter. I added some #p method calls to make sure the query variable is referencing a real value for debuggin. I have confirmed that this error occurs when the query references a string object with value oats.
What is causing this error to still occur if I am casting the datatype and it is not nil? Am I passing the parameters incorrectly? Am I casting the parameters incorrectly? Is it possible there is a way to pass datatypes as arguments to the #exec_params method? Is there another way to safely pass parameters to be executed by the instance of the PG.connect class?
Simply:
WHERE labels ILIKE $1::text
I assume labels is a plain character type like text, too.

Remove enum value with gem sequel

I have a db - Postgresql v11.3 and Sequel with pg_enum, so how I can remove value from enum type?
For example, need change column enum type :
Sequel.migration do
up do
removed_values = %w["val1" "val2"]
remove_enum_value= (:old_enum_type, removed_values)
create_enum(
:enum_column,
%w[val3 val4]
)
alter_table :users do
set_column_type(
:enum_column,
'enum_column[]',
using: 'old_enum_column::text[]:::enum_column[]'
)
set_column_default :enum_column, '{}'
end
end
end
New enum type same as previous, with little different - new type doesn't have a some values. But, may be situation if somebody use missing values - migration will be crashed.
Unfortunatly, pg_enum don't have a method, that will able simple remove enum value
If you review https://www.postgresql.org/docs/12/sql-altertype.html, you will see that PostgreSQL supports adding and renaming enum values, but not removing them.

Updating unpermitted values in an Ecto changeset

I want to update some meta data that is not a permitted attribute in my schema's changeset:
def changeset(%Comment{} = comment, attrs) do
comment
|> cast(attrs, [:text])
|> validate_required([:text])
end
And then something like:
changeset = Comment.changeset(commet, %{under_moderation: true})
Repo.update(changeset)
Since under_moderation is not whitelisted, it gets ignored. What options do I have to force the update? If there are multiple options, is there a convention?
I would just create another changeset function that has the rights to set the value.
def admin_changeset(%Comment{} = comment, attrs) do
comment
|> cast(attrs, [:text, :under_moderation])
|> validate_required([:text])
end
Then simply use that to update the value. As you can see, I named it admin_changeset because it seems like this is a value that would be set by an admin. In your controller or context module, simply check the user role (if you have something like that) and then decide which changeset function you want to use.

Create an object if one is not found

How do I create an object if one is not found? This is the query I was running:
#event_object = #event_entry.event_objects.find_all_by_plantype('dog')
and I was trying this:
#event_object = EventObject.new unless #event_entry.event_objects.find_all_by_plantype('dog')
but that does not seem to work. I know I'm missing something very simple like normal :( Thanks for any help!!! :)
find_all style methods return an array of matching records. That is an empty array if no matching records are found. And an empty is truthy. Which means:
arr = []
if arr
puts 'arr is considered turthy!' # this line will execute
end
Also, the dynamic finder methods (like find_by_whatever) are officially depreacted So you shouldn't be using them.
You probably want something more like:
#event_object = #event_entry.event_objects.where(plantype: 'dog').first || EventObject.new
But you can also configure the event object better, since you obviously want it to belong to #event_entry.
#event_object = #event_entry.event_objects.where(plantype: 'dog').first
#event_object ||= #event_entry.event_objects.build(plantype: dog)
In this last example, we try to find an existing object by getting an array of matching records and asking for the first item. If there are no items, #event_object will be nil.
Then we use the ||= operator that says "assign the value on the right if this is currently set to a falsy value". And nil is falsy. So if it's nil we can build the object form the association it should belong to. And we can preset it's attributes while we are at it.
Why not use built in query methods like find_or_create_by or find_or_initialize_by
#event_object = #event_entry.event_objects.find_or_create_by(plantype:'dog')
This will find an #event_entry.event_object with plantype = 'dog' if one does not exist it will then create one instead.
find_or_initialize_by is probably more what you want as it will leave #event_object in an unsaved state with just the association and plantype set
#event_object = #event_entry.event_objects.find_or_initialize_by(plantype:'dog')
This assumes you are looking for a single event_object as it will return the first one it finds with plantype = 'dog'. If more than 1 event_object can have the plantype ='dog' within the #event_entry scope then this might not be the best solution but it seems to fit with your description.

Single Ruby Value in One Line From a Collection

I have a collection of objects. There are 3 properties in each object
'id', 'name', 'is_primary'
The collection of objects will usually have anywhere from 1 to 5 objects.
What I want to do is check the collection to see if is_primary is true. If so output the name, or at least return it.
I want to do this in 1 line of code if possible. I am trying to slim up this one line for erb output in rails. Later in the page i'll output them all. I thought I had it, but if I return nil it adds extra space which shifts all the html oddly.
Thanks.
Hmm, this doesn't quite work if no element is_primary...I'm still thinking...
c.detect(&:is_primary).name
Ok, how about:
((a = c.detect(&:is_primary)) && a.name).to_s
As it happens, it is OK in an erb template for the <%= expression to return nil, that just results in an empty string, so for that case you can use:
(a = c.detect(&:is_primary)) && a.name
Update: Responding to the first comment, I do have a test case that I didn't post...
class A; attr_accessor :is_primary, :name, :id; end
t = [A.new, A.new, A.new, (a = A.new; a.name = 'xyz'; a.is_primary = true; a)]
puts (a = t.detect(&:is_primary)) && a.name
puts ((a = [].detect(&:is_primary)) && a.name).to_s
Complementing #DigitalRoss, you can also write:
collection.detect(&:is_primary).try(:name) || "default_if_no_element_or_name"
(well, to be honest I prefer Ick's maybe over Rails' try: c.detect(&:is_primary).maybe.name)
Side note: IMHO a flag that can only be active for a row it's not such a good idea. You may have inconsistent states with more than one being active and you'll have worry about it when updating (transactions, and so on). Try to store the PK reference somewhere else (a parent model? a state model?).
I want to do this in 1 line of code if possible. I am trying to slim up this one line for erb output in rails. Later in the page i'll output them all.
No need for one-liners (funny since I just wrote one): move the code to yous models or helpers as appropriate and keep your views pristine.

Resources