Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Noticed seems incompatible with MS SQL server (undefined method `has_key?' for an instance of String) #450

Closed
3 tasks done
erlingur opened this issue May 28, 2024 · 21 comments · Fixed by #451
Closed
3 tasks done

Comments

@erlingur
Copy link

Bug Report

Describe the Bug:
When using Noticed with the MS SQL server adapter, it seems that the params get serialised as a string and Noticed isn't expecting that. This has something to do with ActiveJob it seems, but I'm not 100% sure. When I try to deliver a notification I get the error:

(irb):1:in `<main>': undefined method `has_key?' for an instance of String (NoMethodError)
        raise ValidationError, "Param `#{param_name}` is required for #{self.class.name}." unless params.has_key?(param_name)

To Reproduce:

  1. Set up a fresh Rails 7.1 application, with the tiny_tds and activerecord-sqlserver-adapter gems (and noticed of course).
  2. Generate a notifier
  3. Try to deliver the notifier like so: GenericNotifier.with(title: "Hello", message: "World").deliver(User.all)

Repo that demonstrates the problem: https://github.com/erlingur/noticed-bug

You will need to launch a MS SQL container with Docker like so:

sudo docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=sapassword" \
   -p 1433:1433 --name sql1 --hostname sql1 \
   -d \
   mcr.microsoft.com/mssql/server:2022-latest

If you have an issue installing tiny_tds you might have to install it like so:

gem install tiny_tds -v '2.1.5' -- --with-freetds-include=/opt/homebrew/include --with-freetds-lib=/opt/homebrew/lib

(This is what I have to do in my setup at least)

Expected Behavior:
I expect the notification to be delivered to all users

Actual Behavior:
I get an error:

(irb):1:in `<main>': undefined method `has_key?' for an instance of String (NoMethodError)
        raise ValidationError, "Param `#{param_name}` is required for #{self.class.name}." unless params.has_key?(param_name)

Environment:

  • Noticed gem version: 2.3.1
  • Ruby version: 3.3.1
  • Rails version: 7.1.3
  • Operating System: macOS Sonoma 14.5

Checklist:

  • I have searched for similar issues and couldn't find any
  • I have checked the documentation for relevant information
  • I have included all the required information
@excid3
Copy link
Owner

excid3 commented May 28, 2024

We only test against Postgres, MySQL, and SQLite. Looks like you have a text column for params instead of json?

@erlingur
Copy link
Author

Yeah, internally the activerecord-sqlserver-adapter will set a json column as nvarchar(MAX).

I can actually "fix" this by creating a notifier like this:

class GenericNotifier < Noticed::Event
  attribute :params, ActiveRecord::Type::SQLServer::Json.new
  required_params "title", "message"
end

@excid3
Copy link
Owner

excid3 commented May 28, 2024

Good to know!

One of the troubles we had in the past was trying to detect the database format at runtime causing issues so I'm not sure what's best here. We can definitely document it in the README, but we could possibly add it to the generator as well.

@erlingur
Copy link
Author

I am also not sure what's best to do here. I fixed this by defining my ApplicationNotifier like so:

class ApplicationNotifier < Noticed::Event
  attribute :params, ActiveRecord::Type::SQLServer::Json.new

  def self.required_params(*args)
    super(*args.map(&:to_s))
  end
end

That way I can still define my notifiers like normal:

class GenericNotifier < ApplicationNotifier
  required_params :title, :message
end

Maybe just a note in the docs is enough for this? I can live with this solution for my project at least.

@excid3
Copy link
Owner

excid3 commented May 28, 2024

If you open the Rails console, does ActiveRecord::Base.connection_db_config.adapter return sqlserver?

We might be able to inject that line into ApplicationNotifier in the generators easy enough.

This was added in Rails 6.1 and we require 6.1+ for Noticed, so we should be able to use that during generators.

@erlingur
Copy link
Author

Yeah, it does. That could work actually.

3.3.1 :001 > ActiveRecord::Base.connection_db_config.adapter
 => "sqlserver"

If you want to support also the singular require_param method you might want to do this:

class ApplicationNotifier < Noticed::Event
 attribute :params, ActiveRecord::Type::SQLServer::Json.new

 class << self
   def required_params(*args)
     super(*args.map(&:to_s))
   end
   alias_method :required_param, :required_params
 end
end

@excid3
Copy link
Owner

excid3 commented May 28, 2024

This can be simplified with which preserves the symbols and other objects in the JSON.

attribute :params, ActiveRecord::Type::SQLServer::Json.new
serialize :params, coder: Noticed::Coder

@erlingur
Copy link
Author

Just tried it, works great 😄

@excid3
Copy link
Owner

excid3 commented May 28, 2024

Looking at the code, ActiveRecord::Type::SQLServer::Json doesn't do anything but inherit from ActiveRecord::Type::Json so I'm wondering if there's any benefit from changing our builtin attribute :params, default: {} to attribute :params, default: ActiveRecord::Type::Json.new 🤔

@excid3
Copy link
Owner

excid3 commented May 28, 2024

That didn't work. Guess I'll just leave it alone. 😜

ActiveJob::SerializationError: Unsupported argument type: ActiveRecord::Type::Json
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activejob-7.1.3/lib/active_job/serializers.rb:34:in `serialize'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activejob-7.1.3/lib/active_job/arguments.rb:114:in `serialize_argument'
    lib/noticed/coder.rb:14:in `dump'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/type/serialized.rb:29:in `serialize'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activemodel-7.1.3/lib/active_model/type/helpers/mutable.rb:8:in `cast'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activemodel-7.1.3/lib/active_model/attribute.rb:199:in `type_cast'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activemodel-7.1.3/lib/active_model/attribute.rb:43:in `value'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activemodel-7.1.3/lib/active_model/attribute_set.rb:51:in `fetch_value'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/transactions.rb:421:in `block in restore_transaction_record_state'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activemodel-7.1.3/lib/active_model/attribute_set.rb:98:in `transform_values'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activemodel-7.1.3/lib/active_model/attribute_set.rb:98:in `map'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/transactions.rb:420:in `restore_transaction_record_state'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/transactions.rb:345:in `rolledback!'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/connection_adapters/abstract/transaction.rb:200:in `block in rollback_records'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/connection_adapters/abstract/transaction.rb:263:in `run_action_on_records'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/connection_adapters/abstract/transaction.rb:199:in `rollback_records'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/connection_adapters/abstract/transaction.rb:527:in `block in rollback_transaction'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3/lib/active_support/concurrency/load_interlock_aware_monitor.rb:23:in `handle_interrupt'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3/lib/active_support/concurrency/load_interlock_aware_monitor.rb:23:in `block in synchronize'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3/lib/active_support/concurrency/load_interlock_aware_monitor.rb:19:in `handle_interrupt'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3/lib/active_support/concurrency/load_interlock_aware_monitor.rb:19:in `synchronize'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/connection_adapters/abstract/transaction.rb:520:in `rollback_transaction'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/connection_adapters/abstract/transaction.rb:539:in `rescue in block in within_new_transaction'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/connection_adapters/abstract/transaction.rb:538:in `block in within_new_transaction'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3/lib/active_support/concurrency/load_interlock_aware_monitor.rb:23:in `handle_interrupt'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3/lib/active_support/concurrency/load_interlock_aware_monitor.rb:23:in `block in synchronize'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3/lib/active_support/concurrency/load_interlock_aware_monitor.rb:19:in `handle_interrupt'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activesupport-7.1.3/lib/active_support/concurrency/load_interlock_aware_monitor.rb:19:in `synchronize'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/connection_adapters/abstract/transaction.rb:532:in `within_new_transaction'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/connection_adapters/abstract/database_statements.rb:344:in `transaction'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/transactions.rb:212:in `transaction'
    /Users/chris/.asdf/installs/ruby/3.3.1/lib/ruby/gems/3.3.0/gems/activerecord-7.1.3/lib/active_record/transactions.rb:301:in `transaction'
    app/models/concerns/noticed/deliverable.rb:97:in `deliver'
    app/models/concerns/noticed/deliverable.rb:84:in `deliver'
    test/delivery_method_test.rb:34:in `block in <class:DeliveryMethodTest>'

@erlingur
Copy link
Author

Was that by changing the default? I noticed you only changed the default value and not the type of the attribute.

My change is making the type of the params attribute JSON

excid3 added a commit that referenced this issue May 28, 2024
This injects SQLServer configuration into ApplicationNotifier as needed
when sqlserver adapter is detected.

Fixes #450
@excid3
Copy link
Owner

excid3 commented May 28, 2024

Oh thanks! That's exactly what I did wrong haha

@erlingur
Copy link
Author

Np :) The following works on my end:

class ApplicationNotifier < Noticed::Event
  attribute :params, ActiveRecord::Type::Json.new, default: {}
  serialize :params, coder: Noticed::Coder
end

@excid3
Copy link
Owner

excid3 commented May 28, 2024

That worked, but does break a test with the Ephemeral notifier which uses ActiveModel.

@erlingur
Copy link
Author

Ah ok, nevermind then. Worth a shot 😄

@erlingur
Copy link
Author

Any idea why it would put the _aj_symbol_keys in there?

image

@excid3
Copy link
Owner

excid3 commented May 28, 2024

That's the thing we using ActiveJob::Arguments to retain symbols, classes, objects with GlobalID, etc.

@excid3
Copy link
Owner

excid3 commented May 28, 2024

Try out #451. Setting the type seems to be the missing piece so Rails handles all that nicely.

@erlingur
Copy link
Author

That's the thing we using ActiveJob::Arguments to retain symbols, classes, objects with GlobalID, etc.

Ok, so this is expected?

Try out #451. Setting the type seems to be the missing piece so Rails handles all that nicely.

Doing it right now 😃

@erlingur
Copy link
Author

Can confirm it works great! 😃 Thanks for fixing this! 🚀

@excid3
Copy link
Owner

excid3 commented May 28, 2024

You're welcome and thanks for the help on this! I think this'll fix other databases and be more compatible all around. 👍

excid3 added a commit that referenced this issue May 28, 2024
By specifying the type for the params column, we can make sure ActiveRecord knows how to handle this for more databases like sqlserver.

Fixes #450
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants