Just lately one of many RailsEventStore customers posted an subject that one needed to make use of RES on a Postgres database with PostGIS extension. Migration generator used to setup tables for occasions and streams was failing with UnsupportedAdapter
error.
In RailsEventStore we supported thus far PostgreSQL, MySQL2 and SQLite adapters representing given database engines. However if you wish to make use of talked about PostGIS extension, you want set up further activerecord-postgis-adapter
and set adapter: postgis
in database.yml
. Our code relied on worth returned by:
ActiveRecord::Base.connection.adapter_name.downcase
=> "postgis"
I assumed — Okay, that’s a simple repair, PostGIS is simply an extension, we have to deal with it like Postgres internally when producing migration. Similar information varieties ought to be allowed.
Straightforward repair, requiring a variety of modifications
This simple repair required me to alter 8 recordsdata (4 with implementation and 4 with assessments). One thing is just not okay right here — I assumed. So let’s look via every of them:
I had so as to add postgis
to the checklist of SUPPORTED_ADAPTERS
in VerifyAdapter
class
module RubyEventStore
module ActiveRecord
UnsupportedAdapter = Class.new(StandardError)
class VerifyAdapter
- SUPPORTED_ADAPTERS = %w[mysql2 postgresql sqlite].freeze
+ SUPPORTED_ADAPTERS = %w[mysql2 postgresql postgis sqlite].freeze
def name(adapter)
increase UnsupportedAdapter, "Unsupported adapter" until supported?(adapter)
finish
personal
private_constant :SUPPORTED_ADAPTERS
def supported?(adapter)
SUPPORTED_ADAPTERS.embody?(adapter.downcase)
finish
finish
finish
finish
Then I needed to lengthen case assertion in ForeignKeyOnEventIdMigrationGenerator#each_migration
methodology
module RubyEventStore
module ActiveRecord
class ForeignKeyOnEventIdMigrationGenerator
def name(database_adapter, migration_path)
VerifyAdapter.new.name(database_adapter)
each_migration(database_adapter) do |migration_name|
path = build_path(migration_path, migration_name)
write_to_file(path, migration_code(database_adapter, migration_name))
finish
finish
personal
def each_migration(database_adapter, &block)
case database_adapter
- when "postgresql"
+ when "postgresql", "postgis"
[
'add_foreign_key_on_event_id_to_event_store_events_in_streams',
'validate_add_foreign_key_on_event_id_to_event_store_events_in_streams'
]
else
['add_foreign_key_on_event_id_to_event_store_events_in_streams']
finish.every(&block)
finish
def absolute_path(path)
File.expand_path(path, __dir__)
finish
def migration_code(database_adapter, migration_name)
migration_template(template_root(database_adapter), migration_name).result_with_hash(migration_version: migration_version)
finish
def migration_template(template_root, identify)
ERB.new(File.learn(File.be part of(template_root, "#{identify}_template.erb")))
finish
def template_root(database_adapter)
absolute_path("./templates/#{template_directory(database_adapter)}")
finish
def template_directory(database_adapter)
TemplateDirectory.for_adapter(database_adapter)
finish
def migration_version
::ActiveRecord::Migration.current_version
finish
def timestamp
Time.now.strftime("%YpercentmpercentdpercentHpercentMpercentS")
finish
def write_to_file(path, migration_code)
File.write(path, migration_code)
finish
def build_path(migration_path, migration_name)
File.be part of("#{migration_path}", "#{timestamp}_#{migration_name}.rb")
finish
finish
finish
finish
Similar goes for Rails model of migration generator
start
require "rails/mills"
rescue LoadError
finish
if outlined?(Rails::Turbines::Base)
module RubyEventStore
module ActiveRecord
class RailsForeignKeyOnEventIdMigrationGenerator < Rails::Turbines::Base
class Error < Thor::Error
finish
namespace "rails_event_store_active_record:migration_for_foreign_key_on_event_id"
source_root File.expand_path(File.be part of(File.dirname(__FILE__), "../mills/templates"))
def initialize(*args)
tremendous
VerifyAdapter.new.name(adapter)
rescue UnsupportedAdapter => e
increase Error, e.message
finish
def create_migration
case adapter
- when "postgresql"
+ when "postgresql", "postgis"
template "#{template_directory}add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb",
"db/migrate/#{timestamp}_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"
template "#{template_directory}validate_add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb",
"db/migrate/#{timestamp}_validate_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"
else
template "#{template_directory}add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb",
"db/migrate/#{timestamp}_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"
finish
finish
personal
def adapter
::ActiveRecord::Base.connection.adapter_name.downcase
finish
def migration_version
::ActiveRecord::Migration.current_version
finish
def timestamp
Time.now.strftime("%YpercentmpercentdpercentHpercentMpercentS")
finish
def template_directory
TemplateDirectory.for_adapter(adapter)
finish
finish
finish
finish
finish
What’s essential, each of the migrators used VerifyAdapter
class (and two different migrators too).
TemplateDirectory
class additionally suffered from primitive obsession and it was utilized by the entire migrators too.
module RubyEventStore
module ActiveRecord
class TemplateDirectory
def self.for_adapter(database_adapter)
case database_adapter.downcase
- when "postgresql"
+ when "postgresql", "postgis"
"postgres/"
when "mysql2"
"mysql/"
finish
finish
finish
finish
finish
There was additionally another place — VerifyDataTypeForAdapter
which was composed of VerifyAdapter
, including verification of knowledge varieties obtainable to given database engine.
Right here we go once more, one other checks of string values, however in a extra particular context:
# frozen_string_literal: true
module RubyEventStore
module ActiveRecord
InvalidDataTypeForAdapter = Class.new(StandardError)
class VerifyDataTypeForAdapter
SUPPORTED_POSTGRES_DATA_TYPES = %w[binary json jsonb].freeze
SUPPORTED_MYSQL_DATA_TYPES = %w[binary json].freeze
SUPPORTED_SQLITE_DATA_TYPES = %w[binary].freeze
def name(adapter, data_type)
VerifyAdapter.new.name(adapter)
increase InvalidDataTypeForAdapter, "MySQL2 does not help #{data_type}" if is_mysql2?(adapter) && !SUPPORTED_MYSQL_DATA_TYPES.embody?(data_type)
increase InvalidDataTypeForAdapter, "sqlite does not help #{data_type}" if is_sqlite?(adapter) && supported_by_sqlite?(data_type)
increase InvalidDataTypeForAdapter, "PostgreSQL does not help #{data_type}" until supported_by_postgres?(data_type)
finish
personal
private_constant :SUPPORTED_POSTGRES_DATA_TYPES, :SUPPORTED_MYSQL_DATA_TYPES, :SUPPORTED_SQLITE_DATA_TYPES
def is_sqlite?(adapter)
adapter.downcase.eql?("sqlite")
finish
def is_mysql2?(adapter)
adapter.downcase.eql?("mysql2")
finish
def supported_by_sqlite?(data_type)
!SUPPORTED_SQLITE_DATA_TYPES.embody?(data_type)
finish
def supported_by_postgres?(data_type)
SUPPORTED_POSTGRES_DATA_TYPES.embody?(data_type)
finish
finish
finish
finish
I’ve observed the sample
- at begin we have to examine whether or not given adapter is allowed (PostgreSQL, MySQL, SQLite)
- we have to confirm sure information varieties for given adapters to be aligned with database engines
- then we’ve to decided to generate particular migration for given information sort
- migration template listing depends upon adapter sort
Let’s do it
Having all this inside a devoted Worth Object would permit decreasing variety of choice timber within the code, checking the identical primitives on and on.
One thing like:
DatabaseAdapter.from_string("postgres")
=> DatabaseAdapter::PostgreSQL.new
DatabaseAdapter.from_string("bazinga")
=> UnsupportedAdapter: "bazinga" (RubyEventStore::ActiveRecord::UnsupportedAdapter)
DatabaseAdapter.from_string("sqlite", "jsonb")
=> SQLite doesn't help "jsonb". Supported varieties are: binary. (RubyEventStore::ActiveRecord::InvalidDataTypeForAdapter)
After few iterations we ended up with the implementation under:
# frozen_string_literal: true
module RubyEventStore
module ActiveRecord
UnsupportedAdapter = Class.new(StandardError)
InvalidDataTypeForAdapter = Class.new(StandardError)
class DatabaseAdapter
NOT_SET = Object.new.freeze
class PostgreSQL < self
SUPPORTED_DATA_TYPES = %w[binary json jsonb].freeze
def initialize(data_type = NOT_SET)
tremendous("postgresql", data_type)
finish
def template_directory
"postgres/"
finish
finish
class MySQL < self
SUPPORTED_DATA_TYPES = %w[binary json].freeze
def initialize(data_type = NOT_SET)
tremendous("mysql2", data_type)
finish
def template_directory
"mysql/"
finish
finish
class SQLite < self
SUPPORTED_DATA_TYPES = %w[binary].freeze
def initialize(data_type = NOT_SET)
tremendous("sqlite", data_type)
finish
finish
def initialize(adapter_name, data_type)
increase UnsupportedAdapter if instance_of?(DatabaseAdapter)
validate_data_type!(data_type)
@adapter_name = adapter_name
@data_type = data_type
finish
attr_reader :adapter_name, :data_type
def supported_data_types
self.class::SUPPORTED_DATA_TYPES
finish
def eql?(different)
different.is_a?(DatabaseAdapter) && adapter_name.eql?(different.adapter_name)
finish
alias == eql?
def hash
DatabaseAdapter.hash ^ adapter_name.hash
finish
def template_directory
finish
def self.from_string(adapter_name, data_type = NOT_SET)
increase NoMethodError until eql?(DatabaseAdapter)
case adapter_name.to_s.downcase
when "postgresql", "postgis"
PostgreSQL.new(data_type)
when "mysql2"
MySQL.new(data_type)
when "sqlite"
SQLite.new(data_type)
else
increase UnsupportedAdapter, "Unsupported adapter: #{adapter_name.examine}"
finish
finish
personal
def validate_data_type!(data_type)
if !data_type.eql?(NOT_SET) && !supported_data_types.embody?(data_type)
increase InvalidDataTypeForAdapter,
"#{class_name} does not help #{data_type.examine}. Supported varieties are: #{supported_data_types.be part of(", ")}."
finish
finish
def class_name
self.class.identify.break up("::").final
finish
finish
finish
finish
DatabaseAdadpter
acts like a guardian class to all the particular adapters.
Particular adapters comprise lists of supported_data_types
to entry these by shopper courses and render informative error messages if chosen information is just not supported by given database engine.
They will additionally inform how the template_directory
is known as for given adapter.
We now have a single entry with DatabaseAdapter.from_string
which accepts adapter_name
and optionally data_type
that are each validated when creating an occasion of particular adapter.
What’s the result?
Three utility courses may very well be eliminated:
VerifyAdapter
VerifyDataTypeForAdapter
TemplateDirectory
4 courses and two rake duties had been simplified because the Worth Object carriers all the mandatory info for them to proceed:
ForeignKeyOnEventIdMigrationGenerator
module RubyEventStore
module ActiveRecord
class ForeignKeyOnEventIdMigrationGenerator
- def name(database_adapter, migration_path)
- VerifyAdapter.new.name(database_adapter)
+ def name(database_adapter_name, migration_path)
+ database_adapter = DatabaseAdapter.from_string(database_adapter_name)
each_migration(database_adapter) do |migration_name|
path = build_path(migration_path, migration_name)
write_to_file(path, migration_code(database_adapter, migration_name))
def each_migration(database_adapter, &block)
case database_adapter
- when "postgresql", "postgis"
+ when DatabaseAdapter::PostgreSQL
[
'add_foreign_key_on_event_id_to_event_store_events_in_streams',
'validate_add_foreign_key_on_event_id_to_event_store_events_in_streams'
RailsForeignKeyOnEventIdMigrationGenerator
def initialize(*args)
super
- VerifyAdapter.new.call(adapter)
+ @database_adapter = DatabaseAdapter.from_string(adapter_name)
rescue UnsupportedAdapter => e
raise Error, e.message
end
def create_migration
- case adapter
- when "postgresql", "postgis"
- template "#{template_directory}add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb",
+ case @database_adapter
+ when DatabaseAdapter::PostgreSQL
+ template "#{@database_adapter.template_directory}add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb",
"db/migrate/#{timestamp}_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"
- template "#{template_directory}validate_add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb",
+ template "#{@database_adapter.template_directory}validate_add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb",
"db/migrate/#{timestamp}_validate_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"
else
- template "#{template_directory}add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb",
+ template "#{@database_adapter.template_directory}add_foreign_key_on_event_id_to_event_store_events_in_streams_template.erb",
"db/migrate/#{timestamp}_add_foreign_key_on_event_id_to_event_store_events_in_streams.rb"
end
end
private
- def adapter
- ::ActiveRecord::Base.connection.adapter_name.downcase
+ def adapter_name
+ ::ActiveRecord::Base.connection.adapter_name
end
- def template_directory
- TemplateDirectory.for_adapter(adapter)
- end
module RubyEventStore
module ActiveRecord
class MigrationGenerator
- DATA_TYPES = %w[binary json jsonb].freeze
-
- def name(data_type, database_adapter, migration_path)
- increase ArgumentError, "Invalid worth for information sort. Supported for choices are: #{DATA_TYPES.be part of(", ")}." until DATA_TYPES.embody?(data_type)
- VerifyDataTypeForAdapter.new.name(database_adapter, data_type)
-
- migration_code = migration_code(data_type, database_adapter)
+ def name(database_adapter, migration_path)
+ migration_code = migration_code(database_adapter)
path = build_path(migration_path)
write_to_file(migration_code, path)
path
- def migration_code(data_type, database_adapter)
- migration_template(template_root(database_adapter), "create_event_store_events").result_with_hash(migration_version: migration_version, data_type: data_type)
+ def migration_code(database_adapter)
+ migration_template(template_root(database_adapter), "create_event_store_events").result_with_hash(migration_version: migration_version, data_type: database_adapter.data_type)
finish
def template_root(database_adapter)
- absolute_path("./templates/#{template_directory(database_adapter)}")
- finish
-
- def template_directory(database_adapter)
- TemplateDirectory.for_adapter(database_adapter)
+ absolute_path("./templates/#{database_adapter.template_directory}")
finish
class Error < Thor::Error
finish
- DATA_TYPES = %w[binary json jsonb].freeze
-
namespace "rails_event_store_active_record:migration"
source_root File.expand_path(File.be part of(File.dirname(__FILE__), "../mills/templates"))
sort: :string,
default: "binary",
desc:
- "Configure the information sort for `information` and `meta information` fields in Postgres migration (choices: #{DATA_TYPES.be part of("https://weblog.arkency.com/")})"
+ "Configure the information sort for `information` and `meta information` fields in migration (choices: #{DatabaseAdapter::PostgreSQL.new.supported_data_types.be part of(", ")})"
)
def initialize(*args)
tremendous
- if DATA_TYPES.exclude?(data_type)
- increase Error, "Invalid worth for --data-type possibility. Supported for choices are: #{DATA_TYPES.be part of(", ")}."
- finish
-
- VerifyDataTypeForAdapter.new.name(adapter, data_type)
- rescue InvalidDataTypeForAdapter, UnsupportedAdapter => e
+ @database_adapter = DatabaseAdapter.from_string(adapter_name, data_type)
+ rescue UnsupportedAdapter => e
+ increase Error, e.message
+ rescue InvalidDataTypeForAdapter
+ increase Error,
+ "Invalid worth for --data-type possibility. Supported for choices are: #{DatabaseAdapter.from_string(adapter_name).supported_data_types.be part of(", ")}."
finish
def create_migration
- template "#{template_directory}create_event_store_events_template.erb", "db/migrate/#{timestamp}_create_event_store_events.rb"
+ template "#{@database_adapter.template_directory}create_event_store_events_template.erb",
"db/migrate/#{timestamp}_create_event_store_events.rb"
finish
personal
- def template_directory
- TemplateDirectory.for_adapter(adapter)
- finish
def data_type
choices.fetch("data_type")
finish
- def adapter
- ::ActiveRecord::Base.connection.adapter_name.downcase
+ def adapter_name
+ ::ActiveRecord::Base.connection.adapter_name
finish
db:migrations:copy
anddb:migrations:add_foreign_key_on_event_id
process "db:migrations:copy" do
data_type =
ENV["DATA_TYPE"] || increase("Specify information sort (binary, json, jsonb): rake db:migrations:copy DATA_TYPE=json")
::ActiveRecord::Base.establish_connection(ENV["DATABASE_URL"])
- database_adapter = ::ActiveRecord::Base.connection.adapter_name
+ database_adapter =
+ RubyEventStore::ActiveRecord::DatabaseAdapter.from_string(::ActiveRecord::Base.connection.adapter_name, data_type)
path =
- RubyEventStore::ActiveRecord::MigrationGenerator.new.name(
- data_type,
- database_adapter,
- ENV["MIGRATION_PATH"] || "db/migrate"
- )
+ RubyEventStore::ActiveRecord::MigrationGenerator.new.name(database_adapter, ENV["MIGRATION_PATH"] || "db/migrate")
places "Migration file created #{path}"
finish
@@ -30,7 +27,8 @@ desc "Generate migration for including international key on event_store_events_in_streams
process "db:migrations:add_foreign_key_on_event_id" do
::ActiveRecord::Base.establish_connection(ENV["DATABASE_URL"])
- path = RubyEventStore::ActiveRecord::ForeignKeyOnEventIdMigrationGenerator.new.name(ENV["MIGRATION_PATH"] || "db/migrate")
+ path =
+ RubyEventStore::ActiveRecord::ForeignKeyOnEventIdMigrationGenerator.new.name(ENV["MIGRATION_PATH"] || "db/migrate")
places "Migration file created #{path}"
finish
We might scale back branching and take away quite a few personal strategies in these.
The assessments of above courses simplified quite a bit and at the moment are targeted on core obligations of the courses reasonably than checking which information varieties are suitable with given adapter.
New adapter, not a giant deal
Quickly, there will likely be new default MySQL adapter in Rails 7.1 referred to as Trilogy. It could be cool to cowl this case already.
The one factor which we needed to do on this case, was to alter one line of code and add single line of check — since we already owned a very good abstraction:
module RubyEventStore
module ActiveRecord
class DatabaseAdapter
def self.from_string(adapter_name, data_type = NOT_SET)
increase NoMethodError until eql?(DatabaseAdapter)
case adapter_name.to_s.downcase
when "postgresql", "postgis"
PostgreSQL.new(data_type)
- when "mysql2"
+ when "mysql2", "trilogy"
MySQL.new(data_type)
when "sqlite"
SQLite.new(data_type)
else
increase UnsupportedAdapter, "Unsupported adapter: #{adapter_name.examine}"
finish
finish
finish
finish
finish
+ anticipate(DatabaseAdapter.from_string("Trilogy")).to eql(DatabaseAdapter::MySQL.new)
Trilogy is an adapter for MySQL, there’s no distinction from our perspective, we wish to deal with it as such.
Abstract
In case you’re curious on the total course of, right here’s the PR with the introduction of DatabaseAdapter
worth object. The code is 100% lined with mutation testing due to mutant.
I imagine that Worth Object is a completely underused sample in Ruby ecosystem. That’s why I needed to supply one more instance which differs from typical Cash
one you often see.
It’s an excellent device to scale back complexity of your code by eradicating pointless or repeatable branching.