]> source.dussan.org Git - jackcess.git/commitdiff
Add option to DatabaseBuilder for ignoring broken system catalog indexes. Fixes #46
authorJames Ahlborn <jtahlborn@yahoo.com>
Thu, 12 Jan 2023 22:22:29 +0000 (22:22 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Thu, 12 Jan 2023 22:22:29 +0000 (22:22 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1395 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/main/java/com/healthmarketscience/jackcess/DatabaseBuilder.java
src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java
src/main/java/com/healthmarketscience/jackcess/impl/IndexPageCache.java
src/main/java/com/healthmarketscience/jackcess/util/CustomLinkResolver.java
src/test/java/com/healthmarketscience/jackcess/BigIndexTest.java
src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java
src/test/java/com/healthmarketscience/jackcess/IndexTest.java
src/test/java/com/healthmarketscience/jackcess/TestUtil.java

index d0640e8db4c6dc3ed7d7730b76157309f3839766..e821ac4d9ad49f08996d1b7c77c06ebc6c7ffc11 100644 (file)
@@ -4,6 +4,14 @@
     <author email="javajedi@users.sf.net">Tim McCune</author>
   </properties>
   <body>
+    <release version="4.0.5" date="TBD">
+      <action dev="jahlborn" type="add" system="SourceForge2Features"
+              issue="46">
+        Add option to DatabaseBuilder for ignoring broken system catalog
+        indexes.  This is a workaround to allow jackcess to read tables from
+        the database even if the index is non-functional..
+      </action>
+    </release>
     <release version="4.0.4" date="2022-10-29">
       <action dev="jahlborn" type="update">
         Update parent pom to fix release process.
index 18ba994aa79fa0030ccb068ed624d28fd3a3ffd4..9b622d933cc5fbad529b2c14e677412396213067 100644 (file)
@@ -77,7 +77,8 @@ public class DatabaseBuilder
   private Map<String,PropertyMap.Property> _summaryProps;
   /** database user-defined (if any) */
   private Map<String,PropertyMap.Property> _userProps;
-
+  /** flag indicating that the system catalog index is borked */
+  private boolean _ignoreBrokenSystemCatalogIndex;
 
   public DatabaseBuilder() {
     this((Path)null);
@@ -260,12 +261,23 @@ public class DatabaseBuilder
     return props;
   }
 
+  /**
+   * Sets flag which, if {@code true}, will make the database ignore the index
+   * on the system catalog when looking up tables.  This will make table
+   * retrieval slower, but can be used to workaround broken indexes.
+   */
+  public DatabaseBuilder setIgnoreBrokenSystemCatalogIndex(boolean ignore) {
+    _ignoreBrokenSystemCatalogIndex = ignore;
+    return this;
+  }
+
   /**
    * Opens an existingnew Database using the configured information.
    */
   public Database open() throws IOException {
     return DatabaseImpl.open(_mdbFile, _readOnly, _channel, _autoSync, _charset,
-                             _timeZone, _codecProvider);
+                             _timeZone, _codecProvider,
+                             _ignoreBrokenSystemCatalogIndex);
   }
 
   /**
index 7370626e472636e8262065143e5378181f21eeb6..7c499dc9d917a0f2f69ddcf658272d3a7836d350 100644 (file)
@@ -393,7 +393,7 @@ public class DatabaseImpl implements Database, DateTimeContext
   public static DatabaseImpl open(
       Path mdbFile, boolean readOnly, FileChannel channel,
       boolean autoSync, Charset charset, TimeZone timeZone,
-      CodecProvider provider)
+      CodecProvider provider, boolean ignoreSystemCatalogIndex)
     throws IOException
   {
     boolean closeChannel = false;
@@ -439,7 +439,7 @@ public class DatabaseImpl implements Database, DateTimeContext
 
       DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync,
                                          null, charset, timeZone, provider,
-                                         readOnly);
+                                         readOnly, ignoreSystemCatalogIndex);
       success = true;
       return db;
 
@@ -499,7 +499,7 @@ public class DatabaseImpl implements Database, DateTimeContext
       channel.force(true);
       DatabaseImpl db = new DatabaseImpl(mdbFile, channel, closeChannel, autoSync,
                                          fileFormat, charset, timeZone, null,
-                                         false);
+                                         false, false);
       success = true;
       return db;
     } finally {
@@ -554,7 +554,7 @@ public class DatabaseImpl implements Database, DateTimeContext
   protected DatabaseImpl(Path file, FileChannel channel, boolean closeChannel,
                          boolean autoSync, FileFormat fileFormat, Charset charset,
                          TimeZone timeZone, CodecProvider provider,
-                         boolean readOnly)
+                         boolean readOnly, boolean ignoreSystemCatalogIndex)
     throws IOException
   {
     _file = file;
@@ -578,7 +578,7 @@ public class DatabaseImpl implements Database, DateTimeContext
     // needed
     _pageChannel.initialize(this, provider);
     _buffer = _pageChannel.createPageBuffer();
-    readSystemCatalog();
+    readSystemCatalog(ignoreSystemCatalogIndex);
   }
 
   @Override
@@ -992,27 +992,40 @@ public class DatabaseImpl implements Database, DateTimeContext
   /**
    * Read the system catalog
    */
-  private void readSystemCatalog() throws IOException {
+  private void readSystemCatalog(boolean ignoreSystemCatalogIndex)
+    throws IOException {
     _systemCatalog = loadTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG,
                                SYSTEM_OBJECT_FLAGS, TYPE_TABLE);
 
-    try {
-      _tableFinder = new DefaultTableFinder(
-          _systemCatalog.newCursor()
+    if(!ignoreSystemCatalogIndex) {
+      try {
+        _tableFinder = new DefaultTableFinder(
+            _systemCatalog.newCursor()
             .setIndexByColumnNames(CAT_COL_PARENT_ID, CAT_COL_NAME)
             .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE)
             .toIndexCursor());
-    } catch(IllegalArgumentException e) {
+      } catch(IllegalArgumentException e) {
+        if(LOG.isDebugEnabled()) {
+          LOG.debug(withErrorContext(
+                        "Could not find expected index on table " +
+                        _systemCatalog.getName()));
+        }
+        // use table scan instead
+        _tableFinder = new FallbackTableFinder(
+            _systemCatalog.newCursor()
+            .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE)
+            .toCursor());
+      }
+    } else {
       if(LOG.isDebugEnabled()) {
         LOG.debug(withErrorContext(
-                "Could not find expected index on table " +
-                _systemCatalog.getName()));
+                      "Ignoring index on table " + _systemCatalog.getName()));
       }
       // use table scan instead
       _tableFinder = new FallbackTableFinder(
           _systemCatalog.newCursor()
-            .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE)
-            .toCursor());
+          .setColumnMatcher(CaseInsensitiveColumnMatcher.INSTANCE)
+          .toCursor());
     }
 
     _tableParentId = _tableFinder.findObjectId(DB_PARENT_ID,
index 5afe8943b53d92c8113d61ff9603fa4fbd81854a..1c430fc686e65666c4c807fb908be0855931fcdc 100644 (file)
@@ -388,10 +388,12 @@ public class IndexData {
 
   /**
    * Used by unit tests to validate the internal status of the index.
+   * @param forceLoad if {@code false} only validate currently loaded index
+   *                  data pages, otherwise, load and validate all index pages
    * @usage _advanced_method_
    */
-  public void validate() throws IOException {
-    _pageCache.validate();
+  public void validate(boolean forceLoad) throws IOException {
+    _pageCache.validate(forceLoad);
   }
 
   /**
index d594c1c176cde117b8df7951b342c970ce79cc8f..b64b93daabb963ac08cbb043b7ef0cfa531fe011 100644 (file)
@@ -22,13 +22,19 @@ import java.lang.ref.SoftReference;
 import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.RandomAccess;
+import java.util.Set;
 
 import static com.healthmarketscience.jackcess.impl.IndexData.*;
+import com.healthmarketscience.jackcess.impl.IndexData.DataPage;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 
 /**
@@ -44,7 +50,7 @@ public class IndexPageCache
   /** max number of pages to cache (unless a write operation is in
       progress) */
   private static final int MAX_CACHE_SIZE = 25;
-  
+
   /** the index whose pages this cache is managing */
   private final IndexData _indexData;
   /** the root page for the index */
@@ -67,7 +73,7 @@ public class IndexPageCache
   /** the currently modified index pages */
   private final List<CacheDataPage> _modifiedPages =
     new ArrayList<CacheDataPage>();
-  
+
   public IndexPageCache(IndexData indexData) {
     _indexData = indexData;
   }
@@ -75,11 +81,11 @@ public class IndexPageCache
   public IndexData getIndexData() {
     return _indexData;
   }
-  
+
   public PageChannel getPageChannel() {
     return getIndexData().getPageChannel();
   }
-  
+
   /**
    * Sets the root page for this index, must be called before normal usage.
    *
@@ -90,7 +96,7 @@ public class IndexPageCache
     // root page has no parent
     _rootPage.initParentPage(INVALID_INDEX_PAGE_NUMBER, false);
   }
-  
+
   /**
    * Writes any outstanding changes for this index to the file.
    */
@@ -129,7 +135,7 @@ public class IndexPageCache
       }
     }
   }
-  
+
   /**
    * Prepares any non-empty modified pages for writing as the second pass
    * during a {@link #write} call.  Updates entry prefixes, promotes/demotes
@@ -158,14 +164,14 @@ public class IndexPageCache
           if(dpMain.hasChildTail()) {
             if(size == 1) {
               demoteTail(cacheDataPage);
-            } 
+            }
           } else {
             if(size > 1) {
               promoteTail(cacheDataPage);
             }
           }
         }
-        
+
         // look for pages with more entries than can fit on a page
         if(cacheDataPage.getTotalEntrySize() > maxPageEntrySize) {
 
@@ -181,7 +187,7 @@ public class IndexPageCache
           }
         }
       }
-      
+
     } while(splitPages);
   }
 
@@ -211,7 +217,7 @@ public class IndexPageCache
     DataPageMain main = getDataPage(pageNumber);
     return((main != null) ? new CacheDataPage(main) : null);
   }
-  
+
   /**
    * Returns a DataPageMain for the given page number, may be {@code null} if
    * the given page number is invalid.  Loads the given page if necessary.
@@ -236,9 +242,9 @@ public class IndexPageCache
     getIndexData().writeDataPage(cacheDataPage);
 
     // lastly, mark the page as no longer modified
-    cacheDataPage._extra._modified = false;    
+    cacheDataPage._extra._modified = false;
   }
-  
+
   /**
    * Deletes the given index page from the file (clears the page).
    */
@@ -250,11 +256,11 @@ public class IndexPageCache
 
     // discard from our cache
     _dataPages.remove(cacheDataPage._main._pageNumber);
-    
+
     // lastly, mark the page as no longer modified
-    cacheDataPage._extra._modified = false;    
+    cacheDataPage._extra._modified = false;
   }
-  
+
   /**
    * Reads the given index page from the file.
    */
@@ -268,9 +274,9 @@ public class IndexPageCache
 
     // associate the extra info with the main data page
     dataPage.setExtra(extra);
-    
+
     return cacheDataPage;
-  }  
+  }
 
   /**
    * Removes the entry with the given index from the given page.
@@ -298,7 +304,7 @@ public class IndexPageCache
   {
     updateEntry(cacheDataPage, entryIdx, newEntry, UpdateType.ADD);
   }
-  
+
   /**
    * Updates the entries on the given page according to the given updateType.
    *
@@ -325,7 +331,7 @@ public class IndexPageCache
     CacheDataPage parentDataPage = (!dpMain.isRoot() ?
                                     new CacheDataPage(dpMain.getParentPage()) :
                                     null);
-    
+
     Entry oldLastEntry = dpExtra._entryView.getLast();
     Entry oldEntry = null;
     int entrySizeDiff = 0;
@@ -352,7 +358,7 @@ public class IndexPageCache
     }
 
     boolean updateLast = (oldLastEntry != dpExtra._entryView.getLast());
-    
+
     // child tail entry updates do not modify the page
     if(!updateLast || !dpMain.hasChildTail()) {
       dpExtra._totalEntrySize += entrySizeDiff;
@@ -368,7 +374,7 @@ public class IndexPageCache
       return oldEntry;
     }
 
-    // determine if we need to update our parent page 
+    // determine if we need to update our parent page
     if(!updateLast || dpMain.isRoot()) {
       // no parent
       return oldEntry;
@@ -404,7 +410,7 @@ public class IndexPageCache
               "Empty page but size is not 0? " + dpExtra._totalEntrySize + ", " +
               cacheDataPage));
     }
-    
+
     if(dpMain.isRoot()) {
       // clear out this page (we don't actually remove it)
       dpExtra._entryPrefix = EMPTY_PREFIX;
@@ -433,7 +439,7 @@ public class IndexPageCache
 
     Integer prevPageNumber = dpMain._prevPageNumber;
     Integer nextPageNumber = dpMain._nextPageNumber;
-    
+
     DataPageMain prevMain = dpMain.getPrevPage();
     if(prevMain != null) {
       setModified(new CacheDataPage(prevMain));
@@ -461,7 +467,7 @@ public class IndexPageCache
     updateParentEntry(parentDataPage, childDataPage, null,
                       childExtra._entryView.getLast(), UpdateType.ADD);
   }
-  
+
   /**
    * Replaces the entry for the given child page in the given parent page.
    *
@@ -478,7 +484,7 @@ public class IndexPageCache
     updateParentEntry(parentDataPage, childDataPage, oldEntry,
                       childExtra._entryView.getLast(), UpdateType.REPLACE);
   }
-  
+
   /**
    * Updates the entry for the given child page in the given parent page
    * according to the given updateType.
@@ -513,7 +519,7 @@ public class IndexPageCache
 
     boolean expectFound = true;
     int idx = 0;
-    
+
     switch(upType) {
     case ADD:
       expectFound = false;
@@ -524,12 +530,12 @@ public class IndexPageCache
     case REMOVE:
       idx = parentExtra._entryView.find(oldEntry);
       break;
-    
+
     default:
       throw new RuntimeException(withErrorContext(
               "unknown update type " + upType));
     }
-        
+
     if(idx < 0) {
       if(expectFound) {
         throw new IllegalStateException(withErrorContext(
@@ -564,7 +570,6 @@ public class IndexPageCache
   private void updateParentTail(CacheDataPage parentDataPage,
                                 CacheDataPage childDataPage,
                                 UpdateType upType)
-    throws IOException
   {
     DataPageMain parentMain = parentDataPage._main;
 
@@ -577,7 +582,7 @@ public class IndexPageCache
       parentMain._childTailPageNumber = newChildTailPageNumber;
     }
   }
-  
+
   /**
    * Verifies that the given entry type (node/leaf) is valid for the given
    * page (node/leaf).
@@ -607,13 +612,13 @@ public class IndexPageCache
     DataPageExtra origExtra = origDataPage._extra;
 
     setModified(origDataPage);
-    
+
     int numEntries = origExtra._entries.size();
     if(numEntries < 2) {
       throw new IllegalStateException(withErrorContext(
               "Cannot split page with less than 2 entries " + origDataPage));
     }
-    
+
     if(origMain.isRoot()) {
       // we can't split the root page directly, so we need to put another page
       // between the root page and its sub-pages, and then split that page.
@@ -629,7 +634,7 @@ public class IndexPageCache
     // start mucking with our entries because our parent may use our entries.
     DataPageMain parentMain = origMain.getParentPage();
     CacheDataPage parentDataPage = new CacheDataPage(parentMain);
-    
+
     // note, there are many, many ways this could be improved/tweaked.  for
     // now, we just want it to be functional...
     // so, we will naively move half the entries from one page to a new page.
@@ -638,7 +643,7 @@ public class IndexPageCache
         parentMain._pageNumber, origMain._leaf);
     DataPageMain newMain = newDataPage._main;
     DataPageExtra newExtra = newDataPage._extra;
-    
+
     List<Entry> headEntries =
       origExtra._entries.subList(0, ((numEntries + 1) / 2));
 
@@ -657,7 +662,7 @@ public class IndexPageCache
 
     // insert this new page between the old page and any previous page
     addToPeersBefore(newDataPage, origDataPage);
-    
+
     if(!newMain._leaf) {
       // reparent the children pages of the new page
       reparentChildren(newDataPage);
@@ -683,7 +688,7 @@ public class IndexPageCache
    * split.
    *
    * @param rootDataPage the root data page
-   * 
+   *
    * @return the newly created page nested under the root page
    */
   private CacheDataPage nestRootDataPage(CacheDataPage rootDataPage)
@@ -696,7 +701,7 @@ public class IndexPageCache
       throw new IllegalArgumentException(withErrorContext(
               "should be called with root, duh"));
     }
-    
+
     CacheDataPage newDataPage =
       allocateNewCacheDataPage(rootMain._pageNumber, rootMain._leaf);
     DataPageMain newMain = newDataPage._main;
@@ -713,7 +718,7 @@ public class IndexPageCache
       // we need to re-parent all the child pages
       reparentChildren(newDataPage);
     }
-      
+
     // clear the root page
     rootMain._leaf = false;
     rootMain._childTailPageNumber = INVALID_INDEX_PAGE_NUMBER;
@@ -727,7 +732,7 @@ public class IndexPageCache
 
     return newDataPage;
   }
-  
+
   /**
    * Allocates a new index page with the given parent page and type.
    *
@@ -783,7 +788,7 @@ public class IndexPageCache
     newMain._nextPageNumber = origMain._pageNumber;
     newMain._prevPageNumber = origMain._prevPageNumber;
     origMain._prevPageNumber = newMain._pageNumber;
-    
+
     if(prevMain != null) {
       setModified(new CacheDataPage(prevMain));
       prevMain._nextPageNumber = newMain._pageNumber;
@@ -808,7 +813,7 @@ public class IndexPageCache
     nextMain._prevPageNumber = INVALID_INDEX_PAGE_NUMBER;
     dpMain._nextPageNumber = INVALID_INDEX_PAGE_NUMBER;
   }
-  
+
   /**
    * Sets the parent info for the children of the given page to the given
    * page.
@@ -816,7 +821,6 @@ public class IndexPageCache
    * @param cacheDataPage the page whose children need to be updated
    */
   private void reparentChildren(CacheDataPage cacheDataPage)
-    throws IOException
   {
     DataPageMain dpMain = cacheDataPage._main;
     DataPageExtra dpExtra = cacheDataPage._extra;
@@ -849,7 +853,7 @@ public class IndexPageCache
     DataPageExtra dpExtra = cacheDataPage._extra;
 
     setModified(cacheDataPage);
-    
+
     DataPageMain tailMain = dpMain.getChildTailPage();
     CacheDataPage tailDataPage = new CacheDataPage(tailMain);
 
@@ -858,10 +862,10 @@ public class IndexPageCache
     Entry tailEntry = dpExtra._entryView.demoteTail();
     dpExtra._totalEntrySize += tailEntry.size();
     dpExtra._entryPrefix = EMPTY_PREFIX;
-    
+
     tailMain.setParentPage(dpMain._pageNumber, false);
   }
-  
+
   /**
    * Makes the last normal entry of the given page the tail entry on that
    * page, done when there are multiple entries on a page and no tail entry.
@@ -876,7 +880,7 @@ public class IndexPageCache
     DataPageExtra dpExtra = cacheDataPage._extra;
 
     setModified(cacheDataPage);
-    
+
     DataPageMain lastMain = dpMain.getChildPage(dpExtra._entryView.getLast());
     CacheDataPage lastDataPage = new CacheDataPage(lastMain);
 
@@ -888,7 +892,7 @@ public class IndexPageCache
 
     lastMain.setParentPage(dpMain._pageNumber, true);
   }
-  
+
   /**
    * Finds the index page on which the given entry does or should reside.
    *
@@ -904,7 +908,7 @@ public class IndexPageCache
         // nowhere to go from here
         return new CacheDataPage(curPage);
       }
-      
+
       DataPageExtra extra = curPage.getExtra();
 
       // need to descend
@@ -949,24 +953,24 @@ public class IndexPageCache
   {
     byte[] b1 = e1.getEntryBytes();
     byte[] b2 = e2.getEntryBytes();
-    
+
     int maxLen = b1.length;
     byte[] prefix = b1;
     if(b1.length > b2.length) {
       maxLen = b2.length;
       prefix = b2;
     }
-    
+
     int len = 0;
     while((len < maxLen) && (b1[len] == b2[len])) {
       ++len;
     }
-    
+
     if(len < prefix.length) {
       if(len == 0) {
         return EMPTY_PREFIX;
       }
-      
+
       // need new prefix
       prefix = ByteUtil.copyOf(prefix, len);
     }
@@ -977,133 +981,10 @@ public class IndexPageCache
   /**
    * Used by unit tests to validate the internal status of the index.
    */
-  void validate() throws IOException {
-    // copy the values as the validation methods might trigger map updates
-    for(DataPageMain dpMain : new ArrayList<DataPageMain>(_dataPages.values())) {
-      DataPageExtra dpExtra = dpMain.getExtra();
-      validateEntries(dpExtra);
-      validateChildren(dpMain, dpExtra);
-      validatePeers(dpMain);
-    }
-  }
-
-  /**
-   * Validates the entries for an index page
-   *
-   * @param dpExtra the entries to validate
-   */
-  private void validateEntries(DataPageExtra dpExtra) throws IOException {
-    int entrySize = 0;
-    Entry prevEntry = FIRST_ENTRY;
-    for(Entry e : dpExtra._entries) {
-      entrySize += e.size();
-      if(prevEntry.compareTo(e) >= 0) {
-        throw new IOException(withErrorContext(
-                "Unexpected order in index entries, " + prevEntry + " >= " + e));
-      }
-      prevEntry = e;
-    }
-    if(entrySize != dpExtra._totalEntrySize) {
-      throw new IllegalStateException(withErrorContext(
-              "Expected size " + entrySize +
-              " but was " + dpExtra._totalEntrySize));
-    }
-  }
-
-  /**
-   * Validates the children for an index page
-   *
-   * @param dpMain the index page
-   * @param dpExtra the child entries to validate
-   */
-  private void validateChildren(DataPageMain dpMain,
-                                DataPageExtra dpExtra) throws IOException {
-    int childTailPageNumber = dpMain._childTailPageNumber;
-    if(dpMain._leaf) {
-      if(childTailPageNumber != INVALID_INDEX_PAGE_NUMBER) {
-        throw new IllegalStateException(withErrorContext(
-                "Leaf page has tail " + dpMain));
-      }
-      return;
-    }
-    if((dpExtra._entryView.size() == 1) && dpMain.hasChildTail()) {
-      throw new IllegalStateException(withErrorContext(
-              "Single child is tail " + dpMain));
-    }
-    for(Entry e : dpExtra._entryView) {
-      validateEntryForPage(dpMain, e);
-      Integer subPageNumber = e.getSubPageNumber();
-      DataPageMain childMain = _dataPages.get(subPageNumber);
-      if(childMain != null) {
-        if(childMain._parentPageNumber != null) {
-          if(childMain._parentPageNumber != dpMain._pageNumber) {
-            throw new IllegalStateException(withErrorContext(
-                    "Child's parent is incorrect " + childMain));
-          }
-          boolean expectTail = (subPageNumber == childTailPageNumber);
-          if(expectTail != childMain._tail) {
-            throw new IllegalStateException(withErrorContext(
-                    "Child tail status incorrect " + childMain));
-          }
-        }
-        Entry lastEntry = childMain.getExtra()._entryView.getLast();
-        if(e.compareTo(lastEntry) != 0) {
-          throw new IllegalStateException(withErrorContext(
-                  "Invalid entry " + e + " but child is " + lastEntry));
-        }
-      }
-    }
-  }
-
-  /**
-   * Validates the peer pages for an index page.
-   *
-   * @param dpMain the index page
-   */
-  private void validatePeers(DataPageMain dpMain) throws IOException {
-    DataPageMain prevMain = _dataPages.get(dpMain._prevPageNumber);
-    if(prevMain != null) {
-      if(prevMain._nextPageNumber != dpMain._pageNumber) {
-        throw new IllegalStateException(withErrorContext(
-                "Prev page " + prevMain + " does not ref " + dpMain));
-      }
-      validatePeerStatus(dpMain, prevMain);
-    }
-    
-    DataPageMain nextMain = _dataPages.get(dpMain._nextPageNumber);
-    if(nextMain != null) {
-      if(nextMain._prevPageNumber != dpMain._pageNumber) {
-        throw new IllegalStateException(withErrorContext(
-                "Next page " + nextMain + " does not ref " + dpMain));
-      }
-      validatePeerStatus(dpMain, nextMain);
-    }
+  void validate(boolean forceLoad) throws IOException {
+    new Validator(forceLoad).validate();
   }
 
-  /**
-   * Validates the given peer page against the given index page
-   *
-   * @param dpMain the index page
-   * @param peerMain the peer index page
-   */
-  private void validatePeerStatus(DataPageMain dpMain, DataPageMain peerMain)
-    throws IOException
-  {
-    if(dpMain._leaf != peerMain._leaf) {
-      throw new IllegalStateException(withErrorContext(
-              "Mismatched peer status " + dpMain._leaf + " " + peerMain._leaf));
-    }
-    if(!dpMain._leaf) {
-      if((dpMain._parentPageNumber != null) &&
-         (peerMain._parentPageNumber != null) &&
-         ((int)dpMain._parentPageNumber != (int)peerMain._parentPageNumber)) {
-        throw new IllegalStateException(withErrorContext(
-                "Mismatched node parents " + dpMain._parentPageNumber + " " +
-                peerMain._parentPageNumber));
-      }
-    }
-  }
-  
   /**
    * Collects all the cache pages in the cache.
    *
@@ -1139,17 +1020,17 @@ public class IndexPageCache
         iter.remove();
         if(_dataPages.size() <= MAX_CACHE_SIZE) {
           break;
-        } 
+        }
       }
     }
   }
-  
+
   @Override
   public String toString() {
     ToStringBuilder sb = CustomToStringStyle.builder(this);
     if(_rootPage == null) {
       sb.append("pages", "(uninitialized)");
-    } else {    
+    } else {
       sb.append("pages", collectPages(new ArrayList<Object>(), _rootPage));
     }
     return sb.toString();
@@ -1158,7 +1039,7 @@ public class IndexPageCache
   private String withErrorContext(String msg) {
     return _indexData.withErrorContext(msg);
   }
-  
+
 
   /**
    * Keeps track of the main info for an index page.
@@ -1181,11 +1062,11 @@ public class IndexPageCache
     public IndexPageCache getCache() {
       return IndexPageCache.this;
     }
-    
+
     public boolean isRoot() {
       return(this == _rootPage);
     }
-    
+
     public boolean isTail() throws IOException
     {
       resolveParent();
@@ -1199,7 +1080,7 @@ public class IndexPageCache
     public boolean isChildTailPageNumber(int pageNumber) {
       return(_childTailPageNumber == pageNumber);
     }
-    
+
     public DataPageMain getParentPage() throws IOException
     {
       resolveParent();
@@ -1212,7 +1093,7 @@ public class IndexPageCache
         setParentPage(parentPageNumber, isTail);
       }
     }
-    
+
     public void setParentPage(Integer parentPageNumber, boolean isTail) {
       _parentPageNumber = parentPageNumber;
       _tail = isTail;
@@ -1222,19 +1103,19 @@ public class IndexPageCache
     {
       return IndexPageCache.this.getDataPage(_prevPageNumber);
     }
-    
+
     public DataPageMain getNextPage() throws IOException
     {
       return IndexPageCache.this.getDataPage(_nextPageNumber);
     }
-    
+
     public DataPageMain getChildPage(Entry e) throws IOException
     {
       Integer childPageNumber = e.getSubPageNumber();
       return getChildPage(childPageNumber,
                           isChildTailPageNumber(childPageNumber));
     }
-    
+
     public DataPageMain getChildTailPage() throws IOException
     {
       return getChildPage(_childTailPageNumber, true);
@@ -1254,7 +1135,7 @@ public class IndexPageCache
       }
       return child;
     }
-    
+
     public DataPageExtra getExtra() throws IOException
     {
       DataPageExtra extra = _extra.get();
@@ -1262,10 +1143,10 @@ public class IndexPageCache
         extra = readDataPage(_pageNumber)._extra;
         setExtra(extra);
       }
-      
+
       return extra;
     }
-    
+
     public void setExtra(DataPageExtra extra) throws IOException
     {
       extra.setEntryView(this);
@@ -1313,7 +1194,7 @@ public class IndexPageCache
     public void setEntryView(DataPageMain main) throws IOException {
       _entryView = new EntryListView(main, this);
     }
-    
+
     public void updateEntryPrefix() {
       if(_entryPrefix.length == 0) {
         // prefix is only related to *real* entries, tail not included
@@ -1321,7 +1202,7 @@ public class IndexPageCache
                                         _entries.get(_entries.size() - 1));
       }
     }
-    
+
     @Override
     public String toString() {
       return CustomToStringStyle.builder("DPExtra")
@@ -1341,7 +1222,7 @@ public class IndexPageCache
     private CacheDataPage(DataPageMain dataPage) throws IOException {
       this(dataPage, dataPage.getExtra());
     }
-    
+
     private CacheDataPage(DataPageMain dataPage, DataPageExtra extra) {
       _main = dataPage;
       _extra = extra;
@@ -1351,7 +1232,7 @@ public class IndexPageCache
     public int getPageNumber() {
       return _main._pageNumber;
     }
-    
+
     @Override
     public boolean isLeaf() {
       return _main._leaf;
@@ -1393,7 +1274,7 @@ public class IndexPageCache
       _main._childTailPageNumber = pageNumber;
     }
 
-    
+
     @Override
     public int getTotalEntrySize() {
       return _extra._totalEntrySize;
@@ -1429,12 +1310,12 @@ public class IndexPageCache
     public void addEntry(int idx, Entry entry) throws IOException {
       _main.getCache().addEntry(this, idx, entry);
     }
-    
+
     @Override
     public Entry removeEntry(int idx) throws IOException {
       return _main.getCache().removeEntry(this, idx);
     }
-    
+
   }
 
   /**
@@ -1460,7 +1341,7 @@ public class IndexPageCache
     private List<Entry> getEntries() {
       return _extra._entries;
     }
-    
+
     @Override
     public int size() {
       int size = getEntries().size();
@@ -1483,31 +1364,31 @@ public class IndexPageCache
               setChildTailEntry(newEntry) :
               getEntries().set(idx, newEntry));
     }
-    
+
     @Override
     public void add(int idx, Entry newEntry) {
       // note, we will never add to the "tail" entry, that will always be
       // handled through promoteTail
       getEntries().add(idx, newEntry);
     }
-    
+
     @Override
     public Entry remove(int idx) {
       return (isCurrentChildTailIndex(idx) ?
               setChildTailEntry(null) :
               getEntries().remove(idx));
     }
-    
+
     public Entry setChildTailEntry(Entry newEntry) {
       Entry old = _childTailEntry;
       _childTailEntry = newEntry;
       return old;
     }
-    
+
     private boolean hasChildTail() {
       return(_childTailEntry != null);
     }
-    
+
     private boolean isCurrentChildTailIndex(int idx) {
       return(idx == getEntries().size());
     }
@@ -1524,17 +1405,199 @@ public class IndexPageCache
       getEntries().add(tail);
       return tail;
     }
-    
+
     public Entry promoteTail() {
       Entry last = getEntries().remove(getEntries().size() - 1);
       _childTailEntry = last;
       return last;
     }
-    
+
     public int find(Entry e) {
       return Collections.binarySearch(this, e);
     }
 
   }
 
+  /**
+   * Utility class for running index validation.
+   */
+  private final class Validator {
+    private final boolean _forceLoad;
+    private final Map<Integer,DataPageMain> _knownPages = new HashMap<>();
+    private final Queue<DataPageMain> _pendingPages = new LinkedList<>();
+
+    private Validator(boolean forceLoad) {
+      _forceLoad = forceLoad;
+      _knownPages.putAll(_dataPages);
+      _pendingPages.addAll(_knownPages.values());
+    }
+
+    void validate() throws IOException {
+      DataPageMain dpMain = null;
+      while((dpMain = _pendingPages.poll()) != null) {
+        DataPageExtra dpExtra = dpMain.getExtra();
+        validateEntries(dpExtra);
+        validateChildren(dpMain, dpExtra);
+        validatePeers(dpMain);
+      }
+    }
+
+    /**
+     * Validates the entries for an index page
+     *
+     * @param dpExtra the entries to validate
+     */
+    private void validateEntries(DataPageExtra dpExtra) throws IOException {
+      int entrySize = 0;
+      Entry prevEntry = FIRST_ENTRY;
+      for(Entry e : dpExtra._entries) {
+        entrySize += e.size();
+        if(prevEntry.compareTo(e) >= 0) {
+          throw new IOException(withErrorContext(
+                  "Unexpected order in index entries, " + prevEntry +
+                  " >= " + e));
+        }
+        prevEntry = e;
+      }
+      if(entrySize != dpExtra._totalEntrySize) {
+        throw new IllegalStateException(withErrorContext(
+                "Expected size " + entrySize +
+                " but was " + dpExtra._totalEntrySize));
+      }
+    }
+
+    /**
+     * Validates the children for an index page
+     *
+     * @param dpMain the index page
+     * @param dpExtra the child entries to validate
+     */
+    private void validateChildren(DataPageMain dpMain,
+                                  DataPageExtra dpExtra) throws IOException {
+      int childTailPageNumber = dpMain._childTailPageNumber;
+      if(dpMain._leaf) {
+        if(childTailPageNumber != INVALID_INDEX_PAGE_NUMBER) {
+          throw new IllegalStateException(
+              withErrorContext("Leaf page has tail " + dpMain));
+        }
+        return;
+      }
+      if((dpExtra._entryView.size() == 1) && dpMain.hasChildTail()) {
+        throw new IllegalStateException(
+            withErrorContext("Single child is tail " + dpMain));
+      }
+      Integer prevPageNumber = null;
+      Integer nextPageNumber = null;
+      for(Entry e : dpExtra._entryView) {
+        validateEntryForPage(dpMain, e);
+        Integer subPageNumber = e.getSubPageNumber();
+        DataPageMain childMain = getPageForValidate(subPageNumber);
+        if(childMain != null) {
+          if((prevPageNumber != null) &&
+             ((int)childMain._prevPageNumber != prevPageNumber)) {
+            throw new IllegalStateException(withErrorContext(
+                    "Child's prevPageNumber is not the previous child for " +
+                    childMain + " " + dpExtra._entryView + " " +
+                    prevPageNumber));
+          }
+          if((nextPageNumber != null) &&
+             (childMain._pageNumber != nextPageNumber)) {
+            throw new IllegalStateException(withErrorContext(
+                    "Child's pageNumber is not the expected next child for " +
+                    childMain));
+          }
+          if(childMain._parentPageNumber != null) {
+            if(childMain._parentPageNumber != dpMain._pageNumber) {
+              throw new IllegalStateException(
+                  withErrorContext("Child's parent is incorrect " + childMain));
+            }
+            boolean expectTail = (subPageNumber == childTailPageNumber);
+            if(expectTail != childMain._tail) {
+              throw new IllegalStateException(withErrorContext(
+                      "Child tail status incorrect " + childMain));
+            }
+          }
+          Entry lastEntry = childMain.getExtra()._entryView.getLast();
+          if(e.compareTo(lastEntry) != 0) {
+            throw new IllegalStateException(withErrorContext(
+                    "Invalid entry " + e + " but child is " + lastEntry));
+          }
+          nextPageNumber = childMain._nextPageNumber;
+          prevPageNumber = childMain._pageNumber;
+        } else {
+          // if we aren't force loading, we may have gaps in the children so we
+          // can't validate these for the current child
+          nextPageNumber = null;
+          prevPageNumber = null;
+        }
+      }
+    }
+
+    /**
+     * Validates the peer pages for an index page.
+     *
+     * @param dpMain the index page
+     */
+    private void validatePeers(DataPageMain dpMain)
+      throws IOException {
+
+      DataPageMain prevMain = getPageForValidate(dpMain._prevPageNumber);
+      if(prevMain != null) {
+        if(prevMain._nextPageNumber != dpMain._pageNumber) {
+          throw new IllegalStateException(withErrorContext(
+                  "Prev page " + prevMain + " does not ref " + dpMain));
+        }
+        validatePeerStatus(dpMain, prevMain);
+      }
+
+      DataPageMain nextMain =
+        getPageForValidate(dpMain._nextPageNumber);
+      if(nextMain != null) {
+        if(nextMain._prevPageNumber != dpMain._pageNumber) {
+          throw new IllegalStateException(withErrorContext(
+                  "Next page " + nextMain + " does not ref " + dpMain));
+        }
+        validatePeerStatus(dpMain, nextMain);
+      }
+    }
+
+    /**
+     * Validates the given peer page against the given index page
+     *
+     * @param dpMain the index page
+     * @param peerMain the peer index page
+     */
+    private void validatePeerStatus(DataPageMain dpMain, DataPageMain peerMain)
+    {
+      if(dpMain._leaf != peerMain._leaf) {
+        throw new IllegalStateException(withErrorContext(
+                "Mismatched peer status " + dpMain._leaf + " " +
+                peerMain._leaf));
+      }
+      if(!dpMain._leaf) {
+        if((dpMain._parentPageNumber != null) &&
+           (peerMain._parentPageNumber != null) &&
+           ((int)dpMain._parentPageNumber != (int)peerMain._parentPageNumber)) {
+          throw new IllegalStateException(withErrorContext(
+                  "Mismatched node parents " + dpMain._parentPageNumber + " " +
+                  peerMain._parentPageNumber));
+        }
+      }
+    }
+
+    private DataPageMain getPageForValidate(
+        Integer pageNumber) throws IOException {
+      DataPageMain dpMain = _knownPages.get(pageNumber);
+      if((dpMain == null) && _forceLoad &&
+         (pageNumber != INVALID_INDEX_PAGE_NUMBER)) {
+        dpMain = getDataPage(pageNumber);
+        if(dpMain != null) {
+          _knownPages.put(pageNumber, dpMain);
+          _pendingPages.add(dpMain);
+        }
+      }
+      return dpMain;
+    }
+  }
+
 }
index e36c569ad495292ce68a020f52ed25a8b6d0e3c0..253351d575d637b441df08edf4418aae5be1de57 100644 (file)
@@ -268,7 +268,7 @@ public abstract class CustomLinkResolver implements LinkResolver
       throws IOException
     {
       super(file, channel, true, false, fileFormat, null, null, null,
-            readOnly);
+            readOnly, false);
       _resolver = resolver;
       _customFile = customFile;
     }
index 004b3b28a8611efc29c96058049d341004c3ed84..9f2e6cd52d4cb32293ab453410bf3ff2841c082b 100644 (file)
@@ -35,7 +35,7 @@ public class BigIndexTest extends TestCase {
   public BigIndexTest(String name) {
     super(name);
   }
-  
+
   public void testComplexIndex() throws Exception
   {
     for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMP_INDEX, true)) {
@@ -99,12 +99,12 @@ public class BigIndexTest extends TestCase {
           }
         }
 
-        index.getIndexData().validate();
+        index.getIndexData().validate(false);
 
         db.flush();
         t = null;
         System.gc();
-        
+
         t = (TableImpl)db.getTable("Table1");
         index = t.getIndex("col1");
 
@@ -130,7 +130,7 @@ public class BigIndexTest extends TestCase {
 
         assertEquals(2000, rowCount);
 
-        index.getIndexData().validate();
+        index.getIndexData().validate(false);
 
         // delete an entry in the middle
         Cursor cursor = CursorBuilder.createCursor(index);
@@ -147,7 +147,7 @@ public class BigIndexTest extends TestCase {
           cursor.deleteCurrentRow();
         }
 
-        index.getIndexData().validate();
+        index.getIndexData().validate(false);
 
         List<String> found = new ArrayList<String>();
         for(Row row : CursorBuilder.createCursor(index)) {
@@ -166,7 +166,7 @@ public class BigIndexTest extends TestCase {
         assertFalse(cursor.moveToNextRow());
         assertFalse(cursor.moveToPreviousRow());
 
-        index.getIndexData().validate();
+        index.getIndexData().validate(false);
 
         // add 50 (pseudo) random entries to the table
         rand = new Random(42L);
@@ -179,14 +179,14 @@ public class BigIndexTest extends TestCase {
           t.addRow(nextVal, "this is some row data " + nextInt);
         }
 
-        index.getIndexData().validate();
+        index.getIndexData().validate(false);
 
         cursor = CursorBuilder.createCursor(index);
         while(cursor.moveToNextRow()) {
           cursor.deleteCurrentRow();
         }
 
-        index.getIndexData().validate();
+        index.getIndexData().validate(false);
 
         db.close();
 
index 3b905459d06c1198588467e21fd35cb818be993c..06a05b87a8e784dff2d82b23b2685f9bbda75303 100644 (file)
@@ -989,6 +989,31 @@ public class DatabaseTest extends TestCase
       assertEquals(expectedUpdateDate, table.getUpdatedDate().toString());
     }
   }
+
+  public void testBrokenIndex() throws Exception {
+    TestDB testDb = TestDB.getSupportedForBasename(Basename.TEST).get(0);
+    try (Database db = new DatabaseBuilder(testDb.getFile())
+         .setReadOnly(true).setIgnoreBrokenSystemCatalogIndex(true).open()) {
+      Table test = db.getTable("Table1");
+      assertNotNull(test);
+      verifyFinderType(db, "FallbackTableFinder");
+    }
+    try (Database db = openMem(testDb)) {
+      Table test = db.getTable("Table1");
+      assertNotNull(test);
+      verifyFinderType(db, "DefaultTableFinder");
+    }
+  }
+
+  private static void verifyFinderType(Database db, String clazzName)
+    throws Exception{
+    java.lang.reflect.Field f = db.getClass().getDeclaredField("_tableFinder");
+    f.setAccessible(true);
+    Object finder = f.get(db);
+    assertNotNull(finder);
+    assertEquals(clazzName, finder.getClass().getSimpleName());
+  }
+
   private static void checkRawValue(String expected, Object val)
   {
     if(expected != null) {
index a312ae8539a9d52595add3c7ab7ad485a464f3a9..0ddf171ee12127e8362b2ac3e28877aab14da00a 100644 (file)
@@ -342,7 +342,7 @@ public class IndexTest extends TestCase {
       if(expectedSuccess) {
         assertNull(failure);
       } else {
-        assertTrue(failure != null);
+        assertNotNull(failure);
         assertTrue(failure.getMessage().contains("uniqueness"));
       }
     }
index 7d1d6b341c6ae61d29602567f891d271eb56ced4..97139b871f51e1327c6e9257e7c16dd3c142a79e 100644 (file)
@@ -200,10 +200,14 @@ public class TestUtil
     final Database db = new DatabaseBuilder(file).setReadOnly(readOnly)
       .setAutoSync(getTestAutoSync()).setChannel(channel)
       .setCharset(charset).open();
-    Assert.assertEquals("Wrong JetFormat.",
-                        DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(),
-                        ((DatabaseImpl)db).getFormat());
-    Assert.assertEquals("Wrong FileFormat.", fileFormat, db.getFileFormat());
+    if(fileFormat != null) {
+      Assert.assertEquals(
+          "Wrong JetFormat.",
+          DatabaseImpl.getFileFormatDetails(fileFormat).getFormat(),
+          ((DatabaseImpl)db).getFormat());
+      Assert.assertEquals(
+          "Wrong FileFormat.", fileFormat, db.getFileFormat());
+    }
     return db;
   }