hyperion-sql 0.0.1.alpha2

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,84 @@
1
+ require 'hyperion/api'
2
+ require 'hyperion/key'
3
+ require 'hyperion/sql/query_builder'
4
+ require 'hyperion/sql/query_executor'
5
+
6
+ module Hyperion
7
+ module Sql
8
+
9
+ class Datastore
10
+
11
+ def initialize(db_strategy, query_executor_strategy, query_builder_strategy)
12
+ @db_strategy = db_strategy
13
+ @query_executor = QueryExecutor.new(query_executor_strategy)
14
+ @query_builder = QueryBuilder.new(query_builder_strategy)
15
+ end
16
+
17
+ def save(records)
18
+ records.map do |record|
19
+ if API.new?(record)
20
+ execute_save_query(query_builder.build_insert(record), record)
21
+ elsif non_empty_record?(record)
22
+ execute_save_query(query_builder.build_update(record), record)
23
+ else
24
+ record
25
+ end
26
+ end
27
+ end
28
+
29
+ def find_by_key(key)
30
+ find(query_from_key(key)).first
31
+ end
32
+
33
+ def find(query)
34
+ sql_query = query_builder.build_select(query)
35
+ results = query_executor.execute_query(sql_query)
36
+ results.map { |record| record_from_db(record, query.kind) }
37
+ end
38
+
39
+ def delete_by_key(key)
40
+ delete(query_from_key(key))
41
+ end
42
+
43
+ def delete(query)
44
+ sql_query = query_builder.build_delete(query)
45
+ query_executor.execute_mutation(sql_query)
46
+ nil
47
+ end
48
+
49
+ def count(query)
50
+ sql_query = query_builder.build_count(query)
51
+ results = query_executor.execute_query(sql_query)
52
+ db_strategy.process_count_result(results[0])
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :query_builder, :query_executor, :db_strategy
58
+
59
+ def non_empty_record?(record)
60
+ record = record.dup
61
+ record.delete(:kind)
62
+ record.delete(:key)
63
+ !record.empty?
64
+ end
65
+
66
+ def execute_save_query(sql_query, record)
67
+ result = query_executor.execute_write(sql_query)
68
+ returned_record = db_strategy.process_result(record, result)
69
+ record_from_db(returned_record, record[:kind])
70
+ end
71
+
72
+ def record_from_db(record, table)
73
+ record[:key] = Key.compose_key(table, record.delete('id')) if API.new?(record)
74
+ record[:kind] = table
75
+ record
76
+ end
77
+
78
+ def query_from_key(key)
79
+ table, id = Key.decompose_key(key)
80
+ Query.new(table, [Filter.new(:id, '=', id)], nil, nil, nil)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,125 @@
1
+ require 'hyperion/key'
2
+ require 'hyperion/sql/sql_query'
3
+
4
+ module Hyperion
5
+ module Sql
6
+
7
+ class QueryBuilder
8
+
9
+ def initialize(qb_strategy)
10
+ @qb_strategy = qb_strategy
11
+ end
12
+
13
+ def build_insert(record)
14
+ record = record.dup
15
+ table = format_table(record.delete(:kind))
16
+ unless record.empty?
17
+ columns = format_array(record.keys.map {|c| format_column(c) })
18
+ values = format_array(record.values.map {|v| '?'})
19
+ query = "INSERT INTO #{table} #{columns} VALUES #{values}"
20
+ else
21
+ query = qb_strategy.empty_insert_query(table)
22
+ end
23
+ SqlQuery.new(qb_strategy.normalize_insert(query), record.values)
24
+ end
25
+
26
+ def build_update(record)
27
+ record = record.dup
28
+ table, id = Key.decompose_key(record.delete(:key))
29
+ table = format_table(record.delete(:kind))
30
+ column_values = record.keys.map {|field| "#{format_column(field)} = ?"}
31
+ query = qb_strategy.normalize_update("UPDATE #{table} SET #{column_values.join(', ')} WHERE #{quote('id')} = #{id}")
32
+ SqlQuery.new(query, record.values)
33
+ end
34
+
35
+ def build_select(query)
36
+ sql_query = SqlQuery.new("SELECT * FROM \"#{query.kind}\"")
37
+ apply_filters(sql_query, query.filters)
38
+ apply_sorts(sql_query, query.sorts)
39
+ qb_strategy.apply_limit_and_offset(sql_query, query.limit, query.offset)
40
+ sql_query
41
+ end
42
+
43
+ def build_delete(query)
44
+ sql_query = SqlQuery.new("DELETE FROM \"#{query.kind}\"")
45
+ apply_filters(sql_query, query.filters)
46
+ sql_query
47
+ end
48
+
49
+ def build_count(query)
50
+ sql_query = SqlQuery.new("SELECT COUNT(*) FROM \"#{query.kind}\"")
51
+ apply_filters(sql_query, query.filters)
52
+ sql_query
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :qb_strategy
58
+
59
+ def quote(str)
60
+ tick = qb_strategy.quote_tick
61
+ tick + str.to_s.gsub(tick, tick + tick) + tick
62
+ end
63
+
64
+ def format_column(column)
65
+ quote(column)
66
+ end
67
+
68
+ def format_table(table)
69
+ quote(table)
70
+ end
71
+
72
+ def format_array(arr)
73
+ "(#{arr.join(', ')})"
74
+ end
75
+
76
+ def apply_filters(sql_query, filters)
77
+ if filters.empty?
78
+ sql_query
79
+ else
80
+ filter_sql = []
81
+ filter_values = []
82
+ filters.each do |filter|
83
+ filter_sql << "#{format_column(filter.field)} #{format_operator(filter.operator)} ?"
84
+ filter_values << filter.value
85
+ end
86
+ sql_query.append("WHERE #{filter_sql.join(' AND ')}", filter_values)
87
+ end
88
+ end
89
+
90
+ def format_operator(operator)
91
+ case operator
92
+ when 'contains?'
93
+ "IN"
94
+ when '!='
95
+ "<>"
96
+ else
97
+ operator
98
+ end
99
+ end
100
+
101
+ def apply_sorts(sql_query, sorts)
102
+ if sorts.empty?
103
+ sql_query
104
+ else
105
+ sort_sql = []
106
+ sort_values = []
107
+ sort_sql = sorts.map do |sort|
108
+ "#{format_column(sort.field)} #{format_order(sort.order)}"
109
+ end
110
+ sql_query.append("ORDER BY #{sort_sql.join(', ')}")
111
+ end
112
+ end
113
+
114
+ def format_order(order)
115
+ case order
116
+ when :asc
117
+ "ASC"
118
+ when :desc
119
+ "DESC"
120
+ end
121
+ end
122
+ end
123
+
124
+ end
125
+ end
@@ -0,0 +1,35 @@
1
+ require 'hyperion/sql'
2
+
3
+ module Hyperion
4
+ module Sql
5
+
6
+ class QueryExecutor
7
+
8
+ attr_reader :strategy
9
+
10
+ def initialize(strategy)
11
+ @strategy = strategy
12
+ end
13
+
14
+ def execute_mutation(sql_query)
15
+ command = connection.create_command(sql_query.query_str)
16
+ command.execute_non_query(*sql_query.bind_values)
17
+ end
18
+
19
+ def execute_query(sql_query)
20
+ command = connection.create_command(sql_query.query_str)
21
+ command.execute_reader(*sql_query.bind_values).to_a
22
+ end
23
+
24
+ def execute_write(sql_query)
25
+ strategy.execute_write(sql_query)
26
+ end
27
+
28
+ def connection
29
+ Sql.connection
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,19 @@
1
+
2
+ module Hyperion
3
+ module Sql
4
+
5
+ class SqlQuery
6
+ attr_reader :query_str, :bind_values
7
+
8
+ def initialize(query_str, bind_values=[])
9
+ @query_str = query_str
10
+ @bind_values = bind_values || []
11
+ end
12
+
13
+ def append(str, values=[])
14
+ @query_str << " #{str}"
15
+ @bind_values += values if values
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,56 @@
1
+ require 'socket'
2
+ require 'digest'
3
+ require 'digest/sha2'
4
+
5
+ module Hyperion
6
+ module Sql
7
+ class Transaction
8
+
9
+ HOST = "#{Socket::gethostbyname(Socket::gethostname)[0]}" rescue "localhost"
10
+
11
+ attr_reader :connection
12
+
13
+ @@counter = 0
14
+
15
+ def initialize(connection)
16
+ @connection = connection
17
+ end
18
+
19
+ def begin
20
+ run "BEGIN"
21
+ end
22
+
23
+ def commit
24
+ run "COMMIT"
25
+ end
26
+
27
+ def rollback
28
+ run "ROLLBACK"
29
+ end
30
+
31
+ def begin_savepoint
32
+ id = new_savepoint_id
33
+ run %{SAVEPOINT "#{id}"}
34
+ id
35
+ end
36
+
37
+ def release_savepoint(id)
38
+ run %{RELEASE SAVEPOINT "#{id}"}
39
+ end
40
+
41
+ def rollback_to_savepoint(id)
42
+ run %{ROLLBACK TO SAVEPOINT "#{id}"}
43
+ end
44
+
45
+ private
46
+
47
+ def run(cmd)
48
+ connection.create_command(cmd).execute_non_query
49
+ end
50
+
51
+ def new_savepoint_id
52
+ Digest::SHA256.hexdigest("#{HOST}:#{$$}:#{Time.now.to_f}:#{@@counter += 1}")[0..-2]
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,156 @@
1
+ shared_examples_for 'Sql Transactions' do
2
+ def write(query)
3
+ command = Hyperion::Sql.connection.create_command(query)
4
+ command.execute_non_query
5
+ end
6
+
7
+ def create_table(table_name)
8
+ write("CREATE TABLE #{table_name} (name VARCHAR(20), age INTEGER)")
9
+ end
10
+
11
+ def drop_table(table_name)
12
+ write("DROP TABLE IF EXISTS #{table_name}")
13
+ end
14
+
15
+ around :each do |example|
16
+ begin
17
+ create_table('test')
18
+ example.run
19
+ ensure
20
+ drop_table('test')
21
+ end
22
+ end
23
+
24
+ def test_count
25
+ Hyperion::API.count_by_kind('test')
26
+ end
27
+
28
+ context 'rollback' do
29
+
30
+ it 'rolls back all changes' do
31
+ Hyperion::Sql.rollback do
32
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
33
+ test_count.should == 1
34
+ end
35
+ test_count.should == 0
36
+ end
37
+
38
+ it 'rolls back multiple' do
39
+ Hyperion::Sql.rollback do
40
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
41
+ Hyperion::Sql.rollback do
42
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
43
+ test_count.should == 2
44
+ end
45
+ test_count.should == 1
46
+ end
47
+ test_count.should == 0
48
+ end
49
+
50
+ it 'returns the result of the body' do
51
+ Hyperion::Sql.rollback do
52
+ :result
53
+ end.should == :result
54
+ end
55
+
56
+ end
57
+
58
+ context 'transaction' do
59
+
60
+ it 'commits' do
61
+ Hyperion::Sql.transaction do
62
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
63
+ test_count.should == 1
64
+ end
65
+ test_count.should == 1
66
+ end
67
+
68
+ it 'commits multiple' do
69
+ Hyperion::Sql.transaction do
70
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
71
+ end
72
+ Hyperion::Sql.transaction do
73
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
74
+ end
75
+ test_count.should == 2
76
+ end
77
+
78
+ it 'commits one and then rolls back the next' do
79
+ Hyperion::Sql.transaction do
80
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
81
+ end
82
+ expect {
83
+ Hyperion::Sql.transaction do
84
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
85
+ raise
86
+ end
87
+ }.to raise_error
88
+ test_count.should == 1
89
+ end
90
+
91
+ it 'rolls back when an exception is thrown' do
92
+ expect {
93
+ Hyperion::Sql.transaction do
94
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
95
+ test_count.should == 1
96
+ raise
97
+ end
98
+ }.to raise_error
99
+ test_count.should == 0
100
+ end
101
+
102
+ it 'commits nested transactions' do
103
+ Hyperion::Sql.transaction do
104
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
105
+ Hyperion::Sql.transaction do
106
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
107
+ end
108
+ end
109
+ test_count.should == 2
110
+ end
111
+
112
+ it 'rolls back nested transactions' do
113
+ expect {
114
+ Hyperion::Sql.transaction do
115
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
116
+ Hyperion::Sql.transaction do
117
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
118
+ end
119
+ test_count.should == 2
120
+ raise
121
+ end
122
+ }.to raise_error
123
+ test_count.should == 0
124
+ end
125
+
126
+ it 'returns the result of the transaction' do
127
+ Hyperion::Sql.transaction do
128
+ :result
129
+ end.should == :result
130
+ end
131
+
132
+ end
133
+
134
+ it 'can handle outer rollback and inner transaction' do
135
+ Hyperion::Sql.rollback do
136
+ Hyperion::Sql.transaction do
137
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
138
+ end
139
+ test_count.should == 1
140
+ end
141
+ test_count.should == 0
142
+ end
143
+
144
+ it 'can handle outer transaction and inner rollback' do
145
+ Hyperion::Sql.transaction do
146
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
147
+ test_count.should == 1
148
+ Hyperion::Sql.rollback do
149
+ write("INSERT INTO test (name, age) VALUES ('Myles', 23)")
150
+ test_count.should == 2
151
+ end
152
+ test_count.should == 1
153
+ end
154
+ test_count.should == 1
155
+ end
156
+ end
@@ -0,0 +1,66 @@
1
+ require 'hyperion/sql/transaction'
2
+
3
+ module Hyperion
4
+ module Sql
5
+
6
+ def self.with_connection(url)
7
+ connection = DataObjects::Connection.new(url)
8
+ Thread.current[:connection] = connection
9
+ yield(connection)
10
+ connection.close
11
+ Thread.current[:connection] = nil
12
+ end
13
+
14
+ def self.connection
15
+ Thread.current[:connection] || raise('No Connection Established')
16
+ end
17
+
18
+ def self.rollback
19
+ with_txn do |txn|
20
+ begin
21
+ savepoint_id = txn.begin_savepoint
22
+ yield
23
+ ensure
24
+ txn.rollback_to_savepoint(savepoint_id)
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.transaction
30
+ with_txn do |txn|
31
+ begin
32
+ savepoint_id = txn.begin_savepoint
33
+ result = yield
34
+ txn.release_savepoint(savepoint_id)
35
+ result
36
+ rescue Exception => e
37
+ txn.rollback_to_savepoint(savepoint_id)
38
+ raise e
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def self.in_transaction?
46
+ !Thread.current[:transaction].nil?
47
+ end
48
+
49
+ def self.with_txn
50
+ if Thread.current[:transaction]
51
+ yield(Thread.current[:transaction])
52
+ else
53
+ txn = (Thread.current[:transaction] = Transaction.new(connection))
54
+ txn.begin
55
+ result = yield(txn)
56
+ txn.commit
57
+ result
58
+ end
59
+ rescue Exception => e
60
+ txn.rollback
61
+ raise e
62
+ ensure
63
+ Thread.current[:transaction] = nil
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,4 @@
1
+ require 'hyperion/sql'
2
+
3
+ describe Hyperion::Sql do
4
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hyperion-sql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.alpha2
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - 8th Light, Inc.
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-17 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - '='
20
+ - !ruby/object:Gem::Version
21
+ version: 2.11.0
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - '='
28
+ - !ruby/object:Gem::Version
29
+ version: 2.11.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: hyperion-api
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 0.0.1.alpha2
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.0.1.alpha2
46
+ description: Shared behavior for Sql databases
47
+ email:
48
+ - myles@8thlight.com
49
+ - skim@8thlight.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - lib/hyperion/sql.rb
55
+ - lib/hyperion/sql/query_executor.rb
56
+ - lib/hyperion/sql/sql_query.rb
57
+ - lib/hyperion/sql/transaction.rb
58
+ - lib/hyperion/sql/transaction_spec.rb
59
+ - lib/hyperion/sql/query_builder.rb
60
+ - lib/hyperion/sql/datastore.rb
61
+ - spec/hyperion/sql_spec.rb
62
+ homepage: https://github.com/mylesmegyesi/hyperion-ruby
63
+ licenses:
64
+ - Eclipse Public License
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: 1.8.7
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>'
79
+ - !ruby/object:Gem::Version
80
+ version: 1.3.1
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.24
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: Shared behavior for Sql databases
87
+ test_files:
88
+ - spec/hyperion/sql_spec.rb