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 org.h2.api.ErrorCode; |
9 | import org.h2.message.DbException; |
10 | import org.h2.result.Row; |
11 | import org.h2.store.Data; |
12 | import org.h2.store.FileStore; |
13 | import org.h2.table.Table; |
14 | import org.h2.value.Value; |
15 | |
16 | /** |
17 | * An entry in a undo log. |
18 | */ |
19 | public class UndoLogRecord { |
20 | |
21 | /** |
22 | * Operation type meaning the row was inserted. |
23 | */ |
24 | public static final short INSERT = 0; |
25 | |
26 | /** |
27 | * Operation type meaning the row was deleted. |
28 | */ |
29 | public static final short DELETE = 1; |
30 | |
31 | private static final int IN_MEMORY = 0, STORED = 1, IN_MEMORY_INVALID = 2; |
32 | private Table table; |
33 | private Row row; |
34 | private short operation; |
35 | private short state; |
36 | private int filePos; |
37 | |
38 | /** |
39 | * Create a new undo log record |
40 | * |
41 | * @param table the table |
42 | * @param op the operation type |
43 | * @param row the row that was deleted or inserted |
44 | */ |
45 | UndoLogRecord(Table table, short op, Row row) { |
46 | this.table = table; |
47 | this.row = row; |
48 | this.operation = op; |
49 | this.state = IN_MEMORY; |
50 | } |
51 | |
52 | /** |
53 | * Check if the log record is stored in the file. |
54 | * |
55 | * @return true if it is |
56 | */ |
57 | boolean isStored() { |
58 | return state == STORED; |
59 | } |
60 | |
61 | /** |
62 | * Check if this undo log record can be store. Only record can be stored if |
63 | * the table has a unique index. |
64 | * |
65 | * @return if it can be stored |
66 | */ |
67 | boolean canStore() { |
68 | // if large transactions are enabled, this method is not called |
69 | if (table.getUniqueIndex() != null) { |
70 | return true; |
71 | } |
72 | return false; |
73 | } |
74 | |
75 | /** |
76 | * Un-do the operation. If the row was inserted before, it is deleted now, |
77 | * and vice versa. |
78 | * |
79 | * @param session the session |
80 | */ |
81 | void undo(Session session) { |
82 | Database db = session.getDatabase(); |
83 | switch (operation) { |
84 | case INSERT: |
85 | if (state == IN_MEMORY_INVALID) { |
86 | state = IN_MEMORY; |
87 | } |
88 | if (db.getLockMode() == Constants.LOCK_MODE_OFF) { |
89 | if (row.isDeleted()) { |
90 | // it might have been deleted by another thread |
91 | return; |
92 | } |
93 | } |
94 | try { |
95 | row.setDeleted(false); |
96 | table.removeRow(session, row); |
97 | table.fireAfterRow(session, row, null, true); |
98 | } catch (DbException e) { |
99 | if (session.getDatabase().getLockMode() == Constants.LOCK_MODE_OFF |
100 | && e.getErrorCode() == ErrorCode.ROW_NOT_FOUND_WHEN_DELETING_1) { |
101 | // it might have been deleted by another thread |
102 | // ignore |
103 | } else { |
104 | throw e; |
105 | } |
106 | } |
107 | break; |
108 | case DELETE: |
109 | try { |
110 | table.addRow(session, row); |
111 | table.fireAfterRow(session, null, row, true); |
112 | // reset session id, otherwise other sessions think |
113 | // that this row was inserted by this session |
114 | row.commit(); |
115 | } catch (DbException e) { |
116 | if (session.getDatabase().getLockMode() == Constants.LOCK_MODE_OFF |
117 | && e.getSQLException().getErrorCode() == ErrorCode.DUPLICATE_KEY_1) { |
118 | // it might have been added by another thread |
119 | // ignore |
120 | } else { |
121 | throw e; |
122 | } |
123 | } |
124 | break; |
125 | default: |
126 | DbException.throwInternalError("op=" + operation); |
127 | } |
128 | } |
129 | |
130 | /** |
131 | * Append the row to the buffer. |
132 | * |
133 | * @param buff the buffer |
134 | * @param log the undo log |
135 | */ |
136 | void append(Data buff, UndoLog log) { |
137 | int p = buff.length(); |
138 | buff.writeInt(0); |
139 | buff.writeInt(operation); |
140 | buff.writeByte(row.isDeleted() ? (byte) 1 : (byte) 0); |
141 | buff.writeInt(log.getTableId(table)); |
142 | buff.writeLong(row.getKey()); |
143 | buff.writeInt(row.getSessionId()); |
144 | int count = row.getColumnCount(); |
145 | buff.writeInt(count); |
146 | for (int i = 0; i < count; i++) { |
147 | Value v = row.getValue(i); |
148 | buff.checkCapacity(buff.getValueLen(v)); |
149 | buff.writeValue(v); |
150 | } |
151 | buff.fillAligned(); |
152 | buff.setInt(p, (buff.length() - p) / Constants.FILE_BLOCK_SIZE); |
153 | } |
154 | |
155 | /** |
156 | * Save the row in the file using a buffer. |
157 | * |
158 | * @param buff the buffer |
159 | * @param file the file |
160 | * @param log the undo log |
161 | */ |
162 | void save(Data buff, FileStore file, UndoLog log) { |
163 | buff.reset(); |
164 | append(buff, log); |
165 | filePos = (int) (file.getFilePointer() / Constants.FILE_BLOCK_SIZE); |
166 | file.write(buff.getBytes(), 0, buff.length()); |
167 | row = null; |
168 | state = STORED; |
169 | } |
170 | |
171 | /** |
172 | * Load an undo log record row using a buffer. |
173 | * |
174 | * @param buff the buffer |
175 | * @param log the log |
176 | * @return the undo log record |
177 | */ |
178 | static UndoLogRecord loadFromBuffer(Data buff, UndoLog log) { |
179 | UndoLogRecord rec = new UndoLogRecord(null, (short) 0, null); |
180 | int pos = buff.length(); |
181 | int len = buff.readInt() * Constants.FILE_BLOCK_SIZE; |
182 | rec.load(buff, log); |
183 | buff.setPos(pos + len); |
184 | return rec; |
185 | } |
186 | |
187 | /** |
188 | * Load an undo log record row using a buffer. |
189 | * |
190 | * @param buff the buffer |
191 | * @param file the source file |
192 | * @param log the log |
193 | */ |
194 | void load(Data buff, FileStore file, UndoLog log) { |
195 | int min = Constants.FILE_BLOCK_SIZE; |
196 | log.seek(filePos); |
197 | buff.reset(); |
198 | file.readFully(buff.getBytes(), 0, min); |
199 | int len = buff.readInt() * Constants.FILE_BLOCK_SIZE; |
200 | buff.checkCapacity(len); |
201 | if (len - min > 0) { |
202 | file.readFully(buff.getBytes(), min, len - min); |
203 | } |
204 | int oldOp = operation; |
205 | load(buff, log); |
206 | if (SysProperties.CHECK) { |
207 | if (operation != oldOp) { |
208 | DbException.throwInternalError("operation=" + operation + " op=" + oldOp); |
209 | } |
210 | } |
211 | } |
212 | |
213 | private void load(Data buff, UndoLog log) { |
214 | operation = (short) buff.readInt(); |
215 | boolean deleted = buff.readByte() == 1; |
216 | table = log.getTable(buff.readInt()); |
217 | long key = buff.readLong(); |
218 | int sessionId = buff.readInt(); |
219 | int columnCount = buff.readInt(); |
220 | Value[] values = new Value[columnCount]; |
221 | for (int i = 0; i < columnCount; i++) { |
222 | values[i] = buff.readValue(); |
223 | } |
224 | row = new Row(values, Row.MEMORY_CALCULATE); |
225 | row.setKey(key); |
226 | row.setDeleted(deleted); |
227 | row.setSessionId(sessionId); |
228 | state = IN_MEMORY_INVALID; |
229 | } |
230 | |
231 | /** |
232 | * Get the table. |
233 | * |
234 | * @return the table |
235 | */ |
236 | public Table getTable() { |
237 | return table; |
238 | } |
239 | |
240 | /** |
241 | * Get the position in the file. |
242 | * |
243 | * @return the file position |
244 | */ |
245 | public long getFilePos() { |
246 | return filePos; |
247 | } |
248 | |
249 | /** |
250 | * This method is called after the operation was committed. |
251 | * It commits the change to the indexes. |
252 | */ |
253 | void commit() { |
254 | table.commit(operation, row); |
255 | } |
256 | |
257 | /** |
258 | * Get the row that was deleted or inserted. |
259 | * |
260 | * @return the row |
261 | */ |
262 | public Row getRow() { |
263 | return row; |
264 | } |
265 | |
266 | /** |
267 | * Change the state from IN_MEMORY to IN_MEMORY_INVALID. This method is |
268 | * called if a later record was read from the temporary file, and therefore |
269 | * the position could have changed. |
270 | */ |
271 | void invalidatePos() { |
272 | if (this.state == IN_MEMORY) { |
273 | state = IN_MEMORY_INVALID; |
274 | } |
275 | } |
276 | } |