sequelizer 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,17 @@
1
- require 'sequel'
2
- require_relative 'options'
1
+ require "sequel"
2
+ require_relative "options"
3
3
 
4
4
  module Sequelizer
5
+ # Raised when no configuration source names an adapter or a URL, so there
6
+ # is no database to connect to.
7
+ class MissingAdapterError < ArgumentError
8
+ def initialize(msg = nil)
9
+ super(msg || "no database configured: none of the passed options, .env, SEQUELIZER_* " \
10
+ "environment variables, config/sequelizer.yml or ~/.config/sequelizer/database.yml " \
11
+ "sets an :adapter or a :url")
12
+ end
13
+ end
14
+
5
15
  # = ConnectionMaker
6
16
  #
7
17
  # Class that handles loading/interpreting the database options and
@@ -14,7 +24,6 @@ module Sequelizer
14
24
  # maker = ConnectionMaker.new(adapter: 'postgres', host: 'localhost')
15
25
  # db = maker.connection
16
26
  class ConnectionMaker
17
-
18
27
  # @!attribute [r] options
19
28
  # @return [Options] the database connection options
20
29
  attr_reader :options
@@ -25,10 +34,11 @@ module Sequelizer
25
34
  # in order of precedence:
26
35
  # 1. .env file
27
36
  # 2. Environment variables
28
- # 3. config/database.yml
37
+ # 3. config/sequelizer.yml
29
38
  # 4. ~/.config/sequelizer/database.yml
30
39
  #
31
- # @param options [Hash, nil] database connection options
40
+ # @param options [Hash, String, nil] database connection options, or a
41
+ # database URL String, which is the same as passing <tt>{ url: string }</tt>
32
42
  # @option options [String] :adapter database adapter (e.g., 'postgres', 'mysql2')
33
43
  # @option options [String] :host database host
34
44
  # @option options [Integer] :port database port
@@ -46,6 +56,7 @@ module Sequelizer
46
56
  # using the configured options.
47
57
  #
48
58
  # @return [Sequel::Database] configured database connection
59
+ # @raise [MissingAdapterError] if no source sets an adapter or a URL
49
60
  # @raise [Sequel::Error] if connection fails
50
61
  #
51
62
  # @example
@@ -53,6 +64,8 @@ module Sequelizer
53
64
  # users = connection[:users].all
54
65
  def connection
55
66
  opts = options.to_hash
67
+ raise MissingAdapterError unless opts[:adapter] || opts[:url] || opts[:uri]
68
+
56
69
  extensions = options.extensions
57
70
 
58
71
  conn = create_sequel_connection(opts)
@@ -62,18 +75,36 @@ module Sequelizer
62
75
 
63
76
  private
64
77
 
78
+ # Connects with an after_connect proc that applies db_opts to every new
79
+ # connection. The proc needs the Database, which Sequel does not pass to
80
+ # after_connect, so it closes over +db+. Sequel hands the Database to
81
+ # :before_preconnect before it makes any connection (preconnect, the
82
+ # :extensions option, and the connection test all come after it), so
83
+ # that is where +db+ is assigned.
65
84
  def create_sequel_connection(opts)
85
+ db = nil
86
+ user_after_connect = opts[:after_connect]
87
+ user_before_preconnect = opts[:before_preconnect]
88
+
89
+ opts = opts.merge(
90
+ before_preconnect: proc do |database|
91
+ db = database
92
+ user_before_preconnect&.call(database)
93
+ end,
94
+ after_connect: proc do |conn, server|
95
+ if user_after_connect
96
+ user_after_connect.arity == 2 ? user_after_connect.call(conn, server) : user_after_connect.call(conn)
97
+ end
98
+ db.extension :db_opts
99
+ db.db_opts.apply(conn)
100
+ end,
101
+ )
102
+
66
103
  if (url = opts.delete(:uri) || opts.delete(:url))
67
104
  Sequel.connect(url, opts)
68
105
  else
69
- configure_adapter_specific_options(opts)
70
106
  Sequel.connect(opts)
71
107
  end
72
108
  end
73
-
74
- def configure_adapter_specific_options(opts)
75
- # No adapter-specific configuration needed currently
76
- end
77
-
78
109
  end
79
110
  end
@@ -1,10 +1,9 @@
1
- require 'dotenv'
1
+ require "dotenv"
2
2
 
3
3
  module Sequelizer
4
4
  # Creates a set of database configuration options from environment
5
5
  # variables
6
6
  class EnvConfig
7
-
8
7
  # Any environment variables in the .env file are loaded and then
9
8
  # any environment variable starting with SEQUELIZER_ will be used
10
9
  # as an option for the database
@@ -12,7 +11,7 @@ module Sequelizer
12
11
  Dotenv.load
13
12
 
14
13
  seq_config = ENV.keys.grep(/^SEQUELIZER_/).each_with_object({}) do |key, config|
15
- new_key = key.gsub(/^SEQUELIZER_/, '').downcase
14
+ new_key = key.gsub(/^SEQUELIZER_/, "").downcase
16
15
  config[new_key] = ENV.fetch(key, nil)
17
16
  end
18
17
 
@@ -23,6 +22,5 @@ module Sequelizer
23
22
 
24
23
  db_config.merge(seq_config)
25
24
  end
26
-
27
25
  end
28
26
  end
@@ -1,7 +1,6 @@
1
- require 'uri'
2
- require_relative 'yaml_config'
3
- require_relative 'env_config'
4
- require_relative 'options_hash'
1
+ require "uri"
2
+ require_relative "yaml_config"
3
+ require_relative "env_config"
5
4
 
6
5
  module Sequelizer
7
6
  # = Options
@@ -20,7 +19,7 @@ module Sequelizer
20
19
  # 1. Passed options (highest priority)
21
20
  # 2. .env file
22
21
  # 3. Environment variables
23
- # 4. config/database.yml
22
+ # 4. config/sequelizer.yml
24
23
  # 5. ~/.config/sequelizer/database.yml (lowest priority)
25
24
  #
26
25
  # @example Basic usage
@@ -32,16 +31,22 @@ module Sequelizer
32
31
  # options = Options.new
33
32
  # puts options.adapter # => 'postgres'
34
33
  class Options
35
-
36
34
  # @!attribute [r] extensions
37
35
  # @return [Array<Symbol>] list of Sequel extensions to load
38
36
  attr_reader :extensions
39
37
 
38
+ # The three names of the PostgreSQL search path, in the order that decides
39
+ # between them when one source sets more than one.
40
+ SEARCH_PATH_KEYS = %i[search_path schema_search_path schema].freeze
41
+
40
42
  # Creates a new Options instance, processing configuration from multiple sources.
41
43
  #
42
44
  # @param options [Hash, String, nil] database connection options or connection URL
43
45
  # If a Hash is provided, it will be merged with configuration from other sources.
44
- # If a String is provided, it's treated as a database URL and returned as-is.
46
+ # If a String is provided, it is a database URL, the way Sequel.connect accepts one:
47
+ # <tt>Options.new("postgres://host/db")</tt> is the same as
48
+ # <tt>Options.new(url: "postgres://host/db")</tt>, so it is merged with the other
49
+ # sources too.
45
50
  # If nil, configuration is loaded entirely from external sources.
46
51
  def initialize(options = nil)
47
52
  opts = fix_options(options)
@@ -50,13 +55,13 @@ module Sequelizer
50
55
 
51
56
  # Returns the processed options as a hash suitable for Sequel.connect.
52
57
  #
53
- # @return [Hash] the database connection options
58
+ # @return [Hash{Symbol => Object}] the database connection options, keyed by Symbol
54
59
  def to_hash
55
60
  @options
56
61
  end
57
62
 
58
63
  # Define accessor methods for common database options
59
- %w[adapter database username password search_path].each do |name|
64
+ %i[adapter database username password search_path].each do |name|
60
65
  # @!method #{name}
61
66
  # @return [String, nil] the #{name} option value
62
67
  define_method(name) do
@@ -66,77 +71,75 @@ module Sequelizer
66
71
 
67
72
  private
68
73
 
69
- # Creates an after_connect callback proc that handles custom callbacks
70
- # and applies database-specific options.
71
- #
72
- # @param opts [Hash] options hash that may contain an :after_connect callback
73
- # @return [Proc] callback to be executed after database connection
74
- def make_ac(opts)
75
- proc do |conn, server, db|
76
- if (ac = opts[:after_connect])
77
- ac.arity == 2 ? ac.call(conn, server) : ac.call(conn)
78
- end
79
- db.extension :db_opts
80
- db.db_opts.apply(conn)
81
- end
82
- end
83
-
84
74
  # Processes and fixes the passed options, handling various input types
85
75
  # and applying adapter-specific transformations.
86
76
  #
87
- # If passed a hash, scans hash for certain options and sets up hash
88
- # to be fed to Sequel.connect. Handles PostgreSQL schema setup and
89
- # timeout conversion.
77
+ # Scans the options for certain keys and sets up a hash to be fed to
78
+ # Sequel.connect. Handles PostgreSQL schema setup and timeout conversion.
79
+ # A String is a database URL and is processed as <tt>{ url: string }</tt>.
90
80
  #
91
- # If passed anything else (like a string that represents a database URL),
92
- # the value is returned without modification.
81
+ # For PostgreSQL, +search_path+, +schema_search_path+ and +schema+ are three
82
+ # names for one option. The highest-precedence source that sets any of them
83
+ # decides the search path; within that source, the names rank in the order of
84
+ # SEARCH_PATH_KEYS. The search path setup runs on each new connection, and
85
+ # then any +after_connect+ the caller passed.
93
86
  #
94
87
  # @param passed_options [Hash, String, nil] the options to process
95
- # @return [Hash, String] processed options or original string
88
+ # @return [Hash{Symbol => Object}] processed options
96
89
  def fix_options(passed_options)
97
- return passed_options unless passed_options.nil? || passed_options.is_a?(Hash)
98
-
99
- opts = OptionsHash.new(passed_options || {}).to_hash
100
- sequelizer_options = db_config(opts).merge(opts)
90
+ passed_options = { url: passed_options } if passed_options.is_a?(String)
91
+ opts = symbolize_keys(passed_options || {})
92
+ sources = [ opts, *config_sources(opts) ]
93
+ sequelizer_options = sources.reverse.reduce({}, :merge)
101
94
 
102
95
  adapter = sequelizer_options[:adapter]
103
96
  adapter ||= URI.parse(sequelizer_options[:url].to_s).scheme if sequelizer_options[:url]
104
97
 
105
98
  if adapter =~ /^postgres/
106
- sequelizer_options[:adapter] = 'postgres'
107
- paths = %w[search_path schema_search_path schema].map { |key| sequelizer_options.delete(key) }.compact
99
+ sequelizer_options[:adapter] = "postgres"
100
+ search_path = sources.filter_map { |source| source.values_at(*SEARCH_PATH_KEYS).compact.first }.first
101
+ SEARCH_PATH_KEYS.each { |key| sequelizer_options.delete(key) }
108
102
 
109
- unless paths.empty?
110
- sequelizer_options[:search_path] = paths.first
111
- sequelizer_options[:after_connect] = after_connect(paths.first)
103
+ if search_path
104
+ sequelizer_options[:search_path] = search_path
105
+ sequelizer_options[:after_connect] = after_connect(search_path, sequelizer_options[:after_connect])
112
106
  end
113
107
  end
114
108
 
115
- if sequelizer_options[:timeout]
116
- # I'm doing a merge! here because the indifferent access part
117
- # of OptionsHash seemed to not work when I tried
118
- # sequelizer_options[:timeout] = sequelizer_options[:timeout].to_i
119
- sequelizer_options.merge!(timeout: sequelizer_options[:timeout].to_i)
120
- end
109
+ sequelizer_options[:timeout] = sequelizer_options[:timeout].to_i if sequelizer_options[:timeout]
121
110
 
122
- sequelizer_options.merge(after_connect: make_ac(sequelizer_options))
111
+ sequelizer_options
123
112
  end
124
113
 
125
- # Loads database configuration from external sources in order of precedence.
126
- # Sources checked (in order):
127
- # - ~/.config/sequelizer.yml (if it exists and not ignored)
128
- # - config/database.yml (if it exists and not ignored)
129
- # - environment variables (including .env file if not ignored)
114
+ # Loads database configuration from the external sources that +opts+ does
115
+ # not ignore. They are read in this order:
116
+ # - ~/.config/sequelizer/database.yml (unless +:ignore_yaml+)
117
+ # - config/sequelizer.yml (unless +:ignore_yaml+)
118
+ # - environment variables, including the .env file (unless +:ignore_env+)
130
119
  #
131
- # @param opts [Hash] base options that may contain ignore flags
132
- # @return [OptionsHash] merged configuration from all sources
133
- def db_config(opts)
134
- @db_config ||= begin
135
- opts = OptionsHash.new(opts)
136
- opts.merge!(YamlConfig.user_config.options) unless opts[:ignore_yaml]
137
- opts.merge!(YamlConfig.local_config.options) unless opts[:ignore_yaml]
138
- opts.merge!(EnvConfig.new.options) unless opts[:ignore_env]
139
- opts
120
+ # @param opts [Hash] passed options that may contain ignore flags
121
+ # @return [Array<Hash{Symbol => Object}>] one hash per source, highest
122
+ # precedence first
123
+ def config_sources(opts)
124
+ sources = []
125
+ sources << YamlConfig.user_config.options unless opts[:ignore_yaml]
126
+ sources << YamlConfig.local_config.options unless opts[:ignore_yaml]
127
+ sources << EnvConfig.new.options unless opts[:ignore_env]
128
+ sources.reverse.map { |source| symbolize_keys(source) }
129
+ end
130
+
131
+ # Returns a copy of +value+ with every Hash key, at any depth, converted
132
+ # to a Symbol. Options arrive with String keys from YAML, ENV, and some
133
+ # callers; Sequel expects Symbols, including for nested hashes such as
134
+ # +:servers+, which it indexes by server name.
135
+ #
136
+ # @param value [Object] a Hash, an Array, or any other value
137
+ # @return [Object] the converted copy; the caller's objects are not modified
138
+ def symbolize_keys(value)
139
+ case value
140
+ when Hash then value.to_h { |k, v| [ k.to_sym, symbolize_keys(v) ] }
141
+ when Array then value.map { |v| symbolize_keys(v) }
142
+ else value
140
143
  end
141
144
  end
142
145
 
@@ -146,23 +149,29 @@ module Sequelizer
146
149
  # For PostgreSQL connections with a search_path defined, this proc will:
147
150
  # 1. Create each schema in the search path if it doesn't exist
148
151
  # 2. Set the search_path for the connection
152
+ # 3. Call +user_after_connect+, if given, with the connection (and the
153
+ # server, if it takes two arguments, as Sequel does)
149
154
  #
150
155
  # @param search_path [String, Array<String>] PostgreSQL schema names as a
151
156
  # comma-separated string, an Array of schema names, or a mixed Array
152
157
  # containing comma-separated strings
158
+ # @param user_after_connect [Proc, nil] the caller's own after_connect
153
159
  # @return [Proc] callback to execute after connection
154
160
  # @example String input
155
161
  # callback = after_connect('public,app_schema')
156
162
  # @example Array input
157
163
  # callback = after_connect(['public', 'app_schema'])
158
- def after_connect(search_path)
159
- schemas = Array(search_path).flat_map { |p| p.to_s.split(',') }.map(&:strip)
160
- path_string = schemas.join(', ')
161
- proc do |conn|
164
+ def after_connect(search_path, user_after_connect = nil)
165
+ schemas = Array(search_path).flat_map { |p| p.to_s.split(",") }.map(&:strip)
166
+ path_string = schemas.join(", ")
167
+ proc do |conn, server|
162
168
  schemas.each do |schema|
163
169
  conn.execute("CREATE SCHEMA IF NOT EXISTS #{schema}")
164
170
  end
165
171
  conn.execute("SET search_path TO #{path_string}")
172
+ if user_after_connect
173
+ user_after_connect.arity == 2 ? user_after_connect.call(conn, server) : user_after_connect.call(conn)
174
+ end
166
175
  end
167
176
  end
168
177
 
@@ -184,10 +193,9 @@ module Sequelizer
184
193
  extension_keys = options.keys.select { |k| k.to_s =~ extension_regexp }
185
194
  extensions = extension_keys.map do |key|
186
195
  options.delete(key)
187
- key.to_s.gsub(extension_regexp, '').to_sym
196
+ key.to_s.gsub(extension_regexp, "").to_sym
188
197
  end
189
- [options, extensions]
198
+ [ options, extensions ]
190
199
  end
191
-
192
200
  end
193
201
  end
@@ -1,6 +1,4 @@
1
1
  module Sequelizer
2
-
3
2
  # Version for the gem
4
- VERSION = '0.3.0'.freeze
5
-
3
+ VERSION = "0.4.0".freeze
6
4
  end
@@ -1,13 +1,11 @@
1
- require 'psych'
2
- require 'erb'
1
+ require "psych"
2
+ require "erb"
3
3
 
4
4
  module Sequelizer
5
5
  class YamlConfig
6
-
7
6
  attr_reader :config_file_path
8
7
 
9
8
  class << self
10
-
11
9
  def local_config
12
10
  new
13
11
  end
@@ -19,24 +17,24 @@ module Sequelizer
19
17
  def user_config_path
20
18
  return nil unless Dir.home
21
19
 
22
- Pathname.new(Dir.home).join('.config', 'sequelizer', 'database.yml')
20
+ Pathname.new(Dir.home).join(".config", "sequelizer", "database.yml")
23
21
  end
24
-
25
22
  end
26
23
 
27
24
  def initialize(config_file_path = nil)
28
- @config_file_path = Pathname.new(config_file_path || Pathname.pwd.join('config', 'sequelizer.yml')).expand_path
25
+ @config_file_path = Pathname.new(config_file_path || Pathname.pwd.join("config", "sequelizer.yml")).expand_path
29
26
  end
30
27
 
31
- # Returns a set of options pulled from config/database.yml
32
- # or +nil+ if config/database.yml doesn't exist
28
+ # Returns a set of options pulled from the YAML file at +config_file_path+
29
+ # (config/sequelizer.yml by default), or an empty hash if that file
30
+ # doesn't exist
33
31
  def options
34
32
  return {} unless config_file_path.exist?
35
33
 
36
34
  config[environment] || config
37
35
  end
38
36
 
39
- # The environment to load from database.yml
37
+ # The environment to load from the YAML file
40
38
  #
41
39
  # Searches the following environment variables in this order:
42
40
  # * SEQUELIZER_ENV
@@ -46,15 +44,14 @@ module Sequelizer
46
44
  # Lastly, if none of those environment variables are specified, the
47
45
  # environment defaults to 'development'
48
46
  def environment
49
- ENV['SEQUELIZER_ENV'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
47
+ ENV["SEQUELIZER_ENV"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
50
48
  end
51
49
 
52
50
  private
53
51
 
54
- # The config as read from config/database.yml
52
+ # The config as read from the YAML file at +config_file_path+
55
53
  def config
56
54
  @config ||= Psych.load(ERB.new(File.read(config_file_path)).result)
57
55
  end
58
-
59
56
  end
60
57
  end
data/lib/sequelizer.rb CHANGED
@@ -1,13 +1,12 @@
1
- require_relative 'sequelizer/version'
2
- require_relative 'sequelizer/connection_maker'
3
- require_relative 'sequelizer/monkey_patches/database_in_after_connect'
4
- require_relative 'sequel/extensions/db_opts'
5
- require_relative 'sequel/extensions/settable'
1
+ require_relative "sequelizer/version"
2
+ require_relative "sequelizer/connection_maker"
3
+ require_relative "sequel/extensions/db_opts"
4
+ require_relative "sequel/extensions/settable"
6
5
 
7
6
  # = Sequelizer
8
7
  #
9
8
  # Sequelizer is a Ruby gem that simplifies database connections using Sequel.
10
- # It allows users to configure database connections via config/database.yml
9
+ # It allows users to configure database connections via config/sequelizer.yml
11
10
  # or .env files, providing an easy-to-use interface for establishing database
12
11
  # connections without hardcoding sensitive information.
13
12
  #
@@ -30,7 +29,7 @@ require_relative 'sequel/extensions/settable'
30
29
  # 1. Passed options
31
30
  # 2. .env file
32
31
  # 3. Environment variables
33
- # 4. config/database.yml
32
+ # 4. config/sequelizer.yml
34
33
  # 5. ~/.config/sequelizer/database.yml
35
34
  #
36
35
  # == Examples
@@ -42,7 +41,6 @@ require_relative 'sequel/extensions/settable'
42
41
  # new_db(adapter: 'postgres', host: 'localhost')
43
42
  #
44
43
  module Sequelizer
45
-
46
44
  # Returns the default options hash for database connections.
47
45
  #
48
46
  # @return [Hash] the default connection options
@@ -54,7 +52,8 @@ module Sequelizer
54
52
  # the connection on the first call and then memoizes itself so only a single
55
53
  # connection is used on repeated calls.
56
54
  #
57
- # @param options [Hash] an optional set of database connection options.
55
+ # @param options [Hash, String] an optional set of database connection options,
56
+ # or a database URL string, which means { url: string }.
58
57
  # If no options are provided, options are read from config/sequelizer.yml
59
58
  # or from .env or from environment variables.
60
59
  # @return [Sequel::Database] the memoized database connection
@@ -68,7 +67,8 @@ module Sequelizer
68
67
 
69
68
  # Instantiates and returns a new database connection on each call.
70
69
  #
71
- # @param options [Hash] an optional set of database connection options.
70
+ # @param options [Hash, String] an optional set of database connection options,
71
+ # or a database URL string, which means { url: string }.
72
72
  # If no options are provided, options are read from config/sequelizer.yml
73
73
  # or from .env or from environment variables.
74
74
  # @return [Sequel::Database] a new database connection
@@ -78,6 +78,7 @@ module Sequelizer
78
78
  # conn2 = new_db # Different connection instance
79
79
  # new_db(force_new: true) # Bypasses cache entirely
80
80
  def new_db(options = {})
81
+ options = { url: options } if options.is_a?(String)
81
82
  cached = find_cached(options)
82
83
  return cached if cached && !options[:force_new]
83
84
 
@@ -86,11 +87,12 @@ module Sequelizer
86
87
 
87
88
  # Finds a cached connection for the given options.
88
89
  #
89
- # @param options [Hash] the connection options to look up
90
+ # @param options [Hash, String] the connection options to look up, or a
91
+ # database URL string, which means { url: string }
90
92
  # @return [Sequel::Database, nil] the cached connection or nil if not found
91
93
  def find_cached(options)
94
+ options = { url: options } if options.is_a?(String)
92
95
  @_sequelizer_cache ||= {}
93
96
  @_sequelizer_cache[options]
94
97
  end
95
-
96
98
  end