bigbroda 0.0.7 → 0.1.0.pre

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Appraisals +15 -0
  3. data/Gemfile +1 -0
  4. data/README.md +39 -21
  5. data/Rakefile +5 -2
  6. data/{google_bigquery.gemspec → bigbroda.gemspec} +2 -2
  7. data/gemfiles/rails_3.gemfile +20 -0
  8. data/gemfiles/rails_4.0.3.gemfile +20 -0
  9. data/gemfiles/rails_4.0.3.gemfile.lock +176 -0
  10. data/gemfiles/rails_4.1.gemfile +20 -0
  11. data/gemfiles/rails_4.1.gemfile.lock +182 -0
  12. data/gemfiles/rails_4.2.gemfile +20 -0
  13. data/gemfiles/rails_4.2.gemfile.lock +202 -0
  14. data/gemfiles/rails_4.gemfile +20 -0
  15. data/gemfiles/rails_4.gemfile.lock +176 -0
  16. data/lib/active_record/connection_adapters/bigquery_adapter.rb +32 -601
  17. data/lib/active_record/connection_adapters/rails_41.rb +607 -0
  18. data/lib/active_record/connection_adapters/rails_42.rb +628 -0
  19. data/lib/{google_bigquery → bigbroda}/auth.rb +3 -3
  20. data/lib/{google_bigquery → bigbroda}/client.rb +3 -3
  21. data/lib/{google_bigquery → bigbroda}/config.rb +1 -1
  22. data/lib/{google_bigquery → bigbroda}/dataset.rb +23 -23
  23. data/lib/{google_bigquery → bigbroda}/engine.rb +4 -4
  24. data/lib/{google_bigquery → bigbroda}/jobs.rb +28 -28
  25. data/lib/bigbroda/project.rb +16 -0
  26. data/lib/{google_bigquery → bigbroda}/railtie.rb +3 -3
  27. data/lib/{google_bigquery → bigbroda}/table.rb +19 -19
  28. data/lib/{google_bigquery → bigbroda}/table_data.rb +7 -7
  29. data/lib/bigbroda/version.rb +3 -0
  30. data/lib/bigbroda.rb +27 -0
  31. data/lib/generators/{google_bigquery → bigbroda}/install/install_generator.rb +2 -2
  32. data/lib/generators/templates/{bigquery.rb.erb → bigbroda.rb.erb} +1 -1
  33. data/spec/dummy/config/application.rb +1 -1
  34. data/spec/functional/adapter/adapter_spec.rb +40 -38
  35. data/spec/functional/auth_spec.rb +3 -3
  36. data/spec/functional/config_spec.rb +5 -5
  37. data/spec/functional/dataset_spec.rb +19 -19
  38. data/spec/functional/project_spec.rb +4 -4
  39. data/spec/functional/table_data_spec.rb +13 -13
  40. data/spec/functional/table_spec.rb +30 -30
  41. data/spec/spec_helper.rb +2 -2
  42. metadata +32 -20
  43. data/lib/google_bigquery/project.rb +0 -16
  44. data/lib/google_bigquery/version.rb +0 -3
  45. data/lib/google_bigquery.rb +0 -27
@@ -0,0 +1,607 @@
1
+ require 'active_record/connection_adapters/abstract_adapter'
2
+ require 'active_record/connection_adapters/statement_pool'
3
+ require 'active_record/connection_adapters/abstract/schema_statements'
4
+ require 'arel/visitors/bind_visitor'
5
+
6
+ module ActiveRecord
7
+
8
+ module ConnectionAdapters
9
+
10
+
11
+ class BigqueryColumn < Column
12
+ class << self
13
+ TRUE_VALUES = [true, 1, '1', 'true', 'TRUE'].to_set
14
+ FALSE_VALUES = [false, 0, '0','false', 'FALSE'].to_set
15
+
16
+ def binary_to_string(value)
17
+ if value.encoding != Encoding::ASCII_8BIT
18
+ value = value.force_encoding(Encoding::ASCII_8BIT)
19
+ end
20
+ value
21
+ end
22
+
23
+ def string_to_time(string)
24
+ return string unless string.is_a?(String)
25
+ return nil if string.empty?
26
+ fast_string_to_time(string) || fallback_string_to_time(string) || Time.at(string.to_f).send(Base.default_timezone)
27
+ end
28
+ end
29
+ end
30
+
31
+ class BigqueryAdapter < AbstractAdapter
32
+
33
+ include SchemaStatements
34
+
35
+ class Version
36
+ end
37
+
38
+ class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
39
+ attr_accessor :array
40
+ end
41
+
42
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
43
+
44
+ def primary_key(name, type = :primary_key, options = {})
45
+ return column name, :string, options
46
+ end
47
+
48
+ def record(*args)
49
+ options = args.extract_options!
50
+ column(:created_at, :record, options)
51
+ end
52
+
53
+ def timestamps(*args)
54
+ options = args.extract_options!
55
+ column(:created_at, :timestamp, options)
56
+ column(:updated_at, :timestamp, options)
57
+ end
58
+
59
+ def references(*args)
60
+ options = args.extract_options!
61
+ polymorphic = options.delete(:polymorphic)
62
+ index_options = options.delete(:index)
63
+ args.each do |col|
64
+ column("#{col}_id", :string, options)
65
+ column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
66
+ index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
67
+ end
68
+ end
69
+
70
+ end
71
+
72
+ class StatementPool < ConnectionAdapters::StatementPool
73
+ def initialize(connection, max)
74
+ super
75
+ @cache = Hash.new { |h,pid| h[pid] = {} }
76
+ end
77
+
78
+ def each(&block); cache.each(&block); end
79
+ def key?(key); cache.key?(key); end
80
+ def [](key); cache[key]; end
81
+ def length; cache.length; end
82
+
83
+ def []=(sql, key)
84
+ while @max <= cache.size
85
+ dealloc(cache.shift.last[:stmt])
86
+ end
87
+ cache[sql] = key
88
+ end
89
+
90
+ def clear
91
+ cache.values.each do |hash|
92
+ dealloc hash[:stmt]
93
+ end
94
+ cache.clear
95
+ end
96
+
97
+ private
98
+ def cache
99
+ @cache[$$]
100
+ end
101
+
102
+ def dealloc(stmt)
103
+ stmt.close unless stmt.closed?
104
+ end
105
+ end
106
+
107
+ class BindSubstitution < Arel::Visitors::SQLite # :nodoc:
108
+ include Arel::Visitors::BindVisitor
109
+ end
110
+
111
+ def initialize(connection, logger, config)
112
+ super(connection, logger)
113
+
114
+ @active = nil
115
+ @statements = StatementPool.new(@connection,
116
+ self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
117
+ @config = config
118
+
119
+ #if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
120
+ # @prepared_statements = true
121
+ # @visitor = Arel::Visitors::SQLite.new self
122
+ #else
123
+ #use the sql without prepraded statements, as I know BQ doesn't support them.
124
+
125
+ @visitor = unprepared_visitor unless ActiveRecord::VERSION::MINOR >= 2
126
+ end
127
+
128
+ def adapter_name #:nodoc:
129
+ 'BigQuery'
130
+ end
131
+
132
+ def supports_ddl_transactions?
133
+ false
134
+ end
135
+
136
+ def supports_savepoints?
137
+ false
138
+ end
139
+
140
+ def supports_partial_index?
141
+ true
142
+ end
143
+
144
+ # Returns true, since this connection adapter supports prepared statement
145
+ # caching.
146
+ def supports_statement_cache?
147
+ false
148
+ end
149
+
150
+ # Returns true, since this connection adapter supports migrations.
151
+ def supports_migrations? #:nodoc:
152
+ true
153
+ end
154
+
155
+ def supports_primary_key? #:nodoc:
156
+ true
157
+ end
158
+
159
+ def requires_reloading?
160
+ false
161
+ end
162
+
163
+ def supports_add_column?
164
+ true
165
+ end
166
+
167
+ def active?
168
+ @active != false
169
+ end
170
+
171
+ # Disconnects from the database if already connected. Otherwise, this
172
+ # method does nothing.
173
+ def disconnect!
174
+ super
175
+ @active = false
176
+ @connection.close rescue nil
177
+ end
178
+
179
+ # Clears the prepared statements cache.
180
+ def clear_cache!
181
+ @statements.clear
182
+ end
183
+
184
+ def supports_index_sort_order?
185
+ true
186
+ end
187
+
188
+ # Returns true
189
+ def supports_count_distinct? #:nodoc:
190
+ true
191
+ end
192
+
193
+ # Returns false
194
+ def supports_autoincrement? #:nodoc:
195
+ false
196
+ end
197
+
198
+ def supports_index_sort_order?
199
+ false
200
+ end
201
+
202
+ # Returns 62. SQLite supports index names up to 64
203
+ # characters. The rest is used by rails internally to perform
204
+ # temporary rename operations
205
+ def allowed_index_name_length
206
+ index_name_length - 2
207
+ end
208
+
209
+ def default_primary_key_type
210
+ if supports_autoincrement?
211
+ 'STRING'
212
+ else
213
+ 'STRING'
214
+ end
215
+ end
216
+
217
+ def native_database_types #:nodoc:
218
+ {
219
+ :primary_key => default_primary_key_type,
220
+ :string => { :name => "STRING", :default=> nil },
221
+ #:text => { :name => "text" },
222
+ :integer => { :name => "INTEGER", :default=> nil },
223
+ :float => { :name => "FLOAT", :default=> 0.0 },
224
+ #:decimal => { :name => "decimal" },
225
+ :datetime => { :name => "TIMESTAMP" },
226
+ #:timestamp => { :name => "datetime" },
227
+ :timestamp => { name: "TIMESTAMP" },
228
+ #:time => { :name => "time" },
229
+ :date => { :name => "TIMESTAMP" },
230
+ :record => { :name => "RECORD" },
231
+ :boolean => { :name => "BOOLEAN" }
232
+ }
233
+ end
234
+
235
+ # Returns the current database encoding format as a string, eg: 'UTF-8'
236
+ def encoding
237
+ @connection.encoding.to_s
238
+ end
239
+
240
+ # Returns false.
241
+ def supports_explain?
242
+ false
243
+ end
244
+
245
+ def create_database(database)
246
+ result = BigBroda::Dataset.create(@config[:project],
247
+ {"datasetReference"=> { "datasetId" => database }} )
248
+ result
249
+ end
250
+
251
+ def drop_database(database)
252
+ tables = BigBroda::Table.list(@config[:project], database)["tables"]
253
+ unless tables.blank?
254
+ tables.map!{|o| o["tableReference"]["tableId"]}
255
+ tables.each do |table_id|
256
+ BigBroda::Table.delete(@config[:project], database, table_id)
257
+ end
258
+ end
259
+ result = BigBroda::Dataset.delete(@config[:project], database )
260
+ result
261
+ end
262
+
263
+ # QUOTING ==================================================
264
+
265
+ def quote(value, column = nil)
266
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
267
+ s = column.class.string_to_binary(value).unpack("H*")[0]
268
+ "x'#{s}'"
269
+ else
270
+ super
271
+ end
272
+ end
273
+
274
+ def quote_table_name(name)
275
+ "#{@config[:database]}.#{name}"
276
+ end
277
+
278
+ def quote_table_name_for_assignment(table, attr)
279
+ quote_column_name(attr)
280
+ end
281
+
282
+ def quote_column_name(name) #:nodoc:
283
+ name
284
+ end
285
+
286
+ # Quote date/time values for use in SQL input. Includes microseconds
287
+ # if the value is a Time responding to usec.
288
+ def quoted_date(value) #:nodoc:
289
+ if value.respond_to?(:usec)
290
+ "#{super}.#{sprintf("%06d", value.usec)}"
291
+ else
292
+ super
293
+ end
294
+ end
295
+
296
+ def quoted_true
297
+ "1"
298
+ end
299
+
300
+ def quoted_false
301
+ "0"
302
+ end
303
+
304
+ def type_cast(value, column) # :nodoc:
305
+ return value.to_f if BigDecimal === value
306
+ return super unless String === value
307
+ return super unless column && value
308
+
309
+ value = super
310
+ if column.type == :string && value.encoding == Encoding::ASCII_8BIT
311
+ logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
312
+ value = value.encode Encoding::UTF_8
313
+ end
314
+ value
315
+ end
316
+
317
+ # DATABASE STATEMENTS ======================================
318
+
319
+ def explain(arel, binds = [])
320
+ bypass_feature
321
+ end
322
+
323
+ class ExplainPrettyPrinter
324
+ # Pretty prints the result of a EXPLAIN QUERY PLAN in a way that resembles
325
+ # the output of the SQLite shell:
326
+ #
327
+ # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows)
328
+ # 0|1|1|SCAN TABLE posts (~100000 rows)
329
+ #
330
+ def pp(result) # :nodoc:
331
+ result.rows.map do |row|
332
+ row.join('|')
333
+ end.join("\n") + "\n"
334
+ end
335
+ end
336
+
337
+ def exec_query(sql, name = nil, binds = [])
338
+ log(sql, name, binds) do
339
+
340
+ # Don't cache statements if they are not prepared
341
+ #we set prepared_statements to false in config initialization
342
+ #if without_prepared_statement?(binds)
343
+ result = BigBroda::Jobs.query(@config[:project], {"query"=> sql })
344
+ cols = result["schema"]["fields"].map{|o| o["name"] }
345
+ records = result["totalRows"].to_i.zero? ? [] : result["rows"].map{|o| o["f"].map{|k,v| k["v"]} }
346
+ stmt = records
347
+ #else
348
+ #binding.pry
349
+ #BQ does not support prepared statements, yiak!
350
+ #end
351
+
352
+ ActiveRecord::Result.new(cols, stmt)
353
+ end
354
+ end
355
+
356
+ def exec_delete(sql, name = 'SQL', binds = [])
357
+ exec_query(sql, name, binds)
358
+ @connection.changes
359
+ end
360
+
361
+ alias :exec_update :exec_delete
362
+
363
+ def last_inserted_id(result)
364
+ @connection.last_insert_row_id
365
+ end
366
+
367
+ def execute(sql, name = nil) #:nodoc:
368
+ log(sql, name) { @connection.execute(sql) }
369
+ end
370
+
371
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
372
+ super
373
+ id_value || @connection.last_insert_row_id
374
+ end
375
+ alias :create :insert_sql
376
+
377
+ def select_rows(sql, name = nil)
378
+ exec_query(sql, name).rows
379
+ end
380
+
381
+ def begin_db_transaction #:nodoc:
382
+ log('begin transaction',nil) { } #@connection.transaction
383
+ end
384
+
385
+ def commit_db_transaction #:nodoc:
386
+ log('commit transaction',nil) { } #@connection.commit
387
+ end
388
+
389
+ def rollback_db_transaction #:nodoc:
390
+ log('rollback transaction',nil) { } #@connection.rollback
391
+ end
392
+
393
+ # SCHEMA STATEMENTS ========================================
394
+
395
+ def tables(name = nil, table_name = nil) #:nodoc:
396
+ table = BigBroda::Table.list(@config[:project], @config[:database])
397
+ return [] if table["tables"].blank?
398
+ table_names = table["tables"].map{|o| o["tableReference"]["tableId"]}
399
+ table_names = table_names.select{|o| o == table_name } if table_name
400
+ table_names
401
+ end
402
+
403
+ def table_exists?(table_name)
404
+ table_name && tables(nil, table_name).any?
405
+ end
406
+
407
+ # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+.
408
+ def columns(table_name) #:nodoc:
409
+ schema = BigBroda::Table.get(@config[:project], @config[:database], table_name)
410
+ schema["schema"]["fields"].map do |field|
411
+ mode = field['mode'].present? && field['mode'] == "REQUIRED" ? false : true
412
+ #column expects (name, default, sql_type = nil, null = true)
413
+ BigqueryColumn.new(field['name'], nil, field['type'], mode )
414
+ end
415
+ end
416
+
417
+ # Returns an array of indexes for the given table.
418
+ def indexes(table_name, name = nil) #:nodoc:
419
+ []
420
+ end
421
+
422
+ def primary_key(table_name) #:nodoc:
423
+ "id"
424
+ end
425
+
426
+ def remove_index!(table_name, index_name) #:nodoc:
427
+ #exec_query "DROP INDEX #{quote_column_name(index_name)}"
428
+ end
429
+
430
+ def add_column(table_name, column_name, type, options = {}) #:nodoc:
431
+ if supports_add_column? && valid_alter_table_options( type, options )
432
+ super(table_name, column_name, type, options)
433
+ else
434
+ alter_table(table_name) do |definition|
435
+ definition.column(column_name, type, options)
436
+ end
437
+ end
438
+ end
439
+
440
+ # See also TableDefinition#column for details on how to create columns.
441
+ def create_table(table_name, options = {})
442
+ td = create_table_definition table_name, options[:temporary], options[:options]
443
+
444
+ unless options[:id] == false
445
+ pk = options.fetch(:primary_key) {
446
+ Base.get_primary_key table_name.to_s.singularize
447
+ }
448
+
449
+ td.primary_key pk, options.fetch(:id, :primary_key), options
450
+ end
451
+
452
+ yield td if block_given?
453
+
454
+ if options[:force] && table_exists?(table_name)
455
+ drop_table(table_name, options)
456
+ end
457
+
458
+ hsh = td.columns.map { |c| {"name"=> c[:name], "type"=> type_to_sql(c[:type]) } }
459
+
460
+ @table_body = { "tableReference"=> {
461
+ "projectId"=> @config[:project],
462
+ "datasetId"=> @config[:database],
463
+ "tableId"=> td.name},
464
+ "schema"=> [fields: hsh]
465
+ }
466
+
467
+ res = BigBroda::Table.create(@config[:project], @config[:database], @table_body )
468
+
469
+ raise res["error"]["errors"].map{|o| "[#{o['domain']}]: #{o['reason']} #{o['message']}" }.join(", ") if res["error"].present?
470
+ end
471
+
472
+ # See also Table for details on all of the various column transformation.
473
+ def change_table(table_name, options = {})
474
+ if supports_bulk_alter? && options[:bulk]
475
+ recorder = ActiveRecord::Migration::CommandRecorder.new(self)
476
+ yield update_table_definition(table_name, recorder)
477
+ bulk_change_table(table_name, recorder.commands)
478
+ else
479
+ yield update_table_definition(table_name, self)
480
+ end
481
+ end
482
+ # Renames a table.
483
+ #
484
+ # Example:
485
+ # rename_table('octopuses', 'octopi')
486
+ def rename_table(table_name, new_name)
487
+ raise Error::PendingFeature
488
+ end
489
+
490
+ # See: http://www.sqlite.org/lang_altertable.html
491
+ # SQLite has an additional restriction on the ALTER TABLE statement
492
+ def valid_alter_table_options( type, options)
493
+ type.to_sym != :primary_key
494
+ end
495
+
496
+ def add_column(table_name, column_name, type, options = {}) #:nodoc:
497
+
498
+ if supports_add_column? && valid_alter_table_options( type, options )
499
+
500
+ hsh = table_name.classify.constantize.columns.map { |c| {"name"=> c.name, "type"=> c.type } }
501
+ hsh << {"name"=> column_name, :type=> type}
502
+ fields = [ fields: hsh ]
503
+
504
+ res = BigBroda::Table.patch(@config[:project], @config[:database], table_name,
505
+ {"tableReference"=> {
506
+ "projectId" => @config[:project],
507
+ "datasetId" =>@config[:database],
508
+ "tableId" => table_name },
509
+ "schema" => fields,
510
+ "description"=> "added from migration"} )
511
+
512
+ else
513
+ bypass_feature
514
+ end
515
+ end
516
+
517
+ def bypass_feature
518
+ begin
519
+ raise Error::NotImplementedColumnOperation
520
+ rescue => e
521
+ puts e.message
522
+ logger.warn(e.message)
523
+ end
524
+ end
525
+
526
+ def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc:
527
+ bypass_feature
528
+ end
529
+
530
+ def change_column_default(table_name, column_name, default) #:nodoc:
531
+ bypass_feature
532
+ end
533
+
534
+ def change_column_null(table_name, column_name, null, default = nil)
535
+ bypass_feature
536
+ end
537
+
538
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
539
+ bypass_feature
540
+ end
541
+
542
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
543
+ bypass_feature
544
+ end
545
+
546
+ def add_reference(table_name, ref_name, options = {})
547
+ polymorphic = options.delete(:polymorphic)
548
+ index_options = options.delete(:index)
549
+ add_column(table_name, "#{ref_name}_id", :string, options)
550
+ add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
551
+ add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options
552
+ end
553
+
554
+ def drop_table(table_name)
555
+ BigBroda::Table.delete(@config[:project], @config[:database], table_name )
556
+ end
557
+
558
+ def dump_schema_information #:nodoc:
559
+ bypass_feature
560
+ end
561
+
562
+ def assume_migrated_upto_version(version, migrations_paths = ActiveRecord::Migrator.migrations_paths)
563
+ bypass_feature
564
+ end
565
+
566
+
567
+ protected
568
+ def select(sql, name = nil, binds = []) #:nodoc:
569
+ exec_query(sql, name, binds)
570
+ end
571
+
572
+ def table_structure(table_name)
573
+ structure = BigBroda::Table.get(@config[:project], @config[:database], table_name)["schema"]["fields"]
574
+ raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty?
575
+ structure
576
+ end
577
+
578
+ def alter_table(table_name, options = {}) #:nodoc:
579
+
580
+ end
581
+
582
+ def move_table(from, to, options = {}, &block) #:nodoc:
583
+ copy_table(from, to, options, &block)
584
+ drop_table(from)
585
+ end
586
+
587
+ def copy_table(from, to, options = {}) #:nodoc:
588
+
589
+ end
590
+
591
+ def copy_table_indexes(from, to, rename = {}) #:nodoc:
592
+
593
+ end
594
+
595
+ def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
596
+
597
+ end
598
+
599
+ def create_table_definition(name, temporary, options)
600
+ TableDefinition.new native_database_types, name, temporary, options
601
+ end
602
+
603
+ end
604
+
605
+ end
606
+
607
+ end