Skip to content

Conversation

andrewmarkle
Copy link
Contributor

New pattern that makes it possible to support multiple database adapters

This extracts anything SQLite-specific into a generic database adapter class which forwards to a SQLite adapter class. This is a new pattern that will make it pretty easy to support different databases (I've got MySQL working in a different branch). Since this is a big change I wanted to validate the approach first and get this merged in for SQLite. But adding support for MYSQL is fairly trivial after this change.

This pattern is very similar to how Rails does it. In Rails there's the main ActiveRecord::Tasks::DatabaseTasks which defines the API and then forwards the implementation over to the specific adapter to do the work. For example here's the class for SQLite.

Originally I tried tackling this by using the adapters / methods that are provided by Rails but...it's just slightly different enough that it won't work as is. I think there's a world where we could upstream a bunch of these changes to into Rails and make it more compatible and/or make this gem more compatible for how Rails does it. Either way I thought it best to just do our own thing for now and cross that bridge another day. 😊

No breaking changes

There should be no actual changes from the current implementation for SQLite—except for one place when we drop the db (I'll add a comment at that line of code).

How I tested

I relied on the test suite for this one and extracted everything without breaking any tests. I also added tests for the adapter to ensure that it forwards everything over to the correct adapter methods. I didn't unit test the actual SQLite adapter since this is well covered by other tests but I would be happy to add those as well.

# Remove the SQLite database file and associated files
FileUtils.rm_f(database_path)
FileUtils.rm_f("#{database_path}-wal") # Write-Ahead Logging file
FileUtils.rm_f("#{database_path}-shm") # Shared Memory file
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only potential breaking change but I think it's a bug fix? This adds also removing the -wal and -shm when we drop the database.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the gem should do this (the Rails SQLiteDatabaseTasks#drop method does this as well).

@flavorjones
Copy link
Member

Thanks! I'll take a look in the next day or so.

@andrewmarkle
Copy link
Contributor Author

I noticed a few issues since I first pushed this up but I got everything fixed up now. No rush or anything on the review but I'm done making changes now! 😂

Copy link
Member

@flavorjones flavorjones left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great work, thank you so much for extracting all of this so nicely!

I made some comments, but they are almost all small things like naming, unnecessary variables, or breaking a method into multiple methods.

One thing I do think still remains to be extracted into the adapters: the BaseConfig#database_path_for method, which only exists because the SQLite :database value might be a URI.

The idea I have in mind is to remove the "pathiness" from the database configs:

  • delete the BaseConfig#database_path_for method
  • remove the :database_path value from the TenantConfig hash
  • move BaseConfig#coerce_path into the SQLite adapter
  • and change SQLite#get_database_path into an instance variable @database_path that's computed in the initializer by calling coerce_path

but we can take care of that in a second PR if you prefer to keep the complexity down!

Comment on lines +40 to +41
rescue
[]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious why we're rescuing here instead of just letting any exceptions bubble up. Is there a specific exception type we can rescue (and a test we can add!) if there's a specific case you want to handle?

test "raises error for unsupported adapter" do
unsupported_config = create_config("mongodb")

error = assert_raises ActiveRecord::Tenanted::Error do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's please create a new exception type for this! Maybe ActiveRecord::Tenanted::UnsupportedDatabaseError?

adapter_for(db_config).drop_database
end

def database_exists?(db_config, arguments = {})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit: can we follow the convention set by the Ruby File class and use exist? instead of exists??

class SQLite
def initialize(db_config)
@db_config = db_config
@configuration_hash = db_config.configuration_hash
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think @configuration_hash is being used.

end

private
attr_reader :db_config, :db_configuration_hash
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also don't see db_configuration_hash being used.

Comment on lines +16 to +17
database_dir = File.dirname(database_path)
FileUtils.mkdir_p(database_dir) unless File.directory?(database_dir)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think explicitly creating the directory is unnecessary in practice because the ready lock creates it already. I'm OK keeping it for clarity/completeness but I wanted to mention that it's probably duplicative.

# Remove the SQLite database file and associated files
FileUtils.rm_f(database_path)
FileUtils.rm_f("#{database_path}-wal") # Write-Ahead Logging file
FileUtils.rm_f("#{database_path}-shm") # Shared Memory file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree the gem should do this (the Rails SQLiteDatabaseTasks#drop method does this as well).

glob = db_config.database_path_for("*")
scanner = Regexp.new(db_config.database_path_for("(.+)"))

Dir.glob(glob).filter_map do |path|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

map -> filter_map is also probably a bug fix. 👏

Comment on lines +69 to +73
if db_config.respond_to?(:database_path) && db_config.database_path
db_config.database_path
else
db_config.database
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we only ever call this on a DatabaseConfigurations::TenantConfig? If that's true I don't think we need the fallback logic here and this can just be:

Suggested change
if db_config.respond_to?(:database_path) && db_config.database_path
db_config.database_path
else
db_config.database
end
db_config.database_path

and maybe the whole method goes away? WDYT?

Comment on lines 30 to 34
root_config.database_path_for(tenant).tap do |path|
FileUtils.rm(path)
db_config = root_config.new_tenant_config(tenant)
ActiveRecord::Tenanted::DatabaseAdapter.drop_database(db_config)
$stdout.puts "Dropped database '#{path}'" if verbose?
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this code should call root_config.database_path_for directly (especially since we don't use the yielded path other than for printing to stdout).

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 this pull request may close these issues.

2 participants