sequelizer 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 +7 -0
- data/README.md +51 -1
- data/lib/sequel/extensions/funky.rb +11 -1
- data/lib/sequelizer/duckdb_dataset.rb +157 -0
- data/lib/sequelizer/version.rb +1 -1
- data/lib/sequelizer.rb +63 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50510a32c25ed7c0218c60a7ba9d615a2ccf909685cc719dc1110a3fe43a5baf
|
|
4
|
+
data.tar.gz: a11d4852a6f18ffa2fca2fb5a9a75b67fd6c605d8d38321cb2b7a042e7ed8427
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f859dc2afb3bf840d8f2a53215ba7c2437a511eafdd23467cdab868092c45a3e4a8772159ca9098f1b808eda15041d357b05d7fd2f0e7c0f4acc7ec82c68df5
|
|
7
|
+
data.tar.gz: 69bb86b1fc5965eef2fa4a5449618050cb984d285c93d99fa2e56269c9d2967573f2b8b918c0a992880f7278a0c55bdd48c165a2e744ccf814f356a8565befdb
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.4.1] - 2026-10-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **sequelizer:** Add Sequelizer.dataset_db, a read-only DuckDB dataset connection ([acf8ac8](https://github.com/outcomesinsights/sequelizer/commit/acf8ac8f130d3db87dd3e99374d1d44de4a214f4))
|
|
10
|
+
|
|
5
11
|
## [0.4.0] - 2026-10-25
|
|
6
12
|
|
|
7
13
|
### Breaking changes
|
|
@@ -173,3 +179,4 @@ All notable changes to this project will be documented in this file.
|
|
|
173
179
|
- The project itself
|
|
174
180
|
|
|
175
181
|
[0.4.0]: https://github.com/outcomesinsights/sequelizer/compare/v0.3.0...v0.4.0
|
|
182
|
+
[0.4.1]: https://github.com/outcomesinsights/sequelizer/compare/v0.4.0...v0.4.1
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Sequelizer [](https://github.com/outcomesinsights/sequelizer/actions/workflows/test.yml)
|
|
2
2
|
|
|
3
3
|
I was tired of writing the code to bootstrap a connection to my databases.
|
|
4
4
|
|
|
@@ -78,6 +78,56 @@ For PostgreSQL, `search_path`, `schema_search_path` and `schema` are three names
|
|
|
78
78
|
|
|
79
79
|
Take a look at the examples directory for a few ways you can specify your database configuration options. Copy a `sequelizer*.yml` example to `config/sequelizer.yml` to use it.
|
|
80
80
|
|
|
81
|
+
## Read-only DuckDB dataset connections
|
|
82
|
+
|
|
83
|
+
`Sequelizer.dataset_db` returns a read-only connection to one DuckDB dataset. It needs the `sequel-duckdb` and `duckdb` gems.
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
db = Sequelizer.dataset_db(url: "duckdb:///data/datasets", schema: "synpuf",
|
|
87
|
+
memory_limit: "51GB", temp_directory: "/scratch/duckdb",
|
|
88
|
+
max_temp_directory_size: "500GB")
|
|
89
|
+
db[:patients].count
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `url:` is `duckdb://<directory>` or `duckdb://<file>`. For a directory, the dataset file is the one `*.duckdb` file in `<directory>/<schema>/`. A URL that names an existing DuckDB file uses that file.
|
|
93
|
+
- `schema:` is the schema in the file that holds the dataset's tables. A table name without a schema reads that schema.
|
|
94
|
+
- The connection is an in-memory DuckDB database that attaches the dataset file read-only, so a write to a dataset table fails. A `CREATE` without a catalog goes to the in-memory database.
|
|
95
|
+
- `memory_limit:`, `temp_directory:`, `max_temp_directory_size:` and `threads:` are DuckDB settings. A setting that you do not pass keeps DuckDB's default. DuckDB's default `memory_limit` is 80% of the memory that the container may use, for each DuckDB database in the process.
|
|
96
|
+
- `readonly:` must be true. `readonly: false` raises an `ArgumentError`, because read-write connections are not supported yet.
|
|
97
|
+
- Each process caches one connection for each (`url`, `schema`), and every thread gets the same one. A second call for a cached (`url`, `schema`) with other DuckDB settings raises `Sequelizer::DatasetSettingsConflictError`.
|
|
98
|
+
- The .env file, environment variables and YAML files are not read.
|
|
99
|
+
|
|
100
|
+
## Loading Sequel extensions
|
|
101
|
+
|
|
102
|
+
An option named `extension_<name>` loads the Sequel extension `<name>` on each connection that Sequelizer makes. The option's value does not matter. For example, this line in .env loads `funky`:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
SEQUELIZER_EXTENSION_FUNKY=1
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
In config/sequelizer.yml, write `extension_funky: 1`. You can also load an extension yourself with `db.extension :funky`.
|
|
109
|
+
|
|
110
|
+
An option named `<database type>_db_opt_<setting>` becomes a `SET <setting>=<value>` statement on each new connection. For example, `postgres_db_opt_work_mem: 256MB` runs `SET work_mem=256MB`.
|
|
111
|
+
|
|
112
|
+
## Sequel extensions
|
|
113
|
+
|
|
114
|
+
Sequelizer ships these Sequel extensions. You can load each one on any Sequel connection, not only on one that Sequelizer made.
|
|
115
|
+
|
|
116
|
+
- **cold_col**: gives the columns of a dataset without a query to the database. It reads table schemas from a YAML file (`db.load_schema`), from `db.add_table_schema`, and from the tables and views that the session creates.
|
|
117
|
+
- **db_opts**: applies the `<database type>_db_opt_<setting>` options above. Sequelizer loads it on every connection.
|
|
118
|
+
- **funky**: functions that are written differently on Spark and on DuckDB, through `db.funky`. It supports only those two databases, and it raises an error on any other.
|
|
119
|
+
- `db.funky.hash(*parts)` returns a signed 64-bit integer that hashes the parts. The default strategy, `:md5`, gives the same id on DuckDB and on Spark when each part turns into the same string on both. That is true for strings, integers, booleans, decimals, dates and timestamps without a time zone. It is false for doubles of 10,000,000 or more, or under 0.001, in absolute value, and for DuckDB's TIMESTAMPTZ. Cast such a part to a decimal or a timestamp first.
|
|
120
|
+
- `db.funky.hash(*parts, strategy: :legacy)` gives the ids that sequelizer 0.3 and earlier built. Use it only to reproduce those ids, because on DuckDB it gives duplicate ids. An unknown strategy raises `Sequel::Funky::UnknownHashStrategyError`.
|
|
121
|
+
- `str_to_date(value, format)`, `collect_list(column)` and `make_json_column(dataset, key_column, value_column)` write the same operation for each database.
|
|
122
|
+
- **make_readyable**: `db.make_ready(use_schema:, search_path:, only:, except:)` prepares a Spark or DuckDB database for queries. It creates temporary views for the tables in a list of schemas, in order of precedence, and for external Parquet, ORC or other files given as `Pathname`s.
|
|
123
|
+
- **more_sql**: `db.create_schema(name, if_not_exists: true)`.
|
|
124
|
+
- **platform**: `db.platform` answers what the database supports and prefers (`supports?(:cte)`, `prefers?(:parquet)`, `[:interim_schema]`), and translates functions such as `date_diff(from, to)` and `days_between(from, to)`. The answers come from the CSV files in config/platforms/.
|
|
125
|
+
- **settable**: `db.set(timezone: "UTC")` runs `SET timezone=UTC`. A value is written into the statement as it is. Wrap a value in `Sequel.expr` when the database needs a quoted literal. `db.set_sql` returns the statements and runs nothing.
|
|
126
|
+
- **smart_select_remove**: replaces Sequel's `select_remove`. It finds the columns from the dataset's own SELECT list, or through `from_self` chains, so it does not need to query the database. It raises `Sequel::SmartSelectRemove::NoColumnsError` when it cannot find the columns, and `Sequel::SmartSelectRemove::AllColumnsRemovedError` when every column would be removed. In both cases Sequel's own `select_remove` would produce `SELECT *`, which still contains the removed columns.
|
|
127
|
+
- **sql_recorder**: `db.sql_recorder` is an array of every SQL statement sent to the database. Call `db.sql_recorder.clear` to empty it.
|
|
128
|
+
- **unionize**: `db.unionize(datasets, chunk_size: 100, all: true)` combines many datasets with UNION, one chunk at a time, for databases that cannot run one very large UNION. Each chunk goes into a temporary view on Spark and a temporary table on DuckDB. It supports only those two databases.
|
|
129
|
+
- **usable**: `db.use(:my_schema)` runs a USE statement that switches to that schema.
|
|
130
|
+
|
|
81
131
|
## Frustrations
|
|
82
132
|
|
|
83
133
|
I can't seem to figure out a way to avoid having to specify the database gem in the a user's bundler file. If anyone has ideas on how to automagically load the correct database gem based on the database options fed to Sequelizer, please let me know (#1)!
|
|
@@ -28,7 +28,17 @@ module Sequel
|
|
|
28
28
|
# strategy: :md5 (the default) casts each part to a string, writes a NULL
|
|
29
29
|
# part as chr(30), joins the parts with chr(31), and reads the first 16
|
|
30
30
|
# hex characters of their md5 as a signed integer. It gives the same id
|
|
31
|
-
# on DuckDB and on Spark
|
|
31
|
+
# on DuckDB and on Spark when each part turns into the same string on
|
|
32
|
+
# both. Measured on DuckDB 1.5 and Spark 3.5, that is true for strings,
|
|
33
|
+
# integers, booleans, decimals, dates, timestamps without a time zone,
|
|
34
|
+
# and doubles that are zero or from 0.001 up to (not including)
|
|
35
|
+
# 10,000,000 in absolute value. It is false for other doubles, which
|
|
36
|
+
# Spark writes in E notation (1.0E7) and DuckDB does not (10000000.0),
|
|
37
|
+
# and for DuckDB's TIMESTAMPTZ, whose text ends in a UTC offset.
|
|
38
|
+
# Before you pass such a part, cast a TIMESTAMPTZ to a timestamp, and
|
|
39
|
+
# cast a double to a decimal. The decimal gives the same text for
|
|
40
|
+
# 123456789.5, but not for 1e20 at DECIMAL(38,6), where DuckDB keeps
|
|
41
|
+
# the double's rounding error.
|
|
32
42
|
#
|
|
33
43
|
# strategy: :legacy is each database's form before :md5 existed:
|
|
34
44
|
# hash(concat(...)) on DuckDB and xxhash64(...) on Spark. Use it only to
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
require_relative "connection_maker"
|
|
3
|
+
|
|
4
|
+
module Sequelizer
|
|
5
|
+
# Raised when a dataset URL is not a duckdb:// URL.
|
|
6
|
+
class UnsupportedDatasetURLError < ArgumentError
|
|
7
|
+
def initialize(url)
|
|
8
|
+
super("dataset_db supports only duckdb:// URLs, not #{url.inspect}")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Raised when a dataset connection is asked to write.
|
|
13
|
+
class ReadWriteDatasetError < ArgumentError
|
|
14
|
+
def initialize
|
|
15
|
+
super("dataset_db supports only readonly: true; read-write dataset connections are not supported yet")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Raised when a directory URL has no dataset directory for the schema, or
|
|
20
|
+
# when that directory does not hold exactly one DuckDB file.
|
|
21
|
+
class MissingDatasetError < ArgumentError
|
|
22
|
+
def initialize(directory, schema, files)
|
|
23
|
+
super("#{directory}/#{schema} must hold exactly one *.duckdb file to be the dataset #{schema}, " \
|
|
24
|
+
"but it holds #{files.size}#{": #{files.join(", ")}" unless files.empty?}")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Raised when dataset_db is called again for a cached (url, schema) with
|
|
29
|
+
# other DuckDB settings than the cached connection uses.
|
|
30
|
+
class DatasetSettingsConflictError < ArgumentError
|
|
31
|
+
def initialize(url, schema, held, requested)
|
|
32
|
+
super("the dataset connection for url #{url.inspect}, schema #{schema.inspect} is already open " \
|
|
33
|
+
"in this process with the DuckDB settings #{held.inspect}, not the requested settings " \
|
|
34
|
+
"#{requested.inspect}. The first dataset_db call for a (url, schema) fixes its settings.")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# = DuckDBDataset
|
|
39
|
+
#
|
|
40
|
+
# A read-only connection to one dataset in DuckDB. The connection is an
|
|
41
|
+
# in-memory DuckDB database, which attaches the dataset's file read-only
|
|
42
|
+
# as the catalog +dataset+. The search path is <tt>memory.main</tt>, then
|
|
43
|
+
# <tt>dataset.<schema></tt>, so a table name without a schema reads the
|
|
44
|
+
# dataset, and a CREATE without a catalog goes to the in-memory database.
|
|
45
|
+
#
|
|
46
|
+
# The URL is <tt>duckdb://<path></tt>. When +path+ is a file, it is the
|
|
47
|
+
# dataset file. Otherwise +path+ is a directory of datasets, and the dataset
|
|
48
|
+
# file is the one <tt>*.duckdb</tt> file in <tt><path>/<schema></tt>.
|
|
49
|
+
#
|
|
50
|
+
# Use Sequelizer.dataset_db, which caches one connection per (url, schema).
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# DuckDBDataset.new(url: "duckdb:///data/datasets", schema: "synpuf").connection
|
|
54
|
+
class DuckDBDataset
|
|
55
|
+
URL_PREFIX = "duckdb://".freeze
|
|
56
|
+
MEMORY_URL = "duckdb::memory:".freeze
|
|
57
|
+
|
|
58
|
+
# The dataset file is attached under this fixed catalog name. A catalog
|
|
59
|
+
# named like its file can equal the schema inside it (seer_x.duckdb holds
|
|
60
|
+
# the schema seer_x), and DuckDB then stops with "Ambiguous reference to
|
|
61
|
+
# catalog or schema".
|
|
62
|
+
DATASET_CATALOG = "dataset".freeze
|
|
63
|
+
|
|
64
|
+
# The catalog of the in-memory database. It stays first on the search
|
|
65
|
+
# path: the first entry is the default database, which DuckDB cannot
|
|
66
|
+
# detach, and a read-only default database makes every CREATE without a
|
|
67
|
+
# catalog fail.
|
|
68
|
+
MEMORY_CATALOG = "memory".freeze
|
|
69
|
+
|
|
70
|
+
# The DuckDB settings that dataset_db accepts.
|
|
71
|
+
SETTINGS = %i[memory_limit temp_directory max_temp_directory_size threads].freeze
|
|
72
|
+
|
|
73
|
+
# Whether +url+ names a directory of datasets: a duckdb:// URL whose path
|
|
74
|
+
# is not an existing file.
|
|
75
|
+
#
|
|
76
|
+
# @param url [String] a database URL
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def self.directory_url?(url)
|
|
79
|
+
url.to_s.start_with?(URL_PREFIX) && !Pathname(url.to_s.delete_prefix(URL_PREFIX)).file?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# The settings Hash in the one form that dataset_db compares: String
|
|
83
|
+
# names and String values, without the settings that are nil.
|
|
84
|
+
#
|
|
85
|
+
# @param settings [Hash{Symbol => Object}] DuckDB settings
|
|
86
|
+
# @return [Hash{String => String}]
|
|
87
|
+
def self.normalize_settings(settings)
|
|
88
|
+
settings.compact.to_h { |name, value| [ name.to_s, value.to_s ] }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @!attribute [r] url
|
|
92
|
+
# @return [String] the duckdb:// URL of the dataset file or directory
|
|
93
|
+
# @!attribute [r] schema
|
|
94
|
+
# @return [String] the dataset's schema name
|
|
95
|
+
# @!attribute [r] settings
|
|
96
|
+
# @return [Hash{String => String}] the DuckDB settings
|
|
97
|
+
attr_reader :url, :schema, :settings
|
|
98
|
+
|
|
99
|
+
# @param url [String] a duckdb:// URL of a dataset file or of a directory
|
|
100
|
+
# of datasets
|
|
101
|
+
# @param schema [String, Symbol] the schema that holds the dataset's tables
|
|
102
|
+
# @param settings [Hash{String => String}] DuckDB settings, already
|
|
103
|
+
# normalized (see normalize_settings)
|
|
104
|
+
# @raise [UnsupportedDatasetURLError] if +url+ is not a duckdb:// URL
|
|
105
|
+
def initialize(url:, schema:, settings: {})
|
|
106
|
+
raise UnsupportedDatasetURLError, url unless url.to_s.start_with?(URL_PREFIX)
|
|
107
|
+
|
|
108
|
+
@url = url.to_s
|
|
109
|
+
@schema = schema.to_s
|
|
110
|
+
@settings = settings
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# The DuckDB file that holds the dataset.
|
|
114
|
+
#
|
|
115
|
+
# @return [Pathname] the absolute path of the dataset file
|
|
116
|
+
# @raise [MissingDatasetError] if a directory URL has no single dataset
|
|
117
|
+
# file for the schema
|
|
118
|
+
def dataset_file
|
|
119
|
+
path = Pathname(url.delete_prefix(URL_PREFIX)).expand_path
|
|
120
|
+
return path unless self.class.directory_url?(url)
|
|
121
|
+
|
|
122
|
+
dir = path + schema
|
|
123
|
+
files = dir.directory? ? dir.glob("*.duckdb").sort : []
|
|
124
|
+
raise MissingDatasetError.new(path, schema, files) unless files.one?
|
|
125
|
+
|
|
126
|
+
files.first
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Opens the connection. Every new pool connection attaches the dataset
|
|
130
|
+
# file read-only and sets its search path, in :after_connect. DuckDB keeps
|
|
131
|
+
# an ATTACH for the whole database until its last connection closes, but
|
|
132
|
+
# the search path is per connection, so both run on each connection.
|
|
133
|
+
# :after_connect gets the driver's connection, which runs only SQL
|
|
134
|
+
# strings, so it runs the SQL of sequel-duckdb's attach, not db.attach.
|
|
135
|
+
#
|
|
136
|
+
# The configuration sources (.env, environment variables and the YAML
|
|
137
|
+
# files) are ignored: the URL and the settings passed here decide the
|
|
138
|
+
# connection.
|
|
139
|
+
#
|
|
140
|
+
# @return [Sequel::Database] the connection, already tested
|
|
141
|
+
# @raise [MissingDatasetError] if the dataset file cannot be found
|
|
142
|
+
# @raise [Sequel::DatabaseConnectionError] if DuckDB cannot attach the file
|
|
143
|
+
def connection
|
|
144
|
+
file = dataset_file
|
|
145
|
+
sqls = nil
|
|
146
|
+
after_connect = ->(conn) { sqls.each { |sql| conn.query(sql) } }
|
|
147
|
+
db = ConnectionMaker.new(url: MEMORY_URL, config: settings, after_connect: after_connect,
|
|
148
|
+
test: false, ignore_yaml: true, ignore_env: true).connection
|
|
149
|
+
sqls = [
|
|
150
|
+
db.attach_sql(file.to_s, as: DATASET_CATALOG, read_only: true, if_not_exists: true),
|
|
151
|
+
"SET search_path = #{db.literal("#{MEMORY_CATALOG}.main,#{DATASET_CATALOG}.#{schema}")}"
|
|
152
|
+
]
|
|
153
|
+
db.test_connection
|
|
154
|
+
db
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
data/lib/sequelizer/version.rb
CHANGED
data/lib/sequelizer.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require_relative "sequelizer/version"
|
|
2
2
|
require_relative "sequelizer/connection_maker"
|
|
3
|
+
require_relative "sequelizer/duckdb_dataset"
|
|
3
4
|
require_relative "sequel/extensions/db_opts"
|
|
4
5
|
require_relative "sequel/extensions/settable"
|
|
5
6
|
|
|
@@ -48,6 +49,68 @@ module Sequelizer
|
|
|
48
49
|
Options.new.to_hash
|
|
49
50
|
end
|
|
50
51
|
|
|
52
|
+
@dataset_dbs = {}
|
|
53
|
+
@dataset_dbs_mutex = Mutex.new
|
|
54
|
+
|
|
55
|
+
# Returns a read-only connection to one DuckDB dataset, cached per process
|
|
56
|
+
# by (url, schema). Every thread that asks for the same (url, schema) gets
|
|
57
|
+
# the same Sequel::Database.
|
|
58
|
+
#
|
|
59
|
+
# The connection is an in-memory DuckDB database that attaches the dataset
|
|
60
|
+
# file read-only, so a write to a dataset table fails. Its search path puts
|
|
61
|
+
# the dataset's schema after the in-memory database, so a table name without
|
|
62
|
+
# a schema reads the dataset (<tt>db[:patients]</tt>), and a CREATE without
|
|
63
|
+
# a catalog goes to the in-memory database. See DuckDBDataset.
|
|
64
|
+
#
|
|
65
|
+
# The DuckDB settings apply to the in-memory database when it opens. A
|
|
66
|
+
# setting that is nil keeps DuckDB's default. DuckDB's default memory_limit
|
|
67
|
+
# is 80% of the memory that the container may use, for each DuckDB database
|
|
68
|
+
# in the process, so pass memory_limit where more than one runs.
|
|
69
|
+
#
|
|
70
|
+
# The .env file, environment variables and YAML files are not read.
|
|
71
|
+
#
|
|
72
|
+
# @param url [String] <tt>duckdb://<directory></tt>, a directory that holds
|
|
73
|
+
# one directory for each dataset, <tt><directory>/<schema>/</tt>, with
|
|
74
|
+
# exactly one <tt>*.duckdb</tt> file in it; or <tt>duckdb://<file></tt>,
|
|
75
|
+
# an existing DuckDB file, which is attached directly
|
|
76
|
+
# @param schema [String, Symbol] the schema in the DuckDB file that holds the
|
|
77
|
+
# dataset's tables
|
|
78
|
+
# @param readonly [Boolean] must be true; read-write connections are not
|
|
79
|
+
# supported yet
|
|
80
|
+
# @param memory_limit [String, nil] DuckDB's memory_limit, for example "51GB"
|
|
81
|
+
# @param temp_directory [String, nil] DuckDB's temp_directory, where it
|
|
82
|
+
# spills data that does not fit in memory
|
|
83
|
+
# @param max_temp_directory_size [String, nil] DuckDB's
|
|
84
|
+
# max_temp_directory_size, for example "500GB"
|
|
85
|
+
# @param threads [Integer, String, nil] DuckDB's threads
|
|
86
|
+
# @return [Sequel::Database] the cached read-only connection
|
|
87
|
+
# @raise [ReadWriteDatasetError] if +readonly+ is false
|
|
88
|
+
# @raise [UnsupportedDatasetURLError] if +url+ is not a duckdb:// URL
|
|
89
|
+
# @raise [MissingDatasetError] if a directory URL has no single DuckDB file
|
|
90
|
+
# for +schema+
|
|
91
|
+
# @raise [DatasetSettingsConflictError] if (url, schema) is cached with other
|
|
92
|
+
# DuckDB settings
|
|
93
|
+
#
|
|
94
|
+
# @example
|
|
95
|
+
# db = Sequelizer.dataset_db(url: "duckdb:///data/datasets", schema: "synpuf",
|
|
96
|
+
# memory_limit: "51GB", temp_directory: "/scratch/duckdb",
|
|
97
|
+
# max_temp_directory_size: "500GB")
|
|
98
|
+
# db[:patients].count
|
|
99
|
+
def self.dataset_db(url:, schema:, readonly: true, memory_limit: nil, temp_directory: nil,
|
|
100
|
+
max_temp_directory_size: nil, threads: nil)
|
|
101
|
+
raise ReadWriteDatasetError unless readonly
|
|
102
|
+
|
|
103
|
+
settings = DuckDBDataset.normalize_settings(memory_limit:, temp_directory:, max_temp_directory_size:, threads:)
|
|
104
|
+
key = [ url.to_s, schema.to_s ]
|
|
105
|
+
|
|
106
|
+
@dataset_dbs_mutex.synchronize do
|
|
107
|
+
cached = @dataset_dbs[key] ||= { db: DuckDBDataset.new(url:, schema:, settings:).connection, settings: }
|
|
108
|
+
raise DatasetSettingsConflictError.new(*key, cached[:settings], settings) unless cached[:settings] == settings
|
|
109
|
+
|
|
110
|
+
cached[:db]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
51
114
|
# Instantiates and memoizes a database connection. The +db+ method instantiates
|
|
52
115
|
# the connection on the first call and then memoizes itself so only a single
|
|
53
116
|
# connection is used on repeated calls.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sequelizer
|
|
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
|
|
@@ -285,6 +285,7 @@ files:
|
|
|
285
285
|
- lib/sequelizer.rb
|
|
286
286
|
- lib/sequelizer/cli.rb
|
|
287
287
|
- lib/sequelizer/connection_maker.rb
|
|
288
|
+
- lib/sequelizer/duckdb_dataset.rb
|
|
288
289
|
- lib/sequelizer/env_config.rb
|
|
289
290
|
- lib/sequelizer/options.rb
|
|
290
291
|
- lib/sequelizer/version.rb
|