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 +4 -4
- data/CHANGELOG.md +39 -6
- data/README +89 -0
- data/lib/sequel/adapters/hexspace/thrift_compat.rb +54 -0
- data/lib/sequel/adapters/hexspace.rb +11 -83
- data/lib/sequel/adapters/shared/hexspace.rb +3 -16
- data/lib/sequel/adapters/shared/spark.rb +266 -76
- metadata +24 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ce1fd4d7fed71e9678ed12c286b3a1b1e0bff80d52ef2d78c222a52e7a09713
|
|
4
|
+
data.tar.gz: 9ae18e5f352c95e8390d6d26ea09d217f63dad5c105a2bd30b2290c87483d566
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
3
|
+
## [2.0.1] - 2026-10-07
|
|
4
4
|
|
|
5
|
-
###
|
|
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)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
2
|
-
require
|
|
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 !=
|
|
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
|
|
77
|
+
when "binary"
|
|
144
78
|
Sequel.method(:blob)
|
|
145
|
-
when
|
|
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] =
|
|
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
|
|
3
|
+
require_relative "spark"
|
|
4
4
|
|
|
5
5
|
module Sequel
|
|
6
|
-
|
|
7
6
|
module Hexspace
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
{:
|
|
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(
|
|
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(:
|
|
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 <<
|
|
95
|
-
sql <<
|
|
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 <<
|
|
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 <<
|
|
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 <<
|
|
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 <<
|
|
122
|
-
sql <<
|
|
123
|
-
sql <<
|
|
124
|
-
sql <<
|
|
125
|
-
sql <<
|
|
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 <<
|
|
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 <<
|
|
209
|
-
sql <<
|
|
208
|
+
sql << "DROP SCHEMA "
|
|
209
|
+
sql << "IF EXISTS " if opts[:if_exists]
|
|
210
210
|
sql << literal(schema_name)
|
|
211
|
-
sql <<
|
|
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]), {:
|
|
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(
|
|
234
|
-
|
|
233
|
+
def type_literal_generic_file(_column)
|
|
234
|
+
"binary"
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
-
def type_literal_generic_float(
|
|
238
|
-
|
|
237
|
+
def type_literal_generic_float(_column)
|
|
238
|
+
"float"
|
|
239
239
|
end
|
|
240
240
|
|
|
241
|
-
def type_literal_generic_string(
|
|
242
|
-
|
|
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, [[
|
|
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
|
-
|
|
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, :
|
|
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,
|
|
288
|
-
# table
|
|
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 =
|
|
310
|
-
db.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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 <<
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
637
|
+
clone(with: with.freeze, with_references: references.freeze)
|
|
454
638
|
end
|
|
455
639
|
|
|
456
640
|
private def select_values_sql(sql)
|
|
457
|
-
sql <<
|
|
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.
|
|
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.
|
|
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.
|
|
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:
|
|
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:
|
|
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"
|