activerecord-jdbc-adapter 5.0.pre1 → 50.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord-jdbc-adapter might be problematic. Click here for more details.

Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -2
  3. data/.travis.yml +15 -416
  4. data/Gemfile +35 -37
  5. data/README.md +23 -118
  6. data/RUNNING_TESTS.md +31 -26
  7. data/Rakefile +2 -3
  8. data/lib/arjdbc/abstract/connection_management.rb +21 -0
  9. data/lib/arjdbc/abstract/core.rb +62 -0
  10. data/lib/arjdbc/abstract/database_statements.rb +46 -0
  11. data/lib/arjdbc/abstract/statement_cache.rb +58 -0
  12. data/lib/arjdbc/abstract/transaction_support.rb +86 -0
  13. data/lib/arjdbc/derby/adapter.rb +6 -1
  14. data/lib/arjdbc/discover.rb +0 -7
  15. data/lib/arjdbc/firebird/adapter.rb +2 -2
  16. data/lib/arjdbc/jdbc.rb +2 -2
  17. data/lib/arjdbc/jdbc/adapter.rb +10 -252
  18. data/lib/arjdbc/jdbc/adapter_java.jar +0 -0
  19. data/lib/arjdbc/jdbc/connection.rb +6 -0
  20. data/lib/arjdbc/mysql/adapter.rb +82 -946
  21. data/lib/arjdbc/mysql/connection_methods.rb +4 -2
  22. data/lib/arjdbc/postgresql/adapter.rb +270 -970
  23. data/lib/arjdbc/postgresql/base/array_decoder.rb +26 -0
  24. data/lib/arjdbc/postgresql/base/array_encoder.rb +25 -0
  25. data/lib/arjdbc/postgresql/base/pgconn.rb +8 -5
  26. data/lib/arjdbc/postgresql/column.rb +10 -599
  27. data/lib/arjdbc/postgresql/connection_methods.rb +9 -0
  28. data/lib/arjdbc/postgresql/name.rb +24 -0
  29. data/lib/arjdbc/postgresql/oid_types.rb +28 -109
  30. data/lib/arjdbc/sqlite3/adapter.rb +18 -42
  31. data/lib/arjdbc/tasks/database_tasks.rb +1 -3
  32. data/lib/arjdbc/tasks/db2_database_tasks.rb +2 -2
  33. data/lib/arjdbc/version.rb +1 -1
  34. data/pom.xml +3 -3
  35. data/rakelib/02-test.rake +0 -12
  36. data/rakelib/compile.rake +1 -1
  37. data/rakelib/db.rake +7 -5
  38. data/rakelib/rails.rake +67 -64
  39. data/src/java/arjdbc/firebird/FirebirdRubyJdbcConnection.java +1 -17
  40. data/src/java/arjdbc/jdbc/RubyJdbcConnection.java +518 -1260
  41. data/src/java/arjdbc/mysql/MySQLModule.java +3 -3
  42. data/src/java/arjdbc/mysql/MySQLRubyJdbcConnection.java +53 -134
  43. data/src/java/arjdbc/postgresql/PostgreSQLRubyJdbcConnection.java +214 -240
  44. data/src/java/arjdbc/sqlite3/SQLite3Module.java +0 -20
  45. data/src/java/arjdbc/sqlite3/SQLite3RubyJdbcConnection.java +85 -10
  46. metadata +16 -29
  47. data/Appraisals +0 -41
  48. data/lib/active_record/connection_adapters/oracle_adapter.rb +0 -1
  49. data/lib/arjdbc/common_jdbc_methods.rb +0 -89
  50. data/lib/arjdbc/mysql/bulk_change_table.rb +0 -150
  51. data/lib/arjdbc/mysql/column.rb +0 -162
  52. data/lib/arjdbc/mysql/explain_support.rb +0 -82
  53. data/lib/arjdbc/mysql/schema_creation.rb +0 -58
  54. data/lib/arjdbc/oracle.rb +0 -4
  55. data/lib/arjdbc/oracle/adapter.rb +0 -952
  56. data/lib/arjdbc/oracle/column.rb +0 -126
  57. data/lib/arjdbc/oracle/connection_methods.rb +0 -21
  58. data/lib/arjdbc/postgresql/base/oid.rb +0 -412
  59. data/lib/arjdbc/postgresql/base/schema_definitions.rb +0 -131
  60. data/lib/arjdbc/postgresql/explain_support.rb +0 -53
  61. data/lib/arjdbc/postgresql/oid/bytea.rb +0 -2
  62. data/lib/arjdbc/postgresql/schema_creation.rb +0 -60
  63. data/lib/arjdbc/tasks/oracle/enhanced_structure_dump.rb +0 -297
  64. data/lib/arjdbc/tasks/oracle_database_tasks.rb +0 -65
  65. data/src/java/arjdbc/oracle/OracleModule.java +0 -75
  66. data/src/java/arjdbc/oracle/OracleRubyJdbcConnection.java +0 -465
@@ -100,9 +100,9 @@ public class MySQLModule {
100
100
 
101
101
  final Ruby runtime = context.getRuntime();
102
102
  final RubyString quoted = runtime.newString(quotedBytes);
103
- if ( runtime.is1_9() ) { // only due mysql2 compatibility
104
- quoted.associateEncoding( UTF8Encoding.INSTANCE );
105
- }
103
+
104
+ quoted.associateEncoding(UTF8Encoding.INSTANCE);
105
+
106
106
  return quoted;
107
107
  }
108
108
 
@@ -26,7 +26,6 @@
26
26
  package arjdbc.mysql;
27
27
 
28
28
  import arjdbc.jdbc.RubyJdbcConnection;
29
- import arjdbc.jdbc.Callable;
30
29
 
31
30
  import java.lang.reflect.Field;
32
31
  import java.lang.reflect.InvocationTargetException;
@@ -43,17 +42,18 @@ import java.util.TimeZone;
43
42
  import java.util.regex.Matcher;
44
43
  import java.util.regex.Pattern;
45
44
 
45
+ import org.joda.time.DateTime;
46
46
  import org.jruby.Ruby;
47
- import org.jruby.RubyArray;
48
47
  import org.jruby.RubyClass;
49
- import org.jruby.RubyFloat;
50
48
  import org.jruby.RubyInteger;
51
- import org.jruby.RubyModule;
52
49
  import org.jruby.RubyString;
50
+ import org.jruby.RubyTime;
51
+ import org.jruby.anno.JRubyMethod;
53
52
  import org.jruby.exceptions.RaiseException;
54
53
  import org.jruby.runtime.ObjectAllocator;
55
54
  import org.jruby.runtime.ThreadContext;
56
55
  import org.jruby.runtime.builtin.IRubyObject;
56
+ import org.jruby.util.TypeConverter;
57
57
 
58
58
  /**
59
59
  *
@@ -79,25 +79,20 @@ public class MySQLRubyJdbcConnection extends RubyJdbcConnection {
79
79
  return clazz;
80
80
  }
81
81
 
82
- @Override
83
- protected boolean doExecute(final Statement statement, final String query)
84
- throws SQLException {
85
- return statement.execute(query, Statement.RETURN_GENERATED_KEYS);
82
+ @JRubyMethod
83
+ public IRubyObject query(final ThreadContext context, final IRubyObject sql) throws SQLException {
84
+ final String query = sql.convertToString().getUnicodeValue(); // sql
85
+ return executeUpdate(context, query, false);
86
86
  }
87
87
 
88
88
  @Override
89
- protected IRubyObject mapGeneratedKeysOrUpdateCount(final ThreadContext context,
90
- final Connection connection, final Statement statement) throws SQLException {
91
- final Ruby runtime = context.getRuntime();
92
- final IRubyObject key = mapGeneratedKeys(runtime, connection, statement);
93
- return ( key == null || key.isNil() ) ? runtime.newFixnum( statement.getUpdateCount() ) : key;
89
+ protected boolean doExecute(final Statement statement, final String query) throws SQLException {
90
+ return statement.execute(query, Statement.RETURN_GENERATED_KEYS);
94
91
  }
95
92
 
96
93
  @Override
97
- protected IRubyObject jdbcToRuby(
98
- final ThreadContext context, final Ruby runtime,
99
- final int column, final int type, final ResultSet resultSet)
100
- throws SQLException {
94
+ protected IRubyObject jdbcToRuby(final ThreadContext context, final Ruby runtime,
95
+ final int column, final int type, final ResultSet resultSet) throws SQLException {
101
96
  if ( type == Types.BIT ) {
102
97
  final int value = resultSet.getInt(column);
103
98
  return resultSet.wasNull() ? runtime.getNil() : runtime.newFixnum(value);
@@ -106,53 +101,50 @@ public class MySQLRubyJdbcConnection extends RubyJdbcConnection {
106
101
  }
107
102
 
108
103
  @Override // can not use statement.setTimestamp( int, Timestamp, Calendar )
109
- protected void setTimestampParameter(final ThreadContext context,
110
- final Connection connection, final PreparedStatement statement,
111
- final int index, IRubyObject value,
112
- final IRubyObject column, final int type) throws SQLException {
113
- if ( value.isNil() ) statement.setNull(index, Types.TIMESTAMP);
114
- else {
115
- value = getTimeInDefaultTimeZone(context, value);
116
- if ( value instanceof RubyString ) { // yyyy-[m]m-[d]d hh:mm:ss[.f...]
117
- final Timestamp timestamp = Timestamp.valueOf( value.toString() );
118
- statement.setTimestamp( index, timestamp ); // assume local time-zone
119
- }
120
- else { // Time or DateTime ( ActiveSupport::TimeWithZone.to_time )
121
- final double time = adjustTimeFromDefaultZone(value);
122
- final RubyFloat timeValue = context.getRuntime().newFloat( time );
123
- statement.setTimestamp( index, convertToTimestamp(timeValue) );
124
- }
125
- }
104
+ protected void setTimestampParameter(ThreadContext context, Connection connection, PreparedStatement statement,
105
+ int index, IRubyObject value, IRubyObject column, int type) throws SQLException {
106
+ value = callMethod(context, "time_in_default_timezone", value);
107
+ TypeConverter.checkType(context, value, context.runtime.getTime());
108
+ setTimestamp(statement, index, (RubyTime) value, type);
126
109
  }
127
110
 
128
- @Override // can not use statement.setTime( int, Time, Calendar )
129
- protected void setTimeParameter(final ThreadContext context,
130
- final Connection connection, final PreparedStatement statement,
131
- final int index, IRubyObject value,
132
- final IRubyObject column, final int type) throws SQLException {
133
- if ( value.isNil() ) statement.setNull(index, Types.TIME);
134
- else {
135
- value = getTimeInDefaultTimeZone(context, value);
136
- if ( value instanceof RubyString ) {
137
- final Time time = Time.valueOf( value.toString() );
138
- statement.setTime( index, time ); // assume local time-zone
139
- }
140
- else { // Time or DateTime ( ActiveSupport::TimeWithZone.to_time )
141
- final double timeValue = adjustTimeFromDefaultZone(value);
142
- final Time time = new Time(( (long) timeValue ) * 1000); // millis
143
- // java.sql.Time is expected to be only up to second precision
144
- statement.setTime( index, time );
145
- }
146
- }
111
+ @Override
112
+ protected void setTimeParameter(ThreadContext context, Connection connection, PreparedStatement statement,
113
+ int index, IRubyObject value, IRubyObject column, int type) throws SQLException {
114
+ setTimestampParameter(context, connection, statement, index, value, column, type);
147
115
  }
148
116
 
149
- private static double adjustTimeFromDefaultZone(final IRubyObject value) {
150
- // Time's to_f is : ( millis * 1000 + usec ) / 1_000_000.0
151
- final double time = value.convertToFloat().getDoubleValue(); // to_f
152
- // NOTE: MySQL assumes default TZ thus need to adjust to match :
153
- final int offset = TimeZone.getDefault().getOffset((long) time * 1000);
154
- // Time's to_f is : ( millis * 1000 + usec ) / 1_000_000.0
155
- return time - ( offset / 1000.0 );
117
+ // FIXME: we should detect adapter and not do this timezone offset calculation is it is jdbc version 6+.
118
+ private void setTimestamp(PreparedStatement statement, int index, RubyTime value, int type) throws SQLException {
119
+ DateTime dateTime = value.getDateTime();
120
+ int offset = TimeZone.getDefault().getOffset(dateTime.getMillis()); // JDBC <6.x ignores time zone info (we adjust manually).
121
+ Timestamp timestamp = new Timestamp(dateTime.getMillis() - offset);
122
+
123
+ // 1942-11-30T01:02:03.123_456
124
+ if (type != Types.DATE && value.getNSec() >= 0) timestamp.setNanos((int) (timestamp.getNanos() + value.getNSec()));
125
+
126
+ statement.setTimestamp(index, timestamp);
127
+ }
128
+
129
+ // FIXME: I think we can unify this back to main adapter code since previous conflict involved not using
130
+ // the raw string return type and not the extra formatting logic.
131
+ @Override
132
+ protected IRubyObject timeToRuby(ThreadContext context, Ruby runtime, ResultSet resultSet, int column) throws SQLException {
133
+ Time value = resultSet.getTime(column);
134
+
135
+ if (value == null) return resultSet.wasNull() ? runtime.getNil() : runtime.newString();
136
+
137
+ String strValue = value.toString();
138
+
139
+ // If time is column type but that time had a precision which included
140
+ // nanoseconds we used timestamp to save the data. Since this is conditional
141
+ // we grab data a second time as a timestamp to look for nsecs.
142
+ Timestamp nsecTimeHack = resultSet.getTimestamp(column);
143
+ if (nsecTimeHack.getNanos() != 0) {
144
+ strValue = String.format("%s.%09d", strValue, nsecTimeHack.getNanos());
145
+ }
146
+
147
+ return RubyString.newUnicodeString(runtime,strValue);
156
148
  }
157
149
 
158
150
  @Override
@@ -192,80 +184,7 @@ public class MySQLRubyJdbcConnection extends RubyJdbcConnection {
192
184
  }
193
185
 
194
186
  @Override
195
- protected IRubyObject indexes(final ThreadContext context, final String tableName, final String name, final String schemaName) {
196
- return withConnection(context, new Callable<IRubyObject>() {
197
- public IRubyObject call(final Connection connection) throws SQLException {
198
- final Ruby runtime = context.getRuntime();
199
- final RubyModule indexDefinition = getIndexDefinition(runtime);
200
- final String jdbcTableName = caseConvertIdentifierForJdbc(connection, tableName);
201
- final String jdbcSchemaName = caseConvertIdentifierForJdbc(connection, schemaName);
202
- final IRubyObject rubyTableName = RubyString.newUnicodeString(
203
- runtime, caseConvertIdentifierForJdbc(connection, tableName)
204
- );
205
-
206
- StringBuilder query = new StringBuilder("SHOW KEYS FROM ");
207
- if (jdbcSchemaName != null) {
208
- query.append(jdbcSchemaName).append(".");
209
- }
210
- query.append(jdbcTableName);
211
- query.append(" WHERE key_name != 'PRIMARY'");
212
-
213
- final RubyArray indexes = runtime.newArray(8);
214
- PreparedStatement statement = null;
215
- ResultSet keySet = null;
216
-
217
- try {
218
- statement = connection.prepareStatement(query.toString());
219
- keySet = statement.executeQuery();
220
-
221
- String currentKeyName = null;
222
-
223
- while ( keySet.next() ) {
224
- final String keyName = caseConvertIdentifierForRails(connection, keySet.getString("key_name"));
225
-
226
- if ( ! keyName.equals(currentKeyName) ) {
227
- currentKeyName = keyName;
228
-
229
- final boolean nonUnique = keySet.getBoolean("non_unique");
230
-
231
- IRubyObject[] args = new IRubyObject[] {
232
- rubyTableName, // table_name
233
- RubyString.newUnicodeString(runtime, keyName), // index_name
234
- runtime.newBoolean( ! nonUnique ), // unique
235
- runtime.newArray(), // [] for column names, we'll add to that in just a bit
236
- runtime.newArray() // lengths
237
- };
238
-
239
- indexes.append( indexDefinition.callMethod(context, "new", args) ); // IndexDefinition.new
240
- }
241
-
242
- IRubyObject lastIndexDef = indexes.isEmpty() ? null : indexes.entry(-1);
243
- if ( lastIndexDef != null ) {
244
- final String columnName = caseConvertIdentifierForRails(connection, keySet.getString("column_name"));
245
- final int length = keySet.getInt("sub_part");
246
- final boolean nullLength = keySet.wasNull();
247
-
248
- lastIndexDef.callMethod(context, "columns").callMethod(context,
249
- "<<", RubyString.newUnicodeString(runtime, columnName));
250
- lastIndexDef.callMethod(context, "lengths").callMethod(context,
251
- "<<", nullLength ? runtime.getNil() : runtime.newFixnum(length));
252
- }
253
- }
254
-
255
- return indexes;
256
- }
257
- finally {
258
- close(keySet);
259
- close(statement);
260
- }
261
- }
262
- });
263
- }
264
-
265
- @Override
266
- protected String caseConvertIdentifierForRails(final Connection connection, final String value)
267
- throws SQLException {
268
- if ( value == null ) return null;
187
+ protected String caseConvertIdentifierForRails(Connection connection, String value) throws SQLException {
269
188
  return value; // MySQL does not storesUpperCaseIdentifiers() :
270
189
  }
271
190
 
@@ -27,6 +27,7 @@ package arjdbc.postgresql;
27
27
 
28
28
  import java.io.ByteArrayInputStream;
29
29
  import java.io.InputStream;
30
+ import java.lang.StringBuilder;
30
31
  import java.sql.Array;
31
32
  import java.sql.Connection;
32
33
  import java.sql.PreparedStatement;
@@ -35,8 +36,10 @@ import java.sql.SQLException;
35
36
  import java.sql.Statement;
36
37
  import java.sql.Timestamp;
37
38
  import java.sql.Types;
39
+ import java.util.HashMap;
38
40
  import java.util.Map;
39
41
  import java.util.UUID;
42
+ import java.util.regex.Pattern;
40
43
 
41
44
  import org.jruby.Ruby;
42
45
  import org.jruby.RubyArray;
@@ -46,6 +49,7 @@ import org.jruby.RubyFixnum;
46
49
  import org.jruby.RubyFloat;
47
50
  import org.jruby.RubyHash;
48
51
  import org.jruby.RubyIO;
52
+ import org.jruby.RubyModule;
49
53
  import org.jruby.RubyString;
50
54
  import org.jruby.anno.JRubyMethod;
51
55
  import org.jruby.javasupport.JavaUtil;
@@ -57,7 +61,9 @@ import org.jruby.util.ByteList;
57
61
  import org.postgresql.PGConnection;
58
62
  import org.postgresql.PGStatement;
59
63
  import org.postgresql.core.BaseConnection;
60
- import org.postgresql.jdbc4.Jdbc4Array;
64
+ import org.postgresql.geometric.PGline;
65
+ import org.postgresql.geometric.PGlseg;
66
+ import org.postgresql.geometric.PGpoint;
61
67
  import org.postgresql.util.PGInterval;
62
68
  import org.postgresql.util.PGobject;
63
69
 
@@ -67,6 +73,52 @@ import org.postgresql.util.PGobject;
67
73
  */
68
74
  public class PostgreSQLRubyJdbcConnection extends arjdbc.jdbc.RubyJdbcConnection {
69
75
  private static final long serialVersionUID = 7235537759545717760L;
76
+ private static final int HSTORE_TYPE = 100000 + 1111;
77
+ private static final Pattern binaryStringPattern = Pattern.compile("^[01]+$");
78
+ private static final Pattern uuidPattern = Pattern.compile("^\\p{XDigit}{8}-(?:\\p{XDigit}{4}-){3}\\p{XDigit}{12}$");
79
+
80
+ private static final String[] binaryStrings = {
81
+ "0000",
82
+ "0001",
83
+ "0010",
84
+ "0011",
85
+ "0100",
86
+ "0101",
87
+ "0110",
88
+ "0111",
89
+ "1000",
90
+ "1001",
91
+ "1010",
92
+ "1011",
93
+ "1100",
94
+ "1101",
95
+ "1110",
96
+ "1111"
97
+ };
98
+
99
+ private static final Map<String, Integer> POSTGRES_JDBC_TYPE_FOR = new HashMap<String, Integer>(32, 1);
100
+ static {
101
+ POSTGRES_JDBC_TYPE_FOR.put("bit", Types.OTHER);
102
+ POSTGRES_JDBC_TYPE_FOR.put("bit_varying", Types.OTHER);
103
+ POSTGRES_JDBC_TYPE_FOR.put("citext", Types.OTHER);
104
+ POSTGRES_JDBC_TYPE_FOR.put("daterange", Types.OTHER);
105
+ POSTGRES_JDBC_TYPE_FOR.put("hstore", Types.OTHER);
106
+ POSTGRES_JDBC_TYPE_FOR.put("int4range", Types.OTHER);
107
+ POSTGRES_JDBC_TYPE_FOR.put("int8range", Types.OTHER);
108
+ POSTGRES_JDBC_TYPE_FOR.put("interval", Types.OTHER);
109
+ POSTGRES_JDBC_TYPE_FOR.put("json", Types.OTHER);
110
+ POSTGRES_JDBC_TYPE_FOR.put("jsonb", Types.OTHER);
111
+ POSTGRES_JDBC_TYPE_FOR.put("line", Types.OTHER);
112
+ POSTGRES_JDBC_TYPE_FOR.put("lseg", Types.OTHER);
113
+ POSTGRES_JDBC_TYPE_FOR.put("ltree", Types.OTHER);
114
+ POSTGRES_JDBC_TYPE_FOR.put("numrange", Types.OTHER);
115
+ POSTGRES_JDBC_TYPE_FOR.put("point", Types.OTHER);
116
+ POSTGRES_JDBC_TYPE_FOR.put("tsrange", Types.OTHER);
117
+ POSTGRES_JDBC_TYPE_FOR.put("tstzrange", Types.OTHER);
118
+ POSTGRES_JDBC_TYPE_FOR.put("tsvector", Types.OTHER);
119
+ POSTGRES_JDBC_TYPE_FOR.put("uuid", Types.OTHER);
120
+ }
121
+
70
122
 
71
123
  protected PostgreSQLRubyJdbcConnection(Ruby runtime, RubyClass metaClass) {
72
124
  super(runtime, metaClass);
@@ -128,6 +180,20 @@ public class PostgreSQLRubyJdbcConnection extends arjdbc.jdbc.RubyJdbcConnection
128
180
  return value;
129
181
  }
130
182
 
183
+ @Override
184
+ protected String internedTypeFor(final ThreadContext context, final IRubyObject attribute) throws SQLException {
185
+
186
+ final RubyModule postgreSQL = (RubyModule) getConnectionAdapters(context.runtime).getConstant("PostgreSQL");
187
+ final RubyModule oid = (RubyModule) postgreSQL.getConstant("OID");
188
+ final RubyClass arrayClass = oid.getClass("Array");
189
+
190
+ if ( arrayClass.isInstance(attributeType(context, attribute)) ) {
191
+ return "array";
192
+ }
193
+
194
+ return super.internedTypeFor(context, attribute);
195
+ }
196
+
131
197
  @Override
132
198
  protected Connection newConnection() throws SQLException {
133
199
  final Connection connection = getConnectionFactory().newConnection();
@@ -147,43 +213,27 @@ public class PostgreSQLRubyJdbcConnection extends arjdbc.jdbc.RubyJdbcConnection
147
213
  return connection;
148
214
  }
149
215
 
150
- @Override // due statement.setNull(index, Types.BLOB) not working :
151
- // org.postgresql.util.PSQLException: ERROR: column "sample_binary" is of type bytea but expression is of type oid
152
- protected void setBlobParameter(final ThreadContext context,
153
- final Connection connection, final PreparedStatement statement,
154
- final int index, final Object value,
155
- final IRubyObject column, final int type) throws SQLException {
156
- if ( value instanceof IRubyObject ) {
157
- setBlobParameter(context, connection, statement, index, (IRubyObject) value, column, type);
158
- }
159
- else {
160
- if ( value == null ) statement.setNull(index, Types.BINARY);
161
- else {
162
- statement.setBinaryStream(index, (InputStream) value);
163
- }
164
- }
165
- }
166
-
167
216
  @Override // due statement.setNull(index, Types.BLOB) not working :
168
217
  // org.postgresql.util.PSQLException: ERROR: column "sample_binary" is of type bytea but expression is of type oid
169
218
  protected void setBlobParameter(final ThreadContext context,
170
219
  final Connection connection, final PreparedStatement statement,
171
220
  final int index, final IRubyObject value,
172
- final IRubyObject column, final int type) throws SQLException {
221
+ final IRubyObject attribute, final int type) throws SQLException {
222
+
223
+ // TODO: Somewhere in the process of storing binary data, we lose about 50 bytes in one of the tests
224
+
173
225
  if ( value.isNil() ) {
174
226
  statement.setNull(index, Types.BINARY);
175
227
  }
176
- else {
177
- if ( value instanceof RubyIO ) { // IO/File
178
- statement.setBinaryStream(index, ((RubyIO) value).getInStream());
179
- }
180
- else { // should be a RubyString
181
- final ByteList blob = value.asString().getByteList();
182
- statement.setBinaryStream(index,
183
- new ByteArrayInputStream(blob.unsafeBytes(), blob.getBegin(), blob.getRealSize()),
184
- blob.getRealSize() // length
185
- );
186
- }
228
+ else if ( value instanceof RubyIO ) { // IO/File
229
+ statement.setBinaryStream(index, ((RubyIO) value).getInStream());
230
+ }
231
+ else { // should be a RubyString
232
+ final ByteList blob = value.asString().getByteList();
233
+ statement.setBinaryStream(index,
234
+ new ByteArrayInputStream(blob.unsafeBytes(), blob.getBegin(), blob.getRealSize()),
235
+ blob.getRealSize() // length
236
+ );
187
237
  }
188
238
  }
189
239
 
@@ -211,250 +261,178 @@ public class PostgreSQLRubyJdbcConnection extends arjdbc.jdbc.RubyJdbcConnection
211
261
  super.setTimestampParameter(context, connection, statement, index, value, column, type);
212
262
  }
213
263
 
214
- private static final ByteList INTERVAL =
215
- new ByteList( new byte[] { 'i','n','t','e','r','v','a','l' }, false );
216
-
217
- private static final ByteList ARRAY_END = new ByteList( new byte[] { '[',']' }, false );
218
-
219
264
  @Override
220
- protected void setStringParameter(final ThreadContext context,
265
+ protected void setObjectParameter(final ThreadContext context,
221
266
  final Connection connection, final PreparedStatement statement,
222
- final int index, final IRubyObject value,
223
- final IRubyObject column, final int type) throws SQLException {
224
- final RubyString sqlType;
225
- if ( column != null && ! column.isNil() ) {
226
- sqlType = (RubyString) column.callMethod(context, "sql_type");
227
- }
228
- else {
229
- sqlType = null;
230
- }
231
-
232
- if ( value.isNil() ) {
233
- if ( rawArrayType == Boolean.TRUE ) { // array's type is :string
234
- if ( sqlType != null && sqlType.getByteList().endsWith( ARRAY_END ) ) {
235
- statement.setNull(index, Types.ARRAY); return;
236
- }
237
- statement.setNull(index, type); return;
238
- }
239
- statement.setNull(index, Types.VARCHAR);
240
- }
241
- else {
242
- final String valueStr = value.asString().toString();
243
- if ( sqlType != null ) {
244
- if ( rawArrayType == Boolean.TRUE && sqlType.getByteList().endsWith( ARRAY_END ) ) {
245
- final int oid = oid(context, column);
246
- final Array valueArr = new Jdbc4Array(connection.unwrap(BaseConnection.class), oid, valueStr);
247
- statement.setArray(index, valueArr); return;
267
+ final int index, IRubyObject value,
268
+ final IRubyObject attribute, final int type) throws SQLException {
269
+
270
+ final String columnType = attributeSQLType(context, attribute).asJavaString();
271
+
272
+ switch ( columnType ) {
273
+ case "bit":
274
+ case "bit_varying":
275
+ setBitStringParameter(statement, index, value);
276
+ break;
277
+
278
+ case "cidr":
279
+ case "citext":
280
+ case "hstore":
281
+ case "inet":
282
+ case "ltree":
283
+ case "macaddr":
284
+ case "tsvector":
285
+ setPGobjectParameter(statement, index, value, columnType);
286
+ break;
287
+
288
+ case "interval":
289
+ statement.setObject(index, new PGInterval(value.toString()));
290
+ break;
291
+
292
+ case "json":
293
+ case "jsonb":
294
+ setJsonParameter(context, statement, index, value, columnType);
295
+ break;
296
+
297
+ case "line":
298
+ statement.setObject(index, new PGline(value.toString()));
299
+ break;
300
+
301
+ case "lseg":
302
+ statement.setObject(index, new PGlseg(value.toString()));
303
+ break;
304
+
305
+ case "point":
306
+ statement.setObject(index, new PGpoint(value.toString()));
307
+ break;
308
+
309
+ case "uuid":
310
+ setUUIDParameter(statement, index, value);
311
+ break;
312
+
313
+ default:
314
+ if ( columnType.endsWith("range") ) {
315
+ setRangeParameter(context, statement, index, value, columnType);
248
316
  }
249
- if ( sqlType.getByteList().startsWith( INTERVAL ) ) {
250
- statement.setObject( index, new PGInterval( valueStr ) ); return;
317
+ else {
318
+ super.setObjectParameter(context, connection, statement, index, value, attribute, type);
251
319
  }
252
- }
253
- statement.setString( index, valueStr );
254
320
  }
255
321
  }
256
322
 
257
- private static int oid(final ThreadContext context, final IRubyObject column) {
258
- // our column convention :
259
- IRubyObject oid = column.getInstanceVariables().getInstanceVariable("@oid");
260
- if ( oid == null || oid.isNil() ) { // only for user instantiated Column
261
- throw new IllegalStateException("missing @oid for column: " + column.inspect());
262
- }
263
- return RubyFixnum.fix2int(oid);
264
- }
265
-
266
- @Override
267
- protected void setObjectParameter(final ThreadContext context,
268
- final Connection connection, final PreparedStatement statement,
269
- final int index, Object value,
270
- final IRubyObject column, final int type) throws SQLException {
271
-
272
- final String columnType = column.callMethod(context, "type").asJavaString();
273
-
274
- if ( columnType == (Object) "uuid" ) {
275
- setUUIDParameter(statement, index, value);
276
- return;
277
- }
278
-
279
- if ( columnType == (Object) "json" ) {
280
- setJsonParameter(context, statement, index, value, column);
281
- return;
282
- }
283
-
284
- if ( columnType == (Object) "tsvector" ) {
285
- setTsVectorParameter(statement, index, value);
286
- return;
287
- }
288
-
289
- if ( columnType == (Object) "cidr" || columnType == (Object) "inet"
290
- || columnType == (Object) "macaddr" ) {
291
- setAddressParameter(context, statement, index, value, column, columnType);
292
- return;
293
- }
294
-
295
- if ( columnType != null && columnType.endsWith("range") ) {
296
- setRangeParameter(context, statement, index, value, column, columnType);
297
- return;
298
- }
323
+ // value should be a ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Bit::Data
324
+ private void setBitStringParameter(final PreparedStatement statement, final int index,
325
+ final IRubyObject value) throws SQLException {
299
326
 
300
- super.setObjectParameter(context, connection, statement, index, value, column, type);
301
- }
327
+ String valueForDB = value.toString();
328
+ int length = valueForDB.length();
302
329
 
303
- private void setUUIDParameter(
304
- final PreparedStatement statement, final int index,
305
- Object value) throws SQLException {
306
-
307
- if ( value instanceof IRubyObject ) {
308
- final IRubyObject rubyValue = (IRubyObject) value;
309
- if ( rubyValue.isNil() ) {
310
- statement.setNull(index, Types.OTHER); return;
330
+ /*
331
+ This means that if somebody sets their binary string to be "111" it
332
+ will always be assumed to be in binary. This matches the AR (5.0) functionality.
333
+ If it is meant to be a hex string they should use "0x111".
334
+ */
335
+ if (length > 0 && !binaryStringPattern.matcher(valueForDB).matches()) {
336
+ StringBuilder builder = new StringBuilder(length * 4);
337
+ for (int i = 0; i < length; i++) {
338
+ builder.append(binaryStrings[Character.digit(valueForDB.charAt(i), 16)]);
311
339
  }
312
- }
313
- else if ( value == null ) {
314
- statement.setNull(index, Types.OTHER); return;
340
+ valueForDB = builder.toString();
315
341
  }
316
342
 
317
- final Object uuid = UUID.fromString( value.toString() );
318
- statement.setObject(index, uuid);
343
+ setPGobjectParameter(statement, index, valueForDB, "bit");
319
344
  }
320
345
 
321
346
  private void setJsonParameter(final ThreadContext context,
322
347
  final PreparedStatement statement, final int index,
323
- Object value, final IRubyObject column) throws SQLException {
324
-
325
- if ( value instanceof IRubyObject ) {
326
- final IRubyObject rubyValue = (IRubyObject) value;
327
- if ( rubyValue.isNil() ) {
328
- statement.setNull(index, Types.OTHER); return;
329
- }
330
- if (!isAr42(column)) { // Value has already been cast for AR42
331
- value = column.getMetaClass().callMethod(context, "json_to_string", rubyValue);
332
- }
333
- }
334
- else if ( value == null ) {
335
- statement.setNull(index, Types.OTHER); return;
336
- }
348
+ final IRubyObject value, final String columnType) throws SQLException {
337
349
 
338
350
  final PGobject pgJson = new PGobject();
339
- pgJson.setType("json");
351
+ pgJson.setType(columnType);
340
352
  pgJson.setValue(value.toString());
341
353
  statement.setObject(index, pgJson);
342
354
  }
343
355
 
344
- private void setTsVectorParameter(
345
- final PreparedStatement statement, final int index,
346
- Object value) throws SQLException {
347
-
348
- if ( value instanceof IRubyObject ) {
349
- final IRubyObject rubyValue = (IRubyObject) value;
350
- if ( rubyValue.isNil() ) {
351
- statement.setNull(index, Types.OTHER); return;
352
- }
353
- }
354
- else if ( value == null ) {
355
- statement.setNull(index, Types.OTHER); return;
356
- }
357
-
358
- final PGobject pgTsVector = new PGobject();
359
- pgTsVector.setType("tsvector");
360
- pgTsVector.setValue(value.toString());
361
- statement.setObject(index, pgTsVector);
362
- }
363
-
364
- private void setAddressParameter(final ThreadContext context,
365
- final PreparedStatement statement, final int index,
366
- Object value, final IRubyObject column,
367
- final String columnType) throws SQLException {
368
-
369
- if ( value instanceof IRubyObject ) {
370
- final IRubyObject rubyValue = (IRubyObject) value;
371
- if ( rubyValue.isNil() ) {
372
- statement.setNull(index, Types.OTHER); return;
373
- }
374
- if (!isAr42(column)) { // Value has already been cast for AR42
375
- value = column.getMetaClass().callMethod(context, "cidr_to_string", rubyValue);
376
- }
377
- }
378
- else if ( value == null ) {
379
- statement.setNull(index, Types.OTHER); return;
380
- }
356
+ private void setPGobjectParameter(final PreparedStatement statement, final int index,
357
+ final Object value, final String columnType) throws SQLException {
381
358
 
382
- final PGobject pgAddress = new PGobject();
383
- pgAddress.setType(columnType);
384
- pgAddress.setValue(value.toString());
385
- statement.setObject(index, pgAddress);
359
+ final PGobject param = new PGobject();
360
+ param.setType(columnType);
361
+ param.setValue(value.toString());
362
+ statement.setObject(index, param);
386
363
  }
387
364
 
388
365
  private void setRangeParameter(final ThreadContext context,
389
366
  final PreparedStatement statement, final int index,
390
- final Object value, final IRubyObject column,
391
- final String columnType) throws SQLException {
367
+ final IRubyObject value, final String columnType) throws SQLException {
392
368
 
393
- final String rangeValue;
369
+ final String rangeValue = value.toString();
370
+ final Object pgRange;
394
371
 
395
- if ( value instanceof IRubyObject ) {
396
- final IRubyObject rubyValue = (IRubyObject) value;
397
- if ( rubyValue.isNil() ) {
398
- statement.setNull(index, Types.OTHER); return;
399
- }
400
- rangeValue = column.getMetaClass().callMethod(context, "range_to_string", rubyValue).toString();
401
- }
402
- else {
403
- if ( value == null ) {
404
- statement.setNull(index, Types.OTHER); return;
405
- }
406
- rangeValue = value.toString();
372
+ switch ( columnType ) {
373
+ case "daterange":
374
+ pgRange = new DateRangeType(rangeValue);
375
+ break;
376
+ case "tsrange":
377
+ pgRange = new TsRangeType(rangeValue);
378
+ break;
379
+ case "tstzrange":
380
+ pgRange = new TstzRangeType(rangeValue);
381
+ break;
382
+ case "int4range":
383
+ pgRange = new Int4RangeType(rangeValue);
384
+ break;
385
+ case "int8range":
386
+ pgRange = new Int8RangeType(rangeValue);
387
+ break;
388
+ default:
389
+ pgRange = new NumRangeType(rangeValue);
407
390
  }
408
391
 
409
- final Object pgRange;
410
- if ( columnType == (Object) "daterange" ) {
411
- pgRange = new DateRangeType(rangeValue);
412
- }
413
- else if ( columnType == (Object) "tsrange" ) {
414
- pgRange = new TsRangeType(rangeValue);
415
- }
416
- else if ( columnType == (Object) "tstzrange" ) {
417
- pgRange = new TstzRangeType(rangeValue);
418
- }
419
- else if ( columnType == (Object) "int4range" ) {
420
- pgRange = new Int4RangeType(rangeValue);
421
- }
422
- else if ( columnType == (Object) "int8range" ) {
423
- pgRange = new Int8RangeType(rangeValue);
424
- }
425
- else { // if ( columnType == (Object) "numrange" )
426
- pgRange = new NumRangeType(rangeValue);
427
- }
428
392
  statement.setObject(index, pgRange);
429
393
  }
430
394
 
431
395
  @Override
432
- protected String resolveArrayBaseTypeName(final ThreadContext context,
433
- final Object value, final IRubyObject column, final int type) {
434
- String sqlType = column.callMethod(context, "sql_type").toString();
435
- if ( sqlType.startsWith("character varying") ) return "text";
436
- final int index = sqlType.indexOf('('); // e.g. "character varying(255)"
437
- if ( index > 0 ) sqlType = sqlType.substring(0, index);
438
- return sqlType;
396
+ protected void setStringParameter(final ThreadContext context,
397
+ final Connection connection, final PreparedStatement statement,
398
+ final int index, final IRubyObject value,
399
+ final IRubyObject attribute, final int type) throws SQLException {
400
+
401
+ if ( attributeSQLType(context, attribute).isNil() ) {
402
+ /*
403
+ We have to check for a uuid here because in some cases
404
+ (for example, when doing "exists?" checks, or with legacy binds)
405
+ ActiveRecord doesn't send us the actual type of the attribute
406
+ and Postgres won't compare a uuid column with a string
407
+ */
408
+ final String uuid = value.toString();
409
+
410
+ // Checking the length so we don't have the overhead of the regex unless it "looks" like a UUID
411
+ if ( uuid.length() == 36 && uuidPattern.matcher(uuid).matches() ) {
412
+ setUUIDParameter(statement, index, value);
413
+ return;
414
+ }
415
+ }
416
+
417
+ super.setStringParameter(context, connection, statement, index, value, attribute, type);
439
418
  }
440
419
 
441
- private static final int HSTORE_TYPE = 100000 + 1111;
420
+ private void setUUIDParameter(final PreparedStatement statement,
421
+ final int index, final IRubyObject value) throws SQLException {
422
+
423
+ statement.setObject(index, UUID.fromString(value.toString()));
424
+ }
442
425
 
443
426
  @Override
444
- protected int jdbcTypeFor(final ThreadContext context, final Ruby runtime,
445
- final IRubyObject column, final Object value) throws SQLException {
446
- // NOTE: likely wrong but native adapters handles this thus we should
447
- // too - used from #table_exists? `binds << [ nil, schema ] if schema`
448
- if ( column == null || column.isNil() ) return Types.VARCHAR; // assume type == :string
449
- final int type = super.jdbcTypeFor(context, runtime, column, value);
450
- /*
451
- if ( type == Types.OTHER ) {
452
- final IRubyObject columnType = column.callMethod(context, "type");
453
- if ( "hstore" == (Object) columnType.asJavaString() ) {
454
- return HSTORE_TYPE;
455
- }
456
- } */
457
- return type;
427
+ protected Integer jdbcTypeFor(final String type) {
428
+
429
+ Integer typeValue = POSTGRES_JDBC_TYPE_FOR.get(type);
430
+
431
+ if ( typeValue != null ) {
432
+ return typeValue;
433
+ }
434
+
435
+ return super.jdbcTypeFor(type);
458
436
  }
459
437
 
460
438
  /**
@@ -498,12 +476,8 @@ public class PostgreSQLRubyJdbcConnection extends arjdbc.jdbc.RubyJdbcConnection
498
476
  if ( rawDateTime != null && rawDateTime.booleanValue() ) return strValue;
499
477
 
500
478
  final IRubyObject adapter = callMethod(context, "adapter"); // self.adapter
501
- if ( usesType(runtime) ) {
502
- return typeCastFromDatabase(context, adapter, runtime.newSymbol("timestamp"), strValue);
503
- }
504
479
 
505
- if ( adapter.isNil() ) return strValue; // NOTE: we warn on init_connection
506
- return adapter.callMethod(context, "_string_to_timestamp", strValue);
480
+ return typeCastFromDatabase(context, adapter, runtime.newSymbol("timestamp"), strValue);
507
481
  }
508
482
 
509
483
  @Override