sequel-duckdb 0.1.0 → 0.2.1
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/.beads/.beads-credential-key +1 -0
- data/.beads/.gitignore +66 -0
- data/.beads/README.md +85 -0
- data/.beads/config.yaml +56 -0
- data/.beads/hooks/post-checkout +24 -0
- data/.beads/hooks/post-merge +24 -0
- data/.beads/hooks/pre-commit +24 -0
- data/.beads/hooks/pre-push +24 -0
- data/.beads/hooks/prepare-commit-msg +24 -0
- data/.beads/metadata.json +7 -0
- data/.kiro/specs/advanced-sql-features-implementation/design.md +3 -1
- data/.kiro/specs/advanced-sql-features-implementation/requirements.md +1 -1
- data/.kiro/specs/advanced-sql-features-implementation/tasks.md +5 -1
- data/.kiro/specs/duckdb-sql-syntax-compatibility/design.md +15 -1
- data/.kiro/specs/duckdb-sql-syntax-compatibility/requirements.md +1 -1
- data/.kiro/specs/duckdb-sql-syntax-compatibility/tasks.md +13 -0
- data/.kiro/specs/edge-cases-and-validation-fixes/requirements.md +1 -1
- data/.kiro/specs/integration-test-database-setup/requirements.md +1 -1
- data/.kiro/specs/sequel-duckdb-adapter/design.md +8 -1
- data/.kiro/specs/sequel-duckdb-adapter/requirements.md +10 -10
- data/.kiro/specs/sequel-duckdb-adapter/tasks.md +48 -3
- data/.kiro/specs/sql-expression-handling-fix/design.md +34 -1
- data/.kiro/specs/sql-expression-handling-fix/requirements.md +1 -1
- data/.kiro/specs/sql-expression-handling-fix/tasks.md +3 -0
- data/.kiro/specs/test-infrastructure-improvements/requirements.md +1 -1
- data/.kiro/steering/product.md +5 -1
- data/.kiro/steering/structure.md +1 -1
- data/.kiro/steering/tech.md +14 -1
- data/.kiro/steering/testing.md +22 -1
- data/.mdformat.toml +2 -0
- data/.rubocop.yml +116 -58
- data/.rubocop_todo.yml +323 -0
- data/AGENTS.md +180 -0
- data/API_DOCUMENTATION.md +73 -49
- data/CHANGELOG.md +47 -10
- data/FINAL_STATUS.md +99 -0
- data/LICENSE +1 -1
- data/MIGRATION_EXAMPLES.md +1 -1
- data/PERFORMANCE_OPTIMIZATIONS.md +4 -1
- data/README.md +90 -1
- data/REFACTORING_SUMMARY.md +264 -0
- data/Rakefile +21 -5
- data/TASK_10.2_IMPLEMENTATION_SUMMARY.md +19 -1
- data/docs/DUCKDB_SQL_PATTERNS.md +39 -1
- data/docs/TASK_12_VERIFICATION_SUMMARY.md +14 -1
- data/justfile +50 -0
- data/lib/sequel/adapters/duckdb.rb +137 -108
- data/lib/sequel/adapters/shared/duckdb.rb +292 -1490
- data/lib/sequel/duckdb/helpers/copier.rb +50 -0
- data/lib/sequel/duckdb/helpers/pathifier.rb +141 -0
- data/lib/sequel/duckdb/version.rb +2 -2
- data/plans/date_arithmetic.md +420 -0
- data/plans/engineering/Sequel.md +471 -0
- data/plans/engineering/duckdb.md +712 -0
- data/plans/engineering/sqlite.md +453 -0
- data/plans/mock_connection_bug.md +333 -0
- data/plans/mock_without_driver_gem.md +371 -0
- data/plans/over_engineering_analysis.md +122 -0
- data/plans/schema_management.md +383 -0
- metadata +47 -27
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "../../../sequel/duckdb"
|
|
4
|
+
require_relative "../../../sequel/duckdb/helpers/pathifier"
|
|
5
|
+
require_relative "../../../sequel/duckdb/helpers/copier"
|
|
4
6
|
|
|
5
7
|
# Sequel is the database toolkit for Ruby
|
|
6
8
|
module Sequel
|
|
@@ -58,185 +60,37 @@ module Sequel
|
|
|
58
60
|
true
|
|
59
61
|
end
|
|
60
62
|
|
|
61
|
-
|
|
63
|
+
# Error classification using DATABASE_ERROR_REGEXPS following SQLite pattern
|
|
64
|
+
DATABASE_ERROR_REGEXPS = {
|
|
65
|
+
/NOT NULL constraint failed/i => Sequel::NotNullConstraintViolation,
|
|
66
|
+
/UNIQUE constraint failed|PRIMARY KEY|duplicate/i => Sequel::UniqueConstraintViolation,
|
|
67
|
+
/FOREIGN KEY constraint failed/i => Sequel::ForeignKeyConstraintViolation,
|
|
68
|
+
/CHECK constraint failed/i => Sequel::CheckConstraintViolation,
|
|
69
|
+
/constraint failed/i => Sequel::ConstraintViolation
|
|
70
|
+
}.freeze
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
false
|
|
72
|
+
def database_error_regexps
|
|
73
|
+
DATABASE_ERROR_REGEXPS
|
|
66
74
|
end
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# Execute SQL statement
|
|
76
|
+
# Return a dataset that represents a VALUES clause with the given rows.
|
|
77
|
+
# DuckDB supports standard SQL VALUES syntax like PostgreSQL.
|
|
71
78
|
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
# Handle both old-style (sql, opts) and new-style (sql, params) calls
|
|
77
|
-
if opts.is_a?(Array)
|
|
78
|
-
params = opts
|
|
79
|
-
opts = {}
|
|
80
|
-
elsif opts.is_a?(Hash)
|
|
81
|
-
params = opts[:params] || []
|
|
82
|
-
else
|
|
83
|
-
# Handle other types (like strings) by treating as empty params
|
|
84
|
-
params = []
|
|
85
|
-
opts = {}
|
|
86
|
-
end
|
|
79
|
+
# DB.values([[1, 2], [3, 4]])
|
|
80
|
+
# # VALUES ((1, 2), (3, 4))
|
|
81
|
+
def values(v)
|
|
82
|
+
raise Error, "Cannot provide an empty array for values" if v.empty?
|
|
87
83
|
|
|
88
|
-
|
|
89
|
-
result = execute_statement(conn, sql, params, opts, &block)
|
|
90
|
-
|
|
91
|
-
# For UPDATE/DELETE operations without a block, return the number of affected rows
|
|
92
|
-
# This is what Sequel models expect
|
|
93
|
-
if !block && result.is_a?(::DuckDB::Result) \
|
|
94
|
-
&& (sql.strip.upcase.start_with?("UPDATE ") \
|
|
95
|
-
|| sql.strip.upcase.start_with?("DELETE "))
|
|
96
|
-
return result.rows_changed
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
return result
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Execute INSERT statement
|
|
104
|
-
#
|
|
105
|
-
# @param sql [String] INSERT SQL statement
|
|
106
|
-
# @param opts [Hash] Options for execution
|
|
107
|
-
# @return [Object] Result of execution
|
|
108
|
-
def execute_insert(sql, opts = {})
|
|
109
|
-
execute(sql, opts)
|
|
110
|
-
# For INSERT statements, we should return the inserted ID if possible
|
|
111
|
-
# Since DuckDB doesn't support AUTOINCREMENT, we'll return nil for now
|
|
112
|
-
# This matches the behavior expected by Sequel
|
|
113
|
-
nil
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Execute UPDATE statement
|
|
117
|
-
#
|
|
118
|
-
# @param sql [String] UPDATE SQL statement
|
|
119
|
-
# @param opts [Hash] Options for execution
|
|
120
|
-
# @return [Object] Result of execution
|
|
121
|
-
def execute_update(sql, opts = {})
|
|
122
|
-
result = execute(sql, opts)
|
|
123
|
-
# For UPDATE/DELETE statements, return the number of affected rows
|
|
124
|
-
# DuckDB::Result has a rows_changed method for affected row count
|
|
125
|
-
if result.respond_to?(:rows_changed)
|
|
126
|
-
result.rows_changed
|
|
127
|
-
else
|
|
128
|
-
# Fallback: try to get row count from result
|
|
129
|
-
result.is_a?(Integer) ? result : 0
|
|
130
|
-
end
|
|
84
|
+
@default_dataset.clone(values: v)
|
|
131
85
|
end
|
|
132
86
|
|
|
133
87
|
private
|
|
134
88
|
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
# @return [Array<Class>] Array of DuckDB error classes
|
|
138
|
-
def database_error_classes
|
|
139
|
-
[::DuckDB::Error]
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Extract SQL state from DuckDB exception if available
|
|
143
|
-
#
|
|
144
|
-
# @param _exception [::DuckDB::Error] The DuckDB exception
|
|
145
|
-
# @param _opts [Hash] Additional options
|
|
146
|
-
# @return [String, nil] SQL state code or nil if not available
|
|
147
|
-
def database_exception_sqlstate(_exception, _opts)
|
|
148
|
-
# DuckDB errors may not always have SQL state codes
|
|
149
|
-
# This can be enhanced when more detailed error information is available
|
|
150
|
-
nil
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Whether to use SQL states for exception handling
|
|
154
|
-
#
|
|
155
|
-
# @return [Boolean] true if SQL states should be used
|
|
156
|
-
def database_exception_use_sqlstates?
|
|
89
|
+
# DuckDB doesn't fold unquoted identifiers to uppercase
|
|
90
|
+
def folds_unquoted_identifiers_to_uppercase?
|
|
157
91
|
false
|
|
158
92
|
end
|
|
159
93
|
|
|
160
|
-
# Map DuckDB errors to appropriate Sequel exception types (Requirements 8.1, 8.2, 8.3, 8.7)
|
|
161
|
-
#
|
|
162
|
-
# @param exception [::DuckDB::Error] The DuckDB exception
|
|
163
|
-
# @param _opts [Hash] Additional options
|
|
164
|
-
# @return [Class] Sequel exception class to use
|
|
165
|
-
def database_exception_class(exception, _opts)
|
|
166
|
-
message = exception.message.to_s
|
|
167
|
-
|
|
168
|
-
# Map specific DuckDB error patterns to appropriate Sequel exceptions
|
|
169
|
-
case message
|
|
170
|
-
when /connection/i, /database.*not.*found/i, /cannot.*open/i
|
|
171
|
-
# Connection-related errors (Requirement 8.1)
|
|
172
|
-
Sequel::DatabaseConnectionError
|
|
173
|
-
when /violates.*not.*null/i, /not.*null.*constraint/i, /null.*value.*not.*allowed/i
|
|
174
|
-
# NOT NULL constraint violations (Requirement 8.3) - moved up for priority
|
|
175
|
-
Sequel::NotNullConstraintViolation
|
|
176
|
-
when /unique.*constraint/i, /duplicate.*key/i, /already.*exists/i,
|
|
177
|
-
/primary.*key.*constraint/i, /duplicate.*primary.*key/i
|
|
178
|
-
# UNIQUE and PRIMARY KEY constraint violations (Requirement 8.3)
|
|
179
|
-
# Primary key violations are a type of unique constraint
|
|
180
|
-
Sequel::UniqueConstraintViolation
|
|
181
|
-
when /foreign.*key.*constraint/i, /violates.*foreign.*key/i
|
|
182
|
-
# Foreign key constraint violations (Requirement 8.3)
|
|
183
|
-
Sequel::ForeignKeyConstraintViolation
|
|
184
|
-
when /check.*constraint/i, /violates.*check/i
|
|
185
|
-
# CHECK constraint violations (Requirement 8.3)
|
|
186
|
-
Sequel::CheckConstraintViolation
|
|
187
|
-
when /constraint.*violation/i, /violates.*constraint/i
|
|
188
|
-
# Generic constraint violations (Requirement 8.3) - moved to end for lower priority
|
|
189
|
-
Sequel::ConstraintViolation
|
|
190
|
-
else
|
|
191
|
-
# when /syntax.*error/i, /parse.*error/i, /unexpected.*token/i,
|
|
192
|
-
# /table.*does.*not.*exist/i, /relation.*does.*not.*exist/i,
|
|
193
|
-
# /no.*such.*table/i, /column.*does.*not.*exist/i,
|
|
194
|
-
# /no.*such.*column/i, /unknown.*column/i,
|
|
195
|
-
# /referenced.*column.*not.*found/i,
|
|
196
|
-
# /does.*not.*have.*a.*column/i, /schema.*does.*not.*exist/i,
|
|
197
|
-
# /no.*such.*schema/i, /function.*does.*not.*exist/i,
|
|
198
|
-
# /no.*such.*function/i, /unknown.*function/i, /type.*error/i,
|
|
199
|
-
# /cannot.*cast/i, /invalid.*type/i, /permission.*denied/i,
|
|
200
|
-
# /access.*denied/i, /insufficient.*privileges/i
|
|
201
|
-
# Various database errors (Requirements 8.2, 8.7):
|
|
202
|
-
# - SQL syntax errors
|
|
203
|
-
# - Table/column/schema/function not found errors
|
|
204
|
-
# - Type conversion errors
|
|
205
|
-
# - Permission/access errors
|
|
206
|
-
Sequel::DatabaseError
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# Enhanced error message formatting for better debugging (Requirements 8.2, 8.7)
|
|
211
|
-
#
|
|
212
|
-
# @param exception [::DuckDB::Error] The DuckDB exception
|
|
213
|
-
# @param opts [Hash] Additional options including SQL and parameters
|
|
214
|
-
# @return [String] Enhanced error message
|
|
215
|
-
def database_exception_message(exception, opts)
|
|
216
|
-
message = "DuckDB error: #{exception.message}"
|
|
217
|
-
|
|
218
|
-
# Add SQL context if available for better debugging
|
|
219
|
-
message += " -- SQL: #{opts[:sql]}" if opts[:sql]
|
|
220
|
-
|
|
221
|
-
# Add parameter context if available
|
|
222
|
-
message += " -- Parameters: #{opts[:params].inspect}" if opts[:params] && !opts[:params].empty?
|
|
223
|
-
|
|
224
|
-
message
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# Handle constraint violation errors with specific categorization (Requirement 8.3)
|
|
228
|
-
#
|
|
229
|
-
# @param exception [::DuckDB::Error] The DuckDB exception
|
|
230
|
-
# @param opts [Hash] Additional options
|
|
231
|
-
# @return [Exception] Appropriate Sequel constraint exception
|
|
232
|
-
def handle_constraint_violation(exception, opts = {})
|
|
233
|
-
message = database_exception_message(exception, opts)
|
|
234
|
-
exception_class = database_exception_class(exception, opts)
|
|
235
|
-
|
|
236
|
-
# Create the appropriate exception with enhanced message
|
|
237
|
-
exception_class.new(message)
|
|
238
|
-
end
|
|
239
|
-
|
|
240
94
|
# Schema introspection methods
|
|
241
95
|
|
|
242
96
|
# Parse table list from database
|
|
@@ -244,12 +98,18 @@ module Sequel
|
|
|
244
98
|
# @param opts [Hash] Options for table parsing
|
|
245
99
|
# @return [Array<Symbol>] Array of table names as symbols
|
|
246
100
|
def schema_parse_tables(opts = {})
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
sql =
|
|
101
|
+
schema_ref = opts[:schema] || "main"
|
|
102
|
+
|
|
103
|
+
sql = if schema_ref.is_a?(Sequel::SQL::QualifiedIdentifier)
|
|
104
|
+
"SELECT table_name FROM information_schema.tables " \
|
|
105
|
+
"WHERE table_catalog = '#{schema_ref.table}' AND table_schema = '#{schema_ref.column}' AND table_type = 'BASE TABLE'"
|
|
106
|
+
else
|
|
107
|
+
"SELECT table_name FROM information_schema.tables " \
|
|
108
|
+
"WHERE table_schema = '#{schema_ref}' AND table_type = 'BASE TABLE'"
|
|
109
|
+
end
|
|
250
110
|
|
|
251
111
|
tables = []
|
|
252
|
-
execute(sql
|
|
112
|
+
execute(sql) do |row|
|
|
253
113
|
tables << row[:table_name].to_sym
|
|
254
114
|
end
|
|
255
115
|
|
|
@@ -279,12 +139,12 @@ module Sequel
|
|
|
279
139
|
numeric_precision,
|
|
280
140
|
numeric_scale
|
|
281
141
|
FROM information_schema.columns
|
|
282
|
-
WHERE table_schema =
|
|
142
|
+
WHERE table_schema = '#{schema_name}' AND table_name = '#{table_name}'
|
|
283
143
|
ORDER BY ordinal_position
|
|
284
144
|
SQL
|
|
285
145
|
|
|
286
146
|
columns = []
|
|
287
|
-
execute(sql
|
|
147
|
+
execute(sql) do |row|
|
|
288
148
|
column_name = row[:column_name].to_sym
|
|
289
149
|
|
|
290
150
|
# Map DuckDB types to Sequel types
|
|
@@ -340,11 +200,11 @@ module Sequel
|
|
|
340
200
|
expressions,
|
|
341
201
|
sql
|
|
342
202
|
FROM duckdb_indexes()
|
|
343
|
-
WHERE schema_name =
|
|
203
|
+
WHERE schema_name = '#{schema_name}' AND table_name = '#{table_name}'
|
|
344
204
|
SQL
|
|
345
205
|
|
|
346
206
|
indexes = {}
|
|
347
|
-
execute(sql
|
|
207
|
+
execute(sql) do |row|
|
|
348
208
|
index_name = row[:index_name].to_sym
|
|
349
209
|
|
|
350
210
|
# Parse column expressions - DuckDB returns them as JSON array strings
|
|
@@ -377,19 +237,12 @@ module Sequel
|
|
|
377
237
|
#
|
|
378
238
|
# @raise [Sequel::DatabaseError] If the pragma setting is invalid or fails
|
|
379
239
|
#
|
|
380
|
-
# @example Set memory limit
|
|
381
|
-
# db.set_pragma("memory_limit", "2GB")
|
|
382
240
|
# db.set_pragma(:memory_limit, "1GB")
|
|
383
241
|
#
|
|
384
|
-
# @example Set thread count
|
|
385
|
-
# db.set_pragma("threads", 4)
|
|
386
242
|
#
|
|
387
|
-
# @example Enable/disable features
|
|
388
|
-
# db.set_pragma("enable_progress_bar", true)
|
|
389
243
|
# db.set_pragma("enable_profiling", false)
|
|
390
244
|
#
|
|
391
245
|
# @see configure_duckdb
|
|
392
|
-
# @since 0.1.0
|
|
393
246
|
def set_pragma(key, value)
|
|
394
247
|
# Convert key to string for consistency
|
|
395
248
|
pragma_key = key.to_s
|
|
@@ -424,22 +277,17 @@ module Sequel
|
|
|
424
277
|
#
|
|
425
278
|
# @raise [Sequel::DatabaseError] If any pragma setting fails
|
|
426
279
|
#
|
|
427
|
-
# @example Configure multiple settings
|
|
428
|
-
# db.configure_duckdb(
|
|
429
280
|
# memory_limit: "2GB",
|
|
430
281
|
# threads: 8,
|
|
431
282
|
# enable_progress_bar: true,
|
|
432
283
|
# default_order: "ASC"
|
|
433
284
|
# )
|
|
434
285
|
#
|
|
435
|
-
# @example Configure with string keys
|
|
436
|
-
# db.configure_duckdb(
|
|
437
286
|
# "memory_limit" => "1GB",
|
|
438
287
|
# "threads" => 4
|
|
439
288
|
# )
|
|
440
289
|
#
|
|
441
290
|
# @see set_pragma
|
|
442
|
-
# @since 0.1.0
|
|
443
291
|
def configure_duckdb(options = {})
|
|
444
292
|
return if options.empty?
|
|
445
293
|
|
|
@@ -451,16 +299,24 @@ module Sequel
|
|
|
451
299
|
|
|
452
300
|
# Check if table exists
|
|
453
301
|
#
|
|
454
|
-
# @param table_name [Symbol, String] Name of the table
|
|
302
|
+
# @param table_name [Symbol, String, Sequel::SQL::QualifiedIdentifier] Name of the table
|
|
455
303
|
# @param opts [Hash] Options
|
|
456
304
|
# @return [Boolean] true if table exists
|
|
457
305
|
def table_exists?(table_name, opts = {})
|
|
458
|
-
|
|
306
|
+
# Handle qualified identifiers (e.g., Sequel[:schema][:table])
|
|
307
|
+
if table_name.is_a?(Sequel::SQL::QualifiedIdentifier)
|
|
308
|
+
schema_name = table_name.table.to_s
|
|
309
|
+
table_name = table_name.column.to_s
|
|
310
|
+
else
|
|
311
|
+
schema_name = opts[:schema] || "main"
|
|
312
|
+
table_name = table_name.to_s
|
|
313
|
+
end
|
|
459
314
|
|
|
460
|
-
sql = "SELECT 1 FROM information_schema.tables
|
|
315
|
+
sql = "SELECT 1 FROM information_schema.tables " \
|
|
316
|
+
"WHERE table_schema = '#{schema_name}' AND table_name = '#{table_name}' LIMIT 1"
|
|
461
317
|
|
|
462
318
|
result = nil
|
|
463
|
-
execute(sql
|
|
319
|
+
execute(sql) do |_row|
|
|
464
320
|
result = true
|
|
465
321
|
end
|
|
466
322
|
|
|
@@ -475,15 +331,41 @@ module Sequel
|
|
|
475
331
|
schema_parse_tables(opts)
|
|
476
332
|
end
|
|
477
333
|
|
|
334
|
+
# Get list of views
|
|
335
|
+
#
|
|
336
|
+
# @param opts [Hash] Options
|
|
337
|
+
# @return [Array<Symbol>] Array of view names
|
|
338
|
+
def views(opts = {})
|
|
339
|
+
schema_ref = opts[:schema] || "main"
|
|
340
|
+
|
|
341
|
+
sql = if schema_ref.is_a?(Sequel::SQL::QualifiedIdentifier)
|
|
342
|
+
"SELECT table_name FROM information_schema.tables " \
|
|
343
|
+
"WHERE table_catalog = '#{schema_ref.table}' AND table_schema = '#{schema_ref.column}' AND table_type = 'VIEW'"
|
|
344
|
+
else
|
|
345
|
+
"SELECT table_name FROM information_schema.tables " \
|
|
346
|
+
"WHERE table_schema = '#{schema_ref}' AND table_type = 'VIEW'"
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
views = []
|
|
350
|
+
execute(sql) do |row|
|
|
351
|
+
views << row[:table_name].to_sym
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
views
|
|
355
|
+
end
|
|
356
|
+
|
|
478
357
|
# Get schema information for a table
|
|
479
358
|
#
|
|
480
|
-
# @param table_name [Symbol, String, Dataset] Name of the table or dataset
|
|
359
|
+
# @param table_name [Symbol, String, Dataset, QualifiedIdentifier] Name of the table or dataset
|
|
481
360
|
# @param opts [Hash] Options
|
|
482
361
|
# @return [Array<Array>] Schema information
|
|
483
362
|
def schema(table_name, opts = {})
|
|
484
|
-
# Handle
|
|
485
|
-
if table_name.is_a?(Sequel::
|
|
486
|
-
|
|
363
|
+
# Handle qualified identifiers (e.g., Sequel[:schema][:table])
|
|
364
|
+
if table_name.is_a?(Sequel::SQL::QualifiedIdentifier)
|
|
365
|
+
opts = opts.merge(schema: table_name.table.to_s)
|
|
366
|
+
actual_table_name = table_name.column.to_s.to_sym
|
|
367
|
+
elsif table_name.is_a?(Sequel::Dataset)
|
|
368
|
+
# Handle case where Sequel passes a Dataset object instead of table name
|
|
487
369
|
if table_name.opts[:from]&.first
|
|
488
370
|
actual_table_name = table_name.opts[:from].first
|
|
489
371
|
# Handle case where table name is wrapped in an identifier
|
|
@@ -494,7 +376,6 @@ module Sequel
|
|
|
494
376
|
raise Sequel::Error, "Cannot determine table name from dataset: #{table_name}" unless sql =~ /FROM\s+(\w+)/i
|
|
495
377
|
|
|
496
378
|
actual_table_name = ::Regexp.last_match(1).to_sym
|
|
497
|
-
|
|
498
379
|
end
|
|
499
380
|
else
|
|
500
381
|
actual_table_name = table_name
|
|
@@ -596,12 +477,12 @@ module Sequel
|
|
|
596
477
|
AND tc.table_schema = kcu.table_schema
|
|
597
478
|
AND tc.table_name = kcu.table_name
|
|
598
479
|
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
599
|
-
AND tc.table_schema =
|
|
600
|
-
AND tc.table_name =
|
|
480
|
+
AND tc.table_schema = '#{schema_name}'
|
|
481
|
+
AND tc.table_name = '#{table_name}'
|
|
601
482
|
SQL
|
|
602
483
|
|
|
603
484
|
primary_key_columns = []
|
|
604
|
-
execute(sql
|
|
485
|
+
execute(sql) do |row|
|
|
605
486
|
primary_key_columns << row[:column_name].to_sym
|
|
606
487
|
end
|
|
607
488
|
|
|
@@ -629,198 +510,19 @@ module Sequel
|
|
|
629
510
|
|
|
630
511
|
public
|
|
631
512
|
|
|
632
|
-
#
|
|
633
|
-
|
|
634
|
-
# Check if DuckDB supports savepoints for nested transactions
|
|
635
|
-
#
|
|
636
|
-
# @return [Boolean] true if savepoints are supported
|
|
513
|
+
# Transaction support - DuckDB has basic transaction support only
|
|
637
514
|
def supports_savepoints?
|
|
638
|
-
# DuckDB does not currently support SAVEPOINT/ROLLBACK TO SAVEPOINT syntax
|
|
639
|
-
# Nested transactions are handled by Sequel's default behavior
|
|
640
515
|
false
|
|
641
516
|
end
|
|
642
517
|
|
|
643
|
-
# Check if DuckDB supports the specified transaction isolation level
|
|
644
|
-
#
|
|
645
|
-
# @param _level [Symbol] Isolation level (:read_uncommitted, :read_committed, :repeatable_read, :serializable)
|
|
646
|
-
# @return [Boolean] true if the isolation level is supported
|
|
647
518
|
def supports_transaction_isolation_level?(_level)
|
|
648
|
-
# DuckDB does not currently support setting transaction isolation levels
|
|
649
|
-
# It uses a default isolation level similar to READ_COMMITTED
|
|
650
519
|
false
|
|
651
520
|
end
|
|
652
521
|
|
|
653
|
-
# Check if DuckDB supports manual transaction control
|
|
654
|
-
#
|
|
655
|
-
# @return [Boolean] true if manual transaction control is supported
|
|
656
522
|
def supports_manual_transaction_control?
|
|
657
|
-
# DuckDB supports BEGIN, COMMIT, and ROLLBACK statements
|
|
658
523
|
true
|
|
659
524
|
end
|
|
660
525
|
|
|
661
|
-
# Check if DuckDB supports autocommit control
|
|
662
|
-
#
|
|
663
|
-
# @return [Boolean] true if autocommit can be controlled
|
|
664
|
-
def supports_autocommit_control?
|
|
665
|
-
# DuckDB has autocommit behavior but limited control over it
|
|
666
|
-
false
|
|
667
|
-
end
|
|
668
|
-
|
|
669
|
-
# Check if DuckDB supports disabling autocommit
|
|
670
|
-
#
|
|
671
|
-
# @return [Boolean] true if autocommit can be disabled
|
|
672
|
-
def supports_autocommit_disable?
|
|
673
|
-
# DuckDB doesn't support disabling autocommit mode
|
|
674
|
-
false
|
|
675
|
-
end
|
|
676
|
-
|
|
677
|
-
# Check if currently in a transaction
|
|
678
|
-
#
|
|
679
|
-
# @return [Boolean] true if in a transaction
|
|
680
|
-
def in_transaction?
|
|
681
|
-
# Use Sequel's built-in transaction tracking
|
|
682
|
-
# Sequel tracks transaction state internally
|
|
683
|
-
@transactions && !@transactions.empty?
|
|
684
|
-
end
|
|
685
|
-
|
|
686
|
-
# Begin a transaction manually
|
|
687
|
-
# Sequel calls this with (conn, opts) arguments
|
|
688
|
-
#
|
|
689
|
-
# @param conn [::DuckDB::Connection] Database connection
|
|
690
|
-
# @param opts [Hash] Transaction options
|
|
691
|
-
# @return [void]
|
|
692
|
-
def begin_transaction(conn, opts = {})
|
|
693
|
-
if opts[:isolation]
|
|
694
|
-
isolation_sql = case opts[:isolation]
|
|
695
|
-
when :read_uncommitted
|
|
696
|
-
"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"
|
|
697
|
-
when :read_committed
|
|
698
|
-
"SET TRANSACTION ISOLATION LEVEL READ COMMITTED"
|
|
699
|
-
else
|
|
700
|
-
raise Sequel::DatabaseError, "Unsupported isolation level: #{opts[:isolation]}"
|
|
701
|
-
end
|
|
702
|
-
conn.query(isolation_sql)
|
|
703
|
-
end
|
|
704
|
-
|
|
705
|
-
conn.query("BEGIN TRANSACTION")
|
|
706
|
-
end
|
|
707
|
-
|
|
708
|
-
# Commit the current transaction manually
|
|
709
|
-
# Sequel calls this with (conn, opts) arguments
|
|
710
|
-
#
|
|
711
|
-
# @param conn [::DuckDB::Connection] Database connection
|
|
712
|
-
# @param _opts [Hash] Options
|
|
713
|
-
# @return [void]
|
|
714
|
-
def commit_transaction(conn, _opts = {})
|
|
715
|
-
conn.query("COMMIT")
|
|
716
|
-
end
|
|
717
|
-
|
|
718
|
-
# Rollback the current transaction manually
|
|
719
|
-
# Sequel calls this with (conn, opts) arguments
|
|
720
|
-
#
|
|
721
|
-
# @param conn [::DuckDB::Connection] Database connection
|
|
722
|
-
# @param _opts [Hash] Options
|
|
723
|
-
# @return [void]
|
|
724
|
-
def rollback_transaction(conn, _opts = {})
|
|
725
|
-
conn.query("ROLLBACK")
|
|
726
|
-
end
|
|
727
|
-
|
|
728
|
-
# Override Sequel's transaction method to support advanced features
|
|
729
|
-
def transaction(opts = {}, &)
|
|
730
|
-
# Handle savepoint transactions (nested transactions)
|
|
731
|
-
return savepoint_transaction(opts, &) if opts[:savepoint] && supports_savepoints?
|
|
732
|
-
|
|
733
|
-
# Handle isolation level setting
|
|
734
|
-
if opts[:isolation] && supports_transaction_isolation_level?(opts[:isolation])
|
|
735
|
-
return isolation_transaction(
|
|
736
|
-
opts,
|
|
737
|
-
&
|
|
738
|
-
)
|
|
739
|
-
end
|
|
740
|
-
|
|
741
|
-
# Fall back to standard Sequel transaction handling
|
|
742
|
-
super
|
|
743
|
-
end
|
|
744
|
-
|
|
745
|
-
private
|
|
746
|
-
|
|
747
|
-
# Handle savepoint-based nested transactions
|
|
748
|
-
#
|
|
749
|
-
# @param opts [Hash] Transaction options
|
|
750
|
-
# @return [Object] Result of the transaction block
|
|
751
|
-
def savepoint_transaction(opts = {})
|
|
752
|
-
# Generate a unique savepoint name
|
|
753
|
-
savepoint_name = "sp_#{Time.now.to_f.to_s.gsub(".", "_")}"
|
|
754
|
-
|
|
755
|
-
synchronize(opts[:server]) do |conn|
|
|
756
|
-
# Create savepoint
|
|
757
|
-
conn.query("SAVEPOINT #{savepoint_name}")
|
|
758
|
-
|
|
759
|
-
# Execute the block
|
|
760
|
-
result = yield
|
|
761
|
-
|
|
762
|
-
# Release savepoint on success
|
|
763
|
-
conn.query("RELEASE SAVEPOINT #{savepoint_name}")
|
|
764
|
-
|
|
765
|
-
result
|
|
766
|
-
rescue Sequel::Rollback
|
|
767
|
-
# Rollback to savepoint on explicit rollback
|
|
768
|
-
conn.query("ROLLBACK TO SAVEPOINT #{savepoint_name}")
|
|
769
|
-
conn.query("RELEASE SAVEPOINT #{savepoint_name}")
|
|
770
|
-
nil
|
|
771
|
-
rescue StandardError => e
|
|
772
|
-
# Rollback to savepoint on any other exception
|
|
773
|
-
begin
|
|
774
|
-
conn.query("ROLLBACK TO SAVEPOINT #{savepoint_name}")
|
|
775
|
-
conn.query("RELEASE SAVEPOINT #{savepoint_name}")
|
|
776
|
-
rescue ::DuckDB::Error
|
|
777
|
-
# Ignore errors during rollback cleanup
|
|
778
|
-
end
|
|
779
|
-
raise e
|
|
780
|
-
end
|
|
781
|
-
end
|
|
782
|
-
|
|
783
|
-
# Handle transactions with specific isolation levels
|
|
784
|
-
#
|
|
785
|
-
# @param opts [Hash] Transaction options including :isolation
|
|
786
|
-
# @return [Object] Result of the transaction block
|
|
787
|
-
def isolation_transaction(opts = {})
|
|
788
|
-
synchronize(opts[:server]) do |conn|
|
|
789
|
-
# Set isolation level before beginning transaction
|
|
790
|
-
isolation_sql = case opts[:isolation]
|
|
791
|
-
when :read_uncommitted
|
|
792
|
-
"SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"
|
|
793
|
-
when :read_committed
|
|
794
|
-
"SET TRANSACTION ISOLATION LEVEL READ COMMITTED"
|
|
795
|
-
else
|
|
796
|
-
raise Sequel::DatabaseError, "Unsupported isolation level: #{opts[:isolation]}"
|
|
797
|
-
end
|
|
798
|
-
|
|
799
|
-
conn.query(isolation_sql)
|
|
800
|
-
conn.query("BEGIN TRANSACTION")
|
|
801
|
-
|
|
802
|
-
# Execute the block
|
|
803
|
-
result = yield
|
|
804
|
-
|
|
805
|
-
# Commit on success
|
|
806
|
-
conn.query("COMMIT")
|
|
807
|
-
|
|
808
|
-
result
|
|
809
|
-
rescue Sequel::Rollback
|
|
810
|
-
# Rollback on explicit rollback
|
|
811
|
-
conn.query("ROLLBACK")
|
|
812
|
-
nil
|
|
813
|
-
rescue StandardError => e
|
|
814
|
-
# Rollback on any other exception
|
|
815
|
-
begin
|
|
816
|
-
conn.query("ROLLBACK")
|
|
817
|
-
rescue ::DuckDB::Error
|
|
818
|
-
# Ignore errors during rollback cleanup
|
|
819
|
-
end
|
|
820
|
-
raise e
|
|
821
|
-
end
|
|
822
|
-
end
|
|
823
|
-
|
|
824
526
|
# DuckDB-specific schema generation methods
|
|
825
527
|
|
|
826
528
|
# Generate SQL for primary key column
|
|
@@ -896,276 +598,190 @@ module Sequel
|
|
|
896
598
|
end
|
|
897
599
|
end
|
|
898
600
|
|
|
899
|
-
#
|
|
900
|
-
#
|
|
901
|
-
# @param conn [::DuckDB::Connection] Database connection (already connected)
|
|
902
|
-
# @param sql [String] SQL statement to execute
|
|
903
|
-
# @param params [Array] Parameters for prepared statement
|
|
904
|
-
# @param _opts [Hash] Options for execution
|
|
905
|
-
# @return [Object] Result of execution
|
|
906
|
-
def execute_statement(conn, sql, params = [], _opts = {})
|
|
907
|
-
# Log the SQL query with timing information (Requirements 8.4, 8.5)
|
|
908
|
-
start_time = Time.now
|
|
909
|
-
|
|
910
|
-
begin
|
|
911
|
-
# Log the SQL query before execution
|
|
912
|
-
log_sql_query(sql, params)
|
|
913
|
-
|
|
914
|
-
# Handle parameterized queries
|
|
915
|
-
if params && !params.empty?
|
|
916
|
-
# Prepare statement with ? placeholders
|
|
917
|
-
stmt = conn.prepare(sql)
|
|
918
|
-
|
|
919
|
-
# Bind parameters using 1-based indexing
|
|
920
|
-
params.each_with_index do |param, index|
|
|
921
|
-
stmt.bind(index + 1, param)
|
|
922
|
-
end
|
|
601
|
+
# Schema management methods (Requirements: schema creation, deletion, introspection)
|
|
923
602
|
|
|
924
|
-
|
|
925
|
-
result = stmt.execute
|
|
926
|
-
else
|
|
927
|
-
# Execute directly without parameters
|
|
928
|
-
result = conn.query(sql)
|
|
929
|
-
end
|
|
930
|
-
|
|
931
|
-
# Log timing information for the operation
|
|
932
|
-
end_time = Time.now
|
|
933
|
-
execution_time = end_time - start_time
|
|
934
|
-
log_sql_timing(sql, execution_time)
|
|
935
|
-
|
|
936
|
-
if block_given?
|
|
937
|
-
# Get column names from the result
|
|
938
|
-
columns = result.columns
|
|
939
|
-
|
|
940
|
-
# Iterate through each row
|
|
941
|
-
result.each do |row_array|
|
|
942
|
-
# Convert array to hash with column names as keys
|
|
943
|
-
row_hash = {}
|
|
944
|
-
columns.each_with_index do |column, index|
|
|
945
|
-
# DuckDB::Column objects have a name method
|
|
946
|
-
column_name = column.respond_to?(:name) ? column.name : column.to_s
|
|
947
|
-
row_hash[column_name.to_sym] = row_array[index]
|
|
948
|
-
end
|
|
949
|
-
yield row_hash
|
|
950
|
-
end
|
|
951
|
-
else
|
|
952
|
-
result
|
|
953
|
-
end
|
|
954
|
-
rescue ::DuckDB::Error => e
|
|
955
|
-
# Log the error for debugging (Requirement 8.6)
|
|
956
|
-
end_time = Time.now
|
|
957
|
-
execution_time = end_time - start_time
|
|
958
|
-
log_sql_error(sql, params, e, execution_time)
|
|
959
|
-
|
|
960
|
-
# Use enhanced error mapping for better exception categorization (Requirements 8.1, 8.2, 8.3, 8.7)
|
|
961
|
-
error_opts = { sql: sql, params: params }
|
|
962
|
-
exception_class = database_exception_class(e, error_opts)
|
|
963
|
-
enhanced_message = database_exception_message(e, error_opts)
|
|
964
|
-
|
|
965
|
-
raise exception_class, enhanced_message
|
|
966
|
-
rescue StandardError => e
|
|
967
|
-
# Log unexpected errors
|
|
968
|
-
end_time = Time.now
|
|
969
|
-
execution_time = end_time - start_time
|
|
970
|
-
log_sql_error(sql, params, e, execution_time)
|
|
971
|
-
raise e
|
|
972
|
-
end
|
|
973
|
-
end
|
|
974
|
-
|
|
975
|
-
# Log SQL query execution (Requirement 8.4)
|
|
603
|
+
# Create a schema
|
|
976
604
|
#
|
|
977
|
-
# @param
|
|
978
|
-
# @param
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
if params && !params.empty?
|
|
983
|
-
# Log parameterized query with parameters
|
|
984
|
-
log_info("SQL Query: #{sql} -- Parameters: #{params.inspect}")
|
|
985
|
-
else
|
|
986
|
-
# Log simple query
|
|
987
|
-
log_info("SQL Query: #{sql}")
|
|
988
|
-
end
|
|
989
|
-
end
|
|
990
|
-
|
|
991
|
-
# Log SQL query timing information (Requirement 8.5)
|
|
605
|
+
# @param name [String, Symbol] Schema name
|
|
606
|
+
# @param opts [Hash] Options
|
|
607
|
+
# @option opts [Boolean] :if_not_exists Add IF NOT EXISTS clause
|
|
608
|
+
# @option opts [Boolean] :or_replace Add OR REPLACE clause (mutually exclusive with :if_not_exists)
|
|
609
|
+
# @return [void]
|
|
992
610
|
#
|
|
993
|
-
# @param sql [String] SQL statement
|
|
994
|
-
# @param execution_time [Float] Time taken to execute in seconds
|
|
995
|
-
def log_sql_timing(sql, execution_time)
|
|
996
|
-
return unless log_connection_info?
|
|
997
|
-
|
|
998
|
-
# Log timing information, highlighting slow operations
|
|
999
|
-
time_ms = (execution_time * 1000).round(2)
|
|
1000
|
-
|
|
1001
|
-
if execution_time > 1.0 # Log slow operations (> 1 second) as warnings
|
|
1002
|
-
log_warn("SLOW SQL Query (#{time_ms}ms): #{sql}")
|
|
1003
|
-
else
|
|
1004
|
-
log_info("SQL Query completed in #{time_ms}ms")
|
|
1005
|
-
end
|
|
1006
|
-
end
|
|
1007
|
-
|
|
1008
|
-
# Log SQL query errors (Requirement 8.6)
|
|
1009
611
|
#
|
|
1010
|
-
# @param sql [String] SQL statement that failed
|
|
1011
|
-
# @param params [Array] Parameters for the query
|
|
1012
|
-
# @param error [Exception] The error that occurred
|
|
1013
|
-
# @param execution_time [Float] Time taken before error
|
|
1014
|
-
def log_sql_error(sql, params, error, execution_time)
|
|
1015
|
-
return unless log_connection_info?
|
|
1016
|
-
|
|
1017
|
-
time_ms = (execution_time * 1000).round(2)
|
|
1018
|
-
|
|
1019
|
-
if params && !params.empty?
|
|
1020
|
-
log_error("SQL Error after #{time_ms}ms: #{error.message} -- SQL: #{sql} -- Parameters: #{params.inspect}")
|
|
1021
|
-
else
|
|
1022
|
-
log_error("SQL Error after #{time_ms}ms: #{error.message} -- SQL: #{sql}")
|
|
1023
|
-
end
|
|
1024
|
-
end
|
|
1025
|
-
|
|
1026
|
-
# Check if connection info should be logged
|
|
1027
612
|
#
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
# Use Sequel's built-in logging mechanism
|
|
1031
|
-
!loggers.empty?
|
|
613
|
+
def create_schema(name, opts = OPTS)
|
|
614
|
+
self << create_schema_sql(name, opts)
|
|
1032
615
|
end
|
|
1033
616
|
|
|
1034
|
-
#
|
|
617
|
+
# Generate SQL for creating a schema
|
|
1035
618
|
#
|
|
1036
|
-
# @param
|
|
1037
|
-
|
|
1038
|
-
|
|
619
|
+
# @param name [String, Symbol] Schema name
|
|
620
|
+
# @param opts [Hash] Options
|
|
621
|
+
# @option opts [Boolean] :if_not_exists Add IF NOT EXISTS clause
|
|
622
|
+
# @option opts [Boolean] :or_replace Add OR REPLACE clause (mutually exclusive with :if_not_exists)
|
|
623
|
+
# @return [String] CREATE SCHEMA SQL
|
|
624
|
+
def create_schema_sql(name, opts = OPTS)
|
|
625
|
+
# DuckDB doesn't support both OR REPLACE and IF NOT EXISTS together
|
|
626
|
+
if opts[:or_replace] && opts[:if_not_exists]
|
|
627
|
+
raise Sequel::Error, "Cannot use both :or_replace and :if_not_exists options"
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
sql = "CREATE"
|
|
631
|
+
sql += " OR REPLACE" if opts[:or_replace]
|
|
632
|
+
sql += " SCHEMA"
|
|
633
|
+
sql += " IF NOT EXISTS" if opts[:if_not_exists]
|
|
634
|
+
sql += " #{quote_identifier(name)}"
|
|
635
|
+
sql
|
|
1039
636
|
end
|
|
1040
637
|
|
|
1041
|
-
#
|
|
638
|
+
# Drop a schema
|
|
639
|
+
#
|
|
640
|
+
# @param name [String, Symbol] Schema name
|
|
641
|
+
# @param opts [Hash] Options
|
|
642
|
+
# @option opts [Boolean] :if_exists Add IF EXISTS clause
|
|
643
|
+
# @option opts [Boolean] :cascade Add CASCADE clause to drop dependent objects
|
|
644
|
+
# @return [void]
|
|
645
|
+
#
|
|
646
|
+
#
|
|
1042
647
|
#
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
648
|
+
def drop_schema(name, opts = OPTS)
|
|
649
|
+
self << drop_schema_sql(name, opts)
|
|
650
|
+
remove_all_cached_schemas
|
|
1046
651
|
end
|
|
1047
652
|
|
|
1048
|
-
#
|
|
653
|
+
# Generate SQL for dropping a schema
|
|
1049
654
|
#
|
|
1050
|
-
# @param
|
|
1051
|
-
|
|
1052
|
-
|
|
655
|
+
# @param name [String, Symbol] Schema name
|
|
656
|
+
# @param opts [Hash] Options
|
|
657
|
+
# @option opts [Boolean] :if_exists Add IF EXISTS clause
|
|
658
|
+
# @option opts [Boolean] :cascade Add CASCADE clause to drop dependent objects
|
|
659
|
+
# @return [String] DROP SCHEMA SQL
|
|
660
|
+
def drop_schema_sql(name, opts = OPTS)
|
|
661
|
+
sql = "DROP SCHEMA"
|
|
662
|
+
sql += " IF EXISTS" if opts[:if_exists]
|
|
663
|
+
sql += " #{quote_identifier(name)}"
|
|
664
|
+
sql += " CASCADE" if opts[:cascade]
|
|
665
|
+
sql
|
|
1053
666
|
end
|
|
1054
667
|
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
# EXPLAIN functionality access for query plans (Requirement 9.6)
|
|
668
|
+
# Remove all cached schema information
|
|
1058
669
|
#
|
|
1059
|
-
#
|
|
1060
|
-
#
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
end
|
|
1068
|
-
|
|
1069
|
-
plan_rows
|
|
670
|
+
# Called after schema modifications to ensure cache consistency
|
|
671
|
+
#
|
|
672
|
+
# @return [void]
|
|
673
|
+
def remove_all_cached_schemas
|
|
674
|
+
@schema_cache = {}
|
|
675
|
+
@schemas = {}
|
|
676
|
+
@primary_keys = {}
|
|
677
|
+
@primary_key_sequences = {}
|
|
1070
678
|
end
|
|
1071
679
|
|
|
1072
|
-
#
|
|
680
|
+
# List all schemas in the database
|
|
681
|
+
#
|
|
682
|
+
# @param opts [Hash] Options
|
|
683
|
+
# @option opts [String] :catalog Catalog name to filter by
|
|
684
|
+
# @return [Array<Symbol>] Array of schema names as symbols
|
|
1073
685
|
#
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
plan_rows = explain_query(sql)
|
|
686
|
+
def schemas(opts = OPTS)
|
|
687
|
+
sql = "SELECT schema_name FROM information_schema.schemata"
|
|
688
|
+
sql += " WHERE catalog_name = '#{opts[:catalog]}'" if opts[:catalog]
|
|
1078
689
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
# Format the plan rows into a readable string
|
|
1083
|
-
plan_rows.map { |row| row.values.join(" | ") }.join("\n")
|
|
690
|
+
schemas = []
|
|
691
|
+
execute(sql) do |row|
|
|
692
|
+
schemas << row[:schema_name].to_sym
|
|
1084
693
|
end
|
|
1085
|
-
end
|
|
1086
694
|
|
|
1087
|
-
|
|
1088
|
-
#
|
|
1089
|
-
# @return [Boolean] true if EXPLAIN is supported
|
|
1090
|
-
def supports_explain?
|
|
1091
|
-
true # DuckDB supports EXPLAIN
|
|
695
|
+
schemas
|
|
1092
696
|
end
|
|
1093
697
|
|
|
1094
|
-
#
|
|
698
|
+
# Check if a schema exists
|
|
1095
699
|
#
|
|
1096
|
-
# @param
|
|
1097
|
-
# @
|
|
1098
|
-
|
|
1099
|
-
{
|
|
1100
|
-
plan: query_plan(sql),
|
|
1101
|
-
explain_output: explain_query(sql),
|
|
1102
|
-
supports_explain: supports_explain?
|
|
1103
|
-
}
|
|
1104
|
-
end
|
|
1105
|
-
|
|
1106
|
-
# DuckDB configuration methods for performance optimization
|
|
1107
|
-
|
|
1108
|
-
# Set DuckDB configuration value
|
|
700
|
+
# @param name [String, Symbol] Schema name
|
|
701
|
+
# @param opts [Hash] Options (reserved for future use)
|
|
702
|
+
# @return [Boolean] true if schema exists
|
|
1109
703
|
#
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
def set_config_value(key, value)
|
|
1113
|
-
synchronize do |conn|
|
|
1114
|
-
# Use PRAGMA for DuckDB configuration
|
|
1115
|
-
conn.query("PRAGMA #{key} = #{value}")
|
|
1116
|
-
end
|
|
1117
|
-
end
|
|
704
|
+
def schema_exists?(name, _opts = OPTS)
|
|
705
|
+
sql = "SELECT 1 FROM information_schema.schemata WHERE schema_name = '#{name}' LIMIT 1"
|
|
1118
706
|
|
|
1119
|
-
# Get DuckDB configuration value
|
|
1120
|
-
#
|
|
1121
|
-
# @param key [String] Configuration key
|
|
1122
|
-
# @return [Object] Configuration value
|
|
1123
|
-
def get_config_value(key)
|
|
1124
707
|
result = nil
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
conn.query("PRAGMA #{key}") do |row|
|
|
1128
|
-
result = row.values.first
|
|
1129
|
-
break
|
|
1130
|
-
end
|
|
708
|
+
execute(sql) do |_row|
|
|
709
|
+
result = true
|
|
1131
710
|
end
|
|
1132
|
-
|
|
711
|
+
|
|
712
|
+
!!result
|
|
1133
713
|
end
|
|
1134
714
|
|
|
1135
|
-
#
|
|
715
|
+
# Override create_view_prefix_sql to support DuckDB options
|
|
1136
716
|
#
|
|
1137
|
-
# @param
|
|
1138
|
-
|
|
1139
|
-
|
|
717
|
+
# @param name [Symbol, String] View name
|
|
718
|
+
# @param options [Hash] View options
|
|
719
|
+
# @option options [Boolean] :temp Create a TEMPORARY view
|
|
720
|
+
# @option options [Boolean] :replace Use OR REPLACE
|
|
721
|
+
# @option options [Array<Symbol>] :columns Column names for the view
|
|
722
|
+
# @return [String] CREATE VIEW prefix SQL
|
|
723
|
+
def create_view_prefix_sql(name, options)
|
|
724
|
+
sql = String.new
|
|
725
|
+
sql << "CREATE "
|
|
726
|
+
sql << "OR REPLACE " if options[:replace]
|
|
727
|
+
sql << "TEMPORARY " if options[:temp]
|
|
728
|
+
sql << "VIEW #{quote_schema_table(name)}"
|
|
729
|
+
|
|
730
|
+
# Add columns if specified
|
|
731
|
+
if options[:columns]
|
|
732
|
+
sql << " ("
|
|
733
|
+
schema_utility_dataset.send(:identifier_list_append, sql, options[:columns])
|
|
734
|
+
sql << ")"
|
|
735
|
+
end
|
|
1140
736
|
|
|
1141
|
-
|
|
1142
|
-
set_config_value("enable_optimizer", true)
|
|
1143
|
-
set_config_value("enable_profiling", false) # Disable for performance
|
|
737
|
+
sql
|
|
1144
738
|
end
|
|
1145
739
|
|
|
1146
|
-
#
|
|
1147
|
-
#
|
|
1148
|
-
# @param
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
740
|
+
# Override create_view_sql to handle DuckDB-specific view sources
|
|
741
|
+
#
|
|
742
|
+
# @param name [Symbol, String] View name
|
|
743
|
+
# @param source [String, Dataset, Hash] View source - can be SQL, Dataset, or Hash for parquet files
|
|
744
|
+
# @param options [Hash] View options
|
|
745
|
+
# @option options [String] :using Data source type (e.g., "parquet")
|
|
746
|
+
# @option options [Hash] :options Options for the data source (e.g., path for parquet)
|
|
747
|
+
# @return [String] CREATE VIEW SQL
|
|
748
|
+
def create_view_sql(name, source, options = OPTS)
|
|
749
|
+
options = source if source.is_a?(Hash)
|
|
750
|
+
some_paths = options[:path] || options[:paths] || options.dig(:options, :path) || options.dig(:options, :paths)
|
|
751
|
+
# Handle DuckDB-specific source patterns
|
|
752
|
+
source = if options[:using] || some_paths
|
|
753
|
+
# Build read_parquet or similar function call
|
|
754
|
+
read_something_sql(some_paths, options).then do |read_stmt|
|
|
755
|
+
from(read_stmt)
|
|
756
|
+
end.sql
|
|
757
|
+
elsif source.is_a?(Dataset)
|
|
758
|
+
source.sql
|
|
759
|
+
elsif source.is_a?(String)
|
|
760
|
+
source
|
|
761
|
+
else
|
|
762
|
+
raise Sequel::Error, "Unsupported source type: #{source.class}"
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
sql = String.new
|
|
766
|
+
sql << "#{create_view_prefix_sql(name, options)} AS #{source}"
|
|
767
|
+
|
|
768
|
+
if (check = options[:check])
|
|
769
|
+
sql << " WITH#{" LOCAL" if check == :local} CHECK OPTION"
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
sql
|
|
1152
773
|
end
|
|
1153
774
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
set_config_value("enable_progress_bar", false)
|
|
775
|
+
def copy_to(source, path, options = Sequel::OPTS)
|
|
776
|
+
Sequel::DuckDB::Helpers::Copier.new(source, path, options).then do |copier|
|
|
777
|
+
run(copier.to_sql)
|
|
778
|
+
end
|
|
1159
779
|
end
|
|
1160
780
|
|
|
1161
781
|
private
|
|
1162
782
|
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
require "etc"
|
|
1166
|
-
Etc.nprocessors
|
|
1167
|
-
rescue StandardError
|
|
1168
|
-
4 # Default fallback
|
|
783
|
+
def read_something_sql(paths, options = OPTS)
|
|
784
|
+
Sequel::DuckDB::Helpers::Pathifier.new(paths, options).to_sql
|
|
1169
785
|
end
|
|
1170
786
|
|
|
1171
787
|
# Type conversion methods for DuckDB-specific handling
|
|
@@ -1236,6 +852,19 @@ module Sequel
|
|
|
1236
852
|
array struct map
|
|
1237
853
|
].freeze
|
|
1238
854
|
|
|
855
|
+
# DuckDB interval unit mapping for date arithmetic
|
|
856
|
+
DUCKDB_DURATION_UNITS = {
|
|
857
|
+
years: "YEAR",
|
|
858
|
+
months: "MONTH",
|
|
859
|
+
days: "DAY",
|
|
860
|
+
hours: "HOUR",
|
|
861
|
+
minutes: "MINUTE",
|
|
862
|
+
seconds: "SECOND"
|
|
863
|
+
}.freeze
|
|
864
|
+
|
|
865
|
+
# Override select SQL clause order to support VALUES
|
|
866
|
+
Dataset.def_sql_method(self, :select, [["if opts[:values]", %w[values compounds order limit]], ["else", %w[with select distinct columns from join where group having compounds order limit lock]]])
|
|
867
|
+
|
|
1239
868
|
private
|
|
1240
869
|
|
|
1241
870
|
# DuckDB uses lowercase identifiers
|
|
@@ -1262,83 +891,12 @@ module Sequel
|
|
|
1262
891
|
DUCKDB_RESERVED_WORDS.include?(name.to_s.downcase)
|
|
1263
892
|
end
|
|
1264
893
|
|
|
1265
|
-
#
|
|
1266
|
-
#
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
# Handle empty values case
|
|
1273
|
-
if values.empty? || (values.length == 1 && values.first.empty?)
|
|
1274
|
-
return "INSERT INTO #{table_name_sql} DEFAULT VALUES"
|
|
1275
|
-
end
|
|
1276
|
-
|
|
1277
|
-
# Handle single hash of values
|
|
1278
|
-
if values.length == 1 && values.first.is_a?(Hash)
|
|
1279
|
-
values_hash = values.first
|
|
1280
|
-
columns = values_hash.keys
|
|
1281
|
-
column_list = literal(columns)
|
|
1282
|
-
values_list = literal(columns.map { |k| values_hash[k] })
|
|
1283
|
-
|
|
1284
|
-
return "INSERT INTO #{table_name_sql} #{column_list} VALUES #{values_list}"
|
|
1285
|
-
end
|
|
1286
|
-
|
|
1287
|
-
# Handle array of hashes (multiple records)
|
|
1288
|
-
if values.length == 1 && values.first.is_a?(Array)
|
|
1289
|
-
records = values.first
|
|
1290
|
-
return "INSERT INTO #{table_name_sql} DEFAULT VALUES" if records.empty?
|
|
1291
|
-
|
|
1292
|
-
first_record = records.first
|
|
1293
|
-
columns = first_record.keys
|
|
1294
|
-
column_list = literal(columns)
|
|
1295
|
-
|
|
1296
|
-
values_lists = records.map do |record|
|
|
1297
|
-
literal(columns.map { |k| record[k] })
|
|
1298
|
-
end
|
|
1299
|
-
|
|
1300
|
-
return "INSERT INTO #{table_name_sql} #{column_list} VALUES #{values_lists.join(", ")}"
|
|
1301
|
-
end
|
|
1302
|
-
|
|
1303
|
-
# Fallback for other cases
|
|
1304
|
-
"INSERT INTO #{table_name_sql} DEFAULT VALUES"
|
|
1305
|
-
end
|
|
1306
|
-
|
|
1307
|
-
# Generate UPDATE SQL statement
|
|
1308
|
-
#
|
|
1309
|
-
# @param values [Hash] Values to update
|
|
1310
|
-
# @return [String] The UPDATE SQL statement
|
|
1311
|
-
def update_sql(values = {})
|
|
1312
|
-
return @opts[:sql] if @opts[:sql]
|
|
1313
|
-
|
|
1314
|
-
sql = "UPDATE #{table_name_sql} SET "
|
|
1315
|
-
|
|
1316
|
-
# Add SET clause
|
|
1317
|
-
set_clauses = values.map do |column, value|
|
|
1318
|
-
col_sql = String.new
|
|
1319
|
-
quote_identifier_append(col_sql, column)
|
|
1320
|
-
"#{col_sql} = #{literal(value)}"
|
|
1321
|
-
end
|
|
1322
|
-
sql << set_clauses.join(", ")
|
|
1323
|
-
|
|
1324
|
-
# Add WHERE clause
|
|
1325
|
-
select_where_sql(sql) if @opts[:where]
|
|
1326
|
-
|
|
1327
|
-
sql
|
|
1328
|
-
end
|
|
1329
|
-
|
|
1330
|
-
# Generate DELETE SQL statement
|
|
1331
|
-
#
|
|
1332
|
-
# @return [String] The DELETE SQL statement
|
|
1333
|
-
def delete_sql
|
|
1334
|
-
return @opts[:sql] if @opts[:sql]
|
|
1335
|
-
|
|
1336
|
-
sql = "DELETE FROM #{table_name_sql}"
|
|
1337
|
-
|
|
1338
|
-
# Add WHERE clause
|
|
1339
|
-
select_where_sql(sql) if @opts[:where]
|
|
1340
|
-
|
|
1341
|
-
sql
|
|
894
|
+
# DuckDB doesn't support "schema"."table".* syntax.
|
|
895
|
+
# Strip the schema qualifier so it generates "table".* instead.
|
|
896
|
+
def column_all_sql_append(sql, ca)
|
|
897
|
+
table = ca.table
|
|
898
|
+
table = table.column if table.is_a?(Sequel::SQL::QualifiedIdentifier)
|
|
899
|
+
qualified_identifier_sql_append(sql, table, Sequel::LiteralString.new("*"))
|
|
1342
900
|
end
|
|
1343
901
|
|
|
1344
902
|
# DuckDB capability flags
|
|
@@ -1358,259 +916,35 @@ module Sequel
|
|
|
1358
916
|
true
|
|
1359
917
|
end
|
|
1360
918
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
# Validate table name for SELECT operations
|
|
1366
|
-
def validate_table_name_for_select
|
|
1367
|
-
return unless @opts[:from] # Skip if no FROM clause
|
|
1368
|
-
|
|
1369
|
-
@opts[:from].each do |table|
|
|
1370
|
-
if table.nil? || (table.respond_to?(:to_s) && table.to_s.strip.empty?)
|
|
1371
|
-
raise ArgumentError,
|
|
1372
|
-
"Table name cannot be nil or empty"
|
|
1373
|
-
end
|
|
1374
|
-
end
|
|
919
|
+
# DuckDB supports multi-row inserts using VALUES syntax
|
|
920
|
+
# This allows inserting multiple rows in a single INSERT statement
|
|
921
|
+
def multi_insert_sql_strategy
|
|
922
|
+
:values
|
|
1375
923
|
end
|
|
1376
924
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
%w[order group select from where having limit offset].include?(word.downcase)
|
|
925
|
+
def supports_join_using?
|
|
926
|
+
true
|
|
1380
927
|
end
|
|
1381
928
|
|
|
1382
|
-
#
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
# Check if the table name is nil
|
|
1387
|
-
table_name = @opts[:from].first
|
|
1388
|
-
raise ArgumentError, "Table name cannot be nil" if table_name.nil?
|
|
1389
|
-
|
|
1390
|
-
table_name = table_name.to_s
|
|
1391
|
-
raise ArgumentError, "Table name cannot be empty" if table_name.empty?
|
|
1392
|
-
|
|
1393
|
-
# Use quote_identifier_append to respect quote_identifiers? setting
|
|
1394
|
-
sql = String.new
|
|
1395
|
-
quote_identifier_append(sql, table_name)
|
|
1396
|
-
sql
|
|
929
|
+
# DuckDB requires WITH RECURSIVE if any CTE is recursive
|
|
930
|
+
# This follows the same pattern as PostgreSQL
|
|
931
|
+
def select_with_sql_base
|
|
932
|
+
opts[:with].any? { |w| w[:recursive] } ? "WITH RECURSIVE " : "WITH "
|
|
1397
933
|
end
|
|
1398
934
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
return unless opts[:with]
|
|
1404
|
-
|
|
1405
|
-
# Check if any WITH clause is recursive (either explicitly marked or auto-detected)
|
|
1406
|
-
has_recursive = opts[:with].any? { |w| w[:recursive] || cte_is_recursive?(w) }
|
|
1407
|
-
|
|
1408
|
-
# Add WITH or WITH RECURSIVE prefix
|
|
1409
|
-
sql << (has_recursive ? "WITH RECURSIVE " : "WITH ")
|
|
1410
|
-
|
|
1411
|
-
# Add each CTE
|
|
1412
|
-
opts[:with].each_with_index do |w, i|
|
|
1413
|
-
sql << ", " if i.positive?
|
|
1414
|
-
name_sql = String.new
|
|
1415
|
-
quote_identifier_append(name_sql, w[:name])
|
|
1416
|
-
sql << "#{name_sql} AS (#{w[:dataset].sql})"
|
|
1417
|
-
end
|
|
1418
|
-
|
|
1419
|
-
sql << " "
|
|
935
|
+
# Support VALUES clause instead of SELECT to return rows.
|
|
936
|
+
def select_values_sql(sql)
|
|
937
|
+
sql << "VALUES "
|
|
938
|
+
expression_list_append(sql, opts[:values])
|
|
1420
939
|
end
|
|
1421
940
|
|
|
1422
|
-
#
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
# @return [Boolean] true if the CTE appears to be recursive
|
|
1426
|
-
def cte_is_recursive?(cte_info)
|
|
1427
|
-
return false unless cte_info[:dataset]
|
|
941
|
+
# Return true if this dataset uses a VALUES clause (not a real table query).
|
|
942
|
+
def empty?
|
|
943
|
+
return false if @opts[:values]
|
|
1428
944
|
|
|
1429
|
-
cte_name = cte_info[:name].to_s
|
|
1430
|
-
cte_sql = cte_info[:dataset].sql
|
|
1431
|
-
|
|
1432
|
-
# Check if the CTE SQL contains references to its own name
|
|
1433
|
-
# Look for patterns like "FROM table_name" or "JOIN table_name"
|
|
1434
|
-
# Use word boundaries to avoid false positives with partial matches
|
|
1435
|
-
recursive_pattern = /\b(?:FROM|JOIN)\s+#{Regexp.escape(cte_name)}\b/i
|
|
1436
|
-
|
|
1437
|
-
cte_sql.match?(recursive_pattern)
|
|
1438
|
-
end
|
|
1439
|
-
|
|
1440
|
-
public
|
|
1441
|
-
|
|
1442
|
-
# Override select_from_sql to validate table names
|
|
1443
|
-
def select_from_sql(sql)
|
|
1444
|
-
if (f = @opts[:from])
|
|
1445
|
-
# Validate that no table names are nil
|
|
1446
|
-
f.each do |table|
|
|
1447
|
-
raise ArgumentError, "Table name cannot be nil" if table.nil?
|
|
1448
|
-
end
|
|
1449
|
-
end
|
|
1450
|
-
|
|
1451
|
-
# Call parent implementation
|
|
1452
945
|
super
|
|
1453
946
|
end
|
|
1454
947
|
|
|
1455
|
-
# Add JOIN clauses to SQL (Requirement 6.9)
|
|
1456
|
-
def select_join_sql(sql)
|
|
1457
|
-
return unless @opts[:join]
|
|
1458
|
-
|
|
1459
|
-
@opts[:join].each do |join| # rubocop:disable Metrics/BlockLength
|
|
1460
|
-
# Handle different join clause types
|
|
1461
|
-
case join
|
|
1462
|
-
when Sequel::SQL::JoinOnClause
|
|
1463
|
-
join_type = join.join_type || :inner
|
|
1464
|
-
table = join.table
|
|
1465
|
-
conditions = join.on
|
|
1466
|
-
|
|
1467
|
-
# Format join type
|
|
1468
|
-
join_clause = case join_type
|
|
1469
|
-
when :left, :left_outer
|
|
1470
|
-
"LEFT JOIN"
|
|
1471
|
-
when :right, :right_outer
|
|
1472
|
-
"RIGHT JOIN"
|
|
1473
|
-
when :full, :full_outer
|
|
1474
|
-
"FULL JOIN"
|
|
1475
|
-
else
|
|
1476
|
-
# when :inner
|
|
1477
|
-
"INNER JOIN"
|
|
1478
|
-
end
|
|
1479
|
-
|
|
1480
|
-
sql << " #{join_clause} "
|
|
1481
|
-
|
|
1482
|
-
# Add table name
|
|
1483
|
-
sql << if table.is_a?(Sequel::Dataset)
|
|
1484
|
-
alias_sql = String.new
|
|
1485
|
-
quote_identifier_append(alias_sql, join.table_alias || "subquery")
|
|
1486
|
-
"(#{table.sql}) AS #{alias_sql}"
|
|
1487
|
-
else
|
|
1488
|
-
literal(table)
|
|
1489
|
-
end
|
|
1490
|
-
|
|
1491
|
-
# Add ON conditions
|
|
1492
|
-
if conditions
|
|
1493
|
-
sql << " ON "
|
|
1494
|
-
literal_append(sql, conditions)
|
|
1495
|
-
end
|
|
1496
|
-
|
|
1497
|
-
when Sequel::SQL::JoinUsingClause
|
|
1498
|
-
join_type = join.join_type || :inner
|
|
1499
|
-
table = join.table
|
|
1500
|
-
using_columns = join.using
|
|
1501
|
-
|
|
1502
|
-
join_clause = case join_type
|
|
1503
|
-
when :left, :left_outer
|
|
1504
|
-
"LEFT JOIN"
|
|
1505
|
-
when :right, :right_outer
|
|
1506
|
-
"RIGHT JOIN"
|
|
1507
|
-
when :full, :full_outer
|
|
1508
|
-
"FULL JOIN"
|
|
1509
|
-
else
|
|
1510
|
-
# when :inner
|
|
1511
|
-
"INNER JOIN"
|
|
1512
|
-
end
|
|
1513
|
-
|
|
1514
|
-
sql << " #{join_clause} "
|
|
1515
|
-
|
|
1516
|
-
# Handle table with alias
|
|
1517
|
-
sql << if table.is_a?(Sequel::Dataset)
|
|
1518
|
-
# Subquery with alias
|
|
1519
|
-
"(#{table.sql})"
|
|
1520
|
-
else
|
|
1521
|
-
# Regular table (may have alias)
|
|
1522
|
-
literal(table)
|
|
1523
|
-
# Add alias if present
|
|
1524
|
-
end
|
|
1525
|
-
if join.table_alias
|
|
1526
|
-
sql << " AS "
|
|
1527
|
-
quote_identifier_append(sql, join.table_alias)
|
|
1528
|
-
end
|
|
1529
|
-
|
|
1530
|
-
if using_columns
|
|
1531
|
-
sql << " USING ("
|
|
1532
|
-
Array(using_columns).each_with_index do |col, i|
|
|
1533
|
-
sql << ", " if i.positive?
|
|
1534
|
-
quote_identifier_append(sql, col)
|
|
1535
|
-
end
|
|
1536
|
-
sql << ")"
|
|
1537
|
-
end
|
|
1538
|
-
|
|
1539
|
-
when Sequel::SQL::JoinClause
|
|
1540
|
-
join_type = join.join_type || :inner
|
|
1541
|
-
table = join.table
|
|
1542
|
-
|
|
1543
|
-
join_clause = case join_type
|
|
1544
|
-
when :cross
|
|
1545
|
-
"CROSS JOIN"
|
|
1546
|
-
when :natural
|
|
1547
|
-
"NATURAL JOIN"
|
|
1548
|
-
else
|
|
1549
|
-
"INNER JOIN"
|
|
1550
|
-
end
|
|
1551
|
-
|
|
1552
|
-
sql << " #{join_clause} "
|
|
1553
|
-
sql << literal(table)
|
|
1554
|
-
end
|
|
1555
|
-
end
|
|
1556
|
-
end
|
|
1557
|
-
|
|
1558
|
-
# Add WHERE clause to SQL (enhanced for complex conditions - Requirement 6.4)
|
|
1559
|
-
def select_where_sql(sql)
|
|
1560
|
-
return unless @opts[:where]
|
|
1561
|
-
|
|
1562
|
-
sql << " WHERE "
|
|
1563
|
-
literal_append(sql, @opts[:where])
|
|
1564
|
-
end
|
|
1565
|
-
|
|
1566
|
-
# Add GROUP BY clause to SQL (Requirement 6.7)
|
|
1567
|
-
def select_group_sql(sql)
|
|
1568
|
-
return unless @opts[:group]
|
|
1569
|
-
|
|
1570
|
-
sql << " GROUP BY "
|
|
1571
|
-
if @opts[:group].is_a?(Array)
|
|
1572
|
-
sql << @opts[:group].map { |col| literal(col) }.join(", ")
|
|
1573
|
-
else
|
|
1574
|
-
literal_append(sql, @opts[:group])
|
|
1575
|
-
end
|
|
1576
|
-
end
|
|
1577
|
-
|
|
1578
|
-
# Add HAVING clause to SQL (Requirement 6.8)
|
|
1579
|
-
def select_having_sql(sql)
|
|
1580
|
-
return unless @opts[:having]
|
|
1581
|
-
|
|
1582
|
-
sql << " HAVING "
|
|
1583
|
-
literal_append(sql, @opts[:having])
|
|
1584
|
-
end
|
|
1585
|
-
|
|
1586
|
-
# Add ORDER BY clause to SQL (enhanced - Requirement 6.5)
|
|
1587
|
-
def select_order_sql(sql)
|
|
1588
|
-
return unless @opts[:order]
|
|
1589
|
-
|
|
1590
|
-
sql << " ORDER BY "
|
|
1591
|
-
sql << if @opts[:order].is_a?(Array)
|
|
1592
|
-
@opts[:order].map { |col| order_column_sql(col) }.join(", ")
|
|
1593
|
-
else
|
|
1594
|
-
order_column_sql(@opts[:order])
|
|
1595
|
-
end
|
|
1596
|
-
end
|
|
1597
|
-
|
|
1598
|
-
# Format individual ORDER BY column
|
|
1599
|
-
def order_column_sql(column)
|
|
1600
|
-
case column
|
|
1601
|
-
when Sequel::SQL::OrderedExpression
|
|
1602
|
-
col_sql = literal(column.expression)
|
|
1603
|
-
col_sql << (column.descending ? " DESC" : " ASC")
|
|
1604
|
-
# Check if nulls option exists (may not be available in all Sequel versions)
|
|
1605
|
-
if column.respond_to?(:nulls) && column.nulls
|
|
1606
|
-
col_sql << (column.nulls == :first ? " NULLS FIRST" : " NULLS LAST")
|
|
1607
|
-
end
|
|
1608
|
-
col_sql
|
|
1609
|
-
else
|
|
1610
|
-
literal(column)
|
|
1611
|
-
end
|
|
1612
|
-
end
|
|
1613
|
-
|
|
1614
948
|
# DuckDB-specific SQL generation enhancements
|
|
1615
949
|
|
|
1616
950
|
# Override complex_expression_sql_append for DuckDB-specific handling
|
|
@@ -1666,20 +1000,6 @@ module Sequel
|
|
|
1666
1000
|
end
|
|
1667
1001
|
end
|
|
1668
1002
|
|
|
1669
|
-
# Override join method to support USING clause syntax
|
|
1670
|
-
def join(table, expr = nil, options = {})
|
|
1671
|
-
# Handle the case where using parameter is passed
|
|
1672
|
-
if options.is_a?(Hash) && options[:using]
|
|
1673
|
-
using_columns = Array(options[:using])
|
|
1674
|
-
join_type = options[:type] || :inner
|
|
1675
|
-
join_clause = Sequel::SQL::JoinUsingClause.new(using_columns, join_type, table)
|
|
1676
|
-
clone(join: (@opts[:join] || []) + [join_clause])
|
|
1677
|
-
else
|
|
1678
|
-
# Fall back to standard Sequel join behavior
|
|
1679
|
-
super
|
|
1680
|
-
end
|
|
1681
|
-
end
|
|
1682
|
-
|
|
1683
1003
|
# Override literal methods for DuckDB-specific formatting
|
|
1684
1004
|
def literal_string_append(sql, string)
|
|
1685
1005
|
sql << "'" << string.gsub("'", "''") << "'"
|
|
@@ -1766,568 +1086,50 @@ module Sequel
|
|
|
1766
1086
|
"'#{blob.unpack1("H*")}'"
|
|
1767
1087
|
end
|
|
1768
1088
|
|
|
1769
|
-
#
|
|
1770
|
-
|
|
1771
|
-
#
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
# Apply row_proc if it exists (for model instantiation)
|
|
1777
|
-
row_proc = @row_proc || opts[:row_proc]
|
|
1778
|
-
processed_row = row_proc ? row_proc.call(row) : row
|
|
1779
|
-
records << processed_row
|
|
1780
|
-
end
|
|
1781
|
-
records
|
|
1782
|
-
end
|
|
1783
|
-
|
|
1784
|
-
# Insert a record into the dataset's table
|
|
1785
|
-
#
|
|
1786
|
-
# @param values [Hash] Column values to insert
|
|
1787
|
-
# @return [Integer, nil] Number of affected rows (always nil for DuckDB due to no AUTOINCREMENT)
|
|
1788
|
-
def insert(values = {})
|
|
1789
|
-
sql = insert_sql(values)
|
|
1790
|
-
result = db.execute(sql)
|
|
1791
|
-
|
|
1792
|
-
# For DuckDB, we need to return the number of affected rows
|
|
1793
|
-
# Since DuckDB doesn't support AUTOINCREMENT, we return nil for the ID
|
|
1794
|
-
# but we should return 1 to indicate successful insertion
|
|
1795
|
-
if result.is_a?(::DuckDB::Result)
|
|
1796
|
-
# DuckDB::Result doesn't have a direct way to get affected rows for INSERT
|
|
1797
|
-
# For INSERT operations, if no error occurred, assume 1 row was affected
|
|
1798
|
-
1
|
|
1799
|
-
else
|
|
1800
|
-
result
|
|
1801
|
-
end
|
|
1802
|
-
end
|
|
1803
|
-
|
|
1804
|
-
# Update records in the dataset
|
|
1805
|
-
#
|
|
1806
|
-
# @param values [Hash] Column values to update
|
|
1807
|
-
# @return [Integer] Number of affected rows
|
|
1808
|
-
def update(values = {})
|
|
1809
|
-
sql = update_sql(values)
|
|
1810
|
-
# Use execute_update which properly returns the row count
|
|
1811
|
-
db.execute_update(sql)
|
|
1812
|
-
end
|
|
1813
|
-
|
|
1814
|
-
# Delete records from the dataset
|
|
1815
|
-
#
|
|
1816
|
-
# @return [Integer] Number of affected rows
|
|
1817
|
-
def delete
|
|
1818
|
-
sql = delete_sql
|
|
1819
|
-
# Use execute_update which properly returns the row count
|
|
1820
|
-
db.execute_update(sql)
|
|
1821
|
-
end
|
|
1822
|
-
|
|
1823
|
-
# Streaming result support where possible (Requirement 9.5)
|
|
1824
|
-
#
|
|
1825
|
-
# @param sql [String] SQL to execute
|
|
1826
|
-
# @yield [Hash] Block to process each row
|
|
1827
|
-
# @return [Enumerator] If no block given, returns enumerator
|
|
1828
|
-
def stream(sql = select_sql, &)
|
|
1829
|
-
if block_given?
|
|
1830
|
-
# Stream results by processing them one at a time
|
|
1831
|
-
fetch_rows(sql, &)
|
|
1832
|
-
else
|
|
1833
|
-
# Return enumerator for lazy evaluation
|
|
1834
|
-
enum_for(:stream, sql)
|
|
1835
|
-
end
|
|
1836
|
-
end
|
|
1837
|
-
|
|
1838
|
-
# Performance optimization methods (Requirements 9.1, 9.2, 9.3, 9.4)
|
|
1839
|
-
# These methods are public to provide enhanced performance capabilities
|
|
1840
|
-
|
|
1841
|
-
# Optimized fetch_rows method for large result sets (Requirement 9.1)
|
|
1842
|
-
# This method provides efficient row fetching with streaming capabilities
|
|
1843
|
-
# Override the existing fetch_rows method to make it public and optimized
|
|
1844
|
-
def fetch_rows(sql)
|
|
1845
|
-
# Use streaming approach to avoid loading all results into memory at once
|
|
1846
|
-
# This is particularly important for large result sets
|
|
1847
|
-
if block_given?
|
|
1848
|
-
# Get schema information for type conversion
|
|
1849
|
-
table_schema = table_schema_for_conversion
|
|
1850
|
-
|
|
1851
|
-
# Execute with type conversion
|
|
1852
|
-
db.execute(sql) do |row|
|
|
1853
|
-
# Apply type conversion for TIME columns
|
|
1854
|
-
converted_row = convert_row_types(row, table_schema)
|
|
1855
|
-
yield converted_row
|
|
1856
|
-
end
|
|
1857
|
-
else
|
|
1858
|
-
# Return enumerator if no block given (for compatibility)
|
|
1859
|
-
enum_for(:fetch_rows, sql)
|
|
1860
|
-
end
|
|
1861
|
-
end
|
|
1862
|
-
|
|
1863
|
-
private
|
|
1864
|
-
|
|
1865
|
-
# Get table schema information for type conversion
|
|
1866
|
-
def table_schema_for_conversion
|
|
1867
|
-
return nil unless @opts[:from]&.first
|
|
1868
|
-
|
|
1869
|
-
table_name = @opts[:from].first
|
|
1870
|
-
# Handle case where table name is wrapped in an identifier
|
|
1871
|
-
table_name = table_name.value if table_name.respond_to?(:value)
|
|
1872
|
-
|
|
1873
|
-
begin
|
|
1874
|
-
schema_info = db.schema(table_name)
|
|
1875
|
-
schema_hash = {}
|
|
1876
|
-
schema_info.each do |column_name, column_info|
|
|
1877
|
-
schema_hash[column_name] = column_info
|
|
1878
|
-
end
|
|
1879
|
-
schema_hash
|
|
1880
|
-
rescue StandardError
|
|
1881
|
-
# If schema lookup fails, return nil to skip type conversion
|
|
1882
|
-
nil
|
|
1883
|
-
end
|
|
1884
|
-
end
|
|
1885
|
-
|
|
1886
|
-
# Convert row values based on column types
|
|
1887
|
-
def convert_row_types(row, table_schema)
|
|
1888
|
-
return row unless table_schema
|
|
1889
|
-
|
|
1890
|
-
converted_row = {}
|
|
1891
|
-
row.each do |column_name, value|
|
|
1892
|
-
column_info = table_schema[column_name]
|
|
1893
|
-
converted_row[column_name] = if column_info && column_info[:type] == :time && value.is_a?(Time)
|
|
1894
|
-
# Convert TIME columns to time-only values
|
|
1895
|
-
Time.local(1970, 1, 1, value.hour, value.min, value.sec, value.usec)
|
|
1896
|
-
else
|
|
1897
|
-
value
|
|
1898
|
-
end
|
|
1899
|
-
end
|
|
1900
|
-
converted_row
|
|
1901
|
-
end
|
|
1902
|
-
|
|
1903
|
-
public
|
|
1904
|
-
|
|
1905
|
-
# Enhanced bulk insert optimization (Requirement 9.3)
|
|
1906
|
-
# Override multi_insert to use DuckDB's efficient bulk loading capabilities
|
|
1907
|
-
def multi_insert(columns = nil, &)
|
|
1908
|
-
if columns.is_a?(Array) && !columns.empty? && columns.first.is_a?(Hash)
|
|
1909
|
-
# Handle array of hashes (most common case)
|
|
1910
|
-
bulk_insert_optimized(columns)
|
|
1911
|
-
else
|
|
1912
|
-
# Fall back to standard Sequel behavior for other cases
|
|
1913
|
-
super
|
|
1914
|
-
end
|
|
1915
|
-
end
|
|
1916
|
-
|
|
1917
|
-
# Optimized bulk insert implementation using DuckDB's capabilities
|
|
1918
|
-
def bulk_insert_optimized(rows)
|
|
1919
|
-
return 0 if rows.empty?
|
|
1920
|
-
|
|
1921
|
-
# Get column names from first row
|
|
1922
|
-
columns = rows.first.keys
|
|
1923
|
-
|
|
1924
|
-
# Get table name from opts[:from]
|
|
1925
|
-
table_name = @opts[:from].first
|
|
1926
|
-
|
|
1927
|
-
# Build optimized INSERT statement with VALUES clause
|
|
1928
|
-
# DuckDB handles multiple VALUES efficiently
|
|
1929
|
-
values_placeholders = rows.map { |_| "(#{columns.map { "?" }.join(", ")})" }.join(", ")
|
|
1930
|
-
table_sql = String.new
|
|
1931
|
-
quote_identifier_append(table_sql, table_name)
|
|
1932
|
-
col_list = columns.map do |c|
|
|
1933
|
-
col_sql = String.new
|
|
1934
|
-
quote_identifier_append(col_sql, c)
|
|
1935
|
-
col_sql
|
|
1936
|
-
end.join(", ")
|
|
1937
|
-
sql = "INSERT INTO #{table_sql} (#{col_list}) VALUES #{values_placeholders}"
|
|
1938
|
-
|
|
1939
|
-
# Flatten all row values for parameter binding
|
|
1940
|
-
params = rows.flat_map { |row| columns.map { |col| row[col] } }
|
|
1941
|
-
|
|
1942
|
-
# Execute the bulk insert
|
|
1943
|
-
db.execute(sql, params)
|
|
1944
|
-
|
|
1945
|
-
rows.length
|
|
1946
|
-
end
|
|
1947
|
-
|
|
1948
|
-
# Prepared statement support for performance (Requirement 9.2)
|
|
1949
|
-
# Enhanced prepare method that leverages DuckDB's prepared statement capabilities
|
|
1950
|
-
def prepare(type, name = nil, *values)
|
|
1951
|
-
# Check if DuckDB connection supports prepared statements
|
|
1952
|
-
if db.respond_to?(:prepare_statement)
|
|
1953
|
-
# Use DuckDB's native prepared statement support
|
|
1954
|
-
sql = case type
|
|
1955
|
-
when :select, :all
|
|
1956
|
-
select_sql
|
|
1957
|
-
when :first
|
|
1958
|
-
clone(limit: 1).select_sql
|
|
1959
|
-
when :insert
|
|
1960
|
-
insert_sql(*values)
|
|
1961
|
-
when :update
|
|
1962
|
-
update_sql(*values)
|
|
1963
|
-
when :delete
|
|
1964
|
-
delete_sql
|
|
1965
|
-
else
|
|
1966
|
-
raise ArgumentError, "Unsupported prepared statement type: #{type}"
|
|
1967
|
-
end
|
|
1968
|
-
|
|
1969
|
-
# Create and cache prepared statement
|
|
1970
|
-
prepared_stmt = db.prepare_statement(sql)
|
|
1971
|
-
|
|
1972
|
-
# Return a callable object that executes the prepared statement
|
|
1973
|
-
lambda do |*params|
|
|
1974
|
-
case type
|
|
1975
|
-
when :select, :all
|
|
1976
|
-
prepared_stmt.execute(*params).to_a
|
|
1977
|
-
when :first
|
|
1978
|
-
result = prepared_stmt.execute(*params).first
|
|
1979
|
-
result
|
|
1980
|
-
else
|
|
1981
|
-
prepared_stmt.execute(*params)
|
|
1982
|
-
end
|
|
1983
|
-
end
|
|
1984
|
-
else
|
|
1985
|
-
# Fall back to standard Sequel prepared statement handling
|
|
1986
|
-
super
|
|
1987
|
-
end
|
|
1988
|
-
end
|
|
1989
|
-
|
|
1990
|
-
# Connection pooling optimization (Requirement 9.4)
|
|
1991
|
-
# Enhanced connection management for better performance
|
|
1992
|
-
def with_connection_pooling
|
|
1993
|
-
# Ensure efficient connection reuse
|
|
1994
|
-
db.synchronize do |conn|
|
|
1995
|
-
# Verify connection is still valid before use
|
|
1996
|
-
unless db.valid_connection?(conn)
|
|
1997
|
-
# Reconnect if connection is invalid
|
|
1998
|
-
conn = db.connect(db.opts)
|
|
1999
|
-
end
|
|
2000
|
-
|
|
2001
|
-
yield conn
|
|
2002
|
-
end
|
|
2003
|
-
end
|
|
2004
|
-
|
|
2005
|
-
# Memory-efficient streaming for large result sets (Requirement 9.5)
|
|
2006
|
-
# Enhanced each method with better memory management
|
|
2007
|
-
def each(&)
|
|
2008
|
-
return enum_for(:each) unless block_given?
|
|
1089
|
+
# DuckDB-specific implementation of date arithmetic
|
|
1090
|
+
# This will be called by Sequel's date_arithmetic extension
|
|
1091
|
+
# via the `super` mechanism when the extension is loaded
|
|
1092
|
+
def date_add_sql_append(sql, date_arith)
|
|
1093
|
+
expr = date_arith.expr
|
|
1094
|
+
interval_hash = date_arith.interval
|
|
1095
|
+
cast_type = date_arith.cast_type
|
|
2009
1096
|
|
|
2010
|
-
#
|
|
2011
|
-
|
|
1097
|
+
# Build expression with chained interval additions
|
|
1098
|
+
result = expr
|
|
1099
|
+
interval_hash.each do |unit, value|
|
|
1100
|
+
sql_unit = DUCKDB_DURATION_UNITS[unit]
|
|
1101
|
+
next unless sql_unit
|
|
2012
1102
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
fetch_rows(sql, &)
|
|
2017
|
-
return self
|
|
1103
|
+
# Create interval addition
|
|
1104
|
+
interval = build_interval_literal(value, sql_unit)
|
|
1105
|
+
result = Sequel.+(result, interval)
|
|
2018
1106
|
end
|
|
2019
1107
|
|
|
2020
|
-
#
|
|
2021
|
-
|
|
2022
|
-
|
|
1108
|
+
# Apply cast if specified or default to Time (TIMESTAMP)
|
|
1109
|
+
# Note: DuckDB returns TIMESTAMP when adding intervals to DATE
|
|
1110
|
+
result = Sequel.cast(result, cast_type || Time)
|
|
2023
1111
|
|
|
2024
|
-
|
|
2025
|
-
# Fetch a batch of results
|
|
2026
|
-
batch_sql = "#{sql} LIMIT #{batch_size} OFFSET #{offset}"
|
|
2027
|
-
batch_count = 0
|
|
2028
|
-
|
|
2029
|
-
fetch_rows(batch_sql) do |row|
|
|
2030
|
-
yield row
|
|
2031
|
-
batch_count += 1
|
|
2032
|
-
end
|
|
2033
|
-
|
|
2034
|
-
# Break if we got fewer rows than the batch size (end of results)
|
|
2035
|
-
break if batch_count < batch_size
|
|
2036
|
-
|
|
2037
|
-
offset += batch_size
|
|
2038
|
-
end
|
|
2039
|
-
|
|
2040
|
-
self
|
|
2041
|
-
end
|
|
2042
|
-
|
|
2043
|
-
# Set custom batch size for streaming operations (Requirement 9.5)
|
|
2044
|
-
#
|
|
2045
|
-
# @param size [Integer] Batch size for streaming
|
|
2046
|
-
# @return [Dataset] New dataset with custom batch size
|
|
2047
|
-
def stream_batch_size(size)
|
|
2048
|
-
clone(stream_batch_size: size)
|
|
2049
|
-
end
|
|
2050
|
-
|
|
2051
|
-
# Stream results with memory limit enforcement (Requirement 9.5)
|
|
2052
|
-
#
|
|
2053
|
-
# @param memory_limit [Integer] Maximum memory growth allowed in bytes
|
|
2054
|
-
# @yield [Hash] Block to process each row
|
|
2055
|
-
# @return [Enumerator] If no block given
|
|
2056
|
-
def stream_with_memory_limit(memory_limit, &)
|
|
2057
|
-
return enum_for(:stream_with_memory_limit, memory_limit) unless block_given?
|
|
2058
|
-
|
|
2059
|
-
sql = select_sql
|
|
2060
|
-
|
|
2061
|
-
# Check if SQL already has LIMIT/OFFSET - if so, don't add batching
|
|
2062
|
-
if sql.match?(/\bLIMIT\b/i) || sql.match?(/\bOFFSET\b/i)
|
|
2063
|
-
# SQL already has LIMIT/OFFSET, execute directly without batching
|
|
2064
|
-
fetch_rows(sql, &)
|
|
2065
|
-
return self
|
|
2066
|
-
end
|
|
2067
|
-
|
|
2068
|
-
initial_memory = memory_usage
|
|
2069
|
-
batch_size = @opts[:stream_batch_size] || 500
|
|
2070
|
-
offset = 0
|
|
2071
|
-
|
|
2072
|
-
loop do
|
|
2073
|
-
# Check memory usage before processing batch
|
|
2074
|
-
current_memory = memory_usage
|
|
2075
|
-
memory_growth = current_memory - initial_memory
|
|
2076
|
-
|
|
2077
|
-
# Reduce batch size if memory usage is high
|
|
2078
|
-
batch_size = [batch_size / 2, 100].max if memory_growth > memory_limit * 0.8
|
|
2079
|
-
|
|
2080
|
-
batch_sql = "#{sql} LIMIT #{batch_size} OFFSET #{offset}"
|
|
2081
|
-
batch_count = 0
|
|
2082
|
-
|
|
2083
|
-
fetch_rows(batch_sql) do |row|
|
|
2084
|
-
yield row
|
|
2085
|
-
batch_count += 1
|
|
2086
|
-
|
|
2087
|
-
# Force garbage collection periodically to manage memory
|
|
2088
|
-
GC.start if (batch_count % 100).zero?
|
|
2089
|
-
end
|
|
2090
|
-
|
|
2091
|
-
break if batch_count < batch_size
|
|
2092
|
-
|
|
2093
|
-
offset += batch_size
|
|
2094
|
-
end
|
|
2095
|
-
|
|
2096
|
-
self
|
|
2097
|
-
end
|
|
2098
|
-
|
|
2099
|
-
private
|
|
2100
|
-
|
|
2101
|
-
# Get approximate memory usage for streaming optimization
|
|
2102
|
-
def memory_usage
|
|
2103
|
-
GC.start
|
|
2104
|
-
ObjectSpace.count_objects[:TOTAL] * 40
|
|
2105
|
-
end
|
|
2106
|
-
|
|
2107
|
-
public
|
|
2108
|
-
|
|
2109
|
-
# Optimized count method for DuckDB
|
|
2110
|
-
# Provides fast path for simple COUNT(*) queries on base tables
|
|
2111
|
-
# Falls back to Sequel's implementation for complex scenarios
|
|
2112
|
-
def count(*args, &block)
|
|
2113
|
-
# Only optimize if:
|
|
2114
|
-
# - No arguments or block provided
|
|
2115
|
-
# - No grouping, having, distinct, or where clauses
|
|
2116
|
-
# - Has a from clause with a table
|
|
2117
|
-
if args.empty? && !block && !@opts[:group] && !@opts[:having] &&
|
|
2118
|
-
!@opts[:distinct] && !@opts[:where] && @opts[:from]&.first
|
|
2119
|
-
# Use optimized COUNT(*) for simple cases
|
|
2120
|
-
table_name = @opts[:from].first
|
|
2121
|
-
table_sql = String.new
|
|
2122
|
-
quote_identifier_append(table_sql, table_name)
|
|
2123
|
-
single_value("SELECT COUNT(*) FROM #{table_sql}")
|
|
2124
|
-
else
|
|
2125
|
-
# Fall back to standard Sequel count behavior for complex cases
|
|
2126
|
-
super
|
|
2127
|
-
end
|
|
1112
|
+
literal_append(sql, result)
|
|
2128
1113
|
end
|
|
2129
1114
|
|
|
2130
1115
|
private
|
|
2131
1116
|
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
value
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
end
|
|
2141
|
-
|
|
2142
|
-
# Helper method to check if bulk operations should be used
|
|
2143
|
-
def should_use_bulk_operations?(row_count)
|
|
2144
|
-
# Use bulk operations for more than 10 rows
|
|
2145
|
-
row_count > 10
|
|
2146
|
-
end
|
|
2147
|
-
|
|
2148
|
-
# Helper method to optimize query execution based on result set size
|
|
2149
|
-
def optimize_for_result_size(sql)
|
|
2150
|
-
# Add DuckDB-specific optimization hints if needed
|
|
2151
|
-
if @opts[:small_result_set]
|
|
2152
|
-
# For small result sets, DuckDB can use different optimization strategies
|
|
2153
|
-
end
|
|
2154
|
-
sql
|
|
2155
|
-
end
|
|
2156
|
-
|
|
2157
|
-
public
|
|
2158
|
-
|
|
2159
|
-
# Index-aware query generation methods (Requirement 9.7)
|
|
2160
|
-
|
|
2161
|
-
# Get query execution plan with index usage information
|
|
2162
|
-
#
|
|
2163
|
-
# @return [String] Query execution plan
|
|
2164
|
-
def explain
|
|
2165
|
-
explain_sql = "EXPLAIN #{select_sql}"
|
|
2166
|
-
plan_text = ""
|
|
2167
|
-
|
|
2168
|
-
fetch_rows(explain_sql) do |row|
|
|
2169
|
-
plan_text += "#{row.values.join(" ")}\n"
|
|
2170
|
-
end
|
|
2171
|
-
|
|
2172
|
-
plan_text
|
|
2173
|
-
end
|
|
2174
|
-
|
|
2175
|
-
# Get detailed query analysis including index usage
|
|
2176
|
-
#
|
|
2177
|
-
# @return [Hash] Analysis information
|
|
2178
|
-
def analyze_query
|
|
2179
|
-
{
|
|
2180
|
-
plan: explain,
|
|
2181
|
-
indexes_used: extract_indexes_from_plan(explain),
|
|
2182
|
-
optimization_hints: generate_optimization_hints
|
|
2183
|
-
}
|
|
2184
|
-
end
|
|
2185
|
-
|
|
2186
|
-
# Override where method to add index-aware optimization hints
|
|
2187
|
-
def where(*cond, &)
|
|
2188
|
-
result = super
|
|
2189
|
-
|
|
2190
|
-
# Add index optimization hints based on WHERE conditions
|
|
2191
|
-
result = result.add_index_hints(cond.first.keys) if cond.length == 1 && cond.first.is_a?(Hash)
|
|
2192
|
-
|
|
2193
|
-
result
|
|
2194
|
-
end
|
|
2195
|
-
|
|
2196
|
-
# Override order method to leverage index optimization
|
|
2197
|
-
def order(*columns)
|
|
2198
|
-
result = super
|
|
2199
|
-
|
|
2200
|
-
# Add index hints for ORDER BY optimization
|
|
2201
|
-
order_columns = columns.map do |col|
|
|
2202
|
-
case col
|
|
2203
|
-
when Sequel::SQL::OrderedExpression
|
|
2204
|
-
col.expression
|
|
1117
|
+
def build_interval_literal(value, unit)
|
|
1118
|
+
# If value is numeric, use direct syntax
|
|
1119
|
+
# If value is expression, wrap in parentheses
|
|
1120
|
+
if value.is_a?(Numeric)
|
|
1121
|
+
# Direct numeric: INTERVAL 5 HOUR or INTERVAL (-5) HOUR for negatives
|
|
1122
|
+
# DuckDB requires parentheses around negative numbers
|
|
1123
|
+
if value.negative?
|
|
1124
|
+
Sequel.lit("INTERVAL (#{value}) #{unit}")
|
|
2205
1125
|
else
|
|
2206
|
-
|
|
1126
|
+
Sequel.lit(["INTERVAL ", " #{unit}"], value)
|
|
2207
1127
|
end
|
|
1128
|
+
else
|
|
1129
|
+
# Expression: INTERVAL (column_name) HOUR
|
|
1130
|
+
# Note: expressions already include negation from date_sub
|
|
1131
|
+
Sequel.lit(["INTERVAL (", ") #{unit}"], value)
|
|
2208
1132
|
end
|
|
2209
|
-
|
|
2210
|
-
result.add_index_hints(order_columns)
|
|
2211
|
-
end
|
|
2212
|
-
|
|
2213
|
-
# Add index optimization hints to the dataset
|
|
2214
|
-
#
|
|
2215
|
-
# @param columns [Array] Columns that might benefit from index usage
|
|
2216
|
-
# @return [Dataset] Dataset with index hints
|
|
2217
|
-
def add_index_hints(columns)
|
|
2218
|
-
# Get available indexes for the table
|
|
2219
|
-
table_name = @opts[:from]&.first
|
|
2220
|
-
return self unless table_name
|
|
2221
|
-
|
|
2222
|
-
available_indexes = begin
|
|
2223
|
-
db.indexes(table_name)
|
|
2224
|
-
rescue StandardError
|
|
2225
|
-
{}
|
|
2226
|
-
end
|
|
2227
|
-
|
|
2228
|
-
# Find indexes that match the columns
|
|
2229
|
-
matching_indexes = available_indexes.select do |_index_name, index_info|
|
|
2230
|
-
index_columns = index_info[:columns] || []
|
|
2231
|
-
columns.any? { |col| index_columns.include?(col.to_sym) }
|
|
2232
|
-
end
|
|
2233
|
-
|
|
2234
|
-
# Add index hints to options
|
|
2235
|
-
clone(index_hints: matching_indexes.keys)
|
|
2236
|
-
end
|
|
2237
|
-
|
|
2238
|
-
# Columnar storage optimization methods (Requirement 9.7)
|
|
2239
|
-
|
|
2240
|
-
# Override select method to add columnar optimization
|
|
2241
|
-
def select(*columns)
|
|
2242
|
-
result = super
|
|
2243
|
-
|
|
2244
|
-
# Mark as columnar-optimized if selecting specific columns
|
|
2245
|
-
result = result.clone(columnar_optimized: true) if columns.length.positive? && columns.length < 10
|
|
2246
|
-
|
|
2247
|
-
result
|
|
2248
|
-
end
|
|
2249
|
-
|
|
2250
|
-
# Optimize aggregation queries for columnar storage
|
|
2251
|
-
def group(*columns)
|
|
2252
|
-
result = super
|
|
2253
|
-
|
|
2254
|
-
# Add columnar aggregation optimization hints
|
|
2255
|
-
result.clone(columnar_aggregation: true)
|
|
2256
|
-
end
|
|
2257
|
-
|
|
2258
|
-
# Parallel query execution support (Requirement 9.7)
|
|
2259
|
-
|
|
2260
|
-
# Enable parallel execution for the query
|
|
2261
|
-
#
|
|
2262
|
-
# @param thread_count [Integer] Number of threads to use (optional)
|
|
2263
|
-
# @return [Dataset] Dataset configured for parallel execution
|
|
2264
|
-
def parallel(thread_count = nil)
|
|
2265
|
-
opts = { parallel_execution: true }
|
|
2266
|
-
opts[:parallel_threads] = thread_count if thread_count
|
|
2267
|
-
clone(opts)
|
|
2268
|
-
end
|
|
2269
|
-
|
|
2270
|
-
private
|
|
2271
|
-
|
|
2272
|
-
# Extract index names from query execution plan
|
|
2273
|
-
def extract_indexes_from_plan(plan)
|
|
2274
|
-
indexes = []
|
|
2275
|
-
plan.scan(/idx_\w+|index\s+(\w+)/i) do |match|
|
|
2276
|
-
indexes << (match.is_a?(Array) ? match.first : match)
|
|
2277
|
-
end
|
|
2278
|
-
indexes.compact.uniq
|
|
2279
|
-
end
|
|
2280
|
-
|
|
2281
|
-
# Generate optimization hints based on query structure
|
|
2282
|
-
def generate_optimization_hints
|
|
2283
|
-
hints = []
|
|
2284
|
-
|
|
2285
|
-
# Check for potential index usage
|
|
2286
|
-
hints << "Consider adding indexes on WHERE clause columns" if @opts[:where]
|
|
2287
|
-
|
|
2288
|
-
# Check for ORDER BY optimization
|
|
2289
|
-
hints << "ORDER BY may benefit from index on ordered columns" if @opts[:order]
|
|
2290
|
-
|
|
2291
|
-
# Check for GROUP BY optimization
|
|
2292
|
-
hints << "GROUP BY operations are optimized for columnar storage" if @opts[:group]
|
|
2293
|
-
|
|
2294
|
-
hints
|
|
2295
|
-
end
|
|
2296
|
-
|
|
2297
|
-
# Optimize SQL for columnar projection
|
|
2298
|
-
def optimize_for_columnar_projection(sql)
|
|
2299
|
-
# Add DuckDB-specific hints for columnar projection
|
|
2300
|
-
if @opts[:columnar_optimized]
|
|
2301
|
-
# DuckDB automatically optimizes column access, but we can add hints
|
|
2302
|
-
end
|
|
2303
|
-
sql
|
|
2304
|
-
end
|
|
2305
|
-
|
|
2306
|
-
# Determine if parallel execution should be used
|
|
2307
|
-
def should_use_parallel_execution?
|
|
2308
|
-
# Use parallel execution for:
|
|
2309
|
-
# 1. Explicit parallel requests
|
|
2310
|
-
# 2. Complex aggregations
|
|
2311
|
-
# 3. Large joins
|
|
2312
|
-
# 4. Window functions
|
|
2313
|
-
|
|
2314
|
-
return true if @opts[:parallel_execution]
|
|
2315
|
-
return true if @opts[:group] && @opts[:columnar_aggregation]
|
|
2316
|
-
return true if @opts[:join] && @opts[:join].length > 1
|
|
2317
|
-
return true if sql.downcase.include?("over(")
|
|
2318
|
-
|
|
2319
|
-
false
|
|
2320
|
-
end
|
|
2321
|
-
|
|
2322
|
-
# Add parallel execution hints to SQL
|
|
2323
|
-
def add_parallel_hints(sql)
|
|
2324
|
-
# DuckDB handles parallelization automatically, but we can add configuration
|
|
2325
|
-
if @opts[:parallel_threads]
|
|
2326
|
-
# NOTE: This would require connection-level configuration in practice
|
|
2327
|
-
# For now, we'll rely on DuckDB's automatic parallelization
|
|
2328
|
-
end
|
|
2329
|
-
|
|
2330
|
-
sql
|
|
2331
1133
|
end
|
|
2332
1134
|
end
|
|
2333
1135
|
end
|