1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.table; |
7 | |
8 | import java.sql.DatabaseMetaData; |
9 | import java.sql.PreparedStatement; |
10 | import java.sql.ResultSet; |
11 | import java.sql.ResultSetMetaData; |
12 | import java.sql.SQLException; |
13 | import java.sql.Statement; |
14 | import java.sql.Types; |
15 | import java.util.ArrayList; |
16 | import java.util.HashMap; |
17 | |
18 | import org.h2.api.ErrorCode; |
19 | import org.h2.command.Prepared; |
20 | import org.h2.engine.Session; |
21 | import org.h2.engine.UndoLogRecord; |
22 | import org.h2.index.Index; |
23 | import org.h2.index.IndexType; |
24 | import org.h2.index.LinkedIndex; |
25 | import org.h2.jdbc.JdbcSQLException; |
26 | import org.h2.message.DbException; |
27 | import org.h2.result.Row; |
28 | import org.h2.result.RowList; |
29 | import org.h2.schema.Schema; |
30 | import org.h2.util.JdbcUtils; |
31 | import org.h2.util.MathUtils; |
32 | import org.h2.util.New; |
33 | import org.h2.util.StatementBuilder; |
34 | import org.h2.util.StringUtils; |
35 | import org.h2.value.DataType; |
36 | import org.h2.value.Value; |
37 | import org.h2.value.ValueDate; |
38 | import org.h2.value.ValueTime; |
39 | import org.h2.value.ValueTimestamp; |
40 | |
41 | /** |
42 | * A linked table contains connection information for a table accessible by |
43 | * JDBC. The table may be stored in a different database. |
44 | */ |
45 | public class TableLink extends Table { |
46 | |
47 | private static final int MAX_RETRY = 2; |
48 | |
49 | private static final long ROW_COUNT_APPROXIMATION = 100000; |
50 | |
51 | private final String originalSchema; |
52 | private String driver, url, user, password, originalTable, qualifiedTableName; |
53 | private TableLinkConnection conn; |
54 | private HashMap<String, PreparedStatement> preparedMap = New.hashMap(); |
55 | private final ArrayList<Index> indexes = New.arrayList(); |
56 | private final boolean emitUpdates; |
57 | private LinkedIndex linkedIndex; |
58 | private DbException connectException; |
59 | private boolean storesLowerCase; |
60 | private boolean storesMixedCase; |
61 | private boolean storesMixedCaseQuoted; |
62 | private boolean supportsMixedCaseIdentifiers; |
63 | private boolean globalTemporary; |
64 | private boolean readOnly; |
65 | |
66 | public TableLink(Schema schema, int id, String name, String driver, |
67 | String url, String user, String password, String originalSchema, |
68 | String originalTable, boolean emitUpdates, boolean force) { |
69 | super(schema, id, name, false, true); |
70 | this.driver = driver; |
71 | this.url = url; |
72 | this.user = user; |
73 | this.password = password; |
74 | this.originalSchema = originalSchema; |
75 | this.originalTable = originalTable; |
76 | this.emitUpdates = emitUpdates; |
77 | try { |
78 | connect(); |
79 | } catch (DbException e) { |
80 | if (!force) { |
81 | throw e; |
82 | } |
83 | Column[] cols = { }; |
84 | setColumns(cols); |
85 | linkedIndex = new LinkedIndex(this, id, IndexColumn.wrap(cols), |
86 | IndexType.createNonUnique(false)); |
87 | indexes.add(linkedIndex); |
88 | } |
89 | } |
90 | |
91 | private void connect() { |
92 | connectException = null; |
93 | for (int retry = 0;; retry++) { |
94 | try { |
95 | conn = database.getLinkConnection(driver, url, user, password); |
96 | synchronized (conn) { |
97 | try { |
98 | readMetaData(); |
99 | return; |
100 | } catch (Exception e) { |
101 | // could be SQLException or RuntimeException |
102 | conn.close(true); |
103 | conn = null; |
104 | throw DbException.convert(e); |
105 | } |
106 | } |
107 | } catch (DbException e) { |
108 | if (retry >= MAX_RETRY) { |
109 | connectException = e; |
110 | throw e; |
111 | } |
112 | } |
113 | } |
114 | } |
115 | |
116 | private void readMetaData() throws SQLException { |
117 | DatabaseMetaData meta = conn.getConnection().getMetaData(); |
118 | storesLowerCase = meta.storesLowerCaseIdentifiers(); |
119 | storesMixedCase = meta.storesMixedCaseIdentifiers(); |
120 | storesMixedCaseQuoted = meta.storesMixedCaseQuotedIdentifiers(); |
121 | supportsMixedCaseIdentifiers = meta.supportsMixedCaseIdentifiers(); |
122 | ResultSet rs = meta.getTables(null, originalSchema, originalTable, null); |
123 | if (rs.next() && rs.next()) { |
124 | throw DbException.get(ErrorCode.SCHEMA_NAME_MUST_MATCH, originalTable); |
125 | } |
126 | rs.close(); |
127 | rs = meta.getColumns(null, originalSchema, originalTable, null); |
128 | int i = 0; |
129 | ArrayList<Column> columnList = New.arrayList(); |
130 | HashMap<String, Column> columnMap = New.hashMap(); |
131 | String catalog = null, schema = null; |
132 | while (rs.next()) { |
133 | String thisCatalog = rs.getString("TABLE_CAT"); |
134 | if (catalog == null) { |
135 | catalog = thisCatalog; |
136 | } |
137 | String thisSchema = rs.getString("TABLE_SCHEM"); |
138 | if (schema == null) { |
139 | schema = thisSchema; |
140 | } |
141 | if (!StringUtils.equals(catalog, thisCatalog) || |
142 | !StringUtils.equals(schema, thisSchema)) { |
143 | // if the table exists in multiple schemas or tables, |
144 | // use the alternative solution |
145 | columnMap.clear(); |
146 | columnList.clear(); |
147 | break; |
148 | } |
149 | String n = rs.getString("COLUMN_NAME"); |
150 | n = convertColumnName(n); |
151 | int sqlType = rs.getInt("DATA_TYPE"); |
152 | long precision = rs.getInt("COLUMN_SIZE"); |
153 | precision = convertPrecision(sqlType, precision); |
154 | int scale = rs.getInt("DECIMAL_DIGITS"); |
155 | scale = convertScale(sqlType, scale); |
156 | int displaySize = MathUtils.convertLongToInt(precision); |
157 | int type = DataType.convertSQLTypeToValueType(sqlType); |
158 | Column col = new Column(n, type, precision, scale, displaySize); |
159 | col.setTable(this, i++); |
160 | columnList.add(col); |
161 | columnMap.put(n, col); |
162 | } |
163 | rs.close(); |
164 | if (originalTable.indexOf('.') < 0 && !StringUtils.isNullOrEmpty(schema)) { |
165 | qualifiedTableName = schema + "." + originalTable; |
166 | } else { |
167 | qualifiedTableName = originalTable; |
168 | } |
169 | // check if the table is accessible |
170 | Statement stat = null; |
171 | try { |
172 | stat = conn.getConnection().createStatement(); |
173 | rs = stat.executeQuery("SELECT * FROM " + |
174 | qualifiedTableName + " T WHERE 1=0"); |
175 | if (columnList.size() == 0) { |
176 | // alternative solution |
177 | ResultSetMetaData rsMeta = rs.getMetaData(); |
178 | for (i = 0; i < rsMeta.getColumnCount();) { |
179 | String n = rsMeta.getColumnName(i + 1); |
180 | n = convertColumnName(n); |
181 | int sqlType = rsMeta.getColumnType(i + 1); |
182 | long precision = rsMeta.getPrecision(i + 1); |
183 | precision = convertPrecision(sqlType, precision); |
184 | int scale = rsMeta.getScale(i + 1); |
185 | scale = convertScale(sqlType, scale); |
186 | int displaySize = rsMeta.getColumnDisplaySize(i + 1); |
187 | int type = DataType.getValueTypeFromResultSet(rsMeta, i + 1); |
188 | Column col = new Column(n, type, precision, scale, displaySize); |
189 | col.setTable(this, i++); |
190 | columnList.add(col); |
191 | columnMap.put(n, col); |
192 | } |
193 | } |
194 | rs.close(); |
195 | } catch (Exception e) { |
196 | throw DbException.get(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1, e, |
197 | originalTable + "(" + e.toString() + ")"); |
198 | } finally { |
199 | JdbcUtils.closeSilently(stat); |
200 | } |
201 | Column[] cols = new Column[columnList.size()]; |
202 | columnList.toArray(cols); |
203 | setColumns(cols); |
204 | int id = getId(); |
205 | linkedIndex = new LinkedIndex(this, id, IndexColumn.wrap(cols), |
206 | IndexType.createNonUnique(false)); |
207 | indexes.add(linkedIndex); |
208 | try { |
209 | rs = meta.getPrimaryKeys(null, originalSchema, originalTable); |
210 | } catch (Exception e) { |
211 | // Some ODBC bridge drivers don't support it: |
212 | // some combinations of "DataDirect SequeLink(R) for JDBC" |
213 | // http://www.datadirect.com/index.ssp |
214 | rs = null; |
215 | } |
216 | String pkName = ""; |
217 | ArrayList<Column> list; |
218 | if (rs != null && rs.next()) { |
219 | // the problem is, the rows are not sorted by KEY_SEQ |
220 | list = New.arrayList(); |
221 | do { |
222 | int idx = rs.getInt("KEY_SEQ"); |
223 | if (pkName == null) { |
224 | pkName = rs.getString("PK_NAME"); |
225 | } |
226 | while (list.size() < idx) { |
227 | list.add(null); |
228 | } |
229 | String col = rs.getString("COLUMN_NAME"); |
230 | col = convertColumnName(col); |
231 | Column column = columnMap.get(col); |
232 | if (idx == 0) { |
233 | // workaround for a bug in the SQLite JDBC driver |
234 | list.add(column); |
235 | } else { |
236 | list.set(idx - 1, column); |
237 | } |
238 | } while (rs.next()); |
239 | addIndex(list, IndexType.createPrimaryKey(false, false)); |
240 | rs.close(); |
241 | } |
242 | try { |
243 | rs = meta.getIndexInfo(null, originalSchema, originalTable, false, true); |
244 | } catch (Exception e) { |
245 | // Oracle throws an exception if the table is not found or is a |
246 | // SYNONYM |
247 | rs = null; |
248 | } |
249 | String indexName = null; |
250 | list = New.arrayList(); |
251 | IndexType indexType = null; |
252 | if (rs != null) { |
253 | while (rs.next()) { |
254 | if (rs.getShort("TYPE") == DatabaseMetaData.tableIndexStatistic) { |
255 | // ignore index statistics |
256 | continue; |
257 | } |
258 | String newIndex = rs.getString("INDEX_NAME"); |
259 | if (pkName.equals(newIndex)) { |
260 | continue; |
261 | } |
262 | if (indexName != null && !indexName.equals(newIndex)) { |
263 | addIndex(list, indexType); |
264 | indexName = null; |
265 | } |
266 | if (indexName == null) { |
267 | indexName = newIndex; |
268 | list.clear(); |
269 | } |
270 | boolean unique = !rs.getBoolean("NON_UNIQUE"); |
271 | indexType = unique ? IndexType.createUnique(false, false) : |
272 | IndexType.createNonUnique(false); |
273 | String col = rs.getString("COLUMN_NAME"); |
274 | col = convertColumnName(col); |
275 | Column column = columnMap.get(col); |
276 | list.add(column); |
277 | } |
278 | rs.close(); |
279 | } |
280 | if (indexName != null) { |
281 | addIndex(list, indexType); |
282 | } |
283 | } |
284 | |
285 | private static long convertPrecision(int sqlType, long precision) { |
286 | // workaround for an Oracle problem: |
287 | // for DATE columns, the reported precision is 7 |
288 | // for DECIMAL columns, the reported precision is 0 |
289 | switch (sqlType) { |
290 | case Types.DECIMAL: |
291 | case Types.NUMERIC: |
292 | if (precision == 0) { |
293 | precision = 65535; |
294 | } |
295 | break; |
296 | case Types.DATE: |
297 | precision = Math.max(ValueDate.PRECISION, precision); |
298 | break; |
299 | case Types.TIMESTAMP: |
300 | precision = Math.max(ValueTimestamp.PRECISION, precision); |
301 | break; |
302 | case Types.TIME: |
303 | precision = Math.max(ValueTime.PRECISION, precision); |
304 | break; |
305 | } |
306 | return precision; |
307 | } |
308 | |
309 | private static int convertScale(int sqlType, int scale) { |
310 | // workaround for an Oracle problem: |
311 | // for DECIMAL columns, the reported precision is -127 |
312 | switch (sqlType) { |
313 | case Types.DECIMAL: |
314 | case Types.NUMERIC: |
315 | if (scale < 0) { |
316 | scale = 32767; |
317 | } |
318 | break; |
319 | } |
320 | return scale; |
321 | } |
322 | |
323 | private String convertColumnName(String columnName) { |
324 | if ((storesMixedCase || storesLowerCase) && |
325 | columnName.equals(StringUtils.toLowerEnglish(columnName))) { |
326 | columnName = StringUtils.toUpperEnglish(columnName); |
327 | } else if (storesMixedCase && !supportsMixedCaseIdentifiers) { |
328 | // TeraData |
329 | columnName = StringUtils.toUpperEnglish(columnName); |
330 | } else if (storesMixedCase && storesMixedCaseQuoted) { |
331 | // MS SQL Server (identifiers are case insensitive even if quoted) |
332 | columnName = StringUtils.toUpperEnglish(columnName); |
333 | } |
334 | return columnName; |
335 | } |
336 | |
337 | private void addIndex(ArrayList<Column> list, IndexType indexType) { |
338 | Column[] cols = new Column[list.size()]; |
339 | list.toArray(cols); |
340 | Index index = new LinkedIndex(this, 0, IndexColumn.wrap(cols), indexType); |
341 | indexes.add(index); |
342 | } |
343 | |
344 | @Override |
345 | public String getDropSQL() { |
346 | return "DROP TABLE IF EXISTS " + getSQL(); |
347 | } |
348 | |
349 | @Override |
350 | public String getCreateSQL() { |
351 | StringBuilder buff = new StringBuilder("CREATE FORCE "); |
352 | if (isTemporary()) { |
353 | if (globalTemporary) { |
354 | buff.append("GLOBAL "); |
355 | } else { |
356 | buff.append("LOCAL "); |
357 | } |
358 | buff.append("TEMPORARY "); |
359 | } |
360 | buff.append("LINKED TABLE ").append(getSQL()); |
361 | if (comment != null) { |
362 | buff.append(" COMMENT ").append(StringUtils.quoteStringSQL(comment)); |
363 | } |
364 | buff.append('('). |
365 | append(StringUtils.quoteStringSQL(driver)). |
366 | append(", "). |
367 | append(StringUtils.quoteStringSQL(url)). |
368 | append(", "). |
369 | append(StringUtils.quoteStringSQL(user)). |
370 | append(", "). |
371 | append(StringUtils.quoteStringSQL(password)). |
372 | append(", "). |
373 | append(StringUtils.quoteStringSQL(originalTable)). |
374 | append(')'); |
375 | if (emitUpdates) { |
376 | buff.append(" EMIT UPDATES"); |
377 | } |
378 | if (readOnly) { |
379 | buff.append(" READONLY"); |
380 | } |
381 | buff.append(" /*" + JdbcSQLException.HIDE_SQL + "*/"); |
382 | return buff.toString(); |
383 | } |
384 | |
385 | @Override |
386 | public Index addIndex(Session session, String indexName, int indexId, |
387 | IndexColumn[] cols, IndexType indexType, boolean create, |
388 | String indexComment) { |
389 | throw DbException.getUnsupportedException("LINK"); |
390 | } |
391 | |
392 | @Override |
393 | public boolean lock(Session session, boolean exclusive, boolean forceLockEvenInMvcc) { |
394 | // nothing to do |
395 | return false; |
396 | } |
397 | |
398 | @Override |
399 | public boolean isLockedExclusively() { |
400 | return false; |
401 | } |
402 | |
403 | @Override |
404 | public Index getScanIndex(Session session) { |
405 | return linkedIndex; |
406 | } |
407 | |
408 | private void checkReadOnly() { |
409 | if (readOnly) { |
410 | throw DbException.get(ErrorCode.DATABASE_IS_READ_ONLY); |
411 | } |
412 | } |
413 | |
414 | @Override |
415 | public void removeRow(Session session, Row row) { |
416 | checkReadOnly(); |
417 | getScanIndex(session).remove(session, row); |
418 | } |
419 | |
420 | @Override |
421 | public void addRow(Session session, Row row) { |
422 | checkReadOnly(); |
423 | getScanIndex(session).add(session, row); |
424 | } |
425 | |
426 | @Override |
427 | public void close(Session session) { |
428 | if (conn != null) { |
429 | try { |
430 | conn.close(false); |
431 | } finally { |
432 | conn = null; |
433 | } |
434 | } |
435 | } |
436 | |
437 | @Override |
438 | public synchronized long getRowCount(Session session) { |
439 | String sql = "SELECT COUNT(*) FROM " + qualifiedTableName; |
440 | try { |
441 | PreparedStatement prep = execute(sql, null, false); |
442 | ResultSet rs = prep.getResultSet(); |
443 | rs.next(); |
444 | long count = rs.getLong(1); |
445 | rs.close(); |
446 | reusePreparedStatement(prep, sql); |
447 | return count; |
448 | } catch (Exception e) { |
449 | throw wrapException(sql, e); |
450 | } |
451 | } |
452 | |
453 | /** |
454 | * Wrap a SQL exception that occurred while accessing a linked table. |
455 | * |
456 | * @param sql the SQL statement |
457 | * @param ex the exception from the remote database |
458 | * @return the wrapped exception |
459 | */ |
460 | public static DbException wrapException(String sql, Exception ex) { |
461 | SQLException e = DbException.toSQLException(ex); |
462 | return DbException.get(ErrorCode.ERROR_ACCESSING_LINKED_TABLE_2, |
463 | e, sql, e.toString()); |
464 | } |
465 | |
466 | public String getQualifiedTable() { |
467 | return qualifiedTableName; |
468 | } |
469 | |
470 | /** |
471 | * Execute a SQL statement using the given parameters. Prepared |
472 | * statements are kept in a hash map to avoid re-creating them. |
473 | * |
474 | * @param sql the SQL statement |
475 | * @param params the parameters or null |
476 | * @param reusePrepared if the prepared statement can be re-used immediately |
477 | * @return the prepared statement, or null if it is re-used |
478 | */ |
479 | public PreparedStatement execute(String sql, ArrayList<Value> params, |
480 | boolean reusePrepared) { |
481 | if (conn == null) { |
482 | throw connectException; |
483 | } |
484 | for (int retry = 0;; retry++) { |
485 | try { |
486 | synchronized (conn) { |
487 | PreparedStatement prep = preparedMap.remove(sql); |
488 | if (prep == null) { |
489 | prep = conn.getConnection().prepareStatement(sql); |
490 | } |
491 | if (trace.isDebugEnabled()) { |
492 | StatementBuilder buff = new StatementBuilder(); |
493 | buff.append(getName()).append(":\n").append(sql); |
494 | if (params != null && params.size() > 0) { |
495 | buff.append(" {"); |
496 | int i = 1; |
497 | for (Value v : params) { |
498 | buff.appendExceptFirst(", "); |
499 | buff.append(i++).append(": ").append(v.getSQL()); |
500 | } |
501 | buff.append('}'); |
502 | } |
503 | buff.append(';'); |
504 | trace.debug(buff.toString()); |
505 | } |
506 | if (params != null) { |
507 | for (int i = 0, size = params.size(); i < size; i++) { |
508 | Value v = params.get(i); |
509 | v.set(prep, i + 1); |
510 | } |
511 | } |
512 | prep.execute(); |
513 | if (reusePrepared) { |
514 | reusePreparedStatement(prep, sql); |
515 | return null; |
516 | } |
517 | return prep; |
518 | } |
519 | } catch (SQLException e) { |
520 | if (retry >= MAX_RETRY) { |
521 | throw DbException.convert(e); |
522 | } |
523 | conn.close(true); |
524 | connect(); |
525 | } |
526 | } |
527 | } |
528 | |
529 | @Override |
530 | public void unlock(Session s) { |
531 | // nothing to do |
532 | } |
533 | |
534 | @Override |
535 | public void checkRename() { |
536 | // ok |
537 | } |
538 | |
539 | @Override |
540 | public void checkSupportAlter() { |
541 | throw DbException.getUnsupportedException("LINK"); |
542 | } |
543 | |
544 | @Override |
545 | public void truncate(Session session) { |
546 | throw DbException.getUnsupportedException("LINK"); |
547 | } |
548 | |
549 | @Override |
550 | public boolean canGetRowCount() { |
551 | return true; |
552 | } |
553 | |
554 | @Override |
555 | public boolean canDrop() { |
556 | return true; |
557 | } |
558 | |
559 | @Override |
560 | public String getTableType() { |
561 | return Table.TABLE_LINK; |
562 | } |
563 | |
564 | @Override |
565 | public void removeChildrenAndResources(Session session) { |
566 | super.removeChildrenAndResources(session); |
567 | close(session); |
568 | database.removeMeta(session, getId()); |
569 | driver = null; |
570 | url = user = password = originalTable = null; |
571 | preparedMap = null; |
572 | invalidate(); |
573 | } |
574 | |
575 | public boolean isOracle() { |
576 | return url.startsWith("jdbc:oracle:"); |
577 | } |
578 | |
579 | @Override |
580 | public ArrayList<Index> getIndexes() { |
581 | return indexes; |
582 | } |
583 | |
584 | @Override |
585 | public long getMaxDataModificationId() { |
586 | // data may have been modified externally |
587 | return Long.MAX_VALUE; |
588 | } |
589 | |
590 | @Override |
591 | public Index getUniqueIndex() { |
592 | for (Index idx : indexes) { |
593 | if (idx.getIndexType().isUnique()) { |
594 | return idx; |
595 | } |
596 | } |
597 | return null; |
598 | } |
599 | |
600 | @Override |
601 | public void updateRows(Prepared prepared, Session session, RowList rows) { |
602 | boolean deleteInsert; |
603 | checkReadOnly(); |
604 | if (emitUpdates) { |
605 | for (rows.reset(); rows.hasNext();) { |
606 | prepared.checkCanceled(); |
607 | Row oldRow = rows.next(); |
608 | Row newRow = rows.next(); |
609 | linkedIndex.update(oldRow, newRow); |
610 | session.log(this, UndoLogRecord.DELETE, oldRow); |
611 | session.log(this, UndoLogRecord.INSERT, newRow); |
612 | } |
613 | deleteInsert = false; |
614 | } else { |
615 | deleteInsert = true; |
616 | } |
617 | if (deleteInsert) { |
618 | super.updateRows(prepared, session, rows); |
619 | } |
620 | } |
621 | |
622 | public void setGlobalTemporary(boolean globalTemporary) { |
623 | this.globalTemporary = globalTemporary; |
624 | } |
625 | |
626 | public void setReadOnly(boolean readOnly) { |
627 | this.readOnly = readOnly; |
628 | } |
629 | |
630 | @Override |
631 | public long getRowCountApproximation() { |
632 | return ROW_COUNT_APPROXIMATION; |
633 | } |
634 | |
635 | @Override |
636 | public long getDiskSpaceUsed() { |
637 | return 0; |
638 | } |
639 | |
640 | /** |
641 | * Add this prepared statement to the list of cached statements. |
642 | * |
643 | * @param prep the prepared statement |
644 | * @param sql the SQL statement |
645 | */ |
646 | public void reusePreparedStatement(PreparedStatement prep, String sql) { |
647 | synchronized (conn) { |
648 | preparedMap.put(sql, prep); |
649 | } |
650 | } |
651 | |
652 | @Override |
653 | public boolean isDeterministic() { |
654 | return false; |
655 | } |
656 | |
657 | /** |
658 | * Linked tables don't know if they are readonly. This overwrites |
659 | * the default handling. |
660 | */ |
661 | @Override |
662 | public void checkWritingAllowed() { |
663 | // only the target database can verify this |
664 | } |
665 | |
666 | /** |
667 | * Convert the values if required. Default values are not set (kept as |
668 | * null). |
669 | * |
670 | * @param session the session |
671 | * @param row the row |
672 | */ |
673 | @Override |
674 | public void validateConvertUpdateSequence(Session session, Row row) { |
675 | for (int i = 0; i < columns.length; i++) { |
676 | Value value = row.getValue(i); |
677 | if (value != null) { |
678 | // null means use the default value |
679 | Column column = columns[i]; |
680 | Value v2 = column.validateConvertUpdateSequence(session, value); |
681 | if (v2 != value) { |
682 | row.setValue(i, v2); |
683 | } |
684 | } |
685 | } |
686 | } |
687 | |
688 | /** |
689 | * Get or generate a default value for the given column. Default values are |
690 | * not set (kept as null). |
691 | * |
692 | * @param session the session |
693 | * @param column the column |
694 | * @return the value |
695 | */ |
696 | @Override |
697 | public Value getDefaultValue(Session session, Column column) { |
698 | return null; |
699 | } |
700 | |
701 | } |