bq_factory 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # BqFactory
2
+
3
+ [![Build Status](https://travis-ci.org/yuemori/bq_factory.svg?branch=master)](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
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
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
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -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,3 @@
1
+ module BqFactory
2
+ class DuplicateDefinitionError < StandardError; end
3
+ 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
@@ -0,0 +1,3 @@
1
+ module BqFactory
2
+ VERSION = "0.1.0"
3
+ 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