jdbc-helper 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,126 @@
1
+ # encoding: UTF-8
2
+ # Junegunn Choi (junegunn.c@gmail.com)
3
+
4
+ module JDBCHelper
5
+ # Abstract base class for wrappers for various database objects.
6
+ # @abstract
7
+ # @since 0.2.0
8
+ # @todo Need to add more subclasses (such as for functions and procedures)
9
+ class ObjectWrapper
10
+ # Underlying JDBCHelper::Connection object
11
+ # @return [JDBCHelper::Connection]
12
+ attr_reader :connection
13
+
14
+ # Object name (or expression)
15
+ # @return [String]
16
+ attr_reader :name
17
+
18
+ # Base constructor.
19
+ # @param [JDBCHelper::Connection] conn JDBCHelper::Connection object
20
+ # @param [String] name Name of the object to be wrapped
21
+ def initialize(conn, name)
22
+ raise Exception.new(
23
+ "JDBCHelper::ObjectWrapper is an abstract class") if
24
+ self.instance_of? ObjectWrapper
25
+
26
+ @connection = conn
27
+ @name = name.to_s
28
+ JDBCHelper::SQL.check @name
29
+ end
30
+ end#ObjectWrapper
31
+
32
+ # A wrapper object representing a database table. Allows you to perform table operations easily.
33
+ # @since 0.2.0
34
+ # @example Usage
35
+ # # For more complex examples, refer to test/test_object_wrapper.rb
36
+ #
37
+ # conn.table('test.data').count
38
+ # conn.table('test.data').empty?
39
+ # conn.table('test.data').select(:c => 3) do |row|
40
+ # puts row.a
41
+ # end
42
+ # conn.table('test.data').update(:a => 1, :b => 2, :where => { :c => 3 })
43
+ # conn.table('test.data').insert(:a => 10, :b => 20, :c => 30)
44
+ # conn.table('test.data').insert_ignore(:a => 10, :b => 20, :c => 30)
45
+ # conn.table('test.data').insert_replace(:a => 10, :b => 20, :c => 30)
46
+ # conn.table('test.data').delete(:c => 3)
47
+ # conn.table('test.data').truncate_table!
48
+ # conn.table('test.data').drop_table!
49
+ class TableWrapper < ObjectWrapper
50
+ # Retrieves the count of the table
51
+ # @return [Fixnum] Count of the records.
52
+ def count(where = nil)
53
+ @connection.query(JDBCHelper::SQL.count name, where)[0][0]
54
+ end
55
+
56
+ # Sees if the table is empty
57
+ # @return [boolean]
58
+ def empty?
59
+ count == 0
60
+ end
61
+
62
+ # Select * with optional conditions
63
+ # @param [Hash/String] where Select filters
64
+ # @return [Array] Array is returned if block is not given
65
+ # @yield [JDBCHelper::Connection::Row]
66
+ def select where = nil, &block
67
+ @connection.query(JDBCHelper::SQL.select(name, where), &block)
68
+ end
69
+
70
+ # Inserts a record into the table with the given hash
71
+ # @param [Hash] data_hash Column values in Hash
72
+ # @return [Fixnum] Number of affected records
73
+ def insert data_hash
74
+ @connection.update(JDBCHelper::SQL.insert name, data_hash)
75
+ end
76
+
77
+ # Inserts a record into the table with the given hash.
78
+ # Skip insertion when duplicate record is found.
79
+ # @note This is not SQL standard. Only works if the database supports insert ignore syntax.
80
+ # @param [Hash] data_hash Column values in Hash
81
+ # @return [Fixnum] Number of affected records
82
+ def insert_ignore data_hash
83
+ @connection.update(JDBCHelper::SQL.insert_ignore name, data_hash)
84
+ end
85
+
86
+ # Replaces a record in the table with the new one with the same unique key.
87
+ # @note This is not SQL standard. Only works if the database supports replace syntax.
88
+ # @param [Hash] data_hash Column values in Hash
89
+ # @return [Fixnum] Number of affected records
90
+ def replace data_hash
91
+ @connection.update(JDBCHelper::SQL.replace name, data_hash)
92
+ end
93
+
94
+ # Executes update with the given hash.
95
+ # :where element of the hash is taken out to generate where clause of the update SQL.
96
+ # @param [Hash] data_hash_with_where Column values in Hash.
97
+ # :where element of the given hash can (usually should) point to another Hash representing update filters.
98
+ # @return [Fixnum] Number of affected records
99
+ def update data_hash_with_where
100
+ @connection.update(JDBCHelper::SQL.update name, data_hash_with_where)
101
+ end
102
+
103
+ # Deletes records matching given condtion
104
+ # @param [Hash] where Delete filters
105
+ # @return [Fixnum] Number of affected records
106
+ def delete where = nil
107
+ @connection.update(JDBCHelper::SQL.delete name, where)
108
+ end
109
+
110
+ # Empties the table.
111
+ # @note This operation cannot be undone
112
+ # @return [Fixnum] executeUpdate return value
113
+ def truncate_table!
114
+ @connection.update(JDBCHelper::SQL.check "truncate table #{name}")
115
+ end
116
+
117
+ # Drops the table.
118
+ # @note This operation cannot be undone
119
+ # @return [Fixnum] executeUpdate return value
120
+ def drop_table!
121
+ @connection.update(JDBCHelper::SQL.check "drop table #{name}")
122
+ end
123
+ end#TableWrapper
124
+
125
+ end#JDBCHelper
126
+
@@ -0,0 +1,168 @@
1
+ # encoding: UTF-8
2
+ # Junegunn Choi (junegunn.c@gmail.com)
3
+
4
+ module JDBCHelper
5
+ module SQL
6
+ # Prevents a string from being quoted
7
+ def self.expr str
8
+ Expr.new str
9
+ end
10
+
11
+ # Returns NotNilClass singleton object
12
+ def self.not_nil
13
+ NotNilClass.singleton
14
+ end
15
+
16
+ # Formats the given data so that it can be injected into SQL
17
+ def self.value data
18
+ case data
19
+ when NilClass
20
+ 'null'
21
+ when Fixnum, Bignum, Float
22
+ data
23
+ when JDBCHelper::SQL::Expr
24
+ data.to_s
25
+ when String
26
+ "'#{esc data}'"
27
+ else
28
+ raise NotImplementedError.new("Unsupported datatype: #{data.class}")
29
+ end
30
+ end
31
+
32
+ # Generates SQL where cluase with the given conditions.
33
+ # Parameter can be either Hash of String.
34
+ def self.where conds
35
+ check(where_internal conds)
36
+ end
37
+
38
+ # SQL Helpers
39
+ # ===========
40
+
41
+ # Generates insert SQL with hash
42
+ def self.insert table, data_hash
43
+ insert_internal 'insert', table, data_hash
44
+ end
45
+
46
+ # Generates insert ignore SQL (Non-standard syntax)
47
+ def self.insert_ignore table, data_hash
48
+ insert_internal 'insert ignore', table, data_hash
49
+ end
50
+
51
+ # Generates replace SQL (Non-standard syntax)
52
+ def self.replace table, data_hash
53
+ insert_internal 'replace', table, data_hash
54
+ end
55
+
56
+ # Generates update SQL with hash.
57
+ # :where element of the given hash is taken out to generate where clause.
58
+ def self.update table, data_hash
59
+ where_clause = make_where(data_hash.delete :where)
60
+ updates = data_hash.map { |k, v| "#{k} = #{value v}" }.join(', ')
61
+ check "update #{table} set #{updates}#{where_clause}"
62
+ end
63
+
64
+ # Generates select * SQL with the given conditions
65
+ def self.select table, conds = nil
66
+ check "select * from #{table}#{make_where conds}"
67
+ end
68
+
69
+ # Generates count SQL with the given conditions
70
+ def self.count table, conds = nil
71
+ check "select count(*) from #{table}#{make_where conds}"
72
+ end
73
+
74
+ # Generates delete SQL with the given conditions
75
+ def self.delete table, conds = nil
76
+ check "delete from #{table}#{make_where conds}"
77
+ end
78
+
79
+ # FIXME: Naive protection for SQL Injection
80
+ def self.check expr
81
+ return nil if expr.nil?
82
+
83
+ test = expr.gsub(/'[^']*'/, '').gsub(/`[^`]*`/, '').gsub(/"[^"]*"/, '')
84
+ raise ArgumentError.new("Expression cannot contain semi-colons: #{test}") if test.include?(';')
85
+ raise ArgumentError.new("Expression cannot contain comments: #{test}") if test.match(%r{--|/\*|\*/})
86
+ raise ArgumentError.new("Unclosed quotation mark: #{test}") if test.match(/['"`]/)
87
+
88
+ return expr
89
+ end
90
+
91
+ private
92
+ def self.esc str
93
+ str.gsub("'", "''")
94
+ end
95
+
96
+ def self.make_where conds
97
+ str = where_internal conds
98
+ str = ' where ' + str if str
99
+ str
100
+ end
101
+
102
+ # No check
103
+ def self.where_internal conds
104
+ return nil if conds.nil?
105
+
106
+ case conds
107
+ when String
108
+ return nil if conds.strip.empty?
109
+ conds
110
+ when Hash
111
+ return nil if conds.empty?
112
+ conds.map { |k, v|
113
+ "#{k} " +
114
+ case v
115
+ when NilClass
116
+ "is null"
117
+ when NotNilClass
118
+ "is not null"
119
+ when Fixnum, Bignum, Float, JDBCHelper::SQL::Expr
120
+ "= #{v}"
121
+ when Range
122
+ ">= #{v.first} and #{k} <#{'=' unless v.exclude_end?} #{v.last}"
123
+ when Array
124
+ "in (" +
125
+ v.map { |e|
126
+ case e
127
+ when String
128
+ "'#{esc e}'"
129
+ else
130
+ e
131
+ end }.join(', ') + ")"
132
+ when String
133
+ "= '#{esc v}'"
134
+ else
135
+ raise NotImplementedError.new("Unsupported class: #{v.class}")
136
+ end
137
+ }.join(' and ')
138
+ else
139
+ raise NotImplementedError.new("Parameter to where must be either Hash or String")
140
+ end
141
+ end
142
+
143
+ def self.insert_internal cmd, table, data_hash
144
+ cols = data_hash.keys
145
+ check "#{cmd} into #{table} (#{cols.join ', '}) values (#{cols.map{|c|value data_hash[c]}.join ', '})"
146
+ end
147
+
148
+ class Expr
149
+ def initialize str
150
+ @expr = str
151
+ end
152
+
153
+ def to_s
154
+ @expr
155
+ end
156
+ end
157
+
158
+ # Class to represent "(IS) NOT NULL" expression in SQL
159
+ class NotNilClass
160
+ # Returns the singleton object of NotNilClass
161
+ # @return [NotNilClass]
162
+ def self.singleton
163
+ @@singleton ||= NotNilClass.new
164
+ end
165
+ end
166
+ end#SQL
167
+ end#JDBCHelper
168
+
data/lib/jdbc-helper.rb CHANGED
@@ -15,6 +15,8 @@ module JavaSql # :nodoc:
15
15
  include_package 'java.sql'
16
16
  end
17
17
 
18
+ require 'jdbc-helper/sql'
19
+ require 'jdbc-helper/object_wrapper'
18
20
  require 'jdbc-helper/constants'
19
21
  require 'jdbc-helper/connection'
20
22
  require 'jdbc-helper/connector'
data/test/database.yml CHANGED
@@ -4,10 +4,12 @@ mysql:
4
4
  url: jdbc:mysql://localhost/test
5
5
  user: mysql
6
6
  password:
7
+ database: test
7
8
 
8
9
  # oracle:
9
10
  # driver: com.mysql.jdbc.Driver
10
- # url: jdbc:mysql://localhost/test
11
- # user: mysql
11
+ # url: jdbc:oracle:thin:@localhost/test
12
+ # user: test
12
13
  # password:
14
+ # database: test
13
15
 
data/test/helper.rb CHANGED
@@ -15,3 +15,25 @@ require 'jdbc-helper'
15
15
 
16
16
  class Test::Unit::TestCase
17
17
  end
18
+
19
+ module JDBCHelperTestHelper
20
+ require 'yaml'
21
+ def config
22
+ @db_config ||= YAML.load File.read(File.dirname(__FILE__) + '/database.yml')
23
+ end
24
+
25
+ def each_connection(&block)
26
+ config.each do | db, conn_info |
27
+ conn = JDBCHelper::Connection.new(conn_info)
28
+ begin
29
+ if block.arity == 1
30
+ yield conn
31
+ else
32
+ yield conn, conn_info
33
+ end
34
+ ensure
35
+ conn.close
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,28 +1,16 @@
1
1
  require 'helper'
2
2
 
3
- class TestJdbcHelper < Test::Unit::TestCase
3
+ class TestConnection < Test::Unit::TestCase
4
+ include JDBCHelperTestHelper
5
+
4
6
  def setup
5
- require 'yaml'
6
- @config = YAML.load File.read(File.dirname(__FILE__) + '/database.yml')
7
7
  end
8
8
 
9
9
  def teardown
10
-
11
10
  end
12
11
 
13
12
  TEST_TABLE = 'tmp_jdbc_helper_test'
14
13
 
15
- def each_connection
16
- @config.each do | db, conn_info |
17
- conn = JDBCHelper::Connection.new(conn_info)
18
- begin
19
- yield conn
20
- ensure
21
- conn.close
22
- end
23
- end
24
- end
25
-
26
14
  def get_one_two
27
15
  "
28
16
  select 1 one, 'two' two from dual
@@ -42,7 +30,7 @@ class TestJdbcHelper < Test::Unit::TestCase
42
30
  assert_equal 'two', rec[1]
43
31
  assert_equal 'two', rec['two']
44
32
 
45
- assert_raise(NameError) { rec.three }
33
+ assert_raise(NoMethodError) { rec.three }
46
34
  assert_raise(NameError) { rec['three'] }
47
35
  assert_raise(RangeError) { rec[3] }
48
36
  end
@@ -69,7 +57,7 @@ class TestJdbcHelper < Test::Unit::TestCase
69
57
  # ---------------------------------------------------------------
70
58
 
71
59
  def test_connect_and_close
72
- @config.each do | db, conn_info |
60
+ config.each do | db, conn_info |
73
61
  4.times do | i |
74
62
  # With or without timeout parameter
75
63
  conn_info['timeout'] = 60 if i % 2 == 1
@@ -276,28 +264,6 @@ class TestJdbcHelper < Test::Unit::TestCase
276
264
  end
277
265
  end
278
266
 
279
- def test_connectors
280
- @config.each do | db, conn_info |
281
- if db == 'mysql'
282
- host = conn_info['url'].match(%r{//(.*?)/})[1]
283
- db = conn_info['url'].match(%r{/([^/?]*?)(\?.*)?$})[1]
284
- conn = JDBCHelper::MySQLConnector.connect(host, conn_info['user'], conn_info['password'], db)
285
-
286
- assert conn.closed? == false
287
- conn.close
288
- assert conn.closed?
289
- elsif db == 'oracle'
290
- host = conn_info['url'].match(%r{//(.*?)/})[1]
291
- svc = conn_info['url'].match(%r{/([^/?]*?)(\?.*)?$})[1]
292
- conn = JDBCHelper::OracleConnector.connect(host, conn_info['user'], conn_info['password'], svc)
293
-
294
- assert conn.closed? == false
295
- conn.close
296
- assert conn.closed?
297
- end
298
- end
299
- end
300
-
301
267
  def test_setter_timestamp
302
268
  each_connection do | conn |
303
269
  # Java timestamp
@@ -0,0 +1,34 @@
1
+ require 'helper'
2
+
3
+ class TestConnectors < Test::Unit::TestCase
4
+ include JDBCHelperTestHelper
5
+
6
+ def setup
7
+ end
8
+
9
+ def teardown
10
+ end
11
+
12
+ def test_connectors
13
+ config.each do | db, conn_info |
14
+ if db == 'mysql'
15
+ host = conn_info['url'].match(%r{//(.*?)/})[1]
16
+ db = conn_info['url'].match(%r{/([^/?]*?)(\?.*)?$})[1]
17
+ conn = JDBCHelper::MySQLConnector.connect(host, conn_info['user'], conn_info['password'], db)
18
+
19
+ assert conn.closed? == false
20
+ conn.close
21
+ assert conn.closed?
22
+ elsif db == 'oracle'
23
+ host = conn_info['url'].match(%r{//(.*?)/})[1]
24
+ svc = conn_info['url'].match(%r{/([^/?]*?)(\?.*)?$})[1]
25
+ conn = JDBCHelper::OracleConnector.connect(host, conn_info['user'], conn_info['password'], svc)
26
+
27
+ assert conn.closed? == false
28
+ conn.close
29
+ assert conn.closed?
30
+ end
31
+ end
32
+ end
33
+ end
34
+
@@ -0,0 +1,221 @@
1
+ require 'helper'
2
+
3
+ class TestObjectWrapper < Test::Unit::TestCase
4
+ include JDBCHelperTestHelper
5
+
6
+ def setup
7
+ @table_name = "tmp_jdbc_helper"
8
+ end
9
+
10
+ def teardown
11
+ each_connection do |conn|
12
+ drop_table conn
13
+ end
14
+ end
15
+
16
+ def create_table conn
17
+ drop_table conn
18
+ conn.update "
19
+ create table #{@table_name} (
20
+ id int primary key,
21
+ alpha int,
22
+ beta float,
23
+ gamma varchar(100)
24
+ )
25
+ "
26
+ end
27
+
28
+ def drop_table conn
29
+ begin
30
+ conn.update "drop table #{@table_name}"
31
+ return true
32
+ rescue Exception
33
+ return false
34
+ end
35
+ end
36
+
37
+ def test_wrapper
38
+ each_connection do |conn|
39
+ # With symbol
40
+ assert_kind_of JDBCHelper::ObjectWrapper, conn.table(:some_table)
41
+ assert_instance_of JDBCHelper::TableWrapper, conn.table(:some_table)
42
+ assert_equal 'some_table', conn.table(:some_table).name
43
+
44
+ # With string
45
+ assert_kind_of JDBCHelper::ObjectWrapper, conn.table('db.table')
46
+ assert_instance_of JDBCHelper::TableWrapper, conn.table('db.table')
47
+ assert_equal 'db.table', conn.table('db.table').name
48
+
49
+ # Invalid table name
50
+ assert_raise(ArgumentError) { conn.table('table;') }
51
+ assert_raise(ArgumentError) { conn.table('table -- ') }
52
+ assert_raise(ArgumentError) { conn.table("tab'le") }
53
+ assert_raise(ArgumentError) { conn.table('tab"le') }
54
+ assert_raise(ArgumentError) { conn.table("tab`le") }
55
+
56
+ # Abstract class
57
+ assert_raise(Exception) { JDBCHelper::ObjectWrapper.new(conn, 'table') }
58
+ end
59
+ end
60
+
61
+ def insert table
62
+ params = {
63
+ :alpha => 100,
64
+ :beta => JDBCHelper::SQL.expr('0.1 + 0.2'),
65
+ :gamma => 'hello world' }
66
+
67
+ (1..100).each do |pk|
68
+ icnt = table.insert(params.merge(:id => pk))
69
+ assert_equal icnt, 1
70
+ end
71
+ end
72
+
73
+ def test_empty
74
+ each_connection do |conn|
75
+ create_table conn
76
+ table = conn.table(@table_name)
77
+
78
+ assert table.empty?
79
+ end
80
+ end
81
+
82
+ def test_insert_count
83
+ each_connection do |conn|
84
+ create_table conn
85
+ table = conn.table(@table_name)
86
+
87
+ # Count
88
+ assert_equal 0, table.count
89
+ assert table.empty?
90
+
91
+ # Insert
92
+ insert table
93
+
94
+ # Count
95
+ assert_equal 100, table.count
96
+ assert_equal 100, table.count(:alpha => 100)
97
+ assert_equal 0, table.count(:beta => nil)
98
+ end
99
+ end
100
+
101
+ # This will fail if the database doesn't support insert ignore syntax
102
+ def test_insert_ignore
103
+ each_connection do |conn|
104
+ create_table conn
105
+ table = conn.table(@table_name)
106
+ params = {
107
+ :id => 1,
108
+ :alpha => 100,
109
+ :beta => JDBCHelper::SQL.expr('0.1 + 0.2'),
110
+ :gamma => 'hello world' }
111
+
112
+ 100.times do
113
+ table.insert_ignore(params)
114
+ end
115
+
116
+ assert_equal 1, table.count
117
+ end
118
+ end
119
+
120
+ # This will fail if the database doesn't support replace syntax
121
+ def test_replace
122
+ each_connection do |conn|
123
+ create_table conn
124
+ table = conn.table(@table_name)
125
+ params = {
126
+ :id => 1,
127
+ :beta => JDBCHelper::SQL.expr('0.1 + 0.2'),
128
+ :gamma => 'hello world' }
129
+
130
+ 100.times do |i|
131
+ table.replace(params.merge(:alpha => i))
132
+ end
133
+
134
+ assert_equal 1, table.count
135
+ assert_equal 99, table.select.first.alpha
136
+ end
137
+ end
138
+
139
+ def test_select
140
+ each_connection do |conn|
141
+ create_table conn
142
+ table = conn.table(@table_name)
143
+ insert table
144
+ assert_equal 100, table.count
145
+
146
+ cnt = 0
147
+ table.select do |row|
148
+ cnt += 1
149
+ assert_equal 100, row.alpha
150
+ assert_equal 'hello world', row.gamma
151
+ end
152
+ assert_equal 100, cnt
153
+
154
+ cnt = 0
155
+ table.select(:id => 11..20) do |row|
156
+ cnt += 1
157
+ assert_equal 100, row.alpha
158
+ assert_equal 'hello world', row.gamma
159
+ end
160
+ assert_equal 10, cnt
161
+ end
162
+ end
163
+
164
+ def test_delete
165
+ each_connection do |conn|
166
+ create_table conn
167
+ table = conn.table(@table_name)
168
+ insert table
169
+
170
+ # Count
171
+ assert_equal 100, table.count
172
+
173
+ # Delete
174
+ assert_equal 10, table.delete(:id => (1...11))
175
+ assert_equal 10, table.delete(:id => (11..20))
176
+ assert_equal 1, table.delete(:id => 21)
177
+ assert_equal 4, table.delete(:id => [22, 23, 24, 25])
178
+ assert_equal 5, table.delete("id <= 30")
179
+
180
+ # Could be dangerous (XXX)
181
+ assert_equal 70, table.delete
182
+
183
+ # Count
184
+ assert_equal 0, table.count
185
+ end
186
+ end
187
+
188
+ def test_update
189
+ each_connection do |conn|
190
+ create_table conn
191
+ table = conn.table(@table_name)
192
+ insert table
193
+
194
+ assert_equal 10, table.update(:beta => 0, :where => { :id => (1..10) })
195
+ assert_equal 10, table.count(:beta => 0)
196
+
197
+ end
198
+ end
199
+
200
+ def test_truncate_table
201
+ each_connection do |conn|
202
+ create_table conn
203
+ table = conn.table(@table_name)
204
+ insert table
205
+
206
+ table.truncate_table!
207
+ assert table.empty?
208
+ end
209
+ end
210
+
211
+ def test_drop_table
212
+ each_connection do |conn|
213
+ create_table conn
214
+ table = conn.table(@table_name)
215
+ table.drop_table!
216
+
217
+ assert_equal false, drop_table(conn)
218
+ end
219
+ end
220
+ end
221
+