sequelizer 0.1.4 → 0.2.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.beads/.gitignore +54 -0
  3. data/.beads/.jsonl.lock +0 -0
  4. data/.beads/.migration-hint-ts +1 -0
  5. data/.beads/README.md +81 -0
  6. data/.beads/config.yaml +42 -0
  7. data/.beads/issues.jsonl +20 -0
  8. data/.beads/metadata.json +7 -0
  9. data/.coderabbit.yaml +94 -0
  10. data/.devcontainer/.p10k.zsh +1713 -0
  11. data/.devcontainer/.zshrc +29 -0
  12. data/.devcontainer/Dockerfile +137 -0
  13. data/.devcontainer/copy-claude-credentials.sh +32 -0
  14. data/.devcontainer/devcontainer.json +102 -0
  15. data/.devcontainer/init-firewall.sh +123 -0
  16. data/.devcontainer/setup-credentials.sh +95 -0
  17. data/.github/dependabot.yml +18 -0
  18. data/.github/workflows/dependabot-auto-merge.yml +36 -0
  19. data/.github/workflows/test.yml +44 -9
  20. data/.gitignore +6 -1
  21. data/.overcommit.yml +73 -0
  22. data/.rubocop.yml +167 -0
  23. data/AGENTS.md +126 -0
  24. data/CHANGELOG.md +41 -0
  25. data/CLAUDE.md +230 -0
  26. data/Gemfile +6 -2
  27. data/Gemfile.lock +189 -0
  28. data/Guardfile +1 -1
  29. data/Rakefile +28 -3
  30. data/config/platforms/base.csv +5 -0
  31. data/config/platforms/rdbms/athena.csv +4 -0
  32. data/config/platforms/rdbms/postgres.csv +3 -0
  33. data/config/platforms/rdbms/snowflake.csv +1 -0
  34. data/config/platforms/rdbms/spark.csv +3 -0
  35. data/lib/sequel/extensions/cold_col.rb +436 -0
  36. data/lib/sequel/extensions/db_opts.rb +65 -4
  37. data/lib/sequel/extensions/funky.rb +136 -0
  38. data/lib/sequel/extensions/make_readyable.rb +146 -30
  39. data/lib/sequel/extensions/more_sql.rb +76 -0
  40. data/lib/sequel/extensions/platform.rb +301 -0
  41. data/lib/sequel/extensions/settable.rb +64 -0
  42. data/lib/sequel/extensions/sql_recorder.rb +85 -0
  43. data/lib/sequel/extensions/unionize.rb +169 -0
  44. data/lib/sequel/extensions/usable.rb +30 -1
  45. data/lib/sequelizer/cli.rb +61 -18
  46. data/lib/sequelizer/connection_maker.rb +54 -72
  47. data/lib/sequelizer/env_config.rb +6 -6
  48. data/lib/sequelizer/gemfile_modifier.rb +23 -21
  49. data/lib/sequelizer/monkey_patches/database_in_after_connect.rb +7 -5
  50. data/lib/sequelizer/options.rb +102 -19
  51. data/lib/sequelizer/options_hash.rb +2 -0
  52. data/lib/sequelizer/version.rb +3 -1
  53. data/lib/sequelizer/yaml_config.rb +9 -4
  54. data/lib/sequelizer.rb +65 -9
  55. data/sequelizer.gemspec +20 -12
  56. data/test/lib/sequel/extensions/test_cold_col.rb +251 -0
  57. data/test/lib/sequel/extensions/test_db_opts.rb +10 -8
  58. data/test/lib/sequel/extensions/test_make_readyable.rb +198 -28
  59. data/test/lib/sequel/extensions/test_more_sql.rb +132 -0
  60. data/test/lib/sequel/extensions/test_platform.rb +222 -0
  61. data/test/lib/sequel/extensions/test_settable.rb +109 -0
  62. data/test/lib/sequel/extensions/test_sql_recorder.rb +231 -0
  63. data/test/lib/sequel/extensions/test_unionize.rb +76 -0
  64. data/test/lib/sequel/extensions/test_usable.rb +5 -2
  65. data/test/lib/sequelizer/test_connection_maker.rb +21 -17
  66. data/test/lib/sequelizer/test_env_config.rb +5 -2
  67. data/test/lib/sequelizer/test_gemfile_modifier.rb +7 -6
  68. data/test/lib/sequelizer/test_options.rb +42 -9
  69. data/test/lib/sequelizer/test_yaml_config.rb +13 -12
  70. data/test/test_helper.rb +37 -8
  71. metadata +196 -39
  72. data/lib/sequel/extensions/sqls.rb +0 -31
@@ -1,10 +1,11 @@
1
- require_relative "../../../test_helper"
2
- require "fileutils"
3
- require "pathname"
4
- require "sequel"
5
- require "sequel/extensions/make_readyable"
1
+ require_relative '../../../test_helper'
2
+ require 'fileutils'
3
+ require 'tmpdir'
4
+ require 'sequel'
5
+ require 'sequel/extensions/make_readyable'
6
6
 
7
7
  class TestUsable < Minitest::Test
8
+
8
9
  def setup
9
10
  # These features are mostly intended for Spark, but sqlite is a close enough
10
11
  # mock that we'll just roll with it
@@ -15,74 +16,243 @@ class TestUsable < Minitest::Test
15
16
  when :schema1
16
17
  [:a]
17
18
  when :schema2
18
- [:a, :b]
19
+ %i[a b]
19
20
  when :schema3
20
- [:a, :b]
21
+ %i[a b]
21
22
  end
22
23
  end
23
24
  end
24
25
 
25
26
  def test_should_call_use_schema
26
27
  @db.make_ready(use_schema: :some_schema)
27
- assert_equal(["USE `some_schema`"], @db.sqls)
28
+
29
+ assert_equal(['USE `some_schema`'], @db.sqls)
28
30
  end
29
31
 
30
32
  def test_should_create_views_based_on_tables_in_search_paths
31
- @db.make_ready(search_path: [:schema1, :schema2, :schema3])
33
+ @db.make_ready(search_path: %i[schema1 schema2 schema3])
34
+
32
35
  assert_equal([
33
- "CREATE TEMPORARY VIEW `a` AS SELECT * FROM `schema1`.`a`",
34
- "CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`"
35
- ], @db.sqls)
36
+ 'CREATE TEMPORARY VIEW `a` AS SELECT * FROM `schema1`.`a`',
37
+ 'CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`',
38
+ ], @db.sqls)
36
39
  end
37
40
 
38
41
  def test_should_create_views_based_on_tables_in_search_paths_passed_as_strings
39
- @db.make_ready(search_path: ["schema1", "schema2", "schema3"])
42
+ @db.make_ready(search_path: %w[schema1 schema2 schema3])
43
+
40
44
  assert_equal([
41
- "CREATE TEMPORARY VIEW `a` AS SELECT * FROM `schema1`.`a`",
42
- "CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`"
43
- ], @db.sqls)
45
+ 'CREATE TEMPORARY VIEW `a` AS SELECT * FROM `schema1`.`a`',
46
+ 'CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`',
47
+ ], @db.sqls)
44
48
  end
45
49
 
46
50
  def test_should_create_views_based_on_tables_in_search_paths_accepts_except
47
- @db.make_ready(search_path: [:schema1, :schema2, :schema3], except: :a)
51
+ @db.make_ready(search_path: %i[schema1 schema2 schema3], except: :a)
52
+
48
53
  assert_equal([
49
- "CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`"
50
- ], @db.sqls)
54
+ 'CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`',
55
+ ], @db.sqls)
51
56
  end
52
57
 
53
58
  def test_should_create_views_based_on_tables_in_search_paths_accepts_only
54
- @db.make_ready(search_path: [:schema1, :schema2, :schema3], only: :b)
59
+ @db.make_ready(search_path: %i[schema1 schema2 schema3], only: :b)
60
+
55
61
  assert_equal([
56
- "CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`"
57
- ], @db.sqls)
62
+ 'CREATE TEMPORARY VIEW `b` AS SELECT * FROM `schema2`.`b`',
63
+ ], @db.sqls)
58
64
  end
59
65
 
60
66
  def test_should_create_views_based_on_path
61
67
  dir = Pathname.new(Dir.mktmpdir)
62
- a_file = dir + "a.parquet"
63
- b_file = dir + "b.parquet"
68
+ a_file = dir.join('a.parquet')
69
+ b_file = dir.join('b.parquet')
64
70
  FileUtils.touch(a_file.to_s)
65
71
  FileUtils.touch(b_file.to_s)
66
72
 
67
73
  @db.make_ready(search_path: [:schema1, a_file, b_file, :schema2])
68
74
  sqls = @db.sqls.dup
69
- assert_equal("CREATE TEMPORARY VIEW `a` AS SELECT * FROM `schema1`.`a`", sqls[0])
75
+
76
+ assert_equal('CREATE TEMPORARY VIEW `a` AS SELECT * FROM `schema1`.`a`', sqls[0])
70
77
  assert_match(%r{CREATE TEMPORARY VIEW `b` USING parquet OPTIONS \('path'='/tmp/[^/]+/b.parquet'\)}, sqls[1])
71
78
  end
72
79
 
73
80
  def test_should_create_views_format_based_on_path
74
81
  dir = Pathname.new(Dir.mktmpdir)
75
- a_file = dir + "a.parquet"
76
- b_file = dir + "b.delta"
77
- c_file = dir + "c.csv"
82
+ a_file = dir.join('a.parquet')
83
+ b_file = dir.join('b.delta')
84
+ c_file = dir.join('c.csv')
78
85
  FileUtils.touch(a_file.to_s)
79
86
  FileUtils.touch(b_file.to_s)
80
87
  FileUtils.touch(c_file.to_s)
81
88
 
82
89
  @db.make_ready(search_path: [a_file, b_file, c_file])
83
90
  sqls = @db.sqls.dup
91
+
84
92
  assert_match(%r{CREATE TEMPORARY VIEW `a` USING parquet OPTIONS \('path'='/tmp/[^/]+/a.parquet'\)}, sqls[0])
85
93
  assert_match(%r{CREATE TEMPORARY VIEW `b` USING delta OPTIONS \('path'='/tmp/[^/]+/b.delta'\)}, sqls[1])
86
94
  assert_match(%r{CREATE TEMPORARY VIEW `c` USING csv OPTIONS \('path'='/tmp/[^/]+/c.csv'\)}, sqls[2])
87
95
  end
96
+
97
+ def test_should_create_a_single_view_if_multiple_files_have_the_same_name
98
+ dir = Pathname.new(Dir.mktmpdir)
99
+ a_file = dir.join('a.parquet')
100
+ b_file = dir.join('a.delta')
101
+ c_file = dir.join('a.csv')
102
+ FileUtils.touch(a_file.to_s)
103
+ FileUtils.touch(b_file.to_s)
104
+ FileUtils.touch(c_file.to_s)
105
+
106
+ @db.make_ready(search_path: [a_file, b_file, c_file])
107
+ sqls = @db.sqls.dup
108
+
109
+ assert_equal(1, sqls.size)
110
+ assert_match(%r{CREATE TEMPORARY VIEW `a` USING parquet OPTIONS \('path'='/tmp/[^/]+/a.parquet'\)}, sqls[0])
111
+ refute_match(%r{CREATE TEMPORARY VIEW `a` USING delta OPTIONS \('path'='/tmp/[^/]+/a.delta'\)}, sqls[1])
112
+ refute_match(%r{CREATE TEMPORARY VIEW `a` USING csv OPTIONS \('path'='/tmp/[^/]+/a.csv'\)}, sqls[2])
113
+ end
114
+
115
+ def test_should_create_a_single_view_if_multiple_files_have_the_same_name_and_are_in_different_directories
116
+ dir = Pathname.new(Dir.mktmpdir)
117
+ a_file = dir / 'one' / 'a.parquet'
118
+ b_file = dir / 'two' / 'a.delta'
119
+ c_file = dir / 'three' / 'a.csv'
120
+ FileUtils.mkdir_p(a_file.dirname)
121
+ FileUtils.mkdir_p(b_file.dirname)
122
+ FileUtils.mkdir_p(c_file.dirname)
123
+ FileUtils.touch(a_file.to_s)
124
+ FileUtils.touch(b_file.to_s)
125
+ FileUtils.touch(c_file.to_s)
126
+
127
+ @db.make_ready(search_path: [a_file, b_file, c_file])
128
+ sqls = @db.sqls.dup
129
+
130
+ assert_equal(1, sqls.size)
131
+ assert_match(%r{CREATE TEMPORARY VIEW `a` USING parquet OPTIONS \('path'='/tmp/[^/]+/one/a.parquet'\)}, sqls[0])
132
+ refute_match(%r{CREATE TEMPORARY VIEW `a` USING delta OPTIONS \('path'='/tmp/[^/]+/two/a.delta'\)}, sqls[1])
133
+ refute_match(%r{CREATE TEMPORARY VIEW `a` USING csv OPTIONS \('path'='/tmp/[^/]+/three/a.csv'\)}, sqls[2])
134
+ end
135
+
136
+ def test_should_create_view_from_compact_style_path
137
+ dir = Pathname.new(Dir.mktmpdir)
138
+ a_file = dir / 'one' / 'a.parquet'
139
+ b_file = dir / 'two' / 'b.delta'
140
+ c_file = dir / 'three' / 'c.csv'
141
+ FileUtils.mkdir_p(a_file.dirname)
142
+ FileUtils.mkdir_p(b_file.dirname)
143
+ FileUtils.mkdir_p(c_file.dirname)
144
+ FileUtils.touch(a_file.to_s)
145
+ FileUtils.touch(b_file.to_s)
146
+ FileUtils.touch(c_file.to_s)
147
+
148
+ @db.make_ready(search_path: [Dir["#{dir}/{one,two,three}"].map { |path| Pathname.new(path).glob('*') }])
149
+ sqls = @db.sqls.dup
150
+
151
+ assert_equal(3, sqls.size)
152
+ assert_match(%r{CREATE TEMPORARY VIEW `a` USING parquet OPTIONS \('path'='/tmp/[^/]+/one/a.parquet'\)}, sqls[0])
153
+ assert_match(%r{CREATE TEMPORARY VIEW `b` USING delta OPTIONS \('path'='/tmp/[^/]+/two/b.delta'\)}, sqls[1])
154
+ assert_match(%r{CREATE TEMPORARY VIEW `c` USING csv OPTIONS \('path'='/tmp/[^/]+/three/c.csv'\)}, sqls[2])
155
+ end
156
+
157
+ def test_should_create_view_from_compact_style_path_with_multiple_files
158
+ dir = Pathname.new(Dir.mktmpdir)
159
+ a_file = dir / 'one' / 'a.parquet'
160
+ b_file = dir / 'two' / 'a.delta'
161
+ c_file = dir / 'three' / 'a.csv'
162
+ FileUtils.mkdir_p(a_file.dirname)
163
+ FileUtils.mkdir_p(b_file.dirname)
164
+ FileUtils.mkdir_p(c_file.dirname)
165
+ FileUtils.touch(a_file.to_s)
166
+ FileUtils.touch(b_file.to_s)
167
+ FileUtils.touch(c_file.to_s)
168
+
169
+ @db.make_ready(search_path: [Dir["#{dir}/{one,two,three}"].map { |path| Pathname.new(path).glob('*') }])
170
+ sqls = @db.sqls.dup
171
+
172
+ assert_equal(1, sqls.size)
173
+ assert_match(%r{CREATE TEMPORARY VIEW `a` USING parquet OPTIONS \('path'='/tmp/[^/]+/one/a.parquet'\)}, sqls[0])
174
+ refute_match(%r{CREATE TEMPORARY VIEW `a` USING delta OPTIONS \('path'='/tmp/[^/]+/two/a.delta'\)}, sqls[1])
175
+ refute_match(%r{CREATE TEMPORARY VIEW `a` USING csv OPTIONS \('path'='/tmp/[^/]+/three/a.csv'\)}, sqls[2])
176
+ end
177
+
178
+ def test_duckdb_external_file_support
179
+ # Test DuckDB - uses read_* functions for external files
180
+ duckdb_db = Sequel.mock
181
+ duckdb_db.extension :make_readyable
182
+ def duckdb_db.database_type
183
+ :duckdb
184
+ end
185
+
186
+ dir = Pathname.new(Dir.mktmpdir)
187
+ parquet_file = dir / 'test.parquet'
188
+ csv_file = dir / 'test.csv'
189
+ json_file = dir / 'test.json'
190
+ FileUtils.touch(parquet_file.to_s)
191
+ FileUtils.touch(csv_file.to_s)
192
+ FileUtils.touch(json_file.to_s)
193
+
194
+ # Test parquet file
195
+ duckdb_db.make_ready(search_path: [parquet_file])
196
+
197
+ assert_match(/CREATE VIEW \w*test\w* AS SELECT \* FROM read_parquet\('.*test\.parquet'\)/, duckdb_db.sqls.last)
198
+
199
+ # Test CSV file
200
+ duckdb_db.sqls.clear
201
+ duckdb_db.make_ready(search_path: [csv_file])
202
+
203
+ assert_match(/CREATE VIEW \w*test\w* AS SELECT \* FROM read_csv_auto\('.*test\.csv'\)/, duckdb_db.sqls.last)
204
+
205
+ # Test JSON file
206
+ duckdb_db.sqls.clear
207
+ duckdb_db.make_ready(search_path: [json_file])
208
+
209
+ assert_match(/CREATE VIEW \w*test\w* AS SELECT \* FROM read_json_auto\('.*test\.json'\)/, duckdb_db.sqls.last)
210
+ end
211
+
212
+ def test_unsupported_file_format_for_duckdb
213
+ # Test unsupported file format for DuckDB
214
+ duckdb_db = Sequel.mock
215
+ duckdb_db.extension :make_readyable
216
+ def duckdb_db.database_type
217
+ :duckdb
218
+ end
219
+
220
+ dir = Pathname.new(Dir.mktmpdir)
221
+ orc_file = dir / 'test.orc'
222
+ FileUtils.touch(orc_file.to_s)
223
+
224
+ error = assert_raises(Sequel::Error) do
225
+ duckdb_db.make_ready(search_path: [orc_file])
226
+ end
227
+ assert_match(/Unsupported file format 'orc' for DuckDB/, error.message)
228
+ end
229
+
230
+ def test_duckdb_directory_support
231
+ # Test DuckDB with directory paths (globbing)
232
+ duckdb_db = Sequel.mock
233
+ duckdb_db.extension :make_readyable
234
+ def duckdb_db.database_type
235
+ :duckdb
236
+ end
237
+
238
+ # Mock directory? to return true
239
+ dir = Pathname.new(Dir.mktmpdir)
240
+ def dir.directory?
241
+ true
242
+ end
243
+
244
+ file_sourcerer = Sequel::ReadyMaker::FileSourcerer.new(duckdb_db, dir)
245
+
246
+ # Override format to return parquet
247
+ def file_sourcerer.format
248
+ 'parquet'
249
+ end
250
+
251
+ # For directory support, DuckDB should use globbing pattern
252
+ file_sourcerer.create_view(:test_table)
253
+
254
+ assert_match(%r{CREATE VIEW \w*test_table\w* AS SELECT \* FROM read_parquet\('.*/\*\*/\*\.parquet'\)},
255
+ duckdb_db.sqls.last)
256
+ end
257
+
88
258
  end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'sequel'
5
+ require 'sequel/extensions/more_sql'
6
+
7
+ class TestMoreSql < Minitest::Test
8
+
9
+ def setup
10
+ @db = Sequel.mock
11
+ @db.extension :more_sql
12
+ end
13
+
14
+ def test_create_schema_with_symbol_name
15
+ @db.create_schema(:test_schema)
16
+
17
+ sqls = @db.sqls
18
+
19
+ assert_equal 1, sqls.length
20
+ assert_equal 'CREATE SCHEMA test_schema', sqls.first
21
+ end
22
+
23
+ def test_create_schema_with_string_name
24
+ @db.create_schema('my_schema')
25
+
26
+ sqls = @db.sqls
27
+
28
+ assert_equal 1, sqls.length
29
+ assert_equal "CREATE SCHEMA 'my_schema'", sqls.first
30
+ end
31
+
32
+ def test_create_schema_with_if_not_exists_option
33
+ @db.create_schema(:analytics, if_not_exists: true)
34
+
35
+ sqls = @db.sqls
36
+
37
+ assert_equal 1, sqls.length
38
+ assert_equal 'CREATE SCHEMA IF NOT EXISTS analytics', sqls.first
39
+ end
40
+
41
+ def test_create_schema_without_if_not_exists_option
42
+ @db.create_schema(:reports, if_not_exists: false)
43
+
44
+ sqls = @db.sqls
45
+
46
+ assert_equal 1, sqls.length
47
+ assert_equal 'CREATE SCHEMA reports', sqls.first
48
+ end
49
+
50
+ def test_create_schema_with_empty_options
51
+ @db.create_schema(:staging, {})
52
+
53
+ sqls = @db.sqls
54
+
55
+ assert_equal 1, sqls.length
56
+ assert_equal 'CREATE SCHEMA staging', sqls.first
57
+ end
58
+
59
+ def test_create_schema_with_special_characters_in_name
60
+ @db.create_schema('schema-with-dashes')
61
+
62
+ sqls = @db.sqls
63
+
64
+ assert_equal 1, sqls.length
65
+ assert_equal "CREATE SCHEMA 'schema-with-dashes'", sqls.first
66
+ end
67
+
68
+ def test_create_schema_returns_nil
69
+ result = @db.create_schema(:test)
70
+
71
+ assert_nil result
72
+ end
73
+
74
+ def test_create_schema_multiple_calls
75
+ @db.create_schema(:first_schema)
76
+ @db.create_schema(:second_schema, if_not_exists: true)
77
+ @db.create_schema('third_schema')
78
+
79
+ sqls = @db.sqls
80
+
81
+ assert_equal 3, sqls.length
82
+ assert_equal 'CREATE SCHEMA first_schema', sqls[0]
83
+ assert_equal 'CREATE SCHEMA IF NOT EXISTS second_schema', sqls[1]
84
+ assert_equal "CREATE SCHEMA 'third_schema'", sqls[2]
85
+ end
86
+
87
+ def test_extension_registration
88
+ assert_respond_to Sequel::Database, :extension
89
+
90
+ db = Sequel.mock
91
+
92
+ assert_respond_to db, :extension
93
+
94
+ db.extension :more_sql
95
+
96
+ assert_respond_to db, :create_schema
97
+ end
98
+
99
+ def test_create_schema_with_qualified_name
100
+ @db.create_schema(Sequel[:public][:my_schema])
101
+
102
+ sqls = @db.sqls
103
+
104
+ assert_equal 1, sqls.length
105
+ assert_match(/CREATE SCHEMA/, sqls.first)
106
+ end
107
+
108
+ def test_create_schema_with_identifier
109
+ @db.create_schema(Sequel.identifier(:test_schema))
110
+
111
+ sqls = @db.sqls
112
+
113
+ assert_equal 1, sqls.length
114
+ assert_equal 'CREATE SCHEMA test_schema', sqls.first
115
+ end
116
+
117
+ def test_create_schema_handles_nil_options
118
+ @db.create_schema(:test_schema)
119
+
120
+ sqls = @db.sqls
121
+
122
+ assert_equal 1, sqls.length
123
+ assert_equal 'CREATE SCHEMA test_schema', sqls.first
124
+ end
125
+
126
+ def test_private_create_schema_sql_method_not_accessible
127
+ assert_raises(NoMethodError) do
128
+ @db.create_schema_sql(:test, {})
129
+ end
130
+ end
131
+
132
+ end
@@ -0,0 +1,222 @@
1
+ require_relative '../../../test_helper'
2
+ require 'sequel'
3
+ require 'sequel/extensions/platform'
4
+
5
+ class TestPlatform < Minitest::Test
6
+
7
+ def setup
8
+ # Point to the gem's config directory
9
+ @original_config_dir = Sequel::Platform.config_dir
10
+ Sequel::Platform.config_dir = File.expand_path('../../../../config/platforms', __dir__)
11
+ end
12
+
13
+ def teardown
14
+ Sequel::Platform.config_dir = @original_config_dir
15
+ end
16
+
17
+ # --- Extension Registration ---
18
+
19
+ def test_should_register_extension
20
+ db = Sequel.mock(host: :postgres)
21
+
22
+ assert_respond_to db, :extension
23
+
24
+ db.extension :platform
25
+
26
+ assert_respond_to db, :platform
27
+ end
28
+
29
+ def test_platform_returns_platform_instance
30
+ db = Sequel.mock(host: :postgres)
31
+ db.extension :platform
32
+
33
+ assert_kind_of Sequel::Platform::Base, db.platform
34
+ end
35
+
36
+ # --- Platform Class Selection ---
37
+
38
+ def test_selects_postgres_platform_for_postgres_adapter
39
+ db = Sequel.mock(host: :postgres)
40
+ db.extension :platform
41
+
42
+ assert_kind_of Sequel::Platform::Postgres, db.platform
43
+ end
44
+
45
+ def test_selects_spark_platform_for_spark_adapter
46
+ db = Sequel.mock(host: :spark)
47
+ db.extension :platform
48
+
49
+ assert_kind_of Sequel::Platform::Spark, db.platform
50
+ end
51
+
52
+ def test_selects_snowflake_platform_for_snowflake_adapter
53
+ db = Sequel.mock(host: :snowflake)
54
+ db.extension :platform
55
+
56
+ assert_kind_of Sequel::Platform::Snowflake, db.platform
57
+ end
58
+
59
+ def test_selects_athena_platform_for_athena_adapter
60
+ db = Sequel.mock(host: :athena)
61
+ db.extension :platform
62
+
63
+ assert_kind_of Sequel::Platform::Athena, db.platform
64
+ end
65
+
66
+ def test_selects_athena_platform_for_presto_adapter
67
+ db = Sequel.mock(host: :presto)
68
+ db.extension :platform
69
+
70
+ assert_kind_of Sequel::Platform::Athena, db.platform
71
+ end
72
+
73
+ def test_selects_base_platform_for_unknown_adapter
74
+ db = Sequel.mock(host: :sqlite)
75
+ db.extension :platform
76
+
77
+ assert_instance_of Sequel::Platform::Base, db.platform
78
+ end
79
+
80
+ # --- Config Loading ---
81
+
82
+ def test_supports_returns_boolean_from_config
83
+ db = Sequel.mock(host: :postgres)
84
+ db.extension :platform
85
+
86
+ assert db.platform.supports?(:cte)
87
+ assert db.platform.supports?(:cte_recursive)
88
+ end
89
+
90
+ def test_supports_returns_false_for_unsupported_feature
91
+ db = Sequel.mock(host: :athena)
92
+ db.extension :platform
93
+
94
+ refute db.platform.supports?(:temp_tables)
95
+ refute db.platform.supports?(:cte_recursive)
96
+ end
97
+
98
+ def test_prefers_returns_boolean_from_config
99
+ db = Sequel.mock(host: :postgres)
100
+ db.extension :platform
101
+
102
+ assert db.platform.prefers?(:cte)
103
+ end
104
+
105
+ def test_prefers_returns_false_when_not_preferred
106
+ db = Sequel.mock(host: :spark)
107
+ db.extension :platform
108
+
109
+ refute db.platform.prefers?(:cte)
110
+ assert db.platform.prefers?(:parquet)
111
+ end
112
+
113
+ def test_bracket_access_returns_config_values
114
+ db = Sequel.mock(host: :postgres)
115
+ db.extension :platform
116
+
117
+ assert_equal 'search_path', db.platform[:schema_switching_method]
118
+ end
119
+
120
+ def test_config_stacking_overrides_base
121
+ db = Sequel.mock(host: :athena)
122
+ db.extension :platform
123
+
124
+ # Athena overrides base supports_temp_tables from true to false
125
+ refute db.platform.supports?(:temp_tables)
126
+ # Athena overrides drop_table_needs_unquoted from false to true
127
+ assert db.platform[:drop_table_needs_unquoted]
128
+ end
129
+
130
+ # --- Function Translations ---
131
+
132
+ def test_postgres_date_diff_uses_subtraction
133
+ db = Sequel.mock(host: :postgres)
134
+ db.extension :platform
135
+
136
+ expr = db.platform.date_diff(:start_date, :end_date)
137
+
138
+ # Should produce subtraction syntax - use dataset.literal to get SQL
139
+ sql = db.literal(expr)
140
+
141
+ assert_includes sql, '-'
142
+ end
143
+
144
+ def test_spark_date_diff_uses_datediff_function
145
+ db = Sequel.mock(host: :spark)
146
+ db.extension :platform
147
+
148
+ expr = db.platform.date_diff(:start_date, :end_date)
149
+
150
+ # Should use datediff function with reversed args
151
+ sql = db.literal(expr)
152
+
153
+ assert_includes sql.downcase, 'datediff'
154
+ end
155
+
156
+ def test_snowflake_date_diff_includes_day_unit
157
+ db = Sequel.mock(host: :snowflake)
158
+ db.extension :platform
159
+
160
+ expr = db.platform.date_diff(:start_date, :end_date)
161
+
162
+ sql = db.literal(expr)
163
+
164
+ assert_includes sql.downcase, 'datediff'
165
+ assert_includes sql, 'day'
166
+ end
167
+
168
+ def test_athena_date_diff_uses_date_diff_function
169
+ db = Sequel.mock(host: :athena)
170
+ db.extension :platform
171
+
172
+ expr = db.platform.date_diff(:start_date, :end_date)
173
+
174
+ sql = db.literal(expr)
175
+
176
+ assert_includes sql.downcase, 'date_diff'
177
+ assert_includes sql, 'day'
178
+ end
179
+
180
+ def test_cast_date_returns_cast_expression
181
+ db = Sequel.mock(host: :postgres)
182
+ db.extension :platform
183
+
184
+ expr = db.platform.cast_date(:some_column)
185
+
186
+ assert_kind_of Sequel::SQL::Cast, expr
187
+ end
188
+
189
+ # --- Edge Cases ---
190
+
191
+ def test_supports_unknown_feature_returns_false
192
+ db = Sequel.mock(host: :postgres)
193
+ db.extension :platform
194
+
195
+ refute db.platform.supports?(:nonexistent_feature)
196
+ end
197
+
198
+ def test_prefers_unknown_feature_returns_false
199
+ db = Sequel.mock(host: :postgres)
200
+ db.extension :platform
201
+
202
+ refute db.platform.prefers?(:nonexistent_feature)
203
+ end
204
+
205
+ def test_bracket_access_unknown_key_returns_nil
206
+ db = Sequel.mock(host: :postgres)
207
+ db.extension :platform
208
+
209
+ assert_nil db.platform[:nonexistent_key]
210
+ end
211
+
212
+ def test_fetch_with_default
213
+ db = Sequel.mock(host: :postgres)
214
+ db.extension :platform
215
+
216
+ # Known key
217
+ assert_equal 'search_path', db.platform.fetch(:schema_switching_method, 'default')
218
+ # Unknown key
219
+ assert_equal 'default', db.platform.fetch(:unknown_key, 'default')
220
+ end
221
+
222
+ end