kudu_adapter 0.1.0
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 +7 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +8 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.md +178 -0
- data/kudu_adapter.gemspec +33 -0
- data/lib/active_record/connection_adapters/kudu/column.rb +17 -0
- data/lib/active_record/connection_adapters/kudu/database_statements.rb +41 -0
- data/lib/active_record/connection_adapters/kudu/quoting.rb +51 -0
- data/lib/active_record/connection_adapters/kudu/schema_creation.rb +89 -0
- data/lib/active_record/connection_adapters/kudu/schema_statements.rb +507 -0
- data/lib/active_record/connection_adapters/kudu/sql_type_metadata.rb +16 -0
- data/lib/active_record/connection_adapters/kudu/table_definition.rb +32 -0
- data/lib/active_record/connection_adapters/kudu/type/big_int.rb +22 -0
- data/lib/active_record/connection_adapters/kudu/type/boolean.rb +23 -0
- data/lib/active_record/connection_adapters/kudu/type/char.rb +17 -0
- data/lib/active_record/connection_adapters/kudu/type/date_time.rb +21 -0
- data/lib/active_record/connection_adapters/kudu/type/double.rb +17 -0
- data/lib/active_record/connection_adapters/kudu/type/float.rb +18 -0
- data/lib/active_record/connection_adapters/kudu/type/integer.rb +22 -0
- data/lib/active_record/connection_adapters/kudu/type/small_int.rb +22 -0
- data/lib/active_record/connection_adapters/kudu/type/string.rb +17 -0
- data/lib/active_record/connection_adapters/kudu/type/time.rb +30 -0
- data/lib/active_record/connection_adapters/kudu/type/tiny_int.rb +22 -0
- data/lib/active_record/connection_adapters/kudu_adapter.rb +173 -0
- data/lib/active_record/tasks/kudu_database_tasks.rb +29 -0
- data/lib/arel/visitors/kudu.rb +7 -0
- data/lib/kudu_adapter/bind_substitution.rb +15 -0
- data/lib/kudu_adapter/table_definition_extensions.rb +28 -0
- data/lib/kudu_adapter/version.rb +5 -0
- data/lib/kudu_adapter.rb +5 -0
- data/spec/spec_config.yaml.template +8 -0
- data/spec/spec_helper.rb +124 -0
- metadata +205 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model/type/integer'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module Kudu
|
8
|
+
module Type
|
9
|
+
# :nodoc:
|
10
|
+
class Integer < ::ActiveModel::Type::Integer
|
11
|
+
def type
|
12
|
+
:integer
|
13
|
+
end
|
14
|
+
|
15
|
+
def limit
|
16
|
+
4
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model/type/integer'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module Kudu
|
8
|
+
module Type
|
9
|
+
# :nodoc:
|
10
|
+
class SmallInt < ::ActiveModel::Type::Integer
|
11
|
+
def type
|
12
|
+
:smallint
|
13
|
+
end
|
14
|
+
|
15
|
+
def limit
|
16
|
+
2
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model/type/string'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module Kudu
|
8
|
+
module Type
|
9
|
+
class String < ::ActiveModel::Type::String
|
10
|
+
def type
|
11
|
+
:string
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model/type/big_integer'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module Kudu
|
8
|
+
module Type
|
9
|
+
# :nodoc:
|
10
|
+
class Time < ::ActiveModel::Type::BigInteger
|
11
|
+
def type
|
12
|
+
:time
|
13
|
+
end
|
14
|
+
|
15
|
+
def serialize(value)
|
16
|
+
value.to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def deserialize(value)
|
20
|
+
::Time.at value.to_i
|
21
|
+
end
|
22
|
+
|
23
|
+
def user_input_in_time_zone(value)
|
24
|
+
value.in_time_zone
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model/type/integer'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
module ConnectionAdapters
|
7
|
+
module Kudu
|
8
|
+
module Type
|
9
|
+
# :nodoc:
|
10
|
+
class TinyInt < ::ActiveModel::Type::Integer
|
11
|
+
def type
|
12
|
+
:tinyint
|
13
|
+
end
|
14
|
+
|
15
|
+
def limit
|
16
|
+
1
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record/connection_adapters/kudu/quoting'
|
4
|
+
require 'active_record/connection_adapters/kudu/database_statements'
|
5
|
+
require 'active_record/connection_adapters/kudu/schema_statements'
|
6
|
+
require 'active_record/connection_adapters/kudu/type/big_int'
|
7
|
+
require 'active_record/connection_adapters/kudu/type/boolean'
|
8
|
+
require 'active_record/connection_adapters/kudu/type/char'
|
9
|
+
require 'active_record/connection_adapters/kudu/type/date_time'
|
10
|
+
require 'active_record/connection_adapters/kudu/type/double'
|
11
|
+
require 'active_record/connection_adapters/kudu/type/float'
|
12
|
+
require 'active_record/connection_adapters/kudu/type/integer'
|
13
|
+
require 'active_record/connection_adapters/kudu/type/small_int'
|
14
|
+
require 'active_record/connection_adapters/kudu/type/string'
|
15
|
+
require 'active_record/connection_adapters/kudu/type/time'
|
16
|
+
require 'active_record/connection_adapters/kudu/type/tiny_int'
|
17
|
+
require 'impala'
|
18
|
+
require 'kudu_adapter/bind_substitution'
|
19
|
+
|
20
|
+
module ActiveRecord
|
21
|
+
# Create new connection with Impala database
|
22
|
+
# @param config [::Hash] Connection configuration options
|
23
|
+
class Base
|
24
|
+
def self.kudu_connection(config)
|
25
|
+
::ActiveRecord::ConnectionAdapters::KuduAdapter.new(nil, logger, config)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# :nodoc:
|
30
|
+
module Timestamp
|
31
|
+
private
|
32
|
+
def _create_record
|
33
|
+
if record_timestamps
|
34
|
+
current_time = current_time_from_proper_timezone
|
35
|
+
all_timestamp_attributes_in_model.each do |column|
|
36
|
+
# force inserting of current time for timestamp columns if is needed
|
37
|
+
write_attribute(column, current_time)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# :nodoc:
|
45
|
+
module ConnectionAdapters
|
46
|
+
# Main Impala connection adapter class
|
47
|
+
class KuduAdapter < ::ActiveRecord::ConnectionAdapters::AbstractAdapter
|
48
|
+
|
49
|
+
include Kudu::DatabaseStatements
|
50
|
+
include Kudu::SchemaStatements
|
51
|
+
include Kudu::Quoting
|
52
|
+
|
53
|
+
ADAPTER_NAME = 'Kudu'
|
54
|
+
|
55
|
+
# @!attribute [r] connection
|
56
|
+
# @return [::Impala::Connection] Connection which we are working on
|
57
|
+
attr_reader :connection
|
58
|
+
|
59
|
+
NATIVE_DATABASE_TYPES = {
|
60
|
+
primary_key: { name: 'INT' },
|
61
|
+
tinyint: { name: 'TINYINT' }, # 1 byte
|
62
|
+
smallint: { name: 'SMALLINT' }, # 2 bytes
|
63
|
+
integer: { name: 'INT' }, # 4 bytes
|
64
|
+
bigint: { name: 'BIGINT' }, # 8 bytes
|
65
|
+
float: { name: 'FLOAT' },
|
66
|
+
double: { name: 'DOUBLE' },
|
67
|
+
boolean: { name: 'BOOLEAN' },
|
68
|
+
char: { name: 'CHAR', limit: 255 },
|
69
|
+
string: { name: 'STRING' }, # 32767 characters
|
70
|
+
time: { name: 'BIGINT' },
|
71
|
+
datetime: { name: 'BIGINT' }
|
72
|
+
}.freeze
|
73
|
+
|
74
|
+
def initialize(connection, logger, connection_params)
|
75
|
+
super(connection, logger)
|
76
|
+
|
77
|
+
@connection_params = connection_params
|
78
|
+
connect
|
79
|
+
@visitor = ::KuduAdapter::BindSubstition.new self
|
80
|
+
end
|
81
|
+
|
82
|
+
def connect
|
83
|
+
@connection = ::Impala.connect(
|
84
|
+
@connection_params[:host],
|
85
|
+
@connection_params[:port]
|
86
|
+
)
|
87
|
+
|
88
|
+
db_names = @connection.query('SHOW DATABASES').map {|db| db[:name]}
|
89
|
+
|
90
|
+
@connection.execute('USE ' + @connection_params[:database]) if
|
91
|
+
@connection_params[:database].present? && db_names.include?(@connection_params[:database])
|
92
|
+
end
|
93
|
+
|
94
|
+
def disconnect!
|
95
|
+
@connection.close
|
96
|
+
@connection = nil
|
97
|
+
end
|
98
|
+
|
99
|
+
def reconnect!
|
100
|
+
disconnect!
|
101
|
+
connect
|
102
|
+
end
|
103
|
+
|
104
|
+
def active?
|
105
|
+
@connection.execute('SELECT now()')
|
106
|
+
true
|
107
|
+
rescue
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
def execute(sql, name = nil)
|
112
|
+
with_auto_reconnect do
|
113
|
+
log(sql, name) { @connection.execute(sql) }
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def query(sql, name = nil)
|
118
|
+
with_auto_reconnect do
|
119
|
+
log(sql, name) do
|
120
|
+
@connection.query sql
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def lookup_cast_type_from_column(column)
|
126
|
+
lookup_cast_type column.type.to_s
|
127
|
+
end
|
128
|
+
|
129
|
+
def supports_migrations?
|
130
|
+
true
|
131
|
+
end
|
132
|
+
|
133
|
+
def supports_multi_insert?
|
134
|
+
false
|
135
|
+
end
|
136
|
+
|
137
|
+
def supports_primary_key?
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
def native_database_types
|
142
|
+
::ActiveRecord::ConnectionAdapters::KuduAdapter::NATIVE_DATABASE_TYPES
|
143
|
+
end
|
144
|
+
|
145
|
+
def initialize_type_map(mapping)
|
146
|
+
mapping.register_type(/bigint/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::BigInt.new)
|
147
|
+
mapping.register_type(/boolean/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Boolean.new)
|
148
|
+
mapping.register_type(/char/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Char.new)
|
149
|
+
mapping.register_type(/datetime/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::DateTime.new)
|
150
|
+
mapping.register_type(/double/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Double.new)
|
151
|
+
mapping.register_type(/float/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Float.new)
|
152
|
+
mapping.register_type(/integer/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Integer.new)
|
153
|
+
mapping.register_type(/smallint/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::SmallInt.new)
|
154
|
+
mapping.register_type(/string/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::String.new)
|
155
|
+
mapping.register_type(/(date){0}time/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::Time.new)
|
156
|
+
mapping.register_type(/tinyint/i, ::ActiveRecord::ConnectionAdapters::Kudu::Type::TinyInt.new)
|
157
|
+
end
|
158
|
+
|
159
|
+
def with_auto_reconnect
|
160
|
+
yield
|
161
|
+
rescue Thrift::TransportException => e
|
162
|
+
raise unless e.message == 'end of file reached'
|
163
|
+
reconnect!
|
164
|
+
yield
|
165
|
+
end
|
166
|
+
|
167
|
+
def create_database(database_name)
|
168
|
+
# TODO: escape name
|
169
|
+
execute "CREATE DATABASE IF NOT EXISTS `#{database_name}`"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record/tasks/database_tasks'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
# :nodoc:
|
7
|
+
module Tasks
|
8
|
+
# :nodoc:
|
9
|
+
class KuduDatabaseTasks
|
10
|
+
delegate :connection, :establish_connection, :clear_active_connections!,
|
11
|
+
to: ::ActiveRecord::Base
|
12
|
+
|
13
|
+
# @!attribute [r] configuration
|
14
|
+
# @return [Hash] Database configuration
|
15
|
+
attr_reader :configuration
|
16
|
+
|
17
|
+
def initialize(configuration)
|
18
|
+
@configuration = configuration
|
19
|
+
end
|
20
|
+
|
21
|
+
def create
|
22
|
+
establish_connection
|
23
|
+
connection.create_database configuration['database']
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
DatabaseTasks.register_task(/kudu/, KuduDatabaseTasks)
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'arel/visitors/bind_visitor'
|
4
|
+
require 'arel/visitors/kudu'
|
5
|
+
|
6
|
+
module KuduAdapter
|
7
|
+
# Bind substition class definition
|
8
|
+
class BindSubstition < ::Arel::Visitors::Kudu
|
9
|
+
include ::Arel::Visitors::BindVisitor
|
10
|
+
|
11
|
+
def preparable
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module KuduAdapter
|
4
|
+
# Definitions of additional table capabilities in Kudu
|
5
|
+
module TableDefinitionExtensions
|
6
|
+
# @!attribute [r] partitions
|
7
|
+
# @return [Array] List of table partition definitions
|
8
|
+
attr_reader :partitions
|
9
|
+
|
10
|
+
# Define single partition
|
11
|
+
# @param name [String] Parition name
|
12
|
+
# @param type [String] Partition type
|
13
|
+
# @param options [Hash] Parition options
|
14
|
+
def partition(name, type, options = {})
|
15
|
+
column(name, type, options)
|
16
|
+
@partitions ||= []
|
17
|
+
@partitions << @columns.pop
|
18
|
+
end
|
19
|
+
|
20
|
+
def row_format
|
21
|
+
'ROW FORMAT DELIMITED FIELDS TERMINATED BY "\t"'
|
22
|
+
end
|
23
|
+
|
24
|
+
def external
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/kudu_adapter.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'simplecov'
|
4
|
+
SimpleCov.start
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'bundler'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
Bundler.setup :default, :development
|
11
|
+
|
12
|
+
$LOAD_PATH.unshift(File.expand('../../lib', __FILE__))
|
13
|
+
|
14
|
+
# Load config, if present
|
15
|
+
config_path = File.expand('../spec_config.yaml', __FILE__)
|
16
|
+
config = if File.exist?(config_path)
|
17
|
+
puts "==> Loading config from #{config_path}"
|
18
|
+
YAML.load_file config_path
|
19
|
+
else
|
20
|
+
puts '==> Loading config from env or use default'
|
21
|
+
{
|
22
|
+
'database' => {}
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'rspec'
|
27
|
+
|
28
|
+
require 'active_record'
|
29
|
+
|
30
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
31
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
32
|
+
|
33
|
+
require 'active_support/log_subscriber'
|
34
|
+
require 'active_record/log_subscriber'
|
35
|
+
|
36
|
+
require 'logger'
|
37
|
+
|
38
|
+
require 'active_record/connection_adapters/kudu_adapter'
|
39
|
+
|
40
|
+
puts "==> Effective ActiveRecord version #{ActiveRecord::VERSION::STRING}"
|
41
|
+
|
42
|
+
# :nodoc:
|
43
|
+
module LoggerSpecHelper
|
44
|
+
def set_logger
|
45
|
+
@logger = MockLogger.new
|
46
|
+
@old_logger = ActiveRecord::Base.logger
|
47
|
+
|
48
|
+
@notifier = ActiveSupport::Notifications::Fanout.new
|
49
|
+
|
50
|
+
ActiveSupport::LogSubscriber.colorize_logging = false
|
51
|
+
|
52
|
+
ActiveRecord::Base.logger = @logger
|
53
|
+
|
54
|
+
@old_notifier = ActiveSupport::Notifications.notifier
|
55
|
+
ActiveSupport::Notifications.notifier = @notifier
|
56
|
+
|
57
|
+
ActiveRecord::LogSubscriber.attach_to :active_record
|
58
|
+
ActiveSupport::Notifications.subscribe 'sql.active_record',
|
59
|
+
ActiveRecord::ExplainSubscriber.new
|
60
|
+
end
|
61
|
+
|
62
|
+
# :nodoc:
|
63
|
+
class MockLogger
|
64
|
+
attr_reader :flush_count
|
65
|
+
|
66
|
+
def initialize
|
67
|
+
@flush_count = 0
|
68
|
+
@logged = Hash.new { |h, k| h[k] = [] }
|
69
|
+
end
|
70
|
+
|
71
|
+
def method_missing(level, message)
|
72
|
+
if respond_to_missing?(level)
|
73
|
+
@logged[level] << message
|
74
|
+
else
|
75
|
+
super
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def respond_to_missing?(method, *)
|
80
|
+
%i[debug info warn error].include?(method) || super
|
81
|
+
end
|
82
|
+
|
83
|
+
def logged(level)
|
84
|
+
@logged[level].compact.map { |l| l.to_s.strip }
|
85
|
+
end
|
86
|
+
|
87
|
+
def output(level)
|
88
|
+
logged(level).join "\n"
|
89
|
+
end
|
90
|
+
|
91
|
+
def flush
|
92
|
+
@flush_count += 1
|
93
|
+
end
|
94
|
+
|
95
|
+
def clear(level)
|
96
|
+
@logged[level] = []
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
DATABASE_NAME = config['database']['name'] ||
|
102
|
+
ENV['DATABASE_NAME'] ||
|
103
|
+
'test'
|
104
|
+
DATABASE_HOST = config['database']['host'] ||
|
105
|
+
ENV['DATABASE_HOST'] ||
|
106
|
+
'127.0.0.1'
|
107
|
+
DATABASE_PORT = config['database']['port'] ||
|
108
|
+
ENV['DATABASE_PORT'] ||
|
109
|
+
28_050
|
110
|
+
DATABASE_USER = config['database']['user'] ||
|
111
|
+
ENV['DATABASE_USER'] ||
|
112
|
+
'kudu'
|
113
|
+
DATABASE_PASSWORD = config['database']['password'] ||
|
114
|
+
ENV['DATABASE_PASSWORD'] ||
|
115
|
+
'impala'
|
116
|
+
|
117
|
+
CONNECTION_PARAMS = {
|
118
|
+
adapter: 'kudu',
|
119
|
+
database: DATABASE_NAME,
|
120
|
+
host: DATABASE_HOST,
|
121
|
+
port: DATABASE_PORT,
|
122
|
+
username: DATABASE_USER,
|
123
|
+
password: DATABASE_PASSWORD
|
124
|
+
}.freeze
|