bq_factory 0.1.0
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 +7 -0
- data/.env.sample +2 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +609 -0
- data/.travis.yml +11 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +132 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/bq_factory.gemspec +31 -0
- data/lib/bq_factory/attribute.rb +32 -0
- data/lib/bq_factory/client.rb +56 -0
- data/lib/bq_factory/configuration.rb +13 -0
- data/lib/bq_factory/dsl.rb +15 -0
- data/lib/bq_factory/errors.rb +3 -0
- data/lib/bq_factory/query_builder.rb +17 -0
- data/lib/bq_factory/record.rb +63 -0
- data/lib/bq_factory/registory.rb +40 -0
- data/lib/bq_factory/registory_decorator.rb +27 -0
- data/lib/bq_factory/version.rb +3 -0
- data/lib/bq_factory.rb +76 -0
- metadata +194 -0
data/README.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# BqFactory
|
2
|
+
|
3
|
+
[](https://travis-ci.org/yuemori/bq_factory)
|
4
|
+
|
5
|
+
This gem create Static SQL View on Google Bigquery from Hash and Array with DSL.
|
6
|
+
|
7
|
+
Inspired by [bq_fake_view](https://github.com/joker1007/bq_fake_view)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
```ruby
|
14
|
+
gem 'bq_factory'
|
15
|
+
```
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install bq_factory
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### Configuration
|
28
|
+
|
29
|
+
Add configuration to initializer or spec_helper.
|
30
|
+
|
31
|
+
```
|
32
|
+
# RSpec
|
33
|
+
# spec/spec_helper.rb
|
34
|
+
|
35
|
+
BqFactory.configure do |config|
|
36
|
+
config.project_id = 'google_project_id'
|
37
|
+
config.keyfile_path = '/path/to/keyfile.json' # from google developer console
|
38
|
+
end
|
39
|
+
```
|
40
|
+
|
41
|
+
and, setup factories in fixture file.
|
42
|
+
|
43
|
+
```
|
44
|
+
BqFactory.define do
|
45
|
+
# Reference target of dataset and tables in options.
|
46
|
+
# Factory read schema from reference target!
|
47
|
+
factory :user, dataset: 'my_dataset', table: 'my_table'
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
### Use Case
|
52
|
+
|
53
|
+
Example, if `user` table schema is this:
|
54
|
+
|
55
|
+
|column_name|type|
|
56
|
+
|:----|:----|
|
57
|
+
|name|STRING|
|
58
|
+
|age|INTEGER|
|
59
|
+
|create_at|TIMESTAMP|
|
60
|
+
|height|FLOAT|
|
61
|
+
|admin|BOOLEAN|
|
62
|
+
|
63
|
+
In your test code:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# RSpec
|
67
|
+
|
68
|
+
describe TestClass do
|
69
|
+
before(:all) do
|
70
|
+
BqFactory.create_dataset!('test_dataset') # => create test dataset
|
71
|
+
end
|
72
|
+
let(:alice) { { name: 'alice', age: 20, create_at: "2016-01-01 00:00:00", height: 150.1, admin: true } }
|
73
|
+
let(:bob) { { name: 'bob', age: 30, create_at: "2016-01-01 00:00:00", height: 170.1, admin: false } }
|
74
|
+
|
75
|
+
describe 'build query' do
|
76
|
+
subject { BqFactory.build_query 'user', alice }
|
77
|
+
let(:query) { 'SELECT * FROM (SELECT "alice" AS name, 20 AS age, TIMESTAMP("2016-01-01 00:00:00") AS create_at, 150.1 AS height, true AS admin )' }
|
78
|
+
it { is_expected.to eq query }
|
79
|
+
end
|
80
|
+
|
81
|
+
describe 'from Hash' do
|
82
|
+
before { BqFactory.create_view 'test_dataset', 'test_view', alice }
|
83
|
+
# => Build query 'SELECT * FROM (SELECT "alice" AS name, 20 AS age, TIMESTAMP("2016-01-01 00:00:00") AS create_at, 150.1 AS height, true AS admin )'
|
84
|
+
# And create view "test_view" to "test_dataset"
|
85
|
+
|
86
|
+
let(:query) { "SELECT * FROM [test_dataset.test_view]" }
|
87
|
+
subject { BqFactory.query(query) }
|
88
|
+
|
89
|
+
it { expect(subject.first.name).to eq alise["name"] }
|
90
|
+
end
|
91
|
+
|
92
|
+
describe 'from Array' do
|
93
|
+
before { BqFactory.create_view 'test_dataset', 'test_view', [alice, bob] }
|
94
|
+
# => Build query 'SELECT * FROM (SELECT "alice" AS name, 20 AS age, TIMESTAMP("2016-01-01 00:00:00") AS create_at, 150.1 AS height, true AS admin ), (SELECT "bob" AS name, 30 AS age, TIMESTAMP("2016-01-01 00:00:00") AS create_at, 170.1 AS height, false AS admafterin)'
|
95
|
+
# And create view "test_view" to "test_dataset"
|
96
|
+
|
97
|
+
let(:query) { "SELECT * FROM [test_dataset.test_view]" }
|
98
|
+
subject { BqFactory.query(query) }
|
99
|
+
|
100
|
+
it { expect(subject.first.name).to eq alise["name"] }
|
101
|
+
it { expect(subject.last.name).to eq bob["name"] }
|
102
|
+
end
|
103
|
+
|
104
|
+
after(:all) do
|
105
|
+
BqFactory.delete_dataset!('test_dataset') # => clean up!
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
## Testing
|
111
|
+
|
112
|
+
### Install dependencies
|
113
|
+
|
114
|
+
```shell
|
115
|
+
bundle install
|
116
|
+
```
|
117
|
+
|
118
|
+
### Run rspec
|
119
|
+
|
120
|
+
```
|
121
|
+
bundle exec rspec
|
122
|
+
```
|
123
|
+
|
124
|
+
## Contributing
|
125
|
+
|
126
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/bq_factory. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
|
127
|
+
|
128
|
+
|
129
|
+
## License
|
130
|
+
|
131
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
132
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "bq_factory"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
data/bq_factory.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'bq_factory/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "bq_factory"
|
8
|
+
spec.version = BqFactory::VERSION
|
9
|
+
spec.authors = ["yuemori"]
|
10
|
+
spec.email = ["yuemori@aiming-inc.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{Create BigQuery view from hash}
|
13
|
+
spec.description = %q{Create BigQuery view from hash}
|
14
|
+
spec.homepage = "http://github.com/yuemori/bq_factory"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "exe"
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency "gcloud", "~> 0.6.1"
|
23
|
+
spec.add_dependency "hashie", "~> 3.4"
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
spec.add_development_dependency "shoulda-matchers"
|
28
|
+
spec.add_development_dependency "rubocop"
|
29
|
+
spec.add_development_dependency "simplecov", "~> 0.8.0"
|
30
|
+
spec.add_development_dependency "dotenv"
|
31
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module BqFactory
|
2
|
+
class Attribute
|
3
|
+
PERMIT_TYPES = %i(STRING INTEGER FLOAT TIMESTAMP BOOLEAN RECORD).freeze
|
4
|
+
|
5
|
+
attr_accessor :value
|
6
|
+
attr_reader :name, :type
|
7
|
+
|
8
|
+
def initialize(name, type)
|
9
|
+
type = type.to_sym
|
10
|
+
raise ArgumentError.new, "#{type} is not implemented" unless PERMIT_TYPES.include?(type)
|
11
|
+
@name = name
|
12
|
+
@type = type
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_sql
|
16
|
+
"#{cast_to_sql(value)} AS #{name}"
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def cast_to_sql(value)
|
22
|
+
return "CAST(NULL AS #{type})" if value.nil?
|
23
|
+
|
24
|
+
case type
|
25
|
+
when :STRING then %("#{value.gsub(/"/, '\"')}")
|
26
|
+
when :INTEGER, :FLOAT, :BOOLEAN then value.to_s
|
27
|
+
when :TIMESTAMP then %{TIMESTAMP("#{value.strftime('%Y-%m-%d %X')}")}
|
28
|
+
when :RECORD then raise NotImplementedError.new, "sorry, RECORD is not implemented"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'gcloud'
|
2
|
+
|
3
|
+
module BqFactory
|
4
|
+
class Client
|
5
|
+
delegate :bigquery, to: :gcloud
|
6
|
+
|
7
|
+
def initialize(project_id, keyfile_path)
|
8
|
+
@gcloud = Gcloud.new project_id, keyfile_path
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_view(dataset_name, table_id, query)
|
12
|
+
dataset(dataset_name).create_view(table_id, query)
|
13
|
+
end
|
14
|
+
|
15
|
+
def create_dataset!(dataset_name)
|
16
|
+
bigquery.create_dataset(dataset_name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_table!(dataset_name, table_id, schema)
|
20
|
+
dataset(dataset_name).create_table(table_id, schema: { fields: schema })
|
21
|
+
end
|
22
|
+
|
23
|
+
def delete_dataset!(dataset_name)
|
24
|
+
dataset(dataset_name).delete(force: true)
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete_table!(dataset_name, table_id)
|
28
|
+
dataset(dataset_name).table(table_id).delete(force: true)
|
29
|
+
end
|
30
|
+
|
31
|
+
def fetch_schema(dataset_name, table_id)
|
32
|
+
table(dataset_name, table_id).schema["fields"]
|
33
|
+
end
|
34
|
+
|
35
|
+
def query(query)
|
36
|
+
bigquery.query(query)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :gcloud
|
42
|
+
|
43
|
+
def table(dataset_name, table_id)
|
44
|
+
dataset = bigquery.dataset(dataset_name)
|
45
|
+
table = dataset.table(table_id)
|
46
|
+
raise ArgumentError.new, "table not found: #{table_id}" if table.nil?
|
47
|
+
table
|
48
|
+
end
|
49
|
+
|
50
|
+
def dataset(name)
|
51
|
+
dataset = bigquery.dataset(name)
|
52
|
+
raise ArgumentError.new, "dataset not found: #{name}" if dataset.nil?
|
53
|
+
dataset
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module BqFactory
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :project_id, :keyfile_path
|
4
|
+
|
5
|
+
def schemas
|
6
|
+
@schemas ||= RegistoryDecorator.new(Registory.new('schema'))
|
7
|
+
end
|
8
|
+
|
9
|
+
def client
|
10
|
+
@client ||= BqFactory::Client.new(project_id, keyfile_path)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module BqFactory
|
2
|
+
class DSL
|
3
|
+
def self.run(block)
|
4
|
+
new.instance_eval(&block)
|
5
|
+
end
|
6
|
+
|
7
|
+
def factory(name, options = {})
|
8
|
+
name = name.to_sym
|
9
|
+
dataset_name = options.key?(:dataset) ? options[:dataset] : BqFactory.default_dataset
|
10
|
+
table_name = options.key?(:table) ? options[:table] : name
|
11
|
+
schema = BqFactory.fetch_schema_from_bigquery(dataset_name, table_name)
|
12
|
+
BqFactory.register(name, schema)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module BqFactory
|
2
|
+
class QueryBuilder < Array
|
3
|
+
attr_reader :records
|
4
|
+
|
5
|
+
def initialize(records)
|
6
|
+
@records = records
|
7
|
+
end
|
8
|
+
|
9
|
+
def build
|
10
|
+
%{SELECT * FROM #{records.map { |record| build_subquery(record) }.join(', ')}}
|
11
|
+
end
|
12
|
+
|
13
|
+
def build_subquery(record)
|
14
|
+
"(#{record.to_sql})"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
|
3
|
+
module BqFactory
|
4
|
+
class Record
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
delegate :include?, to: :items
|
8
|
+
|
9
|
+
def initialize(schema, params = {})
|
10
|
+
raise ArgumentError.new, "Schema is not Array" unless schema.is_a? Array
|
11
|
+
|
12
|
+
schema.each do |hash|
|
13
|
+
column = Hashie::Mash.new(hash)
|
14
|
+
items[column.name] = Attribute.new(column.name, column.type)
|
15
|
+
end
|
16
|
+
|
17
|
+
params.each { |key, value| send(:"#{key}=", value) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def find(name)
|
21
|
+
if include?(name)
|
22
|
+
items[name].value
|
23
|
+
else
|
24
|
+
raise ArgumentError.new, "#{name} is not attribute"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
alias :[] :find
|
28
|
+
|
29
|
+
def each(&block)
|
30
|
+
items.values.uniq.each(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_missing(method_name, *args, &block)
|
34
|
+
name = trim_equal(method_name)
|
35
|
+
return super unless respond_to?(name)
|
36
|
+
return items[name].value = args.first if include_equal?(method_name)
|
37
|
+
items[name].value
|
38
|
+
end
|
39
|
+
|
40
|
+
def respond_to?(method_name)
|
41
|
+
include? trim_equal(method_name) || super
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_sql
|
45
|
+
"SELECT #{items.values.map(&:to_sql).join(', ')}"
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def include_equal?(value)
|
51
|
+
!!value.to_s.match(/(?<name>.*)=\Z/)
|
52
|
+
end
|
53
|
+
|
54
|
+
def trim_equal(value)
|
55
|
+
md = value.to_s.match(/(?<name>.*)=\Z/)
|
56
|
+
md ? md[:name] : value.to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
def items
|
60
|
+
@items ||= {}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module BqFactory
|
2
|
+
class Registory
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
attr_reader :name
|
6
|
+
|
7
|
+
def initialize(name)
|
8
|
+
@name = name
|
9
|
+
end
|
10
|
+
|
11
|
+
def find(name)
|
12
|
+
if registered?(name)
|
13
|
+
@items[name]
|
14
|
+
else
|
15
|
+
raise ArgumentError.new, "#{@name} not registered: #{name}"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
alias :[] :find
|
19
|
+
|
20
|
+
def clear
|
21
|
+
items.clear
|
22
|
+
end
|
23
|
+
|
24
|
+
def each(&block)
|
25
|
+
items.values.uniq.each(&block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def registered?(name)
|
29
|
+
items.key?(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def register(name, item)
|
33
|
+
items[name] = item
|
34
|
+
end
|
35
|
+
|
36
|
+
def items
|
37
|
+
@items ||= {}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module BqFactory
|
2
|
+
class RegistoryDecorator
|
3
|
+
attr_reader :registory
|
4
|
+
|
5
|
+
def initialize(registory)
|
6
|
+
@registory = registory
|
7
|
+
end
|
8
|
+
|
9
|
+
def register(name, table)
|
10
|
+
name = name.to_sym
|
11
|
+
|
12
|
+
if registered?(name)
|
13
|
+
raise DuplicateDefinitionError.new, "#{registory.name} already registered: #{name}"
|
14
|
+
else
|
15
|
+
registory.register(name, table)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def registered?(name)
|
20
|
+
registory.registered? name.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
def find(name)
|
24
|
+
registory.find(name.to_sym)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/bq_factory.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_support/core_ext"
|
3
|
+
require "bq_factory/version"
|
4
|
+
require "bq_factory/attribute"
|
5
|
+
require "bq_factory/record"
|
6
|
+
require "bq_factory/client"
|
7
|
+
require "bq_factory/configuration"
|
8
|
+
require "bq_factory/dsl"
|
9
|
+
require "bq_factory/errors"
|
10
|
+
require "bq_factory/query_builder"
|
11
|
+
require "bq_factory/record"
|
12
|
+
require "bq_factory/registory"
|
13
|
+
require "bq_factory/registory_decorator"
|
14
|
+
|
15
|
+
module BqFactory
|
16
|
+
class << self
|
17
|
+
delegate :client, :project_id, :keyfile_path, :schemas, to: :configuration
|
18
|
+
|
19
|
+
def configure
|
20
|
+
yield configuration if block_given?
|
21
|
+
configuration
|
22
|
+
end
|
23
|
+
|
24
|
+
def configuration
|
25
|
+
@configuration ||= Configuration.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def define(&block)
|
29
|
+
DSL.run(block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_view(dataset_name, factory_name, rows)
|
33
|
+
query = build_query(factory_name, rows)
|
34
|
+
client.create_view(dataset_name, factory_name, query)
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_query(factory_name, rows)
|
38
|
+
rows = [rows] unless rows.instance_of? Array
|
39
|
+
schema = schema_by_name(factory_name)
|
40
|
+
records = rows.flatten.map { |row| Record.new(schema, row) }
|
41
|
+
QueryBuilder.new(records).build
|
42
|
+
end
|
43
|
+
|
44
|
+
def schema_by_name(factory_name)
|
45
|
+
schemas.find(factory_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def create_dataset!(dataset_name)
|
49
|
+
client.create_dataset!(dataset_name)
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_table!(dataset_name, table_name, schema)
|
53
|
+
client.create_table!(dataset_name, table_name, schema)
|
54
|
+
end
|
55
|
+
|
56
|
+
def delete_dataset!(dataset_name)
|
57
|
+
client.delete_dataset!(dataset_name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def delete_table!(dataset_name, table_name)
|
61
|
+
client.delete_table!(dataset_name, table_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
def fetch_schema_from_bigquery(dataset_name, table_name)
|
65
|
+
client.fetch_schema(dataset_name, table_name)
|
66
|
+
end
|
67
|
+
|
68
|
+
def query(query)
|
69
|
+
client.query(query)
|
70
|
+
end
|
71
|
+
|
72
|
+
def register(factory_name, schema)
|
73
|
+
schemas.register(factory_name, schema)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|