activerecord-bq-adapter 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in activerecord-bq-adapter.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Dan Draper
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,51 @@
1
+ # Activerecord::Bq::Adapter
2
+
3
+ Basic Implementation of an ActiveRecord Adapter for Google's Big Query. Just handles queries right now. See TODO list in the Readme for whats coming.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'activerecord-bq-adapter'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install activerecord-bq-adapter
18
+
19
+ ## Usage
20
+
21
+ Set your model to use the adapter. You probably won't want to do it for your entire app but rather just specific models like:
22
+
23
+ class MyModel < ActiveRecord::Base
24
+ set_table_name "<bq_dataset>.<bq_table>"
25
+ establish_connection(
26
+ :adapter => "bq",
27
+ :project_id => <project_id>,
28
+ :client_id => "<project_id>.apps.googleusercontent.com",
29
+ :client_secret => <client-secret>,
30
+ :refresh_token => <token>
31
+ )
32
+ end
33
+
34
+
35
+ ## Contributing
36
+
37
+ 1. Fork it
38
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
39
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
40
+ 4. Push to the branch (`git push origin my-new-feature`)
41
+ 5. Create new Pull Request
42
+
43
+ ## TODO
44
+
45
+ * Asynchronous Queries, Inserts, Job handling
46
+ * Insert
47
+ * Bulk Inserts (from file, URL)
48
+ * Append vs Rewrite on insertion
49
+ * Specify table for results output
50
+ * Handle select * type queries (BigQuery requires explicit column names in select) - possibly use the Browsing Data part of the API
51
+ * Caching - perhaps using Faraday Middleware
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/activerecord-bq-adapter/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Dan Draper"]
6
+ gem.email = ["daniel@codefire.com"]
7
+ gem.description = %q{Basic Implementation of an ActiveRecord Adapter for Google's Big Query. Just handles queries right now. See TODO list in the Readme for whats coming.}
8
+ gem.summary = %q{Basic Implementation of an ActiveRecord Adapter for Google's Big Query}
9
+ gem.homepage = ""
10
+
11
+ gem.add_dependency "multipart_body", "~> 0.2.1"
12
+
13
+ gem.files = `git ls-files`.split($\)
14
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
15
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
16
+ gem.name = "activerecord-bq-adapter"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = Activerecord::Bq::Adapter::VERSION
19
+ end
@@ -0,0 +1,157 @@
1
+
2
+ module ActiveRecord
3
+ class Base
4
+ def self.bq_connection(config)
5
+ ConnectionAdapters::BQAdapter.new(logger, config)
6
+ end
7
+ end
8
+
9
+ module ConnectionAdapters
10
+
11
+ class BQColumn < Column
12
+ private
13
+ def simplified_type(field_type)
14
+ field_type.downcase.to_sym
15
+ end
16
+ end
17
+
18
+ class BQAdapter < AbstractAdapter
19
+ attr_reader :project_id
20
+
21
+ # Steal from SQLite for now :)
22
+ class BindSubstitution < Arel::Visitors::SQLite # :nodoc:
23
+ include Arel::Visitors::BindVisitor
24
+ end
25
+
26
+ def initialize(logger, config)
27
+ super(nil, logger)
28
+
29
+ @token = nil
30
+ @project_id = config[:project_id]
31
+ @client_id = config[:client_id]
32
+ @client_secret = config[:client_secret]
33
+ @refresh = config[:refresh_token]
34
+ @visitor = BindSubstitution.new(self)
35
+ @api_client_obj = OAuth2::Client.new(@client_id, @client_secret, {:site => 'https://www.googleapis.com'})
36
+ establish_api_connection
37
+ end
38
+
39
+ def adapter_name
40
+ "BQ"
41
+ end
42
+
43
+ # This is really only suitable for queries
44
+ def execute(sql, kind = "bigquery#queryRequest")
45
+ log(sql, kind) do
46
+ do_api do
47
+ payload = { "kind" => kind, "query" => "#{sql};" }
48
+ response = api_connection.post("#{base_url}/queries", :body => JSON(payload), :headers => { "Content-Type" => "application/json" })
49
+ JSON(response.body)
50
+ end
51
+ end
52
+ end
53
+
54
+ def select(sql, name, binds)
55
+ exec_query(sql, name, binds).to_a
56
+ end
57
+
58
+ def exec_query(sql, name = 'SQL', binds = [])
59
+ result = execute(sql)
60
+ ActiveRecord::Result.new(
61
+ result['schema']['fields'].map { |f| f['name'] },
62
+ result['rows'].map do |row|
63
+ row['f'].map { |col| col['v'] }
64
+ end
65
+ )
66
+ end
67
+
68
+ def exec_insert(sql, name, binds)
69
+ raise ActiveRecordError, "Cannot use create for BQ - call ActiveRecord::Base.bq_append instead"
70
+ end
71
+
72
+ def exec_update(sql, name, binds)
73
+ raise ActiveRecordError, "Update is not implemented for Google BigQuery"
74
+ end
75
+
76
+ def explain(sql, bind)
77
+ " -> {Explain not implemented}"
78
+ end
79
+
80
+ def lease
81
+ #TODO: NOt sure why this is needed
82
+ end
83
+
84
+ def columns(table, name = nil)
85
+ do_api do
86
+ puts "Calling columns for #{table}"
87
+ dataset, tablename = table.split(".")
88
+ response = api_connection.get("#{base_url}/datasets/#{dataset}/tables/#{tablename}")
89
+ JSON(response.body)['schema']['fields'].map do |field|
90
+ BQColumn.new(field["name"], nil, field["type"])
91
+ end
92
+ end
93
+ end
94
+
95
+ def table_exists?(table)
96
+ do_api do
97
+ puts "Calling table exists with #{table}"
98
+ dataset, tablename = table.split(".")
99
+ response = api_connection.get("#{base_url}/datasets/#{dataset}/tables/#{tablename}")
100
+ response.status == 200
101
+ end
102
+ end
103
+
104
+ def primary_key(table)
105
+ # TODO: Doesn't really apply for BQ
106
+ "id"
107
+ end
108
+
109
+ def schema_hash(table)
110
+ {
111
+ :schema => {
112
+ :fields => columns(table).map do |column|
113
+ { :name => column.name, :type => column.sql_type }
114
+ end
115
+ }
116
+ }
117
+ end
118
+
119
+ private
120
+ def do_api
121
+ attempt = 1
122
+ begin
123
+ yield if block_given?
124
+ rescue OAuth2::Error => e
125
+ if e.code["code"] == 401 && attempt < 2
126
+ puts "Auth failed: Attempting to reset token"
127
+ refresh_access_token
128
+ establish_api_connection
129
+ attempt += 1
130
+ retry
131
+ else
132
+ raise ActiveRecordError, e.code["message"]
133
+ end
134
+ end
135
+ end
136
+
137
+ def api_connection
138
+ @api_connection || establish_api_connection
139
+ end
140
+
141
+ def establish_api_connection
142
+ refresh_access_token if @token.blank?
143
+ @api_connection = OAuth2::AccessToken.new(@api_client_obj, @token)
144
+ end
145
+
146
+ def refresh_access_token
147
+ refresh_client_obj = OAuth2::Client.new(@client_id, @client_secret, {:site => 'https://accounts.google.com', :authorize_url => '/o/oauth2/auth', :token_url => '/o/oauth2/token'})
148
+ refresh_access_token_obj = OAuth2::AccessToken.new(refresh_client_obj, @token, {refresh_token: @refresh})
149
+ @token = refresh_access_token_obj.refresh!.token
150
+ end
151
+
152
+ def base_url
153
+ "/bigquery/v2/projects/#{@project_id}"
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,7 @@
1
+ require 'active_record/extra/bq'
2
+
3
+ module ActiveRecord
4
+ class Base
5
+ extend ActiveRecord::Extra::BQ
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ require 'csv'
2
+ require 'multipart_body'
3
+
4
+ module ActiveRecord
5
+ module Extra
6
+ module BQ
7
+ # Can provide a CSV via the block
8
+ # Or specify a filename or URI
9
+ #
10
+ def bq_append(options = {}, &block)
11
+ if block_given?
12
+ headers = columns.map(&:name)
13
+ # TODO: Validate row length (AND types?)
14
+ csv = CSV.generate { |csv| yield(csv) }
15
+ data_part(csv)
16
+ end
17
+ end
18
+
19
+ # TODO: split these on table name
20
+ def table_id
21
+
22
+ end
23
+
24
+ def dataset_id
25
+
26
+ end
27
+
28
+ private
29
+ def data_part(csv)
30
+ Part.new(:body => csv, :content_type => "application/octet-stream")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+
2
+ require "arel/visitors/bind_visitor"
3
+ require "activerecord-bq-adapter/version"
4
+ require "active_record/connection_adapters/bq_adapter"
5
+
6
+ require 'active_record/extra'
7
+
8
+ module Activerecord
9
+ module Bq
10
+ module Adapter
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module Activerecord
2
+ module Bq
3
+ module Adapter
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-bq-adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dan Draper
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-10-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: multipart_body
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.2.1
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.2.1
30
+ description: Basic Implementation of an ActiveRecord Adapter for Google's Big Query.
31
+ Just handles queries right now. See TODO list in the Readme for whats coming.
32
+ email:
33
+ - daniel@codefire.com
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - .gitignore
39
+ - Gemfile
40
+ - LICENSE
41
+ - README.md
42
+ - Rakefile
43
+ - activerecord-bq-adapter.gemspec
44
+ - lib/active_record/connection_adapters/bq_adapter.rb
45
+ - lib/active_record/extra.rb
46
+ - lib/active_record/extra/bq.rb
47
+ - lib/activerecord-bq-adapter.rb
48
+ - lib/activerecord-bq-adapter/version.rb
49
+ homepage: ''
50
+ licenses: []
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.24
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Basic Implementation of an ActiveRecord Adapter for Google's Big Query
73
+ test_files: []