activerecord-jdbc-adapter 0.9.0.1 → 0.9.1

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.
Files changed (49) hide show
  1. data/History.txt +31 -0
  2. data/Manifest.txt +7 -0
  3. data/README.txt +15 -2
  4. data/Rakefile +28 -30
  5. data/lib/active_record/connection_adapters/h2_adapter.rb +13 -1
  6. data/lib/active_record/connection_adapters/jdbc_adapter.rb +78 -96
  7. data/lib/jdbc_adapter/jdbc.rake +15 -5
  8. data/lib/jdbc_adapter/jdbc_adapter_internal.jar +0 -0
  9. data/lib/jdbc_adapter/jdbc_cachedb.rb +4 -4
  10. data/lib/jdbc_adapter/jdbc_db2.rb +5 -7
  11. data/lib/jdbc_adapter/jdbc_derby.rb +57 -30
  12. data/lib/jdbc_adapter/jdbc_firebird.rb +2 -2
  13. data/lib/jdbc_adapter/jdbc_hsqldb.rb +53 -46
  14. data/lib/jdbc_adapter/jdbc_informix.rb +4 -5
  15. data/lib/jdbc_adapter/jdbc_mimer.rb +2 -2
  16. data/lib/jdbc_adapter/jdbc_mssql.rb +25 -23
  17. data/lib/jdbc_adapter/jdbc_mysql.rb +20 -22
  18. data/lib/jdbc_adapter/jdbc_oracle.rb +115 -117
  19. data/lib/jdbc_adapter/jdbc_postgre.rb +129 -59
  20. data/lib/jdbc_adapter/jdbc_sqlite3.rb +149 -28
  21. data/lib/jdbc_adapter/jdbc_sybase.rb +13 -2
  22. data/lib/jdbc_adapter/missing_functionality_helper.rb +12 -3
  23. data/lib/jdbc_adapter/version.rb +1 -1
  24. data/src/java/jdbc_adapter/JdbcAdapterInternalService.java +6 -1101
  25. data/src/java/jdbc_adapter/JdbcDerbySpec.java +26 -23
  26. data/src/java/jdbc_adapter/JdbcMySQLSpec.java +79 -28
  27. data/src/java/jdbc_adapter/PostgresRubyJdbcConnection.java +35 -0
  28. data/src/java/jdbc_adapter/RubyJdbcConnection.java +1149 -0
  29. data/src/java/jdbc_adapter/SQLBlock.java +12 -3
  30. data/src/java/jdbc_adapter/Sqlite3RubyJdbcConnection.java +41 -0
  31. data/test/activerecord/connection_adapters/type_conversion_test.rb +1 -1
  32. data/test/db/derby.rb +0 -3
  33. data/test/db/h2.rb +0 -3
  34. data/test/db/hsqldb.rb +1 -4
  35. data/test/db/mysql.rb +1 -0
  36. data/test/db/oracle.rb +5 -0
  37. data/test/db/sqlite3.rb +7 -3
  38. data/test/derby_migration_test.rb +21 -0
  39. data/test/has_many_through.rb +11 -4
  40. data/test/jdbc_common.rb +13 -1
  41. data/test/models/data_types.rb +11 -1
  42. data/test/models/mixed_case.rb +20 -0
  43. data/test/mysql_multibyte_test.rb +4 -0
  44. data/test/oracle_simple_test.rb +1 -1
  45. data/test/postgres_mixed_case_test.rb +19 -0
  46. data/test/simple.rb +220 -41
  47. data/test/sqlite3_simple_test.rb +83 -0
  48. data/test/sybase_jtds_simple_test.rb +6 -0
  49. metadata +12 -10
@@ -71,8 +71,10 @@ public class JdbcDerbySpec {
71
71
  case 't': //text, timestamp, time
72
72
  if (type.equals("text")) {
73
73
  return value;
74
- } else {
75
- return rubyApi.callMethod(recv, "cast_to_time", value);
74
+ } else if (type.equals("timestamp")) {
75
+ return rubyApi.callMethod(recv.getMetaClass(), "string_to_time", value);
76
+ } else { //time
77
+ return rubyApi.callMethod(recv.getMetaClass(), "string_to_dummy_time", value);
76
78
  }
77
79
  case 'i': //integer
78
80
  case 'p': //primary key
@@ -83,7 +85,7 @@ public class JdbcDerbySpec {
83
85
  }
84
86
  case 'd': //decimal, datetime, date
85
87
  if (type.equals("datetime")) {
86
- return rubyApi.callMethod(recv, "cast_to_date_or_time", value);
88
+ return rubyApi.callMethod(recv.getMetaClass(), "string_to_time", value);
87
89
  } else if (type.equals("date")) {
88
90
  return rubyApi.callMethod(recv.getMetaClass(), "string_to_date", value);
89
91
  } else {
@@ -93,7 +95,7 @@ public class JdbcDerbySpec {
93
95
  return rubyApi.callMethod(value, "to_f");
94
96
  case 'b': //binary, boolean
95
97
  if (type.equals("binary")) {
96
- return rubyApi.callMethod(recv, "value_to_binary", value);
98
+ return rubyApi.callMethod(recv.getMetaClass(), "binary_to_string", value);
97
99
  } else {
98
100
  return rubyApi.callMethod(recv.getMetaClass(), "value_to_boolean", value);
99
101
  }
@@ -108,14 +110,14 @@ public class JdbcDerbySpec {
108
110
  IRubyObject value = args[0];
109
111
  if (args.length > 1) {
110
112
  IRubyObject col = args[1];
111
- IRubyObject type = rubyApi.callMethod(col, "type");
113
+ String type = rubyApi.callMethod(col, "type").toString();
112
114
  if (value instanceof RubyString) {
113
- if (type == runtime.newSymbol("string")) {
115
+ if (type.equals("string")) {
114
116
  return quote_string_with_surround(runtime, "'", (RubyString)value, "'");
115
- } else if (type == runtime.newSymbol("text")) {
117
+ } else if (type.equals("text")) {
116
118
  return quote_string_with_surround(runtime, "CAST('", (RubyString)value, "' AS CLOB)");
117
- } else if (type == runtime.newSymbol("binary")) {
118
- return hexquote_string_with_surround(runtime, "CAST('", (RubyString)value, "' AS BLOB)");
119
+ } else if (type.equals("binary")) {
120
+ return hexquote_string_with_surround(runtime, "CAST(X'", (RubyString)value, "' AS BLOB)");
119
121
  } else {
120
122
  // column type :integer or other numeric or date version
121
123
  if (only_digits((RubyString)value)) {
@@ -125,7 +127,7 @@ public class JdbcDerbySpec {
125
127
  }
126
128
  }
127
129
  } else if ((value instanceof RubyFloat) || (value instanceof RubyFixnum) || (value instanceof RubyBignum)) {
128
- if (type == runtime.newSymbol("string")) {
130
+ if (type.equals("string")) {
129
131
  return quote_string_with_surround(runtime, "'", RubyString.objAsString(context, value), "'");
130
132
  }
131
133
  }
@@ -155,7 +157,7 @@ public class JdbcDerbySpec {
155
157
  return quote_string_with_surround(runtime, "'", svalue, "'");
156
158
  }
157
159
  } else if (value.isNil()) {
158
- return runtime.newStringShared(NULL);
160
+ return runtime.newString(NULL);
159
161
  } else if (value instanceof RubyBoolean) {
160
162
  return (value.isTrue() ?
161
163
  (type == runtime.newSymbol(":integer")) ? runtime.newString("1") : rubyApi.callMethod(recv, "quoted_true") :
@@ -164,9 +166,7 @@ public class JdbcDerbySpec {
164
166
  return RubyString.objAsString(context, value);
165
167
  } else if(value instanceof RubyBigDecimal) {
166
168
  return rubyApi.callMethod(value, "to_s", runtime.newString("F"));
167
- } else if (rubyApi.isKindOf(value, runtime.getModule("Date"))) {
168
- return quote_string_with_surround(runtime, "'", RubyString.objAsString(context, value), "'");
169
- } else if (rubyApi.isKindOf(value, runtime.getModule("Time")) || rubyApi.isKindOf(value, runtime.getModule("DateTime"))) {
169
+ } else if (rubyApi.callMethod(value, "acts_like?", runtime.newString("date")).isTrue() || rubyApi.callMethod(value, "acts_like?", runtime.newString("time")).isTrue()) {
170
170
  return quote_string_with_surround(runtime, "'", (RubyString)(rubyApi.callMethod(recv, "quoted_date", value)), "'");
171
171
  } else {
172
172
  return quote_string_with_surround(runtime, "'", (RubyString)(rubyApi.callMethod(value, "to_yaml")), "'");
@@ -191,7 +191,7 @@ public class JdbcDerbySpec {
191
191
 
192
192
  output.append(after.getBytes());
193
193
 
194
- return runtime.newStringShared(output);
194
+ return runtime.newString(output);
195
195
  }
196
196
 
197
197
  private final static byte[] HEX = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
@@ -199,16 +199,18 @@ public class JdbcDerbySpec {
199
199
  private static IRubyObject hexquote_string_with_surround(Ruby runtime, String before, RubyString string, String after) {
200
200
  ByteList input = string.getByteList();
201
201
  ByteList output = new ByteList(before.getBytes());
202
+ int written = 0;
202
203
  for(int i = input.begin; i< input.begin + input.realSize; i++) {
203
204
  byte b1 = input.bytes[i];
204
205
  byte higher = HEX[(((char)b1)>>4)%16];
205
206
  byte lower = HEX[((char)b1)%16];
206
- if(b1 == '\'') {
207
- output.append(higher);
208
- output.append(lower);
209
- }
210
207
  output.append(higher);
211
208
  output.append(lower);
209
+ written += 2;
210
+ if(written >= 16334) { // max hex length = 16334
211
+ output.append("'||X'".getBytes());
212
+ written = 0;
213
+ }
212
214
  }
213
215
 
214
216
  output.append(after.getBytes());
@@ -284,13 +286,14 @@ public class JdbcDerbySpec {
284
286
  public static IRubyObject _execute(ThreadContext context, IRubyObject recv, IRubyObject[] args) throws SQLException, java.io.IOException {
285
287
  Ruby runtime = recv.getRuntime();
286
288
  try {
287
- IRubyObject conn = rubyApi.getInstanceVariable(recv, "@connection");
289
+ // TODO: Ouch....this looks fragile
290
+ RubyJdbcConnection conn = (RubyJdbcConnection) rubyApi.getInstanceVariable(recv, "@connection");
288
291
  String sql = args[0].toString().trim().toLowerCase();
289
292
  if (sql.charAt(0) == '(') {
290
293
  sql = sql.substring(1).trim();
291
294
  }
292
295
  if (sql.startsWith("insert")) {
293
- return JdbcAdapterInternalService.execute_insert(conn, args[0]);
296
+ return conn.execute_insert(context, args[0]);
294
297
  } else if (sql.startsWith("select") || sql.startsWith("show")) {
295
298
  IRubyObject offset = rubyApi.getInstanceVariable(recv, "@offset");
296
299
  if(offset == null || offset.isNil()) {
@@ -307,7 +310,7 @@ public class JdbcDerbySpec {
307
310
  range = RubyRange.newRange(runtime, context, offset, v1, true);
308
311
  max = rubyApi.callMethod(v1, "+", RubyFixnum.one(runtime));
309
312
  }
310
- IRubyObject result = JdbcAdapterInternalService.execute_query(conn, new IRubyObject[]{args[0], max});
313
+ IRubyObject result = conn.execute_query(context, args[0], max);
311
314
  IRubyObject ret = rubyApi.callMethod(result, "[]", range);
312
315
  if (ret.isNil()) {
313
316
  return runtime.newArray();
@@ -315,7 +318,7 @@ public class JdbcDerbySpec {
315
318
  return ret;
316
319
  }
317
320
  } else {
318
- return JdbcAdapterInternalService.execute_update(conn, args[0]);
321
+ return conn.execute_update(context, args[0]);
319
322
  }
320
323
  } finally {
321
324
  rubyApi.setInstanceVariable(recv, "@limit", runtime.getNil());
@@ -1,5 +1,5 @@
1
1
  /***** BEGIN LICENSE BLOCK *****
2
- * Copyright (c) 2006-2007 Nick Sieger <nick@nicksieger.com>
2
+ * Copyright (c) 2006-2009 Nick Sieger <nick@nicksieger.com>
3
3
  * Copyright (c) 2006-2007 Ola Bini <ola.bini@gmail.com>
4
4
  *
5
5
  * Permission is hereby granted, free of charge, to any person obtaining
@@ -24,11 +24,13 @@
24
24
 
25
25
  package jdbc_adapter;
26
26
 
27
- import org.jruby.Ruby;
27
+ import java.sql.Connection;
28
+
28
29
  import org.jruby.RubyModule;
29
30
  import org.jruby.RubyString;
30
31
 
31
32
  import org.jruby.anno.JRubyMethod;
33
+ import org.jruby.runtime.ThreadContext;
32
34
  import org.jruby.runtime.builtin.IRubyObject;
33
35
 
34
36
  import org.jruby.util.ByteList;
@@ -39,23 +41,27 @@ public class JdbcMySQLSpec {
39
41
  mysql.defineAnnotatedMethods(JdbcMySQLSpec.class);
40
42
  }
41
43
 
42
- private final static ByteList ZERO = new ByteList(new byte[]{'\\','0'});
43
- private final static ByteList NEWLINE = new ByteList(new byte[]{'\\','n'});
44
- private final static ByteList CARRIAGE = new ByteList(new byte[]{'\\','r'});
45
- private final static ByteList ZED = new ByteList(new byte[]{'\\','Z'});
46
- private final static ByteList DBL = new ByteList(new byte[]{'\\','"'});
47
- private final static ByteList SINGLE = new ByteList(new byte[]{'\\','\''});
48
- private final static ByteList ESCAPE = new ByteList(new byte[]{'\\','\\'});
49
-
50
- @JRubyMethod(name = "quote_string", required = 1)
51
- public static IRubyObject quote_string(IRubyObject recv, IRubyObject string) {
52
- ByteList bl = ((RubyString) string).getByteList();
53
- ByteList blNew = new ByteList();
54
- int startOfExtend = bl.begin;
44
+ private final static byte BACKQUOTE = '`';
45
+ private final static byte[] QUOTED_DOT = new byte[] {'`', '.', '`'};
46
+
47
+ private final static byte[] ZERO = new byte[] {'\\','0'};
48
+ private final static byte[] NEWLINE = new byte[] {'\\','n'};
49
+ private final static byte[] CARRIAGE = new byte[] {'\\','r'};
50
+ private final static byte[] ZED = new byte[] {'\\','Z'};
51
+ private final static byte[] DBL = new byte[] {'\\','"'};
52
+ private final static byte[] SINGLE = new byte[] {'\\','\''};
53
+ private final static byte[] ESCAPE = new byte[] {'\\','\\'};
54
+
55
+ @JRubyMethod(name = "quote_string", required = 1, frame=false)
56
+ public static IRubyObject quote_string(ThreadContext context, IRubyObject recv, IRubyObject string) {
57
+ ByteList bytes = ((RubyString) string).getByteList();
58
+ ByteList newBytes = new ByteList();
55
59
 
56
- for(int i = bl.begin; i < bl.begin + bl.realSize; i++) {
57
- ByteList rep = null;
58
- switch (bl.bytes[i]) {
60
+ newBytes.append(bytes);
61
+
62
+ for(int i = newBytes.begin; i < newBytes.begin + newBytes.realSize; i++) {
63
+ byte[] rep = null;
64
+ switch (newBytes.bytes[i]) {
59
65
  case 0: rep = ZERO; break;
60
66
  case '\n': rep = NEWLINE; break;
61
67
  case '\r': rep = CARRIAGE; break;
@@ -63,20 +69,65 @@ public class JdbcMySQLSpec {
63
69
  case '"': rep = DBL; break;
64
70
  case '\'': rep = SINGLE; break;
65
71
  case '\\': rep = ESCAPE; break;
66
- default: continue;
67
72
  }
68
- if(i > startOfExtend)
69
- blNew.append(bl, startOfExtend-bl.begin, i-startOfExtend);
70
- blNew.append(rep, 0, 2);
71
- startOfExtend = i+1;
73
+
74
+ if (rep != null) {
75
+ newBytes.replace(i, 1, rep);
76
+ i += rep.length - 1; // We subtract one since for loop already adds one
77
+ }
72
78
  }
79
+
73
80
  // Nothing changed, can return original
74
- if (startOfExtend == bl.begin) {
75
- return string;
81
+ if (newBytes.length() == bytes.length()) return string;
82
+
83
+ return context.getRuntime().newString(newBytes);
84
+ }
85
+
86
+ @JRubyMethod(name = "quote_column_name", frame=false)
87
+ public static IRubyObject quote_column_name(ThreadContext context, IRubyObject recv, IRubyObject arg) {
88
+ ByteList bytes = arg.asString().getByteList();
89
+ ByteList newBytes = new ByteList();
90
+
91
+ newBytes.insert(0, BACKQUOTE);
92
+ newBytes.append(bytes);
93
+ newBytes.append(BACKQUOTE);
94
+
95
+ return context.getRuntime().newString(newBytes);
96
+ }
97
+
98
+ @JRubyMethod(name = "quote_table_name", frame=false)
99
+ public static IRubyObject quote_table_name(ThreadContext context, IRubyObject recv, IRubyObject arg) {
100
+ ByteList bytes = arg.asString().getByteList();
101
+ ByteList newBytes = new ByteList();
102
+
103
+ newBytes.insert(0, BACKQUOTE);
104
+ newBytes.append(bytes);
105
+ int i = 0;
106
+ while ((i = newBytes.indexOf('.')) != -1) {
107
+ newBytes.replace(i, 1, QUOTED_DOT);
76
108
  }
77
- if (bl.begin + bl.realSize > startOfExtend)
78
- blNew.append(bl, startOfExtend-bl.begin, bl.begin + bl.realSize - startOfExtend);
109
+ newBytes.append(BACKQUOTE);
79
110
 
80
- return recv.getRuntime().newStringShared(blNew);
111
+ return context.getRuntime().newString(newBytes);
112
+ }
113
+
114
+ /**
115
+ * HACK HACK HACK See http://bugs.mysql.com/bug.php?id=36565
116
+ * MySQL's statement cancel timer can cause memory leaks, so cancel it
117
+ * if we loaded MySQL classes from the same classloader as JRuby
118
+ */
119
+ @JRubyMethod(module = true, frame = false)
120
+ public static IRubyObject kill_cancel_timer(ThreadContext context, IRubyObject recv, IRubyObject raw_connection) {
121
+ Connection conn = (Connection) raw_connection.dataGetStruct();
122
+ if (conn != null && conn.getClass().getClassLoader() == recv.getRuntime().getJRubyClassLoader()) {
123
+ try {
124
+ java.lang.reflect.Field f = conn.getClass().getDeclaredField("cancelTimer");
125
+ f.setAccessible(true);
126
+ java.util.Timer timer = (java.util.Timer) f.get(null);
127
+ timer.cancel();
128
+ } catch (Exception e) {
129
+ }
130
+ }
131
+ return recv.getRuntime().getNil();
81
132
  }
82
133
  }
@@ -0,0 +1,35 @@
1
+ /*
2
+ * To change this template, choose Tools | Templates
3
+ * and open the template in the editor.
4
+ */
5
+
6
+ package jdbc_adapter;
7
+
8
+ import org.jruby.Ruby;
9
+ import org.jruby.RubyClass;
10
+ import org.jruby.runtime.ObjectAllocator;
11
+ import org.jruby.runtime.builtin.IRubyObject;
12
+
13
+ /**
14
+ *
15
+ * @author enebo
16
+ */
17
+ public class PostgresRubyJdbcConnection extends RubyJdbcConnection {
18
+ protected PostgresRubyJdbcConnection(Ruby runtime, RubyClass metaClass) {
19
+ super(runtime, metaClass);
20
+ }
21
+
22
+ public static RubyClass createPostgresJdbcConnectionClass(Ruby runtime, RubyClass jdbcConnection) {
23
+ RubyClass clazz = RubyJdbcConnection.getConnectionAdapters(runtime).defineClassUnder("PostgresJdbcConnection",
24
+ jdbcConnection, POSTGRES_JDBCCONNECTION_ALLOCATOR);
25
+ clazz.defineAnnotatedMethods(PostgresRubyJdbcConnection.class);
26
+
27
+ return clazz;
28
+ }
29
+
30
+ private static ObjectAllocator POSTGRES_JDBCCONNECTION_ALLOCATOR = new ObjectAllocator() {
31
+ public IRubyObject allocate(Ruby runtime, RubyClass klass) {
32
+ return new PostgresRubyJdbcConnection(runtime, klass);
33
+ }
34
+ };
35
+ }
@@ -0,0 +1,1149 @@
1
+ /*
2
+ **** BEGIN LICENSE BLOCK *****
3
+ * Copyright (c) 2006-2009 Nick Sieger <nick@nicksieger.com>
4
+ * Copyright (c) 2006-2007 Ola Bini <ola.bini@gmail.com>
5
+ * Copyright (c) 2008-2009 Thomas E Enebo <enebo@acm.org>
6
+ *
7
+ * Permission is hereby granted, free of charge, to any person obtaining
8
+ * a copy of this software and associated documentation files (the
9
+ * "Software"), to deal in the Software without restriction, including
10
+ * without limitation the rights to use, copy, modify, merge, publish,
11
+ * distribute, sublicense, and/or sell copies of the Software, and to
12
+ * permit persons to whom the Software is furnished to do so, subject to
13
+ * the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be
16
+ * included in all copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+ ***** END LICENSE BLOCK *****/
26
+ package jdbc_adapter;
27
+
28
+ import java.io.ByteArrayInputStream;
29
+ import java.io.IOException;
30
+ import java.io.InputStream;
31
+ import java.io.Reader;
32
+ import java.io.StringReader;
33
+ import org.jruby.Ruby;
34
+ import org.jruby.RubyClass;
35
+ import org.jruby.RubyModule;
36
+ import org.jruby.RubyObject;
37
+ import org.jruby.RubyObjectAdapter;
38
+ import org.jruby.anno.JRubyMethod;
39
+ import org.jruby.javasupport.JavaEmbedUtils;
40
+ import org.jruby.runtime.ObjectAllocator;
41
+ import org.jruby.runtime.ThreadContext;
42
+ import org.jruby.runtime.builtin.IRubyObject;
43
+ import org.jruby.util.ByteList;
44
+
45
+ import java.sql.Connection;
46
+ import java.sql.DatabaseMetaData;
47
+ import java.sql.PreparedStatement;
48
+ import java.sql.ResultSet;
49
+ import java.sql.ResultSetMetaData;
50
+ import java.sql.SQLException;
51
+ import java.sql.Statement;
52
+ import java.sql.Timestamp;
53
+ import java.sql.Types;
54
+ import java.text.DateFormat;
55
+ import java.text.SimpleDateFormat;
56
+ import java.util.ArrayList;
57
+ import java.util.Calendar;
58
+ import java.util.Date;
59
+ import java.util.List;
60
+ import org.jruby.RubyArray;
61
+ import org.jruby.RubyHash;
62
+ import org.jruby.RubyNumeric;
63
+ import org.jruby.RubyString;
64
+ import org.jruby.RubySymbol;
65
+ import org.jruby.RubyTime;
66
+ import org.jruby.exceptions.RaiseException;
67
+ import org.jruby.javasupport.Java;
68
+ import org.jruby.javasupport.JavaObject;
69
+ import org.jruby.runtime.Arity;
70
+ import org.jruby.runtime.Block;
71
+
72
+ /**
73
+ * Part of our ActiveRecord::ConnectionAdapters::Connection impl.
74
+ */
75
+ public class RubyJdbcConnection extends RubyObject {
76
+ private static final String[] TABLE_TYPE = new String[]{"TABLE"};
77
+
78
+ private static RubyObjectAdapter rubyApi;
79
+
80
+ protected RubyJdbcConnection(Ruby runtime, RubyClass metaClass) {
81
+ super(runtime, metaClass);
82
+ }
83
+
84
+ public static RubyClass createJdbcConnectionClass(Ruby runtime) {
85
+ RubyClass jdbcConnection = getConnectionAdapters(runtime).defineClassUnder("JdbcConnection",
86
+ runtime.getObject(), JDBCCONNECTION_ALLOCATOR);
87
+ jdbcConnection.defineAnnotatedMethods(RubyJdbcConnection.class);
88
+
89
+ rubyApi = JavaEmbedUtils.newObjectAdapter();
90
+
91
+ return jdbcConnection;
92
+ }
93
+
94
+ private static ObjectAllocator JDBCCONNECTION_ALLOCATOR = new ObjectAllocator() {
95
+ public IRubyObject allocate(Ruby runtime, RubyClass klass) {
96
+ return new RubyJdbcConnection(runtime, klass);
97
+ }
98
+ };
99
+
100
+ protected static RubyModule getConnectionAdapters(Ruby runtime) {
101
+ return (RubyModule) runtime.getModule("ActiveRecord").getConstant("ConnectionAdapters");
102
+ }
103
+
104
+ @JRubyMethod(name = "begin")
105
+ public IRubyObject begin(ThreadContext context) throws SQLException {
106
+ getConnection(true).setAutoCommit(false);
107
+
108
+ return context.getRuntime().getNil();
109
+ }
110
+
111
+ @JRubyMethod(name = {"columns", "columns_internal"}, required = 1, optional = 2)
112
+ public IRubyObject columns_internal(final ThreadContext context, final IRubyObject[] args)
113
+ throws SQLException, IOException {
114
+ return (IRubyObject) withConnectionAndRetry(context, new SQLBlock() {
115
+ public Object call(Connection c) throws SQLException {
116
+ ResultSet results = null;
117
+ try {
118
+ String table_name = rubyApi.convertToRubyString(args[0]).getUnicodeValue();
119
+ String schemaName = null;
120
+
121
+ int index = table_name.indexOf(".");
122
+ if(index != -1) {
123
+ schemaName = table_name.substring(0, index);
124
+ table_name = table_name.substring(index + 1);
125
+ }
126
+
127
+ DatabaseMetaData metadata = c.getMetaData();
128
+
129
+ if(args.length > 2) schemaName = args[2].toString();
130
+
131
+ if (schemaName != null) schemaName = caseConvertIdentifierForJdbc(metadata, schemaName);
132
+ table_name = caseConvertIdentifierForJdbc(metadata, table_name);
133
+
134
+ String[] tableTypes = new String[]{"TABLE","VIEW","SYNONYM"};
135
+ RubyArray matchingTables = (RubyArray) tableLookupBlock(context.getRuntime(),
136
+ c.getCatalog(), schemaName, table_name, tableTypes, false).call(c);
137
+ if (matchingTables.isEmpty()) {
138
+ throw new SQLException("Table " + table_name + " does not exist");
139
+ }
140
+
141
+ results = metadata.getColumns(c.getCatalog(),schemaName,table_name,null);
142
+ return unmarshal_columns(context, metadata, results);
143
+ } finally {
144
+ close(results);
145
+ }
146
+ }
147
+ });
148
+ }
149
+
150
+ @JRubyMethod(name = "commit")
151
+ public IRubyObject commit(ThreadContext context) throws SQLException {
152
+ Connection connection = getConnection(true);
153
+
154
+ if (!connection.getAutoCommit()) {
155
+ try {
156
+ connection.commit();
157
+ } finally {
158
+ connection.setAutoCommit(true);
159
+ }
160
+ }
161
+
162
+ return context.getRuntime().getNil();
163
+ }
164
+
165
+ @JRubyMethod(name = "connection", frame = false)
166
+ public IRubyObject connection() {
167
+ if (getConnection() == null) reconnect();
168
+
169
+ return getInstanceVariable("@connection");
170
+ }
171
+
172
+ @JRubyMethod(name = "database_name", frame = false)
173
+ public IRubyObject database_name(ThreadContext context) throws SQLException {
174
+ Connection connection = getConnection(true);
175
+ String name = connection.getCatalog();
176
+
177
+ if (null == name) {
178
+ name = connection.getMetaData().getUserName();
179
+
180
+ if (null == name) name = "db1";
181
+ }
182
+
183
+ return context.getRuntime().newString(name);
184
+ }
185
+
186
+ @JRubyMethod(name = "disconnect!", frame = false)
187
+ public IRubyObject disconnect() {
188
+ return setConnection(null);
189
+ }
190
+
191
+ @JRubyMethod(name = "execute_id_insert", required = 2)
192
+ public IRubyObject execute_id_insert(ThreadContext context, final IRubyObject sql,
193
+ final IRubyObject id) throws SQLException {
194
+ return (IRubyObject) withConnectionAndRetry(context, new SQLBlock() {
195
+ public Object call(Connection c) throws SQLException {
196
+ PreparedStatement ps = c.prepareStatement(rubyApi.convertToRubyString(sql).getUnicodeValue());
197
+ try {
198
+ ps.setLong(1, RubyNumeric.fix2long(id));
199
+ ps.executeUpdate();
200
+ } finally {
201
+ close(ps);
202
+ }
203
+ return id;
204
+ }
205
+ });
206
+ }
207
+
208
+ @JRubyMethod(name = "execute_insert", required = 1)
209
+ public IRubyObject execute_insert(final ThreadContext context, final IRubyObject sql)
210
+ throws SQLException {
211
+ return (IRubyObject) withConnectionAndRetry(context, new SQLBlock() {
212
+ public Object call(Connection c) throws SQLException {
213
+ Statement stmt = null;
214
+ try {
215
+ stmt = c.createStatement();
216
+ stmt.executeUpdate(rubyApi.convertToRubyString(sql).getUnicodeValue(), Statement.RETURN_GENERATED_KEYS);
217
+ return unmarshal_id_result(context.getRuntime(), stmt.getGeneratedKeys());
218
+ } finally {
219
+ close(stmt);
220
+ }
221
+ }
222
+ });
223
+ }
224
+
225
+ @JRubyMethod(name = "execute_query", required = 1)
226
+ public IRubyObject execute_query(final ThreadContext context, IRubyObject _sql)
227
+ throws SQLException, IOException {
228
+ String sql = rubyApi.convertToRubyString(_sql).getUnicodeValue();
229
+
230
+ return executeQuery(context, sql, 0);
231
+ }
232
+
233
+ @JRubyMethod(name = "execute_query", required = 2)
234
+ public IRubyObject execute_query(final ThreadContext context, IRubyObject _sql,
235
+ IRubyObject _maxRows) throws SQLException, IOException {
236
+ String sql = rubyApi.convertToRubyString(_sql).getUnicodeValue();
237
+ int maxrows = RubyNumeric.fix2int(_maxRows);
238
+
239
+ return executeQuery(context, sql, maxrows);
240
+ }
241
+
242
+ protected IRubyObject executeQuery(final ThreadContext context, final String query, final int maxRows) {
243
+ return (IRubyObject) withConnectionAndRetry(context, new SQLBlock() {
244
+ public Object call(Connection c) throws SQLException {
245
+ Statement stmt = null;
246
+ try {
247
+ DatabaseMetaData metadata = c.getMetaData();
248
+ stmt = c.createStatement();
249
+ stmt.setMaxRows(maxRows);
250
+ return unmarshalResult(context, metadata, stmt.executeQuery(query), false);
251
+ } finally {
252
+ close(stmt);
253
+ }
254
+ }
255
+ });
256
+ }
257
+
258
+ @JRubyMethod(name = "execute_update", required = 1)
259
+ public IRubyObject execute_update(final ThreadContext context, final IRubyObject sql)
260
+ throws SQLException {
261
+ return (IRubyObject) withConnectionAndRetry(context, new SQLBlock() {
262
+ public Object call(Connection c) throws SQLException {
263
+ Statement stmt = null;
264
+ try {
265
+ stmt = c.createStatement();
266
+ return context.getRuntime().newFixnum((long)stmt.executeUpdate(rubyApi.convertToRubyString(sql).getUnicodeValue()));
267
+ } finally {
268
+ close(stmt);
269
+ }
270
+ }
271
+ });
272
+ }
273
+
274
+ @JRubyMethod(name = "indexes")
275
+ public IRubyObject indexes(ThreadContext context, IRubyObject tableName, IRubyObject name, IRubyObject schemaName) {
276
+ return indexes(context, toStringOrNull(tableName), toStringOrNull(name), toStringOrNull(schemaName));
277
+ }
278
+
279
+ private static final int INDEX_TABLE_NAME = 3;
280
+ private static final int INDEX_NON_UNIQUE = 4;
281
+ private static final int INDEX_NAME = 6;
282
+ private static final int INDEX_COLUMN_NAME = 9;
283
+
284
+ /**
285
+ * Default JDBC introspection for index metadata on the JdbcConnection.
286
+ *
287
+ * JDBC index metadata is denormalized (multiple rows may be returned for
288
+ * one index, one row per column in the index), so a simple block-based
289
+ * filter like that used for tables doesn't really work here. Callers
290
+ * should filter the return from this method instead.
291
+ */
292
+ protected IRubyObject indexes(final ThreadContext context, final String tableNameArg, final String name, final String schemaNameArg) {
293
+ return (IRubyObject) withConnectionAndRetry(context, new SQLBlock() {
294
+ public Object call(Connection c) throws SQLException {
295
+ Ruby runtime = context.getRuntime();
296
+ DatabaseMetaData metadata = c.getMetaData();
297
+ String tableName = caseConvertIdentifierForJdbc(metadata, tableNameArg);
298
+ String schemaName = caseConvertIdentifierForJdbc(metadata, schemaNameArg);
299
+
300
+ ResultSet resultSet = null;
301
+ List indexes = new ArrayList();
302
+ try {
303
+ resultSet = metadata.getIndexInfo(null, schemaName, tableName, false, false);
304
+ List primaryKeys = primaryKeys(context, tableName);
305
+ String currentIndex = null;
306
+ RubyModule indexDefinitionClass = getConnectionAdapters(runtime).getClass("IndexDefinition");
307
+
308
+ while (resultSet.next()) {
309
+ String indexName = resultSet.getString(INDEX_NAME);
310
+
311
+ if (indexName == null) continue;
312
+
313
+ indexName = caseConvertIdentifierForRails(metadata, indexName);
314
+
315
+ RubyString columnName = RubyString.newUnicodeString(runtime, caseConvertIdentifierForRails(metadata, resultSet.getString(INDEX_COLUMN_NAME)));
316
+
317
+ if (primaryKeys.contains(columnName)) continue;
318
+
319
+ // We are working on a new index
320
+ if (!indexName.equals(currentIndex)) {
321
+ currentIndex = indexName;
322
+
323
+ tableName = caseConvertIdentifierForRails(metadata, resultSet.getString(INDEX_TABLE_NAME));
324
+ boolean nonUnique = resultSet.getBoolean(INDEX_NON_UNIQUE);
325
+
326
+ IRubyObject indexDefinition = indexDefinitionClass.callMethod(context, "new",
327
+ new IRubyObject[] {
328
+ RubyString.newUnicodeString(runtime, tableName),
329
+ RubyString.newUnicodeString(runtime, indexName),
330
+ runtime.newBoolean(!nonUnique),
331
+ runtime.newArray()
332
+ });
333
+
334
+ // empty list for column names, we'll add to that in just a bit
335
+ indexes.add(indexDefinition);
336
+ }
337
+
338
+ // One or more columns can be associated with an index
339
+ IRubyObject lastIndex = (IRubyObject) indexes.get(indexes.size() - 1);
340
+
341
+ if (lastIndex != null) {
342
+ lastIndex.callMethod(context, "columns").callMethod(context, "<<", columnName);
343
+ }
344
+ }
345
+
346
+ return runtime.newArray(indexes);
347
+ } finally {
348
+ close(resultSet);
349
+ }
350
+ }
351
+ });
352
+ }
353
+
354
+ @JRubyMethod(name = "insert?", required = 1, meta = true, frame = false)
355
+ public static IRubyObject insert_p(ThreadContext context, IRubyObject recv, IRubyObject _sql) {
356
+ ByteList sql = rubyApi.convertToRubyString(_sql).getByteList();
357
+
358
+ return context.getRuntime().newBoolean(startsWithNoCaseCmp(sql, INSERT));
359
+ }
360
+
361
+ /*
362
+ * sql, values, types, name = nil, pk = nil, id_value = nil, sequence_name = nil
363
+ */
364
+ @JRubyMethod(name = "insert_bind", required = 3, rest = true)
365
+ public IRubyObject insert_bind(final ThreadContext context, final IRubyObject[] args) throws SQLException {
366
+ final Ruby runtime = context.getRuntime();
367
+ return (IRubyObject) withConnectionAndRetry(context, new SQLBlock() {
368
+ public Object call(Connection c) throws SQLException {
369
+ PreparedStatement ps = null;
370
+ try {
371
+ ps = c.prepareStatement(rubyApi.convertToRubyString(args[0]).toString(), Statement.RETURN_GENERATED_KEYS);
372
+ setValuesOnPS(ps, context, args[1], args[2]);
373
+ ps.executeUpdate();
374
+ return unmarshal_id_result(runtime, ps.getGeneratedKeys());
375
+ } finally {
376
+ close(ps);
377
+ }
378
+ }
379
+ });
380
+ }
381
+
382
+ @JRubyMethod(name = "native_database_types", frame = false)
383
+ public IRubyObject native_database_types() {
384
+ return getInstanceVariable("@native_database_types");
385
+ }
386
+
387
+
388
+ @JRubyMethod(name = "primary_keys", required = 1)
389
+ public IRubyObject primary_keys(ThreadContext context, IRubyObject tableName) throws SQLException {
390
+ return context.getRuntime().newArray(primaryKeys(context, tableName.toString()));
391
+ }
392
+
393
+ protected List primaryKeys(final ThreadContext context, final String tableNameArg) {
394
+ return (List) withConnectionAndRetry(context, new SQLBlock() {
395
+ public Object call(Connection c) throws SQLException {
396
+ Ruby runtime = context.getRuntime();
397
+ DatabaseMetaData metadata = c.getMetaData();
398
+ String tableName = caseConvertIdentifierForJdbc(metadata, tableNameArg);
399
+ ResultSet resultSet = null;
400
+ List keyNames = new ArrayList();
401
+ try {
402
+ resultSet = metadata.getPrimaryKeys(null, null, tableName);
403
+
404
+ while (resultSet.next()) {
405
+ keyNames.add(RubyString.newUnicodeString(runtime,
406
+ caseConvertIdentifierForRails(metadata, resultSet.getString(4))));
407
+ }
408
+ } finally {
409
+ close(resultSet);
410
+ }
411
+
412
+ return keyNames;
413
+ }
414
+ });
415
+ }
416
+
417
+ @JRubyMethod(name = "reconnect!")
418
+ public IRubyObject reconnect() {
419
+ return setConnection(getConnectionFactory().newConnection());
420
+ }
421
+
422
+ @JRubyMethod(name = "rollback")
423
+ public IRubyObject rollback(ThreadContext context) throws SQLException {
424
+ Connection connection = getConnection(true);
425
+
426
+ if (!connection.getAutoCommit()) {
427
+ try {
428
+ connection.rollback();
429
+ } finally {
430
+ connection.setAutoCommit(true);
431
+ }
432
+ }
433
+
434
+ return context.getRuntime().getNil();
435
+ }
436
+
437
+ @JRubyMethod(name = "select?", required = 1, meta = true, frame = false)
438
+ public static IRubyObject select_p(ThreadContext context, IRubyObject recv, IRubyObject _sql) {
439
+ ByteList sql = rubyApi.convertToRubyString(_sql).getByteList();
440
+
441
+ return context.getRuntime().newBoolean(startsWithNoCaseCmp(sql, SELECT) ||
442
+ startsWithNoCaseCmp(sql, SHOW) || startsWithNoCaseCmp(sql, CALL));
443
+ }
444
+
445
+ @JRubyMethod(name = "set_native_database_types")
446
+ public IRubyObject set_native_database_types(ThreadContext context) throws SQLException, IOException {
447
+ Ruby runtime = context.getRuntime();
448
+ DatabaseMetaData metadata = getConnection(true).getMetaData();
449
+ IRubyObject types = unmarshalResult(context, metadata, metadata.getTypeInfo(), true);
450
+ IRubyObject typeConverter = getConnectionAdapters(runtime).getConstant("JdbcTypeConverter");
451
+ IRubyObject value = rubyApi.callMethod(rubyApi.callMethod(typeConverter, "new", types), "choose_best_types");
452
+ setInstanceVariable("@native_types", value);
453
+
454
+ return runtime.getNil();
455
+ }
456
+
457
+ @JRubyMethod(name = "tables")
458
+ public IRubyObject tables(ThreadContext context) {
459
+ return tables(context, null, null, null, TABLE_TYPE);
460
+ }
461
+
462
+ @JRubyMethod(name = "tables")
463
+ public IRubyObject tables(ThreadContext context, IRubyObject catalog) {
464
+ return tables(context, toStringOrNull(catalog), null, null, TABLE_TYPE);
465
+ }
466
+
467
+ @JRubyMethod(name = "tables")
468
+ public IRubyObject tables(ThreadContext context, IRubyObject catalog, IRubyObject schemaPattern) {
469
+ return tables(context, toStringOrNull(catalog), toStringOrNull(schemaPattern), null, TABLE_TYPE);
470
+ }
471
+
472
+ @JRubyMethod(name = "tables")
473
+ public IRubyObject tables(ThreadContext context, IRubyObject catalog, IRubyObject schemaPattern, IRubyObject tablePattern) {
474
+ return tables(context, toStringOrNull(catalog), toStringOrNull(schemaPattern), toStringOrNull(tablePattern), TABLE_TYPE);
475
+ }
476
+
477
+ @JRubyMethod(name = "tables", required = 4, rest = true)
478
+ public IRubyObject tables(ThreadContext context, IRubyObject[] args) {
479
+ return tables(context, toStringOrNull(args[0]), toStringOrNull(args[1]), toStringOrNull(args[2]), getTypes(args[3]));
480
+ }
481
+
482
+ protected IRubyObject tables(ThreadContext context, String catalog, String schemaPattern, String tablePattern, String[] types) {
483
+ return (IRubyObject) withConnectionAndRetry(context, tableLookupBlock(context.getRuntime(), catalog, schemaPattern, tablePattern, types, false));
484
+ }
485
+
486
+ /*
487
+ * sql, values, types, name = nil
488
+ */
489
+ @JRubyMethod(name = "update_bind", required = 3, rest = true)
490
+ public IRubyObject update_bind(final ThreadContext context, final IRubyObject[] args) throws SQLException {
491
+ final Ruby runtime = context.getRuntime();
492
+ Arity.checkArgumentCount(runtime, args, 3, 4);
493
+ return (IRubyObject) withConnectionAndRetry(context, new SQLBlock() {
494
+ public Object call(Connection c) throws SQLException {
495
+ PreparedStatement ps = null;
496
+ try {
497
+ ps = c.prepareStatement(rubyApi.convertToRubyString(args[0]).toString());
498
+ setValuesOnPS(ps, context, args[1], args[2]);
499
+ ps.executeUpdate();
500
+ } finally {
501
+ close(ps);
502
+ }
503
+ return runtime.getNil();
504
+ }
505
+ });
506
+ }
507
+
508
+ @JRubyMethod(name = "with_connection_retry_guard", frame = true)
509
+ public IRubyObject with_connection_retry_guard(final ThreadContext context, final Block block) {
510
+ return (IRubyObject) withConnectionAndRetry(context, new SQLBlock() {
511
+ public Object call(Connection c) throws SQLException {
512
+ return block.call(context, new IRubyObject[] { wrappedConnection(c) });
513
+ }
514
+ });
515
+ }
516
+
517
+ /*
518
+ * (is binary?, colname, tablename, primary key, id, value)
519
+ */
520
+ @JRubyMethod(name = "write_large_object", required = 6)
521
+ public IRubyObject write_large_object(ThreadContext context, final IRubyObject[] args)
522
+ throws SQLException, IOException {
523
+ final Ruby runtime = context.getRuntime();
524
+ return (IRubyObject) withConnectionAndRetry(context, new SQLBlock() {
525
+ public Object call(Connection c) throws SQLException {
526
+ String sql = "UPDATE " + rubyApi.convertToRubyString(args[2])
527
+ + " SET " + rubyApi.convertToRubyString(args[1])
528
+ + " = ? WHERE " + rubyApi.convertToRubyString(args[3])
529
+ + "=" + rubyApi.convertToRubyString(args[4]);
530
+ PreparedStatement ps = null;
531
+ try {
532
+ ps = c.prepareStatement(sql);
533
+ if (args[0].isTrue()) { // binary
534
+ ByteList outp = rubyApi.convertToRubyString(args[5]).getByteList();
535
+ ps.setBinaryStream(1, new ByteArrayInputStream(outp.bytes,
536
+ outp.begin, outp.realSize), outp.realSize);
537
+ } else { // clob
538
+ String ss = rubyApi.convertToRubyString(args[5]).getUnicodeValue();
539
+ ps.setCharacterStream(1, new StringReader(ss), ss.length());
540
+ }
541
+ ps.executeUpdate();
542
+ } finally {
543
+ close(ps);
544
+ }
545
+ return runtime.getNil();
546
+ }
547
+ });
548
+ }
549
+
550
+ /**
551
+ * Convert an identifier coming back from the database to a case which Rails is expecting.
552
+ *
553
+ * Assumption: Rails identifiers will be quoted for mixed or will stay mixed
554
+ * as identifier names in Rails itself. Otherwise, they expect identifiers to
555
+ * be lower-case. Databases which store identifiers uppercase should be made
556
+ * lower-case.
557
+ *
558
+ * Assumption 2: It is always safe to convert all upper case names since it appears that
559
+ * some adapters do not report StoresUpper/Lower/Mixed correctly (am I right postgres/mysql?).
560
+ */
561
+ public static String caseConvertIdentifierForRails(DatabaseMetaData metadata, String value)
562
+ throws SQLException {
563
+ if (value == null) return null;
564
+
565
+ return metadata.storesUpperCaseIdentifiers() ? value.toLowerCase() : value;
566
+ }
567
+
568
+ /**
569
+ * Convert an identifier destined for a method which cares about the databases internal
570
+ * storage case. Methods like DatabaseMetaData.getPrimaryKeys() needs the table name to match
571
+ * the internal storage name. Arbtrary queries and the like DO NOT need to do this.
572
+ */
573
+ public static String caseConvertIdentifierForJdbc(DatabaseMetaData metadata, String value)
574
+ throws SQLException {
575
+ if (value == null) return null;
576
+
577
+ if (metadata.storesUpperCaseIdentifiers()) {
578
+ return value.toUpperCase();
579
+ } else if (metadata.storesLowerCaseIdentifiers()) {
580
+ return value.toLowerCase();
581
+ }
582
+
583
+ return value;
584
+ }
585
+
586
+ // helpers
587
+ protected static void close(Connection connection) {
588
+ if (connection != null) {
589
+ try {
590
+ connection.close();
591
+ } catch(Exception e) {}
592
+ }
593
+ }
594
+
595
+ public static void close(ResultSet resultSet) {
596
+ if (resultSet != null) {
597
+ try {
598
+ resultSet.close();
599
+ } catch(Exception e) {}
600
+ }
601
+ }
602
+
603
+ public static void close(Statement statement) {
604
+ if (statement != null) {
605
+ try {
606
+ statement.close();
607
+ } catch(Exception e) {}
608
+ }
609
+ }
610
+
611
+ protected IRubyObject config_value(ThreadContext context, String key) {
612
+ IRubyObject config_hash = getInstanceVariable("@config");
613
+
614
+ return config_hash.callMethod(context, "[]", context.getRuntime().newSymbol(key));
615
+ }
616
+
617
+ private static String toStringOrNull(IRubyObject arg) {
618
+ return arg.isNil() ? null : arg.toString();
619
+ }
620
+
621
+ private static IRubyObject floatToRuby(Ruby runtime, ResultSet resultSet, float floatValue)
622
+ throws SQLException, IOException {
623
+ if (floatValue == 0 && resultSet.wasNull()) return runtime.getNil();
624
+
625
+ return runtime.newFloat(floatValue);
626
+ }
627
+
628
+ protected Connection getConnection() {
629
+ return getConnection(false);
630
+ }
631
+
632
+ protected Connection getConnection(boolean error) {
633
+ Connection conn = (Connection) dataGetStruct();
634
+ if(error && conn == null) {
635
+ RubyClass err = getRuntime().getModule("ActiveRecord").getClass("ConnectionNotEstablished");
636
+ throw new RaiseException(getRuntime(), err, "no connection available", false);
637
+ }
638
+ return conn;
639
+ }
640
+
641
+ protected JdbcConnectionFactory getConnectionFactory() throws RaiseException {
642
+ IRubyObject connection_factory = getInstanceVariable("@connection_factory");
643
+ JdbcConnectionFactory factory = null;
644
+ try {
645
+ factory = (JdbcConnectionFactory) ((JavaObject) rubyApi.getInstanceVariable(connection_factory, "@java_object")).getValue();
646
+ } catch (Exception e) {
647
+ factory = null;
648
+ }
649
+ if (factory == null) {
650
+ throw getRuntime().newRuntimeError("@connection_factory not set properly");
651
+ }
652
+ return factory;
653
+ }
654
+
655
+ private static String[] getTypes(IRubyObject typeArg) {
656
+ if (!(typeArg instanceof RubyArray)) return new String[] { typeArg.toString() };
657
+
658
+ IRubyObject[] arr = rubyApi.convertToJavaArray(typeArg);
659
+ String[] types = new String[arr.length];
660
+ for (int i = 0; i < types.length; i++) {
661
+ types[i] = arr[i].toString();
662
+ }
663
+
664
+ return types;
665
+ }
666
+
667
+ private static int getTypeValueFor(Ruby runtime, IRubyObject type) throws SQLException {
668
+ // How could this ever yield anything useful?
669
+ if (!(type instanceof RubySymbol)) type = rubyApi.callMethod(type, "class");
670
+
671
+ // Assumption; If this is a symbol then it will be backed by an interned string. (enebo)
672
+ String internedValue = type.asJavaString();
673
+
674
+ if(internedValue == "string") {
675
+ return Types.VARCHAR;
676
+ } else if(internedValue == "text") {
677
+ return Types.CLOB;
678
+ } else if(internedValue == "integer") {
679
+ return Types.INTEGER;
680
+ } else if(internedValue == "decimal") {
681
+ return Types.DECIMAL;
682
+ } else if(internedValue == "float") {
683
+ return Types.FLOAT;
684
+ } else if(internedValue == "datetime") {
685
+ return Types.TIMESTAMP;
686
+ } else if(internedValue == "timestamp") {
687
+ return Types.TIMESTAMP;
688
+ } else if(internedValue == "time") {
689
+ return Types.TIME;
690
+ } else if(internedValue == "date") {
691
+ return Types.DATE;
692
+ } else if(internedValue == "binary") {
693
+ return Types.BLOB;
694
+ } else if(internedValue == "boolean") {
695
+ return Types.BOOLEAN;
696
+ } else {
697
+ return -1;
698
+ }
699
+ }
700
+
701
+ private boolean isConnectionBroken(ThreadContext context, Connection c) {
702
+ try {
703
+ IRubyObject alive = config_value(context, "connection_alive_sql");
704
+ if (select_p(context, this, alive).isTrue()) {
705
+ String connectionSQL = rubyApi.convertToRubyString(alive).toString();
706
+ Statement s = c.createStatement();
707
+ try {
708
+ s.execute(connectionSQL);
709
+ } finally {
710
+ close(s);
711
+ }
712
+ return false;
713
+ } else {
714
+ return !c.isClosed();
715
+ }
716
+ } catch (SQLException sx) {
717
+ return true;
718
+ }
719
+ }
720
+
721
+ private static IRubyObject integerToRuby(Ruby runtime, ResultSet resultSet, long longValue)
722
+ throws SQLException, IOException {
723
+ if (longValue == 0 && resultSet.wasNull()) return runtime.getNil();
724
+
725
+ return runtime.newFixnum(longValue);
726
+ }
727
+
728
+ private static IRubyObject jdbcToRuby(Ruby runtime, int column, int type, ResultSet resultSet)
729
+ throws SQLException {
730
+ try {
731
+ switch (type) {
732
+ case Types.BINARY: case Types.BLOB: case Types.LONGVARBINARY: case Types.VARBINARY:
733
+ case Types.LONGVARCHAR:
734
+ return streamToRuby(runtime, resultSet, resultSet.getBinaryStream(column));
735
+ case Types.CLOB:
736
+ return readerToRuby(runtime, resultSet, resultSet.getCharacterStream(column));
737
+ case Types.TIMESTAMP:
738
+ return timestampToRuby(runtime, resultSet, resultSet.getTimestamp(column));
739
+ case Types.INTEGER: case Types.SMALLINT: case Types.TINYINT:
740
+ return integerToRuby(runtime, resultSet, resultSet.getLong(column));
741
+ case Types.REAL:
742
+ return floatToRuby(runtime, resultSet, resultSet.getFloat(column));
743
+ default:
744
+ return stringToRuby(runtime, resultSet, resultSet.getString(column));
745
+ }
746
+ } catch (IOException ioe) {
747
+ throw (SQLException) new SQLException(ioe.getMessage()).initCause(ioe);
748
+ }
749
+ }
750
+
751
+ private static void populateFromResultSet(ThreadContext context, Ruby runtime, List results,
752
+ ResultSet resultSet, ColumnData[] columns) throws SQLException {
753
+ int columnCount = columns.length;
754
+
755
+ while (resultSet.next()) {
756
+ RubyHash row = RubyHash.newHash(runtime);
757
+
758
+ for (int i = 0; i < columnCount; i++) {
759
+ row.op_aset(context, columns[i].name, jdbcToRuby(runtime, i + 1, columns[i].type, resultSet));
760
+ }
761
+ results.add(row);
762
+ }
763
+ }
764
+
765
+
766
+ private static IRubyObject readerToRuby(Ruby runtime, ResultSet resultSet, Reader reader)
767
+ throws SQLException, IOException {
768
+ if (reader == null && resultSet.wasNull()) return runtime.getNil();
769
+
770
+ StringBuffer str = new StringBuffer(2048);
771
+ try {
772
+ char[] buf = new char[2048];
773
+
774
+ for (int n = reader.read(buf); n != -1; n = reader.read(buf)) {
775
+ str.append(buf, 0, n);
776
+ }
777
+ } finally {
778
+ reader.close();
779
+ }
780
+
781
+ return RubyString.newUnicodeString(runtime, str.toString());
782
+ }
783
+
784
+ private IRubyObject setConnection(Connection c) {
785
+ close(getConnection()); // Close previously open connection if there is one
786
+
787
+ IRubyObject rubyconn = c != null ? wrappedConnection(c) : getRuntime().getNil();
788
+ setInstanceVariable("@connection", rubyconn);
789
+ dataWrapStruct(c);
790
+ return this;
791
+ }
792
+
793
+ private final static DateFormat FORMAT = new SimpleDateFormat("%y-%M-%d %H:%m:%s");
794
+
795
+ private static void setValue(PreparedStatement ps, int index, ThreadContext context,
796
+ IRubyObject value, IRubyObject type) throws SQLException {
797
+ final int tp = getTypeValueFor(context.getRuntime(), type);
798
+ if(value.isNil()) {
799
+ ps.setNull(index, tp);
800
+ return;
801
+ }
802
+
803
+ switch(tp) {
804
+ case Types.VARCHAR:
805
+ case Types.CLOB:
806
+ ps.setString(index, RubyString.objAsString(context, value).toString());
807
+ break;
808
+ case Types.INTEGER:
809
+ ps.setLong(index, RubyNumeric.fix2long(value));
810
+ break;
811
+ case Types.FLOAT:
812
+ ps.setDouble(index, ((RubyNumeric)value).getDoubleValue());
813
+ break;
814
+ case Types.TIMESTAMP:
815
+ case Types.TIME:
816
+ case Types.DATE:
817
+ if(!(value instanceof RubyTime)) {
818
+ try {
819
+ Date dd = FORMAT.parse(RubyString.objAsString(context, value).toString());
820
+ ps.setTimestamp(index, new java.sql.Timestamp(dd.getTime()), Calendar.getInstance());
821
+ } catch(Exception e) {
822
+ ps.setString(index, RubyString.objAsString(context, value).toString());
823
+ }
824
+ } else {
825
+ RubyTime rubyTime = (RubyTime) value;
826
+ java.util.Date date = rubyTime.getJavaDate();
827
+ long millis = date.getTime();
828
+ long micros = rubyTime.microseconds() - millis / 1000;
829
+ java.sql.Timestamp ts = new java.sql.Timestamp(millis);
830
+ java.util.Calendar cal = Calendar.getInstance();
831
+ cal.setTime(date);
832
+ ts.setNanos((int)(micros * 1000));
833
+ ps.setTimestamp(index, ts, cal);
834
+ }
835
+ break;
836
+ case Types.BOOLEAN:
837
+ ps.setBoolean(index, value.isTrue());
838
+ break;
839
+ default: throw new RuntimeException("type " + type + " not supported in _bind yet");
840
+ }
841
+ }
842
+
843
+ private static void setValuesOnPS(PreparedStatement ps, ThreadContext context,
844
+ IRubyObject valuesArg, IRubyObject typesArg) throws SQLException {
845
+ RubyArray values = (RubyArray) valuesArg;
846
+ RubyArray types = (RubyArray) typesArg;
847
+
848
+ for(int i=0, j=values.getLength(); i<j; i++) {
849
+ setValue(ps, i+1, context, values.eltInternal(i), types.eltInternal(i));
850
+ }
851
+ }
852
+
853
+ private static IRubyObject streamToRuby(Ruby runtime, ResultSet resultSet, InputStream is)
854
+ throws SQLException, IOException {
855
+ if (is == null && resultSet.wasNull()) return runtime.getNil();
856
+
857
+ ByteList str = new ByteList(2048);
858
+ try {
859
+ byte[] buf = new byte[2048];
860
+
861
+ for (int n = is.read(buf); n != -1; n = is.read(buf)) {
862
+ str.append(buf, 0, n);
863
+ }
864
+ } finally {
865
+ is.close();
866
+ }
867
+
868
+ return runtime.newString(str);
869
+ }
870
+
871
+ private static IRubyObject stringToRuby(Ruby runtime, ResultSet resultSet, String string)
872
+ throws SQLException, IOException {
873
+ if (string == null && resultSet.wasNull()) return runtime.getNil();
874
+
875
+ return RubyString.newUnicodeString(runtime, string);
876
+ }
877
+
878
+ private static final int TABLE_NAME = 3;
879
+
880
+ protected SQLBlock tableLookupBlock(final Ruby runtime,
881
+ final String catalog, final String schemapat,
882
+ final String tablepat, final String[] types, final boolean downCase) {
883
+ return new SQLBlock() {
884
+ public Object call(Connection c) throws SQLException {
885
+ ResultSet rs = null;
886
+ try {
887
+ DatabaseMetaData metadata = c.getMetaData();
888
+ String clzName = metadata.getClass().getName().toLowerCase();
889
+ boolean isOracle = clzName.indexOf("oracle") != -1 || clzName.indexOf("oci") != -1;
890
+
891
+ String realschema = schemapat;
892
+ String realtablepat = tablepat;
893
+
894
+ if (realtablepat != null) realtablepat = caseConvertIdentifierForJdbc(metadata, realtablepat);
895
+ if (realschema != null) realschema = caseConvertIdentifierForJdbc(metadata, realschema);
896
+
897
+ rs = metadata.getTables(catalog, realschema, realtablepat, types);
898
+ List arr = new ArrayList();
899
+ while (rs.next()) {
900
+ String name;
901
+
902
+ if (downCase) {
903
+ name = rs.getString(TABLE_NAME).toLowerCase();
904
+ } else {
905
+ name = caseConvertIdentifierForRails(metadata, rs.getString(TABLE_NAME));
906
+ }
907
+ // Handle stupid Oracle 10g RecycleBin feature
908
+ if (!isOracle || !name.startsWith("bin$")) {
909
+ arr.add(RubyString.newUnicodeString(runtime, name));
910
+ }
911
+ }
912
+ return runtime.newArray(arr);
913
+ } finally {
914
+ close(rs);
915
+ }
916
+ }
917
+ };
918
+ }
919
+
920
+ private static IRubyObject timestampToRuby(Ruby runtime, ResultSet resultSet, Timestamp time)
921
+ throws SQLException, IOException {
922
+ if (time == null && resultSet.wasNull()) return runtime.getNil();
923
+
924
+ String str = time.toString();
925
+ if (str.endsWith(" 00:00:00.0")) {
926
+ str = str.substring(0, str.length() - (" 00:00:00.0".length()));
927
+ }
928
+
929
+ return RubyString.newUnicodeString(runtime, str);
930
+ }
931
+
932
+ private static final int COLUMN_NAME = 4;
933
+ private static final int DATA_TYPE = 5;
934
+ private static final int TYPE_NAME = 6;
935
+ private static final int COLUMN_SIZE = 7;
936
+ private static final int DECIMAL_DIGITS = 9;
937
+ private static final int COLUMN_DEF = 13;
938
+ private static final int IS_NULLABLE = 18;
939
+
940
+ private int intFromResultSet(ResultSet resultSet, int column) throws SQLException {
941
+ int precision = resultSet.getInt(column);
942
+
943
+ return precision == 0 && resultSet.wasNull() ? -1 : precision;
944
+ }
945
+
946
+ /**
947
+ * Create a string which represents a sql type usable by Rails from the resultSet column
948
+ * metadata object.
949
+ *
950
+ * @param numberAsBoolean the database uses decimal as a boolean data type
951
+ * because it does not support optional SQL92 type or mandatory SQL99
952
+ * booleans.
953
+ */
954
+ private String typeFromResultSet(ResultSet resultSet, boolean numberAsBoolean) throws SQLException {
955
+ int precision = intFromResultSet(resultSet, COLUMN_SIZE);
956
+ int scale = intFromResultSet(resultSet, DECIMAL_DIGITS);
957
+
958
+ // Assume db's which use decimal for boolean will not also specify a
959
+ // valid precision 1 decimal also. Seems sketchy to me...
960
+ if (numberAsBoolean && precision != 1 &&
961
+ resultSet.getInt(DATA_TYPE) == java.sql.Types.DECIMAL) precision = -1;
962
+
963
+ String type = resultSet.getString(TYPE_NAME);
964
+ if (precision > 0) {
965
+ type += "(" + precision;
966
+ if(scale > 0) type += "," + scale;
967
+ type += ")";
968
+ }
969
+
970
+ return type;
971
+ }
972
+
973
+ private IRubyObject defaultValueFromResultSet(Ruby runtime, ResultSet resultSet)
974
+ throws SQLException {
975
+ String defaultValue = resultSet.getString(COLUMN_DEF);
976
+
977
+ return defaultValue == null ? runtime.getNil() : RubyString.newUnicodeString(runtime, defaultValue);
978
+ }
979
+
980
+ private IRubyObject unmarshal_columns(ThreadContext context, DatabaseMetaData metadata,
981
+ ResultSet rs) throws SQLException {
982
+ try {
983
+ Ruby runtime = context.getRuntime();
984
+ List columns = new ArrayList();
985
+ String clzName = metadata.getClass().getName().toLowerCase();
986
+ boolean isOracle = clzName.indexOf("oracle") != -1 || clzName.indexOf("oci") != -1;
987
+
988
+ RubyHash types = (RubyHash) native_database_types();
989
+ IRubyObject jdbcCol = getConnectionAdapters(runtime).getConstant("JdbcColumn");
990
+
991
+ while(rs.next()) {
992
+ IRubyObject column = jdbcCol.callMethod(context, "new",
993
+ new IRubyObject[] {
994
+ getInstanceVariable("@config"),
995
+ RubyString.newUnicodeString(runtime,
996
+ caseConvertIdentifierForRails(metadata, rs.getString(COLUMN_NAME))),
997
+ defaultValueFromResultSet(runtime, rs),
998
+ RubyString.newUnicodeString(runtime, typeFromResultSet(rs, isOracle)),
999
+ runtime.newBoolean(!rs.getString(IS_NULLABLE).trim().equals("NO"))
1000
+ });
1001
+ columns.add(column);
1002
+
1003
+ IRubyObject tp = (IRubyObject)types.fastARef(column.callMethod(context,"type"));
1004
+ if(tp != null && !tp.isNil() && tp.callMethod(context, "[]", runtime.newSymbol("limit")).isNil()) {
1005
+ column.callMethod(context, "limit=", runtime.getNil());
1006
+ if(!column.callMethod(context, "type").equals(runtime.newSymbol("decimal"))) {
1007
+ column.callMethod(context, "precision=", runtime.getNil());
1008
+ }
1009
+ }
1010
+ }
1011
+ return runtime.newArray(columns);
1012
+ } finally {
1013
+ close(rs);
1014
+ }
1015
+ }
1016
+
1017
+
1018
+ public static IRubyObject unmarshal_id_result(Ruby runtime, ResultSet rs) throws SQLException {
1019
+ try {
1020
+ if (rs.next() && rs.getMetaData().getColumnCount() > 0) {
1021
+ return runtime.newFixnum(rs.getLong(1));
1022
+ }
1023
+ return runtime.getNil();
1024
+ } finally {
1025
+ close(rs);
1026
+ }
1027
+ }
1028
+
1029
+ /**
1030
+ * Converts a jdbc resultset into an array (rows) of hashes (row) that AR expects.
1031
+ *
1032
+ * @param downCase should column names only be in lower case?
1033
+ */
1034
+ protected static IRubyObject unmarshalResult(ThreadContext context, DatabaseMetaData metadata,
1035
+ ResultSet resultSet, boolean downCase) throws SQLException {
1036
+ Ruby runtime = context.getRuntime();
1037
+ List results = new ArrayList();
1038
+
1039
+ try {
1040
+ ColumnData[] columns = ColumnData.setup(runtime, metadata, resultSet.getMetaData(), downCase);
1041
+
1042
+ populateFromResultSet(context, runtime, results, resultSet, columns);
1043
+ } finally {
1044
+ close(resultSet);
1045
+ }
1046
+
1047
+ return runtime.newArray(results);
1048
+ }
1049
+
1050
+ protected Object withConnectionAndRetry(ThreadContext context, SQLBlock block) {
1051
+ int tries = 1;
1052
+ int i = 0;
1053
+ Throwable toWrap = null;
1054
+ boolean autoCommit = false;
1055
+ while (i < tries) {
1056
+ Connection c = getConnection(true);
1057
+ try {
1058
+ autoCommit = c.getAutoCommit();
1059
+ return block.call(c);
1060
+ } catch (Exception e) {
1061
+ toWrap = e;
1062
+ while (toWrap.getCause() != null && toWrap.getCause() != toWrap) {
1063
+ toWrap = toWrap.getCause();
1064
+ }
1065
+ i++;
1066
+ if (autoCommit) {
1067
+ if (i == 1) {
1068
+ tries = (int) rubyApi.convertToRubyInteger(config_value(context, "retry_count")).getLongValue();
1069
+ if (tries <= 0) {
1070
+ tries = 1;
1071
+ }
1072
+ }
1073
+ if (isConnectionBroken(context, c)) {
1074
+ reconnect();
1075
+ } else {
1076
+ throw wrap(context, toWrap);
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ throw wrap(context, toWrap);
1082
+ }
1083
+
1084
+ private static RuntimeException wrap(ThreadContext context, Throwable exception) {
1085
+ RubyClass err = context.getRuntime().getModule("ActiveRecord").getClass("ActiveRecordError");
1086
+ return (RuntimeException) new RaiseException(context.getRuntime(), err, exception.getMessage(), false).initCause(exception);
1087
+ }
1088
+
1089
+ private IRubyObject wrappedConnection(Connection c) {
1090
+ return Java.java_to_ruby(this, JavaObject.wrap(getRuntime(), c), Block.NULL_BLOCK);
1091
+ }
1092
+
1093
+ private static int whitespace(int start, ByteList bl) {
1094
+ int end = bl.begin + bl.realSize;
1095
+
1096
+ for (int i = start; i < end; i++) {
1097
+ if (!Character.isWhitespace(bl.bytes[i])) return i;
1098
+ }
1099
+
1100
+ return end;
1101
+ }
1102
+
1103
+ private static byte[] CALL = new byte[]{'c', 'a', 'l', 'l'};
1104
+ private static byte[] INSERT = new byte[] {'i', 'n', 's', 'e', 'r', 't'};
1105
+ private static byte[] SELECT = new byte[] {'s', 'e', 'l', 'e', 'c', 't'};
1106
+ private static byte[] SHOW = new byte[] {'s', 'h', 'o', 'w'};
1107
+
1108
+ private static boolean startsWithNoCaseCmp(ByteList bytelist, byte[] compare) {
1109
+ int p = whitespace(bytelist.begin, bytelist);
1110
+
1111
+ // What the hell is this for?
1112
+ if (bytelist.bytes[p] == '(') p = whitespace(p, bytelist);
1113
+
1114
+ for (int i = 0; i < bytelist.realSize && i < compare.length; i++) {
1115
+ if (Character.toLowerCase(bytelist.bytes[p + i]) != compare[i]) return false;
1116
+ }
1117
+
1118
+ return true;
1119
+ }
1120
+
1121
+ public static class ColumnData {
1122
+ public IRubyObject name;
1123
+ public int type;
1124
+
1125
+ public ColumnData(IRubyObject name, int type) {
1126
+ this.name = name;
1127
+ this.type = type;
1128
+ }
1129
+
1130
+ public static ColumnData[] setup(Ruby runtime, DatabaseMetaData databaseMetadata,
1131
+ ResultSetMetaData metadata, boolean downCase) throws SQLException {
1132
+ int columnsCount = metadata.getColumnCount();
1133
+ ColumnData[] columns = new ColumnData[columnsCount];
1134
+
1135
+ for (int i = 1; i <= columnsCount; i++) { // metadata is one-based
1136
+ String name;
1137
+ if (downCase) {
1138
+ name = metadata.getColumnLabel(i).toLowerCase();
1139
+ } else {
1140
+ name = RubyJdbcConnection.caseConvertIdentifierForRails(databaseMetadata, metadata.getColumnLabel(i));
1141
+ }
1142
+
1143
+ columns[i - 1] = new ColumnData(RubyString.newUnicodeString(runtime, name), metadata.getColumnType(i));
1144
+ }
1145
+
1146
+ return columns;
1147
+ }
1148
+ }
1149
+ }