-
Notifications
You must be signed in to change notification settings - Fork 13
Extract SQLite into an adapter #204
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
base: main
Are you sure you want to change the base?
Conversation
# 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
Thanks! I'll take a look in the next day or so. |
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! 😂 |
There was a problem hiding this 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 callingcoerce_path
but we can take care of that in a second PR if you prefer to keep the complexity down!
rescue | ||
[] |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 = {}) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
database_dir = File.dirname(database_path) | ||
FileUtils.mkdir_p(database_dir) unless File.directory?(database_dir) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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| |
There was a problem hiding this comment.
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. 👏
if db_config.respond_to?(:database_path) && db_config.database_path | ||
db_config.database_path | ||
else | ||
db_config.database | ||
end |
There was a problem hiding this comment.
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:
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?
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 |
There was a problem hiding this comment.
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).
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.