sequel-duckdb 0.4.0 → 0.4.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/CHANGELOG.md +21 -0
- data/README.md +48 -3
- data/lib/sequel/adapters/duckdb.rb +19 -3
- data/lib/sequel/adapters/shared/duckdb.rb +87 -24
- data/lib/sequel/duckdb/helpers/copier.rb +5 -20
- data/lib/sequel/duckdb/helpers/sql_options.rb +44 -0
- data/lib/sequel/duckdb/version.rb +1 -1
- metadata +8 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7d728d2d4748d8ace682454b06155cea6ae570a8fbfbf888efa7d2a314077e5c
|
|
4
|
+
data.tar.gz: 525588b5baceefd4e7b0e3e61f429efb1c505d03250be98ca109bb9567bfce63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63164b64d30f71edcb66edb7e3a01ed07074de9312673b51b823e264268f702609fd6fcf98481535c7fc9165f49fa6eb38aa28adfd4311f129ef10d3b9a6bb8b
|
|
7
|
+
data.tar.gz: 76f6fb5c6e9d2684a013176f3ff7b9414ba6e0a9e67e88d36312e5cb57df5ddda4805ad84f4506ce7c60576461748194e24c2727596493b735eb434bec00cfc7
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.1] - 2026-10-26
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Support for the DuckDB C library 2.x and the duckdb gem 2.x, alongside 1.5.x. CI
|
|
13
|
+
tests library 1.5.6 and 2.0.0, and the duckdb gem 1.5.x and 2.x. Support for library
|
|
14
|
+
1.5.x ends when DuckDB 1.5.x reaches end of life in March 2027. sequel-duckdb still
|
|
15
|
+
does not depend on the duckdb gem, so each project picks its own version.
|
|
16
|
+
- **database:** Add attach and detach for DuckDB catalogs ([f82169b](https://github.com/outcomesinsights/sequel-duckdb/commit/f82169b0ad4cd66abe588dfdad431053535151e1))
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **adapter:** Remove the unused primary key caches ([9a14254](https://github.com/outcomesinsights/sequel-duckdb/commit/9a14254abd8494e71af9e5478d081ff048b46701))
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **connection:** Honor the readonly option ([e8e94c5](https://github.com/outcomesinsights/sequel-duckdb/commit/e8e94c5a5a210aacac0a53cc8190f3dc76b3bbdf))
|
|
25
|
+
- **schema:** Keep the transaction when a name lookup misses on DuckDB 2.x ([b3c02c6](https://github.com/outcomesinsights/sequel-duckdb/commit/b3c02c6029095e11fb048ef2b3c0e84a6c62e3ec))
|
|
26
|
+
- **schema:** Let a mock Database drop and create schemas ([f9aad74](https://github.com/outcomesinsights/sequel-duckdb/commit/f9aad74f94fc9d4ed024accd9ccb4bc6ecdb628e))
|
|
27
|
+
|
|
8
28
|
## [0.4.0] - 2026-10-25
|
|
9
29
|
|
|
10
30
|
### Breaking changes
|
|
@@ -348,3 +368,4 @@ tag-based release workflow.
|
|
|
348
368
|
[0.3.0]: https://github.com/outcomesinsights/sequel-duckdb/compare/v0.2.1...v0.3.0
|
|
349
369
|
[0.3.1]: https://github.com/outcomesinsights/sequel-duckdb/compare/v0.3.0...v0.3.1
|
|
350
370
|
[0.4.0]: https://github.com/outcomesinsights/sequel-duckdb/compare/v0.3.1...v0.4.0
|
|
371
|
+
[0.4.1]: https://github.com/outcomesinsights/sequel-duckdb/compare/v0.4.0...v0.4.1
|
data/README.md
CHANGED
|
@@ -286,6 +286,12 @@ db = Sequel.connect('duckdb://relative/path/to/database.duckdb')
|
|
|
286
286
|
db = Sequel.connect('duckdb:///path/to/database.duckdb?readonly=true')
|
|
287
287
|
```
|
|
288
288
|
|
|
289
|
+
`readonly` opens the file with DuckDB's `access_mode` set to `READ_ONLY`, so writes
|
|
290
|
+
raise `Sequel::DatabaseError`. All `Sequel::Database` objects on one file in one
|
|
291
|
+
process share one DuckDB instance, so the first one to open the file sets its
|
|
292
|
+
settings. Opening the same file again with other settings, such as read-write after
|
|
293
|
+
read-only, raises `Sequel::DatabaseConnectionError`.
|
|
294
|
+
|
|
289
295
|
### Connection Hash
|
|
290
296
|
|
|
291
297
|
```ruby
|
|
@@ -539,6 +545,38 @@ db[:users].copy_to_sql("/data/users.csv", header: true, delimiter: "|")
|
|
|
539
545
|
# => COPY (SELECT * FROM "users") TO '/data/users.csv' (FORMAT CSV, HEADER, DELIMITER '|')
|
|
540
546
|
```
|
|
541
547
|
|
|
548
|
+
### Attaching database files
|
|
549
|
+
|
|
550
|
+
`Database#attach` runs a DuckDB `ATTACH`, and `Database#detach` runs `DETACH DATABASE`.
|
|
551
|
+
The catalog name `as:` is required. A name that DuckDB derives from the file name can
|
|
552
|
+
equal a schema name inside the file, and then `schema.table` is ambiguous. Other DuckDB
|
|
553
|
+
`ATTACH` options pass through, for example `type: :sqlite`.
|
|
554
|
+
|
|
555
|
+
```ruby
|
|
556
|
+
db.attach("/data/dataset.duckdb", as: :dataset, read_only: true)
|
|
557
|
+
db[Sequel[:dataset][:people]].count
|
|
558
|
+
db.detach(:dataset, if_exists: true)
|
|
559
|
+
|
|
560
|
+
db.attach_sql("/data/dataset.duckdb", as: :dataset, read_only: true, if_not_exists: true)
|
|
561
|
+
# => ATTACH IF NOT EXISTS '/data/dataset.duckdb' AS "dataset" (READ_ONLY)
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
An attached catalog belongs to the DuckDB database, which all pool connections of a
|
|
565
|
+
`Sequel::Database` share. It lasts until `Database#disconnect` closes the last
|
|
566
|
+
connection. After that, attach it again; `if_not_exists: true` makes a repeat safe.
|
|
567
|
+
|
|
568
|
+
`search_path` is different: DuckDB keeps it per connection. Set it on every new
|
|
569
|
+
connection with Sequel's `connect_sqls` option. Put the writable catalog first, because
|
|
570
|
+
`CREATE TABLE` without a schema goes to the first entry, and a read-only catalog there
|
|
571
|
+
makes it fail.
|
|
572
|
+
|
|
573
|
+
```ruby
|
|
574
|
+
db = Sequel.connect("duckdb:///exports/42/work.duckdb",
|
|
575
|
+
connect_sqls: [ "SET search_path = 'work.main'" ])
|
|
576
|
+
db.attach("/data/dataset.duckdb", as: :dataset, read_only: true)
|
|
577
|
+
db.create_table(:summary) { Integer :n } # created in work.main
|
|
578
|
+
```
|
|
579
|
+
|
|
542
580
|
### Lists
|
|
543
581
|
|
|
544
582
|
Sequel writes a Ruby Array as a parenthesized list, such as `(1, 2, 3)`, for `IN`
|
|
@@ -765,10 +803,17 @@ db[:users].where(active: true).all
|
|
|
765
803
|
|
|
766
804
|
### Version Compatibility
|
|
767
805
|
|
|
768
|
-
- **Ruby**: 3.
|
|
806
|
+
- **Ruby**: 3.3 or higher (CI tests 3.3, 3.4 and 4.0)
|
|
769
807
|
- **Sequel**: 5.0 or higher
|
|
770
|
-
- **DuckDB**:
|
|
771
|
-
|
|
808
|
+
- **DuckDB C library**: 1.5.x and 2.x. Support for 1.5.x ends when DuckDB 1.5.x
|
|
809
|
+
reaches end of life in March 2027.
|
|
810
|
+
- **ruby-duckdb (the `duckdb` gem)**: 1.5.x and 2.x. sequel-duckdb does not depend on
|
|
811
|
+
it, so add `gem "duckdb"` to your own Gemfile. ruby-duckdb 2.x builds against
|
|
812
|
+
library 1.4 or newer, so it works with library 1.5.x.
|
|
813
|
+
|
|
814
|
+
DuckDB 2.x aborts an open transaction after any failed statement, including a query
|
|
815
|
+
on a missing table; 1.5.x does not. `table_exists?` and `schema` keep the transaction
|
|
816
|
+
on both versions.
|
|
772
817
|
|
|
773
818
|
### Getting Help
|
|
774
819
|
|
|
@@ -69,12 +69,10 @@ module Sequel
|
|
|
69
69
|
# (InstanceRegistry), and #disconnect gives it back once no connection is open.
|
|
70
70
|
def adapter_initialize
|
|
71
71
|
@conversion_procs = {}
|
|
72
|
-
@primary_keys = {}
|
|
73
|
-
@primary_key_sequences = {}
|
|
74
72
|
register_default_conversion_procs
|
|
75
73
|
@duckdb_mutex = Mutex.new
|
|
76
74
|
@duckdb_file = duckdb_file_path(opts[:database])
|
|
77
|
-
@duckdb_settings = InstanceRegistry.normalize_settings(
|
|
75
|
+
@duckdb_settings = InstanceRegistry.normalize_settings(duckdb_config)
|
|
78
76
|
@duckdb_state = ConnectionState.new(open_duckdb_database, 0)
|
|
79
77
|
rescue Sequel::DatabaseConnectionError
|
|
80
78
|
raise
|
|
@@ -96,6 +94,19 @@ module Sequel
|
|
|
96
94
|
end
|
|
97
95
|
|
|
98
96
|
# nil for an in-memory database, which is never shared.
|
|
97
|
+
# The DuckDB settings: the config: option, plus access_mode READ_ONLY when the
|
|
98
|
+
# readonly option (readonly: true, or ?readonly=true in the URL) is true.
|
|
99
|
+
def duckdb_config
|
|
100
|
+
config = opts[:config] || {}
|
|
101
|
+
return config unless typecast_value_boolean(opts[:readonly])
|
|
102
|
+
|
|
103
|
+
mode = config.find { |name, _| name.to_s == "access_mode" }&.last
|
|
104
|
+
return config.merge(access_mode: "READ_ONLY") if mode.nil?
|
|
105
|
+
return config if mode.to_s.casecmp?("READ_ONLY")
|
|
106
|
+
|
|
107
|
+
raise Sequel::DatabaseConnectionError, "readonly: true conflicts with config access_mode #{mode.inspect}"
|
|
108
|
+
end
|
|
109
|
+
|
|
99
110
|
def duckdb_file_path(database_path)
|
|
100
111
|
return nil if database_path.nil? || database_path == ":memory:"
|
|
101
112
|
return "/#{database_path}" if database_path.match?(/^[a-zA-Z]/) && !database_path.start_with?(":")
|
|
@@ -199,6 +210,11 @@ module Sequel
|
|
|
199
210
|
|
|
200
211
|
private
|
|
201
212
|
|
|
213
|
+
# DuckDB 2.0 made binder and catalog errors abort an open transaction.
|
|
214
|
+
def bind_errors_abort_transactions?
|
|
215
|
+
::DuckDB::LIBRARY_VERSION.to_i >= 2
|
|
216
|
+
end
|
|
217
|
+
|
|
202
218
|
def database_error_classes
|
|
203
219
|
[ ::DuckDB::Error ]
|
|
204
220
|
end
|
|
@@ -294,19 +294,21 @@ module Sequel
|
|
|
294
294
|
#
|
|
295
295
|
# DuckDB resolves the name itself, as it does for any query, so every Sequel
|
|
296
296
|
# identifier form, the search_path, temporary tables and views all work. The query
|
|
297
|
-
# reads no rows. A missing table is a catalog error
|
|
298
|
-
#
|
|
297
|
+
# reads no rows. A missing table is a catalog error. It must not abort an open
|
|
298
|
+
# transaction, and DuckDB has no savepoints, so see keep_transaction_on_bind_error.
|
|
299
299
|
#
|
|
300
300
|
# @param table_name [Symbol, String, Sequel::SQL::Identifier, Sequel::SQL::QualifiedIdentifier] Name of the table
|
|
301
301
|
# @param opts [Hash] Options (:schema qualifies an unqualified name)
|
|
302
302
|
# @return [Boolean] true if table exists
|
|
303
303
|
def table_exists?(table_name, opts = {})
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
304
|
+
keep_transaction_on_bind_error do
|
|
305
|
+
from(qualify_with_schema_option(table_name, opts)).where(false).select(Sequel::NULL).all
|
|
306
|
+
true
|
|
307
|
+
rescue Sequel::DatabaseConnectionError, Sequel::DatabaseDisconnectError
|
|
308
|
+
raise
|
|
309
|
+
rescue Sequel::DatabaseError
|
|
310
|
+
false
|
|
311
|
+
end
|
|
310
312
|
end
|
|
311
313
|
|
|
312
314
|
# Get list of tables
|
|
@@ -474,16 +476,18 @@ module Sequel
|
|
|
474
476
|
# @return [Hash, nil] :catalog, :schema and :table, or nil if the name does not resolve
|
|
475
477
|
def resolve_table(table_name, opts = {})
|
|
476
478
|
table_name = qualify_with_schema_option(table_name, opts)
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
479
|
+
keep_transaction_on_bind_error do
|
|
480
|
+
target = explain_plan(table_name)
|
|
481
|
+
next unless target
|
|
482
|
+
|
|
483
|
+
candidates = fetch("SELECT table_catalog, table_schema, table_name FROM information_schema.tables " \
|
|
484
|
+
"WHERE lower(table_name) = lower(?)", bare_table_name(table_name)).all
|
|
485
|
+
matches = candidates.select do |c|
|
|
486
|
+
explain_plan(qualified_name(c[:table_catalog], c[:table_schema], c[:table_name])) == target
|
|
487
|
+
end
|
|
488
|
+
match = matches.size > 1 ? first_in_search_order(matches) : matches.first
|
|
489
|
+
match && { catalog: match[:table_catalog], schema: match[:table_schema], table: match[:table_name] }
|
|
484
490
|
end
|
|
485
|
-
match = matches.size > 1 ? first_in_search_order(matches) : matches.first
|
|
486
|
-
match && { catalog: match[:table_catalog], schema: match[:table_schema], table: match[:table_name] }
|
|
487
491
|
end
|
|
488
492
|
|
|
489
493
|
# resolve_table, but a name that does not resolve raises.
|
|
@@ -514,7 +518,7 @@ module Sequel
|
|
|
514
518
|
end
|
|
515
519
|
|
|
516
520
|
# The EXPLAIN plan of SELECT * FROM the name, or nil if DuckDB cannot bind the name.
|
|
517
|
-
#
|
|
521
|
+
# Call it inside keep_transaction_on_bind_error.
|
|
518
522
|
def explain_plan(table_name)
|
|
519
523
|
plan = []
|
|
520
524
|
execute("EXPLAIN (FORMAT JSON) #{from(table_name).sql}") { |row| plan << row }
|
|
@@ -525,6 +529,32 @@ module Sequel
|
|
|
525
529
|
nil
|
|
526
530
|
end
|
|
527
531
|
|
|
532
|
+
# Runs the block so that a binder or catalog error in it, such as a missing table,
|
|
533
|
+
# does not abort an open transaction.
|
|
534
|
+
#
|
|
535
|
+
# DuckDB 1.x never aborts a transaction on those errors. DuckDB 2.x aborts it by
|
|
536
|
+
# default, and its current_transaction_invalidation_policy setting turns that off
|
|
537
|
+
# for the open transaction only. DuckDB 1.x does not know the value, and a failed
|
|
538
|
+
# SET aborts the transaction, so the setting is used only where
|
|
539
|
+
# bind_errors_abort_transactions? is true. The block must handle its own errors:
|
|
540
|
+
# if it raises, the policy is not restored, and the transaction ends anyway.
|
|
541
|
+
def keep_transaction_on_bind_error
|
|
542
|
+
return yield unless bind_errors_abort_transactions? && in_transaction?
|
|
543
|
+
|
|
544
|
+
setting = "current_transaction_invalidation_policy"
|
|
545
|
+
previous = get(Sequel.function(:current_setting, setting))
|
|
546
|
+
run("SET #{setting} = 'SYNTACTIC_ERRORS_DO_NOT_INVALIDATE'")
|
|
547
|
+
result = yield
|
|
548
|
+
run("SET #{setting} = #{literal(previous)}")
|
|
549
|
+
result
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# True when a binder or catalog error aborts an open transaction. The native adapter
|
|
553
|
+
# answers from the loaded DuckDB library. Other adapters run no queries, so false.
|
|
554
|
+
def bind_errors_abort_transactions?
|
|
555
|
+
false
|
|
556
|
+
end
|
|
557
|
+
|
|
528
558
|
# DuckDB looks in the temp catalog first, then in the search_path schemas, then in the
|
|
529
559
|
# default catalog.
|
|
530
560
|
def first_in_search_order(candidates)
|
|
@@ -758,15 +788,11 @@ module Sequel
|
|
|
758
788
|
# Remove all cached schema information
|
|
759
789
|
#
|
|
760
790
|
# Called after schema modifications to ensure cache consistency. It empties the
|
|
761
|
-
#
|
|
791
|
+
# cache in place, because a frozen Database cannot replace it.
|
|
762
792
|
#
|
|
763
793
|
# @return [void]
|
|
764
794
|
def remove_all_cached_schemas
|
|
765
|
-
Sequel.synchronize
|
|
766
|
-
@schemas&.clear
|
|
767
|
-
@primary_keys.clear
|
|
768
|
-
@primary_key_sequences.clear
|
|
769
|
-
end
|
|
795
|
+
Sequel.synchronize { @schemas&.clear }
|
|
770
796
|
end
|
|
771
797
|
|
|
772
798
|
# List all schemas in the database
|
|
@@ -864,6 +890,43 @@ module Sequel
|
|
|
864
890
|
sql
|
|
865
891
|
end
|
|
866
892
|
|
|
893
|
+
# Runs a DuckDB ATTACH, which opens the database file at +path+ as the catalog
|
|
894
|
+
# +as+. The catalog is part of the DuckDB database, so every pool connection sees
|
|
895
|
+
# it. It lasts until Database#disconnect closes the last connection; attach again
|
|
896
|
+
# after that. See attach_sql for the options.
|
|
897
|
+
def attach(path, as:, **options)
|
|
898
|
+
run(attach_sql(path, as:, **options))
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
# The ATTACH statement that #attach runs. The alias +as+ is required: an alias
|
|
902
|
+
# that DuckDB derives from the file name can equal a schema name in the file,
|
|
903
|
+
# which makes schema.table ambiguous.
|
|
904
|
+
#
|
|
905
|
+
# @param path [String] The database file; a string literal in the SQL
|
|
906
|
+
# @param as [Symbol, String] The catalog name
|
|
907
|
+
# @param if_not_exists [Boolean] Do nothing if the catalog is already attached
|
|
908
|
+
# @param options [Hash] DuckDB ATTACH options, for example read_only: true or
|
|
909
|
+
# type: :sqlite. A true value renders the bare keyword (READ_ONLY).
|
|
910
|
+
# @return [String]
|
|
911
|
+
def attach_sql(path, as:, if_not_exists: false, **options)
|
|
912
|
+
sql = +"ATTACH "
|
|
913
|
+
sql << "IF NOT EXISTS " if if_not_exists
|
|
914
|
+
sql << "#{literal(path.to_s)} AS #{Sequel::DuckDB::Helpers::SqlOptions.quoted_identifier(as)}"
|
|
915
|
+
list = Sequel::DuckDB::Helpers::SqlOptions.list(options)
|
|
916
|
+
sql << " #{list}" unless list.empty?
|
|
917
|
+
sql
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
# Runs a DuckDB DETACH of the catalog +name+.
|
|
921
|
+
def detach(name, if_exists: false)
|
|
922
|
+
run(detach_sql(name, if_exists:))
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
# The DETACH statement that #detach runs.
|
|
926
|
+
def detach_sql(name, if_exists: false)
|
|
927
|
+
"DETACH DATABASE #{"IF EXISTS " if if_exists}#{Sequel::DuckDB::Helpers::SqlOptions.quoted_identifier(name)}"
|
|
928
|
+
end
|
|
929
|
+
|
|
867
930
|
# Runs a DuckDB COPY that writes +source+ (a Dataset or a SQL string) to
|
|
868
931
|
# +path+. For a Dataset this is the SQL from Dataset#copy_to_sql.
|
|
869
932
|
def copy_to(source, path, options = Sequel::OPTS)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "sql_options"
|
|
4
|
+
|
|
3
5
|
module Sequel
|
|
4
6
|
module DuckDB
|
|
5
7
|
module Helpers
|
|
@@ -9,9 +11,6 @@ module Sequel
|
|
|
9
11
|
# destination's file extension. A destination with neither (for example a
|
|
10
12
|
# directory for a partitioned COPY) raises Sequel::Error.
|
|
11
13
|
class Copier
|
|
12
|
-
# Option values that DuckDB accepts as a bare keyword (FORMAT PARQUET).
|
|
13
|
-
BARE_WORD = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
14
|
-
|
|
15
14
|
def initialize(src, dst, options = Sequel::OPTS)
|
|
16
15
|
@src = src
|
|
17
16
|
@dst = dst
|
|
@@ -31,21 +30,11 @@ module Sequel
|
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
def options_str
|
|
34
|
-
|
|
35
|
-
opts_str.empty? ? "" : "(#{opts_str})"
|
|
33
|
+
SqlOptions.list({ format: format }.merge(@options))
|
|
36
34
|
end
|
|
37
35
|
|
|
38
36
|
def format_option(key, value)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
key = key.to_s.upcase
|
|
42
|
-
case value
|
|
43
|
-
when true then key
|
|
44
|
-
when false then nil
|
|
45
|
-
when Integer, Float then "#{key} #{value}"
|
|
46
|
-
when BARE_WORD then "#{key} #{value.to_s.upcase}"
|
|
47
|
-
else "#{key} #{string_literal(value)}"
|
|
48
|
-
end
|
|
37
|
+
SqlOptions.option(key, value)
|
|
49
38
|
end
|
|
50
39
|
|
|
51
40
|
def format
|
|
@@ -61,11 +50,7 @@ module Sequel
|
|
|
61
50
|
private
|
|
62
51
|
|
|
63
52
|
def string_literal(value)
|
|
64
|
-
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def quoted_identifier(name)
|
|
68
|
-
"\"#{name.to_s.gsub('"', '""')}\""
|
|
53
|
+
SqlOptions.string_literal(value)
|
|
69
54
|
end
|
|
70
55
|
end
|
|
71
56
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sequel
|
|
4
|
+
module DuckDB
|
|
5
|
+
module Helpers
|
|
6
|
+
# Renders the option list of a DuckDB statement, as in COPY ... (FORMAT PARQUET)
|
|
7
|
+
# and ATTACH ... (READ_ONLY, TYPE SQLITE).
|
|
8
|
+
module SqlOptions
|
|
9
|
+
# Option values that DuckDB accepts as a bare keyword (FORMAT PARQUET).
|
|
10
|
+
BARE_WORD = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# "(KEY value, ...)", or "" when no option renders. A true value renders the key
|
|
15
|
+
# alone, and a false value leaves the option out.
|
|
16
|
+
def list(options)
|
|
17
|
+
rendered = options.map { |k, v| option(k, v) }.compact
|
|
18
|
+
rendered.empty? ? "" : "(#{rendered.join(", ")})"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def option(key, value)
|
|
22
|
+
return "PARTITION_BY (#{Array(value).map { |name| quoted_identifier(name) }.join(", ")})" if key.to_s == "partition_by"
|
|
23
|
+
|
|
24
|
+
key = key.to_s.upcase
|
|
25
|
+
case value
|
|
26
|
+
when true then key
|
|
27
|
+
when false then nil
|
|
28
|
+
when Integer, Float then "#{key} #{value}"
|
|
29
|
+
when BARE_WORD then "#{key} #{value.to_s.upcase}"
|
|
30
|
+
else "#{key} #{string_literal(value)}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def string_literal(value)
|
|
35
|
+
"'#{value.to_s.gsub("'", "''")}'"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def quoted_identifier(name)
|
|
39
|
+
"\"#{name.to_s.gsub('"', '""')}\""
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sequel-duckdb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ryan Duryea
|
|
@@ -119,42 +119,42 @@ dependencies:
|
|
|
119
119
|
requirements:
|
|
120
120
|
- - "~>"
|
|
121
121
|
- !ruby/object:Gem::Version
|
|
122
|
-
version: 0.
|
|
122
|
+
version: '0.41'
|
|
123
123
|
type: :development
|
|
124
124
|
prerelease: false
|
|
125
125
|
version_requirements: !ruby/object:Gem::Requirement
|
|
126
126
|
requirements:
|
|
127
127
|
- - "~>"
|
|
128
128
|
- !ruby/object:Gem::Version
|
|
129
|
-
version: 0.
|
|
129
|
+
version: '0.41'
|
|
130
130
|
- !ruby/object:Gem::Dependency
|
|
131
131
|
name: rubocop-rake
|
|
132
132
|
requirement: !ruby/object:Gem::Requirement
|
|
133
133
|
requirements:
|
|
134
134
|
- - "~>"
|
|
135
135
|
- !ruby/object:Gem::Version
|
|
136
|
-
version: 0.7
|
|
136
|
+
version: '0.7'
|
|
137
137
|
type: :development
|
|
138
138
|
prerelease: false
|
|
139
139
|
version_requirements: !ruby/object:Gem::Requirement
|
|
140
140
|
requirements:
|
|
141
141
|
- - "~>"
|
|
142
142
|
- !ruby/object:Gem::Version
|
|
143
|
-
version: 0.7
|
|
143
|
+
version: '0.7'
|
|
144
144
|
- !ruby/object:Gem::Dependency
|
|
145
145
|
name: rubocop-sequel
|
|
146
146
|
requirement: !ruby/object:Gem::Requirement
|
|
147
147
|
requirements:
|
|
148
148
|
- - "~>"
|
|
149
149
|
- !ruby/object:Gem::Version
|
|
150
|
-
version: 0.4
|
|
150
|
+
version: '0.4'
|
|
151
151
|
type: :development
|
|
152
152
|
prerelease: false
|
|
153
153
|
version_requirements: !ruby/object:Gem::Requirement
|
|
154
154
|
requirements:
|
|
155
155
|
- - "~>"
|
|
156
156
|
- !ruby/object:Gem::Version
|
|
157
|
-
version: 0.4
|
|
157
|
+
version: '0.4'
|
|
158
158
|
- !ruby/object:Gem::Dependency
|
|
159
159
|
name: simplecov
|
|
160
160
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -186,6 +186,7 @@ files:
|
|
|
186
186
|
- lib/sequel/duckdb.rb
|
|
187
187
|
- lib/sequel/duckdb/helpers/copier.rb
|
|
188
188
|
- lib/sequel/duckdb/helpers/pathifier.rb
|
|
189
|
+
- lib/sequel/duckdb/helpers/sql_options.rb
|
|
189
190
|
- lib/sequel/duckdb/instance_registry.rb
|
|
190
191
|
- lib/sequel/duckdb/sql_time_tz.rb
|
|
191
192
|
- lib/sequel/duckdb/version.rb
|