hyperion-sql 0.0.1.alpha2

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