sequel-hexspace 1.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: b2949511f39f19c48b5120965d8b236fe93215c07ee9cf987d7b80f81d649b81
4
- data.tar.gz: 68b65e86ccc1ad3cc1fef989eb1d79b7927f96c4f4b5a60abb9699c7b425f4a4
3
+ metadata.gz: 8ce1fd4d7fed71e9678ed12c286b3a1b1e0bff80d52ef2d78c222a52e7a09713
4
+ data.tar.gz: 9ae18e5f352c95e8390d6d26ea09d217f63dad5c105a2bd30b2290c87483d566
5
5
  SHA512:
6
- metadata.gz: 021db16089f80aee9a8398b54747265e6eedb59713b7fac36316d4623a324ce96036e532df0ac75171ec47a04ade882ee168c5c773e5487a7591567bfaab707c
7
- data.tar.gz: 2767b9661167bc70fb775513f4abcb0465a77cc19c16f9bdb94972acd30896a310c3daeb6d8a34cfdd089ccf6c8bd6558bc459a9ae2eb3415c7cfd35b5d7273a
6
+ metadata.gz: f35426bc639c4dcf948f425629ef4ec895cc0c0334106c60461b18acb28ee4cf7b0067f6f36196a57d6ba8535476b018b6c1b9b210f1f5e06c2af397cb2760ce
7
+ data.tar.gz: d308e6d823bf60ddacfc2578e3b09a1892e376863ec5101573f4cc6a7c3fd7b496362288c63a0e8ec5ec595f95a981713fe6c4fca5588b9872ce95152d44d8d9
data/CHANGELOG.md ADDED
@@ -0,0 +1,70 @@
1
+ # Changelog
2
+
3
+ ## [2.0.1] - 2026-10-07
4
+
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
35
+
36
+ - Declare `required_ruby_version >= 3.3`. Ruby 3.2 reached end of life in March
37
+ 2026 and is no longer tested. Installing on an older Ruby now fails with a
38
+ clear message rather than breaking at runtime. This is the only breaking
39
+ change, and it is what makes this release a major one.
40
+
41
+ ### Note on the gap since 1.0.0
42
+
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.
48
+
49
+ ### Added
50
+
51
+ - Shared Hexspace mock adapter support, so dependent projects can test without a
52
+ live Spark server.
53
+ - `EXTERNAL`, `LOCATION` and `LIKE` support in `CREATE TABLE`.
54
+ - Always-on SimpleCov coverage and RuboCop enforcement in the development setup.
55
+
56
+ ### Fixed
57
+
58
+ - Support thrift 0.24 by restoring `Thrift::Client#handle_exception` and
59
+ `#reply_seqid`, which 0.24.0 removed while hexspace's generated client still
60
+ calls them. Without this every Spark connection failed with a
61
+ `Sequel::DatabaseConnectionError`. thrift is constrained to `>= 0.18, < 0.25`.
62
+ - Silence a frozen-string-literal warning from thrift 0.22.0 on Ruby 3.4+.
63
+ - `Database#database_type` returns `:spark` rather than `:hexspace`.
64
+ - Ignore `CASCADE` in `drop_table_sql`, which Spark SQL does not accept.
65
+ - Route prepared `DELETE` statements through the emulated delete path.
66
+ - Date and timestamp casting and literalisation, including Ruby 4.0
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/LICENSE CHANGED
@@ -1,4 +1,5 @@
1
1
  Copyright (c) 2023 Jeremy Evans
2
+ Copyright (c) 2026 Outcomes Insights, Inc.
2
3
 
3
4
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
5
  of this software and associated documentation files (the "Software"), to
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,10 +1,11 @@
1
- require 'hexspace'
2
- require_relative 'shared/spark'
1
+ require_relative "shared/hexspace"
2
+ require "hexspace"
3
+ require_relative "hexspace/thrift_compat"
3
4
 
4
5
  module Sequel
5
6
  module Hexspace
6
7
  class Database < Sequel::Database
7
- include Spark::DatabaseMethods
8
+ include DatabaseMethods
8
9
 
9
10
  set_adapter_scheme :hexspace
10
11
 
@@ -13,7 +14,7 @@ module Sequel
13
14
  def connect(server)
14
15
  opts = server_opts(server)
15
16
  opts[:username] = opts[:user]
16
- 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) }
17
18
  ::Hexspace::Client.new(**opts)
18
19
  end
19
20
 
@@ -27,9 +28,20 @@ module Sequel
27
28
  conn.instance_variable_set(:@sequel_invalid, true)
28
29
  end
29
30
 
30
- def execute(sql, opts=OPTS)
31
+ # Hexspace returns timestamp strings in UTC without an explicit offset.
32
+ # When no database timezone is configured, Sequel treats naive strings as
33
+ # local time, which shifts CURRENT_TIMESTAMP by the local UTC offset.
34
+ def to_application_timestamp(value)
35
+ if value.is_a?(String) && timezone.nil? && value !~ /(?:Z|[+-]\d{2}(?::?\d{2})?)\z/
36
+ Sequel.convert_timestamp(value, :utc)
37
+ else
38
+ super
39
+ end
40
+ end
41
+
42
+ def execute(sql, opts = OPTS)
31
43
  synchronize(opts[:server]) do |conn|
32
- 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) }
33
45
  rescue => e
34
46
  raise_error(e)
35
47
  else
@@ -37,7 +49,7 @@ module Sequel
37
49
  end
38
50
  end
39
51
 
40
- def execute_insert(sql, opts=OPTS)
52
+ def execute_insert(sql, opts = OPTS)
41
53
  execute(sql, opts)
42
54
 
43
55
  # Return nil instead of empty array.
@@ -51,7 +63,7 @@ module Sequel
51
63
  end
52
64
 
53
65
  class Dataset < Sequel::Dataset
54
- include Spark::DatasetMethods
66
+ include DatasetMethods
55
67
 
56
68
  def fetch_rows(sql)
57
69
  execute(sql) do |result|
@@ -62,26 +74,20 @@ module Sequel
62
74
  types = result.column_types
63
75
  column_info = columns.map.with_index do |name, i|
64
76
  conversion_proc = case types[i]
65
- when 'binary'
77
+ when "binary"
66
78
  Sequel.method(:blob)
67
- when 'timestamp'
79
+ when "timestamp"
68
80
  db.method(:to_application_timestamp)
69
81
  end
70
82
 
71
- [i, name, conversion_proc]
83
+ [ i, name, conversion_proc ]
72
84
  end
73
85
 
74
86
  result.rows.each do |row|
75
87
  h = {}
76
88
  column_info.each do |i, name, conversion_proc|
77
89
  value = row[i]
78
- h[name] = if value.nil?
79
- nil
80
- elsif conversion_proc
81
- conversion_proc.call(value)
82
- else
83
- value
84
- end
90
+ h[name] = conversion_proc && !value.nil? ? conversion_proc.call(value) : value
85
91
  end
86
92
  yield h
87
93
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "spark"
4
+
5
+ module Sequel
6
+ module Hexspace
7
+ DatabaseMethods = Sequel::Spark::DatabaseMethods
8
+ DatasetMethods = Sequel::Spark::DatasetMethods
9
+ end
10
+
11
+ Sequel::Database.set_shared_adapter_scheme(:hexspace, Sequel::Hexspace)
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,39 +85,54 @@ 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
117
117
  end
118
118
 
119
+ def create_table_prefix_sql(name, options)
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]
126
+ sql << quote_schema_table(name)
127
+ sql
128
+ end
129
+
119
130
  def create_table_sql(name, generator, options)
120
- _append_table_view_options_sql(super, options)
131
+ if options[:like] || (options[:using] && generator.columns.empty?)
132
+ _append_table_view_options_sql(create_table_prefix_sql(name, options), options)
133
+ else
134
+ _append_table_view_options_sql(super, options)
135
+ end
121
136
  end
122
137
 
123
138
  def create_table_as_sql(name, sql, options)
@@ -131,7 +146,7 @@ module Sequel
131
146
  end
132
147
 
133
148
  sql = String.new
134
- 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])
135
150
 
136
151
  if source
137
152
  source = source.sql if source.is_a?(Dataset)
@@ -142,10 +157,18 @@ module Sequel
142
157
  end
143
158
 
144
159
  def _append_table_view_options_sql(sql, options)
160
+ if (like = options[:like])
161
+ sql << " LIKE " << literal(like)
162
+ end
163
+
145
164
  if options[:using]
146
165
  sql << " USING " << options[:using].to_s
147
166
  end
148
167
 
168
+ if (location = options[:location])
169
+ sql << " LOCATION " << literal(location)
170
+ end
171
+
149
172
  if options[:partitioned_by]
150
173
  sql << " PARTITIONED BY "
151
174
  _append_column_list_sql(sql, options[:partitioned_by])
@@ -164,36 +187,42 @@ module Sequel
164
187
  end
165
188
 
166
189
  if options[:options]
167
- sql << ' OPTIONS ('
190
+ sql << " OPTIONS ("
168
191
  options[:options].each do |k, v|
169
192
  sql << literal(k.to_s) << "=" << literal(v.to_s)
170
193
  end
171
- sql << ')'
194
+ sql << ")"
172
195
  end
173
196
 
174
197
  sql
175
198
  end
176
199
 
177
200
  def _append_column_list_sql(sql, columns)
178
- sql << '('
201
+ sql << "("
179
202
  schema_utility_dataset.send(:identifier_list_append, sql, Array(columns))
180
- sql << ')'
203
+ sql << ")"
181
204
  end
182
205
 
183
206
  def drop_schema_sql(schema_name, opts)
184
207
  sql = String.new
185
- sql << 'DROP SCHEMA '
186
- sql << 'IF EXISTS ' if opts[:if_exists]
208
+ sql << "DROP SCHEMA "
209
+ sql << "IF EXISTS " if opts[:if_exists]
187
210
  sql << literal(schema_name)
188
- sql << ' CASCADE' if opts[:cascade]
211
+ sql << " CASCADE" if opts[:cascade]
189
212
  sql
190
213
  end
191
214
 
215
+ # Spark SQL does not support CASCADE on DROP TABLE.
216
+ # Support :purge option, ignore :cascade.
217
+ def drop_table_sql(name, options)
218
+ "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_schema_table(name)}#{' PURGE' if options[:purge]}"
219
+ end
220
+
192
221
  def schema_parse_table(table, opts)
193
222
  m = output_identifier_meth(opts[:dataset])
194
223
  im = input_identifier_meth(opts[:dataset])
195
224
  metadata_dataset.with_sql("DESCRIBE #{"#{im.call(opts[:schema])}." if opts[:schema]}#{im.call(table)}").map do |row|
196
- [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]) } ]
197
226
  end
198
227
  end
199
228
 
@@ -201,36 +230,55 @@ module Sequel
201
230
  true
202
231
  end
203
232
 
204
- def type_literal_generic_file(column)
205
- 'binary'
233
+ def type_literal_generic_file(_column)
234
+ "binary"
206
235
  end
207
236
 
208
- def type_literal_generic_float(column)
209
- 'float'
237
+ def type_literal_generic_float(_column)
238
+ "float"
210
239
  end
211
240
 
212
- def type_literal_generic_string(column)
213
- 'string'
241
+ def type_literal_generic_string(_column)
242
+ "string"
214
243
  end
215
244
  end
216
245
 
217
246
  module DatasetMethods
218
247
  include UnmodifiedIdentifiers::DatasetMethods
219
248
 
220
- 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] ] ])
221
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.
222
273
  def date_add_sql_append(sql, da)
223
274
  expr = da.expr
224
- cast_type = da.cast_type || Time
225
275
 
226
276
  h = Hash.new(0)
227
277
  da.interval.each do |k, v|
228
278
  h[k] = v || 0
229
279
  end
230
280
 
231
- if h[:weeks]
232
- h[:days] += h[:weeks] * 7
233
- end
281
+ h[:days] += h[:weeks] * 7
234
282
 
235
283
  if h[:years] != 0 || h[:months] != 0
236
284
  expr = Sequel.+(expr, Sequel.function(:make_ym_interval, h[:years], h[:months]))
@@ -240,15 +288,28 @@ module Sequel
240
288
  expr = Sequel.+(expr, Sequel.function(:make_dt_interval, h[:days], h[:hours], h[:minutes], h[:seconds]))
241
289
  end
242
290
 
243
- literal_append(sql, expr)
291
+ literal_append(sql, Sequel.cast(expr, da.cast_type || Time))
292
+ end
293
+
294
+ # Route prepared statement / bound variable deletes through the
295
+ # emulated delete path, since Spark does not support native DELETE.
296
+ def call(type, bind_variables = OPTS, *values, &)
297
+ if type == :delete
298
+ ps = to_prepared_statement(type, values, extend: send(:bound_variable_modules))
299
+ ps.bind(bind_variables).delete
300
+ else
301
+ super
302
+ end
244
303
  end
245
304
 
246
305
  # Emulate delete by selecting all rows except the ones being deleted
247
- # into a new table, drop the current table, and rename the new
248
- # table to the current table name.
306
+ # into a new table, then overwriting the current table with that new
307
+ # table's contents.
249
308
  #
250
309
  # This is designed to minimize the changes to the tests, and is
251
- # 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.
252
313
  def delete
253
314
  _with_temp_table
254
315
  end
@@ -259,21 +320,184 @@ module Sequel
259
320
  updated_vals = columns.values
260
321
 
261
322
  _with_temp_table do |tmp_name|
262
- 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))
263
324
  end
264
325
  end
265
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.
266
405
  private def _with_temp_table
267
406
  n = count
268
407
  table_name = first_source_table
269
- tmp_name = literal(table_name).gsub('`', '') + "__sequel_delete_emulate"
270
- db.create_table(tmp_name, :as=>select_all.invert)
271
- yield tmp_name if defined?(yield)
272
- db.drop_table(table_name)
273
- 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
274
435
  n
275
436
  end
276
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
+
277
501
  protected def compound_clone(type, dataset, opts)
278
502
  dataset = dataset.from_self if dataset.opts[:with]
279
503
  super
@@ -303,19 +527,37 @@ module Sequel
303
527
  end
304
528
 
305
529
  def quoted_identifier_append(sql, name)
306
- sql << '`' << name.to_s.gsub('`', '``') << '`'
530
+ sql << "`" << name.to_s.gsub("`", "``") << "`"
307
531
  end
308
532
 
309
533
  def requires_sql_standard_datetimes?
310
534
  true
311
535
  end
312
536
 
313
- def insert_supports_empty_values?
537
+ def insert_supports_empty_values?
314
538
  false
315
539
  end
316
540
 
317
541
  def literal_blob_append(sql, v)
318
- sql << "to_binary('" << [v].pack("m*").gsub("\n", "") << "', 'base64')"
542
+ sql << "to_binary('" << [ v ].pack("m0") << "', 'base64')"
543
+ end
544
+
545
+ # Spark requires DATE 'YYYY-MM-DD' syntax instead of plain quoted strings
546
+ def literal_date_append(sql, v)
547
+ sql << "DATE "
548
+ super
549
+ end
550
+
551
+ # Spark requires TIMESTAMP 'YYYY-MM-DD HH:MM:SS' syntax instead of plain quoted strings
552
+ def literal_time_append(sql, v)
553
+ sql << "TIMESTAMP "
554
+ super
555
+ end
556
+
557
+ # Spark requires TIMESTAMP 'YYYY-MM-DD HH:MM:SS' syntax instead of plain quoted strings
558
+ def literal_datetime_append(sql, v)
559
+ sql << "TIMESTAMP "
560
+ super
319
561
  end
320
562
 
321
563
  def literal_false
@@ -330,7 +572,7 @@ module Sequel
330
572
  "true"
331
573
  end
332
574
 
333
- def supports_cte?(type=:select)
575
+ def supports_cte?(type = :select)
334
576
  type == :select
335
577
  end
336
578
 
@@ -360,16 +602,16 @@ module Sequel
360
602
 
361
603
  # Handle forward references in existing CTEs in the dataset by inserting this
362
604
  # dataset before any dataset that would reference it.
363
- def with(name, dataset, opts=OPTS)
364
- 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
365
607
  references = ReferenceExtractor.references(dataset)
366
608
 
367
- if with = @opts[:with]
609
+ if (with = @opts[:with])
368
610
  with = with.dup
369
611
  existing_references = @opts[:with_references]
370
612
 
371
- if referencing_dataset = existing_references[literal(name)]
372
- 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) })
373
615
  raise Sequel::Error, "internal error finding referencing dataset"
374
616
  end
375
617
 
@@ -389,32 +631,38 @@ module Sequel
389
631
  # Assume we will insert the dataset at the end, so existing references have priority
390
632
  references = references.merge(existing_references)
391
633
  else
392
- with = [opts]
634
+ with = [ opts ]
393
635
  end
394
636
 
395
- clone(:with=>with.freeze, :with_references=>references.freeze)
637
+ clone(with: with.freeze, with_references: references.freeze)
396
638
  end
397
639
 
398
640
  private def select_values_sql(sql)
399
- sql << 'VALUES '
641
+ sql << "VALUES "
400
642
  expression_list_append(sql, opts[:values])
401
643
  end
402
644
  end
403
645
 
404
646
  # ReferenceExtractor extracts references from datasets that will be used as CTEs.
405
647
  class ReferenceExtractor < ASTTransformer
406
- TABLE_IDENTIFIER_KEYS = [:from, :join].freeze
407
- 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
408
650
 
409
651
  # Returns a hash of literal string identifier keys referenced by the given
410
652
  # dataset with the given dataset as the value for each key.
411
653
  def self.references(dataset)
412
- new(dataset).tap{|ext| ext.transform(dataset)}.references
654
+ new(dataset).tap { |ext| ext.transform(dataset) }.references
413
655
  end
414
656
 
415
657
  attr_reader :references
416
658
 
417
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()
418
666
  @dataset = dataset
419
667
  @references = {}
420
668
  end
@@ -446,10 +694,10 @@ module Sequel
446
694
  case o
447
695
  when Sequel::Dataset
448
696
  # Special case FROM/JOIN, because identifiers inside refer to tables and not columns
449
- 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) } }
450
698
 
451
699
  # Look in other keys that may have qualified references or subqueries
452
- COLUMN_IDENTIFIER_KEYS.each{|k| v(o.opts[k])}
700
+ COLUMN_IDENTIFIER_KEYS.each { |k| v(o.opts[k]) }
453
701
  when SQL::QualifiedIdentifier
454
702
  # If a qualified identifier has a qualified identifier as a key,
455
703
  # such as schema.table.column, ignore it, because CTE identifiers shouldn't
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel-hexspace
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
- autorequire:
8
+ - Ryan Duryea
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-03 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -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,81 +41,113 @@ 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
- name: rake
48
+ name: thrift
43
49
  requirement: !ruby/object:Gem::Requirement
44
50
  requirements:
45
51
  - - ">="
46
52
  - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
53
+ version: '0.23'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: '0.25'
57
+ type: :runtime
49
58
  prerelease: false
50
59
  version_requirements: !ruby/object:Gem::Requirement
51
60
  requirements:
52
61
  - - ">="
53
62
  - !ruby/object:Gem::Version
54
- version: '0'
63
+ version: '0.23'
64
+ - - "<"
65
+ - !ruby/object:Gem::Version
66
+ version: '0.25'
55
67
  - !ruby/object:Gem::Dependency
56
68
  name: minitest
57
69
  requirement: !ruby/object:Gem::Requirement
58
70
  requirements:
59
71
  - - "~>"
60
72
  - !ruby/object:Gem::Version
61
- version: '5.7'
73
+ version: '6.0'
62
74
  type: :development
63
75
  prerelease: false
64
76
  version_requirements: !ruby/object:Gem::Requirement
65
77
  requirements:
66
78
  - - "~>"
67
79
  - !ruby/object:Gem::Version
68
- version: '5.7'
80
+ version: '6.0'
69
81
  - !ruby/object:Gem::Dependency
70
82
  name: minitest-hooks
71
83
  requirement: !ruby/object:Gem::Requirement
72
84
  requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '1.5'
73
88
  - - ">="
74
89
  - !ruby/object:Gem::Version
75
- version: '0'
90
+ version: 1.5.4
76
91
  type: :development
77
92
  prerelease: false
78
93
  version_requirements: !ruby/object:Gem::Requirement
79
94
  requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '1.5'
80
98
  - - ">="
81
99
  - !ruby/object:Gem::Version
82
- version: '0'
100
+ version: 1.5.4
83
101
  - !ruby/object:Gem::Dependency
84
102
  name: minitest-global_expectations
85
103
  requirement: !ruby/object:Gem::Requirement
86
104
  requirements:
87
- - - ">="
105
+ - - "~>"
88
106
  - !ruby/object:Gem::Version
89
- version: '0'
107
+ version: '1.0'
90
108
  type: :development
91
109
  prerelease: false
92
110
  version_requirements: !ruby/object:Gem::Requirement
93
111
  requirements:
94
- - - ">="
112
+ - - "~>"
95
113
  - !ruby/object:Gem::Version
96
- version: '0'
114
+ version: '1.0'
115
+ - !ruby/object:Gem::Dependency
116
+ name: simplecov
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - "~>"
120
+ - !ruby/object:Gem::Version
121
+ version: '1.0'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - "~>"
127
+ - !ruby/object:Gem::Version
128
+ version: '1.0'
97
129
  description: |
98
130
  This is a hexspace adapter for Sequel, designed to be used with Spark (not
99
131
  Hive). You can use the hexspace:// protocol in the Sequel connection URL
100
132
  to use this adapter.
101
- email: code@jeremyevans.net
133
+ email: aguynamedryan@gmail.com
102
134
  executables: []
103
135
  extensions: []
104
136
  extra_rdoc_files:
105
137
  - LICENSE
106
138
  files:
139
+ - CHANGELOG.md
107
140
  - LICENSE
108
141
  - README
109
142
  - lib/sequel/adapters/hexspace.rb
143
+ - lib/sequel/adapters/hexspace/thrift_compat.rb
144
+ - lib/sequel/adapters/shared/hexspace.rb
110
145
  - lib/sequel/adapters/shared/spark.rb
111
- homepage: https://github.com/jeremyevans/sequel-hexspace.git
146
+ homepage: https://github.com/outcomesinsights/sequel-hexspace
112
147
  licenses:
113
148
  - MIT
114
- metadata: {}
115
- post_install_message:
149
+ metadata:
150
+ rubygems_mfa_required: 'true'
116
151
  rdoc_options:
117
152
  - "--quiet"
118
153
  - "--line-numbers"
@@ -127,15 +162,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
127
162
  requirements:
128
163
  - - ">="
129
164
  - !ruby/object:Gem::Version
130
- version: '0'
165
+ version: '3.3'
131
166
  required_rubygems_version: !ruby/object:Gem::Requirement
132
167
  requirements:
133
168
  - - ">="
134
169
  - !ruby/object:Gem::Version
135
170
  version: '0'
136
171
  requirements: []
137
- rubygems_version: 3.4.6
138
- signing_key:
172
+ rubygems_version: 4.0.20
139
173
  specification_version: 4
140
174
  summary: Sequel adapter for hexspace driver and Apache Spark database
141
175
  test_files: []