Skip to content

Commit

Permalink
Config db from url (#269)
Browse files Browse the repository at this point in the history
* Add from_url method to Conf::GlobalSettings::Database

* Allow configuring databases from URL

* Handle sqlite3 as a scheme on Conf::GlobalSettings::Database#from_url
  • Loading branch information
jpmartinspt authored Nov 10, 2024
1 parent 31bdbe9 commit 88cfbdc
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 2 deletions.
23 changes: 23 additions & 0 deletions docs/docs/development/how-to/configure-database-backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,26 @@ config.database do |db|
db.name = "my_db.db"
end
```

### Using URLs
```crystal
# Configure db using just a URL
config.database url: "postgres://my_user:my_db@localhost:1234/db"
# Configure a db other than the default using just a URL
config.database url: "postgres://my_user:my_db@localhost:1234/db"
config.database :my_other_db, url: "sqlite://my_other.db?journal_mode=wal&synchronous=normal"
# Configure db with a URL and a block
config.database url: "postgres://my_user:my_db@localhost:1234/db" do |db|
db.retry_delay = 1.0
end
# Configure a db other than the default with a URL and a block
config.database :my_other_db, url: "sqlite://my_other.db" do |db|
db.options = {
"journal_mode" => "wal"
"synchronous" => "normal"
}
end
```
111 changes: 111 additions & 0 deletions spec/marten/conf/global_settings/database_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,117 @@ describe Marten::Conf::GlobalSettings::Database do
db_config.name_set_with_env.should eq "test"
end
end

describe "#from_url" do
it "parses sqlite://:memory:" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url "sqlite://:memory:"
db_config_1.backend.should eq "sqlite"
db_config_1.name.should eq ":memory:"
end

it "parses sqlite url" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url "sqlite://marten.db"
db_config_1.backend.should eq "sqlite"
db_config_1.name.should eq "marten.db"
end

it "parses sqlite url that starts with sqlite3" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url "sqlite3://marten.db"
db_config_1.backend.should eq "sqlite"
db_config_1.name.should eq "marten.db"
end

it "parses a postgres url" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url "postgres://username:[email protected]:25/db"
db_config_1.backend.should eq "postgres"
db_config_1.user.should eq "username"
db_config_1.password.should eq "password"
db_config_1.host.should eq "martenframework.com"
db_config_1.port.should eq 25
db_config_1.name.should eq "db"
end

it "parses a mysql url" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url "mysql://username:[email protected]:25/db"
db_config_1.backend.should eq "mysql"
db_config_1.user.should eq "username"
db_config_1.password.should eq "password"
db_config_1.host.should eq "martenframework.com"
db_config_1.port.should eq 25
db_config_1.name.should eq "db"
end

it "parses a url with IPv4 host" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url "postgres://username:[email protected]:25/db"
db_config_1.backend.should eq "postgres"
db_config_1.user.should eq "username"
db_config_1.password.should eq "password"
db_config_1.host.should eq "127.0.0.1"
db_config_1.port.should eq 25
db_config_1.name.should eq "db"
end

it "parses a url with IPv6 host" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url "postgres://username:password@[::1]:25/db"
db_config_1.backend.should eq "postgres"
db_config_1.user.should eq "username"
db_config_1.password.should eq "password"
db_config_1.host.should eq "[::1]"
db_config_1.port.should eq 25
db_config_1.name.should eq "db"
end

it "parses a url with no params and defaults are set" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url "postgres://username:password@[::1]:25/db"
db_config_1.checkout_timeout.should eq 5.0
db_config_1.retry_attempts.should eq 1
db_config_1.retry_delay.should eq 1.0
db_config_1.max_idle_pool_size.should eq 1
db_config_1.max_pool_size.should eq 0
db_config_1.initial_pool_size.should eq 1
db_config_1.options.empty?.should be_true
end

it "parses a postgres socket url" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url "postgres://%2Fpath%2Fto%2Fsocket/db"
db_config_1.host.should eq "/path/to/socket"
db_config_1.name.should eq "db"
end

it "parses url parameters into mapping object properties" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url(
"postgres:///" \
"?checkout_timeout=2.0" \
"&retry_attempts=1&retry_delay=5.5" \
"&max_idle_pool_size=1&max_pool_size=10&initial_pool_size=5"
)
db_config_1.checkout_timeout.should eq 2.0
db_config_1.retry_attempts.should eq 1
db_config_1.retry_delay.should eq 5.5
db_config_1.max_idle_pool_size.should eq 1
db_config_1.max_pool_size.should eq 10
db_config_1.initial_pool_size.should eq 5
db_config_1.options.empty?.should be_true
end

it "parses url parameters that don't map directly into object properties into options" do
db_config_1 = Marten::Conf::GlobalSettings::Database.new("default")
db_config_1.from_url "sqlite:///?journal_mode=wal&busy_timeout=2.5"
db_config_1.options.size.should eq 2
db_config_1.options["journal_mode"].should eq "wal"
db_config_1.options["busy_timeout"].should eq "2.5"
end
end
end

module Marten::Conf::GlobalSettings::DatabaseSpec
Expand Down
59 changes: 59 additions & 0 deletions spec/marten/conf/global_settings_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,36 @@ describe Marten::Conf::GlobalSettings do
global_settings.databases[0].name.should eq "db.sql"
end

it "allows to configure the default DB connection using a URL" do
global_settings = Marten::Conf::GlobalSettings.new

global_settings.database url: "sqlite://db.sql"

global_settings.databases.size.should eq 1

global_settings.databases[0].id.should eq Marten::DB::Connection::DEFAULT_CONNECTION_NAME
global_settings.databases[0].backend.should eq "sqlite"
global_settings.databases[0].name.should eq "db.sql"
end

it "allows to configure the default DB connection using a URL and a block" do
global_settings = Marten::Conf::GlobalSettings.new

global_settings.database url: "sqlite://db.sql" do |db|
db.options = {
"journal_mode" => "wal",
}
end

global_settings.databases.size.should eq 1

global_settings.databases[0].id.should eq Marten::DB::Connection::DEFAULT_CONNECTION_NAME
global_settings.databases[0].backend.should eq "sqlite"
global_settings.databases[0].name.should eq "db.sql"
global_settings.databases[0].options.size.should eq 1
global_settings.databases[0].options["journal_mode"].should eq "wal"
end

it "allows to configure a non-default DB connection" do
global_settings = Marten::Conf::GlobalSettings.new

Expand All @@ -162,6 +192,35 @@ describe Marten::Conf::GlobalSettings do
global_settings.databases[0].backend.should eq "sqlite"
global_settings.databases[0].name.should eq "other_db.sql"
end

it "allows to configure a non-default DB connection using a URL" do
global_settings = Marten::Conf::GlobalSettings.new

global_settings.database :other, url: "sqlite://other_db.sql"
global_settings.databases.size.should eq 1

global_settings.databases[0].id.should eq "other"
global_settings.databases[0].backend.should eq "sqlite"
global_settings.databases[0].name.should eq "other_db.sql"
end

it "allows to configure a non-default DB connection using a URL and a block" do
global_settings = Marten::Conf::GlobalSettings.new

global_settings.database :other, url: "sqlite://other_db.sql" do |db|
db.options = {
"journal_mode" => "wal",
}
end

global_settings.databases.size.should eq 1

global_settings.databases[0].id.should eq "other"
global_settings.databases[0].backend.should eq "sqlite"
global_settings.databases[0].name.should eq "other_db.sql"
global_settings.databases[0].options.size.should eq 1
global_settings.databases[0].options["journal_mode"].should eq "wal"
end
end

describe "#debug" do
Expand Down
1 change: 1 addition & 0 deletions src/marten.cr
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require "mime/media_type"
require "msgpack"
require "openssl/hmac"
require "option_parser"
require "uri"
require "uuid"

require "./marten/app"
Expand Down
9 changes: 7 additions & 2 deletions src/marten/conf/global_settings.cr
Original file line number Diff line number Diff line change
Expand Up @@ -256,12 +256,17 @@ module Marten
@csrf ||= GlobalSettings::CSRF.new
end

# Allows to configure a specific database connection for the application.
def database(id = DB::Connection::DEFAULT_CONNECTION_NAME, &)
def database(id = DB::Connection::DEFAULT_CONNECTION_NAME, url : String | Nil = nil)
self.database id, url do |_|
end
end

def database(id = DB::Connection::DEFAULT_CONNECTION_NAME, url : String | Nil = nil, &)
db_config = @databases.find { |d| d.id.to_s == id.to_s }
not_yet_defined = db_config.nil?
db_config = Database.new(id.to_s) if db_config.nil?
db_config.not_nil!.with_target_env(@target_env) do |db_config_with_target_env|
db_config.from_url(url) if url
yield db_config_with_target_env
end
@databases << db_config if not_yet_defined
Expand Down
46 changes: 46 additions & 0 deletions src/marten/conf/global_settings/database.cr
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,52 @@ module Marten
def initialize(@id : String)
end

def from_url(url : String)
# URI.parse cant parse 'sqlite://:memory:'
if url.starts_with? "sqlite://:memory:"
self.backend = DB::Connection::SQLITE_ID
self.name = ":memory:"
return
end

uri = URI.parse url

self.backend = uri.scheme == "sqlite3" ? DB::Connection::SQLITE_ID : uri.scheme
self.host = uri.host
self.port = uri.port
self.user = uri.user
self.password = uri.password
self.name = @backend == DB::Connection::SQLITE_ID ? uri.host : uri.path[1..]?

params_map = uri.query_params.to_h

if !(v = params_map.delete("checkout_timeout").try &.to_f64).nil?
self.checkout_timeout = v
end

if !(v = params_map.delete("initial_pool_size").try &.to_i32).nil?
self.initial_pool_size = v
end

if !(v = params_map.delete("max_pool_size").try &.to_i32).nil?
self.max_pool_size = v
end

if !(v = params_map.delete("max_idle_pool_size").try &.to_i32).nil?
self.max_idle_pool_size = v
end

if !(v = params_map.delete("retry_attempts").try &.to_i32).nil?
self.retry_attempts = v
end

if !(v = params_map.delete("retry_delay").try &.to_f64).nil?
self.retry_delay = v
end

self.options = params_map
end

# Allows to set the connection backend of the database.
def backend=(val : Nil | String | Symbol)
@backend = val.try(&.to_s)
Expand Down

0 comments on commit 88cfbdc

Please sign in to comment.