jdbc-helper 0.1.3 → 0.2.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.
@@ -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
+