activerecord-bq-adapter 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []