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.
- checksums.yaml +4 -4
- data/.claude/settings.local.json +64 -0
- data/.devcontainer/.p10k.zsh +1713 -0
- data/.devcontainer/.zshrc +29 -0
- data/.devcontainer/Dockerfile +137 -0
- data/.devcontainer/copy-claude-credentials.sh +32 -0
- data/.devcontainer/devcontainer.json +102 -0
- data/.devcontainer/init-firewall.sh +123 -0
- data/.devcontainer/setup-credentials.sh +95 -0
- data/.github/workflows/test.yml +1 -1
- data/.gitignore +6 -1
- data/.overcommit.yml +73 -0
- data/.rubocop.yml +167 -0
- data/CHANGELOG.md +24 -0
- data/CLAUDE.md +219 -0
- data/Gemfile +6 -2
- data/Gemfile.lock +158 -0
- data/Guardfile +1 -1
- data/Rakefile +28 -3
- data/lib/sequel/extensions/cold_col.rb +436 -0
- data/lib/sequel/extensions/db_opts.rb +65 -4
- data/lib/sequel/extensions/make_readyable.rb +148 -30
- data/lib/sequel/extensions/more_sql.rb +76 -0
- data/lib/sequel/extensions/settable.rb +64 -0
- data/lib/sequel/extensions/sql_recorder.rb +85 -0
- data/lib/sequel/extensions/unionize.rb +169 -0
- data/lib/sequel/extensions/usable.rb +30 -1
- data/lib/sequelizer/cli.rb +61 -18
- data/lib/sequelizer/connection_maker.rb +54 -72
- data/lib/sequelizer/env_config.rb +6 -6
- data/lib/sequelizer/gemfile_modifier.rb +23 -21
- data/lib/sequelizer/monkey_patches/database_in_after_connect.rb +7 -5
- data/lib/sequelizer/options.rb +97 -18
- data/lib/sequelizer/options_hash.rb +2 -0
- data/lib/sequelizer/version.rb +3 -1
- data/lib/sequelizer/yaml_config.rb +9 -3
- data/lib/sequelizer.rb +65 -9
- data/sequelizer.gemspec +12 -7
- data/test/lib/sequel/extensions/test_cold_col.rb +251 -0
- data/test/lib/sequel/extensions/test_db_opts.rb +10 -8
- data/test/lib/sequel/extensions/test_make_readyable.rb +199 -28
- data/test/lib/sequel/extensions/test_more_sql.rb +132 -0
- data/test/lib/sequel/extensions/test_settable.rb +109 -0
- data/test/lib/sequel/extensions/test_sql_recorder.rb +231 -0
- data/test/lib/sequel/extensions/test_unionize.rb +76 -0
- data/test/lib/sequel/extensions/test_usable.rb +5 -2
- data/test/lib/sequelizer/test_connection_maker.rb +21 -17
- data/test/lib/sequelizer/test_env_config.rb +5 -2
- data/test/lib/sequelizer/test_gemfile_modifier.rb +7 -6
- data/test/lib/sequelizer/test_options.rb +14 -9
- data/test/lib/sequelizer/test_yaml_config.rb +13 -12
- data/test/test_helper.rb +36 -8
- metadata +107 -28
- data/lib/sequel/extensions/sqls.rb +0 -31
@@ -0,0 +1,436 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# The cold_col extension adds support for determining dataset column information
|
5
|
+
# without executing queries against a live database. This "cold column" functionality
|
6
|
+
# is useful for testing, development, and static analysis scenarios where
|
7
|
+
# database connections may not be available or desirable.
|
8
|
+
#
|
9
|
+
# The extension maintains schema information through three sources:
|
10
|
+
# - Pre-loaded schemas from YAML files via load_schema
|
11
|
+
# - Automatically recorded tables/views created during the session
|
12
|
+
# - Manually added table schemas via add_table_schema
|
13
|
+
#
|
14
|
+
# Basic usage:
|
15
|
+
#
|
16
|
+
# db = Sequel.mock.extension(:cold_col)
|
17
|
+
#
|
18
|
+
# # Load schema from YAML file
|
19
|
+
# db.load_schema('schemas.yml')
|
20
|
+
#
|
21
|
+
# # Or add schema manually
|
22
|
+
# db.add_table_schema(:users, [[:id, {}], [:name, {}], [:email, {}]])
|
23
|
+
#
|
24
|
+
# # Now datasets can determine columns without database queries
|
25
|
+
# ds = db[:users].select(:name, :email)
|
26
|
+
# ds.columns # => [:name, :email]
|
27
|
+
#
|
28
|
+
# The extension supports complex queries including JOINs, CTEs, subqueries,
|
29
|
+
# and aliased tables. Schema YAML files should follow this format:
|
30
|
+
#
|
31
|
+
# users:
|
32
|
+
# columns:
|
33
|
+
# id: { type: integer, primary_key: true }
|
34
|
+
# name: { type: string }
|
35
|
+
# email: { type: string }
|
36
|
+
#
|
37
|
+
# You can load the extension into the database using:
|
38
|
+
#
|
39
|
+
# DB.extension :cold_col
|
40
|
+
|
41
|
+
require 'active_support/core_ext/object/try'
|
42
|
+
require 'active_support/core_ext/object/blank'
|
43
|
+
|
44
|
+
module Sequel
|
45
|
+
|
46
|
+
module ColdColDatabase
|
47
|
+
|
48
|
+
# Internal schema registry for managing column information across different sources.
|
49
|
+
# This class centralizes the storage and retrieval of table/view column metadata
|
50
|
+
# from multiple sources including created tables, views, and manually loaded schemas.
|
51
|
+
class SchemaRegistry
|
52
|
+
|
53
|
+
# Initialize a new schema registry for the given database.
|
54
|
+
#
|
55
|
+
# @param db [Sequel::Database] the database instance this registry belongs to
|
56
|
+
def initialize(db)
|
57
|
+
@db = db
|
58
|
+
@created_tables = {} # Tables created during the session
|
59
|
+
@created_views = {} # Views created during the session
|
60
|
+
@schemas = {} # Manually loaded/added schemas
|
61
|
+
end
|
62
|
+
|
63
|
+
# Add schema information for a table that was manually specified.
|
64
|
+
# This method stores schemas added via add_table_schema or load_schema.
|
65
|
+
#
|
66
|
+
# @param name [String, Symbol] the table name
|
67
|
+
# @param columns [Array] array of [column_name, column_info] pairs
|
68
|
+
def add_schema(name, columns)
|
69
|
+
Sequel.synchronize { @schemas[name.to_s] = columns }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Record column information for a table created during the session.
|
73
|
+
# This method is called automatically when CREATE TABLE statements are executed.
|
74
|
+
#
|
75
|
+
# @param name [String] the literal table name from the database
|
76
|
+
# @param columns [Array] array of [column_name, column_info] pairs
|
77
|
+
def add_created_table(name, columns)
|
78
|
+
Sequel.synchronize { @created_tables[name] = columns }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Record column information for a view created during the session.
|
82
|
+
# This method is called automatically when CREATE VIEW statements are executed.
|
83
|
+
#
|
84
|
+
# @param name [String] the literal view name from the database
|
85
|
+
# @param columns [Array] array of [column_name, column_info] pairs
|
86
|
+
def add_created_view(name, columns)
|
87
|
+
Sequel.synchronize { @created_views[name] = columns }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Find column information for a given table/view name.
|
91
|
+
# This method searches through all available registries in priority order:
|
92
|
+
# 1. Created views (highest priority - most recent)
|
93
|
+
# 2. Created tables (medium priority - session specific)
|
94
|
+
# 3. Loaded schemas (lowest priority - external definitions)
|
95
|
+
#
|
96
|
+
# @param name [String, Symbol] the table/view name to look up
|
97
|
+
# @return [Array<Symbol>, nil] array of column names as symbols, or nil if not found
|
98
|
+
def find_columns(name)
|
99
|
+
table_name = name.to_s
|
100
|
+
literal_name = @db.literal(name)
|
101
|
+
|
102
|
+
# Search through registries in priority order
|
103
|
+
[@created_views, @created_tables, @schemas].each do |registry|
|
104
|
+
next unless registry
|
105
|
+
|
106
|
+
# Try literal representation first (most common for created tables/views)
|
107
|
+
if (columns = Sequel.synchronize { registry[literal_name] })
|
108
|
+
return columns.map { |c, _| c }
|
109
|
+
end
|
110
|
+
|
111
|
+
# Try string representation (for manually added schemas)
|
112
|
+
if (columns = Sequel.synchronize { registry[table_name] })
|
113
|
+
return columns.map { |c, _| c }
|
114
|
+
end
|
115
|
+
|
116
|
+
# Try finding by Sequel::LiteralString key (for test setup compatibility)
|
117
|
+
registry.each_key do |key|
|
118
|
+
if key.respond_to?(:to_s) && key.to_s == literal_name && (columns = Sequel.synchronize { registry[key] })
|
119
|
+
return columns.map { |c, _| c }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
nil
|
125
|
+
end
|
126
|
+
|
127
|
+
# Merge new schema definitions into the existing schemas registry.
|
128
|
+
# Used when loading schemas from YAML files.
|
129
|
+
#
|
130
|
+
# @param new_schemas [Hash] hash of table_name => column_definitions
|
131
|
+
def merge_schemas(new_schemas)
|
132
|
+
Sequel.synchronize { @schemas.merge!(new_schemas) }
|
133
|
+
end
|
134
|
+
|
135
|
+
# Directly set the schemas registry (primarily for test setup).
|
136
|
+
# This method replaces the entire schemas hash.
|
137
|
+
#
|
138
|
+
# @param schemas_hash [Hash] the new schemas hash to use
|
139
|
+
def set_schemas(schemas_hash)
|
140
|
+
Sequel.synchronize { @schemas = schemas_hash }
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
# Sets up the cold column tracking when the extension is loaded
|
146
|
+
def self.extended(db)
|
147
|
+
db.extend_datasets(ColdColDataset)
|
148
|
+
db.instance_variable_set(:@cold_col_registry, SchemaRegistry.new(db))
|
149
|
+
end
|
150
|
+
|
151
|
+
# Access the schema registry for this database instance.
|
152
|
+
#
|
153
|
+
# @return [SchemaRegistry] the registry managing column information
|
154
|
+
def cold_col_registry
|
155
|
+
@cold_col_registry
|
156
|
+
end
|
157
|
+
|
158
|
+
# Load table schema information from a YAML file
|
159
|
+
def load_schema(path)
|
160
|
+
schema_data = Psych.load_file(path) || {}
|
161
|
+
schemas = schema_data.to_h do |table, info|
|
162
|
+
columns = (info[:columns] || {}).map { |column_name, col_info| [column_name.to_sym, col_info] }
|
163
|
+
[table.to_s, columns]
|
164
|
+
end
|
165
|
+
cold_col_registry.merge_schemas(schemas)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Manually add schema information for a table
|
169
|
+
def add_table_schema(name, info)
|
170
|
+
cold_col_registry.add_schema(name, info)
|
171
|
+
end
|
172
|
+
|
173
|
+
def create_table_as(name, sql, options = {})
|
174
|
+
super.tap do |_|
|
175
|
+
record_table(name, columns_from_sql(sql))
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def create_table_from_generator(name, generator, options)
|
180
|
+
super.tap do |_|
|
181
|
+
record_table(name, columns_from_generator(generator))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def create_table_sql(name, generator, options)
|
186
|
+
super.tap do |_|
|
187
|
+
record_table(name, columns_from_generator(generator))
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def create_view_sql(name, source, options)
|
192
|
+
super.tap do |_|
|
193
|
+
record_view(name, columns_from_sql(source)) unless options[:dont_record]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def record_table(name, columns)
|
198
|
+
cold_col_registry.add_created_table(literal(name), columns)
|
199
|
+
end
|
200
|
+
|
201
|
+
def record_view(name, columns)
|
202
|
+
cold_col_registry.add_created_view(literal(name), columns)
|
203
|
+
end
|
204
|
+
|
205
|
+
def columns_from_sql(sql)
|
206
|
+
sql.columns
|
207
|
+
end
|
208
|
+
|
209
|
+
def columns_from_generator(generator)
|
210
|
+
generator.columns.map { |c| [c[:name], c] }
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
module ColdColDataset
|
216
|
+
|
217
|
+
# Return the columns for the dataset without executing a query
|
218
|
+
def columns
|
219
|
+
columns_search
|
220
|
+
end
|
221
|
+
|
222
|
+
def columns_search(opts_chain = nil)
|
223
|
+
if (cols = _columns)
|
224
|
+
return cols
|
225
|
+
end
|
226
|
+
|
227
|
+
unless (pcs = probable_columns(opts.merge(parent_opts: opts_chain))) && pcs.all?
|
228
|
+
raise("Failed to find columns for #{sql}")
|
229
|
+
end
|
230
|
+
|
231
|
+
self.columns = pcs
|
232
|
+
end
|
233
|
+
|
234
|
+
protected
|
235
|
+
|
236
|
+
WILDCARD = Sequel.lit('*').freeze
|
237
|
+
|
238
|
+
# Determine the probable columns for a dataset based on its query options.
|
239
|
+
# This is the main entry point for column determination logic.
|
240
|
+
#
|
241
|
+
# @param opts_chain [Hash] the dataset's query options
|
242
|
+
# @return [Array<Symbol>] array of probable column names
|
243
|
+
def probable_columns(opts_chain)
|
244
|
+
cols = opts_chain[:select]
|
245
|
+
|
246
|
+
return columns_from_sources(opts_chain) if cols.blank?
|
247
|
+
|
248
|
+
columns_from_select_list(cols, opts_chain)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Extract columns when no explicit SELECT list is present.
|
252
|
+
# Returns columns from all source tables (FROM and JOIN clauses).
|
253
|
+
#
|
254
|
+
# @param opts_chain [Hash] the dataset's query options
|
255
|
+
# @return [Array<Symbol>] array of column names from all sources
|
256
|
+
def columns_from_sources(opts_chain)
|
257
|
+
froms = opts_chain[:from] || []
|
258
|
+
joins = (opts_chain[:join] || []).map(&:table_expr)
|
259
|
+
(froms + joins).flat_map { |from| fetch_columns(from, opts_chain) }
|
260
|
+
end
|
261
|
+
|
262
|
+
# Extract columns from an explicit SELECT list.
|
263
|
+
# Handles three types of column specifications: *, table.*, and explicit columns.
|
264
|
+
#
|
265
|
+
# @param cols [Array] the SELECT column expressions
|
266
|
+
# @param opts_chain [Hash] the dataset's query options
|
267
|
+
# @return [Array<Symbol>] array of column names from the SELECT list
|
268
|
+
def columns_from_select_list(cols, opts_chain)
|
269
|
+
star_columns = extract_star_columns(cols, opts_chain)
|
270
|
+
table_star_columns = extract_table_star_columns(cols, opts_chain)
|
271
|
+
explicit_columns = extract_explicit_columns(cols)
|
272
|
+
|
273
|
+
(star_columns + table_star_columns + explicit_columns).flatten
|
274
|
+
end
|
275
|
+
|
276
|
+
# Extract columns when SELECT * is present.
|
277
|
+
# Returns all columns from tables in the FROM clause.
|
278
|
+
#
|
279
|
+
# @param cols [Array] the SELECT column expressions
|
280
|
+
# @param opts_chain [Hash] the dataset's query options
|
281
|
+
# @return [Array<Symbol>] array of column names from FROM sources
|
282
|
+
def extract_star_columns(cols, opts_chain)
|
283
|
+
return [] unless select_all?(cols)
|
284
|
+
|
285
|
+
(opts_chain[:from] || []).flat_map { |from| fetch_columns(from, opts_chain) }
|
286
|
+
end
|
287
|
+
|
288
|
+
# Extract columns from table.* expressions in the SELECT list.
|
289
|
+
# Handles cases like SELECT users.*, posts.title.
|
290
|
+
#
|
291
|
+
# @param cols [Array] the SELECT column expressions
|
292
|
+
# @param opts_chain [Hash] the dataset's query options
|
293
|
+
# @return [Array<Symbol>] array of column names from table.* expressions
|
294
|
+
def extract_table_star_columns(cols, opts_chain)
|
295
|
+
cols.select { |c| c.is_a?(Sequel::SQL::ColumnAll) }
|
296
|
+
.flat_map { |c| from_named_sources(c.table, opts_chain) }
|
297
|
+
end
|
298
|
+
|
299
|
+
# Extract explicitly named columns from the SELECT list.
|
300
|
+
# Handles individual column references and expressions with aliases.
|
301
|
+
#
|
302
|
+
# @param cols [Array] the SELECT column expressions
|
303
|
+
# @return [Array<Symbol>] array of explicit column names
|
304
|
+
def extract_explicit_columns(cols)
|
305
|
+
cols.reject { |c| c == WILDCARD || c.is_a?(Sequel::SQL::ColumnAll) }
|
306
|
+
.map { |c| probable_column_name(c) }
|
307
|
+
end
|
308
|
+
|
309
|
+
private
|
310
|
+
|
311
|
+
# Check if the SELECT list contains a wildcard (*) expression.
|
312
|
+
#
|
313
|
+
# @param cols [Array] the SELECT column expressions
|
314
|
+
# @return [Boolean] true if SELECT * is present
|
315
|
+
def select_all?(cols)
|
316
|
+
cols.any? { |c| c == WILDCARD }
|
317
|
+
end
|
318
|
+
|
319
|
+
# Find columns for a named source (table, view, alias, or CTE).
|
320
|
+
# This method searches through different types of named sources in order:
|
321
|
+
# 1. Aliased FROM expressions (e.g., FROM table AS alias)
|
322
|
+
# 2. Common Table Expressions (WITH clauses)
|
323
|
+
# 3. Aliased JOIN expressions (e.g., JOIN table AS alias)
|
324
|
+
# 4. Schema registry (created/loaded tables and views)
|
325
|
+
#
|
326
|
+
# @param name [String, Symbol] the source name to look up
|
327
|
+
# @param opts_chain [Hash] the dataset's query options
|
328
|
+
# @return [Array<Symbol>] array of column names for the source
|
329
|
+
# @raise [RuntimeError] if the source cannot be found
|
330
|
+
def from_named_sources(name, opts_chain)
|
331
|
+
# Try aliased FROM expressions first
|
332
|
+
if (columns = find_from_alias(name, opts_chain))
|
333
|
+
return columns
|
334
|
+
end
|
335
|
+
|
336
|
+
# Try CTE (WITH clause) expressions
|
337
|
+
if (columns = find_cte_columns(name, opts_chain))
|
338
|
+
return columns
|
339
|
+
end
|
340
|
+
|
341
|
+
# Try aliased JOIN expressions
|
342
|
+
if (columns = find_join_alias(name, opts_chain))
|
343
|
+
return columns
|
344
|
+
end
|
345
|
+
|
346
|
+
# Try registry lookup (created tables/views and loaded schemas)
|
347
|
+
if (columns = db.cold_col_registry.find_columns(name))
|
348
|
+
return columns
|
349
|
+
end
|
350
|
+
|
351
|
+
raise("Failed to find columns for #{literal(name)}")
|
352
|
+
end
|
353
|
+
|
354
|
+
# Find columns for an aliased FROM expression.
|
355
|
+
# Searches the FROM clause for expressions like "FROM table AS alias".
|
356
|
+
#
|
357
|
+
# @param name [String, Symbol] the alias name to find
|
358
|
+
# @param opts_chain [Hash] the dataset's query options
|
359
|
+
# @return [Array<Symbol>, nil] column names if found, nil otherwise
|
360
|
+
def find_from_alias(name, opts_chain)
|
361
|
+
from = (opts_chain[:from] || [])
|
362
|
+
.select { |f| f.is_a?(Sequel::SQL::AliasedExpression) }
|
363
|
+
.detect { |f| literal(f.alias) == literal(name) }
|
364
|
+
|
365
|
+
from&.expression&.columns_search(opts_chain)
|
366
|
+
end
|
367
|
+
|
368
|
+
# Find columns for a Common Table Expression (CTE).
|
369
|
+
# Searches up the query chain for WITH clauses that define the given name.
|
370
|
+
#
|
371
|
+
# @param name [String, Symbol] the CTE name to find
|
372
|
+
# @param opts_chain [Hash] the dataset's query options
|
373
|
+
# @return [Array<Symbol>, nil] column names if found, nil otherwise
|
374
|
+
def find_cte_columns(name, opts_chain)
|
375
|
+
current_opts = opts_chain
|
376
|
+
|
377
|
+
while current_opts.present?
|
378
|
+
with = (current_opts[:with] || []).detect { |wh| literal(wh[:name]) == literal(name) }
|
379
|
+
return with[:dataset].columns_search(opts_chain) if with
|
380
|
+
|
381
|
+
current_opts = current_opts[:parent_opts]
|
382
|
+
end
|
383
|
+
|
384
|
+
nil
|
385
|
+
end
|
386
|
+
|
387
|
+
# Find columns for an aliased JOIN expression.
|
388
|
+
# Searches JOIN clauses for expressions like "JOIN table AS alias".
|
389
|
+
#
|
390
|
+
# @param name [String, Symbol] the join alias name to find
|
391
|
+
# @param opts_chain [Hash] the dataset's query options
|
392
|
+
# @return [Array<Symbol>, nil] column names if found, nil otherwise
|
393
|
+
def find_join_alias(name, opts_chain)
|
394
|
+
join = (opts_chain[:join] || []).detect { |jc| literal(jc.table_expr.try(:alias)) == literal(name) }
|
395
|
+
return nil unless join
|
396
|
+
|
397
|
+
join_expr = join.table_expr.expression
|
398
|
+
return join_expr.columns_search(opts_chain) if join_expr.is_a?(Sequel::Dataset)
|
399
|
+
|
400
|
+
# If it's a table reference, look it up in the registry
|
401
|
+
db.cold_col_registry.find_columns(join_expr)
|
402
|
+
end
|
403
|
+
|
404
|
+
def fetch_columns(from, opts_chain)
|
405
|
+
from = from.expression if from.is_a?(SQL::AliasedExpression)
|
406
|
+
|
407
|
+
case from
|
408
|
+
when Dataset
|
409
|
+
from.columns_search(opts_chain)
|
410
|
+
when Symbol, SQL::Identifier, SQL::QualifiedIdentifier
|
411
|
+
from_named_sources(from, opts_chain)
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# Return the probable name of the column, or nil if one
|
416
|
+
# cannot be determined.
|
417
|
+
def probable_column_name(c)
|
418
|
+
case c
|
419
|
+
when Symbol
|
420
|
+
_, c, a = split_symbol(c)
|
421
|
+
(a || c).to_sym
|
422
|
+
when SQL::Identifier
|
423
|
+
c.value.to_sym
|
424
|
+
when SQL::QualifiedIdentifier
|
425
|
+
c.column.to_sym
|
426
|
+
when SQL::AliasedExpression
|
427
|
+
a = c.alias
|
428
|
+
a.is_a?(SQL::Identifier) ? a.value.to_sym : a.to_sym
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
end
|
433
|
+
|
434
|
+
Database.register_extension(:cold_col, Sequel::ColdColDatabase)
|
435
|
+
|
436
|
+
end
|
@@ -1,41 +1,102 @@
|
|
1
1
|
module Sequel
|
2
|
+
|
3
|
+
# = DbOpts
|
4
|
+
#
|
5
|
+
# Sequel extension that provides database-specific options handling.
|
6
|
+
# This extension allows setting database-specific configuration options
|
7
|
+
# that get applied during connection establishment.
|
8
|
+
#
|
9
|
+
# The extension looks for options in the database configuration that match
|
10
|
+
# the pattern `{database_type}_db_opt_{option_name}` and converts them to
|
11
|
+
# appropriate SQL SET statements.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# # For PostgreSQL, options like:
|
15
|
+
# postgres_db_opt_work_mem: '256MB'
|
16
|
+
# postgres_db_opt_shared_preload_libraries: 'pg_stat_statements'
|
17
|
+
#
|
18
|
+
# # Will generate:
|
19
|
+
# SET work_mem='256MB'
|
20
|
+
# SET shared_preload_libraries='pg_stat_statements'
|
2
21
|
module DbOpts
|
22
|
+
|
23
|
+
# Handles extraction and application of database-specific options.
|
3
24
|
class DbOptions
|
4
|
-
|
25
|
+
|
26
|
+
# @!attribute [r] db
|
27
|
+
# @return [Sequel::Database] the database instance
|
28
|
+
attr_reader :db
|
29
|
+
|
30
|
+
# Creates a new DbOptions instance for the given database.
|
31
|
+
#
|
32
|
+
# @param db [Sequel::Database] the database to configure
|
5
33
|
def initialize(db)
|
6
34
|
db.extension :settable
|
7
35
|
@db = db
|
8
36
|
end
|
9
37
|
|
38
|
+
# Returns a hash of database-specific options extracted from the database configuration.
|
39
|
+
#
|
40
|
+
# @return [Hash] hash of option names to values
|
10
41
|
def to_hash
|
11
42
|
@_to_hash ||= extract_db_opts
|
12
43
|
end
|
13
44
|
|
45
|
+
# Extracts database-specific options from the database configuration.
|
46
|
+
#
|
47
|
+
# Looks for options matching the pattern `{database_type}_db_opt_{option_name}`
|
48
|
+
# and extracts the option name and value.
|
49
|
+
#
|
50
|
+
# @return [Hash] extracted options with symbolic keys
|
14
51
|
def extract_db_opts
|
15
52
|
opt_regexp = /^#{db.database_type}_db_opt_/i
|
16
53
|
|
17
|
-
|
54
|
+
db.opts.select do |k, _|
|
55
|
+
k.to_s.match(opt_regexp)
|
56
|
+
end.to_h { |k, v| [k.to_s.gsub(opt_regexp, '').to_sym, prep_value(k, v)] }
|
18
57
|
end
|
19
58
|
|
59
|
+
# Applies the database options to the given connection.
|
60
|
+
#
|
61
|
+
# Executes the SQL statements generated from the options on the connection.
|
62
|
+
#
|
63
|
+
# @param c [Object] the database connection
|
20
64
|
def apply(c)
|
21
65
|
sql_statements.each do |stmt|
|
22
66
|
db.send(:log_connection_execute, c, stmt)
|
23
67
|
end
|
24
68
|
end
|
25
69
|
|
26
|
-
|
27
|
-
|
70
|
+
# Prepares a value for use in SQL statements.
|
71
|
+
#
|
72
|
+
# Values containing non-word characters are treated as literals and quoted,
|
73
|
+
# while simple values are used as-is.
|
74
|
+
#
|
75
|
+
# @param _k [Symbol] the option key (unused)
|
76
|
+
# @param v [Object] the option value
|
77
|
+
# @return [String] the prepared value
|
78
|
+
def prep_value(_k, v)
|
79
|
+
v =~ /\W/ ? db.literal(v.to_s) : v
|
28
80
|
end
|
29
81
|
|
82
|
+
# Generates SQL SET statements for the database options.
|
83
|
+
#
|
84
|
+
# @return [Array<String>] array of SQL SET statements
|
30
85
|
def sql_statements
|
31
86
|
db.send(:set_sql, to_hash)
|
32
87
|
end
|
88
|
+
|
33
89
|
end
|
34
90
|
|
91
|
+
# Returns a DbOptions instance for this database.
|
92
|
+
#
|
93
|
+
# @return [DbOptions] the database options handler
|
35
94
|
def db_opts
|
36
95
|
@db_opts ||= DbOptions.new(self)
|
37
96
|
end
|
97
|
+
|
38
98
|
end
|
39
99
|
|
40
100
|
Database.register_extension(:db_opts, DbOpts)
|
101
|
+
|
41
102
|
end
|