Mercurial > hg > monetdb-java
comparison src/main/java/org/monetdb/jdbc/MonetResultSetMetaData.java @ 713:c3c424a90a42
Improve implementation of ResultSet.getMetaData().
The current implementation creates a new ResultSetMetaData each time this method is called
which is quite costly if it is called from inside a fetch-loop such as in the example on:
https://en.wikipedia.org/wiki/Java_Database_Connectivity#Examples
try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM MyTable")) {
while (rs.next()) {
int numColumns = rs.getMetaData().getColumnCount();
for (int i = 1; i <= numColumns; i++) {
// Column numbers start at 1.
// Also there are many methods on the result set to return
// the column as a particular type. Refer to the Sun documentation
// for the list of valid conversions.
System.out.println( "COLUMN " + i + " = " + rs.getObject(i));
}
}
}
As the ResultSetMetaData is static for a ResultSet it is better to create it once,
cache it in the ResultSet object and return the cached object for next calls to ResultSet.getMetaData().
author | Martin van Dinther <martin.van.dinther@monetdbsolutions.com> |
---|---|
date | Wed, 04 Jan 2023 23:34:14 +0100 (2023-01-04) |
parents | |
children | ad7b08ef7745 |
comparison
equal
deleted
inserted
replaced
712:7cec464246f2 | 713:c3c424a90a42 |
---|---|
1 /* | |
2 * This Source Code Form is subject to the terms of the Mozilla Public | |
3 * License, v. 2.0. If a copy of the MPL was not distributed with this | |
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. | |
5 * | |
6 * Copyright 1997 - July 2008 CWI, August 2008 - 2023 MonetDB B.V. | |
7 */ | |
8 | |
9 package org.monetdb.jdbc; | |
10 | |
11 import java.sql.SQLException; | |
12 import java.sql.ResultSetMetaData; | |
13 import java.sql.Types; | |
14 import java.util.Map; | |
15 | |
16 /** | |
17 *<pre> | |
18 * A {@link ResultSetMetaData} suitable for the MonetDB database. | |
19 * | |
20 * An object that can be used to get information about the types and | |
21 * properties of the columns in a ResultSet object. | |
22 *</pre> | |
23 * | |
24 * @author Martin van Dinther | |
25 * @version 1.0 | |
26 */ | |
27 public final class MonetResultSetMetaData | |
28 extends MonetWrapper | |
29 implements ResultSetMetaData | |
30 { | |
31 /** The parental Connection object */ | |
32 private final MonetConnection conn; | |
33 | |
34 /** A ResultSetResponse object to retrieve resultset metadata from */ | |
35 private final MonetConnection.ResultSetResponse header; | |
36 | |
37 /** The schema names of the columns in this ResultSet */ | |
38 private final String[] schemas; | |
39 /** The table names of the columns in this ResultSet */ | |
40 private final String[] tables; | |
41 /** The names of the columns in this ResultSet */ | |
42 private final String[] columns; | |
43 /** The MonetDB type names of the columns in this ResultSet */ | |
44 private final String[] types; | |
45 /** The JDBC SQL type codes of the columns in this ResultSet. | |
46 * The content will be derived once from the MonetDB String[] types */ | |
47 private final int[] JdbcSQLTypes; | |
48 /** The lengths of the columns in this ResultSet */ | |
49 private final int[] lengths; | |
50 /** The precisions of the columns in this ResultSet */ | |
51 private final int[] precisions; | |
52 /** The scales of the columns in this ResultSet */ | |
53 private final int[] scales; | |
54 | |
55 /* For the methods: isNullable() and isAutoIncrement(), we need to query the server. | |
56 * To do this efficiently we query many columns combined in one SELECT | |
57 * query and cache the results in following arrays. | |
58 */ | |
59 private final int array_size; | |
60 /** Whether info for a column is already queried or not */ | |
61 private final boolean[] _is_queried; | |
62 /** Whether info for a column is already fetched or not */ | |
63 private final boolean[] _is_fetched; | |
64 /** The nullability of the columns in this ResultSet */ | |
65 private final int[] _isNullable; | |
66 /** The auto increment property of the columns in this ResultSet */ | |
67 private final boolean[] _isAutoincrement; | |
68 /** an upper bound value to calculate the range of columns to query */ | |
69 private int nextUpperbound; | |
70 | |
71 | |
72 /** | |
73 * Main constructor backed by the given connection and header. | |
74 * | |
75 * @param connection the parent connection | |
76 * @param header a ResultSetResponse containing the metadata | |
77 * @throws IllegalArgumentException if called with null for one of the arguments | |
78 */ | |
79 MonetResultSetMetaData( | |
80 final MonetConnection connection, | |
81 final MonetConnection.ResultSetResponse header) | |
82 throws IllegalArgumentException | |
83 { | |
84 if (connection == null) { | |
85 throw new IllegalArgumentException("Connection may not be null!"); | |
86 } | |
87 if (header == null) { | |
88 throw new IllegalArgumentException("Header may not be null!"); | |
89 } | |
90 this.conn = connection; | |
91 this.header = header; | |
92 schemas = header.getSchemaNames(); | |
93 tables = header.getTableNames(); | |
94 columns = header.getNames(); | |
95 lengths = header.getColumnLengths(); | |
96 types = header.getTypes(); | |
97 precisions = header.getColumnPrecisions(); | |
98 scales = header.getColumnScales(); | |
99 | |
100 if (columns.length != tables.length || columns.length != types.length ) { | |
101 throw new IllegalArgumentException("Inconsistent Header metadata"); | |
102 } | |
103 | |
104 // derive the JDBC SQL type codes from the types[] names once | |
105 JdbcSQLTypes = new int[types.length]; | |
106 for (int i = 0; i < types.length; i++) { | |
107 int javaSQLtype = MonetDriver.getJdbcSQLType(types[i]); | |
108 if (javaSQLtype == Types.CLOB) { | |
109 if (connection.mapClobAsVarChar()) | |
110 javaSQLtype = Types.VARCHAR; | |
111 } else | |
112 if (javaSQLtype == Types.BLOB) { | |
113 if (connection.mapBlobAsVarBinary()) | |
114 javaSQLtype = Types.VARBINARY; | |
115 } | |
116 JdbcSQLTypes[i] = javaSQLtype; | |
117 } | |
118 | |
119 // initialize structures for storing columns info on nullability and autoincrement | |
120 array_size = columns.length + 1; // add 1 as in JDBC columns start from 1 (array from 0). | |
121 _is_queried = new boolean[array_size]; | |
122 _is_fetched = new boolean[array_size]; | |
123 _isNullable = new int[array_size]; | |
124 _isAutoincrement = new boolean[array_size]; | |
125 nextUpperbound = array_size; | |
126 } | |
127 | |
128 /** | |
129 * Returns the number of columns in this ResultSet object. | |
130 * | |
131 * @return the number of columns | |
132 */ | |
133 @Override | |
134 public int getColumnCount() { | |
135 // for debug: System.out.println("In rsmd.getColumnCount() = " + columns.length + ". this rsmd object = " + this.toString()); | |
136 return columns.length; | |
137 } | |
138 | |
139 /** | |
140 * Indicates whether the designated column is automatically numbered. | |
141 * | |
142 * This method is currently very expensive for BIGINT, | |
143 * INTEGER, SMALLINT and TINYINT result column types | |
144 * as it needs to retrieve the information from the | |
145 * database using an SQL meta data query. | |
146 * | |
147 * @param column the first column is 1, the second is 2, ... | |
148 * @return true if so; false otherwise | |
149 * @throws SQLException if there is no such column or a database access error occurs | |
150 */ | |
151 @Override | |
152 public boolean isAutoIncrement(final int column) throws SQLException { | |
153 // only few integer types can be auto incrementable in MonetDB | |
154 // see: https://www.monetdb.org/Documentation/SQLReference/DataTypes/SerialDatatypes | |
155 switch (getColumnType(column)) { | |
156 case Types.BIGINT: | |
157 case Types.INTEGER: | |
158 case Types.SMALLINT: | |
159 case Types.TINYINT: | |
160 try { | |
161 if (_is_fetched[column] != true) { | |
162 fetchColumnInfo(column); | |
163 } | |
164 return _isAutoincrement[column]; | |
165 } catch (IndexOutOfBoundsException e) { | |
166 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
167 } | |
168 } | |
169 | |
170 return false; | |
171 } | |
172 | |
173 /** | |
174 * Indicates whether a column's case matters. | |
175 * | |
176 * @param column the first column is 1, the second is 2, ... | |
177 * @return true for all character string columns else false | |
178 * @throws SQLException if there is no such column | |
179 */ | |
180 @Override | |
181 public boolean isCaseSensitive(final int column) throws SQLException { | |
182 switch (getColumnType(column)) { | |
183 case Types.CHAR: | |
184 case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness | |
185 case Types.CLOB: | |
186 return true; | |
187 case Types.VARCHAR: | |
188 final String monettype = getColumnTypeName(column); | |
189 if (monettype != null && monettype.length() == 4) { | |
190 // data of type inet or uuid is not case sensitive | |
191 if ("inet".equals(monettype) | |
192 || "uuid".equals(monettype)) | |
193 return false; | |
194 } | |
195 return true; | |
196 } | |
197 | |
198 return false; | |
199 } | |
200 | |
201 /** | |
202 * Indicates whether the designated column can be used in a | |
203 * where clause. | |
204 * | |
205 * Returning true for all here, even for CLOB, BLOB. | |
206 * | |
207 * @param column the first column is 1, the second is 2, ... | |
208 * @return true | |
209 * @throws SQLException if there is no such column | |
210 */ | |
211 @Override | |
212 public boolean isSearchable(final int column) throws SQLException { | |
213 checkColumnIndexValidity(column); | |
214 return true; | |
215 } | |
216 | |
217 /** | |
218 * Indicates whether the designated column is a cash value. | |
219 * From the MonetDB database perspective it is by definition | |
220 * unknown whether the value is a currency, because there are | |
221 * no currency datatypes such as MONEY. With this knowledge | |
222 * we can always return false here. | |
223 * | |
224 * @param column the first column is 1, the second is 2, ... | |
225 * @return false | |
226 * @throws SQLException if there is no such column | |
227 */ | |
228 @Override | |
229 public boolean isCurrency(final int column) throws SQLException { | |
230 checkColumnIndexValidity(column); | |
231 return false; | |
232 } | |
233 | |
234 /** | |
235 * Indicates the nullability of values in the designated column. | |
236 * | |
237 * This method is currently very expensive as it needs to | |
238 * retrieve the information from the database using an SQL | |
239 * meta data query. | |
240 * | |
241 * @param column the first column is 1, the second is 2, ... | |
242 * @return the nullability status of the given column; one of | |
243 * columnNoNulls, columnNullable or columnNullableUnknown | |
244 * @throws SQLException if there is no such column or a database access error occurs | |
245 */ | |
246 @Override | |
247 public int isNullable(final int column) throws SQLException { | |
248 checkColumnIndexValidity(column); | |
249 try { | |
250 if (_is_fetched[column] != true) { | |
251 fetchColumnInfo(column); | |
252 } | |
253 return _isNullable[column]; | |
254 } catch (IndexOutOfBoundsException e) { | |
255 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
256 } | |
257 } | |
258 | |
259 /** | |
260 * Indicates whether values in the designated column are signed | |
261 * numbers. | |
262 * Within MonetDB all numeric types (except oid and ptr) are signed. | |
263 * | |
264 * @param column the first column is 1, the second is 2, ... | |
265 * @return true if so; false otherwise | |
266 * @throws SQLException if there is no such column | |
267 */ | |
268 @Override | |
269 public boolean isSigned(final int column) throws SQLException { | |
270 // we can hardcode this, based on the colum type | |
271 switch (getColumnType(column)) { | |
272 case Types.TINYINT: | |
273 case Types.SMALLINT: | |
274 case Types.INTEGER: | |
275 case Types.REAL: | |
276 case Types.FLOAT: | |
277 case Types.DOUBLE: | |
278 case Types.DECIMAL: | |
279 case Types.NUMERIC: | |
280 return true; | |
281 case Types.BIGINT: | |
282 final String monettype = getColumnTypeName(column); | |
283 if (monettype != null && monettype.length() == 3) { | |
284 // data of type oid or ptr is not signed | |
285 if ("oid".equals(monettype) | |
286 || "ptr".equals(monettype)) | |
287 return false; | |
288 } | |
289 return true; | |
290 // All other types should return false | |
291 // case Types.BOOLEAN: | |
292 // case Types.DATE: // can year be negative? | |
293 // case Types.TIME: // can time be negative? | |
294 // case Types.TIME_WITH_TIMEZONE: | |
295 // case Types.TIMESTAMP: // can year be negative? | |
296 // case Types.TIMESTAMP_WITH_TIMEZONE: | |
297 default: | |
298 return false; | |
299 } | |
300 } | |
301 | |
302 /** | |
303 * Indicates the designated column's normal maximum width in | |
304 * characters. | |
305 * | |
306 * @param column the first column is 1, the second is 2, ... | |
307 * @return the normal maximum number of characters allowed as the | |
308 * width of the designated column | |
309 * @throws SQLException if there is no such column | |
310 */ | |
311 @Override | |
312 public int getColumnDisplaySize(final int column) throws SQLException { | |
313 checkColumnIndexValidity(column); | |
314 if (lengths != null) { | |
315 try { | |
316 return lengths[column - 1]; | |
317 } catch (IndexOutOfBoundsException e) { | |
318 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
319 } | |
320 } | |
321 return 1; | |
322 } | |
323 | |
324 /** | |
325 * Gets the designated column's suggested title for use in | |
326 * printouts and displays. The suggested title is usually | |
327 * specified by the SQL AS clause. If a SQL AS is not specified, | |
328 * the value returned from getColumnLabel will be the same as | |
329 * the value returned by the getColumnName method. | |
330 * | |
331 * @param column the first column is 1, the second is 2, ... | |
332 * @return the suggested column title | |
333 * @throws SQLException if there is no such column | |
334 */ | |
335 @Override | |
336 public String getColumnLabel(final int column) throws SQLException { | |
337 return getColumnName(column); | |
338 } | |
339 | |
340 /** | |
341 * Gets the designated column's name | |
342 * | |
343 * @param column the first column is 1, the second is 2, ... | |
344 * @return the column name | |
345 * @throws SQLException if there is no such column | |
346 */ | |
347 @Override | |
348 public String getColumnName(final int column) throws SQLException { | |
349 checkColumnIndexValidity(column); | |
350 try { | |
351 return columns[column - 1]; | |
352 } catch (IndexOutOfBoundsException e) { | |
353 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
354 } | |
355 } | |
356 | |
357 /** | |
358 * Gets the designated column's table's catalog name. | |
359 * MonetDB does not support the catalog naming concept as in: catalog.schema.table naming scheme | |
360 * | |
361 * @param column the first column is 1, the second is 2, ... | |
362 * @return null or the name of the catalog for the table in which the given | |
363 * column appears or "" if not applicable | |
364 * @throws SQLException if there is no such column | |
365 */ | |
366 @Override | |
367 public String getCatalogName(final int column) throws SQLException { | |
368 checkColumnIndexValidity(column); | |
369 return null; // MonetDB does NOT support catalog qualifiers | |
370 | |
371 } | |
372 | |
373 /** | |
374 * Get the designated column's schema name. | |
375 * | |
376 * @param column the first column is 1, the second is 2, ... | |
377 * @return schema name or "" if not applicable | |
378 * @throws SQLException if there is no such column | |
379 */ | |
380 @Override | |
381 public String getSchemaName(final int column) throws SQLException { | |
382 checkColumnIndexValidity(column); | |
383 if (schemas != null) { | |
384 try { | |
385 return schemas[column - 1]; | |
386 } catch (IndexOutOfBoundsException e) { | |
387 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
388 } | |
389 } | |
390 return ""; | |
391 } | |
392 | |
393 /** | |
394 * Gets the designated column's table name. | |
395 * | |
396 * @param column the first column is 1, the second is 2, ... | |
397 * @return table name or "" if not applicable | |
398 * @throws SQLException if there is no such column | |
399 */ | |
400 @Override | |
401 public String getTableName(final int column) throws SQLException { | |
402 checkColumnIndexValidity(column); | |
403 if (tables != null) { | |
404 try { | |
405 return tables[column - 1]; | |
406 } catch (IndexOutOfBoundsException e) { | |
407 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
408 } | |
409 } | |
410 return ""; | |
411 } | |
412 | |
413 /** | |
414 * Retrieves the designated column's SQL type. | |
415 * | |
416 * @param column the first column is 1, the second is 2, ... | |
417 * @return SQL type from java.sql.Types | |
418 * @throws SQLException if there is no such column | |
419 */ | |
420 @Override | |
421 public int getColumnType(final int column) throws SQLException { | |
422 checkColumnIndexValidity(column); | |
423 try { | |
424 return JdbcSQLTypes[column - 1]; | |
425 } catch (IndexOutOfBoundsException e) { | |
426 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
427 } | |
428 } | |
429 | |
430 /** | |
431 * Retrieves the designated column's database-specific type name. | |
432 * | |
433 * @param column the first column is 1, the second is 2, ... | |
434 * @return type name used by the database. If the column type is a | |
435 * user-defined type, then a fully-qualified type name is | |
436 * returned. | |
437 * @throws SQLException if there is no such column | |
438 */ | |
439 @Override | |
440 public String getColumnTypeName(final int column) throws SQLException { | |
441 checkColumnIndexValidity(column); | |
442 try { | |
443 final String monettype = types[column - 1]; | |
444 if (monettype.endsWith("_interval")) { | |
445 /* convert the interval type names to valid SQL data type names, | |
446 * such that generic applications can use them in create table statements | |
447 */ | |
448 if ("day_interval".equals(monettype)) | |
449 return "interval day"; | |
450 if ("month_interval".equals(monettype)) | |
451 return "interval month"; | |
452 if ("sec_interval".equals(monettype)) | |
453 return "interval second"; | |
454 } | |
455 return monettype; | |
456 } catch (IndexOutOfBoundsException e) { | |
457 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
458 } | |
459 } | |
460 | |
461 /** | |
462 * Get the designated column's specified column size. | |
463 * For numeric data, this is the maximum precision. | |
464 * For character data, this is the length in characters. | |
465 * For datetime datatypes, this is the length in characters | |
466 * of the String representation (assuming the maximum | |
467 * allowed precision of the fractional seconds component). | |
468 * For binary data, this is the length in bytes. | |
469 * For the ROWID datatype, this is the length in bytes. | |
470 * 0 is returned for data types where the column size is not applicable. | |
471 * | |
472 * @param column the first column is 1, the second is 2, ... | |
473 * @return precision | |
474 * @throws SQLException if there is no such column | |
475 */ | |
476 @Override | |
477 public int getPrecision(final int column) throws SQLException { | |
478 switch (getColumnType(column)) { | |
479 case Types.BIGINT: | |
480 return 19; | |
481 case Types.INTEGER: | |
482 return 10; | |
483 case Types.SMALLINT: | |
484 return 5; | |
485 case Types.TINYINT: | |
486 return 3; | |
487 case Types.REAL: | |
488 return 7; | |
489 case Types.FLOAT: | |
490 case Types.DOUBLE: | |
491 return 15; | |
492 case Types.DECIMAL: | |
493 case Types.NUMERIC: | |
494 // these data types have a variable precision (max precision is 38) | |
495 if (precisions != null) { | |
496 try { | |
497 return precisions[column - 1]; | |
498 } catch (IndexOutOfBoundsException e) { | |
499 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
500 } | |
501 } | |
502 return 18; | |
503 case Types.CHAR: | |
504 case Types.VARCHAR: | |
505 case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness | |
506 case Types.CLOB: | |
507 // these data types have a variable length | |
508 if (precisions != null) { | |
509 try { | |
510 int prec = precisions[column - 1]; | |
511 if (prec <= 0) { | |
512 // apparently no positive precision or max length could be fetched | |
513 // use columnDisplaySize() value as fallback | |
514 prec = getColumnDisplaySize(column); | |
515 precisions[column - 1] = prec; | |
516 } | |
517 return prec; | |
518 } catch (IndexOutOfBoundsException e) { | |
519 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
520 } | |
521 } | |
522 // apparently no precisions array is available | |
523 // use columnDisplaySize() value as alternative | |
524 return getColumnDisplaySize(column); | |
525 case Types.BINARY: | |
526 case Types.VARBINARY: | |
527 case Types.BLOB: | |
528 // these data types have a variable length | |
529 if (precisions != null) { | |
530 try { | |
531 int prec = precisions[column - 1]; | |
532 if (prec <= 0) { | |
533 // apparently no positive precision or max length could be fetched | |
534 // use columnDisplaySize() value as fallback | |
535 // It expect number of bytes, not number of hex chars | |
536 prec = (getColumnDisplaySize(column) / 2) +1; | |
537 precisions[column - 1] = prec; | |
538 } | |
539 return prec; | |
540 } catch (IndexOutOfBoundsException e) { | |
541 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
542 } | |
543 } | |
544 // apparently no precisions array is available | |
545 // use columnDisplaySize() value as alternative | |
546 // It expect number of bytes, not number of hex chars | |
547 return (getColumnDisplaySize(column) / 2) +1; | |
548 case Types.DATE: | |
549 return 10; // 2020-10-08 | |
550 case Types.TIME: | |
551 return 15; // 21:51:34.399753 | |
552 case Types.TIME_WITH_TIMEZONE: | |
553 return 21; // 21:51:34.399753+02:00 | |
554 case Types.TIMESTAMP: | |
555 return 26; // 2020-10-08 21:51:34.399753 | |
556 case Types.TIMESTAMP_WITH_TIMEZONE: | |
557 return 32; // 2020-10-08 21:51:34.399753+02:00 | |
558 case Types.BOOLEAN: | |
559 return 1; | |
560 default: | |
561 // All other types should return 0 | |
562 return 0; | |
563 } | |
564 } | |
565 | |
566 /** | |
567 * Gets the designated column's number of digits to right of | |
568 * the decimal point. | |
569 * 0 is returned for data types where the scale is not applicable. | |
570 * | |
571 * @param column the first column is 1, the second is 2, ... | |
572 * @return scale | |
573 * @throws SQLException if there is no such column | |
574 */ | |
575 @Override | |
576 public int getScale(final int column) throws SQLException { | |
577 switch (getColumnType(column)) { | |
578 case Types.DECIMAL: | |
579 case Types.NUMERIC: | |
580 { | |
581 // these data types may have a variable scale, max scale is 38 | |
582 | |
583 // Special handling for: day_interval and sec_interval as they are | |
584 // mapped to Types.NUMERIC and Types.DECIMAL types (see MonetDriver typeMap) | |
585 // They appear to have a fixed scale (tested against Oct2020) | |
586 final String monettype = getColumnTypeName(column); | |
587 if ("interval day".equals(monettype)) | |
588 return 0; | |
589 if ("interval second".equals(monettype)) | |
590 return 3; | |
591 | |
592 if (scales != null) { | |
593 try { | |
594 return scales[column - 1]; | |
595 } catch (IndexOutOfBoundsException e) { | |
596 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
597 } | |
598 } | |
599 return 0; | |
600 } | |
601 case Types.TIME: | |
602 case Types.TIME_WITH_TIMEZONE: | |
603 case Types.TIMESTAMP: | |
604 case Types.TIMESTAMP_WITH_TIMEZONE: | |
605 if (scales != null) { | |
606 try { | |
607 return scales[column - 1]; | |
608 } catch (IndexOutOfBoundsException e) { | |
609 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
610 } | |
611 } | |
612 // support microseconds, so scale 6 | |
613 return 6; // 21:51:34.399753 | |
614 | |
615 // All other types should return 0 | |
616 // case Types.BIGINT: | |
617 // case Types.INTEGER: | |
618 // case Types.SMALLINT: | |
619 // case Types.TINYINT: | |
620 // case Types.REAL: | |
621 // case Types.FLOAT: | |
622 // case Types.DOUBLE: | |
623 // case Types.CHAR: | |
624 // case Types.VARCHAR: | |
625 // case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness | |
626 // case Types.CLOB: | |
627 // case Types.BINARY: | |
628 // case Types.VARBINARY: | |
629 // case Types.BLOB: | |
630 // case Types.DATE: | |
631 // case Types.BOOLEAN: | |
632 default: | |
633 return 0; | |
634 } | |
635 } | |
636 | |
637 /** | |
638 * Indicates whether the designated column is definitely not | |
639 * writable. MonetDB does not support cursor updates, so | |
640 * nothing is writable. | |
641 * | |
642 * @param column the first column is 1, the second is 2, ... | |
643 * @return true if so; false otherwise | |
644 * @throws SQLException if there is no such column | |
645 */ | |
646 @Override | |
647 public boolean isReadOnly(final int column) throws SQLException { | |
648 checkColumnIndexValidity(column); | |
649 return true; | |
650 } | |
651 | |
652 /** | |
653 * Indicates whether it is possible for a write on the | |
654 * designated column to succeed. | |
655 * | |
656 * @param column the first column is 1, the second is 2, ... | |
657 * @return true if so; false otherwise | |
658 * @throws SQLException if there is no such column | |
659 */ | |
660 @Override | |
661 public boolean isWritable(final int column) throws SQLException { | |
662 checkColumnIndexValidity(column); | |
663 return false; | |
664 } | |
665 | |
666 /** | |
667 * Indicates whether a write on the designated column will | |
668 * definitely succeed. | |
669 * | |
670 * @param column the first column is 1, the second is 2, ... | |
671 * @return true if so; false otherwise | |
672 * @throws SQLException if there is no such column | |
673 */ | |
674 @Override | |
675 public boolean isDefinitelyWritable(final int column) throws SQLException { | |
676 checkColumnIndexValidity(column); | |
677 return false; | |
678 } | |
679 | |
680 /** | |
681 * Returns the fully-qualified name of the Java class whose instances | |
682 * are manufactured if the method ResultSet.getObject is called to | |
683 * retrieve a value from the column. | |
684 * ResultSet.getObject may return a subclass of the class returned by | |
685 * this method. | |
686 * | |
687 * @param column the first column is 1, the second is 2, ... | |
688 * @return the fully-qualified name of the class in the Java programming | |
689 * language that would be used by the method ResultSet.getObject | |
690 * to retrieve the value in the specified column. This is the | |
691 * class name used for custom mapping. | |
692 * @throws SQLException if there is no such column | |
693 */ | |
694 @Override | |
695 public String getColumnClassName(final int column) throws SQLException { | |
696 checkColumnIndexValidity(column); | |
697 try { | |
698 final String MonetDBType = types[column - 1]; | |
699 Class<?> type = null; | |
700 if (conn != null) { | |
701 final Map<String,Class<?>> map = conn.getTypeMap(); | |
702 if (map != null && map.containsKey(MonetDBType)) { | |
703 type = (Class)map.get(MonetDBType); | |
704 } | |
705 } | |
706 if (type == null) { | |
707 // fallback to the standard SQL type Class mappings | |
708 type = MonetResultSet.getClassForType(JdbcSQLTypes[column - 1]); | |
709 } | |
710 if (type != null) { | |
711 return type.getCanonicalName(); | |
712 } | |
713 throw new SQLException("column type mapping null: " + MonetDBType, "M0M03"); | |
714 } catch (IndexOutOfBoundsException e) { | |
715 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
716 } | |
717 } | |
718 | |
719 | |
720 /** | |
721 * A private utility method to check validity of column index number | |
722 * | |
723 * @param column the first column is 1, the second is 2, ... | |
724 * @throws SQLDataException when invalid column index number | |
725 */ | |
726 private final void checkColumnIndexValidity(final int column) throws java.sql.SQLDataException { | |
727 if (column < 1 || column > columns.length) | |
728 throw MonetResultSet.newSQLInvalidColumnIndexException(column); | |
729 } | |
730 | |
731 /** | |
732 * A private method to fetch the isNullable and isAutoincrement values | |
733 * combined for a specific column. | |
734 * The fetched values are stored in the array caches. | |
735 * | |
736 * @param column the first column is 1, the second is 2, ... | |
737 * @throws SQLException if there is no such column | |
738 */ | |
739 private final void fetchColumnInfo(final int column) throws SQLException { | |
740 // for debug: System.out.println("fetchColumnInfo(" + column + ")"); | |
741 checkColumnIndexValidity(column); | |
742 if (_is_fetched[column] != true) { | |
743 // fetch column info for multiple columns combined in one go | |
744 fetchManyColumnsInfo(column); | |
745 } | |
746 if (_is_fetched[column]) | |
747 return; | |
748 | |
749 // apparently no data could be fetched for this resultset column, fall back to defaults | |
750 _isNullable[column] = columnNullableUnknown; | |
751 _isAutoincrement[column] = false; | |
752 } | |
753 | |
754 /** | |
755 * A private method to fetch the isNullable and isAutoincrement values | |
756 * for many fully qualified columns combined in one SQL query to reduce the number of queries sent. | |
757 * As fetching this meta information from the server per column is costly we combine the querying of | |
758 * the isNullable and isAutoincrement values and cache it in internal arrays. | |
759 * We also do this for many columns combined in one query to reduce | |
760 * the number of queries needed for fetching this metadata for all resultset columns. | |
761 * Many generic JDBC database tools (e.g. SQuirreL, DBeaver) request this meta data for each | |
762 * column of each resultset, so these optimisations reduces the number of meta data queries significantly. | |
763 * | |
764 * @param column the first column is 1, the second is 2, ... | |
765 * @throws SQLException if a database access error occurs | |
766 */ | |
767 private final void fetchManyColumnsInfo(final int column) throws SQLException { | |
768 // for debug: System.out.println("fetchManyColumnsInfo(" + column + ")"); | |
769 | |
770 // Most queries have less than 80 resultset columns | |
771 // So 80 is a good balance between speedup (up to 79x) and size of generated query sent to server | |
772 final int MAX_COLUMNS_PER_QUERY = 80; | |
773 | |
774 // Determine the optimal startcol to make use of fetching up to 80 columns in one query. | |
775 int startcol = column; | |
776 if ((startcol > 1) && (startcol + MAX_COLUMNS_PER_QUERY >= nextUpperbound)) { | |
777 // we can fetch info from more columns in one query if we start with a lower startcol | |
778 startcol = nextUpperbound - MAX_COLUMNS_PER_QUERY; | |
779 if (startcol < 1) { | |
780 startcol = 1; | |
781 } else | |
782 if (startcol > column) { | |
783 startcol = column; | |
784 } | |
785 nextUpperbound = startcol; // next time this nextUpperbound value will be used | |
786 // for debug: System.out.println("fetchManyColumnsInfo(" + column + ")" + (startcol != column ? " changed into startcol: " + startcol : "") + " nextUpperbound: " + nextUpperbound); | |
787 } | |
788 | |
789 final StringBuilder query = new StringBuilder(410 + (MAX_COLUMNS_PER_QUERY * 150)); | |
790 /* next SQL query is a simplified version of query in MonetDatabaseMetaData.getColumns(), to fetch only the needed attributes of a column */ | |
791 query.append("SELECT " + | |
792 "s.\"name\" AS schnm, " + | |
793 "t.\"name\" AS tblnm, " + | |
794 "c.\"name\" AS colnm, " + | |
795 "cast(CASE c.\"null\" WHEN true THEN ").append(ResultSetMetaData.columnNullable) | |
796 .append(" WHEN false THEN ").append(ResultSetMetaData.columnNoNulls) | |
797 .append(" ELSE ").append(ResultSetMetaData.columnNullableUnknown) | |
798 .append(" END AS int) AS nullable, ").append( | |
799 "cast(CASE WHEN c.\"default\" IS NOT NULL AND c.\"default\" LIKE 'next value for %' THEN true ELSE false END AS boolean) AS isautoincrement " + | |
800 "FROM \"sys\".\"columns\" c " + | |
801 "JOIN \"sys\".\"tables\" t ON c.\"table_id\" = t.\"id\" " + | |
802 "JOIN \"sys\".\"schemas\" s ON t.\"schema_id\" = s.\"id\" " + | |
803 "WHERE "); | |
804 | |
805 /* combine the conditions for multiple (up to 80) columns into the WHERE-clause */ | |
806 String schName = null; | |
807 String tblName = null; | |
808 String colName = null; | |
809 int queriedcolcount = 0; | |
810 for (int col = startcol; col < array_size && queriedcolcount < MAX_COLUMNS_PER_QUERY; col++) { | |
811 if (_is_fetched[col] != true) { | |
812 if (_is_queried[col] != true) { | |
813 _isNullable[col] = columnNullableUnknown; | |
814 _isAutoincrement[col] = false; | |
815 schName = getSchemaName(col); | |
816 if (schName != null && !schName.isEmpty()) { | |
817 tblName = getTableName(col); | |
818 if (tblName != null && !tblName.isEmpty()) { | |
819 colName = getColumnName(col); | |
820 if (colName != null && !colName.isEmpty()) { | |
821 if (queriedcolcount > 0) | |
822 query.append(" OR "); | |
823 query.append("(s.\"name\" = ").append(MonetWrapper.sq(schName)); | |
824 query.append(" AND t.\"name\" = ").append(MonetWrapper.sq(tblName)); | |
825 query.append(" AND c.\"name\" = ").append(MonetWrapper.sq(colName)); | |
826 query.append(")"); | |
827 _is_queried[col] = true; // flag it | |
828 queriedcolcount++; | |
829 } | |
830 } | |
831 } | |
832 if (_is_queried[col] != true) { | |
833 // make sure we do not try to query it again next time as it is not queryable | |
834 _is_fetched[col] = true; | |
835 } | |
836 } | |
837 } | |
838 } | |
839 | |
840 if (queriedcolcount == 0) | |
841 return; | |
842 | |
843 // execute query to get information on queriedcolcount (or less) columns. | |
844 final java.sql.Statement stmt = conn.createStatement(); | |
845 if (stmt != null) { | |
846 // for debug: System.out.println("SQL (len " + query.length() + "): " + query.toString()); | |
847 final java.sql.ResultSet rs = stmt.executeQuery(query.toString()); | |
848 if (rs != null) { | |
849 String rsSchema = null; | |
850 String rsTable = null; | |
851 String rsColumn = null; | |
852 while (rs.next()) { | |
853 rsSchema = rs.getString(1); // col 1 is schnm | |
854 rsTable = rs.getString(2); // col 2 is tblnm | |
855 rsColumn = rs.getString(3); // col 3 is colnm | |
856 // find the matching schema.table.column entry in the array | |
857 for (int col = 1; col < array_size; col++) { | |
858 if (_is_fetched[col] != true && _is_queried[col]) { | |
859 colName = getColumnName(col); | |
860 if (colName != null && colName.equals(rsColumn)) { | |
861 tblName = getTableName(col); | |
862 if (tblName != null && tblName.equals(rsTable)) { | |
863 schName = getSchemaName(col); | |
864 if (schName != null && schName.equals(rsSchema)) { | |
865 // found matching entry | |
866 // for debug: System.out.println("Found match at [" + col + "] for " + schName + "." + tblName + "." + colName); | |
867 _isNullable[col] = rs.getInt(4); // col 4 is nullable (or "NULLABLE") | |
868 _isAutoincrement[col] = rs.getBoolean(5); // col 5 is isautoincrement (or "IS_AUTOINCREMENT") | |
869 _is_fetched[col] = true; | |
870 queriedcolcount--; | |
871 // we found the match, exit the for-loop | |
872 col = array_size; | |
873 } | |
874 } | |
875 } | |
876 } | |
877 } | |
878 } | |
879 rs.close(); | |
880 } | |
881 stmt.close(); | |
882 } | |
883 | |
884 if (queriedcolcount != 0) { | |
885 // not all queried columns have resulted in a returned data row. | |
886 // make sure we do not match those columns again next run | |
887 for (int col = startcol; col < array_size; col++) { | |
888 if (_is_fetched[col] != true && _is_queried[col]) { | |
889 _is_fetched[col] = true; | |
890 // for debug: System.out.println("Found NO match at [" + col + "] for " + getSchemaName(col) + "." + getTableName(col) + "." + getColumnName(col)); | |
891 } | |
892 } | |
893 } | |
894 } | |
895 } | |
896 |