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.engine; |
7 | |
8 | import java.util.ArrayList; |
9 | import java.util.HashMap; |
10 | import java.util.HashSet; |
11 | import java.util.Iterator; |
12 | import java.util.Random; |
13 | |
14 | import org.h2.api.ErrorCode; |
15 | import org.h2.command.Command; |
16 | import org.h2.command.CommandInterface; |
17 | import org.h2.command.Parser; |
18 | import org.h2.command.Prepared; |
19 | import org.h2.command.dml.SetTypes; |
20 | import org.h2.constraint.Constraint; |
21 | import org.h2.index.Index; |
22 | import org.h2.jdbc.JdbcConnection; |
23 | import org.h2.message.DbException; |
24 | import org.h2.message.Trace; |
25 | import org.h2.message.TraceSystem; |
26 | import org.h2.mvstore.db.MVTable; |
27 | import org.h2.mvstore.db.TransactionStore.Change; |
28 | import org.h2.mvstore.db.TransactionStore.Transaction; |
29 | import org.h2.result.LocalResult; |
30 | import org.h2.result.Row; |
31 | import org.h2.schema.Schema; |
32 | import org.h2.store.DataHandler; |
33 | import org.h2.store.InDoubtTransaction; |
34 | import org.h2.store.LobStorageFrontend; |
35 | import org.h2.table.Table; |
36 | import org.h2.util.New; |
37 | import org.h2.util.SmallLRUCache; |
38 | import org.h2.value.Value; |
39 | import org.h2.value.ValueArray; |
40 | import org.h2.value.ValueLong; |
41 | import org.h2.value.ValueNull; |
42 | import org.h2.value.ValueString; |
43 | |
44 | /** |
45 | * A session represents an embedded database connection. When using the server |
46 | * mode, this object resides on the server side and communicates with a |
47 | * SessionRemote object on the client side. |
48 | */ |
49 | public class Session extends SessionWithState { |
50 | |
51 | /** |
52 | * This special log position means that the log entry has been written. |
53 | */ |
54 | public static final int LOG_WRITTEN = -1; |
55 | |
56 | /** |
57 | * The prefix of generated identifiers. It may not have letters, because |
58 | * they are case sensitive. |
59 | */ |
60 | private static final String SYSTEM_IDENTIFIER_PREFIX = "_"; |
61 | private static int nextSerialId; |
62 | |
63 | private final int serialId = nextSerialId++; |
64 | private final Database database; |
65 | private ConnectionInfo connectionInfo; |
66 | private final User user; |
67 | private final int id; |
68 | private final ArrayList<Table> locks = New.arrayList(); |
69 | private final UndoLog undoLog; |
70 | private boolean autoCommit = true; |
71 | private Random random; |
72 | private int lockTimeout; |
73 | private Value lastIdentity = ValueLong.get(0); |
74 | private Value lastScopeIdentity = ValueLong.get(0); |
75 | private int firstUncommittedLog = Session.LOG_WRITTEN; |
76 | private int firstUncommittedPos = Session.LOG_WRITTEN; |
77 | private HashMap<String, Savepoint> savepoints; |
78 | private HashMap<String, Table> localTempTables; |
79 | private HashMap<String, Index> localTempTableIndexes; |
80 | private HashMap<String, Constraint> localTempTableConstraints; |
81 | private int throttle; |
82 | private long lastThrottle; |
83 | private Command currentCommand; |
84 | private boolean allowLiterals; |
85 | private String currentSchemaName; |
86 | private String[] schemaSearchPath; |
87 | private Trace trace; |
88 | private HashMap<String, Value> unlinkLobMap; |
89 | private int systemIdentifier; |
90 | private HashMap<String, Procedure> procedures; |
91 | private boolean undoLogEnabled = true; |
92 | private boolean redoLogBinary = true; |
93 | private boolean autoCommitAtTransactionEnd; |
94 | private String currentTransactionName; |
95 | private volatile long cancelAt; |
96 | private boolean closed; |
97 | private final long sessionStart = System.currentTimeMillis(); |
98 | private long transactionStart; |
99 | private long currentCommandStart; |
100 | private HashMap<String, Value> variables; |
101 | private HashSet<LocalResult> temporaryResults; |
102 | private int queryTimeout; |
103 | private boolean commitOrRollbackDisabled; |
104 | private Table waitForLock; |
105 | private Thread waitForLockThread; |
106 | private int modificationId; |
107 | private int objectId; |
108 | private final int queryCacheSize; |
109 | private SmallLRUCache<String, Command> queryCache; |
110 | private long modificationMetaID = -1; |
111 | private ArrayList<Value> temporaryLobs; |
112 | |
113 | private Transaction transaction; |
114 | private long startStatement = -1; |
115 | |
116 | public Session(Database database, User user, int id) { |
117 | this.database = database; |
118 | this.queryTimeout = database.getSettings().maxQueryTimeout; |
119 | this.queryCacheSize = database.getSettings().queryCacheSize; |
120 | this.undoLog = new UndoLog(this); |
121 | this.user = user; |
122 | this.id = id; |
123 | Setting setting = database.findSetting( |
124 | SetTypes.getTypeName(SetTypes.DEFAULT_LOCK_TIMEOUT)); |
125 | this.lockTimeout = setting == null ? |
126 | Constants.INITIAL_LOCK_TIMEOUT : setting.getIntValue(); |
127 | this.currentSchemaName = Constants.SCHEMA_MAIN; |
128 | } |
129 | |
130 | @Override |
131 | public ArrayList<String> getClusterServers() { |
132 | return new ArrayList<String>(); |
133 | } |
134 | |
135 | public boolean setCommitOrRollbackDisabled(boolean x) { |
136 | boolean old = commitOrRollbackDisabled; |
137 | commitOrRollbackDisabled = x; |
138 | return old; |
139 | } |
140 | |
141 | private void initVariables() { |
142 | if (variables == null) { |
143 | variables = database.newStringMap(); |
144 | } |
145 | } |
146 | |
147 | /** |
148 | * Set the value of the given variable for this session. |
149 | * |
150 | * @param name the name of the variable (may not be null) |
151 | * @param value the new value (may not be null) |
152 | */ |
153 | public void setVariable(String name, Value value) { |
154 | initVariables(); |
155 | modificationId++; |
156 | Value old; |
157 | if (value == ValueNull.INSTANCE) { |
158 | old = variables.remove(name); |
159 | } else { |
160 | // link LOB values, to make sure we have our own object |
161 | value = value.link(database, |
162 | LobStorageFrontend.TABLE_ID_SESSION_VARIABLE); |
163 | old = variables.put(name, value); |
164 | } |
165 | if (old != null) { |
166 | // close the old value (in case it is a lob) |
167 | old.unlink(database); |
168 | old.close(); |
169 | } |
170 | } |
171 | |
172 | /** |
173 | * Get the value of the specified user defined variable. This method always |
174 | * returns a value; it returns ValueNull.INSTANCE if the variable doesn't |
175 | * exist. |
176 | * |
177 | * @param name the variable name |
178 | * @return the value, or NULL |
179 | */ |
180 | public Value getVariable(String name) { |
181 | initVariables(); |
182 | Value v = variables.get(name); |
183 | return v == null ? ValueNull.INSTANCE : v; |
184 | } |
185 | |
186 | /** |
187 | * Get the list of variable names that are set for this session. |
188 | * |
189 | * @return the list of names |
190 | */ |
191 | public String[] getVariableNames() { |
192 | if (variables == null) { |
193 | return new String[0]; |
194 | } |
195 | String[] list = new String[variables.size()]; |
196 | variables.keySet().toArray(list); |
197 | return list; |
198 | } |
199 | |
200 | /** |
201 | * Get the local temporary table if one exists with that name, or null if |
202 | * not. |
203 | * |
204 | * @param name the table name |
205 | * @return the table, or null |
206 | */ |
207 | public Table findLocalTempTable(String name) { |
208 | if (localTempTables == null) { |
209 | return null; |
210 | } |
211 | return localTempTables.get(name); |
212 | } |
213 | |
214 | public ArrayList<Table> getLocalTempTables() { |
215 | if (localTempTables == null) { |
216 | return New.arrayList(); |
217 | } |
218 | return New.arrayList(localTempTables.values()); |
219 | } |
220 | |
221 | /** |
222 | * Add a local temporary table to this session. |
223 | * |
224 | * @param table the table to add |
225 | * @throws DbException if a table with this name already exists |
226 | */ |
227 | public void addLocalTempTable(Table table) { |
228 | if (localTempTables == null) { |
229 | localTempTables = database.newStringMap(); |
230 | } |
231 | if (localTempTables.get(table.getName()) != null) { |
232 | throw DbException.get(ErrorCode.TABLE_OR_VIEW_ALREADY_EXISTS_1, |
233 | table.getSQL()); |
234 | } |
235 | modificationId++; |
236 | localTempTables.put(table.getName(), table); |
237 | } |
238 | |
239 | /** |
240 | * Drop and remove the given local temporary table from this session. |
241 | * |
242 | * @param table the table |
243 | */ |
244 | public void removeLocalTempTable(Table table) { |
245 | modificationId++; |
246 | localTempTables.remove(table.getName()); |
247 | synchronized (database) { |
248 | table.removeChildrenAndResources(this); |
249 | } |
250 | } |
251 | |
252 | /** |
253 | * Get the local temporary index if one exists with that name, or null if |
254 | * not. |
255 | * |
256 | * @param name the table name |
257 | * @return the table, or null |
258 | */ |
259 | public Index findLocalTempTableIndex(String name) { |
260 | if (localTempTableIndexes == null) { |
261 | return null; |
262 | } |
263 | return localTempTableIndexes.get(name); |
264 | } |
265 | |
266 | public HashMap<String, Index> getLocalTempTableIndexes() { |
267 | if (localTempTableIndexes == null) { |
268 | return New.hashMap(); |
269 | } |
270 | return localTempTableIndexes; |
271 | } |
272 | |
273 | /** |
274 | * Add a local temporary index to this session. |
275 | * |
276 | * @param index the index to add |
277 | * @throws DbException if a index with this name already exists |
278 | */ |
279 | public void addLocalTempTableIndex(Index index) { |
280 | if (localTempTableIndexes == null) { |
281 | localTempTableIndexes = database.newStringMap(); |
282 | } |
283 | if (localTempTableIndexes.get(index.getName()) != null) { |
284 | throw DbException.get(ErrorCode.INDEX_ALREADY_EXISTS_1, |
285 | index.getSQL()); |
286 | } |
287 | localTempTableIndexes.put(index.getName(), index); |
288 | } |
289 | |
290 | /** |
291 | * Drop and remove the given local temporary index from this session. |
292 | * |
293 | * @param index the index |
294 | */ |
295 | public void removeLocalTempTableIndex(Index index) { |
296 | if (localTempTableIndexes != null) { |
297 | localTempTableIndexes.remove(index.getName()); |
298 | synchronized (database) { |
299 | index.removeChildrenAndResources(this); |
300 | } |
301 | } |
302 | } |
303 | |
304 | /** |
305 | * Get the local temporary constraint if one exists with that name, or |
306 | * null if not. |
307 | * |
308 | * @param name the constraint name |
309 | * @return the constraint, or null |
310 | */ |
311 | public Constraint findLocalTempTableConstraint(String name) { |
312 | if (localTempTableConstraints == null) { |
313 | return null; |
314 | } |
315 | return localTempTableConstraints.get(name); |
316 | } |
317 | |
318 | /** |
319 | * Get the map of constraints for all constraints on local, temporary |
320 | * tables, if any. The map's keys are the constraints' names. |
321 | * |
322 | * @return the map of constraints, or null |
323 | */ |
324 | public HashMap<String, Constraint> getLocalTempTableConstraints() { |
325 | if (localTempTableConstraints == null) { |
326 | return New.hashMap(); |
327 | } |
328 | return localTempTableConstraints; |
329 | } |
330 | |
331 | /** |
332 | * Add a local temporary constraint to this session. |
333 | * |
334 | * @param constraint the constraint to add |
335 | * @throws DbException if a constraint with the same name already exists |
336 | */ |
337 | public void addLocalTempTableConstraint(Constraint constraint) { |
338 | if (localTempTableConstraints == null) { |
339 | localTempTableConstraints = database.newStringMap(); |
340 | } |
341 | String name = constraint.getName(); |
342 | if (localTempTableConstraints.get(name) != null) { |
343 | throw DbException.get(ErrorCode.CONSTRAINT_ALREADY_EXISTS_1, |
344 | constraint.getSQL()); |
345 | } |
346 | localTempTableConstraints.put(name, constraint); |
347 | } |
348 | |
349 | /** |
350 | * Drop and remove the given local temporary constraint from this session. |
351 | * |
352 | * @param constraint the constraint |
353 | */ |
354 | void removeLocalTempTableConstraint(Constraint constraint) { |
355 | if (localTempTableConstraints != null) { |
356 | localTempTableConstraints.remove(constraint.getName()); |
357 | synchronized (database) { |
358 | constraint.removeChildrenAndResources(this); |
359 | } |
360 | } |
361 | } |
362 | |
363 | @Override |
364 | public boolean getAutoCommit() { |
365 | return autoCommit; |
366 | } |
367 | |
368 | public User getUser() { |
369 | return user; |
370 | } |
371 | |
372 | @Override |
373 | public void setAutoCommit(boolean b) { |
374 | autoCommit = b; |
375 | } |
376 | |
377 | public int getLockTimeout() { |
378 | return lockTimeout; |
379 | } |
380 | |
381 | public void setLockTimeout(int lockTimeout) { |
382 | this.lockTimeout = lockTimeout; |
383 | } |
384 | |
385 | @Override |
386 | public synchronized CommandInterface prepareCommand(String sql, |
387 | int fetchSize) { |
388 | return prepareLocal(sql); |
389 | } |
390 | |
391 | /** |
392 | * Parse and prepare the given SQL statement. This method also checks the |
393 | * rights. |
394 | * |
395 | * @param sql the SQL statement |
396 | * @return the prepared statement |
397 | */ |
398 | public Prepared prepare(String sql) { |
399 | return prepare(sql, false); |
400 | } |
401 | |
402 | /** |
403 | * Parse and prepare the given SQL statement. |
404 | * |
405 | * @param sql the SQL statement |
406 | * @param rightsChecked true if the rights have already been checked |
407 | * @return the prepared statement |
408 | */ |
409 | public Prepared prepare(String sql, boolean rightsChecked) { |
410 | Parser parser = new Parser(this); |
411 | parser.setRightsChecked(rightsChecked); |
412 | return parser.prepare(sql); |
413 | } |
414 | |
415 | /** |
416 | * Parse and prepare the given SQL statement. |
417 | * This method also checks if the connection has been closed. |
418 | * |
419 | * @param sql the SQL statement |
420 | * @return the prepared statement |
421 | */ |
422 | public Command prepareLocal(String sql) { |
423 | if (closed) { |
424 | throw DbException.get(ErrorCode.CONNECTION_BROKEN_1, |
425 | "session closed"); |
426 | } |
427 | Command command; |
428 | if (queryCacheSize > 0) { |
429 | if (queryCache == null) { |
430 | queryCache = SmallLRUCache.newInstance(queryCacheSize); |
431 | modificationMetaID = database.getModificationMetaId(); |
432 | } else { |
433 | long newModificationMetaID = database.getModificationMetaId(); |
434 | if (newModificationMetaID != modificationMetaID) { |
435 | queryCache.clear(); |
436 | modificationMetaID = newModificationMetaID; |
437 | } |
438 | command = queryCache.get(sql); |
439 | if (command != null && command.canReuse()) { |
440 | command.reuse(); |
441 | return command; |
442 | } |
443 | } |
444 | } |
445 | Parser parser = new Parser(this); |
446 | command = parser.prepareCommand(sql); |
447 | if (queryCache != null) { |
448 | if (command.isCacheable()) { |
449 | queryCache.put(sql, command); |
450 | } |
451 | } |
452 | return command; |
453 | } |
454 | |
455 | public Database getDatabase() { |
456 | return database; |
457 | } |
458 | |
459 | @Override |
460 | public int getPowerOffCount() { |
461 | return database.getPowerOffCount(); |
462 | } |
463 | |
464 | @Override |
465 | public void setPowerOffCount(int count) { |
466 | database.setPowerOffCount(count); |
467 | } |
468 | |
469 | /** |
470 | * Commit the current transaction. If the statement was not a data |
471 | * definition statement, and if there are temporary tables that should be |
472 | * dropped or truncated at commit, this is done as well. |
473 | * |
474 | * @param ddl if the statement was a data definition statement |
475 | */ |
476 | public void commit(boolean ddl) { |
477 | checkCommitRollback(); |
478 | currentTransactionName = null; |
479 | transactionStart = 0; |
480 | if (transaction != null) { |
481 | // increment the data mod count, so that other sessions |
482 | // see the changes |
483 | // TODO should not rely on locking |
484 | if (locks.size() > 0) { |
485 | for (int i = 0, size = locks.size(); i < size; i++) { |
486 | Table t = locks.get(i); |
487 | if (t instanceof MVTable) { |
488 | ((MVTable) t).commit(); |
489 | } |
490 | } |
491 | } |
492 | transaction.commit(); |
493 | transaction = null; |
494 | } |
495 | if (containsUncommitted()) { |
496 | // need to commit even if rollback is not possible |
497 | // (create/drop table and so on) |
498 | database.commit(this); |
499 | } |
500 | if (temporaryLobs != null) { |
501 | for (Value v : temporaryLobs) { |
502 | if (!v.isLinked()) { |
503 | v.close(); |
504 | } |
505 | } |
506 | temporaryLobs.clear(); |
507 | } |
508 | if (undoLog.size() > 0) { |
509 | // commit the rows when using MVCC |
510 | if (database.isMultiVersion()) { |
511 | ArrayList<Row> rows = New.arrayList(); |
512 | synchronized (database) { |
513 | while (undoLog.size() > 0) { |
514 | UndoLogRecord entry = undoLog.getLast(); |
515 | entry.commit(); |
516 | rows.add(entry.getRow()); |
517 | undoLog.removeLast(false); |
518 | } |
519 | for (int i = 0, size = rows.size(); i < size; i++) { |
520 | Row r = rows.get(i); |
521 | r.commit(); |
522 | } |
523 | } |
524 | } |
525 | undoLog.clear(); |
526 | } |
527 | if (!ddl) { |
528 | // do not clean the temp tables if the last command was a |
529 | // create/drop |
530 | cleanTempTables(false); |
531 | if (autoCommitAtTransactionEnd) { |
532 | autoCommit = true; |
533 | autoCommitAtTransactionEnd = false; |
534 | } |
535 | } |
536 | endTransaction(); |
537 | } |
538 | |
539 | private void checkCommitRollback() { |
540 | if (commitOrRollbackDisabled && locks.size() > 0) { |
541 | throw DbException.get(ErrorCode.COMMIT_ROLLBACK_NOT_ALLOWED); |
542 | } |
543 | } |
544 | |
545 | private void endTransaction() { |
546 | if (unlinkLobMap != null && unlinkLobMap.size() > 0) { |
547 | // need to flush the transaction log, because we can't unlink lobs |
548 | // if the commit record is not written |
549 | database.flush(); |
550 | for (Value v : unlinkLobMap.values()) { |
551 | v.unlink(database); |
552 | v.close(); |
553 | } |
554 | unlinkLobMap = null; |
555 | } |
556 | unlockAll(); |
557 | } |
558 | |
559 | /** |
560 | * Fully roll back the current transaction. |
561 | */ |
562 | public void rollback() { |
563 | checkCommitRollback(); |
564 | currentTransactionName = null; |
565 | boolean needCommit = false; |
566 | if (undoLog.size() > 0) { |
567 | rollbackTo(null, false); |
568 | needCommit = true; |
569 | } |
570 | if (transaction != null) { |
571 | rollbackTo(null, false); |
572 | needCommit = true; |
573 | // rollback stored the undo operations in the transaction |
574 | // committing will end the transaction |
575 | transaction.commit(); |
576 | transaction = null; |
577 | } |
578 | if (locks.size() > 0 || needCommit) { |
579 | database.commit(this); |
580 | } |
581 | cleanTempTables(false); |
582 | if (autoCommitAtTransactionEnd) { |
583 | autoCommit = true; |
584 | autoCommitAtTransactionEnd = false; |
585 | } |
586 | endTransaction(); |
587 | } |
588 | |
589 | /** |
590 | * Partially roll back the current transaction. |
591 | * |
592 | * @param savepoint the savepoint to which should be rolled back |
593 | * @param trimToSize if the list should be trimmed |
594 | */ |
595 | public void rollbackTo(Savepoint savepoint, boolean trimToSize) { |
596 | int index = savepoint == null ? 0 : savepoint.logIndex; |
597 | while (undoLog.size() > index) { |
598 | UndoLogRecord entry = undoLog.getLast(); |
599 | entry.undo(this); |
600 | undoLog.removeLast(trimToSize); |
601 | } |
602 | if (transaction != null) { |
603 | long savepointId = savepoint == null ? 0 : savepoint.transactionSavepoint; |
604 | HashMap<String, MVTable> tableMap = |
605 | database.getMvStore().getTables(); |
606 | Iterator<Change> it = transaction.getChanges(savepointId); |
607 | while (it.hasNext()) { |
608 | Change c = it.next(); |
609 | MVTable t = tableMap.get(c.mapName); |
610 | if (t != null) { |
611 | long key = ((ValueLong) c.key).getLong(); |
612 | ValueArray value = (ValueArray) c.value; |
613 | short op; |
614 | Row row; |
615 | if (value == null) { |
616 | op = UndoLogRecord.INSERT; |
617 | row = t.getRow(this, key); |
618 | } else { |
619 | op = UndoLogRecord.DELETE; |
620 | row = new Row(value.getList(), Row.MEMORY_CALCULATE); |
621 | } |
622 | row.setKey(key); |
623 | UndoLogRecord log = new UndoLogRecord(t, op, row); |
624 | log.undo(this); |
625 | } |
626 | } |
627 | } |
628 | if (savepoints != null) { |
629 | String[] names = new String[savepoints.size()]; |
630 | savepoints.keySet().toArray(names); |
631 | for (String name : names) { |
632 | Savepoint sp = savepoints.get(name); |
633 | int savepointIndex = sp.logIndex; |
634 | if (savepointIndex > index) { |
635 | savepoints.remove(name); |
636 | } |
637 | } |
638 | } |
639 | } |
640 | |
641 | @Override |
642 | public boolean hasPendingTransaction() { |
643 | return undoLog.size() > 0; |
644 | } |
645 | |
646 | /** |
647 | * Create a savepoint to allow rolling back to this state. |
648 | * |
649 | * @return the savepoint |
650 | */ |
651 | public Savepoint setSavepoint() { |
652 | Savepoint sp = new Savepoint(); |
653 | sp.logIndex = undoLog.size(); |
654 | if (database.getMvStore() != null) { |
655 | sp.transactionSavepoint = getStatementSavepoint(); |
656 | } |
657 | return sp; |
658 | } |
659 | |
660 | public int getId() { |
661 | return id; |
662 | } |
663 | |
664 | @Override |
665 | public void cancel() { |
666 | cancelAt = System.currentTimeMillis(); |
667 | } |
668 | |
669 | @Override |
670 | public void close() { |
671 | if (!closed) { |
672 | try { |
673 | database.checkPowerOff(); |
674 | cleanTempTables(true); |
675 | undoLog.clear(); |
676 | database.removeSession(this); |
677 | } finally { |
678 | closed = true; |
679 | } |
680 | } |
681 | } |
682 | |
683 | /** |
684 | * Add a lock for the given table. The object is unlocked on commit or |
685 | * rollback. |
686 | * |
687 | * @param table the table that is locked |
688 | */ |
689 | public void addLock(Table table) { |
690 | if (SysProperties.CHECK) { |
691 | if (locks.contains(table)) { |
692 | DbException.throwInternalError(); |
693 | } |
694 | } |
695 | locks.add(table); |
696 | } |
697 | |
698 | /** |
699 | * Add an undo log entry to this session. |
700 | * |
701 | * @param table the table |
702 | * @param operation the operation type (see {@link UndoLogRecord}) |
703 | * @param row the row |
704 | */ |
705 | public void log(Table table, short operation, Row row) { |
706 | if (table.isMVStore()) { |
707 | return; |
708 | } |
709 | if (undoLogEnabled) { |
710 | UndoLogRecord log = new UndoLogRecord(table, operation, row); |
711 | // called _after_ the row was inserted successfully into the table, |
712 | // otherwise rollback will try to rollback a not-inserted row |
713 | if (SysProperties.CHECK) { |
714 | int lockMode = database.getLockMode(); |
715 | if (lockMode != Constants.LOCK_MODE_OFF && |
716 | !database.isMultiVersion()) { |
717 | String tableType = log.getTable().getTableType(); |
718 | if (locks.indexOf(log.getTable()) < 0 |
719 | && !Table.TABLE_LINK.equals(tableType) |
720 | && !Table.EXTERNAL_TABLE_ENGINE.equals(tableType)) { |
721 | DbException.throwInternalError(); |
722 | } |
723 | } |
724 | } |
725 | undoLog.add(log); |
726 | } else { |
727 | if (database.isMultiVersion()) { |
728 | // see also UndoLogRecord.commit |
729 | ArrayList<Index> indexes = table.getIndexes(); |
730 | for (int i = 0, size = indexes.size(); i < size; i++) { |
731 | Index index = indexes.get(i); |
732 | index.commit(operation, row); |
733 | } |
734 | row.commit(); |
735 | } |
736 | } |
737 | } |
738 | |
739 | /** |
740 | * Unlock all read locks. This is done if the transaction isolation mode is |
741 | * READ_COMMITTED. |
742 | */ |
743 | public void unlockReadLocks() { |
744 | if (database.isMultiVersion()) { |
745 | // MVCC: keep shared locks (insert / update / delete) |
746 | return; |
747 | } |
748 | // locks is modified in the loop |
749 | for (int i = 0; i < locks.size(); i++) { |
750 | Table t = locks.get(i); |
751 | if (!t.isLockedExclusively()) { |
752 | synchronized (database) { |
753 | t.unlock(this); |
754 | locks.remove(i); |
755 | } |
756 | i--; |
757 | } |
758 | } |
759 | } |
760 | |
761 | /** |
762 | * Unlock just this table. |
763 | * |
764 | * @param t the table to unlock |
765 | */ |
766 | void unlock(Table t) { |
767 | locks.remove(t); |
768 | } |
769 | |
770 | private void unlockAll() { |
771 | if (SysProperties.CHECK) { |
772 | if (undoLog.size() > 0) { |
773 | DbException.throwInternalError(); |
774 | } |
775 | } |
776 | if (locks.size() > 0) { |
777 | // don't use the enhanced for loop to save memory |
778 | for (int i = 0, size = locks.size(); i < size; i++) { |
779 | Table t = locks.get(i); |
780 | t.unlock(this); |
781 | } |
782 | locks.clear(); |
783 | } |
784 | savepoints = null; |
785 | sessionStateChanged = true; |
786 | } |
787 | |
788 | private void cleanTempTables(boolean closeSession) { |
789 | if (localTempTables != null && localTempTables.size() > 0) { |
790 | synchronized (database) { |
791 | for (Table table : New.arrayList(localTempTables.values())) { |
792 | if (closeSession || table.getOnCommitDrop()) { |
793 | modificationId++; |
794 | table.setModified(); |
795 | localTempTables.remove(table.getName()); |
796 | table.removeChildrenAndResources(this); |
797 | if (closeSession) { |
798 | // need to commit, otherwise recovery might |
799 | // ignore the table removal |
800 | database.commit(this); |
801 | } |
802 | } else if (table.getOnCommitTruncate()) { |
803 | table.truncate(this); |
804 | } |
805 | } |
806 | } |
807 | } |
808 | } |
809 | |
810 | public Random getRandom() { |
811 | if (random == null) { |
812 | random = new Random(); |
813 | } |
814 | return random; |
815 | } |
816 | |
817 | @Override |
818 | public Trace getTrace() { |
819 | if (trace != null && !closed) { |
820 | return trace; |
821 | } |
822 | String traceModuleName = Trace.JDBC + "[" + id + "]"; |
823 | if (closed) { |
824 | return new TraceSystem(null).getTrace(traceModuleName); |
825 | } |
826 | trace = database.getTrace(traceModuleName); |
827 | return trace; |
828 | } |
829 | |
830 | public void setLastIdentity(Value last) { |
831 | this.lastIdentity = last; |
832 | this.lastScopeIdentity = last; |
833 | } |
834 | |
835 | public Value getLastIdentity() { |
836 | return lastIdentity; |
837 | } |
838 | |
839 | public void setLastScopeIdentity(Value last) { |
840 | this.lastScopeIdentity = last; |
841 | } |
842 | |
843 | public Value getLastScopeIdentity() { |
844 | return lastScopeIdentity; |
845 | } |
846 | |
847 | /** |
848 | * Called when a log entry for this session is added. The session keeps |
849 | * track of the first entry in the transaction log that is not yet |
850 | * committed. |
851 | * |
852 | * @param logId the transaction log id |
853 | * @param pos the position of the log entry in the transaction log |
854 | */ |
855 | public void addLogPos(int logId, int pos) { |
856 | if (firstUncommittedLog == Session.LOG_WRITTEN) { |
857 | firstUncommittedLog = logId; |
858 | firstUncommittedPos = pos; |
859 | } |
860 | } |
861 | |
862 | public int getFirstUncommittedLog() { |
863 | return firstUncommittedLog; |
864 | } |
865 | |
866 | /** |
867 | * This method is called after the transaction log has written the commit |
868 | * entry for this session. |
869 | */ |
870 | void setAllCommitted() { |
871 | firstUncommittedLog = Session.LOG_WRITTEN; |
872 | firstUncommittedPos = Session.LOG_WRITTEN; |
873 | } |
874 | |
875 | /** |
876 | * Whether the session contains any uncommitted changes. |
877 | * |
878 | * @return true if yes |
879 | */ |
880 | public boolean containsUncommitted() { |
881 | if (database.getMvStore() != null) { |
882 | return transaction != null; |
883 | } |
884 | return firstUncommittedLog != Session.LOG_WRITTEN; |
885 | } |
886 | |
887 | /** |
888 | * Create a savepoint that is linked to the current log position. |
889 | * |
890 | * @param name the savepoint name |
891 | */ |
892 | public void addSavepoint(String name) { |
893 | if (savepoints == null) { |
894 | savepoints = database.newStringMap(); |
895 | } |
896 | Savepoint sp = new Savepoint(); |
897 | sp.logIndex = undoLog.size(); |
898 | if (database.getMvStore() != null) { |
899 | sp.transactionSavepoint = getStatementSavepoint(); |
900 | } |
901 | savepoints.put(name, sp); |
902 | } |
903 | |
904 | /** |
905 | * Undo all operations back to the log position of the given savepoint. |
906 | * |
907 | * @param name the savepoint name |
908 | */ |
909 | public void rollbackToSavepoint(String name) { |
910 | checkCommitRollback(); |
911 | if (savepoints == null) { |
912 | throw DbException.get(ErrorCode.SAVEPOINT_IS_INVALID_1, name); |
913 | } |
914 | Savepoint savepoint = savepoints.get(name); |
915 | if (savepoint == null) { |
916 | throw DbException.get(ErrorCode.SAVEPOINT_IS_INVALID_1, name); |
917 | } |
918 | rollbackTo(savepoint, false); |
919 | } |
920 | |
921 | /** |
922 | * Prepare the given transaction. |
923 | * |
924 | * @param transactionName the name of the transaction |
925 | */ |
926 | public void prepareCommit(String transactionName) { |
927 | if (transaction != null) { |
928 | database.prepareCommit(this, transactionName); |
929 | } |
930 | if (containsUncommitted()) { |
931 | // need to commit even if rollback is not possible (create/drop |
932 | // table and so on) |
933 | database.prepareCommit(this, transactionName); |
934 | } |
935 | currentTransactionName = transactionName; |
936 | } |
937 | |
938 | /** |
939 | * Commit or roll back the given transaction. |
940 | * |
941 | * @param transactionName the name of the transaction |
942 | * @param commit true for commit, false for rollback |
943 | */ |
944 | public void setPreparedTransaction(String transactionName, boolean commit) { |
945 | if (currentTransactionName != null && |
946 | currentTransactionName.equals(transactionName)) { |
947 | if (commit) { |
948 | commit(false); |
949 | } else { |
950 | rollback(); |
951 | } |
952 | } else { |
953 | ArrayList<InDoubtTransaction> list = database |
954 | .getInDoubtTransactions(); |
955 | int state = commit ? InDoubtTransaction.COMMIT |
956 | : InDoubtTransaction.ROLLBACK; |
957 | boolean found = false; |
958 | if (list != null) { |
959 | for (InDoubtTransaction p: list) { |
960 | if (p.getTransactionName().equals(transactionName)) { |
961 | p.setState(state); |
962 | found = true; |
963 | break; |
964 | } |
965 | } |
966 | } |
967 | if (!found) { |
968 | throw DbException.get(ErrorCode.TRANSACTION_NOT_FOUND_1, |
969 | transactionName); |
970 | } |
971 | } |
972 | } |
973 | |
974 | @Override |
975 | public boolean isClosed() { |
976 | return closed; |
977 | } |
978 | |
979 | public void setThrottle(int throttle) { |
980 | this.throttle = throttle; |
981 | } |
982 | |
983 | /** |
984 | * Wait for some time if this session is throttled (slowed down). |
985 | */ |
986 | public void throttle() { |
987 | if (currentCommandStart == 0) { |
988 | currentCommandStart = System.currentTimeMillis(); |
989 | } |
990 | if (throttle == 0) { |
991 | return; |
992 | } |
993 | long time = System.currentTimeMillis(); |
994 | if (lastThrottle + Constants.THROTTLE_DELAY > time) { |
995 | return; |
996 | } |
997 | lastThrottle = time + throttle; |
998 | try { |
999 | Thread.sleep(throttle); |
1000 | } catch (Exception e) { |
1001 | // ignore InterruptedException |
1002 | } |
1003 | } |
1004 | |
1005 | /** |
1006 | * Set the current command of this session. This is done just before |
1007 | * executing the statement. |
1008 | * |
1009 | * @param command the command |
1010 | */ |
1011 | public void setCurrentCommand(Command command) { |
1012 | this.currentCommand = command; |
1013 | if (queryTimeout > 0 && command != null) { |
1014 | long now = System.currentTimeMillis(); |
1015 | currentCommandStart = now; |
1016 | cancelAt = now + queryTimeout; |
1017 | } |
1018 | } |
1019 | |
1020 | /** |
1021 | * Check if the current transaction is canceled by calling |
1022 | * Statement.cancel() or because a session timeout was set and expired. |
1023 | * |
1024 | * @throws DbException if the transaction is canceled |
1025 | */ |
1026 | public void checkCanceled() { |
1027 | throttle(); |
1028 | if (cancelAt == 0) { |
1029 | return; |
1030 | } |
1031 | long time = System.currentTimeMillis(); |
1032 | if (time >= cancelAt) { |
1033 | cancelAt = 0; |
1034 | throw DbException.get(ErrorCode.STATEMENT_WAS_CANCELED); |
1035 | } |
1036 | } |
1037 | |
1038 | /** |
1039 | * Get the cancel time. |
1040 | * |
1041 | * @return the time or 0 if not set |
1042 | */ |
1043 | public long getCancel() { |
1044 | return cancelAt; |
1045 | } |
1046 | |
1047 | public Command getCurrentCommand() { |
1048 | return currentCommand; |
1049 | } |
1050 | |
1051 | public long getCurrentCommandStart() { |
1052 | return currentCommandStart; |
1053 | } |
1054 | |
1055 | public boolean getAllowLiterals() { |
1056 | return allowLiterals; |
1057 | } |
1058 | |
1059 | public void setAllowLiterals(boolean b) { |
1060 | this.allowLiterals = b; |
1061 | } |
1062 | |
1063 | public void setCurrentSchema(Schema schema) { |
1064 | modificationId++; |
1065 | this.currentSchemaName = schema.getName(); |
1066 | } |
1067 | |
1068 | public String getCurrentSchemaName() { |
1069 | return currentSchemaName; |
1070 | } |
1071 | |
1072 | /** |
1073 | * Create an internal connection. This connection is used when initializing |
1074 | * triggers, and when calling user defined functions. |
1075 | * |
1076 | * @param columnList if the url should be 'jdbc:columnlist:connection' |
1077 | * @return the internal connection |
1078 | */ |
1079 | public JdbcConnection createConnection(boolean columnList) { |
1080 | String url; |
1081 | if (columnList) { |
1082 | url = Constants.CONN_URL_COLUMNLIST; |
1083 | } else { |
1084 | url = Constants.CONN_URL_INTERNAL; |
1085 | } |
1086 | return new JdbcConnection(this, getUser().getName(), url); |
1087 | } |
1088 | |
1089 | @Override |
1090 | public DataHandler getDataHandler() { |
1091 | return database; |
1092 | } |
1093 | |
1094 | /** |
1095 | * Remember that the given LOB value must be un-linked (disconnected from |
1096 | * the table) at commit. |
1097 | * |
1098 | * @param v the value |
1099 | */ |
1100 | public void unlinkAtCommit(Value v) { |
1101 | if (SysProperties.CHECK && !v.isLinked()) { |
1102 | DbException.throwInternalError(); |
1103 | } |
1104 | if (unlinkLobMap == null) { |
1105 | unlinkLobMap = New.hashMap(); |
1106 | } |
1107 | unlinkLobMap.put(v.toString(), v); |
1108 | } |
1109 | |
1110 | /** |
1111 | * Do not unlink this LOB value at commit any longer. |
1112 | * |
1113 | * @param v the value |
1114 | */ |
1115 | public void unlinkAtCommitStop(Value v) { |
1116 | if (unlinkLobMap != null) { |
1117 | unlinkLobMap.remove(v.toString()); |
1118 | } |
1119 | } |
1120 | |
1121 | /** |
1122 | * Get the next system generated identifiers. The identifier returned does |
1123 | * not occur within the given SQL statement. |
1124 | * |
1125 | * @param sql the SQL statement |
1126 | * @return the new identifier |
1127 | */ |
1128 | public String getNextSystemIdentifier(String sql) { |
1129 | String identifier; |
1130 | do { |
1131 | identifier = SYSTEM_IDENTIFIER_PREFIX + systemIdentifier++; |
1132 | } while (sql.contains(identifier)); |
1133 | return identifier; |
1134 | } |
1135 | |
1136 | /** |
1137 | * Add a procedure to this session. |
1138 | * |
1139 | * @param procedure the procedure to add |
1140 | */ |
1141 | public void addProcedure(Procedure procedure) { |
1142 | if (procedures == null) { |
1143 | procedures = database.newStringMap(); |
1144 | } |
1145 | procedures.put(procedure.getName(), procedure); |
1146 | } |
1147 | |
1148 | /** |
1149 | * Remove a procedure from this session. |
1150 | * |
1151 | * @param name the name of the procedure to remove |
1152 | */ |
1153 | public void removeProcedure(String name) { |
1154 | if (procedures != null) { |
1155 | procedures.remove(name); |
1156 | } |
1157 | } |
1158 | |
1159 | /** |
1160 | * Get the procedure with the given name, or null |
1161 | * if none exists. |
1162 | * |
1163 | * @param name the procedure name |
1164 | * @return the procedure or null |
1165 | */ |
1166 | public Procedure getProcedure(String name) { |
1167 | if (procedures == null) { |
1168 | return null; |
1169 | } |
1170 | return procedures.get(name); |
1171 | } |
1172 | |
1173 | public void setSchemaSearchPath(String[] schemas) { |
1174 | modificationId++; |
1175 | this.schemaSearchPath = schemas; |
1176 | } |
1177 | |
1178 | public String[] getSchemaSearchPath() { |
1179 | return schemaSearchPath; |
1180 | } |
1181 | |
1182 | @Override |
1183 | public int hashCode() { |
1184 | return serialId; |
1185 | } |
1186 | |
1187 | @Override |
1188 | public String toString() { |
1189 | return "#" + serialId + " (user: " + user.getName() + ")"; |
1190 | } |
1191 | |
1192 | public void setUndoLogEnabled(boolean b) { |
1193 | this.undoLogEnabled = b; |
1194 | } |
1195 | |
1196 | public void setRedoLogBinary(boolean b) { |
1197 | this.redoLogBinary = b; |
1198 | } |
1199 | |
1200 | public boolean isUndoLogEnabled() { |
1201 | return undoLogEnabled; |
1202 | } |
1203 | |
1204 | /** |
1205 | * Begin a transaction. |
1206 | */ |
1207 | public void begin() { |
1208 | autoCommitAtTransactionEnd = true; |
1209 | autoCommit = false; |
1210 | } |
1211 | |
1212 | public long getSessionStart() { |
1213 | return sessionStart; |
1214 | } |
1215 | |
1216 | public long getTransactionStart() { |
1217 | if (transactionStart == 0) { |
1218 | transactionStart = System.currentTimeMillis(); |
1219 | } |
1220 | return transactionStart; |
1221 | } |
1222 | |
1223 | public Table[] getLocks() { |
1224 | // copy the data without synchronizing |
1225 | ArrayList<Table> copy = New.arrayList(); |
1226 | for (int i = 0; i < locks.size(); i++) { |
1227 | try { |
1228 | copy.add(locks.get(i)); |
1229 | } catch (Exception e) { |
1230 | // ignore |
1231 | break; |
1232 | } |
1233 | } |
1234 | Table[] list = new Table[copy.size()]; |
1235 | copy.toArray(list); |
1236 | return list; |
1237 | } |
1238 | |
1239 | /** |
1240 | * Wait if the exclusive mode has been enabled for another session. This |
1241 | * method returns as soon as the exclusive mode has been disabled. |
1242 | */ |
1243 | public void waitIfExclusiveModeEnabled() { |
1244 | // Even in exclusive mode, we have to let the LOB session proceed, or we |
1245 | // will get deadlocks. |
1246 | if (database.getLobSession() == this) { |
1247 | return; |
1248 | } |
1249 | while (true) { |
1250 | Session exclusive = database.getExclusiveSession(); |
1251 | if (exclusive == null || exclusive == this) { |
1252 | break; |
1253 | } |
1254 | if (Thread.holdsLock(exclusive)) { |
1255 | // if another connection is used within the connection |
1256 | break; |
1257 | } |
1258 | try { |
1259 | Thread.sleep(100); |
1260 | } catch (InterruptedException e) { |
1261 | // ignore |
1262 | } |
1263 | } |
1264 | } |
1265 | |
1266 | /** |
1267 | * Remember the result set and close it as soon as the transaction is |
1268 | * committed (if it needs to be closed). This is done to delete temporary |
1269 | * files as soon as possible, and free object ids of temporary tables. |
1270 | * |
1271 | * @param result the temporary result set |
1272 | */ |
1273 | public void addTemporaryResult(LocalResult result) { |
1274 | if (!result.needToClose()) { |
1275 | return; |
1276 | } |
1277 | if (temporaryResults == null) { |
1278 | temporaryResults = New.hashSet(); |
1279 | } |
1280 | if (temporaryResults.size() < 100) { |
1281 | // reference at most 100 result sets to avoid memory problems |
1282 | temporaryResults.add(result); |
1283 | } |
1284 | } |
1285 | |
1286 | private void closeTemporaryResults() { |
1287 | if (temporaryResults != null) { |
1288 | for (LocalResult result : temporaryResults) { |
1289 | result.close(); |
1290 | } |
1291 | temporaryResults = null; |
1292 | } |
1293 | } |
1294 | |
1295 | public void setQueryTimeout(int queryTimeout) { |
1296 | int max = database.getSettings().maxQueryTimeout; |
1297 | if (max != 0 && (max < queryTimeout || queryTimeout == 0)) { |
1298 | // the value must be at most max |
1299 | queryTimeout = max; |
1300 | } |
1301 | this.queryTimeout = queryTimeout; |
1302 | // must reset the cancel at here, |
1303 | // otherwise it is still used |
1304 | this.cancelAt = 0; |
1305 | } |
1306 | |
1307 | public int getQueryTimeout() { |
1308 | return queryTimeout; |
1309 | } |
1310 | |
1311 | /** |
1312 | * Set the table this session is waiting for, and the thread that is |
1313 | * waiting. |
1314 | * |
1315 | * @param waitForLock the table |
1316 | * @param waitForLockThread the current thread (the one that is waiting) |
1317 | */ |
1318 | public void setWaitForLock(Table waitForLock, Thread waitForLockThread) { |
1319 | this.waitForLock = waitForLock; |
1320 | this.waitForLockThread = waitForLockThread; |
1321 | } |
1322 | |
1323 | public Table getWaitForLock() { |
1324 | return waitForLock; |
1325 | } |
1326 | |
1327 | public Thread getWaitForLockThread() { |
1328 | return waitForLockThread; |
1329 | } |
1330 | |
1331 | public int getModificationId() { |
1332 | return modificationId; |
1333 | } |
1334 | |
1335 | @Override |
1336 | public boolean isReconnectNeeded(boolean write) { |
1337 | while (true) { |
1338 | boolean reconnect = database.isReconnectNeeded(); |
1339 | if (reconnect) { |
1340 | return true; |
1341 | } |
1342 | if (write) { |
1343 | if (database.beforeWriting()) { |
1344 | return false; |
1345 | } |
1346 | } else { |
1347 | return false; |
1348 | } |
1349 | } |
1350 | } |
1351 | |
1352 | @Override |
1353 | public void afterWriting() { |
1354 | database.afterWriting(); |
1355 | } |
1356 | |
1357 | @Override |
1358 | public SessionInterface reconnect(boolean write) { |
1359 | readSessionState(); |
1360 | close(); |
1361 | Session newSession = Engine.getInstance().createSession(connectionInfo); |
1362 | newSession.sessionState = sessionState; |
1363 | newSession.recreateSessionState(); |
1364 | if (write) { |
1365 | while (!newSession.database.beforeWriting()) { |
1366 | // wait until we are allowed to write |
1367 | } |
1368 | } |
1369 | return newSession; |
1370 | } |
1371 | |
1372 | public void setConnectionInfo(ConnectionInfo ci) { |
1373 | connectionInfo = ci; |
1374 | } |
1375 | |
1376 | public Value getTransactionId() { |
1377 | if (database.getMvStore() != null) { |
1378 | if (transaction == null) { |
1379 | return ValueNull.INSTANCE; |
1380 | } |
1381 | return ValueString.get(Long.toString(getTransaction().getId())); |
1382 | } |
1383 | if (!database.isPersistent()) { |
1384 | return ValueNull.INSTANCE; |
1385 | } |
1386 | if (undoLog.size() == 0) { |
1387 | return ValueNull.INSTANCE; |
1388 | } |
1389 | return ValueString.get(firstUncommittedLog + "-" + firstUncommittedPos + |
1390 | "-" + id); |
1391 | } |
1392 | |
1393 | /** |
1394 | * Get the next object id. |
1395 | * |
1396 | * @return the next object id |
1397 | */ |
1398 | public int nextObjectId() { |
1399 | return objectId++; |
1400 | } |
1401 | |
1402 | public boolean isRedoLogBinaryEnabled() { |
1403 | return redoLogBinary; |
1404 | } |
1405 | |
1406 | /** |
1407 | * Get the transaction to use for this session. |
1408 | * |
1409 | * @return the transaction |
1410 | */ |
1411 | public Transaction getTransaction() { |
1412 | if (transaction == null) { |
1413 | if (database.getMvStore().getStore().isClosed()) { |
1414 | database.shutdownImmediately(); |
1415 | throw DbException.get(ErrorCode.DATABASE_IS_CLOSED); |
1416 | } |
1417 | transaction = database.getMvStore().getTransactionStore().begin(); |
1418 | startStatement = -1; |
1419 | } |
1420 | return transaction; |
1421 | } |
1422 | |
1423 | public long getStatementSavepoint() { |
1424 | if (startStatement == -1) { |
1425 | startStatement = getTransaction().setSavepoint(); |
1426 | } |
1427 | return startStatement; |
1428 | } |
1429 | |
1430 | /** |
1431 | * Start a new statement within a transaction. |
1432 | */ |
1433 | public void startStatementWithinTransaction() { |
1434 | startStatement = -1; |
1435 | } |
1436 | |
1437 | /** |
1438 | * Mark the statement as completed. This also close all temporary result |
1439 | * set, and deletes all temporary files held by the result sets. |
1440 | */ |
1441 | public void endStatement() { |
1442 | startStatement = -1; |
1443 | closeTemporaryResults(); |
1444 | } |
1445 | |
1446 | @Override |
1447 | public void addTemporaryLob(Value v) { |
1448 | if (temporaryLobs == null) { |
1449 | temporaryLobs = new ArrayList<Value>(); |
1450 | } |
1451 | temporaryLobs.add(v); |
1452 | } |
1453 | |
1454 | /** |
1455 | * Represents a savepoint (a position in a transaction to where one can roll |
1456 | * back to). |
1457 | */ |
1458 | public static class Savepoint { |
1459 | |
1460 | /** |
1461 | * The undo log index. |
1462 | */ |
1463 | int logIndex; |
1464 | |
1465 | /** |
1466 | * The transaction savepoint id. |
1467 | */ |
1468 | long transactionSavepoint; |
1469 | } |
1470 | |
1471 | } |