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.
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