1 | /* ============================================================= |
2 | * SmallSQL : a free Java DBMS library for the Java(tm) platform |
3 | * ============================================================= |
4 | * |
5 | * (C) Copyright 2004-2006, by Volker Berlin. |
6 | * |
7 | * Project Info: http://www.smallsql.de/ |
8 | * |
9 | * This library is free software; you can redistribute it and/or modify it |
10 | * under the terms of the GNU Lesser General Public License as published by |
11 | * the Free Software Foundation; either version 2.1 of the License, or |
12 | * (at your option) any later version. |
13 | * |
14 | * This library is distributed in the hope that it will be useful, but |
15 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY |
16 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public |
17 | * License for more details. |
18 | * |
19 | * You should have received a copy of the GNU Lesser General Public |
20 | * License along with this library; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, |
22 | * USA. |
23 | * |
24 | * [Java is a trademark or registered trademark of Sun Microsystems, Inc. |
25 | * in the United States and other countries.] |
26 | * |
27 | * --------------- |
28 | * Table.java |
29 | * --------------- |
30 | * Author: Volker Berlin |
31 | * |
32 | */ |
33 | package smallsql.database; |
34 | |
35 | import java.io.*; |
36 | import java.sql.*; |
37 | import java.util.ArrayList; |
38 | import java.util.HashMap; |
39 | import java.util.Iterator; |
40 | import java.util.List; |
41 | |
42 | |
43 | class Table extends TableView{ |
44 | |
45 | private static final int INDEX = 1; |
46 | |
47 | final Database database; |
48 | RandomAccessFile raFile; // file handle of the table |
49 | private Lobs lobs; // file handle of lob data for this table |
50 | long firstPage; // offset of the first page |
51 | |
52 | final private HashMap locks = new HashMap(); |
53 | private SSConnection tabLockConnection; // wenn gesetzt die Connection die ein LOCK_TAB hat |
54 | private int tabLockCount; |
55 | final private ArrayList locksInsert = new ArrayList(); // liste der LOCK_INSERT |
56 | final private HashMap serializeConnections = new HashMap(); |
57 | final IndexDescriptions indexes; |
58 | final ForeignKeys references; |
59 | |
60 | |
61 | /** |
62 | * Constructor for read existing tables. |
63 | */ |
64 | Table(SSConnection con, String name, RandomAccessFile raFile, long offset, int tableFormatVersion) throws Exception{ |
65 | super( name, new Columns() ); |
66 | this.database = con.getDatabase(false); |
67 | this.raFile = raFile; |
68 | this.firstPage = offset; |
69 | StoreImpl store = getStore(con, firstPage, SQLTokenizer.SELECT); |
70 | int count = store.readInt(); |
71 | |
72 | for(int i=0; i<count; i++){ |
73 | columns.add( store.readColumn(this, tableFormatVersion) ); |
74 | } |
75 | indexes = new IndexDescriptions(); |
76 | references = new ForeignKeys(); |
77 | |
78 | // read additional informations |
79 | int type; |
80 | while((type = store.readInt()) != 0){ |
81 | int offsetInPage = store.getCurrentOffsetInPage(); |
82 | int size = store.readInt(); |
83 | switch(type){ |
84 | case INDEX: |
85 | indexes.add( IndexDescription.load( database, this, store) ); |
86 | break; |
87 | } |
88 | store.setCurrentOffsetInPage(offsetInPage + size); |
89 | } |
90 | |
91 | firstPage = store.getNextPagePos(); |
92 | } |
93 | |
94 | |
95 | /** |
96 | * Constructor for creating of new tables. |
97 | */ |
98 | Table(Database database, SSConnection con, String name, Columns columns, IndexDescriptions indexes, ForeignKeys foreignKeys) throws Exception{ |
99 | super( name, columns ); |
100 | this.database = database; |
101 | this.indexes = indexes; |
102 | this.references = foreignKeys; |
103 | indexes.create( database, this ); |
104 | write(con); |
105 | for(int i=0; i<foreignKeys.size(); i++){ |
106 | ForeignKey foreignKey = foreignKeys.get(i); |
107 | Table pkTable = (Table)database.getTableView(con, foreignKey.pkTable); |
108 | pkTable.references.add(foreignKey); |
109 | } |
110 | } |
111 | |
112 | /** |
113 | * Constructor for extends class Lobs. |
114 | */ |
115 | Table(Database database, String name){ |
116 | super( name, null); |
117 | this.database = database; |
118 | indexes = null; |
119 | references = null; |
120 | } |
121 | |
122 | /** |
123 | * Drop the Table. This method is static that the file does not need to load and also corrupt files can be dropped. |
124 | */ |
125 | static void drop(Database database, String name) throws Exception{ |
126 | boolean ok = new File( Utils.createTableViewFileName( database, name ) ).delete(); |
127 | if(!ok) throw Utils.createSQLException("Table '" + name + "' can't drop."); |
128 | } |
129 | |
130 | |
131 | /** |
132 | * Drop a loaded table. |
133 | * |
134 | */ |
135 | void drop(SSConnection con) throws Exception{ |
136 | TableStorePage storePage = requestLock( con, SQLTokenizer.CREATE, -1 ); |
137 | if(storePage == null) |
138 | throw Utils.createSQLException("Table '" + name + "' can't drop because is locked."); |
139 | |
140 | // remove the all commits that point to this table |
141 | con.rollbackFile(raFile); |
142 | close(); |
143 | if(lobs != null) |
144 | lobs.drop(con); |
145 | if(indexes != null) |
146 | indexes.drop(database); |
147 | boolean ok = getFile( database, name).delete(); |
148 | if(!ok) throw Utils.createSQLException("Table '" + name + "' can't drop."); |
149 | } |
150 | |
151 | |
152 | void close() throws Exception{ |
153 | raFile.close(); |
154 | raFile = null; |
155 | } |
156 | |
157 | |
158 | private void write(SSConnection con) throws Exception{ |
159 | raFile = createFile( database ); |
160 | firstPage = 8; |
161 | StoreImpl store = getStore( con, firstPage, SQLTokenizer.CREATE); |
162 | int count = columns.size(); |
163 | store.writeInt( count ); |
164 | for(int i=0; i<count; i++){ |
165 | store.writeColumn( this, columns.get(i) ); |
166 | } |
167 | |
168 | // write additional informations |
169 | for(int i=0; i<indexes.size(); i++){ |
170 | IndexDescription indexDesc = indexes.get(i); |
171 | store.writeInt( INDEX ); |
172 | int offsetStart = store.getCurrentOffsetInPage(); |
173 | store.setCurrentOffsetInPage( offsetStart + 4 ); // place holder for length |
174 | |
175 | // write the IndexDescription |
176 | indexDesc.save(store); |
177 | |
178 | // write the length information |
179 | int offsetEnd = store.getCurrentOffsetInPage(); |
180 | store.setCurrentOffsetInPage( offsetStart ); |
181 | store.writeInt( offsetEnd - offsetStart); |
182 | store.setCurrentOffsetInPage( offsetEnd ); |
183 | } |
184 | store.writeInt( 0 ); // no more additinal informations |
185 | |
186 | store.writeFinsh(con); |
187 | firstPage = store.getNextPagePos(); |
188 | } |
189 | |
190 | |
191 | void writeMagic(RandomAccessFile raFile) throws Exception{ |
192 | raFile.writeInt(MAGIC_TABLE); |
193 | raFile.writeInt(TABLE_VIEW_VERSION); |
194 | } |
195 | |
196 | |
197 | /*StoreImpl getStoreCreate( SSConnection con, long filePos ) throws Exception{ |
198 | return StoreImpl.createStore( con, raFile, SQLTokenizer.CREATE, filePos ); |
199 | }*/ |
200 | |
201 | StoreImpl getStore( SSConnection con, long filePos, int pageOperation ) throws Exception{ |
202 | TableStorePage storePage = requestLock( con, pageOperation, filePos ); |
203 | return StoreImpl.createStore( this, storePage, pageOperation, filePos ); |
204 | } |
205 | |
206 | |
207 | StoreImpl getStore( TableStorePage storePage, int pageOperation ) throws Exception{ |
208 | // is used for not commited INSERT pages, a new lock is not needed |
209 | return StoreImpl.recreateStore( this, storePage, pageOperation ); |
210 | } |
211 | |
212 | /*StoreImpl getStoreUpdate( SSConnection con, long filePos ) throws Exception{ |
213 | return StoreImpl.createStore( con, raFile, SQLTokenizer.UPDATE, filePos ); |
214 | } |
215 | |
216 | StoreImpl getStoreDelete( SSConnection con, long filePos ) throws Exception{ |
217 | return StoreImpl.createStore( con, raFile, SQLTokenizer.DELETE, filePos ); |
218 | }*/ |
219 | |
220 | |
221 | StoreImpl getStoreInsert( SSConnection con ) throws Exception{ |
222 | TableStorePage storePage = requestLock( con, SQLTokenizer.INSERT, -1 ); |
223 | return StoreImpl.createStore( this, storePage, SQLTokenizer.INSERT, -1 ); |
224 | } |
225 | |
226 | |
227 | /** |
228 | * Create a Store that is not invoke in a transaction for copy of data. |
229 | */ |
230 | StoreImpl getStoreTemp( SSConnection con ) throws Exception{ |
231 | TableStorePage storePage = new TableStorePage( con, this, LOCK_NONE, -2); |
232 | return StoreImpl.createStore( this, storePage, SQLTokenizer.INSERT, -2 ); |
233 | } |
234 | |
235 | |
236 | StoreImpl getLobStore(SSConnection con, long filePos, int pageOperation) throws Exception{ |
237 | if(lobs == null){ |
238 | lobs = new Lobs( this ); |
239 | } |
240 | return lobs.getStore( con, filePos, pageOperation ); |
241 | } |
242 | |
243 | |
244 | |
245 | /** |
246 | * Return the file offset of the first page with data after the table declaration. |
247 | * This is equals to the first row. |
248 | */ |
249 | final long getFirstPage(){ |
250 | return firstPage; |
251 | } |
252 | |
253 | |
254 | /** |
255 | * Return a list of Links to not commited rows. The list include only the rows that are visible for |
256 | * the current isolation level. |
257 | */ |
258 | List getInserts(SSConnection con){ |
259 | synchronized(locks){ |
260 | ArrayList inserts = new ArrayList(); |
261 | if(con.isolationLevel <= Connection.TRANSACTION_READ_UNCOMMITTED){ |
262 | for(int i=0; i<locksInsert.size(); i++){ |
263 | TableStorePageInsert lock = (TableStorePageInsert)locksInsert.get(i); |
264 | inserts.add(lock.getLink()); |
265 | } |
266 | }else{ |
267 | for(int i=0; i<locksInsert.size(); i++){ |
268 | TableStorePageInsert lock = (TableStorePageInsert)locksInsert.get(i); |
269 | if(lock.con == con) |
270 | inserts.add(lock.getLink()); |
271 | } |
272 | } |
273 | return inserts; |
274 | } |
275 | } |
276 | |
277 | |
278 | final private TableStorePage requestLock(SSConnection con, int pageOperation, long page) throws Exception{ |
279 | synchronized(locks){ |
280 | long endTime = 0; |
281 | while(true){ |
282 | TableStorePage storePage = requestLockImpl( con, pageOperation, page); |
283 | if(storePage != null) |
284 | return storePage; // the normal case shoud be the fasted |
285 | if(endTime == 0) |
286 | endTime = System.currentTimeMillis() + 5000; |
287 | long waitTime = endTime - System.currentTimeMillis(); |
288 | if(waitTime <= 0) |
289 | throw Utils.createSQLException("Deadlock, can not create a lock on table '"+name+"'"); |
290 | locks.wait(waitTime); |
291 | } |
292 | } |
293 | } |
294 | |
295 | /** |
296 | * Request an page lock. If the rquest is valid then it return the StorePage. |
297 | * In the other case it return null. |
298 | * @param page The fileOffset or -1 for a new page |
299 | * @throws SQLException |
300 | */ |
301 | final private TableStorePage requestLockImpl(SSConnection con, int pageOperation, long page) throws SQLException{ |
302 | synchronized(locks){ |
303 | if(tabLockConnection != null && tabLockConnection != con) return null; |
304 | switch(con.isolationLevel){ |
305 | case Connection.TRANSACTION_SERIALIZABLE: |
306 | con.serializeCount++; |
307 | serializeConnections.put( con, con); |
308 | break; |
309 | } |
310 | |
311 | switch(pageOperation){ |
312 | case SQLTokenizer.CREATE:{ |
313 | // first check if another connection has a lock bevore creating a table lock |
314 | if(locks.size() > 0){ |
315 | Iterator values = locks.values().iterator(); |
316 | while(values.hasNext()){ |
317 | TableStorePage lock = (TableStorePage)values.next(); |
318 | if(lock.con != con) return null; |
319 | } |
320 | } |
321 | for(int i=0; i<locksInsert.size(); i++){ |
322 | //the first StorePage in the linked list must be ever TableStorePageInsert |
323 | TableStorePageInsert lock = (TableStorePageInsert)locksInsert.get(i); |
324 | if(lock.con != con) return null; |
325 | } |
326 | if(serializeConnections.size() > 0){ |
327 | Iterator values = serializeConnections.values().iterator(); |
328 | while(values.hasNext()){ |
329 | TableStorePage lock = (TableStorePage)values.next(); |
330 | if(lock.con != con) return null; |
331 | } |
332 | } |
333 | tabLockConnection = con; |
334 | tabLockCount++; |
335 | TableStorePage lock = new TableStorePage(con, this, LOCK_TAB, page); |
336 | con.add(lock); |
337 | return lock; |
338 | } |
339 | case SQLTokenizer.INSERT:{ |
340 | // if there are more as one Connection with a serializable lock then an INSERT is not valid |
341 | if(serializeConnections.size() > 1) return null; |
342 | if(serializeConnections.size() == 1 && serializeConnections.get(con) != null) return null; |
343 | TableStorePageInsert lock = new TableStorePageInsert(con, this, LOCK_INSERT); |
344 | locksInsert.add( lock ); |
345 | con.add(lock); |
346 | return lock; |
347 | } |
348 | case SQLTokenizer.SELECT:{ |
349 | Long pageKey = new Long(page); //TODO performance |
350 | TableStorePage lockFirst; |
351 | TableStorePage lock = lockFirst = (TableStorePage)locks.get( pageKey ); |
352 | while(lock != null){ |
353 | if(lock.con == con || |
354 | con.isolationLevel <= Connection.TRANSACTION_READ_UNCOMMITTED) return lock; |
355 | if(lock.lockType == LOCK_WRITE) return null; // write lock of another Connection |
356 | lock = lock.nextLock; |
357 | } |
358 | lock = new TableStorePage( con, this, LOCK_NONE, page); |
359 | if(con.isolationLevel >= Connection.TRANSACTION_REPEATABLE_READ){ |
360 | lock.lockType = LOCK_READ; |
361 | lock.nextLock = lockFirst; |
362 | locks.put( pageKey, lock ); |
363 | con.add(lock); |
364 | } |
365 | return lock; |
366 | } |
367 | case SQLTokenizer.LONGVARBINARY: |
368 | // is used for written BLOB and CLOB |
369 | // the difference to INSERT is that page descript the size of the byte buffer |
370 | return new TableStorePage( con, this, LOCK_INSERT, -1); |
371 | default: |
372 | throw new Error("pageOperation:"+pageOperation); |
373 | } |
374 | } |
375 | } |
376 | |
377 | |
378 | /** |
379 | * Request a write lock for a page that is read. |
380 | * @throws SQLException |
381 | */ |
382 | TableStorePage requestWriteLock(SSConnection con, TableStorePage readlock) throws SQLException{ |
383 | if(readlock.lockType == LOCK_INSERT){ |
384 | TableStorePage lock = new TableStorePage( con, this, LOCK_INSERT, -1); |
385 | readlock.nextLock = lock; |
386 | con.add(lock); |
387 | return lock; |
388 | } |
389 | Long pageKey = new Long(readlock.fileOffset); //TODO performance |
390 | TableStorePage lockFirst; |
391 | TableStorePage lock = lockFirst = (TableStorePage)locks.get( pageKey ); |
392 | while(lock != null){ |
393 | if(lock.con != con) return null; // there is allready any lock from another connection, we can not start write |
394 | if(lock.lockType < LOCK_WRITE){ |
395 | // if there is only a read lock we can transfer it |
396 | // this is requied for rollback to a savepoint |
397 | lock.lockType = LOCK_WRITE; |
398 | return lock; |
399 | } |
400 | lock = lock.nextLock; |
401 | } |
402 | lock = new TableStorePage( con, this, LOCK_WRITE, readlock.fileOffset); |
403 | lock.nextLock = lockFirst; |
404 | locks.put( pageKey, lock ); |
405 | con.add(lock); |
406 | return lock; |
407 | } |
408 | |
409 | |
410 | /** |
411 | * Remove the lock from this table. |
412 | */ |
413 | void freeLock(TableStorePage storePage){ |
414 | final int lockType = storePage.lockType; |
415 | final long fileOffset = storePage.fileOffset; |
416 | synchronized(locks){ |
417 | try{ |
418 | TableStorePage lock; |
419 | TableStorePage prev; |
420 | switch(lockType){ |
421 | case LOCK_INSERT: |
422 | for(int i=0; i<locksInsert.size(); i++){ |
423 | prev = lock = (TableStorePage)locksInsert.get(i); |
424 | while(lock != null){ |
425 | if(lock == storePage){ |
426 | //remove lock |
427 | if(lock == prev){ |
428 | if(lock.nextLock == null){ |
429 | // the first lock is the only lock in the list |
430 | locksInsert.remove(i--); |
431 | }else{ |
432 | // only the first lock of the list is remove |
433 | locksInsert.set( i, lock.nextLock ); |
434 | } |
435 | }else{ |
436 | // a lock in the mid or end is removed |
437 | prev.nextLock = lock.nextLock; |
438 | } |
439 | return; |
440 | } |
441 | prev = lock; |
442 | lock = lock.nextLock; |
443 | } |
444 | } |
445 | break; |
446 | case LOCK_READ: |
447 | case LOCK_WRITE: |
448 | Long pageKey = new Long(fileOffset); //TODO performance |
449 | lock = (TableStorePage)locks.get( pageKey ); |
450 | prev = lock; |
451 | while(lock != null){ |
452 | if(lock == storePage){ |
453 | //lock entfernen |
454 | if(lock == prev){ |
455 | if(lock.nextLock == null){ |
456 | // erste und einzige Lock in Liste |
457 | locks.remove(pageKey); |
458 | }else{ |
459 | // erste Lock in liste fällt weg |
460 | locks.put( pageKey, lock.nextLock ); |
461 | } |
462 | }else{ |
463 | // lock in mitte oder ende der Liste fällt weg |
464 | prev = lock.nextLock; |
465 | } |
466 | return; |
467 | } |
468 | prev = lock; |
469 | lock = lock.nextLock; |
470 | } |
471 | // Durchläufer kann auftreten, wenn eine Lock hochgestuft wurde und damit der type nicht stimmt |
472 | break; |
473 | case LOCK_TAB: |
474 | assert storePage.con == tabLockConnection : "Internale Error with TabLock"; |
475 | if(--tabLockCount == 0) tabLockConnection = null; |
476 | break; |
477 | default: |
478 | throw new Error(); |
479 | } |
480 | }finally{ |
481 | locks.notifyAll(); |
482 | } |
483 | } |
484 | } |
485 | |
486 | } |
487 | |