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 +4 -4
- data/CHANGELOG.md +70 -0
- data/LICENSE +1 -0
- data/README +89 -0
- data/lib/sequel/adapters/hexspace/thrift_compat.rb +54 -0
- data/lib/sequel/adapters/hexspace.rb +24 -18
- data/lib/sequel/adapters/shared/hexspace.rb +12 -0
- data/lib/sequel/adapters/shared/spark.rb +318 -70
- metadata +56 -22
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
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
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
|
-
|
|
2
|
-
|
|
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
|
|
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 !=
|
|
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
|
-
|
|
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
|
|
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
|
|
77
|
+
when "binary"
|
|
66
78
|
Sequel.method(:blob)
|
|
67
|
-
when
|
|
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] =
|
|
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
|
|
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,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 <<
|
|
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
|
|
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
|
-
|
|
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 <<
|
|
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 <<
|
|
186
|
-
sql <<
|
|
208
|
+
sql << "DROP SCHEMA "
|
|
209
|
+
sql << "IF EXISTS " if opts[:if_exists]
|
|
187
210
|
sql << literal(schema_name)
|
|
188
|
-
sql <<
|
|
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]), {:
|
|
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(
|
|
205
|
-
|
|
233
|
+
def type_literal_generic_file(_column)
|
|
234
|
+
"binary"
|
|
206
235
|
end
|
|
207
236
|
|
|
208
|
-
def type_literal_generic_float(
|
|
209
|
-
|
|
237
|
+
def type_literal_generic_float(_column)
|
|
238
|
+
"float"
|
|
210
239
|
end
|
|
211
240
|
|
|
212
|
-
def type_literal_generic_string(
|
|
213
|
-
|
|
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, [[
|
|
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
|
-
|
|
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,
|
|
248
|
-
# table
|
|
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 =
|
|
270
|
-
db.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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 <<
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
637
|
+
clone(with: with.freeze, with_references: references.freeze)
|
|
396
638
|
end
|
|
397
639
|
|
|
398
640
|
private def select_values_sql(sql)
|
|
399
|
-
sql <<
|
|
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:
|
|
4
|
+
version: 2.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeremy Evans
|
|
8
|
-
|
|
8
|
+
- Ryan Duryea
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
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:
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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:
|
|
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:
|
|
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:
|
|
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/
|
|
146
|
+
homepage: https://github.com/outcomesinsights/sequel-hexspace
|
|
112
147
|
licenses:
|
|
113
148
|
- MIT
|
|
114
|
-
metadata:
|
|
115
|
-
|
|
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: '
|
|
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:
|
|
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: []
|