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.
@@ -18,18 +18,15 @@
18
18
  #
19
19
  # Related module: Sequel::Platform
20
20
 
21
- require 'kvcsv'
21
+ require "kvcsv"
22
22
 
23
23
  module Sequel
24
-
25
24
  # The Platform module provides database platform abstraction through
26
25
  # configuration-driven capabilities and code-driven function translations.
27
26
  module Platform
28
-
29
27
  # Base platform class with KVCSV config loading and default implementations.
30
28
  # Subclasses override function translation methods for platform-specific SQL.
31
29
  class Base
32
-
33
30
  attr_reader :db, :config
34
31
 
35
32
  # Initialize platform with database connection and config paths.
@@ -122,27 +119,23 @@ module Sequel
122
119
  def days_between(from, to)
123
120
  date_diff(from, to)
124
121
  end
125
-
126
122
  end
127
123
 
128
124
  # PostgreSQL platform with Postgres-specific function translations.
129
125
  class Postgres < Base
130
-
131
126
  def date_diff(from, to)
132
127
  # Postgres uses date subtraction
133
- Sequel.lit('(? - ?)', to, from)
128
+ Sequel.lit("(? - ?)", to, from)
134
129
  end
135
130
 
136
131
  def days_between(from, to)
137
132
  # Postgres date subtraction returns integer days
138
- Sequel.lit('(? - ?)', to, from)
133
+ Sequel.lit("(? - ?)", to, from)
139
134
  end
140
-
141
135
  end
142
136
 
143
137
  # Spark platform with Spark SQL-specific function translations.
144
138
  class Spark < Base
145
-
146
139
  def date_diff(from, to)
147
140
  # Spark datediff has reversed argument order (end, start)
148
141
  Sequel.function(:datediff, to, from)
@@ -151,35 +144,30 @@ module Sequel
151
144
  def str_to_date(value, format)
152
145
  Sequel.function(:to_date, Sequel.cast_string(value), format)
153
146
  end
154
-
155
147
  end
156
148
 
157
149
  # Snowflake platform with Snowflake-specific function translations.
158
150
  class Snowflake < Base
159
-
160
151
  def date_diff(from, to)
161
152
  # Snowflake requires unit parameter
162
- Sequel.function(:datediff, 'day', from, to)
153
+ Sequel.function(:datediff, "day", from, to)
163
154
  end
164
155
 
165
156
  def days_between(from, to)
166
- Sequel.function(:datediff, 'day', from, to)
157
+ Sequel.function(:datediff, "day", from, to)
167
158
  end
168
-
169
159
  end
170
160
 
171
161
  # Athena platform (Presto/Trino based) with Athena-specific function translations.
172
162
  class Athena < Base
173
-
174
163
  def date_diff(from, to)
175
164
  # Athena/Presto uses date_diff with unit
176
- Sequel.function(:date_diff, 'day', from, to)
165
+ Sequel.function(:date_diff, "day", from, to)
177
166
  end
178
167
 
179
168
  def days_between(from, to)
180
- Sequel.function(:date_diff, 'day', from, to)
169
+ Sequel.function(:date_diff, "day", from, to)
181
170
  end
182
-
183
171
  end
184
172
 
185
173
  # Map adapter schemes to platform classes
@@ -190,22 +178,21 @@ module Sequel
190
178
  athena: Athena,
191
179
  presto: Athena,
192
180
  trino: Athena,
193
- snowflake: Snowflake,
181
+ snowflake: Snowflake
194
182
  }.freeze
195
183
 
196
184
  # Map adapter schemes to config file names
197
185
  ADAPTER_CONFIG_NAMES = {
198
- postgres: 'postgres',
199
- postgresql: 'postgres',
200
- spark: 'spark',
201
- athena: 'athena',
202
- presto: 'athena',
203
- trino: 'athena',
204
- snowflake: 'snowflake',
186
+ postgres: "postgres",
187
+ postgresql: "postgres",
188
+ spark: "spark",
189
+ athena: "athena",
190
+ presto: "athena",
191
+ trino: "athena",
192
+ snowflake: "snowflake"
205
193
  }.freeze
206
194
 
207
195
  class << self
208
-
209
196
  # Find the config directory, searching gem paths
210
197
  def config_dir
211
198
  @config_dir ||= find_config_dir
@@ -218,16 +205,15 @@ module Sequel
218
205
 
219
206
  def find_config_dir
220
207
  # Check relative to this file (gem's config)
221
- gem_config = File.expand_path('../../../config/platforms', __dir__)
208
+ gem_config = File.expand_path("../../../config/platforms", __dir__)
222
209
  return gem_config if File.directory?(gem_config)
223
210
 
224
211
  # Fallback to working directory
225
- local_config = File.join(Dir.pwd, 'config/platforms')
212
+ local_config = File.join(Dir.pwd, "config/platforms")
226
213
  return local_config if File.directory?(local_config)
227
214
 
228
215
  nil
229
216
  end
230
-
231
217
  end
232
218
 
233
219
  # Build config paths for the given adapter
@@ -239,12 +225,12 @@ module Sequel
239
225
  paths = []
240
226
 
241
227
  if config_dir
242
- base_config = File.join(config_dir, 'base.csv')
228
+ base_config = File.join(config_dir, "base.csv")
243
229
  paths << base_config if File.exist?(base_config)
244
230
 
245
231
  adapter_name = ADAPTER_CONFIG_NAMES[adapter_scheme]
246
232
  if adapter_name
247
- rdbms_config = File.join(config_dir, 'rdbms', "#{adapter_name}.csv")
233
+ rdbms_config = File.join(config_dir, "rdbms", "#{adapter_name}.csv")
248
234
  paths << rdbms_config if File.exist?(rdbms_config)
249
235
  end
250
236
  end
@@ -292,9 +278,7 @@ module Sequel
292
278
  def platform
293
279
  @platform
294
280
  end
295
-
296
281
  end
297
282
 
298
283
  Database.register_extension(:platform, Platform)
299
-
300
284
  end
@@ -13,18 +13,26 @@
13
13
  # DB.set(work_mem: '256MB')
14
14
  # # Executes: SET work_mem=256MB
15
15
  #
16
+ # DB.set(memory_limit: Sequel.expr('1GB'))
17
+ # # Executes: SET memory_limit='1GB'
18
+ #
19
+ # Plain values are interpolated raw into the SET statement, so
20
+ # <tt>search_path: 'a,b,public'</tt> stays a list of schemas. When the database
21
+ # needs an SQL literal (for example DuckDB string settings), the caller wraps
22
+ # the value in +Sequel.expr+, and the extension passes it through +literal+.
23
+ #
24
+ # The +set_sql+ method returns the SET statements without executing them.
25
+ #
16
26
  # The extension works with any database adapter and supports various value types
17
27
  # including strings, numbers, booleans, and nil values.
18
28
  #
19
29
  # Related module: Sequel::Settable
20
30
 
21
31
  module Sequel
22
-
23
32
  # The Settable module provides database configuration functionality through
24
- # SET statements. When loaded as an extension, it adds the +set+ method to
25
- # database connections.
33
+ # SET statements. When loaded as an extension, it adds the +set+ and
34
+ # +set_sql+ methods to database connections.
26
35
  module Settable
27
-
28
36
  # Execute SET statements for the given options hash.
29
37
  #
30
38
  # Each key-value pair in the options hash is converted to a SET statement
@@ -50,6 +58,10 @@ module Sequel
50
58
  # # SET autocommit=true
51
59
  # # SET custom_setting=
52
60
  #
61
+ # @example Caller-quoted value
62
+ # DB.set(memory_limit: Sequel.expr('1GB'))
63
+ # # Executes: SET memory_limit='1GB'
64
+ #
53
65
  # @return [void]
54
66
  def set(opts = {})
55
67
  set_sql(opts).each do |sql|
@@ -57,25 +69,26 @@ module Sequel
57
69
  end
58
70
  end
59
71
 
60
- private
61
-
62
- # Generate SET SQL statements from options hash.
72
+ # Generate SET SQL statements from options hash, without executing them.
63
73
  #
64
74
  # Converts each key-value pair in the options hash into a SET SQL statement
65
- # string. This is a private helper method used internally by the +set+ method.
75
+ # string. A value that is a +Sequel::SQL::Expression+ is passed through
76
+ # +literal+. Every other value is interpolated raw.
66
77
  #
67
78
  # @param opts [Hash] Hash of options to convert to SET statements
68
79
  # @return [Array<String>] Array of SET SQL statement strings
69
80
  #
70
81
  # @example
71
- # set_sql(timezone: 'UTC', port: 5432)
82
+ # DB.set_sql(timezone: 'UTC', port: 5432)
72
83
  # # => ["SET timezone=UTC", "SET port=5432"]
84
+ #
85
+ # @example Caller-quoted value
86
+ # DB.set_sql(memory_limit: Sequel.expr('1GB'))
87
+ # # => ["SET memory_limit='1GB'"]
73
88
  def set_sql(opts)
74
- opts.map { |k, v| "SET #{k}=#{v}" }
89
+ opts.map { |k, v| "SET #{k}=#{v.is_a?(Sequel::SQL::Expression) ? literal(v) : v}" }
75
90
  end
76
-
77
91
  end
78
92
 
79
93
  Database.register_extension(:settable, Settable)
80
-
81
94
  end
@@ -8,9 +8,18 @@
8
8
  #
9
9
  # This is essential for lazy query building patterns where datasets
10
10
  # reference tables that may not exist yet at query-build time. By
11
- # walking through from_self chains and inspecting inner SELECT lists,
12
- # this extension can determine which columns to keep without touching
13
- # the database.
11
+ # inspecting the dataset's own SELECT list, or walking through from_self
12
+ # chains to an inner SELECT list, this extension can determine which
13
+ # columns to keep without touching the database.
14
+ #
15
+ # When static resolution fails and the database returns no columns (for
16
+ # example on Sequel.mock), select_remove raises
17
+ # Sequel::SmartSelectRemove::NoColumnsError instead of producing
18
+ # SELECT *, which would still contain the removed columns.
19
+ #
20
+ # When the columns are known but every one of them is removed, the result
21
+ # would also be SELECT *, so select_remove raises
22
+ # Sequel::SmartSelectRemove::AllColumnsRemovedError.
14
23
  #
15
24
  # You should load this extension into all of a database's datasets:
16
25
  #
@@ -21,37 +30,85 @@
21
30
  # Related module: Sequel::SmartSelectRemove
22
31
 
23
32
  module Sequel
24
-
25
33
  module SmartSelectRemove
34
+ # Raised when select_remove cannot determine the dataset's columns, so
35
+ # the result would be SELECT * and would still contain the removed columns.
36
+ class NoColumnsError < Sequel::Error
37
+ def initialize(dataset)
38
+ super("select_remove could not determine the columns of #{dataset.sql}; " \
39
+ "it would produce SELECT * and keep the removed columns")
40
+ end
41
+ end
42
+
43
+ # Raised when select_remove would remove every column, so the result
44
+ # would be SELECT * and would still contain the removed columns.
45
+ class AllColumnsRemovedError < Sequel::Error
46
+ def initialize(dataset, cols)
47
+ super("select_remove(#{cols.map(&:inspect).join(', ')}) would remove every column of " \
48
+ "#{dataset.sql}; the result would be SELECT * and keep the removed columns")
49
+ end
50
+ end
26
51
 
27
52
  # Override select_remove to attempt static column resolution before
28
53
  # falling back to the original implementation.
29
54
  #
30
- # When the dataset has no explicit SELECT clause, walks through
31
- # from_self chains to find inner column expressions and resolves
32
- # them to outer-scope symbol names. This avoids the columns! call
33
- # that the original select_remove uses, which would execute SQL.
55
+ # When the dataset has an explicit SELECT clause, resolves the output
56
+ # name of each entry and keeps the original expressions whose names
57
+ # are not removed, as the original select_remove does with the
58
+ # names that columns returns. When the dataset has no explicit SELECT
59
+ # clause, walks through from_self chains to find inner column
60
+ # expressions and resolves them to outer-scope symbol names. Both
61
+ # avoid the columns! call that the original select_remove uses,
62
+ # which would execute SQL.
63
+ #
64
+ # If any entry cannot be resolved (for example a literal string or a
65
+ # star), the original select_remove is used. Every such fallback
66
+ # first checks columns, and raises NoColumnsError when it is empty.
67
+ #
68
+ # On every path, if the result would have an empty select list because
69
+ # every column is removed, raises AllColumnsRemovedError. An empty
70
+ # select list renders as SELECT * and keeps the removed columns.
34
71
  #
35
72
  # @param cols [Array<Symbol>] columns to remove
36
73
  # @return [Sequel::Dataset] dataset with columns removed
74
+ # @raise [NoColumnsError] if the fallback finds no columns
75
+ # @raise [AllColumnsRemovedError] if every column would be removed
37
76
  def select_remove(*cols)
38
- # If there's an explicit select, original behavior handles it
39
- return super if @opts[:select] && !@opts[:select].empty?
40
-
41
- # Try to resolve columns by walking the from_self chain
42
- if (inner_cols = resolve_selected_columns)
77
+ sel = @opts[:select]
78
+ if sel && !sel.empty?
79
+ names = sel.map { |c| resolve_outer_column_name(c) }
80
+ unless names.any?(&:nil?)
81
+ kept = names.zip(sel).reject { |name, _| cols.include?(name) }.map(&:last)
82
+ return ensure_columns_remain(select(*(kept - cols)), cols)
83
+ end
84
+ elsif (inner_cols = resolve_selected_columns)
43
85
  outer_cols = inner_cols.map { |c| resolve_outer_column_name(c) }
44
- # All columns must be resolvable; bail to super if any are opaque
45
- return super if outer_cols.any?(&:nil?)
46
-
47
- return select(*(outer_cols - cols))
86
+ return ensure_columns_remain(select(*(outer_cols - cols)), cols) unless outer_cols.any?(&:nil?)
48
87
  end
49
88
 
50
- super
89
+ # Every fallback reaches this point. With no columns, the original
90
+ # select_remove would call select() and produce SELECT *.
91
+ raise NoColumnsError, self if columns.empty?
92
+
93
+ ensure_columns_remain(super, cols)
51
94
  end
52
95
 
53
96
  private
54
97
 
98
+ # Return ds unless its select list is empty or nil. Either renders as
99
+ # SELECT *, which would keep the removed columns.
100
+ #
101
+ # @param ds [Sequel::Dataset] the dataset select_remove would return
102
+ # @param cols [Array<Symbol>] the columns that were removed
103
+ # @return [Sequel::Dataset] ds
104
+ # @raise [AllColumnsRemovedError] if ds selects no columns
105
+ def ensure_columns_remain(ds, cols)
106
+ sel = ds.opts[:select]
107
+ raise AllColumnsRemovedError.new(self, cols) if sel.nil? || sel.empty?
108
+
109
+ ds
110
+ end
111
+
55
112
  # Walk through from_self chains to find the innermost explicit
56
113
  # SELECT list. Returns nil if no explicit selection is found.
57
114
  #
@@ -75,12 +132,15 @@ module Sequel
75
132
  # After from_self, inner expressions become subquery output columns
76
133
  # referenced by their alias or base name in the outer scope.
77
134
  #
135
+ # A star (:* or a qualified :*) selects every column of a table, so
136
+ # it has no single name and is unresolvable.
137
+ #
78
138
  # @param col [Object] a column expression from the inner SELECT
79
139
  # @return [Symbol, nil] the outer column name, or nil if unresolvable
80
140
  def resolve_outer_column_name(col)
81
141
  case col
82
142
  when Symbol
83
- col
143
+ col unless col == :*
84
144
  when Sequel::SQL::AliasedExpression
85
145
  a = col.alias
86
146
  a.is_a?(Symbol) ? a : a.to_sym
@@ -90,12 +150,10 @@ module Sequel
90
150
  col.value.to_sym
91
151
  end
92
152
  end
93
-
94
153
  end
95
154
 
96
155
  Database.register_extension(:smart_select_remove) do |db|
97
156
  db.extension(:select_remove)
98
157
  db.extend_datasets(SmartSelectRemove)
99
158
  end
100
-
101
159
  end
@@ -32,12 +32,10 @@
32
32
  # Related module: Sequel::SqlRecorder
33
33
 
34
34
  module Sequel
35
-
36
35
  # Extension module that adds SQL recording capabilities to Sequel databases.
37
36
  # When included, it provides a +sql_recorder+ method that returns an array
38
37
  # of all SQL statements executed against the database.
39
38
  module SqlRecorder
40
-
41
39
  # Returns the array of recorded SQL statements.
42
40
  #
43
41
  # The array accumulates all SQL statements sent to the database since the
@@ -77,9 +75,7 @@ module Sequel
77
75
  @sql_recorder ||= []
78
76
  end
79
77
  end
80
-
81
78
  end
82
79
 
83
80
  Database.register_extension(:sql_recorder, SqlRecorder)
84
-
85
81
  end
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'digest'
3
+ require "digest"
4
4
 
5
5
  module Sequel
6
-
7
6
  # Provides efficient handling of large UNION operations.
8
7
  #
9
8
  # The unionize extension allows combining many datasets through UNION operations
@@ -20,14 +19,12 @@ module Sequel
20
19
  # @example With options
21
20
  # DB.unionize(datasets, chunk_size: 50, all: true, temp_table_prefix: 'my_union')
22
21
  module Unionize
23
-
24
22
  # Handles the chunking and union of multiple datasets.
25
23
  #
26
24
  # This class manages the process of splitting a large collection of datasets
27
25
  # into smaller chunks, creating temporary tables/views for each chunk, and
28
26
  # then recursively combining them until a single unified dataset is produced.
29
27
  class Unionizer
30
-
31
28
  # Default number of datasets to combine in each chunk
32
29
  DEFAULT_CHUNK_SIZE = 100
33
30
 
@@ -36,7 +33,6 @@ module Sequel
36
33
  # Each chunk handles a subset of datasets, creates a temporary table/view
37
34
  # for the combined result, and provides access to the unified dataset.
38
35
  class Chunk
39
-
40
36
  # @!attribute [r] db
41
37
  # @return [Sequel::Database] The database connection
42
38
  # @!attribute [r] dses
@@ -90,7 +86,6 @@ module Sequel
90
86
  raise "Unsupported database type: #{db.database_type}"
91
87
  end
92
88
  end
93
-
94
89
  end
95
90
 
96
91
  # @!attribute [r] db
@@ -111,7 +106,7 @@ module Sequel
111
106
  @ds_set = ds_set
112
107
  @opts = opts
113
108
  opts[:chunk_size] ||= DEFAULT_CHUNK_SIZE
114
- opts[:temp_table_prefix] ||= 'temp_union'
109
+ opts[:temp_table_prefix] ||= "temp_union"
115
110
  opts[:all] ||= false
116
111
  opts[:from_self] = opts.fetch(:from_self, true)
117
112
  end
@@ -132,7 +127,6 @@ module Sequel
132
127
 
133
128
  unionize(chunks.each(&:create).map { |chunk| db[chunk.name] })
134
129
  end
135
-
136
130
  end
137
131
 
138
132
  # Efficiently combines multiple datasets using UNION operations.
@@ -161,9 +155,7 @@ module Sequel
161
155
  def unionize(ds_set, opts = {})
162
156
  Unionizer.new(self, ds_set, opts).unionize
163
157
  end
164
-
165
158
  end
166
159
 
167
160
  Database.register_extension(:unionize, Unionize)
168
-
169
161
  end
@@ -1,5 +1,4 @@
1
1
  module Sequel
2
-
3
2
  # = Usable
4
3
  #
5
4
  # Sequel extension that provides a convenient +use+ method for switching
@@ -12,7 +11,6 @@ module Sequel
12
11
  # db.use(:my_schema)
13
12
  # # Executes: USE `my_schema`
14
13
  module Usable
15
-
16
14
  # Switches to the specified database or schema.
17
15
  #
18
16
  # Executes a USE statement to change the current database context.
@@ -36,9 +34,7 @@ module Sequel
36
34
  def use_sql(schema_name)
37
35
  "USE #{literal(schema_name)}"
38
36
  end
39
-
40
37
  end
41
38
 
42
39
  Database.register_extension(:usable, Usable)
43
-
44
40
  end
@@ -1,6 +1,5 @@
1
- require 'thor'
2
- require 'pp'
3
- require_relative 'gemfile_modifier'
1
+ require "thor"
2
+ require_relative "options"
4
3
 
5
4
  module Sequelizer
6
5
  # = CLI
@@ -8,57 +7,38 @@ module Sequelizer
8
7
  # Command line interface for Sequelizer gem using Thor.
9
8
  # Provides commands for:
10
9
  #
11
- # * Updating Gemfile with database adapters
12
10
  # * Initializing .env files with database configuration
13
11
  # * Displaying current configuration
14
12
  #
15
13
  # @example
16
- # sequelizer update_gemfile
17
14
  # sequelizer init_env --adapter postgres --host localhost
18
15
  # sequelizer config
19
16
  class CLI < Thor
20
-
21
- desc 'update_gemfile',
22
- 'adds or replaces a line in your Gemfile to include the correct database adapter to work with Sequel'
23
- option 'dry-run', type: :boolean, desc: 'Only prints out what it would do, but makes no changes'
24
- option 'skip-bundle', type: :boolean, desc: "Don't run `bundle install` after modifying Gemfile"
25
- # Updates the Gemfile to include the appropriate database adapter gem.
26
- #
27
- # This command analyzes your current database configuration and adds or updates
28
- # the corresponding database adapter gem in your Gemfile. It supports various
29
- # adapters including PostgreSQL, MySQL, SQLite, and JDBC-based adapters.
30
- #
31
- # @option options [Boolean] dry-run Only prints what would be done without making changes
32
- # @option options [Boolean] skip-bundle Skip running `bundle install` after modification
33
- def update_gemfile
34
- GemfileModifier.new(options).modify
35
- end
36
-
37
- desc 'init_env', 'creates a .env file with the parameters listed'
17
+ desc "init_env", "creates a .env file with the parameters listed"
38
18
  option :adapter,
39
19
  aliases: :a,
40
- desc: 'adapter for database'
20
+ desc: "adapter for database"
41
21
  option :host,
42
22
  aliases: :h,
43
- banner: 'localhost',
44
- desc: 'host for database'
23
+ banner: "localhost",
24
+ desc: "host for database"
45
25
  option :username,
46
26
  aliases: :u,
47
- desc: 'username for database'
27
+ desc: "username for database"
48
28
  option :password,
49
29
  aliases: :P,
50
- desc: 'password for database'
30
+ desc: "password for database"
51
31
  option :port,
52
32
  aliases: :p,
53
33
  type: :numeric,
54
- banner: '5432',
55
- desc: 'port for database'
34
+ banner: "5432",
35
+ desc: "port for database"
56
36
  option :database,
57
37
  aliases: :d,
58
- desc: 'database for database'
38
+ desc: "database for database"
59
39
  option :search_path,
60
40
  aliases: :s,
61
- desc: 'schema for database (PostgreSQL only)'
41
+ desc: "schema for database (PostgreSQL only)"
62
42
  # Creates a .env file with database configuration parameters.
63
43
  #
64
44
  # This command generates a .env file with SEQUELIZER_* environment variables
@@ -73,18 +53,18 @@ module Sequelizer
73
53
  # @option options [String] :search_path PostgreSQL schema search path
74
54
  # @raise [SystemExit] if .env file already exists
75
55
  def init_env
76
- if File.exist?('.env')
56
+ if File.exist?(".env")
77
57
  puts ".env already exists! I'm too cowardly to overwrite it!"
78
58
  puts "Here's what I would have put in there:"
79
59
  puts make_env(options)
80
60
  exit(1)
81
61
  end
82
- File.open('.env', 'w') do |file|
62
+ File.open(".env", "w") do |file|
83
63
  file.puts make_env(options)
84
64
  end
85
65
  end
86
66
 
87
- desc 'config', 'prints out the connection parameters'
67
+ desc "config", "prints out the connection parameters"
88
68
  # Displays the current database configuration and extensions.
89
69
  #
90
70
  # This command shows the resolved configuration options that would be used
@@ -103,6 +83,5 @@ module Sequelizer
103
83
  "SEQUELIZER_#{key.upcase}=#{value}"
104
84
  end.join("\n")
105
85
  end
106
-
107
86
  end
108
87
  end