| 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 | |