sequel-hexspace 2.0.0 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4421ed6430cdfa0ede39d8b4956c01de6e65317af4c9ab72238e06aa98d0e18
4
- data.tar.gz: b01b91610baad3b154d7a44c193ad29e88fc940de34473158e60521af958161e
3
+ metadata.gz: 8ce1fd4d7fed71e9678ed12c286b3a1b1e0bff80d52ef2d78c222a52e7a09713
4
+ data.tar.gz: 9ae18e5f352c95e8390d6d26ea09d217f63dad5c105a2bd30b2290c87483d566
5
5
  SHA512:
6
- metadata.gz: bfb4bbe3fddb1be528f0cdd382e78dfcf651edd36de086248742cc4d31d23dfa293b4619ba70acf6066681fe58cb6b0661373badd187c80fd8df54935513e3cd
7
- data.tar.gz: 5a979505d337170a4d41bf96e135118284b34461a21611167856d0e95b79b63bf11eb71f4adae733c7ea579347d40091fba7ab038bc1efa5d10ccdf701801f5e
6
+ metadata.gz: f35426bc639c4dcf948f425629ef4ec895cc0c0334106c60461b18acb28ee4cf7b0067f6f36196a57d6ba8535476b018b6c1b9b210f1f5e06c2af397cb2760ce
7
+ data.tar.gz: d308e6d823bf60ddacfc2578e3b09a1892e376863ec5101573f4cc6a7c3fd7b496362288c63a0e8ec5ec595f95a981713fe6c4fca5588b9872ce95152d44d8d9
data/CHANGELOG.md CHANGED
@@ -1,8 +1,37 @@
1
1
  # Changelog
2
2
 
3
- ## 2.0.0 (unreleased)
3
+ ## [2.0.1] - 2026-10-07
4
4
 
5
- ### Breaking
5
+ ### Fixed
6
+
7
+ - Emulated `DELETE` and `UPDATE` no longer drop the original table. Spark has
8
+ neither statement, so the adapter rebuilds the table under a temporary name and
9
+ swaps it in. The swap used to drop the original and rename the copy into place.
10
+ A process killed between those two steps lost the table, with its rows left
11
+ under a generated name. The swap is now an `INSERT OVERWRITE`, so the original
12
+ table always exists.
13
+ - A temporary table left behind by an interrupted delete or update no longer
14
+ breaks every later delete or update on that table with
15
+ `TABLE_OR_VIEW_ALREADY_EXISTS`. The adapter now tolerates it and cleans it up.
16
+ - The temporary table for a schema-qualified table (`sch.items`) is now created
17
+ in that schema. It used to be created as a single dotted identifier in the
18
+ default database.
19
+ - `Sequel.date_add` / `Sequel.date_sub` now honour the `date_arithmetic`
20
+ extension's cast: the `:cast` option, or the generic timestamp type by
21
+ default. Both were silently ignored.
22
+
23
+ ### Changed
24
+
25
+ - Require `thrift >= 0.23` (was `>= 0.18`), and delete the `Thrift::Bytes`
26
+ backport that older versions needed. The upper bound stays `< 0.25`.
27
+ - Bound `hexspace` to `>= 0.2.1, < 0.4` (was `>= 0.2.1`). hexspace is a 0.x gem,
28
+ so a new minor version may break its API. 0.3.0 is its newest release, so the
29
+ bound excludes nothing that exists.
30
+ - Internal simplifications with no change in behaviour.
31
+
32
+ ## [2.0.0] - 2026-10-01
33
+
34
+ ### Breaking changes
6
35
 
7
36
  - Declare `required_ruby_version >= 3.3`. Ruby 3.2 reached end of life in March
8
37
  2026 and is no longer tested. Installing on an older Ruby now fails with a
@@ -11,10 +40,11 @@
11
40
 
12
41
  ### Note on the gap since 1.0.0
13
42
 
14
- 1.0.0 (April 2024) is the only version ever published; 1.0.1 was tagged in the
15
- repository but never released. Everything below has therefore been unavailable
16
- to users until now, and this is the first release to carry any of it. The list is
17
- reconstructed from git history.
43
+ 1.0.0 (April 2024) was the previous published release. There was never a 1.0.1
44
+ release and never a v1.0.1 tag — 1.0.1 was only a gemspec version bump, at
45
+ `ce8b23e`, never tagged and never published. Everything below had therefore been
46
+ unavailable to users until this release, which is the first to carry any of it.
47
+ The list is reconstructed from git history.
18
48
 
19
49
  ### Added
20
50
 
@@ -35,3 +65,6 @@ reconstructed from git history.
35
65
  - Route prepared `DELETE` statements through the emulated delete path.
36
66
  - Date and timestamp casting and literalisation, including Ruby 4.0
37
67
  compatibility.
68
+
69
+ [2.0.0]: https://github.com/outcomesinsights/sequel-hexspace/compare/v1.0.0...v2.0.0
70
+ [2.0.1]: https://github.com/outcomesinsights/sequel-hexspace/compare/v2.0.0...v2.0.1
data/README CHANGED
@@ -1,3 +1,92 @@
1
1
  This is a hexspace adapter for Sequel, designed to be used with Spark (not
2
2
  Hive). You can use the hexspace:// protocol in the Sequel connection URL
3
3
  to use this adapter.
4
+
5
+ == Emulated DELETE and UPDATE
6
+
7
+ Spark has no DELETE and no UPDATE, so Sequel::Dataset#delete and
8
+ Sequel::Dataset#update are emulated. Both run as:
9
+
10
+ 1. CREATE TABLE <table>__sequel_delete_emulate AS SELECT <the rows the
11
+ operation should leave behind>
12
+ 2. INSERT OVERWRITE TABLE <table> SELECT ... FROM
13
+ <table>__sequel_delete_emulate
14
+ 3. check that <table> now holds as many rows as the temp table did, then DROP
15
+ the temp table
16
+
17
+ The suffix goes on the table's own name, so the temp table is always in the
18
+ same schema -- and catalog -- as your table: deleting from schema.items builds
19
+ schema.items__sequel_delete_emulate there. Before 2026-10-02 the name was
20
+ assembled by stripping the quoting out of the literalized target, which
21
+ collapsed `schema`.`items` into the single identifier
22
+ "schema.items__sequel_delete_emulate", and Spark rejects that outright ("not a
23
+ valid name for tables/databases. Valid names only contain alphabet characters,
24
+ numbers and _"). So an emulated DELETE or UPDATE against a schema-qualified
25
+ dataset used to fail at step 1, without touching your table. If you are
26
+ upgrading past that there is nothing to clean up: the temp table could never be
27
+ created, so there is none to find.
28
+
29
+ This is not a transaction, and this adapter has none to offer you:
30
+ Database#transaction just yields. The adapter's own source says this path is
31
+ "designed to minimize the changes to the tests, and is not recommended for
32
+ production use". The rest of this section is what that disclaimer means in
33
+ practice, so that you can decide rather than discover.
34
+
35
+ === What is guaranteed
36
+
37
+ * Your table is never dropped. There is no window in which reading it fails
38
+ with table-or-view-not-found. (Before 2026-10-02 the swap dropped your table
39
+ and renamed the temp table into its place, and a process that died in between
40
+ left the table GONE.)
41
+
42
+ * The rows the operation intends to keep exist under
43
+ <table>__sequel_delete_emulate, in your table's own schema, from step 1 until
44
+ step 3. If the process dies before step 3, or if the row-count check in step
45
+ 3 disagrees, that table is left in place on purpose -- it is the recovery
46
+ copy, and it is the only name your data may be sitting under. Nothing in your
47
+ own source will name it, so if an emulated delete or update died, look there
48
+ first.
49
+
50
+ * That name is derived from your table's name and nothing else. It carries no
51
+ timestamp, process id or random suffix, and that is deliberate: the deaths
52
+ recovery exists for -- a SIGKILL, a lost connection -- raise nothing, so a
53
+ generated name would appear in no error, in no log, and nowhere in your
54
+ source, and the rows would be unfindable by anyone who had not been watching.
55
+ What that choice costs is the concurrency limitation below.
56
+
57
+ * Step 3 does not drop the temp table on the strength of step 2 merely
58
+ returning. Until the row count has been checked, the temp table is treated as
59
+ the only intact copy.
60
+
61
+ === What is NOT guaranteed
62
+
63
+ * Atomicity. INSERT OVERWRITE is unambiguously atomic only on an ACID table
64
+ such as Delta. This adapter emits USING <format> only when you pass :using to
65
+ create_table, and the temp table above passes none, so unless your metastore
66
+ defaults to an ACID format both tables are not ACID. Against a default Spark
67
+ thriftserver (Provider hive, TextInputFormat, LazySimpleSerDe) the write was
68
+ measured to stage and then commit -- a job that failed after writing 150,000
69
+ of 200,000 rows left the table holding its full pre-operation contents, and
70
+ SIGKILLing the client at seven points across the statement never left the
71
+ table absent, empty or partial. What is left is the commit step: replacing a
72
+ multi-file Hive table in place is not one atomic filesystem operation, so a
73
+ death of the SERVER, or a storage failure, inside that step can leave the
74
+ table PRESENT but empty or partial. A plain SELECT then returns the wrong
75
+ rows SILENTLY rather than failing loudly. That window has not been
76
+ reproduced and is not quantified; it is accepted, not closed.
77
+
78
+ * Re-running is not free. Step 1 is preceded by a DROP TABLE IF EXISTS of
79
+ <table>__sequel_delete_emulate, because Spark's CREATE TABLE is not
80
+ idempotent and a stranded temp table otherwise makes every later delete or
81
+ update on the same table fail. That means re-running a delete or update that
82
+ died DESTROYS the recovery copy. Recover your data first, then re-run.
83
+
84
+ * Concurrency. The temp table name is derived from the target table name and is
85
+ not made unique per operation, so two emulated deletes or updates against the
86
+ same table at the same time will fight over it. Serialize them. This is not
87
+ fixed, and it is not going to be: a unique name is what would fix it, and the
88
+ recovery guarantee above is why that is not on offer.
89
+
90
+ If any of that is unacceptable for your workload, do not use emulated DELETE or
91
+ UPDATE -- build the replacement table yourself, under a name you control, and
92
+ swap it in on terms you have chosen.
@@ -0,0 +1,54 @@
1
+ # rubocop:disable all
2
+ # Restore Thrift::Client#handle_exception and #reply_seqid for thrift >= 0.24.
3
+ #
4
+ # hexspace's generated Thrift client (lib/hexspace/tcli_service.rb) calls both,
5
+ # 21 times each. thrift 0.24.0 removed them, folding their work into the new
6
+ # #validate_message_begin -- so on thrift 0.24 every Spark connection dies in
7
+ # recv_OpenSession with NoMethodError, surfacing as
8
+ # Sequel::DatabaseConnectionError.
9
+ #
10
+ # Upstream will not fix this for us: hexspace's generated client is untouched
11
+ # since 2023-05-08, the latest release (0.3.0, Apr 2025) predates thrift 0.24,
12
+ # and no issue or PR there mentions thrift. Regenerating that client against
13
+ # 0.24 would drop support for thrift < 0.24, which is a maintainer's decision
14
+ # rather than ours to force.
15
+ #
16
+ # Both methods are restored verbatim from thrift 0.23.0 and are implemented
17
+ # purely in terms of primitives thrift 0.24 still provides and still uses in
18
+ # validate_message_begin. The dequeue accounting is unchanged: exactly one
19
+ # dequeue_pending_seqid per call on both the reply and the exception path.
20
+ #
21
+ # Note this keeps 0.23 semantics -- it does NOT add 0.24's stricter checks for
22
+ # invalid message type and wrong method name. Remove once hexspace regenerates
23
+ # its client against thrift >= 0.24.
24
+ if defined?(Thrift::Client)
25
+ # Both method bodies below are copied verbatim from thrift 0.23.0 and
26
+ # deliberately not restyled: keeping them byte-comparable with upstream is
27
+ # what makes them auditable. That is enforced by the `rubocop:disable all` on
28
+ # this file's first line, which rubocop honours however it is invoked -- a
29
+ # .rubocop.yml Exclude is not, because rubocop ignores Exclude for a path named
30
+ # on the command line and treefmt names them. This file holds the shim and
31
+ # nothing else so the rest of the adapter stays linted.
32
+ #
33
+ # reply_seqid also cannot be renamed to a predicate -- hexspace's generated
34
+ # client calls it by that exact name.
35
+ module Thrift
36
+ module Client
37
+ unless method_defined?(:handle_exception) || private_method_defined?(:handle_exception)
38
+ def handle_exception(mtype)
39
+ if mtype == MessageTypes::EXCEPTION
40
+ dequeue_pending_seqid
41
+ raise_application_exception
42
+ end
43
+ end
44
+ end
45
+
46
+ unless method_defined?(:reply_seqid) || private_method_defined?(:reply_seqid)
47
+ def reply_seqid(rseqid)
48
+ expected_seqid = dequeue_pending_seqid
49
+ !expected_seqid.nil? && rseqid == expected_seqid
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -1,72 +1,6 @@
1
- require_relative 'shared/hexspace'
2
- require 'hexspace'
3
-
4
- # Backport upstream fix (THRIFT-5909, PR #3270) for thrift 0.22.0 on Ruby 3.4+.
5
- # Thrift::Bytes.empty_byte_buffer calls force_encoding on string literals,
6
- # which triggers "literal string will be frozen in the future" warnings.
7
- # Remove this once thrift > 0.22.0 is released with the fix.
8
- if defined?(Thrift::Bytes) && (spec = Gem.loaded_specs['thrift']) && spec.version < Gem::Version.new('0.23')
9
- module Thrift
10
- module Bytes
11
- def self.empty_byte_buffer(size=nil)
12
- if size&.positive?
13
- "\0".b * size
14
- else
15
- ''.b
16
- end
17
- end
18
- end
19
- end
20
- end
21
-
22
- # Restore Thrift::Client#handle_exception and #reply_seqid for thrift >= 0.24.
23
- #
24
- # hexspace's generated Thrift client (lib/hexspace/tcli_service.rb) calls both,
25
- # 21 times each. thrift 0.24.0 removed them, folding their work into the new
26
- # #validate_message_begin -- so on thrift 0.24 every Spark connection dies in
27
- # recv_OpenSession with NoMethodError, surfacing as
28
- # Sequel::DatabaseConnectionError.
29
- #
30
- # Upstream will not fix this for us: hexspace's generated client is untouched
31
- # since 2023-05-08, the latest release (0.3.0, Apr 2025) predates thrift 0.24,
32
- # and no issue or PR there mentions thrift. Regenerating that client against
33
- # 0.24 would drop support for thrift < 0.24, which is a maintainer's decision
34
- # rather than ours to force.
35
- #
36
- # Both methods are restored verbatim from thrift 0.23.0 and are implemented
37
- # purely in terms of primitives thrift 0.24 still provides and still uses in
38
- # validate_message_begin. The dequeue accounting is unchanged: exactly one
39
- # dequeue_pending_seqid per call on both the reply and the exception path.
40
- #
41
- # Note this keeps 0.23 semantics -- it does NOT add 0.24's stricter checks for
42
- # invalid message type and wrong method name. Remove once hexspace regenerates
43
- # its client against thrift >= 0.24.
44
- # rubocop:disable-next Style/GuardClause, Naming/PredicateMethod
45
- if defined?(Thrift::Client)
46
- # Both methods below are copied verbatim from thrift 0.23.0 and deliberately
47
- # not restyled: keeping them byte-comparable with upstream is what makes them
48
- # auditable. reply_seqid also cannot be renamed to a predicate -- hexspace's
49
- # generated client calls it by that exact name.
50
- module Thrift
51
- module Client
52
- unless method_defined?(:handle_exception) || private_method_defined?(:handle_exception)
53
- def handle_exception(mtype)
54
- if mtype == MessageTypes::EXCEPTION
55
- dequeue_pending_seqid
56
- raise_application_exception
57
- end
58
- end
59
- end
60
-
61
- unless method_defined?(:reply_seqid) || private_method_defined?(:reply_seqid)
62
- def reply_seqid(rseqid)
63
- expected_seqid = dequeue_pending_seqid
64
- !expected_seqid.nil? && rseqid == expected_seqid
65
- end
66
- end
67
- end
68
- end
69
- end
1
+ require_relative "shared/hexspace"
2
+ require "hexspace"
3
+ require_relative "hexspace/thrift_compat"
70
4
 
71
5
  module Sequel
72
6
  module Hexspace
@@ -80,7 +14,7 @@ module Sequel
80
14
  def connect(server)
81
15
  opts = server_opts(server)
82
16
  opts[:username] = opts[:user]
83
- opts.select!{|k,v| v.to_s != '' && ALLOWED_CLIENT_KEYWORDS.include?(k)}
17
+ opts.select! { |k, v| v.to_s != "" && ALLOWED_CLIENT_KEYWORDS.include?(k) }
84
18
  ::Hexspace::Client.new(**opts)
85
19
  end
86
20
 
@@ -105,9 +39,9 @@ module Sequel
105
39
  end
106
40
  end
107
41
 
108
- def execute(sql, opts=OPTS)
42
+ def execute(sql, opts = OPTS)
109
43
  synchronize(opts[:server]) do |conn|
110
- res = log_connection_yield(sql, conn){conn.execute(sql, result_object: true)}
44
+ res = log_connection_yield(sql, conn) { conn.execute(sql, result_object: true) }
111
45
  rescue => e
112
46
  raise_error(e)
113
47
  else
@@ -115,7 +49,7 @@ module Sequel
115
49
  end
116
50
  end
117
51
 
118
- def execute_insert(sql, opts=OPTS)
52
+ def execute_insert(sql, opts = OPTS)
119
53
  execute(sql, opts)
120
54
 
121
55
  # Return nil instead of empty array.
@@ -140,26 +74,20 @@ module Sequel
140
74
  types = result.column_types
141
75
  column_info = columns.map.with_index do |name, i|
142
76
  conversion_proc = case types[i]
143
- when 'binary'
77
+ when "binary"
144
78
  Sequel.method(:blob)
145
- when 'timestamp'
79
+ when "timestamp"
146
80
  db.method(:to_application_timestamp)
147
81
  end
148
82
 
149
- [i, name, conversion_proc]
83
+ [ i, name, conversion_proc ]
150
84
  end
151
85
 
152
86
  result.rows.each do |row|
153
87
  h = {}
154
88
  column_info.each do |i, name, conversion_proc|
155
89
  value = row[i]
156
- h[name] = if value.nil?
157
- nil
158
- elsif conversion_proc
159
- conversion_proc.call(value)
160
- else
161
- value
162
- end
90
+ h[name] = conversion_proc && !value.nil? ? conversion_proc.call(value) : value
163
91
  end
164
92
  yield h
165
93
  end
@@ -1,25 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'spark'
3
+ require_relative "spark"
4
4
 
5
5
  module Sequel
6
-
7
6
  module Hexspace
8
-
9
- module DatabaseMethods
10
-
11
- include Sequel::Spark::DatabaseMethods
12
-
13
- end
14
-
15
- module DatasetMethods
16
-
17
- include Sequel::Spark::DatasetMethods
18
-
19
- end
20
-
7
+ DatabaseMethods = Sequel::Spark::DatabaseMethods
8
+ DatasetMethods = Sequel::Spark::DatasetMethods
21
9
  end
22
10
 
23
11
  Sequel::Database.set_shared_adapter_scheme(:hexspace, Sequel::Hexspace)
24
-
25
12
  end
@@ -1,6 +1,6 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- require 'sequel/adapters/utils/unmodified_identifiers'
3
+ require "sequel/adapters/utils/unmodified_identifiers"
4
4
 
5
5
  module Sequel
6
6
  module Spark
@@ -9,7 +9,7 @@ module Sequel
9
9
  module DatabaseMethods
10
10
  include UnmodifiedIdentifiers::DatabaseMethods
11
11
 
12
- def create_schema(schema_name, opts=OPTS)
12
+ def create_schema(schema_name, opts = OPTS)
13
13
  run(create_schema_sql(schema_name, opts))
14
14
  end
15
15
 
@@ -17,7 +17,7 @@ module Sequel
17
17
  :spark
18
18
  end
19
19
 
20
- def drop_schema(schema_name, opts=OPTS)
20
+ def drop_schema(schema_name, opts = OPTS)
21
21
  run(drop_schema_sql(schema_name, opts))
22
22
  end
23
23
 
@@ -26,28 +26,28 @@ module Sequel
26
26
  def serial_primary_key_options
27
27
  # We could raise an exception here instead of just
28
28
  # ignoring the primary key setting.
29
- {:type=>Integer}
29
+ { type: Integer }
30
30
  end
31
31
 
32
32
  def supports_create_table_if_not_exists?
33
33
  true
34
34
  end
35
35
 
36
- def tables(opts=OPTS)
36
+ def tables(opts = OPTS)
37
37
  _mangle_tables(_tables("TABLES", :tableName, opts) - _views(opts), opts)
38
38
  end
39
39
 
40
40
  # Spark does not support transactions.
41
- def transaction(opts=nil)
41
+ def transaction(_opts = nil)
42
42
  yield
43
43
  end
44
44
 
45
45
  # Use an inline VALUES table.
46
46
  def values(v)
47
- @default_dataset.clone(:values=>v)
47
+ @default_dataset.clone(values: v)
48
48
  end
49
49
 
50
- def views(opts=OPTS)
50
+ def views(opts = OPTS)
51
51
  _mangle_tables(_views(opts), opts)
52
52
  end
53
53
 
@@ -56,10 +56,10 @@ module Sequel
56
56
  def _tables(type, column, opts)
57
57
  sql = String.new
58
58
  sql << "SHOW " << type
59
- if schema = opts[:schema]
59
+ if (schema = opts[:schema])
60
60
  sql << " IN " << literal(schema)
61
61
  end
62
- if like = opts[:like]
62
+ if (like = opts[:like])
63
63
  sql << " LIKE " << literal(like)
64
64
  end
65
65
 
@@ -68,7 +68,7 @@ module Sequel
68
68
  # Always internally qualify, so that if a table name in a schema
69
69
  # has the same name as a temporary view, it will not exclude
70
70
  # the table name.
71
- ds.map([:namespace, column]).map do |ns, name|
71
+ ds.map([ :namespace, column ]).map do |ns, name|
72
72
  if ns && !ns.empty?
73
73
  Sequel::SQL::QualifiedIdentifier.new(ns, name)
74
74
  else
@@ -85,32 +85,32 @@ module Sequel
85
85
  if opts[:qualify]
86
86
  tables
87
87
  else
88
- tables.map{|t| t.is_a?(Sequel::SQL::QualifiedIdentifier) ? t.column.to_sym : t}
88
+ tables.map { |t| t.is_a?(Sequel::SQL::QualifiedIdentifier) ? t.column.to_sym : t }
89
89
  end
90
90
  end
91
91
 
92
92
  def create_schema_sql(schema_name, opts)
93
93
  sql = String.new
94
- sql << 'CREATE SCHEMA '
95
- sql << 'IF NOT EXISTS ' if opts[:if_not_exists]
94
+ sql << "CREATE SCHEMA "
95
+ sql << "IF NOT EXISTS " if opts[:if_not_exists]
96
96
  sql << literal(schema_name)
97
97
 
98
- if comment = opts[:comment]
99
- sql << ' COMMENT '
98
+ if (comment = opts[:comment])
99
+ sql << " COMMENT "
100
100
  sql << literal(comment)
101
101
  end
102
102
 
103
- if location = opts[:location]
104
- sql << ' LOCATION '
103
+ if (location = opts[:location])
104
+ sql << " LOCATION "
105
105
  sql << literal(location)
106
106
  end
107
107
 
108
- if properties = opts[:properties]
109
- sql << ' WITH DBPROPERTIES ('
108
+ if (properties = opts[:properties])
109
+ sql << " WITH DBPROPERTIES ("
110
110
  properties.each do |k, v|
111
111
  sql << literal(k.to_s) << "=" << literal(v.to_s)
112
112
  end
113
- sql << ')'
113
+ sql << ")"
114
114
  end
115
115
 
116
116
  sql
@@ -118,11 +118,11 @@ module Sequel
118
118
 
119
119
  def create_table_prefix_sql(name, options)
120
120
  sql = String.new
121
- sql << 'CREATE '
122
- sql << 'EXTERNAL ' if options[:external]
123
- sql << 'TEMPORARY ' if options[:temp]
124
- sql << 'TABLE '
125
- sql << 'IF NOT EXISTS ' if options[:if_not_exists]
121
+ sql << "CREATE "
122
+ sql << "EXTERNAL " if options[:external]
123
+ sql << "TEMPORARY " if options[:temp]
124
+ sql << "TABLE "
125
+ sql << "IF NOT EXISTS " if options[:if_not_exists]
126
126
  sql << quote_schema_table(name)
127
127
  sql
128
128
  end
@@ -146,7 +146,7 @@ module Sequel
146
146
  end
147
147
 
148
148
  sql = String.new
149
- sql << create_view_sql_append_columns("CREATE #{'OR REPLACE 'if options[:replace]}#{'TEMPORARY ' if options[:temp]}VIEW#{' IF NOT EXISTS' if options[:if_not_exists]} #{quote_schema_table(name)}", options[:columns])
149
+ sql << create_view_sql_append_columns("CREATE #{'OR REPLACE ' if options[:replace]}#{'TEMPORARY ' if options[:temp]}VIEW#{' IF NOT EXISTS' if options[:if_not_exists]} #{quote_schema_table(name)}", options[:columns])
150
150
 
151
151
  if source
152
152
  source = source.sql if source.is_a?(Dataset)
@@ -157,7 +157,7 @@ module Sequel
157
157
  end
158
158
 
159
159
  def _append_table_view_options_sql(sql, options)
160
- if like = options[:like]
160
+ if (like = options[:like])
161
161
  sql << " LIKE " << literal(like)
162
162
  end
163
163
 
@@ -165,7 +165,7 @@ module Sequel
165
165
  sql << " USING " << options[:using].to_s
166
166
  end
167
167
 
168
- if location = options[:location]
168
+ if (location = options[:location])
169
169
  sql << " LOCATION " << literal(location)
170
170
  end
171
171
 
@@ -187,28 +187,28 @@ module Sequel
187
187
  end
188
188
 
189
189
  if options[:options]
190
- sql << ' OPTIONS ('
190
+ sql << " OPTIONS ("
191
191
  options[:options].each do |k, v|
192
192
  sql << literal(k.to_s) << "=" << literal(v.to_s)
193
193
  end
194
- sql << ')'
194
+ sql << ")"
195
195
  end
196
196
 
197
197
  sql
198
198
  end
199
199
 
200
200
  def _append_column_list_sql(sql, columns)
201
- sql << '('
201
+ sql << "("
202
202
  schema_utility_dataset.send(:identifier_list_append, sql, Array(columns))
203
- sql << ')'
203
+ sql << ")"
204
204
  end
205
205
 
206
206
  def drop_schema_sql(schema_name, opts)
207
207
  sql = String.new
208
- sql << 'DROP SCHEMA '
209
- sql << 'IF EXISTS ' if opts[:if_exists]
208
+ sql << "DROP SCHEMA "
209
+ sql << "IF EXISTS " if opts[:if_exists]
210
210
  sql << literal(schema_name)
211
- sql << ' CASCADE' if opts[:cascade]
211
+ sql << " CASCADE" if opts[:cascade]
212
212
  sql
213
213
  end
214
214
 
@@ -222,7 +222,7 @@ module Sequel
222
222
  m = output_identifier_meth(opts[:dataset])
223
223
  im = input_identifier_meth(opts[:dataset])
224
224
  metadata_dataset.with_sql("DESCRIBE #{"#{im.call(opts[:schema])}." if opts[:schema]}#{im.call(table)}").map do |row|
225
- [m.call(row[:col_name]), {:db_type=>row[:data_type], :type=>schema_column_type(row[:data_type])}]
225
+ [ m.call(row[:col_name]), { db_type: row[:data_type], type: schema_column_type(row[:data_type]) } ]
226
226
  end
227
227
  end
228
228
 
@@ -230,36 +230,55 @@ module Sequel
230
230
  true
231
231
  end
232
232
 
233
- def type_literal_generic_file(column)
234
- 'binary'
233
+ def type_literal_generic_file(_column)
234
+ "binary"
235
235
  end
236
236
 
237
- def type_literal_generic_float(column)
238
- 'float'
237
+ def type_literal_generic_float(_column)
238
+ "float"
239
239
  end
240
240
 
241
- def type_literal_generic_string(column)
242
- 'string'
241
+ def type_literal_generic_string(_column)
242
+ "string"
243
243
  end
244
244
  end
245
245
 
246
246
  module DatasetMethods
247
247
  include UnmodifiedIdentifiers::DatasetMethods
248
248
 
249
- Dataset.def_sql_method(self, :select, [['if opts[:values]', %w'values'], ['else', %w'with select distinct columns from join where group having compounds order limit']])
249
+ Dataset.def_sql_method(self, :select, [ [ "if opts[:values]", %w[values] ], [ "else", %w[with select distinct columns from join where group having compounds order limit] ] ])
250
250
 
251
+ # Handle the date_arithmetic extension's DateAdd expressions using
252
+ # Spark's make_ym_interval/make_dt_interval functions.
253
+ #
254
+ # The extension lets a caller name the result type with the :cast
255
+ # option (DateAdd#cast_type), defaulting to the generic timestamp type,
256
+ # and the result is always cast to it here.
257
+ #
258
+ # The cast is unconditional because Spark's interval arithmetic does
259
+ # not settle on one result type -- it depends on both the input type
260
+ # and which interval function is used. Measured against a live server
261
+ # on the 3.5.0 that ci.yml pins: DATE + make_dt_interval and TIMESTAMP
262
+ # + either function yield a TIMESTAMP, but DATE + make_ym_interval
263
+ # yields a DATE. So there is no subset of cases in which the cast can
264
+ # be skipped and still be honoured, and an interval of all zeroes adds
265
+ # nothing to cast in the first place.
266
+ #
267
+ # Spark's CAST is narrower than other databases': types it does not
268
+ # have (:timestamptz) or cannot convert to (:interval) are rejected by
269
+ # the server with UNSUPPORTED_DATATYPE or DATATYPE_MISMATCH, raising
270
+ # Sequel::DatabaseError. That is deliberately not pre-screened here --
271
+ # an allowlist of castable types in the adapter would go stale against
272
+ # the server, and the server's own message names the offending type.
251
273
  def date_add_sql_append(sql, da)
252
274
  expr = da.expr
253
- cast_type = da.cast_type || Time
254
275
 
255
276
  h = Hash.new(0)
256
277
  da.interval.each do |k, v|
257
278
  h[k] = v || 0
258
279
  end
259
280
 
260
- if h[:weeks]
261
- h[:days] += h[:weeks] * 7
262
- end
281
+ h[:days] += h[:weeks] * 7
263
282
 
264
283
  if h[:years] != 0 || h[:months] != 0
265
284
  expr = Sequel.+(expr, Sequel.function(:make_ym_interval, h[:years], h[:months]))
@@ -269,14 +288,14 @@ module Sequel
269
288
  expr = Sequel.+(expr, Sequel.function(:make_dt_interval, h[:days], h[:hours], h[:minutes], h[:seconds]))
270
289
  end
271
290
 
272
- literal_append(sql, expr)
291
+ literal_append(sql, Sequel.cast(expr, da.cast_type || Time))
273
292
  end
274
293
 
275
294
  # Route prepared statement / bound variable deletes through the
276
295
  # emulated delete path, since Spark does not support native DELETE.
277
- def call(type, bind_variables=OPTS, *values, &)
296
+ def call(type, bind_variables = OPTS, *values, &)
278
297
  if type == :delete
279
- ps = to_prepared_statement(type, values, :extend=>send(:bound_variable_modules))
298
+ ps = to_prepared_statement(type, values, extend: send(:bound_variable_modules))
280
299
  ps.bind(bind_variables).delete
281
300
  else
282
301
  super
@@ -284,11 +303,13 @@ module Sequel
284
303
  end
285
304
 
286
305
  # Emulate delete by selecting all rows except the ones being deleted
287
- # into a new table, drop the current table, and rename the new
288
- # table to the current table name.
306
+ # into a new table, then overwriting the current table with that new
307
+ # table's contents.
289
308
  #
290
309
  # This is designed to minimize the changes to the tests, and is
291
- # not recommended for production use.
310
+ # not recommended for production use. It is not atomic and it has a
311
+ # residual failure window even after sequel-hexspace-rho hardened it --
312
+ # see _with_temp_table and the README section on emulated DELETE/UPDATE.
292
313
  def delete
293
314
  _with_temp_table
294
315
  end
@@ -299,21 +320,184 @@ module Sequel
299
320
  updated_vals = columns.values
300
321
 
301
322
  _with_temp_table do |tmp_name|
302
- db.from(tmp_name).insert([*updated_cols, *other_cols], select(*updated_vals, *other_cols))
323
+ db.from(tmp_name).insert([ *updated_cols, *other_cols ], select(*updated_vals, *other_cols))
303
324
  end
304
325
  end
305
326
 
327
+ # Build `<table>__sequel_delete_emulate` -- in the target's own schema,
328
+ # see _temp_table_name -- holding the rows the operation should leave
329
+ # behind, let the caller adjust it, then INSERT OVERWRITE those rows back
330
+ # into the original table.
331
+ #
332
+ # The original table object is never dropped. That is deliberate: until
333
+ # 2026-10-02 this method dropped the original and renamed the temp table
334
+ # into its place, so a process that died in between left the named table
335
+ # GONE, with its rows sitting under a runtime-generated name that appears
336
+ # nowhere in the caller's source. See sequel-hexspace-rho.
337
+ #
338
+ # WHAT THIS STILL DOES NOT GUARANTEE -- read this before relying on it.
339
+ # INSERT OVERWRITE is unambiguously atomic only on an ACID table. This
340
+ # adapter emits USING <format> only when a caller passes :using, and the
341
+ # create_table below passes none, so the temp table -- and typically the
342
+ # caller's own table -- is whatever the metastore defaults to. Measured
343
+ # against this gem's test server on 2026-10-02: Provider `hive`,
344
+ # TextInputFormat, LazySimpleSerDe. Not ACID.
345
+ #
346
+ # It nonetheless behaved better than "not ACID" implies, and the
347
+ # measurements are recorded here because they bound the risk
348
+ # that is left:
349
+ #
350
+ # * A job failure part way through the write does NOT truncate the
351
+ # target. raise_error fired after 150k of 200k rows and the table was
352
+ # left holding its FULL pre-operation contents, so the old files are
353
+ # not removed up front -- the write stages and then commits.
354
+ # * SIGKILLing the client at seven points across the statement never
355
+ # left the table absent, empty or partial. The thriftserver carries
356
+ # the statement on server-side after the client vanishes, so the table
357
+ # held either its pre- or its post-operation rows every time. The same
358
+ # harness against the old drop/rename sequence destroyed the table on
359
+ # the first try (400,000 rows stranded under the temp name), so it was
360
+ # landing in the window.
361
+ #
362
+ # What is left is the commit itself: replacing a multi-file Hive table in
363
+ # place is not one atomic filesystem operation, so a death of the SERVER
364
+ # -- not the client -- inside that commit, or a storage failure there, can
365
+ # still leave the table PRESENT but empty or partial, which a plain SELECT
366
+ # reports silently rather than loudly. That window was NOT reproduced: it
367
+ # needs the thriftserver killed, and this gem's test server is shared.
368
+ # Treat it as unquantified, not as absent.
369
+ #
370
+ # The trade is therefore deliberate and it is not free. A silently wrong
371
+ # read is in some ways worse than a loud table-not-found. What buys it is
372
+ # recoverability: an absent table is unrecoverable by anyone reading their
373
+ # own source, because the name holding the data appears nowhere in it,
374
+ # whereas the rows to keep are recoverable here at every instant -- see
375
+ # the ensure below. The same residual window is written down in the
376
+ # README, because a consumer cannot see this comment.
377
+ #
378
+ # The temp table is dropped in exactly two states, both of which make it a
379
+ # discardable copy:
380
+ #
381
+ # * the overwrite never started, so the original still holds its
382
+ # pre-operation contents; or
383
+ # * the overwrite finished and its row count was verified against the
384
+ # temp table, so the original holds the intended contents.
385
+ #
386
+ # In any other state -- a raise inside the overwrite, or a row count that
387
+ # disagrees with the temp table -- the temp table holds the only intact
388
+ # copy of the rows to keep and is left alone. That is sequel-hexspace-c5q's
389
+ # rule, carried over from the drop/rename sequence it was written for.
390
+ #
391
+ # Both the leading drop and the ensure exist because this sequence is not
392
+ # atomic and Spark's CREATE TABLE is not idempotent, so a process that
393
+ # dies part way through strands the temp table and every later delete or
394
+ # update on the same table then fails with TABLE_OR_VIEW_ALREADY_EXISTS --
395
+ # an error that names a table nothing in the caller's code refers to.
396
+ # This gem's own suite was poisoned that way on 2026-10-01: one aborted
397
+ # run left `a__sequel_delete_emulate` behind and the next run reported 51
398
+ # errors in tests that never touch table `a`.
399
+ #
400
+ # The ensure handles a raise or an interrupt, which still unwinds; the
401
+ # leading drop handles the rest (SIGKILL, a lost connection, a crash),
402
+ # where nothing of ours gets to run at all. Each names exactly the one
403
+ # table this method creates and owns -- neither enumerates the server,
404
+ # which may be shared with tables belonging to nobody here.
306
405
  private def _with_temp_table
307
406
  n = count
308
407
  table_name = first_source_table
309
- tmp_name = literal(table_name).gsub('`', '') + "__sequel_delete_emulate"
310
- db.create_table(tmp_name, :as=>select_all.invert)
311
- yield tmp_name if defined?(yield)
312
- db.drop_table(table_name)
313
- db.rename_table(tmp_name, table_name)
408
+ tmp_name = _temp_table_name(table_name)
409
+ db.drop_table?(tmp_name)
410
+ db.create_table(tmp_name, as: select_all.invert)
411
+ overwrite_started = false
412
+ overwrite_verified = false
413
+ begin
414
+ yield tmp_name if defined?(yield)
415
+
416
+ # Counted before the overwrite rather than after, so that what the
417
+ # overwrite is checked against is the intended content itself and not
418
+ # a number derived from the same statement being verified.
419
+ expected = db.from(tmp_name).count
420
+
421
+ overwrite_started = true
422
+ _overwrite_from_temp_table(table_name, tmp_name)
423
+
424
+ actual = db.from(table_name).count
425
+ unless actual == expected
426
+ raise Sequel::Error, "emulated delete/update of #{literal(table_name)} left #{actual} rows, " \
427
+ "expected #{expected}; the rows to keep remain in #{literal(tmp_name)}, " \
428
+ "which has deliberately not been dropped"
429
+ end
430
+
431
+ overwrite_verified = true
432
+ ensure
433
+ db.drop_table?(tmp_name) if overwrite_verified || !overwrite_started
434
+ end
314
435
  n
315
436
  end
316
437
 
438
+ # The recovery table's name: the target's own name with
439
+ # `__sequel_delete_emulate` appended, IN THE TARGET'S OWN SCHEMA. The
440
+ # suffix goes on the LAST component only, so any qualifier -- a schema,
441
+ # or a catalog and a schema -- survives intact.
442
+ #
443
+ # Until 2026-10-02 this was `literal(table_name).gsub("`", "") +
444
+ # "__sequel_delete_emulate"`, which stripped the backticks that were
445
+ # keeping a qualified name's parts apart: `sch`.`items` collapsed to the
446
+ # single String "sch.items__sequel_delete_emulate". Sequel does not split
447
+ # a String on the dot -- Dataset#schema_and_table returns [nil, the whole
448
+ # string] for one -- so that went to the server as ONE identifier
449
+ # containing a dot, naming the default database rather than `sch`.
450
+ #
451
+ # That did not merely misplace the recovery copy, it broke the operation
452
+ # outright, which is worth recording because the bug report assumed
453
+ # otherwise. Spark refuses the name at CREATE TABLE:
454
+ #
455
+ # `sch.items__sequel_delete_emulate` is not a valid name for
456
+ # tables/databases. Valid names only contain alphabet characters,
457
+ # numbers and _.
458
+ #
459
+ # Measured against this gem's test server on 2026-10-02. So emulated
460
+ # DELETE and UPDATE against a schema-qualified dataset never worked at
461
+ # all, and no such temp table can ever have existed anywhere for a
462
+ # consumer to depend on. Appending to the last component is therefore a
463
+ # strict widening of the `<table>__sequel_delete_emulate` convention the
464
+ # README documents, not a change to it -- the unqualified case below is
465
+ # byte-for-byte what it was.
466
+ #
467
+ # A UNIQUE per-operation suffix was considered and rejected, even though
468
+ # it would close the concurrent-operation collision the README documents
469
+ # as a hazard. It would put the rows to keep under a runtime-generated
470
+ # name that appears nowhere in the caller's source, and that is the exact
471
+ # property sequel-hexspace-rho removed from this method. It is also
472
+ # unrecoverable in the cases recovery exists for: a SIGKILL or a lost
473
+ # connection raises nothing, so there is no error message for a generated
474
+ # name to be reported in, and the leading drop_table? above cannot find
475
+ # it on the next run either. A predictable name is what makes the README's
476
+ # "look there first" instruction executable. Concurrency stays the
477
+ # caller's problem, documented rather than solved.
478
+ private def _temp_table_name(table_name)
479
+ case table_name
480
+ when SQL::QualifiedIdentifier
481
+ SQL::QualifiedIdentifier.new(table_name.table, "#{table_name.column}__sequel_delete_emulate")
482
+ else
483
+ literal(table_name).gsub("`", "") + "__sequel_delete_emulate"
484
+ end
485
+ end
486
+
487
+ # Replace the whole contents of +table_name+ with the rows in +tmp_name+,
488
+ # keeping the original table object. The column list is taken from the
489
+ # TARGET, because INSERT OVERWRITE matches columns by position: naming them
490
+ # explicitly means a column order difference between the two tables would
491
+ # be a visible error rather than silently transposed data.
492
+ #
493
+ # Extracted so a test can stand in for it, which is the only way to pin
494
+ # the "temp table survives an unfinished overwrite" rule without killing a
495
+ # process in the middle of a statement.
496
+ private def _overwrite_from_temp_table(table_name, tmp_name)
497
+ rows = db.from(tmp_name).select(*db.from(table_name).columns)
498
+ db.run("INSERT OVERWRITE TABLE #{literal(table_name)} #{rows.sql}")
499
+ end
500
+
317
501
  protected def compound_clone(type, dataset, opts)
318
502
  dataset = dataset.from_self if dataset.opts[:with]
319
503
  super
@@ -343,7 +527,7 @@ module Sequel
343
527
  end
344
528
 
345
529
  def quoted_identifier_append(sql, name)
346
- sql << '`' << name.to_s.gsub('`', '``') << '`'
530
+ sql << "`" << name.to_s.gsub("`", "``") << "`"
347
531
  end
348
532
 
349
533
  def requires_sql_standard_datetimes?
@@ -355,7 +539,7 @@ module Sequel
355
539
  end
356
540
 
357
541
  def literal_blob_append(sql, v)
358
- sql << "to_binary('" << [v].pack("m*").gsub("\n", "") << "', 'base64')"
542
+ sql << "to_binary('" << [ v ].pack("m0") << "', 'base64')"
359
543
  end
360
544
 
361
545
  # Spark requires DATE 'YYYY-MM-DD' syntax instead of plain quoted strings
@@ -388,7 +572,7 @@ module Sequel
388
572
  "true"
389
573
  end
390
574
 
391
- def supports_cte?(type=:select)
575
+ def supports_cte?(type = :select)
392
576
  type == :select
393
577
  end
394
578
 
@@ -418,16 +602,16 @@ module Sequel
418
602
 
419
603
  # Handle forward references in existing CTEs in the dataset by inserting this
420
604
  # dataset before any dataset that would reference it.
421
- def with(name, dataset, opts=OPTS)
422
- opts = Hash[opts].merge!(:name=>name, :dataset=>dataset).freeze
605
+ def with(name, dataset, opts = OPTS)
606
+ opts = Hash[opts].merge!(name: name, dataset: dataset).freeze
423
607
  references = ReferenceExtractor.references(dataset)
424
608
 
425
- if with = @opts[:with]
609
+ if (with = @opts[:with])
426
610
  with = with.dup
427
611
  existing_references = @opts[:with_references]
428
612
 
429
- if referencing_dataset = existing_references[literal(name)]
430
- unless i = with.find_index{|o| o[:dataset].equal?(referencing_dataset)}
613
+ if (referencing_dataset = existing_references[literal(name)])
614
+ unless (i = with.find_index { |o| o[:dataset].equal?(referencing_dataset) })
431
615
  raise Sequel::Error, "internal error finding referencing dataset"
432
616
  end
433
617
 
@@ -447,32 +631,38 @@ module Sequel
447
631
  # Assume we will insert the dataset at the end, so existing references have priority
448
632
  references = references.merge(existing_references)
449
633
  else
450
- with = [opts]
634
+ with = [ opts ]
451
635
  end
452
636
 
453
- clone(:with=>with.freeze, :with_references=>references.freeze)
637
+ clone(with: with.freeze, with_references: references.freeze)
454
638
  end
455
639
 
456
640
  private def select_values_sql(sql)
457
- sql << 'VALUES '
641
+ sql << "VALUES "
458
642
  expression_list_append(sql, opts[:values])
459
643
  end
460
644
  end
461
645
 
462
646
  # ReferenceExtractor extracts references from datasets that will be used as CTEs.
463
647
  class ReferenceExtractor < ASTTransformer
464
- TABLE_IDENTIFIER_KEYS = [:from, :join].freeze
465
- COLUMN_IDENTIFIER_KEYS = [:select, :where, :having, :order, :group, :compounds].freeze
648
+ TABLE_IDENTIFIER_KEYS = [ :from, :join ].freeze
649
+ COLUMN_IDENTIFIER_KEYS = [ :select, :where, :having, :order, :group, :compounds ].freeze
466
650
 
467
651
  # Returns a hash of literal string identifier keys referenced by the given
468
652
  # dataset with the given dataset as the value for each key.
469
653
  def self.references(dataset)
470
- new(dataset).tap{|ext| ext.transform(dataset)}.references
654
+ new(dataset).tap { |ext| ext.transform(dataset) }.references
471
655
  end
472
656
 
473
657
  attr_reader :references
474
658
 
475
659
  def initialize(dataset)
660
+ # `super()` with explicit empty parens, NOT bare `super`. ASTTransformer
661
+ # defines no #initialize, so the inherited one is Object's, which takes
662
+ # no arguments -- bare `super` would forward `dataset` to it and raise
663
+ # ArgumentError on every instantiation. The call is a no-op today and is
664
+ # here so the chain still works if ASTTransformer ever gains state.
665
+ super()
476
666
  @dataset = dataset
477
667
  @references = {}
478
668
  end
@@ -504,10 +694,10 @@ module Sequel
504
694
  case o
505
695
  when Sequel::Dataset
506
696
  # Special case FROM/JOIN, because identifiers inside refer to tables and not columns
507
- TABLE_IDENTIFIER_KEYS.each{|k| o.opts[k]&.each{|jc| table_identifier_extract(jc)}}
697
+ TABLE_IDENTIFIER_KEYS.each { |k| o.opts[k]&.each { |jc| table_identifier_extract(jc) } }
508
698
 
509
699
  # Look in other keys that may have qualified references or subqueries
510
- COLUMN_IDENTIFIER_KEYS.each{|k| v(o.opts[k])}
700
+ COLUMN_IDENTIFIER_KEYS.each { |k| v(o.opts[k]) }
511
701
  when SQL::QualifiedIdentifier
512
702
  # If a qualified identifier has a qualified identifier as a key,
513
703
  # such as schema.table.column, ignore it, because CTE identifiers shouldn't
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel-hexspace
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
@@ -31,6 +31,9 @@ dependencies:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: 0.2.1
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '0.4'
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
@@ -38,13 +41,16 @@ dependencies:
38
41
  - - ">="
39
42
  - !ruby/object:Gem::Version
40
43
  version: 0.2.1
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.4'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: thrift
43
49
  requirement: !ruby/object:Gem::Requirement
44
50
  requirements:
45
51
  - - ">="
46
52
  - !ruby/object:Gem::Version
47
- version: '0.18'
53
+ version: '0.23'
48
54
  - - "<"
49
55
  - !ruby/object:Gem::Version
50
56
  version: '0.25'
@@ -54,24 +60,10 @@ dependencies:
54
60
  requirements:
55
61
  - - ">="
56
62
  - !ruby/object:Gem::Version
57
- version: '0.18'
63
+ version: '0.23'
58
64
  - - "<"
59
65
  - !ruby/object:Gem::Version
60
66
  version: '0.25'
61
- - !ruby/object:Gem::Dependency
62
- name: rake
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
- type: :development
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '0'
75
67
  - !ruby/object:Gem::Dependency
76
68
  name: minitest
77
69
  requirement: !ruby/object:Gem::Requirement
@@ -90,30 +82,36 @@ dependencies:
90
82
  name: minitest-hooks
91
83
  requirement: !ruby/object:Gem::Requirement
92
84
  requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '1.5'
93
88
  - - ">="
94
89
  - !ruby/object:Gem::Version
95
- version: '0'
90
+ version: 1.5.4
96
91
  type: :development
97
92
  prerelease: false
98
93
  version_requirements: !ruby/object:Gem::Requirement
99
94
  requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '1.5'
100
98
  - - ">="
101
99
  - !ruby/object:Gem::Version
102
- version: '0'
100
+ version: 1.5.4
103
101
  - !ruby/object:Gem::Dependency
104
102
  name: minitest-global_expectations
105
103
  requirement: !ruby/object:Gem::Requirement
106
104
  requirements:
107
- - - ">="
105
+ - - "~>"
108
106
  - !ruby/object:Gem::Version
109
- version: '0'
107
+ version: '1.0'
110
108
  type: :development
111
109
  prerelease: false
112
110
  version_requirements: !ruby/object:Gem::Requirement
113
111
  requirements:
114
- - - ">="
112
+ - - "~>"
115
113
  - !ruby/object:Gem::Version
116
- version: '0'
114
+ version: '1.0'
117
115
  - !ruby/object:Gem::Dependency
118
116
  name: simplecov
119
117
  requirement: !ruby/object:Gem::Requirement
@@ -142,12 +140,14 @@ files:
142
140
  - LICENSE
143
141
  - README
144
142
  - lib/sequel/adapters/hexspace.rb
143
+ - lib/sequel/adapters/hexspace/thrift_compat.rb
145
144
  - lib/sequel/adapters/shared/hexspace.rb
146
145
  - lib/sequel/adapters/shared/spark.rb
147
146
  homepage: https://github.com/outcomesinsights/sequel-hexspace
148
147
  licenses:
149
148
  - MIT
150
- metadata: {}
149
+ metadata:
150
+ rubygems_mfa_required: 'true'
151
151
  rdoc_options:
152
152
  - "--quiet"
153
153
  - "--line-numbers"