sequelizer 0.1.6 → 0.3.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -0
  3. data/LICENSE +21 -0
  4. data/README.md +28 -14
  5. data/config/platforms/base.csv +5 -0
  6. data/config/platforms/rdbms/athena.csv +4 -0
  7. data/config/platforms/rdbms/postgres.csv +3 -0
  8. data/config/platforms/rdbms/snowflake.csv +1 -0
  9. data/config/platforms/rdbms/spark.csv +3 -0
  10. data/lib/sequel/extensions/cold_col.rb +41 -17
  11. data/lib/sequel/extensions/funky.rb +136 -0
  12. data/lib/sequel/extensions/make_readyable.rb +18 -10
  13. data/lib/sequel/extensions/platform.rb +300 -0
  14. data/lib/sequel/extensions/smart_select_remove.rb +101 -0
  15. data/lib/sequelizer/options.rb +15 -6
  16. data/lib/sequelizer/version.rb +1 -1
  17. data/lib/sequelizer/yaml_config.rb +0 -1
  18. metadata +90 -57
  19. data/.claude/settings.local.json +0 -64
  20. data/.devcontainer/.p10k.zsh +0 -1713
  21. data/.devcontainer/.zshrc +0 -29
  22. data/.devcontainer/Dockerfile +0 -137
  23. data/.devcontainer/copy-claude-credentials.sh +0 -32
  24. data/.devcontainer/devcontainer.json +0 -102
  25. data/.devcontainer/init-firewall.sh +0 -123
  26. data/.devcontainer/setup-credentials.sh +0 -95
  27. data/.github/workflows/test.yml +0 -14
  28. data/.gitignore +0 -40
  29. data/.overcommit.yml +0 -73
  30. data/.rubocop.yml +0 -167
  31. data/CLAUDE.md +0 -219
  32. data/Gemfile +0 -12
  33. data/Gemfile.lock +0 -158
  34. data/Guardfile +0 -28
  35. data/LICENSE.txt +0 -22
  36. data/Rakefile +0 -35
  37. data/examples/database.with_one_set_of_options.yml +0 -15
  38. data/examples/database.yml +0 -30
  39. data/examples/dot_env.txt +0 -10
  40. data/sequelizer.gemspec +0 -35
  41. data/test/lib/sequel/extensions/test_cold_col.rb +0 -251
  42. data/test/lib/sequel/extensions/test_db_opts.rb +0 -42
  43. data/test/lib/sequel/extensions/test_make_readyable.rb +0 -259
  44. data/test/lib/sequel/extensions/test_more_sql.rb +0 -132
  45. data/test/lib/sequel/extensions/test_settable.rb +0 -109
  46. data/test/lib/sequel/extensions/test_sql_recorder.rb +0 -231
  47. data/test/lib/sequel/extensions/test_unionize.rb +0 -76
  48. data/test/lib/sequel/extensions/test_usable.rb +0 -15
  49. data/test/lib/sequelizer/test_connection_maker.rb +0 -123
  50. data/test/lib/sequelizer/test_env_config.rb +0 -37
  51. data/test/lib/sequelizer/test_gemfile_modifier.rb +0 -88
  52. data/test/lib/sequelizer/test_options.rb +0 -83
  53. data/test/lib/sequelizer/test_yaml_config.rb +0 -83
  54. data/test/test_helper.rb +0 -44
data/sequelizer.gemspec DELETED
@@ -1,35 +0,0 @@
1
- lib = File.expand_path('lib', __dir__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require 'sequelizer/version'
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = 'sequelizer'
7
- spec.version = Sequelizer::VERSION
8
- spec.authors = ['Ryan Duryea']
9
- spec.email = ['aguynamedryan@gmail.com']
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'
12
- spec.homepage = 'https://github.com/outcomesinsights/sequelizer'
13
- spec.license = 'MIT'
14
-
15
- spec.files = `git ls-files -z`.split("\x0")
16
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
- spec.require_paths = ['lib']
18
- spec.required_ruby_version = '>= 3.2.0'
19
-
20
- spec.add_development_dependency 'bundler', '~> 2.0'
21
- spec.add_development_dependency 'guard', '~> 2.0'
22
- spec.add_development_dependency 'guard-minitest', '~> 2.3'
23
- spec.add_development_dependency 'minitest', '~> 5.3'
24
- spec.add_development_dependency 'pg', '~> 1.0'
25
- spec.add_development_dependency 'rake', '~> 12.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'
30
- spec.add_dependency 'dotenv', '~> 2.1'
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'
35
- end
@@ -1,251 +0,0 @@
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
@@ -1,42 +0,0 @@
1
- require_relative '../../../test_helper'
2
- require 'sequel'
3
- require 'sequel/extensions/db_opts'
4
-
5
- class TestDbOpts < Minitest::Test
6
-
7
- def with_fake_database_type_and_options(db_type, opts = {})
8
- db = Sequel.mock
9
- db.define_singleton_method(:database_type) { db_type }
10
- db.define_singleton_method(:opts) { opts }
11
- db.extension :db_opts
12
- yield db
13
- end
14
-
15
- def sql_for(db_type, options)
16
- with_fake_database_type_and_options(db_type, options) do |db|
17
- db.synchronize do |conn|
18
- db.db_opts.apply(conn)
19
- return db.sqls
20
- end
21
- end
22
- end
23
-
24
- def test_should_detect_options_for_appropriate_db
25
- assert_equal(['SET flim=flam'], sql_for(:postgres, postgres_db_opt_flim: :flam))
26
- end
27
-
28
- def test_should_ignore_options_for_inappropriate_db
29
- assert_equal(['SET flim=flam'], sql_for(:postgres, postgres_db_opt_flim: :flam, other_db_opt_foo: :bar))
30
- end
31
-
32
- def test_should_ignore_non_db_opts
33
- assert_empty(sql_for(:postgres, postgres_flim: :flam))
34
- end
35
-
36
- def test_should_properly_quote_awkward_values
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'))
40
- end
41
-
42
- end
@@ -1,259 +0,0 @@
1
- require_relative '../../../test_helper'
2
- require 'fileutils'
3
- require 'tmpdir'
4
- require 'pathname'
5
- require 'sequel'
6
- require 'sequel/extensions/make_readyable'
7
-
8
- class TestUsable < Minitest::Test
9
-
10
- def setup
11
- # These features are mostly intended for Spark, but sqlite is a close enough
12
- # mock that we'll just roll with it
13
- @db = Sequel.mock(host: :spark)
14
- @db.extension :make_readyable
15
- def @db.tables(opts = {})
16
- case opts[:schema]
17
- when :schema1
18
- [:a]
19
- when :schema2
20
- %i[a b]
21
- when :schema3
22
- %i[a b]
23
- end
24
- end
25
- end
26
-
27
- def test_should_call_use_schema
28
- @db.make_ready(use_schema: :some_schema)
29
-
30
- assert_equal(['USE `some_schema`'], @db.sqls)
31
- end
32
-
33
- def test_should_create_views_based_on_tables_in_search_paths
34
- @db.make_ready(search_path: %i[schema1 schema2 schema3])
35
-
36
- assert_equal([
37
- 'CREATE TEMPORARY VIEW `a` AS SELECT * FROM `schema1`.`a`',
38
- 'CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`',
39
- ], @db.sqls)
40
- end
41
-
42
- def test_should_create_views_based_on_tables_in_search_paths_passed_as_strings
43
- @db.make_ready(search_path: %w[schema1 schema2 schema3])
44
-
45
- assert_equal([
46
- 'CREATE TEMPORARY VIEW `a` AS SELECT * FROM `schema1`.`a`',
47
- 'CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`',
48
- ], @db.sqls)
49
- end
50
-
51
- def test_should_create_views_based_on_tables_in_search_paths_accepts_except
52
- @db.make_ready(search_path: %i[schema1 schema2 schema3], except: :a)
53
-
54
- assert_equal([
55
- 'CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`',
56
- ], @db.sqls)
57
- end
58
-
59
- def test_should_create_views_based_on_tables_in_search_paths_accepts_only
60
- @db.make_ready(search_path: %i[schema1 schema2 schema3], only: :b)
61
-
62
- assert_equal([
63
- 'CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`',
64
- ], @db.sqls)
65
- end
66
-
67
- def test_should_create_views_based_on_path
68
- dir = Pathname.new(Dir.mktmpdir)
69
- a_file = dir.join('a.parquet')
70
- b_file = dir.join('b.parquet')
71
- FileUtils.touch(a_file.to_s)
72
- FileUtils.touch(b_file.to_s)
73
-
74
- @db.make_ready(search_path: [:schema1, a_file, b_file, :schema2])
75
- sqls = @db.sqls.dup
76
-
77
- assert_equal('CREATE TEMPORARY VIEW `a` AS SELECT * FROM `schema1`.`a`', sqls[0])
78
- assert_match(%r{CREATE TEMPORARY VIEW `b` USING parquet OPTIONS \('path'='/tmp/[^/]+/b.parquet'\)}, sqls[1])
79
- end
80
-
81
- def test_should_create_views_format_based_on_path
82
- dir = Pathname.new(Dir.mktmpdir)
83
- a_file = dir.join('a.parquet')
84
- b_file = dir.join('b.delta')
85
- c_file = dir.join('c.csv')
86
- FileUtils.touch(a_file.to_s)
87
- FileUtils.touch(b_file.to_s)
88
- FileUtils.touch(c_file.to_s)
89
-
90
- @db.make_ready(search_path: [a_file, b_file, c_file])
91
- sqls = @db.sqls.dup
92
-
93
- assert_match(%r{CREATE TEMPORARY VIEW `a` USING parquet OPTIONS \('path'='/tmp/[^/]+/a.parquet'\)}, sqls[0])
94
- assert_match(%r{CREATE TEMPORARY VIEW `b` USING delta OPTIONS \('path'='/tmp/[^/]+/b.delta'\)}, sqls[1])
95
- assert_match(%r{CREATE TEMPORARY VIEW `c` USING csv OPTIONS \('path'='/tmp/[^/]+/c.csv'\)}, sqls[2])
96
- end
97
-
98
- def test_should_create_a_single_view_if_multiple_files_have_the_same_name
99
- dir = Pathname.new(Dir.mktmpdir)
100
- a_file = dir.join('a.parquet')
101
- b_file = dir.join('a.delta')
102
- c_file = dir.join('a.csv')
103
- FileUtils.touch(a_file.to_s)
104
- FileUtils.touch(b_file.to_s)
105
- FileUtils.touch(c_file.to_s)
106
-
107
- @db.make_ready(search_path: [a_file, b_file, c_file])
108
- sqls = @db.sqls.dup
109
-
110
- assert_equal(1, sqls.size)
111
- assert_match(%r{CREATE TEMPORARY VIEW `a` USING parquet OPTIONS \('path'='/tmp/[^/]+/a.parquet'\)}, sqls[0])
112
- refute_match(%r{CREATE TEMPORARY VIEW `a` USING delta OPTIONS \('path'='/tmp/[^/]+/a.delta'\)}, sqls[1])
113
- refute_match(%r{CREATE TEMPORARY VIEW `a` USING csv OPTIONS \('path'='/tmp/[^/]+/a.csv'\)}, sqls[2])
114
- end
115
-
116
- def test_should_create_a_single_view_if_multiple_files_have_the_same_name_and_are_in_different_directories
117
- dir = Pathname.new(Dir.mktmpdir)
118
- a_file = dir / 'one' / 'a.parquet'
119
- b_file = dir / 'two' / 'a.delta'
120
- c_file = dir / 'three' / 'a.csv'
121
- FileUtils.mkdir_p(a_file.dirname)
122
- FileUtils.mkdir_p(b_file.dirname)
123
- FileUtils.mkdir_p(c_file.dirname)
124
- FileUtils.touch(a_file.to_s)
125
- FileUtils.touch(b_file.to_s)
126
- FileUtils.touch(c_file.to_s)
127
-
128
- @db.make_ready(search_path: [a_file, b_file, c_file])
129
- sqls = @db.sqls.dup
130
-
131
- assert_equal(1, sqls.size)
132
- assert_match(%r{CREATE TEMPORARY VIEW `a` USING parquet OPTIONS \('path'='/tmp/[^/]+/one/a.parquet'\)}, sqls[0])
133
- refute_match(%r{CREATE TEMPORARY VIEW `a` USING delta OPTIONS \('path'='/tmp/[^/]+/two/a.delta'\)}, sqls[1])
134
- refute_match(%r{CREATE TEMPORARY VIEW `a` USING csv OPTIONS \('path'='/tmp/[^/]+/three/a.csv'\)}, sqls[2])
135
- end
136
-
137
- def test_should_create_view_from_compact_style_path
138
- dir = Pathname.new(Dir.mktmpdir)
139
- a_file = dir / 'one' / 'a.parquet'
140
- b_file = dir / 'two' / 'b.delta'
141
- c_file = dir / 'three' / 'c.csv'
142
- FileUtils.mkdir_p(a_file.dirname)
143
- FileUtils.mkdir_p(b_file.dirname)
144
- FileUtils.mkdir_p(c_file.dirname)
145
- FileUtils.touch(a_file.to_s)
146
- FileUtils.touch(b_file.to_s)
147
- FileUtils.touch(c_file.to_s)
148
-
149
- @db.make_ready(search_path: [Dir["#{dir}/{one,two,three}"].map { |path| Pathname.new(path).glob('*') }])
150
- sqls = @db.sqls.dup
151
-
152
- assert_equal(3, sqls.size)
153
- assert_match(%r{CREATE TEMPORARY VIEW `a` USING parquet OPTIONS \('path'='/tmp/[^/]+/one/a.parquet'\)}, sqls[0])
154
- assert_match(%r{CREATE TEMPORARY VIEW `b` USING delta OPTIONS \('path'='/tmp/[^/]+/two/b.delta'\)}, sqls[1])
155
- assert_match(%r{CREATE TEMPORARY VIEW `c` USING csv OPTIONS \('path'='/tmp/[^/]+/three/c.csv'\)}, sqls[2])
156
- end
157
-
158
- def test_should_create_view_from_compact_style_path_with_multiple_files
159
- dir = Pathname.new(Dir.mktmpdir)
160
- a_file = dir / 'one' / 'a.parquet'
161
- b_file = dir / 'two' / 'a.delta'
162
- c_file = dir / 'three' / 'a.csv'
163
- FileUtils.mkdir_p(a_file.dirname)
164
- FileUtils.mkdir_p(b_file.dirname)
165
- FileUtils.mkdir_p(c_file.dirname)
166
- FileUtils.touch(a_file.to_s)
167
- FileUtils.touch(b_file.to_s)
168
- FileUtils.touch(c_file.to_s)
169
-
170
- @db.make_ready(search_path: [Dir["#{dir}/{one,two,three}"].map { |path| Pathname.new(path).glob('*') }])
171
- sqls = @db.sqls.dup
172
-
173
- assert_equal(1, sqls.size)
174
- assert_match(%r{CREATE TEMPORARY VIEW `a` USING parquet OPTIONS \('path'='/tmp/[^/]+/one/a.parquet'\)}, sqls[0])
175
- refute_match(%r{CREATE TEMPORARY VIEW `a` USING delta OPTIONS \('path'='/tmp/[^/]+/two/a.delta'\)}, sqls[1])
176
- refute_match(%r{CREATE TEMPORARY VIEW `a` USING csv OPTIONS \('path'='/tmp/[^/]+/three/a.csv'\)}, sqls[2])
177
- end
178
-
179
- def test_duckdb_external_file_support
180
- # Test DuckDB - uses read_* functions for external files
181
- duckdb_db = Sequel.mock
182
- duckdb_db.extension :make_readyable
183
- def duckdb_db.database_type
184
- :duckdb
185
- end
186
-
187
- dir = Pathname.new(Dir.mktmpdir)
188
- parquet_file = dir / 'test.parquet'
189
- csv_file = dir / 'test.csv'
190
- json_file = dir / 'test.json'
191
- FileUtils.touch(parquet_file.to_s)
192
- FileUtils.touch(csv_file.to_s)
193
- FileUtils.touch(json_file.to_s)
194
-
195
- # Test parquet file
196
- duckdb_db.make_ready(search_path: [parquet_file])
197
-
198
- assert_match(/CREATE VIEW \w*test\w* AS SELECT \* FROM read_parquet\('.*test\.parquet'\)/, duckdb_db.sqls.last)
199
-
200
- # Test CSV file
201
- duckdb_db.sqls.clear
202
- duckdb_db.make_ready(search_path: [csv_file])
203
-
204
- assert_match(/CREATE VIEW \w*test\w* AS SELECT \* FROM read_csv_auto\('.*test\.csv'\)/, duckdb_db.sqls.last)
205
-
206
- # Test JSON file
207
- duckdb_db.sqls.clear
208
- duckdb_db.make_ready(search_path: [json_file])
209
-
210
- assert_match(/CREATE VIEW \w*test\w* AS SELECT \* FROM read_json_auto\('.*test\.json'\)/, duckdb_db.sqls.last)
211
- end
212
-
213
- def test_unsupported_file_format_for_duckdb
214
- # Test unsupported file format for DuckDB
215
- duckdb_db = Sequel.mock
216
- duckdb_db.extension :make_readyable
217
- def duckdb_db.database_type
218
- :duckdb
219
- end
220
-
221
- dir = Pathname.new(Dir.mktmpdir)
222
- orc_file = dir / 'test.orc'
223
- FileUtils.touch(orc_file.to_s)
224
-
225
- error = assert_raises(Sequel::Error) do
226
- duckdb_db.make_ready(search_path: [orc_file])
227
- end
228
- assert_match(/Unsupported file format 'orc' for DuckDB/, error.message)
229
- end
230
-
231
- def test_duckdb_directory_support
232
- # Test DuckDB with directory paths (globbing)
233
- duckdb_db = Sequel.mock
234
- duckdb_db.extension :make_readyable
235
- def duckdb_db.database_type
236
- :duckdb
237
- end
238
-
239
- # Mock directory? to return true
240
- dir = Pathname.new(Dir.mktmpdir)
241
- def dir.directory?
242
- true
243
- end
244
-
245
- file_sourcerer = Sequel::ReadyMaker::FileSourcerer.new(duckdb_db, dir)
246
-
247
- # Override format to return parquet
248
- def file_sourcerer.format
249
- 'parquet'
250
- end
251
-
252
- # For directory support, DuckDB should use globbing pattern
253
- file_sourcerer.create_view(:test_table)
254
-
255
- assert_match(%r{CREATE VIEW \w*test_table\w* AS SELECT \* FROM read_parquet\('.*/\*\*/\*\.parquet'\)},
256
- duckdb_db.sqls.last)
257
- end
258
-
259
- end