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.
- data/History.txt +31 -0
- data/Manifest.txt +7 -0
- data/README.txt +15 -2
- data/Rakefile +28 -30
- data/lib/active_record/connection_adapters/h2_adapter.rb +13 -1
- data/lib/active_record/connection_adapters/jdbc_adapter.rb +78 -96
- data/lib/jdbc_adapter/jdbc.rake +15 -5
- data/lib/jdbc_adapter/jdbc_adapter_internal.jar +0 -0
- data/lib/jdbc_adapter/jdbc_cachedb.rb +4 -4
- data/lib/jdbc_adapter/jdbc_db2.rb +5 -7
- data/lib/jdbc_adapter/jdbc_derby.rb +57 -30
- data/lib/jdbc_adapter/jdbc_firebird.rb +2 -2
- data/lib/jdbc_adapter/jdbc_hsqldb.rb +53 -46
- data/lib/jdbc_adapter/jdbc_informix.rb +4 -5
- data/lib/jdbc_adapter/jdbc_mimer.rb +2 -2
- data/lib/jdbc_adapter/jdbc_mssql.rb +25 -23
- data/lib/jdbc_adapter/jdbc_mysql.rb +20 -22
- data/lib/jdbc_adapter/jdbc_oracle.rb +115 -117
- data/lib/jdbc_adapter/jdbc_postgre.rb +129 -59
- data/lib/jdbc_adapter/jdbc_sqlite3.rb +149 -28
- data/lib/jdbc_adapter/jdbc_sybase.rb +13 -2
- data/lib/jdbc_adapter/missing_functionality_helper.rb +12 -3
- data/lib/jdbc_adapter/version.rb +1 -1
- data/src/java/jdbc_adapter/JdbcAdapterInternalService.java +6 -1101
- data/src/java/jdbc_adapter/JdbcDerbySpec.java +26 -23
- data/src/java/jdbc_adapter/JdbcMySQLSpec.java +79 -28
- data/src/java/jdbc_adapter/PostgresRubyJdbcConnection.java +35 -0
- data/src/java/jdbc_adapter/RubyJdbcConnection.java +1149 -0
- data/src/java/jdbc_adapter/SQLBlock.java +12 -3
- data/src/java/jdbc_adapter/Sqlite3RubyJdbcConnection.java +41 -0
- data/test/activerecord/connection_adapters/type_conversion_test.rb +1 -1
- data/test/db/derby.rb +0 -3
- data/test/db/h2.rb +0 -3
- data/test/db/hsqldb.rb +1 -4
- data/test/db/mysql.rb +1 -0
- data/test/db/oracle.rb +5 -0
- data/test/db/sqlite3.rb +7 -3
- data/test/derby_migration_test.rb +21 -0
- data/test/has_many_through.rb +11 -4
- data/test/jdbc_common.rb +13 -1
- data/test/models/data_types.rb +11 -1
- data/test/models/mixed_case.rb +20 -0
- data/test/mysql_multibyte_test.rb +4 -0
- data/test/oracle_simple_test.rb +1 -1
- data/test/postgres_mixed_case_test.rb +19 -0
- data/test/simple.rb +220 -41
- data/test/sqlite3_simple_test.rb +83 -0
- data/test/sybase_jtds_simple_test.rb +6 -0
- 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, "
|
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, "
|
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, "
|
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
|
-
|
113
|
+
String type = rubyApi.callMethod(col, "type").toString();
|
112
114
|
if (value instanceof RubyString) {
|
113
|
-
if (type
|
115
|
+
if (type.equals("string")) {
|
114
116
|
return quote_string_with_surround(runtime, "'", (RubyString)value, "'");
|
115
|
-
} else if (type
|
117
|
+
} else if (type.equals("text")) {
|
116
118
|
return quote_string_with_surround(runtime, "CAST('", (RubyString)value, "' AS CLOB)");
|
117
|
-
} else if (type
|
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
|
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.
|
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.
|
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.
|
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
|
-
|
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
|
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 =
|
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
|
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-
|
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
|
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
|
43
|
-
private final static
|
44
|
-
|
45
|
-
private final static
|
46
|
-
private final static
|
47
|
-
private final static
|
48
|
-
private final static
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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 (
|
75
|
-
|
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
|
-
|
78
|
-
blNew.append(bl, startOfExtend-bl.begin, bl.begin + bl.realSize - startOfExtend);
|
109
|
+
newBytes.append(BACKQUOTE);
|
79
110
|
|
80
|
-
return
|
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
|
+
}
|