activerecord-hbase-adapter 0.0.7 → 0.0.71

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c9b0cf4fd299122fc45486397c36ccc43e2923c6
4
- data.tar.gz: 109f0b4a74e3a763eee842152117aeee01a3ec21
3
+ metadata.gz: d20cfbf09998f3be62222929739135c35bcfe1b7
4
+ data.tar.gz: 5262a6b3e72274fdabb48b0fdb51a9e4acc8d445
5
5
  SHA512:
6
- metadata.gz: efdc375a03df4c79831f209565ad5ce1a377c758054d3c753802a1ce168799cd71178e40b6ad5b0e63cb05c779f04cfe1a8b1feb588ab6fae59f33c3414dbfe2
7
- data.tar.gz: 758e44e26fb0884febac15e40e503b2f809d6aa052aecc93601efaa06499c29ce486c321760da4064f96d7bdbd1ad87a87f306e0e3031afd3546634878a9166c
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
- gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
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
@@ -1,7 +1,7 @@
1
1
  module Activerecord
2
2
  module Hbase
3
3
  module Adapter
4
- VERSION = "0.0.7"
4
+ VERSION = "0.0.71"
5
5
  end
6
6
  end
7
7
  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.7
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