sequelizer 0.1.4 → 0.1.6

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/settings.local.json +64 -0
  3. data/.devcontainer/.p10k.zsh +1713 -0
  4. data/.devcontainer/.zshrc +29 -0
  5. data/.devcontainer/Dockerfile +137 -0
  6. data/.devcontainer/copy-claude-credentials.sh +32 -0
  7. data/.devcontainer/devcontainer.json +102 -0
  8. data/.devcontainer/init-firewall.sh +123 -0
  9. data/.devcontainer/setup-credentials.sh +95 -0
  10. data/.github/workflows/test.yml +1 -1
  11. data/.gitignore +6 -1
  12. data/.overcommit.yml +73 -0
  13. data/.rubocop.yml +167 -0
  14. data/CHANGELOG.md +24 -0
  15. data/CLAUDE.md +219 -0
  16. data/Gemfile +6 -2
  17. data/Gemfile.lock +158 -0
  18. data/Guardfile +1 -1
  19. data/Rakefile +28 -3
  20. data/lib/sequel/extensions/cold_col.rb +436 -0
  21. data/lib/sequel/extensions/db_opts.rb +65 -4
  22. data/lib/sequel/extensions/make_readyable.rb +148 -30
  23. data/lib/sequel/extensions/more_sql.rb +76 -0
  24. data/lib/sequel/extensions/settable.rb +64 -0
  25. data/lib/sequel/extensions/sql_recorder.rb +85 -0
  26. data/lib/sequel/extensions/unionize.rb +169 -0
  27. data/lib/sequel/extensions/usable.rb +30 -1
  28. data/lib/sequelizer/cli.rb +61 -18
  29. data/lib/sequelizer/connection_maker.rb +54 -72
  30. data/lib/sequelizer/env_config.rb +6 -6
  31. data/lib/sequelizer/gemfile_modifier.rb +23 -21
  32. data/lib/sequelizer/monkey_patches/database_in_after_connect.rb +7 -5
  33. data/lib/sequelizer/options.rb +97 -18
  34. data/lib/sequelizer/options_hash.rb +2 -0
  35. data/lib/sequelizer/version.rb +3 -1
  36. data/lib/sequelizer/yaml_config.rb +9 -3
  37. data/lib/sequelizer.rb +65 -9
  38. data/sequelizer.gemspec +12 -7
  39. data/test/lib/sequel/extensions/test_cold_col.rb +251 -0
  40. data/test/lib/sequel/extensions/test_db_opts.rb +10 -8
  41. data/test/lib/sequel/extensions/test_make_readyable.rb +199 -28
  42. data/test/lib/sequel/extensions/test_more_sql.rb +132 -0
  43. data/test/lib/sequel/extensions/test_settable.rb +109 -0
  44. data/test/lib/sequel/extensions/test_sql_recorder.rb +231 -0
  45. data/test/lib/sequel/extensions/test_unionize.rb +76 -0
  46. data/test/lib/sequel/extensions/test_usable.rb +5 -2
  47. data/test/lib/sequelizer/test_connection_maker.rb +21 -17
  48. data/test/lib/sequelizer/test_env_config.rb +5 -2
  49. data/test/lib/sequelizer/test_gemfile_modifier.rb +7 -6
  50. data/test/lib/sequelizer/test_options.rb +14 -9
  51. data/test/lib/sequelizer/test_yaml_config.rb +13 -12
  52. data/test/test_helper.rb +36 -8
  53. metadata +107 -28
  54. data/lib/sequel/extensions/sqls.rb +0 -31
@@ -4,9 +4,11 @@ require 'erb'
4
4
 
5
5
  module Sequelizer
6
6
  class YamlConfig
7
+
7
8
  attr_reader :config_file_path
8
9
 
9
10
  class << self
11
+
10
12
  def local_config
11
13
  new
12
14
  end
@@ -16,19 +18,22 @@ module Sequelizer
16
18
  end
17
19
 
18
20
  def user_config_path
19
- return nil unless ENV['HOME']
20
- Pathname.new(ENV['HOME']) + ".config" + "sequelizer" + "database.yml"
21
+ return nil unless Dir.home
22
+
23
+ Pathname.new(Dir.home).join('.config', 'sequelizer', 'database.yml')
21
24
  end
25
+
22
26
  end
23
27
 
24
28
  def initialize(config_file_path = nil)
25
- @config_file_path = Pathname.new(config_file_path || Pathname.pwd + "config" + "sequelizer.yml").expand_path
29
+ @config_file_path = Pathname.new(config_file_path || Pathname.pwd.join('config', 'sequelizer.yml')).expand_path
26
30
  end
27
31
 
28
32
  # Returns a set of options pulled from config/database.yml
29
33
  # or +nil+ if config/database.yml doesn't exist
30
34
  def options
31
35
  return {} unless config_file_path.exist?
36
+
32
37
  config[environment] || config
33
38
  end
34
39
 
@@ -51,5 +56,6 @@ module Sequelizer
51
56
  def config
52
57
  @config ||= Psych.load(ERB.new(File.read(config_file_path)).result)
53
58
  end
59
+
54
60
  end
55
61
  end
data/lib/sequelizer.rb CHANGED
@@ -4,37 +4,93 @@ require_relative 'sequelizer/monkey_patches/database_in_after_connect'
4
4
  require_relative 'sequel/extensions/db_opts'
5
5
  require_relative 'sequel/extensions/settable'
6
6
 
7
+ # = Sequelizer
8
+ #
9
+ # Sequelizer is a Ruby gem that simplifies database connections using Sequel.
10
+ # It allows users to configure database connections via config/database.yml
11
+ # or .env files, providing an easy-to-use interface for establishing database
12
+ # connections without hardcoding sensitive information.
13
+ #
14
+ # == Usage
15
+ #
7
16
  # Include this module in any class where you'd like to quickly establish
8
- # a Sequel connection to a database.
17
+ # a Sequel connection to a database:
18
+ #
19
+ # class MyClass
20
+ # include Sequelizer
21
+ #
22
+ # def some_method
23
+ # db[:users].all # Uses cached connection
24
+ # end
25
+ # end
26
+ #
27
+ # == Configuration Sources
28
+ #
29
+ # Configuration is loaded from multiple sources in order of precedence:
30
+ # 1. Passed options
31
+ # 2. .env file
32
+ # 3. Environment variables
33
+ # 4. config/database.yml
34
+ # 5. ~/.config/sequelizer/database.yml
35
+ #
36
+ # == Examples
37
+ #
38
+ # # Use cached connection
39
+ # db[:users].all
40
+ #
41
+ # # Create new connection with custom options
42
+ # new_db(adapter: 'postgres', host: 'localhost')
43
+ #
9
44
  module Sequelizer
45
+
46
+ # Returns the default options hash for database connections.
47
+ #
48
+ # @return [Hash] the default connection options
10
49
  def self.options
11
50
  Options.new.to_hash
12
51
  end
13
52
 
14
- # Instantiates and memoizes a database connection. The +db+ method instantiates
53
+ # Instantiates and memoizes a database connection. The +db+ method instantiates
15
54
  # the connection on the first call and then memoizes itself so only a single
16
- # connection is used on repeated calls
55
+ # connection is used on repeated calls.
17
56
  #
18
- # options :: an optional set of database connection options.
19
- # If no options are provided, options are read from
20
- # config/sequelizer.yml or from .env or from environment variables.
57
+ # @param options [Hash] an optional set of database connection options.
58
+ # If no options are provided, options are read from config/sequelizer.yml
59
+ # or from .env or from environment variables.
60
+ # @return [Sequel::Database] the memoized database connection
61
+ #
62
+ # @example
63
+ # db[:users].all # Uses cached connection
64
+ # db(adapter: 'postgres')[:products].count
21
65
  def db(options = {})
22
66
  @_sequelizer_db ||= new_db(options)
23
67
  end
24
68
 
25
69
  # Instantiates and returns a new database connection on each call.
26
70
  #
27
- # options :: an optional set of database connection options.
28
- # If no options are provided, options are read from
29
- # config/sequelizer.yml or from .env or from environment variables.
71
+ # @param options [Hash] an optional set of database connection options.
72
+ # If no options are provided, options are read from config/sequelizer.yml
73
+ # or from .env or from environment variables.
74
+ # @return [Sequel::Database] a new database connection
75
+ #
76
+ # @example
77
+ # conn1 = new_db
78
+ # conn2 = new_db # Different connection instance
79
+ # new_db(force_new: true) # Bypasses cache entirely
30
80
  def new_db(options = {})
31
81
  cached = find_cached(options)
32
82
  return cached if cached && !options[:force_new]
83
+
33
84
  @_sequelizer_cache[options] = ConnectionMaker.new(options).connection
34
85
  end
35
86
 
87
+ # Finds a cached connection for the given options.
88
+ #
89
+ # @param options [Hash] the connection options to look up
90
+ # @return [Sequel::Database, nil] the cached connection or nil if not found
36
91
  def find_cached(options)
37
92
  @_sequelizer_cache ||= {}
38
93
  @_sequelizer_cache[options]
39
94
  end
95
+
40
96
  end
data/sequelizer.gemspec CHANGED
@@ -1,5 +1,4 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
3
  require 'sequelizer/version'
5
4
 
@@ -8,23 +7,29 @@ Gem::Specification.new do |spec|
8
7
  spec.version = Sequelizer::VERSION
9
8
  spec.authors = ['Ryan Duryea']
10
9
  spec.email = ['aguynamedryan@gmail.com']
11
- spec.summary = %q{Sequel database connections via config/database.yml or .env}
12
- spec.description = %q{Easily establish a connection to a database via Sequel gem using options specified in config/database.yml or .env files}
10
+ spec.summary = 'Sequel database connections via config/database.yml or .env'
11
+ spec.description = 'Easily establish a connection to a database via Sequel gem using options specified in config/database.yml or .env files'
13
12
  spec.homepage = 'https://github.com/outcomesinsights/sequelizer'
14
13
  spec.license = 'MIT'
15
14
 
16
15
  spec.files = `git ls-files -z`.split("\x0")
17
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
17
  spec.require_paths = ['lib']
18
+ spec.required_ruby_version = '>= 3.2.0'
20
19
 
21
20
  spec.add_development_dependency 'bundler', '~> 2.0'
22
21
  spec.add_development_dependency 'guard', '~> 2.0'
23
22
  spec.add_development_dependency 'guard-minitest', '~> 2.3'
24
23
  spec.add_development_dependency 'minitest', '~> 5.3'
24
+ spec.add_development_dependency 'pg', '~> 1.0'
25
25
  spec.add_development_dependency 'rake', '~> 12.0'
26
- spec.add_dependency 'sequel', '~> 5.0'
26
+ spec.add_development_dependency 'rubocop', '~> 1.0'
27
+ spec.add_development_dependency 'rubocop-minitest', '~> 0.25'
28
+ spec.add_development_dependency 'simplecov', '~> 0.22'
29
+ spec.add_dependency 'activesupport', '~> 7.0'
27
30
  spec.add_dependency 'dotenv', '~> 2.1'
28
- spec.add_dependency 'thor', '~> 1.0'
29
31
  spec.add_dependency 'hashie', '~> 3.2'
32
+ spec.add_dependency 'sequel', '~> 5.93'
33
+ spec.add_dependency 'thor', '~> 1.0'
34
+ spec.metadata['rubygems_mfa_required'] = 'true'
30
35
  end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../test_helper'
4
+ require_relative '../../../../lib/sequel/extensions/cold_col'
5
+
6
+ describe Sequel::ColdColDatabase do
7
+ let(:db) do
8
+ @db = Sequel.mock.extension(:cold_col)
9
+ @db.cold_col_registry.set_schemas({
10
+ Sequel.lit('tab1') => [[:col1]],
11
+ Sequel.lit('tab2') => [[:col2]],
12
+ Sequel.lit('tab3') => [[:col3], [:col4]],
13
+ Sequel.lit('q.tab4') => [[:col5]],
14
+ })
15
+ @db.extend_datasets do
16
+ def supports_cte?
17
+ true
18
+ end
19
+ end
20
+
21
+ # Add with method to mock database to support CTEs
22
+ def @db.with(*args)
23
+ dataset.with(*args)
24
+ end
25
+
26
+ @db
27
+ end
28
+
29
+ def expect_columns(ds, *cols)
30
+ _(ds.columns).must_equal(cols)
31
+ end
32
+
33
+ it 'should know columns from select * FROM tab' do
34
+ expect_columns(db[:tab1], :col1)
35
+ end
36
+
37
+ it 'should know columns after append' do
38
+ expect_columns(db[:tab1].select_append(Sequel.function(:min, :col1).as(:mini)), :col1, :mini)
39
+ end
40
+
41
+ it 'should know columns after select_all' do
42
+ expect_columns(db[:tab1].select_all, :col1)
43
+ end
44
+
45
+ it 'should know columns after select_all(:tab1)' do
46
+ expect_columns(db[:tab1].select_all(:tab1), :col1)
47
+ end
48
+
49
+ it 'should know columns after from_self' do
50
+ expect_columns(db[:tab1].from_self, :col1)
51
+ end
52
+
53
+ it 'should know columns after a CTE' do
54
+ ds = db[:cte1]
55
+ .with(:cte1, db[:tab1])
56
+ expect_columns(ds, :col1)
57
+ end
58
+
59
+ it 'should know columns after a JOIN' do
60
+ ds = db[:tab1]
61
+ .join(:tab2)
62
+ expect_columns(ds, :col1, :col2)
63
+ end
64
+
65
+ it 'should know columns after a different kind of JOIN' do
66
+ ds = db[:tab1]
67
+ .join(db[:tab2])
68
+ expect_columns(ds, :col1, :col2)
69
+ end
70
+
71
+ it 'should know columns from a JOIN and CTE' do
72
+ ds = db[:tab1]
73
+ .with(:cte1, db[:tab2])
74
+ .join(db[:cte1])
75
+ expect_columns(ds, :col1, :col2)
76
+ end
77
+
78
+ it 'should know columns from a select_all JOIN' do
79
+ ds = db[:tab1]
80
+ .join(db[:tab2], { Sequel[:tab1][:col1] => Sequel[:tab2][:col3] })
81
+ .select_all(:tab1)
82
+ expect_columns(ds, :col1)
83
+ end
84
+
85
+ it 'should know columns from an aliased select_all JOIN' do
86
+ ds = db[:tab1].from_self(alias: :l)
87
+ .join(db[:tab2], { col3: :col1 })
88
+ .select_all(:l)
89
+ expect_columns(ds, :col1)
90
+ end
91
+
92
+ it 'should know columns from an aliased select_all and added rhs column JOIN' do
93
+ ds = db[:tab1].from_self(alias: :l)
94
+ .join(db[:tab2], { col3: :col1 }, table_alias: :r)
95
+ .select_all(:l)
96
+ .select_append(Sequel[:r][:col4])
97
+ expect_columns(ds, :col1, :col4)
98
+ end
99
+
100
+ it 'should know columns from an aliased select_all rhs JOIN' do
101
+ ds = db[:tab1].from_self(alias: :l)
102
+ .join(db[:tab2], { col3: :col1 }, table_alias: :r)
103
+ .select_all(:r)
104
+ expect_columns(ds, :col2)
105
+ end
106
+
107
+ it 'should know columns from a directly aliased select_all rhs JOIN' do
108
+ ds = db[:tab1].from_self(alias: :l)
109
+ .join(:tab2, { col3: :col1 }, table_alias: :r)
110
+ .select_all(:r)
111
+ expect_columns(ds, :col2)
112
+ end
113
+
114
+ it 'should know columns from a a qualified JOIN' do
115
+ ds = db[:tab1].from_self(alias: :l)
116
+ .join(Sequel[:q][:tab4], { col3: :col1 }, table_alias: :r)
117
+ .select_all(:r)
118
+ expect_columns(ds, :col5)
119
+ end
120
+ it 'should remember columns from ctas' do
121
+ db.create_table(:ctas_table, as: db.select(Sequel[1].as(:a)))
122
+ ds = db[:ctas_table]
123
+ expect_columns(ds, :a)
124
+ end
125
+
126
+ it 'should remember columns from create table' do
127
+ db.create_table(:ddl_table) do
128
+ String :a
129
+ end
130
+ ds = db[:ddl_table]
131
+ expect_columns(ds, :a)
132
+ end
133
+
134
+ it 'should remember columns from view' do
135
+ db.create_view(:ctas_view, db.select(Sequel[1].as(:a)))
136
+ ds = db[:ctas_view]
137
+ expect_columns(ds, :a)
138
+ end
139
+
140
+ it 'should ignore columns when asked, thus avoiding an issue with string-only SQL' do
141
+ assert_raises(NoMethodError) { db.create_view(:ctas_view, 'SELECT 1 AS A') }
142
+ db.create_view(:ctas_view, 'SELECT 1 AS A', dont_record: true)
143
+ end
144
+
145
+ it 'should handle load_schema with empty file' do
146
+ require 'tempfile'
147
+ require 'yaml'
148
+
149
+ Tempfile.create(['schema', '.yml']) do |f|
150
+ f.write({}.to_yaml)
151
+ f.flush
152
+
153
+ db.load_schema(f.path)
154
+ # Should not raise error and schemas should remain unchanged
155
+ expect_columns(db[:tab1], :col1)
156
+ end
157
+ end
158
+
159
+ it 'should handle add_table_schema with symbol and string table names' do
160
+ db.add_table_schema(:new_table, [[:col_a, {}], [:col_b, {}]])
161
+ db.add_table_schema('string_table', [[:col_c, {}]])
162
+
163
+ expect_columns(db[:new_table], :col_a, :col_b)
164
+ expect_columns(db[:string_table], :col_c)
165
+ end
166
+
167
+ it 'should handle complex nested CTEs' do
168
+ ds = db.with(:cte1, db[:tab1])
169
+ .with(:cte2, db[:cte1].select(:col1))
170
+ .from(:cte2)
171
+ expect_columns(ds, :col1)
172
+ end
173
+
174
+ it 'should handle qualified table names in schema' do
175
+ expect_columns(db[Sequel[:q][:tab4]], :col5)
176
+ end
177
+
178
+ it 'should handle aliased expressions in select' do
179
+ ds = db[:tab1].select(Sequel[:col1].as(:renamed_col))
180
+ expect_columns(ds, :renamed_col)
181
+ end
182
+
183
+ it 'should handle function calls with aliases' do
184
+ ds = db[:tab1].select(Sequel.function(:count, :col1).as(:count_col1))
185
+ expect_columns(ds, :count_col1)
186
+ end
187
+
188
+ it 'should handle multiple table joins with mixed syntax' do
189
+ ds = db[:tab1]
190
+ .join(:tab2, { col2: :col1 })
191
+ .join(db[:tab3].as(:t3), { col3: :col1 })
192
+ expect_columns(ds, :col1, :col2, :col3, :col4)
193
+ end
194
+
195
+ it 'should handle recursive schema merging with load_schema' do
196
+ require 'tempfile'
197
+ require 'yaml'
198
+
199
+ # First schema file
200
+ Tempfile.create(['schema1', '.yml']) do |f1|
201
+ f1.write({ 'initial_table' => { columns: { 'col_x' => {} } } }.to_yaml)
202
+ f1.flush
203
+ db.load_schema(f1.path)
204
+
205
+ # Second schema file
206
+ Tempfile.create(['schema2', '.yml']) do |f2|
207
+ f2.write({ 'second_table' => { columns: { 'col_y' => {} } } }.to_yaml)
208
+ f2.flush
209
+ db.load_schema(f2.path)
210
+
211
+ # Both schemas should be available
212
+ expect_columns(db[:initial_table], :col_x)
213
+ expect_columns(db[:second_table], :col_y)
214
+ end
215
+ end
216
+ end
217
+
218
+ # Additional tests to ensure refactoring preserves behavior
219
+ describe 'Internal schema lookup behavior' do
220
+ it 'should prioritize created tables over schemas' do
221
+ db.add_table_schema(:priority_test, [[:schema_col, {}]])
222
+ db.create_table(:priority_test) do
223
+ String :created_col
224
+ end
225
+ expect_columns(db[:priority_test], :created_col)
226
+ end
227
+
228
+ it 'should handle deeply nested WITH clauses' do
229
+ ds = db[:cte1]
230
+ .with(:cte1, db[:tab1])
231
+ .with(:cte2, db[:cte1])
232
+ .with(:cte3, db[:cte2])
233
+ .from(:cte3)
234
+ expect_columns(ds, :col1)
235
+ end
236
+
237
+ it 'should handle mixed aliased and non-aliased sources' do
238
+ ds = db[:tab1].from_self(alias: :t1)
239
+ .join(:tab2, { col2: :col1 })
240
+ .join(db[:tab3].as(:t3), { col3: :col1 })
241
+ .select_all(:t1)
242
+ .select_append(Sequel[:t3][:col4])
243
+ expect_columns(ds, :col1, :col4)
244
+ end
245
+
246
+ it 'should handle column lookup with empty select lists' do
247
+ ds = db.from(db[:tab1].where(id: 1))
248
+ expect_columns(ds, :col1)
249
+ end
250
+ end
251
+ end
@@ -3,10 +3,11 @@ require 'sequel'
3
3
  require 'sequel/extensions/db_opts'
4
4
 
5
5
  class TestDbOpts < Minitest::Test
6
+
6
7
  def with_fake_database_type_and_options(db_type, opts = {})
7
8
  db = Sequel.mock
8
- db.define_singleton_method(:database_type){db_type}
9
- db.define_singleton_method(:opts){opts}
9
+ db.define_singleton_method(:database_type) { db_type }
10
+ db.define_singleton_method(:opts) { opts }
10
11
  db.extension :db_opts
11
12
  yield db
12
13
  end
@@ -21,20 +22,21 @@ class TestDbOpts < Minitest::Test
21
22
  end
22
23
 
23
24
  def test_should_detect_options_for_appropriate_db
24
- assert_equal(sql_for(:postgres, postgres_db_opt_flim: :flam), ["SET flim=flam"])
25
+ assert_equal(['SET flim=flam'], sql_for(:postgres, postgres_db_opt_flim: :flam))
25
26
  end
26
27
 
27
28
  def test_should_ignore_options_for_inappropriate_db
28
- assert_equal(sql_for(:postgres, postgres_db_opt_flim: :flam, other_db_opt_foo: :bar), ["SET flim=flam"])
29
+ assert_equal(['SET flim=flam'], sql_for(:postgres, postgres_db_opt_flim: :flam, other_db_opt_foo: :bar))
29
30
  end
30
31
 
31
32
  def test_should_ignore_non_db_opts
32
- assert_equal(sql_for(:postgres, postgres_flim: :flam), [])
33
+ assert_empty(sql_for(:postgres, postgres_flim: :flam))
33
34
  end
34
35
 
35
36
  def test_should_properly_quote_awkward_values
36
- assert_equal(sql_for(:postgres, postgres_db_opt_str: "hello there", postgres_db_opt_hyphen: "i-like-hyphens-though-they-are-dumb"),
37
- ["SET str='hello there'", "SET hyphen='i-like-hyphens-though-they-are-dumb'"])
37
+ assert_equal(["SET str='hello there'", "SET hyphen='i-like-hyphens-though-they-are-dumb'"],
38
+ sql_for(:postgres, postgres_db_opt_str: 'hello there',
39
+ postgres_db_opt_hyphen: 'i-like-hyphens-though-they-are-dumb'))
38
40
  end
39
- end
40
41
 
42
+ end