activerecord-hbase-adapter 0.0.7 → 0.0.71
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/activerecord-hbase-adapter.gemspec +9 -2
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +801 -0
- data/lib/active_record/connection_adapters/hbase/client.rb +210 -0
- data/lib/active_record/connection_adapters/hbase/error.rb +78 -0
- data/lib/active_record/connection_adapters/hbase/result.rb +45 -0
- data/lib/activerecord-hbase-adapter/version.rb +1 -1
- metadata +19 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d20cfbf09998f3be62222929739135c35bcfe1b7
|
4
|
+
data.tar.gz: 5262a6b3e72274fdabb48b0fdb51a9e4acc8d445
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4d54be906d76ad9e6a51653f0425f250015a20fa8eefa7b08cb6b9b3714f893fb8250040c5baae48abec0f37a46f81ce351f16f71d8f266a418ac263a374225f
|
7
|
+
data.tar.gz: d9002bcd7755455128d4f714af265a1105b6c2485cc26143f5bcb91b8c0a076cd539e1fae51cca60371ab01b77a19f8e524c5834c57822282d231524422e83a4
|
@@ -11,8 +11,12 @@ Gem::Specification.new do |gem|
|
|
11
11
|
gem.files = [ 'Gemfile',
|
12
12
|
'activerecord-hbase-adapter.gemspec',
|
13
13
|
'lib/activerecord-hbase-adapter/version.rb',
|
14
|
-
'lib/active_record/connection_adapters/hbase_adapter.rb'
|
15
|
-
|
14
|
+
'lib/active_record/connection_adapters/hbase_adapter.rb',
|
15
|
+
'lib/active_record/connection_adapters/abstract_mysql_adapter.rb',
|
16
|
+
'lib/active_record/connection_adapters/hbase/error.rb',
|
17
|
+
'lib/active_record/connection_adapters/hbase/result.rb',
|
18
|
+
'lib/active_record/connection_adapters/hbase/client.rb']
|
19
|
+
#gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
20
|
gem.name = "activerecord-hbase-adapter"
|
17
21
|
gem.require_paths = ["lib"]
|
18
22
|
gem.version = Activerecord::Hbase::Adapter::VERSION
|
@@ -24,14 +28,17 @@ Gem::Specification.new do |gem|
|
|
24
28
|
gem.add_runtime_dependency(%q<hipster_sql_to_hbase>, [">= 0"])
|
25
29
|
gem.add_runtime_dependency(%q<httparty>, [">= 0"])
|
26
30
|
gem.add_runtime_dependency(%q<msgpack>, [">= 0"])
|
31
|
+
gem.add_runtime_dependency(%q<nokogiri>, [">= 0"])
|
27
32
|
else
|
28
33
|
gem.add_dependency(%q<hipster_sql_to_hbase>, [">= 0"])
|
29
34
|
gem.add_dependency(%q<httparty>, [">= 0"])
|
30
35
|
gem.add_dependency(%q<msgpack>, [">= 0"])
|
36
|
+
gem.add_dependency(%q<nokogiri>, [">= 0"])
|
31
37
|
end
|
32
38
|
else
|
33
39
|
gem.add_dependency(%q<hipster_sql_to_hbase>, [">= 0"])
|
34
40
|
gem.add_dependency(%q<httparty>, [">= 0"])
|
35
41
|
gem.add_dependency(%q<msgpack>, [">= 0"])
|
42
|
+
gem.add_dependency(%q<nokogiri>, [">= 0"])
|
36
43
|
end
|
37
44
|
end
|
@@ -0,0 +1,801 @@
|
|
1
|
+
require 'arel/visitors/bind_visitor'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
class AbstractMysqlAdapter < AbstractAdapter
|
6
|
+
include Savepoints
|
7
|
+
|
8
|
+
class SchemaCreation < AbstractAdapter::SchemaCreation
|
9
|
+
|
10
|
+
def visit_AddColumn(o)
|
11
|
+
add_column_position!(super, column_options(o))
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def visit_ChangeColumnDefinition(o)
|
16
|
+
column = o.column
|
17
|
+
options = o.options
|
18
|
+
sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale])
|
19
|
+
change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}"
|
20
|
+
add_column_options!(change_column_sql, options.merge(column: column))
|
21
|
+
add_column_position!(change_column_sql, options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_column_position!(sql, options)
|
25
|
+
if options[:first]
|
26
|
+
sql << " FIRST"
|
27
|
+
elsif options[:after]
|
28
|
+
sql << " AFTER #{quote_column_name(options[:after])}"
|
29
|
+
end
|
30
|
+
sql
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def schema_creation
|
35
|
+
SchemaCreation.new self
|
36
|
+
end
|
37
|
+
|
38
|
+
class Column < ConnectionAdapters::Column # :nodoc:
|
39
|
+
attr_reader :collation, :strict, :extra
|
40
|
+
|
41
|
+
def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
|
42
|
+
@strict = strict
|
43
|
+
@collation = collation
|
44
|
+
@extra = extra
|
45
|
+
super(name, default, sql_type, null)
|
46
|
+
end
|
47
|
+
|
48
|
+
def extract_default(default)
|
49
|
+
if blob_or_text_column?
|
50
|
+
if default.blank?
|
51
|
+
null || strict ? nil : ''
|
52
|
+
else
|
53
|
+
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
|
54
|
+
end
|
55
|
+
elsif missing_default_forged_as_empty_string?(default)
|
56
|
+
nil
|
57
|
+
else
|
58
|
+
super
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def has_default?
|
63
|
+
return false if blob_or_text_column? #mysql forbids defaults on blob and text columns
|
64
|
+
super
|
65
|
+
end
|
66
|
+
|
67
|
+
def blob_or_text_column?
|
68
|
+
sql_type =~ /blob/i || type == :text
|
69
|
+
end
|
70
|
+
|
71
|
+
# Must return the relevant concrete adapter
|
72
|
+
def adapter
|
73
|
+
raise NotImplementedError
|
74
|
+
end
|
75
|
+
|
76
|
+
def case_sensitive?
|
77
|
+
collation && !collation.match(/_ci$/)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def simplified_type(field_type)
|
83
|
+
return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
|
84
|
+
|
85
|
+
case field_type
|
86
|
+
when /enum/i, /set/i then :string
|
87
|
+
when /year/i then :integer
|
88
|
+
when /bit/i then :binary
|
89
|
+
else
|
90
|
+
super
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def extract_limit(sql_type)
|
95
|
+
case sql_type
|
96
|
+
when /^enum\((.+)\)/i
|
97
|
+
$1.split(',').map{|enum| enum.strip.length - 2}.max
|
98
|
+
when /blob|text/i
|
99
|
+
case sql_type
|
100
|
+
when /tiny/i
|
101
|
+
255
|
102
|
+
when /medium/i
|
103
|
+
16777215
|
104
|
+
when /long/i
|
105
|
+
2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
|
106
|
+
else
|
107
|
+
super # we could return 65535 here, but we leave it undecorated by default
|
108
|
+
end
|
109
|
+
when /^bigint/i; 8
|
110
|
+
when /^int/i; 4
|
111
|
+
when /^mediumint/i; 3
|
112
|
+
when /^smallint/i; 2
|
113
|
+
when /^tinyint/i; 1
|
114
|
+
when /^float/i; 24
|
115
|
+
when /^double/i; 53
|
116
|
+
else
|
117
|
+
super
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# MySQL misreports NOT NULL column default when none is given.
|
122
|
+
# We can't detect this for columns which may have a legitimate ''
|
123
|
+
# default (string) but we can for others (integer, datetime, boolean,
|
124
|
+
# and the rest).
|
125
|
+
#
|
126
|
+
# Test whether the column has default '', is not null, and is not
|
127
|
+
# a type allowing default ''.
|
128
|
+
def missing_default_forged_as_empty_string?(default)
|
129
|
+
type != :string && !null && default == ''
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# :singleton-method:
|
135
|
+
# By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
|
136
|
+
# as boolean. If you wish to disable this emulation (which was the default
|
137
|
+
# behavior in versions 0.13.1 and earlier) you can add the following line
|
138
|
+
# to your application.rb file:
|
139
|
+
#
|
140
|
+
# ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false
|
141
|
+
class_attribute :emulate_booleans
|
142
|
+
self.emulate_booleans = true
|
143
|
+
|
144
|
+
LOST_CONNECTION_ERROR_MESSAGES = [
|
145
|
+
"Server shutdown in progress",
|
146
|
+
"Broken pipe",
|
147
|
+
"Lost connection to MySQL server during query",
|
148
|
+
"MySQL server has gone away" ]
|
149
|
+
|
150
|
+
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
|
151
|
+
|
152
|
+
NATIVE_DATABASE_TYPES = {
|
153
|
+
:primary_key => "int(11) auto_increment PRIMARY KEY",
|
154
|
+
:string => { :name => "varchar", :limit => 255 },
|
155
|
+
:text => { :name => "text" },
|
156
|
+
:integer => { :name => "int", :limit => 4 },
|
157
|
+
:float => { :name => "float" },
|
158
|
+
:decimal => { :name => "decimal" },
|
159
|
+
:datetime => { :name => "datetime" },
|
160
|
+
:timestamp => { :name => "datetime" },
|
161
|
+
:time => { :name => "time" },
|
162
|
+
:date => { :name => "date" },
|
163
|
+
:binary => { :name => "blob" },
|
164
|
+
:boolean => { :name => "tinyint", :limit => 1 }
|
165
|
+
}
|
166
|
+
|
167
|
+
INDEX_TYPES = [:fulltext, :spatial]
|
168
|
+
INDEX_USINGS = [:btree, :hash]
|
169
|
+
|
170
|
+
class BindSubstitution < Arel::Visitors::MySQL # :nodoc:
|
171
|
+
include Arel::Visitors::BindVisitor
|
172
|
+
end
|
173
|
+
|
174
|
+
# FIXME: Make the first parameter more similar for the two adapters
|
175
|
+
def initialize(connection, logger, connection_options, config)
|
176
|
+
super(connection, logger)
|
177
|
+
@connection_options, @config = connection_options, config
|
178
|
+
@quoted_column_names, @quoted_table_names = {}, {}
|
179
|
+
|
180
|
+
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
|
181
|
+
@prepared_statements = true
|
182
|
+
@visitor = Arel::Visitors::MySQL.new self
|
183
|
+
else
|
184
|
+
@visitor = unprepared_visitor
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def adapter_name #:nodoc:
|
189
|
+
self.class::ADAPTER_NAME
|
190
|
+
end
|
191
|
+
|
192
|
+
# Returns true, since this connection adapter supports migrations.
|
193
|
+
def supports_migrations?
|
194
|
+
true
|
195
|
+
end
|
196
|
+
|
197
|
+
def supports_primary_key?
|
198
|
+
true
|
199
|
+
end
|
200
|
+
|
201
|
+
def supports_bulk_alter? #:nodoc:
|
202
|
+
true
|
203
|
+
end
|
204
|
+
|
205
|
+
# Technically MySQL allows to create indexes with the sort order syntax
|
206
|
+
# but at the moment (5.5) it doesn't yet implement them
|
207
|
+
def supports_index_sort_order?
|
208
|
+
true
|
209
|
+
end
|
210
|
+
|
211
|
+
def type_cast(value, column)
|
212
|
+
case value
|
213
|
+
when TrueClass
|
214
|
+
1
|
215
|
+
when FalseClass
|
216
|
+
0
|
217
|
+
else
|
218
|
+
super
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# MySQL 4 technically support transaction isolation, but it is affected by a bug
|
223
|
+
# where the transaction level gets persisted for the whole session:
|
224
|
+
#
|
225
|
+
# http://bugs.mysql.com/bug.php?id=39170
|
226
|
+
def supports_transaction_isolation?
|
227
|
+
version[0] >= 5
|
228
|
+
end
|
229
|
+
|
230
|
+
def native_database_types
|
231
|
+
NATIVE_DATABASE_TYPES
|
232
|
+
end
|
233
|
+
|
234
|
+
def index_algorithms
|
235
|
+
{ default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' }
|
236
|
+
end
|
237
|
+
|
238
|
+
# HELPER METHODS ===========================================
|
239
|
+
|
240
|
+
# The two drivers have slightly different ways of yielding hashes of results, so
|
241
|
+
# this method must be implemented to provide a uniform interface.
|
242
|
+
def each_hash(result) # :nodoc:
|
243
|
+
raise NotImplementedError
|
244
|
+
end
|
245
|
+
|
246
|
+
# Overridden by the adapters to instantiate their specific Column type.
|
247
|
+
def new_column(field, default, type, null, collation, extra = "") # :nodoc:
|
248
|
+
Column.new(field, default, type, null, collation, extra)
|
249
|
+
end
|
250
|
+
|
251
|
+
# Must return the Mysql error number from the exception, if the exception has an
|
252
|
+
# error number.
|
253
|
+
def error_number(exception) # :nodoc:
|
254
|
+
raise NotImplementedError
|
255
|
+
end
|
256
|
+
|
257
|
+
# QUOTING ==================================================
|
258
|
+
|
259
|
+
def quote(value, column = nil)
|
260
|
+
if value.kind_of?(String) && column && column.type == :binary
|
261
|
+
s = value.unpack("H*")[0]
|
262
|
+
"x'#{s}'"
|
263
|
+
elsif value.kind_of?(BigDecimal)
|
264
|
+
value.to_s("F")
|
265
|
+
else
|
266
|
+
super
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def quote_column_name(name) #:nodoc:
|
271
|
+
@quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
|
272
|
+
end
|
273
|
+
|
274
|
+
def quote_table_name(name) #:nodoc:
|
275
|
+
@quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
|
276
|
+
end
|
277
|
+
|
278
|
+
def quoted_true
|
279
|
+
QUOTED_TRUE
|
280
|
+
end
|
281
|
+
|
282
|
+
def quoted_false
|
283
|
+
QUOTED_FALSE
|
284
|
+
end
|
285
|
+
|
286
|
+
# REFERENTIAL INTEGRITY ====================================
|
287
|
+
|
288
|
+
def disable_referential_integrity #:nodoc:
|
289
|
+
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
|
290
|
+
|
291
|
+
begin
|
292
|
+
update("SET FOREIGN_KEY_CHECKS = 0")
|
293
|
+
yield
|
294
|
+
ensure
|
295
|
+
update("SET FOREIGN_KEY_CHECKS = #{old}")
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
# DATABASE STATEMENTS ======================================
|
300
|
+
|
301
|
+
# Executes the SQL statement in the context of this connection.
|
302
|
+
def execute(sql, name = nil)
|
303
|
+
File.open('/tmp/thing.txt', 'a') { |file| file.write("\n\n!!!execute\n#{sql}") }
|
304
|
+
#result = @connection.query('SELECT `mythings`.* FROM `mythings`')
|
305
|
+
result = @connection.query(sql)
|
306
|
+
File.open('/tmp/thing.txt', 'a') { |file| file.write("\n#{result.inspect}") }
|
307
|
+
#binding.pry
|
308
|
+
#puts $the_e.inspect
|
309
|
+
log(sql, name) { result }
|
310
|
+
end
|
311
|
+
|
312
|
+
# MysqlAdapter has to free a result after using it, so we use this method to write
|
313
|
+
# stuff in an abstract way without concerning ourselves about whether it needs to be
|
314
|
+
# explicitly freed or not.
|
315
|
+
def execute_and_free(sql, name = nil) #:nodoc:
|
316
|
+
yield execute(sql, name)
|
317
|
+
end
|
318
|
+
|
319
|
+
def update_sql(sql, name = nil) #:nodoc:
|
320
|
+
super
|
321
|
+
@connection.affected_rows
|
322
|
+
end
|
323
|
+
|
324
|
+
def begin_db_transaction
|
325
|
+
execute "BEGIN"
|
326
|
+
end
|
327
|
+
|
328
|
+
def begin_isolated_db_transaction(isolation)
|
329
|
+
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
|
330
|
+
begin_db_transaction
|
331
|
+
end
|
332
|
+
|
333
|
+
def commit_db_transaction #:nodoc:
|
334
|
+
execute "COMMIT"
|
335
|
+
end
|
336
|
+
|
337
|
+
def rollback_db_transaction #:nodoc:
|
338
|
+
execute "ROLLBACK"
|
339
|
+
end
|
340
|
+
|
341
|
+
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
|
342
|
+
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
|
343
|
+
# these, we must use a subquery.
|
344
|
+
def join_to_update(update, select) #:nodoc:
|
345
|
+
if select.limit || select.offset || select.orders.any?
|
346
|
+
super
|
347
|
+
else
|
348
|
+
update.table select.source
|
349
|
+
update.wheres = select.constraints
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def empty_insert_statement_value
|
354
|
+
"VALUES ()"
|
355
|
+
end
|
356
|
+
|
357
|
+
# SCHEMA STATEMENTS ========================================
|
358
|
+
|
359
|
+
# Drops the database specified on the +name+ attribute
|
360
|
+
# and creates it again using the provided +options+.
|
361
|
+
def recreate_database(name, options = {})
|
362
|
+
drop_database(name)
|
363
|
+
sql = create_database(name, options)
|
364
|
+
reconnect!
|
365
|
+
sql
|
366
|
+
end
|
367
|
+
|
368
|
+
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
|
369
|
+
# Charset defaults to utf8.
|
370
|
+
#
|
371
|
+
# Example:
|
372
|
+
# create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin'
|
373
|
+
# create_database 'matt_development'
|
374
|
+
# create_database 'matt_development', charset: :big5
|
375
|
+
def create_database(name, options = {})
|
376
|
+
if options[:collation]
|
377
|
+
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
|
378
|
+
else
|
379
|
+
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# Drops a MySQL database.
|
384
|
+
#
|
385
|
+
# Example:
|
386
|
+
# drop_database('sebastian_development')
|
387
|
+
def drop_database(name) #:nodoc:
|
388
|
+
execute "DROP DATABASE IF EXISTS `#{name}`"
|
389
|
+
end
|
390
|
+
|
391
|
+
def current_database
|
392
|
+
select_value 'SELECT DATABASE() as db'
|
393
|
+
end
|
394
|
+
|
395
|
+
# Returns the database character set.
|
396
|
+
def charset
|
397
|
+
show_variable 'character_set_database'
|
398
|
+
end
|
399
|
+
|
400
|
+
# Returns the database collation strategy.
|
401
|
+
def collation
|
402
|
+
show_variable 'collation_database'
|
403
|
+
end
|
404
|
+
|
405
|
+
def tables(name = nil, database = nil, like = nil) #:nodoc:
|
406
|
+
sql = "SHOW TABLES "
|
407
|
+
sql << "IN #{quote_table_name(database)} " if database
|
408
|
+
sql << "LIKE #{quote(like)}" if like
|
409
|
+
|
410
|
+
execute_and_free(sql, 'SCHEMA') do |result|
|
411
|
+
#binding.pry
|
412
|
+
result.collect { |field| field.first }
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
def table_exists?(name)
|
417
|
+
return false unless name
|
418
|
+
return true if tables(nil, nil, name).any?
|
419
|
+
|
420
|
+
name = name.to_s
|
421
|
+
schema, table = name.split('.', 2)
|
422
|
+
|
423
|
+
unless table # A table was provided without a schema
|
424
|
+
table = schema
|
425
|
+
schema = nil
|
426
|
+
end
|
427
|
+
|
428
|
+
tables(nil, schema, table).any?
|
429
|
+
end
|
430
|
+
|
431
|
+
# Returns an array of indexes for the given table.
|
432
|
+
def indexes(table_name, name = nil) #:nodoc:
|
433
|
+
indexes = []
|
434
|
+
current_index = nil
|
435
|
+
execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result|
|
436
|
+
each_hash(result) do |row|
|
437
|
+
if current_index != row[:Key_name]
|
438
|
+
next if row[:Key_name] == 'PRIMARY' # skip the primary key
|
439
|
+
current_index = row[:Key_name]
|
440
|
+
|
441
|
+
mysql_index_type = row[:Index_type].downcase.to_sym
|
442
|
+
index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil
|
443
|
+
index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil
|
444
|
+
indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using)
|
445
|
+
end
|
446
|
+
|
447
|
+
indexes.last.columns << row[:Column_name]
|
448
|
+
indexes.last.lengths << row[:Sub_part]
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
indexes
|
453
|
+
end
|
454
|
+
|
455
|
+
# Returns an array of +Column+ objects for the table specified by +table_name+.
|
456
|
+
def columns(table_name)#:nodoc:
|
457
|
+
sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
|
458
|
+
execute_and_free(sql, 'SCHEMA') do |result|
|
459
|
+
each_hash(result).map do |field|
|
460
|
+
#binding.pry
|
461
|
+
field_name = set_field_encoding(field[:Field])
|
462
|
+
new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
def create_table(table_name, options = {}) #:nodoc:
|
468
|
+
super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
|
469
|
+
end
|
470
|
+
|
471
|
+
def bulk_change_table(table_name, operations) #:nodoc:
|
472
|
+
sqls = operations.map do |command, args|
|
473
|
+
table, arguments = args.shift, args
|
474
|
+
method = :"#{command}_sql"
|
475
|
+
|
476
|
+
if respond_to?(method, true)
|
477
|
+
send(method, table, *arguments)
|
478
|
+
else
|
479
|
+
raise "Unknown method called : #{method}(#{arguments.inspect})"
|
480
|
+
end
|
481
|
+
end.flatten.join(", ")
|
482
|
+
|
483
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
|
484
|
+
end
|
485
|
+
|
486
|
+
# Renames a table.
|
487
|
+
#
|
488
|
+
# Example:
|
489
|
+
# rename_table('octopuses', 'octopi')
|
490
|
+
def rename_table(table_name, new_name)
|
491
|
+
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
|
492
|
+
rename_table_indexes(table_name, new_name)
|
493
|
+
end
|
494
|
+
|
495
|
+
def drop_table(table_name, options = {})
|
496
|
+
execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}"
|
497
|
+
end
|
498
|
+
|
499
|
+
def rename_index(table_name, old_name, new_name)
|
500
|
+
if supports_rename_index?
|
501
|
+
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
|
502
|
+
else
|
503
|
+
super
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
def change_column_default(table_name, column_name, default)
|
508
|
+
column = column_for(table_name, column_name)
|
509
|
+
change_column table_name, column_name, column.sql_type, :default => default
|
510
|
+
end
|
511
|
+
|
512
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
513
|
+
column = column_for(table_name, column_name)
|
514
|
+
|
515
|
+
unless null || default.nil?
|
516
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
517
|
+
end
|
518
|
+
|
519
|
+
change_column table_name, column_name, column.sql_type, :null => null
|
520
|
+
end
|
521
|
+
|
522
|
+
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
523
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
|
524
|
+
end
|
525
|
+
|
526
|
+
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
527
|
+
execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
|
528
|
+
rename_column_indexes(table_name, column_name, new_column_name)
|
529
|
+
end
|
530
|
+
|
531
|
+
def add_index(table_name, column_name, options = {}) #:nodoc:
|
532
|
+
index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
|
533
|
+
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}"
|
534
|
+
end
|
535
|
+
|
536
|
+
# Maps logical Rails types to MySQL-specific data types.
|
537
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
538
|
+
case type.to_s
|
539
|
+
when 'binary'
|
540
|
+
case limit
|
541
|
+
when 0..0xfff; "varbinary(#{limit})"
|
542
|
+
when nil; "blob"
|
543
|
+
when 0x1000..0xffffffff; "blob(#{limit})"
|
544
|
+
else raise(ActiveRecordError, "No binary type has character length #{limit}")
|
545
|
+
end
|
546
|
+
when 'integer'
|
547
|
+
case limit
|
548
|
+
when 1; 'tinyint'
|
549
|
+
when 2; 'smallint'
|
550
|
+
when 3; 'mediumint'
|
551
|
+
when nil, 4, 11; 'int(11)' # compatibility with MySQL default
|
552
|
+
when 5..8; 'bigint'
|
553
|
+
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
|
554
|
+
end
|
555
|
+
when 'text'
|
556
|
+
case limit
|
557
|
+
when 0..0xff; 'tinytext'
|
558
|
+
when nil, 0x100..0xffff; 'text'
|
559
|
+
when 0x10000..0xffffff; 'mediumtext'
|
560
|
+
when 0x1000000..0xffffffff; 'longtext'
|
561
|
+
else raise(ActiveRecordError, "No text type has character length #{limit}")
|
562
|
+
end
|
563
|
+
else
|
564
|
+
super
|
565
|
+
end
|
566
|
+
end
|
567
|
+
|
568
|
+
def add_column_position!(sql, options)
|
569
|
+
if options[:first]
|
570
|
+
sql << " FIRST"
|
571
|
+
elsif options[:after]
|
572
|
+
sql << " AFTER #{quote_column_name(options[:after])}"
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
# SHOW VARIABLES LIKE 'name'
|
577
|
+
def show_variable(name)
|
578
|
+
variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA')
|
579
|
+
variables.first['Value'] unless variables.empty?
|
580
|
+
end
|
581
|
+
|
582
|
+
# Returns a table's primary key and belonging sequence.
|
583
|
+
def pk_and_sequence_for(table)
|
584
|
+
execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
|
585
|
+
#binding.pry
|
586
|
+
create_table = each_hash(result).first[:"Create Table"]
|
587
|
+
if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/
|
588
|
+
keys = $1.split(",").map { |key| key.delete('`"') }
|
589
|
+
keys.length == 1 ? [keys.first, nil] : nil
|
590
|
+
else
|
591
|
+
nil
|
592
|
+
end
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
# Returns just a table's primary key
|
597
|
+
def primary_key(table)
|
598
|
+
pk_and_sequence = pk_and_sequence_for(table)
|
599
|
+
pk_and_sequence && pk_and_sequence.first
|
600
|
+
end
|
601
|
+
|
602
|
+
def case_sensitive_modifier(node)
|
603
|
+
Arel::Nodes::Bin.new(node)
|
604
|
+
end
|
605
|
+
|
606
|
+
def case_insensitive_comparison(table, attribute, column, value)
|
607
|
+
if column.case_sensitive?
|
608
|
+
super
|
609
|
+
else
|
610
|
+
table[attribute].eq(value)
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
615
|
+
where_sql
|
616
|
+
end
|
617
|
+
|
618
|
+
def strict_mode?
|
619
|
+
self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
|
620
|
+
end
|
621
|
+
|
622
|
+
def valid_type?(type)
|
623
|
+
!native_database_types[type].nil?
|
624
|
+
end
|
625
|
+
|
626
|
+
protected
|
627
|
+
|
628
|
+
# MySQL is too stupid to create a temporary table for use subquery, so we have
|
629
|
+
# to give it some prompting in the form of a subsubquery. Ugh!
|
630
|
+
def subquery_for(key, select)
|
631
|
+
subsubselect = select.clone
|
632
|
+
subsubselect.projections = [key]
|
633
|
+
|
634
|
+
subselect = Arel::SelectManager.new(select.engine)
|
635
|
+
subselect.project Arel.sql(key.name)
|
636
|
+
subselect.from subsubselect.as('__active_record_temp')
|
637
|
+
end
|
638
|
+
|
639
|
+
def add_index_length(option_strings, column_names, options = {})
|
640
|
+
if options.is_a?(Hash) && length = options[:length]
|
641
|
+
case length
|
642
|
+
when Hash
|
643
|
+
column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?}
|
644
|
+
when Fixnum
|
645
|
+
column_names.each {|name| option_strings[name] += "(#{length})"}
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
return option_strings
|
650
|
+
end
|
651
|
+
|
652
|
+
def quoted_columns_for_index(column_names, options = {})
|
653
|
+
option_strings = Hash[column_names.map {|name| [name, '']}]
|
654
|
+
|
655
|
+
# add index length
|
656
|
+
option_strings = add_index_length(option_strings, column_names, options)
|
657
|
+
|
658
|
+
# add index sort order
|
659
|
+
option_strings = add_index_sort_order(option_strings, column_names, options)
|
660
|
+
|
661
|
+
column_names.map {|name| quote_column_name(name) + option_strings[name]}
|
662
|
+
end
|
663
|
+
|
664
|
+
def translate_exception(exception, message)
|
665
|
+
case error_number(exception)
|
666
|
+
when 1062
|
667
|
+
RecordNotUnique.new(message, exception)
|
668
|
+
when 1452
|
669
|
+
InvalidForeignKey.new(message, exception)
|
670
|
+
else
|
671
|
+
super
|
672
|
+
end
|
673
|
+
end
|
674
|
+
|
675
|
+
def add_column_sql(table_name, column_name, type, options = {})
|
676
|
+
td = create_table_definition table_name, options[:temporary], options[:options]
|
677
|
+
cd = td.new_column_definition(column_name, type, options)
|
678
|
+
schema_creation.visit_AddColumn cd
|
679
|
+
end
|
680
|
+
|
681
|
+
def change_column_sql(table_name, column_name, type, options = {})
|
682
|
+
column = column_for(table_name, column_name)
|
683
|
+
|
684
|
+
unless options_include_default?(options)
|
685
|
+
options[:default] = column.default
|
686
|
+
end
|
687
|
+
|
688
|
+
unless options.has_key?(:null)
|
689
|
+
options[:null] = column.null
|
690
|
+
end
|
691
|
+
|
692
|
+
options[:name] = column.name
|
693
|
+
schema_creation.accept ChangeColumnDefinition.new column, type, options
|
694
|
+
end
|
695
|
+
|
696
|
+
def rename_column_sql(table_name, column_name, new_column_name)
|
697
|
+
options = { name: new_column_name }
|
698
|
+
|
699
|
+
if column = columns(table_name).find { |c| c.name == column_name.to_s }
|
700
|
+
options[:default] = column.default
|
701
|
+
options[:null] = column.null
|
702
|
+
options[:auto_increment] = (column.extra == "auto_increment")
|
703
|
+
else
|
704
|
+
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
705
|
+
end
|
706
|
+
|
707
|
+
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
|
708
|
+
schema_creation.accept ChangeColumnDefinition.new column, current_type, options
|
709
|
+
end
|
710
|
+
|
711
|
+
def remove_column_sql(table_name, column_name, type = nil, options = {})
|
712
|
+
"DROP #{quote_column_name(column_name)}"
|
713
|
+
end
|
714
|
+
|
715
|
+
def remove_columns_sql(table_name, *column_names)
|
716
|
+
column_names.map {|column_name| remove_column_sql(table_name, column_name) }
|
717
|
+
end
|
718
|
+
|
719
|
+
def add_index_sql(table_name, column_name, options = {})
|
720
|
+
index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
|
721
|
+
"ADD #{index_type} INDEX #{index_name} (#{index_columns})"
|
722
|
+
end
|
723
|
+
|
724
|
+
def remove_index_sql(table_name, options = {})
|
725
|
+
index_name = index_name_for_remove(table_name, options)
|
726
|
+
"DROP INDEX #{index_name}"
|
727
|
+
end
|
728
|
+
|
729
|
+
def add_timestamps_sql(table_name)
|
730
|
+
[add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
|
731
|
+
end
|
732
|
+
|
733
|
+
def remove_timestamps_sql(table_name)
|
734
|
+
[remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
|
735
|
+
end
|
736
|
+
|
737
|
+
private
|
738
|
+
|
739
|
+
def version
|
740
|
+
@version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
|
741
|
+
end
|
742
|
+
|
743
|
+
def mariadb?
|
744
|
+
full_version =~ /mariadb/i
|
745
|
+
end
|
746
|
+
|
747
|
+
def supports_views?
|
748
|
+
version[0] >= 5
|
749
|
+
end
|
750
|
+
|
751
|
+
def supports_rename_index?
|
752
|
+
mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6
|
753
|
+
end
|
754
|
+
|
755
|
+
def column_for(table_name, column_name)
|
756
|
+
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
|
757
|
+
raise "No such column: #{table_name}.#{column_name}"
|
758
|
+
end
|
759
|
+
column
|
760
|
+
end
|
761
|
+
|
762
|
+
def configure_connection
|
763
|
+
variables = @config.fetch(:variables, {}).stringify_keys
|
764
|
+
|
765
|
+
# By default, MySQL 'where id is null' selects the last inserted id.
|
766
|
+
# Turn this off. http://dev.rubyonrails.org/ticket/6778
|
767
|
+
variables['sql_auto_is_null'] = 0
|
768
|
+
|
769
|
+
# Increase timeout so the server doesn't disconnect us.
|
770
|
+
wait_timeout = @config[:wait_timeout]
|
771
|
+
wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
|
772
|
+
variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout)
|
773
|
+
|
774
|
+
# Make MySQL reject illegal values rather than truncating or blanking them, see
|
775
|
+
# http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables
|
776
|
+
# If the user has provided another value for sql_mode, don't replace it.
|
777
|
+
unless variables.has_key?('sql_mode')
|
778
|
+
variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
|
779
|
+
end
|
780
|
+
|
781
|
+
# NAMES does not have an equals sign, see
|
782
|
+
# http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430
|
783
|
+
# (trailing comma because variable_assignments will always have content)
|
784
|
+
encoding = "NAMES #{@config[:encoding]}, " if @config[:encoding]
|
785
|
+
|
786
|
+
# Gather up all of the SET variables...
|
787
|
+
variable_assignments = variables.map do |k, v|
|
788
|
+
if v == ':default' || v == :default
|
789
|
+
"@@SESSION.#{k.to_s} = DEFAULT" # Sets the value to the global or compile default
|
790
|
+
elsif !v.nil?
|
791
|
+
"@@SESSION.#{k.to_s} = #{quote(v)}"
|
792
|
+
end
|
793
|
+
# or else nil; compact to clear nils out
|
794
|
+
end.compact.join(', ')
|
795
|
+
|
796
|
+
# ...and send them all in one query
|
797
|
+
@connection.query "SET #{encoding} #{variable_assignments}"
|
798
|
+
end
|
799
|
+
end
|
800
|
+
end
|
801
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'json'
|
3
|
+
require 'msgpack'
|
4
|
+
|
5
|
+
module HbaseRestIface
|
6
|
+
class Client
|
7
|
+
include HTTParty
|
8
|
+
|
9
|
+
def initialize(ops)
|
10
|
+
ops ||= {}
|
11
|
+
ops[:host] ||= 'localhost'
|
12
|
+
ops[:port] ||= '9191'
|
13
|
+
|
14
|
+
@ops = ops
|
15
|
+
@query_options = {}
|
16
|
+
@info = { :version => '1' }
|
17
|
+
|
18
|
+
connect
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](key)
|
22
|
+
@ops[key]
|
23
|
+
end
|
24
|
+
def []=(key,val)
|
25
|
+
@ops[key]=val
|
26
|
+
end
|
27
|
+
|
28
|
+
def query(sql)
|
29
|
+
#File.open('/tmp/thing.txt', 'a') { |file| file.write("\n\n!!!query\n#{sql}") }
|
30
|
+
query_object = HipsterSqlToHbase.parse_hash(sql)
|
31
|
+
#File.open('/tmp/thing.txt', 'a') { |file| file.write("\n#{query_object.inspect}") }
|
32
|
+
begin
|
33
|
+
#TODO: make these be msgpack instead of json
|
34
|
+
result = secure_request("/exec", { body: {query: JSON.generate(query_object)} })
|
35
|
+
Hbase::Result.new(result)
|
36
|
+
rescue Exception => e
|
37
|
+
#File.open('/tmp/thing.txt', 'a') { |file| file.write("\n\n!!!error\n#{e.message}") }
|
38
|
+
Hbase::Result.new()
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def connect
|
43
|
+
self.class.base_uri "#{@ops[:host]}:#{@ops[:port]}"
|
44
|
+
#File.open('/tmp/thing.txt', 'a') { |file| file.write("\n\n!!!connect\n#{@ops[:host]}:#{@ops[:port]}") }
|
45
|
+
ping
|
46
|
+
end
|
47
|
+
|
48
|
+
def ping
|
49
|
+
simple_request("/ping")[0]
|
50
|
+
end
|
51
|
+
|
52
|
+
def simple_request(route, query=nil)
|
53
|
+
if query.nil?
|
54
|
+
response = self.class.get(route)
|
55
|
+
else
|
56
|
+
response = self.class.get(route, query)
|
57
|
+
end
|
58
|
+
|
59
|
+
if response.code.to_s =~ /2\d\d/
|
60
|
+
#TODO: make these be msgpack instead of json
|
61
|
+
JSON.parse(response.body)
|
62
|
+
else
|
63
|
+
error = Nokogiri::HTML(response.body)
|
64
|
+
#binding.pry
|
65
|
+
raise error.css("title")[0].text
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def secure_request(route, body=nil)
|
70
|
+
if body.nil?
|
71
|
+
response = self.class.post(route)
|
72
|
+
else
|
73
|
+
response = self.class.post(route, body)
|
74
|
+
end
|
75
|
+
|
76
|
+
if response.code.to_s =~ /2\d\d/
|
77
|
+
#TODO: make these be msgpack instead of json
|
78
|
+
JSON.parse(response.body)
|
79
|
+
else
|
80
|
+
error = Nokogiri::HTML(response.body)
|
81
|
+
#binding.pry
|
82
|
+
raise error.css("title")[0].text
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def info
|
87
|
+
@info
|
88
|
+
end
|
89
|
+
|
90
|
+
def query_options
|
91
|
+
@query_options
|
92
|
+
end
|
93
|
+
|
94
|
+
def close
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
|
98
|
+
def abandon_results!
|
99
|
+
nil
|
100
|
+
end
|
101
|
+
|
102
|
+
def escape(string)
|
103
|
+
string
|
104
|
+
end
|
105
|
+
|
106
|
+
def info
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
def server_info
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
|
114
|
+
def socket
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
|
118
|
+
def async_result
|
119
|
+
nil
|
120
|
+
end
|
121
|
+
|
122
|
+
def last_id
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
|
126
|
+
def affected_rows
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
|
130
|
+
def thread_id
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
|
134
|
+
def select_db
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def more_results?
|
139
|
+
nil
|
140
|
+
end
|
141
|
+
|
142
|
+
def next_result
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
|
146
|
+
def store_result
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
|
150
|
+
def reconnect=
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
|
154
|
+
def warning_count
|
155
|
+
nil
|
156
|
+
end
|
157
|
+
|
158
|
+
def query_info_string
|
159
|
+
nil
|
160
|
+
end
|
161
|
+
|
162
|
+
def encoding
|
163
|
+
nil
|
164
|
+
end
|
165
|
+
|
166
|
+
def connect_timeout=(o)
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
|
170
|
+
def read_timeout=(o)
|
171
|
+
nil
|
172
|
+
end
|
173
|
+
|
174
|
+
def write_timeout=(o)
|
175
|
+
nil
|
176
|
+
end
|
177
|
+
|
178
|
+
def local_infile=(o)
|
179
|
+
nil
|
180
|
+
end
|
181
|
+
|
182
|
+
def charset_name=(o)
|
183
|
+
nil
|
184
|
+
end
|
185
|
+
|
186
|
+
def secure_auth=(o)
|
187
|
+
nil
|
188
|
+
end
|
189
|
+
|
190
|
+
def default_file=(o)
|
191
|
+
nil
|
192
|
+
end
|
193
|
+
|
194
|
+
def default_group=(o)
|
195
|
+
nil
|
196
|
+
end
|
197
|
+
|
198
|
+
def init_command=(o)
|
199
|
+
nil
|
200
|
+
end
|
201
|
+
|
202
|
+
def ssl_set
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
|
206
|
+
def initialize_ext
|
207
|
+
nil
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Hbase
|
2
|
+
class Error < StandardError
|
3
|
+
REPLACEMENT_CHAR = '?'
|
4
|
+
ENCODE_OPTS = {:undef => :replace, :invalid => :replace, :replace => REPLACEMENT_CHAR}
|
5
|
+
|
6
|
+
attr_accessor :error_number
|
7
|
+
attr_reader :sql_state
|
8
|
+
attr_writer :server_version
|
9
|
+
|
10
|
+
# Mysql gem compatibility
|
11
|
+
alias_method :errno, :error_number
|
12
|
+
alias_method :error, :message
|
13
|
+
|
14
|
+
def initialize(msg, server_version=nil)
|
15
|
+
self.server_version = server_version
|
16
|
+
|
17
|
+
super(clean_message(msg))
|
18
|
+
end
|
19
|
+
|
20
|
+
def sql_state=(state)
|
21
|
+
@sql_state = ''.respond_to?(:encode) ? state.encode(ENCODE_OPTS) : state
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# In MySQL 5.5+ error messages are always constructed server-side as UTF-8
|
27
|
+
# then returned in the encoding set by the `character_set_results` system
|
28
|
+
# variable.
|
29
|
+
#
|
30
|
+
# See http://dev.mysql.com/doc/refman/5.5/en/charset-errors.html for
|
31
|
+
# more contetx.
|
32
|
+
#
|
33
|
+
# Before MySQL 5.5 error message template strings are in whatever encoding
|
34
|
+
# is associated with the error message language.
|
35
|
+
# See http://dev.mysql.com/doc/refman/5.1/en/error-message-language.html
|
36
|
+
# for more information.
|
37
|
+
#
|
38
|
+
# The issue is that the user-data inserted in the message could potentially
|
39
|
+
# be in any encoding MySQL supports and is insert into the latin1, euckr or
|
40
|
+
# koi8r string raw. Meaning there's a high probability the string will be
|
41
|
+
# corrupt encoding-wise.
|
42
|
+
#
|
43
|
+
# See http://dev.mysql.com/doc/refman/5.1/en/charset-errors.html for
|
44
|
+
# more information.
|
45
|
+
#
|
46
|
+
# So in an attempt to make sure the error message string is always in a valid
|
47
|
+
# encoding, we'll assume UTF-8 and clean the string of anything that's not a
|
48
|
+
# valid UTF-8 character.
|
49
|
+
#
|
50
|
+
# Except for if we're on 1.8, where we'll do nothing ;)
|
51
|
+
#
|
52
|
+
# Returns a valid UTF-8 string in Ruby 1.9+, the original string on Ruby 1.8
|
53
|
+
def clean_message(message)
|
54
|
+
return message if !message.respond_to?(:encoding)
|
55
|
+
|
56
|
+
if @server_version && @server_version > 50500
|
57
|
+
message.encode(ENCODE_OPTS)
|
58
|
+
else
|
59
|
+
if message.respond_to? :scrub
|
60
|
+
message.scrub(REPLACEMENT_CHAR).encode(ENCODE_OPTS)
|
61
|
+
else
|
62
|
+
# This is ugly as hell but Ruby 1.9 doesn't provide a way to clean a string
|
63
|
+
# and retain it's valid UTF-8 characters, that I know of.
|
64
|
+
|
65
|
+
new_message = "".force_encoding(Encoding::UTF_8)
|
66
|
+
message.chars.each do |char|
|
67
|
+
if char.valid_encoding?
|
68
|
+
new_message << char
|
69
|
+
else
|
70
|
+
new_message << REPLACEMENT_CHAR
|
71
|
+
end
|
72
|
+
end
|
73
|
+
new_message.encode(ENCODE_OPTS)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract_mysql_adapter'
|
2
|
+
|
3
|
+
module Hbase
|
4
|
+
class Result
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_accessor :members
|
8
|
+
|
9
|
+
def initialize(result = {'fields' => [], 'results' => []})
|
10
|
+
@result = result
|
11
|
+
@members = @result['results']
|
12
|
+
end
|
13
|
+
|
14
|
+
def hasherize_rows(symbolize_keys = false)
|
15
|
+
if symbolize_keys
|
16
|
+
keys = @result['fields'].map! { |v| v.to_sym }
|
17
|
+
else
|
18
|
+
keys = @result['fields']
|
19
|
+
end
|
20
|
+
#binding.pry
|
21
|
+
hashed_rows = @result['results'].map { |row| Hash[keys.zip row] }
|
22
|
+
end
|
23
|
+
|
24
|
+
def each(arg_hash = {},&block)
|
25
|
+
unless arg_hash[:as].nil?
|
26
|
+
symbolize_keys = (arg_hash[:symbolize_keys].nil?) ? false : arg_hash[:symbolize_keys]
|
27
|
+
if arg_hash[:as] == :hash
|
28
|
+
hasherize_rows(symbolize_keys).each &block
|
29
|
+
end
|
30
|
+
else
|
31
|
+
@result['results'].each &block
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def fields
|
36
|
+
@result['fields']
|
37
|
+
end
|
38
|
+
|
39
|
+
def count
|
40
|
+
@result['results'].length
|
41
|
+
end
|
42
|
+
|
43
|
+
alias :size :count
|
44
|
+
end
|
45
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: activerecord-hbase-adapter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.71
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Lescure
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - ">="
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: nokogiri
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
55
69
|
description: HBase ActiveRecord adapter based on HipsterSqlToHbase
|
56
70
|
email:
|
57
71
|
- jean@agilityfeat.com
|
@@ -61,6 +75,10 @@ extra_rdoc_files: []
|
|
61
75
|
files:
|
62
76
|
- Gemfile
|
63
77
|
- activerecord-hbase-adapter.gemspec
|
78
|
+
- lib/active_record/connection_adapters/abstract_mysql_adapter.rb
|
79
|
+
- lib/active_record/connection_adapters/hbase/client.rb
|
80
|
+
- lib/active_record/connection_adapters/hbase/error.rb
|
81
|
+
- lib/active_record/connection_adapters/hbase/result.rb
|
64
82
|
- lib/active_record/connection_adapters/hbase_adapter.rb
|
65
83
|
- lib/activerecord-hbase-adapter/version.rb
|
66
84
|
homepage: http://github.com/jeanlescure/activerecord-hbase-adapter
|