]> source.dussan.org Git - jackcess.git/commitdiff
add support for creating indexes (except foreign key indexes) on a table when a table...
authorJames Ahlborn <jtahlborn@yahoo.com>
Fri, 4 Mar 2011 12:58:55 +0000 (12:58 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Fri, 4 Mar 2011 12:58:55 +0000 (12:58 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@515 f203690c-595d-4dc9-a70b-905162fa7fd2

src/changes/changes.xml
src/java/com/healthmarketscience/jackcess/Column.java
src/java/com/healthmarketscience/jackcess/Database.java
src/java/com/healthmarketscience/jackcess/Index.java
src/java/com/healthmarketscience/jackcess/IndexBuilder.java [new file with mode: 0644]
src/java/com/healthmarketscience/jackcess/IndexData.java
src/java/com/healthmarketscience/jackcess/JetFormat.java
src/java/com/healthmarketscience/jackcess/SimpleIndexData.java
src/java/com/healthmarketscience/jackcess/Table.java
src/java/com/healthmarketscience/jackcess/TableBuilder.java
test/src/java/com/healthmarketscience/jackcess/IndexTest.java

index e4f2b9cda43a441d0a85faf28beba3942aa432af..e65bfde3ab7a284a13606fdb07a9d09b6414b38a 100644 (file)
       <action dev="jahlborn" type="fix" issue="3192058">
         More fixes related to reading and interpreting index information.
         Handle multiple logical indexes backed by the same index data.
+        Interpret foreign key constraint information.
       </action>
       <action dev="jahlborn" type="update">
         Allow MSISAM files to be written (experimental).
       </action>
+      <action dev="jahlborn" type="add">
+        Add support for creating indexes when creating a new table.  Normal
+        indexes and primary key indexes are currently supported.  Foreign key
+        indexes are not yet supported.
+      </action>
     </release>
     <release version="1.2.2" date="2010-11-29">
       <action dev="jahlborn" type="update">
index 4c164adbbd1d3b7f21ab374754d61ffdb5846c5f..9f963be0bdc56d1ea41d85c6757378b5f814773d 100644 (file)
@@ -1022,7 +1022,7 @@ public class Column implements Comparable<Column> {
   {
     lvalPage.put(PageTypes.DATA); //Page type
     lvalPage.put((byte) 1); //Unknown
-    lvalPage.putShort((short)getFormat().PAGE_INITIAL_FREE_SPACE); //Free space
+    lvalPage.putShort((short)getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space
     lvalPage.put((byte) 'L');
     lvalPage.put((byte) 'V');
     lvalPage.put((byte) 'A');
@@ -1594,6 +1594,94 @@ public class Column implements Comparable<Column> {
     return(value instanceof RawData);
   }
 
+  /**
+   * Writes the column definitions into a table definition buffer.
+   * @param buffer Buffer to write to
+   * @param columns List of Columns to write definitions for
+   */
+  protected static void writeDefinitions(
+      ByteBuffer buffer, List<Column> columns, JetFormat format,
+      Charset charset)
+    throws IOException
+  {
+    short columnNumber = (short) 0;
+    short fixedOffset = (short) 0;
+    short variableOffset = (short) 0;
+    // we specifically put the "long variable" values after the normal
+    // variable length values so that we have a better chance of fitting it
+    // all (because "long variable" values can go in separate pages)
+    short longVariableOffset =
+      Column.countNonLongVariableLength(columns);
+    for (Column col : columns) {
+      // record this for later use when writing indexes
+      col.setColumnNumber(columnNumber);
+
+      int position = buffer.position();
+      buffer.put(col.getType().getValue());
+      buffer.putInt(Table.MAGIC_TABLE_NUMBER);  //constant magic number
+      buffer.putShort(columnNumber);  //Column Number
+      if (col.isVariableLength()) {
+        if(!col.getType().isLongValue()) {
+          buffer.putShort(variableOffset++);
+        } else {
+          buffer.putShort(longVariableOffset++);
+        }          
+      } else {
+        buffer.putShort((short) 0);
+      }
+      buffer.putShort(columnNumber); //Column Number again
+      if(col.getType().getHasScalePrecision()) {
+        buffer.put(col.getPrecision());  // numeric precision
+        buffer.put(col.getScale());  // numeric scale
+      } else {
+        buffer.put((byte) 0x00); //unused
+        buffer.put((byte) 0x00); //unused
+      }
+      buffer.putShort((short) 0); //Unknown
+      buffer.put(getColumnBitFlags(col)); // misc col flags
+      if (col.isCompressedUnicode()) {  //Compressed
+        buffer.put((byte) 1);
+      } else {
+        buffer.put((byte) 0);
+      }
+      buffer.putInt(0); //Unknown, but always 0.
+      //Offset for fixed length columns
+      if (col.isVariableLength()) {
+        buffer.putShort((short) 0);
+      } else {
+        buffer.putShort(fixedOffset);
+        fixedOffset += col.getType().getFixedSize(col.getLength());
+      }
+      if(!col.getType().isLongValue()) {
+        buffer.putShort(col.getLength()); //Column length
+      } else {
+        buffer.putShort((short)0x0000); // unused
+      }
+      columnNumber++;
+      if (LOG.isDebugEnabled()) {
+        LOG.debug("Creating new column def block\n" + ByteUtil.toHexString(
+            buffer, position, format.SIZE_COLUMN_DEF_BLOCK));
+      }
+    }
+    for (Column col : columns) {
+      Table.writeName(buffer, col.getName(), charset);
+    }
+  }
+
+  /**
+   * Constructs a byte containing the flags for the given column.
+   */
+  private static byte getColumnBitFlags(Column col) {
+    byte flags = Column.UNKNOWN_FLAG_MASK;
+    if(!col.isVariableLength()) {
+      flags |= Column.FIXED_LEN_FLAG_MASK;
+    }
+    if(col.isAutoNumber()) {
+      flags |= col.getAutoNumberGenerator().getColumnFlags();
+    }
+    return flags;
+  }
+
   /**
    * Date subclass which stashes the original date bits, in case we attempt to
    * re-write the value (will not lose precision).
index b8dcc9482c2717acc831df7ff20d4496a96dfa63..cc14cf5d6bc45986f36bb69575ea323b9299031b 100644 (file)
@@ -45,6 +45,7 @@ import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.ConcurrentModificationException;
 import java.util.Date;
 import java.util.EnumSet;
@@ -941,6 +942,19 @@ public class Database
    */
   public void createTable(String name, List<Column> columns)
     throws IOException
+  {
+    createTable(name, columns, null);
+  }
+
+  /**
+   * Create a new table in this database
+   * @param name Name of the table to create
+   * @param columns List of Columns in the table
+   * @param indexes List of IndexBuilders describing indexes for the table
+   */
+  public void createTable(String name, List<Column> columns,
+                          List<IndexBuilder> indexes)
+    throws IOException
   {
     validateIdentifierName(name, _format.MAX_TABLE_NAME_LENGTH, "table");
     
@@ -980,10 +994,26 @@ public class Database
         }
       }
     }
+
+    if(indexes == null) {
+      indexes = Collections.emptyList();
+    }
+    if(!indexes.isEmpty()) {
+      // now, validate the indexes
+      Set<String> idxNames = new HashSet<String>();
+      for(IndexBuilder index : indexes) {
+        index.validate(colNames);
+        if(!idxNames.add(index.getName().toUpperCase())) {
+          throw new IllegalArgumentException("duplicate index name: " +
+                                             index.getName());
+        }
+      }
+    }
     
     //Write the tdef page to disk.
-    int tdefPageNumber = Table.writeTableDefinition(columns, _pageChannel,
-                                                    _format, getCharset());
+    int tdefPageNumber = Table.writeTableDefinition(columns, indexes,
+                                                    _pageChannel, _format,
+                                                    getCharset());
     
     //Add this table to our internal list.
     addTable(name, Integer.valueOf(tdefPageNumber));
index f5cb87444941eb0e5b32ced9a8136c2d8180189f..9ba24ccf174b0e152d6e795d4c4419dd9cc8636e 100644 (file)
@@ -28,6 +28,8 @@ King of Prussia, PA 19406
 package com.healthmarketscience.jackcess;
 
 import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -47,10 +49,21 @@ public class Index implements Comparable<Index> {
   protected static final Log LOG = LogFactory.getLog(Index.class);
     
   /** index type for primary key indexes */
-  private static final byte PRIMARY_KEY_INDEX_TYPE = (byte)1;
+  static final byte PRIMARY_KEY_INDEX_TYPE = (byte)1;
   
   /** index type for foreign key indexes */
-  private static final byte FOREIGN_KEY_INDEX_TYPE = (byte)2;
+  static final byte FOREIGN_KEY_INDEX_TYPE = (byte)2;
+
+  /** flag for indicating that updates should cascade in a foreign key index */
+  private static final byte CASCADE_UPDATES_FLAG = (byte)1;
+  /** flag for indicating that deletes should cascade in a foreign key index */
+  private static final byte CASCADE_DELETES_FLAG = (byte)1;
+
+  /** index table type for the "primary" table in a foreign key index */
+  private static final byte PRIMARY_TABLE_TYPE = (byte)1;
+
+  /** indicate an invalid index number for foreign key field */
+  private static final int INVALID_INDEX_NUMBER = -1;
 
   /** the actual data backing this index (more than one index may be backed by
       the same data */
@@ -61,12 +74,42 @@ public class Index implements Comparable<Index> {
   private final byte _indexType;
   /** Index name */
   private String _name;
+  /** foreign key reference info, if any */
+  private final ForeignKeyReference _reference;
   
-  protected Index(IndexData data, int indexNumber, byte indexType) {
-    _data = data;
-    _indexNumber = indexNumber;
-    _indexType = indexType;
-    data.addIndex(this);
+  protected Index(ByteBuffer tableBuffer, List<IndexData> indexDatas,
+                  JetFormat format) 
+    throws IOException
+  {
+
+    ByteUtil.forward(tableBuffer, format.SKIP_BEFORE_INDEX_SLOT); //Forward past Unknown
+    _indexNumber = tableBuffer.getInt();
+    int indexDataNumber = tableBuffer.getInt();
+      
+    // read foreign key reference info
+    byte relIndexType = tableBuffer.get();
+    int relIndexNumber = tableBuffer.getInt();
+    int relTablePageNumber = tableBuffer.getInt();
+    byte cascadeUpdatesFlag = tableBuffer.get();
+    byte cascadeDeletesFlag = tableBuffer.get();
+
+    _indexType = tableBuffer.get();
+    if((_indexType == FOREIGN_KEY_INDEX_TYPE) && 
+       (relIndexNumber != INVALID_INDEX_NUMBER)) {
+      _reference = new ForeignKeyReference(
+          relIndexType, relIndexNumber, relTablePageNumber,
+          (cascadeUpdatesFlag == CASCADE_UPDATES_FLAG),
+          (cascadeDeletesFlag == CASCADE_DELETES_FLAG));
+    } else {
+      _reference = null;
+    }
+
+    ByteUtil.forward(tableBuffer, format.SKIP_AFTER_INDEX_SLOT); //Skip past Unknown
+
+    _data = indexDatas.get(indexDataNumber);
+
+    _data.addIndex(this);
   }
 
   public IndexData getIndexData() {
@@ -117,6 +160,10 @@ public class Index implements Comparable<Index> {
     return _indexType == FOREIGN_KEY_INDEX_TYPE;
   }
 
+  public ForeignKeyReference getReference() {
+    return _reference;
+  }
+
   /**
    * Whether or not {@code null} values are actually recorded in the index.
    */
@@ -271,10 +318,14 @@ public class Index implements Comparable<Index> {
   @Override
   public String toString() {
     StringBuilder rtn = new StringBuilder();
-    rtn.append("\tName: (" + getTable().getName() + ") " + _name);
-    rtn.append("\n\tNumber: " + _indexNumber);
-    rtn.append("\n\tIs Primary Key: " + isPrimaryKey());
-    rtn.append("\n\tIs Foreign Key: " + isForeignKey());
+    rtn.append("\tName: (").append(getTable().getName()).append(") ")
+      .append(_name);
+    rtn.append("\n\tNumber: ").append(_indexNumber);
+    rtn.append("\n\tIs Primary Key: ").append(isPrimaryKey());
+    rtn.append("\n\tIs Foreign Key: ").append(isForeignKey());
+    if(_reference != null) {
+      rtn.append("\n\tForeignKeyReference: ").append(_reference);
+    }
     rtn.append(_data.toString());
     rtn.append("\n\n");
     return rtn.toString();
@@ -290,4 +341,91 @@ public class Index implements Comparable<Index> {
     }
   }
 
+  /**
+   * Writes the logical index definitions into a table definition buffer.
+   * @param buffer Buffer to write to
+   * @param indexes List of IndexBuilders to write definitions for
+   */
+  protected static void writeDefinitions(
+      ByteBuffer buffer, List<IndexBuilder> indexes, Charset charset)
+    throws IOException
+  {
+    // write logical index information
+    for(IndexBuilder idx : indexes) {
+      buffer.putInt(Table.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def
+      buffer.putInt(idx.getIndexNumber()); // index num
+      buffer.putInt(idx.getIndexDataNumber()); // index data num
+      buffer.put((byte)0); // related table type
+      buffer.putInt(INVALID_INDEX_NUMBER); // related index num
+      buffer.putInt(0); // related table definition page number
+      buffer.put((byte)0); // cascade updates flag
+      buffer.put((byte)0); // cascade deletes flag
+      buffer.put(idx.getFlags()); // index flags
+      buffer.putInt(0); // unknown
+    }
+
+    // write index names
+    for(IndexBuilder idx : indexes) {
+      Table.writeName(buffer, idx.getName(), charset);
+    }
+  }
+
+  /**
+   * Information about a foreign key reference defined in an index (when
+   * referential integrity should be enforced).
+   */
+  public static class ForeignKeyReference
+  {
+    private final byte _tableType;
+    private final int _otherIndexNumber;
+    private final int _otherTablePageNumber;
+    private final boolean _cascadeUpdates;
+    private final boolean _cascadeDeletes;
+    
+    public ForeignKeyReference(
+        byte tableType, int otherIndexNumber, int otherTablePageNumber,
+        boolean cascadeUpdates, boolean cascadeDeletes)
+    {
+      _tableType = tableType;
+      _otherIndexNumber = otherIndexNumber;
+      _otherTablePageNumber = otherTablePageNumber;
+      _cascadeUpdates = cascadeUpdates;
+      _cascadeDeletes = cascadeDeletes;
+    }
+
+    public byte getTableType() {
+      return _tableType;
+    }
+
+    public boolean isPrimaryTable() {
+      return(getTableType() == PRIMARY_TABLE_TYPE);
+    }
+
+    public int getOtherIndexNumber() {
+      return _otherIndexNumber;
+    }
+
+    public int getOtherTablePageNumber() {
+      return _otherTablePageNumber;
+    }
+
+    public boolean isCascadeUpdates() {
+      return _cascadeUpdates;
+    }
+
+    public boolean isCascadeDeletes() {
+      return _cascadeDeletes;
+    }
+
+    @Override
+    public String toString() {
+      return new StringBuilder()
+        .append("\n\t\tOther Index Number: ").append(_otherIndexNumber)
+        .append("\n\t\tOther Table Page Num: ").append(_otherTablePageNumber)
+        .append("\n\t\tIs Primary Table: ").append(isPrimaryTable())
+        .append("\n\t\tIs Cascade Updates: ").append(isCascadeUpdates())
+        .append("\n\t\tIs Cascade Deletes: ").append(isCascadeDeletes())
+        .toString();
+    }
+  }
 }
diff --git a/src/java/com/healthmarketscience/jackcess/IndexBuilder.java b/src/java/com/healthmarketscience/jackcess/IndexBuilder.java
new file mode 100644 (file)
index 0000000..4799abf
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+Copyright (c) 2011 James Ahlborn
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+USA
+*/
+
+package com.healthmarketscience.jackcess;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Builder style class for constructing an Index.
+ *
+ * @author James Ahlborn
+ */
+public class IndexBuilder 
+{
+  /** name typically used by MS Access for the primary key index */
+  public static final String PRIMARY_KEY_NAME = "PrimaryKey";
+
+  /** name of the new index */
+  private String _name;
+  /** the type of the index */
+  private byte _type;
+  /** additional index flags */
+  private byte _flags;
+  /** the names and orderings of the indexed columns */
+  private final List<Column> _columns = new ArrayList<Column>();
+
+  // used by table definition writing code
+  private int _indexNumber;
+  private int _indexDataNumber;
+  private byte _umapRowNumber;
+  private int _umapPageNumber;
+  private int _rootPageNumber;
+
+  public IndexBuilder(String name) {
+    _name = name;
+  }
+
+  public String getName() {
+    return _name;
+  }
+
+  public byte getType() {
+    return _type;
+  }
+
+  public byte getFlags() {
+    return _flags;
+  }
+
+  public boolean isPrimaryKey() {
+    return (getType() == Index.PRIMARY_KEY_INDEX_TYPE);
+  }
+
+  public boolean isUnique() {
+      return ((getFlags() & IndexData.UNIQUE_INDEX_FLAG) != 0);
+  }
+
+  public boolean isIgnoreNulls() {
+      return ((getFlags() & IndexData.IGNORE_NULLS_INDEX_FLAG) != 0);
+  }
+
+  public List<Column> getColumns() {
+    return _columns;
+  }
+
+  /**
+   * Sets the name of the index.
+   */
+  public IndexBuilder setName(String name) {
+    _name = name;
+    return this;
+  }
+
+  /**
+   * Adds the columns with ASCENDING ordering to the index.
+   */
+  public IndexBuilder addColumns(String... names) {
+    return addColumns(true, names);
+  }
+
+  /**
+   * Adds the columns with the given ordering to the index.
+   */
+  public IndexBuilder addColumns(boolean ascending, String... names) {
+    if(names != null) {
+      for(String name : names) {
+        _columns.add(new Column(name, ascending));
+      }
+    }
+    return this;
+  }
+
+  /**
+   * Sets this index to be a primary key index (additionally sets the index as
+   * unique).
+   */
+  public IndexBuilder setPrimaryKey() {
+    _type = Index.PRIMARY_KEY_INDEX_TYPE;
+    return setUnique();
+  }
+
+  /**
+   * Sets this index to enforce uniqueness.
+   */
+  public IndexBuilder setUnique() {
+    _flags |= IndexData.UNIQUE_INDEX_FLAG;
+    return this;
+  }    
+
+  /**
+   * Sets this index to ignore null values.
+   */
+  public IndexBuilder setIgnoreNulls() {
+    _flags |= IndexData.IGNORE_NULLS_INDEX_FLAG;
+    return this;
+  }    
+
+  public void validate(Set<String> tableColNames) {
+    if(getColumns().isEmpty()) {
+      throw new IllegalArgumentException("index " + getName() +
+                                         " has no columns");
+    }
+    if(getColumns().size() > IndexData.MAX_COLUMNS) {
+      throw new IllegalArgumentException("index " + getName() +
+                                         " has too many columns, max " +
+                                         IndexData.MAX_COLUMNS);
+    }
+
+    Set<String> idxColNames = new HashSet<String>();
+    for(Column col : getColumns()) {
+      String idxColName = col.getName().toUpperCase();
+      if(!idxColNames.add(idxColName)) {
+        throw new IllegalArgumentException("duplicate column name " +
+                                           col.getName() + " in index " +
+                                           getName());
+      }
+      if(!tableColNames.contains(idxColName)) {
+        throw new IllegalArgumentException("column named " + col.getName() +
+                                           " not found in table");
+      }
+    }
+  }
+
+  protected int getIndexNumber() {
+    return _indexNumber;
+  }
+
+  protected void setIndexNumber(int newIndexNumber) {
+    _indexNumber = newIndexNumber;
+  }
+
+  protected int getIndexDataNumber() {
+    return _indexDataNumber;
+  }
+
+  protected void setIndexDataNumber(int newIndexDataNumber) {
+    _indexDataNumber = newIndexDataNumber;
+  }
+
+  protected byte getUmapRowNumber() {
+    return _umapRowNumber;
+  }
+
+  protected void setUmapRowNumber(byte newUmapRowNumber) {
+    _umapRowNumber = newUmapRowNumber;
+  }
+
+  protected int getUmapPageNumber() {
+    return _umapPageNumber;
+  }
+
+  protected void setUmapPageNumber(int newUmapPageNumber) {
+    _umapPageNumber = newUmapPageNumber;
+  }
+
+  protected int getRootPageNumber() {
+    return _rootPageNumber;
+  }
+
+  protected void setRootPageNumber(int newRootPageNumber) {
+    _rootPageNumber = newRootPageNumber;
+  }
+
+  /**
+   * Information about a column in this index (name and ordering).
+   */
+  public static class Column
+  {
+    /** name of the column to be indexed */
+    private String _name;
+    /** column flags (ordering) */
+    private byte _flags;
+
+    private Column(String name, boolean ascending) {
+      _name = name;
+      _flags = (ascending ? IndexData.ASCENDING_COLUMN_FLAG : 0);
+    }
+
+    public String getName() {
+      return _name;
+    }
+
+    public Column setName(String name) {
+      _name = name;
+      return this;
+    }
+
+    public boolean isAscending() {
+      return ((getFlags() & IndexData.ASCENDING_COLUMN_FLAG) != 0);
+    }
+
+    public byte getFlags() {
+      return _flags;
+    }
+  }
+
+}
index 995e3088f9a590c3154c7eeeefb042d87257ae5f..fa2f67e3a4515707f3472f24ef270a020816cfbe 100644 (file)
@@ -66,16 +66,18 @@ public abstract class IndexData {
   protected static final int INVALID_INDEX_PAGE_NUMBER = 0;          
   
   /** Max number of columns in an index */
-  private static final int MAX_COLUMNS = 10;
+  static final int MAX_COLUMNS = 10;
   
   protected static final byte[] EMPTY_PREFIX = new byte[0];
 
   private static final short COLUMN_UNUSED = -1;
 
-  private static final byte ASCENDING_COLUMN_FLAG = (byte)0x01;
+  static final byte ASCENDING_COLUMN_FLAG = (byte)0x01;
 
-  private static final byte UNIQUE_INDEX_FLAG = (byte)0x01;
-  private static final byte IGNORE_NULLS_INDEX_FLAG = (byte)0x02;
+  static final byte UNIQUE_INDEX_FLAG = (byte)0x01;
+  static final byte IGNORE_NULLS_INDEX_FLAG = (byte)0x02;
+
+  private static final int MAGIC_INDEX_NUMBER = 1923;
 
   private static final int MAX_TEXT_INDEX_CHAR_LENGTH =
     (JetFormat.TEXT_FIELD_MAX_LENGTH / JetFormat.TEXT_FIELD_UNIT_SIZE);
@@ -178,6 +180,26 @@ public abstract class IndexData {
     _maxPageEntrySize = calcMaxPageEntrySize(_table.getFormat());
   }
 
+  /**
+   * Creates an IndexData appropriate for the given table, using information
+   * from the given table definition buffer.
+   */
+  public static IndexData create(Table table, ByteBuffer tableBuffer,
+                                 int number, JetFormat format)
+    throws IOException
+  {
+    int uniqueEntryCountOffset =
+      (format.OFFSET_INDEX_DEF_BLOCK +
+       (number * format.SIZE_INDEX_DEFINITION) + 4);
+    int uniqueEntryCount = tableBuffer.getInt(uniqueEntryCountOffset);
+
+    return(table.doUseBigIndex() ?
+           new BigIndexData(table, number, uniqueEntryCount,
+                            uniqueEntryCountOffset) :
+           new SimpleIndexData(table, number, uniqueEntryCount, 
+                               uniqueEntryCountOffset));
+  }
+
   public Table getTable() {
     return _table;
   }
@@ -350,13 +372,15 @@ public abstract class IndexData {
   }
 
   /**
-   * Read the index info from a tableBuffer
+   * Read the rest of the index info from a tableBuffer
    * @param tableBuffer table definition buffer to read from initial info
    * @param availableColumns Columns that this index may use
    */
   public void read(ByteBuffer tableBuffer, List<Column> availableColumns)
     throws IOException
   {
+    ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX); //Forward past Unknown
+
     for (int i = 0; i < MAX_COLUMNS; i++) {
       short columnNumber = tableBuffer.getShort();
       byte colFlags = tableBuffer.get();
@@ -382,13 +406,87 @@ public abstract class IndexData {
     int umapPageNum = ByteUtil.get3ByteInt(tableBuffer);
     _ownedPages = UsageMap.read(getTable().getDatabase(), umapPageNum,
                                 umapRowNum, false);
-
+    
     _rootPageNumber = tableBuffer.getInt();
+
     ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX_FLAGS); //Forward past Unknown
     _indexFlags = tableBuffer.get();
     ByteUtil.forward(tableBuffer, getFormat().SKIP_AFTER_INDEX_FLAGS); //Forward past other stuff
   }
 
+  /**
+   * Writes the index row count definitions into a table definition buffer.
+   * @param buffer Buffer to write to
+   * @param indexes List of IndexBuilders to write definitions for
+   */
+  protected static void writeRowCountDefinitions(
+      ByteBuffer buffer, int indexCount, JetFormat format)
+  {
+    // index row counts (empty data)
+    ByteUtil.forward(buffer, (indexCount * format.SIZE_INDEX_DEFINITION));
+  }
+
+  /**
+   * Writes the index definitions into a table definition buffer.
+   * @param buffer Buffer to write to
+   * @param indexes List of IndexBuilders to write definitions for
+   */
+  protected static void writeDefinitions(
+      ByteBuffer buffer, List<Column> columns, List<IndexBuilder> indexes,
+      int tdefPageNumber, PageChannel pageChannel, JetFormat format)
+    throws IOException
+  {
+    ByteBuffer rootPageBuffer = pageChannel.createPageBuffer();
+    writeDataPage(rootPageBuffer, SimpleIndexData.NEW_ROOT_DATA_PAGE, 
+                  tdefPageNumber, format);
+
+    for(IndexBuilder idx : indexes) {
+      buffer.putInt(MAGIC_INDEX_NUMBER); // seemingly constant magic value
+
+      // write column information (always MAX_COLUMNS entries)
+      List<IndexBuilder.Column> idxColumns = idx.getColumns();
+      for(int i = 0; i < MAX_COLUMNS; ++i) {
+
+        short columnNumber = COLUMN_UNUSED;
+        byte flags = 0;
+
+        if(i < idxColumns.size()) {
+
+          // determine column info
+          IndexBuilder.Column idxCol = idxColumns.get(i);
+          flags = idxCol.getFlags();
+
+          // find actual table column number
+          for(Column col : columns) {
+            if(col.getName().equalsIgnoreCase(idxCol.getName())) {
+              columnNumber = col.getColumnNumber();
+              break;
+            }
+          }
+          if(columnNumber == COLUMN_UNUSED) {
+            // should never happen as this is validated before
+            throw new IllegalArgumentException(
+                "Column with name " + idxCol.getName() + " not found");
+          }
+        }
+         
+        buffer.putShort(columnNumber); // table column number
+        buffer.put(flags); // column flags (e.g. ordering)
+      }
+
+      buffer.put(idx.getUmapRowNumber()); // umap row
+      ByteUtil.put3ByteInt(buffer, idx.getUmapPageNumber()); // umap page
+
+      // write empty root index page
+      pageChannel.writePage(rootPageBuffer, idx.getRootPageNumber());
+
+      buffer.putInt(idx.getRootPageNumber());
+      buffer.putInt(0); // unknown
+      buffer.put(idx.getFlags()); // index flags (unique, etc.)
+      ByteUtil.forward(buffer, 5); // unknown
+    }
+  }
+
   /**
    * Adds a row to this index
    * <p>
@@ -723,16 +821,16 @@ public abstract class IndexData {
   @Override
   public String toString() {
     StringBuilder rtn = new StringBuilder();
-    rtn.append("\n\tData number: " + _number);
-    rtn.append("\n\tPage number: " + _rootPageNumber);
-    rtn.append("\n\tIs Backing Primary Key: " + isBackingPrimaryKey());
-    rtn.append("\n\tIs Unique: " + isUnique());
-    rtn.append("\n\tIgnore Nulls: " + shouldIgnoreNulls());
-    rtn.append("\n\tColumns: " + _columns);
-    rtn.append("\n\tInitialized: " + _initialized);
+    rtn.append("\n\tData number: ").append(_number);
+    rtn.append("\n\tPage number: ").append(_rootPageNumber);
+    rtn.append("\n\tIs Backing Primary Key: ").append(isBackingPrimaryKey());
+    rtn.append("\n\tIs Unique: ").append(isUnique());
+    rtn.append("\n\tIgnore Nulls: ").append(shouldIgnoreNulls());
+    rtn.append("\n\tColumns: ").append(_columns);
+    rtn.append("\n\tInitialized: ").append(_initialized);
     if(_initialized) {
       try {
-        rtn.append("\n\tEntryCount: " + getEntryCount());
+        rtn.append("\n\tEntryCount: ").append(getEntryCount());
       } catch(IOException e) {
         throw new RuntimeException(e);
       }
@@ -755,12 +853,26 @@ public abstract class IndexData {
     }
     
     ByteBuffer buffer = _indexBufferH.getPageBuffer(getPageChannel());
+
+    writeDataPage(buffer, dataPage, getTable().getTableDefPageNumber(),
+                  getFormat());
+
+    getPageChannel().writePage(buffer, dataPage.getPageNumber());
+  }
+
+  /**
+   * Writes the data page info to the given buffer.
+   */
+  protected static void writeDataPage(ByteBuffer buffer, DataPage dataPage,
+                                      int tdefPageNumber, JetFormat format)
+    throws IOException
+  {
     buffer.put(dataPage.isLeaf() ?
                PageTypes.INDEX_LEAF :
                PageTypes.INDEX_NODE );  //Page type
     buffer.put((byte) 0x01);  //Unknown
     buffer.putShort((short) 0); //Free space
-    buffer.putInt(getTable().getTableDefPageNumber());
+    buffer.putInt(tdefPageNumber);
 
     buffer.putInt(0); //Unknown
     buffer.putInt(dataPage.getPrevPageNumber()); //Prev page
@@ -771,7 +883,7 @@ public abstract class IndexData {
     buffer.putShort((short) entryPrefix.length); // entry prefix byte count
     buffer.put((byte) 0); //Unknown
 
-    byte[] entryMask = new byte[getFormat().SIZE_INDEX_ENTRY_MASK];
+    byte[] entryMask = new byte[format.SIZE_INDEX_ENTRY_MASK];
     // first entry includes the prefix
     int totalSize = entryPrefix.length;
     for(Entry entry : dataPage.getEntries()) {
@@ -789,9 +901,7 @@ public abstract class IndexData {
     }
 
     // update free space
-    buffer.putShort(2, (short) (getFormat().PAGE_SIZE - buffer.position()));
-
-    getPageChannel().writePage(buffer, dataPage.getPageNumber());
+    buffer.putShort(2, (short) (format.PAGE_SIZE - buffer.position()));
   }
 
   /**
index 723ac5e38dfd8d48077844ec69b1bf0997522f4a..d512659be67db7ad65add6ef5b181dd1909d7c96 100644 (file)
@@ -158,7 +158,7 @@ public abstract class JetFormat {
   public final long MAX_DATABASE_SIZE;
   
   public final int MAX_ROW_SIZE;
-  public final int PAGE_INITIAL_FREE_SPACE;
+  public final int DATA_PAGE_INITIAL_FREE_SPACE;
 
   public final int OFFSET_MASKED_HEADER;
   public final byte[] HEADER_MASK;
@@ -179,7 +179,8 @@ public abstract class JetFormat {
   public final int OFFSET_FREE_SPACE_PAGES;
   public final int OFFSET_INDEX_DEF_BLOCK;
   
-  public final int OFFSET_INDEX_NUMBER_BLOCK;
+  public final int SIZE_INDEX_COLUMN_BLOCK;
+  public final int SIZE_INDEX_INFO_BLOCK;
   
   public final int OFFSET_COLUMN_TYPE;
   public final int OFFSET_COLUMN_NUMBER;
@@ -280,7 +281,7 @@ public abstract class JetFormat {
     MAX_DATABASE_SIZE = defineMaxDatabaseSize();
     
     MAX_ROW_SIZE = defineMaxRowSize();
-    PAGE_INITIAL_FREE_SPACE = definePageInitialFreeSpace();
+    DATA_PAGE_INITIAL_FREE_SPACE = defineDataPageInitialFreeSpace();
     
     OFFSET_MASKED_HEADER = defineOffsetMaskedHeader();
     HEADER_MASK = defineHeaderMask();
@@ -301,7 +302,8 @@ public abstract class JetFormat {
     OFFSET_FREE_SPACE_PAGES = defineOffsetFreeSpacePages();
     OFFSET_INDEX_DEF_BLOCK = defineOffsetIndexDefBlock();
     
-    OFFSET_INDEX_NUMBER_BLOCK = defineOffsetIndexNumberBlock();
+    SIZE_INDEX_COLUMN_BLOCK = defineSizeIndexColumnBlock();
+    SIZE_INDEX_INFO_BLOCK = defineSizeIndexInfoBlock();
     
     OFFSET_COLUMN_TYPE = defineOffsetColumnType();
     OFFSET_COLUMN_NUMBER = defineOffsetColumnNumber();
@@ -372,7 +374,7 @@ public abstract class JetFormat {
   protected abstract long defineMaxDatabaseSize();
   
   protected abstract int defineMaxRowSize();
-  protected abstract int definePageInitialFreeSpace();
+  protected abstract int defineDataPageInitialFreeSpace();
   
   protected abstract int defineOffsetMaskedHeader();
   protected abstract byte[] defineHeaderMask();
@@ -393,7 +395,8 @@ public abstract class JetFormat {
   protected abstract int defineOffsetFreeSpacePages();
   protected abstract int defineOffsetIndexDefBlock();
   
-  protected abstract int defineOffsetIndexNumberBlock();
+  protected abstract int defineSizeIndexColumnBlock();
+  protected abstract int defineSizeIndexInfoBlock();
   
   protected abstract int defineOffsetColumnType();
   protected abstract int defineOffsetColumnNumber();
@@ -490,7 +493,7 @@ public abstract class JetFormat {
     @Override
     protected int defineMaxRowSize() { return 2012; }
     @Override
-    protected int definePageInitialFreeSpace() { return PAGE_SIZE - 14; }
+    protected int defineDataPageInitialFreeSpace() { return PAGE_SIZE - 14; }
            
     @Override
     protected int defineOffsetMaskedHeader() { return 24; }
@@ -532,7 +535,9 @@ public abstract class JetFormat {
     protected int defineOffsetIndexDefBlock() { return 43; }
 
     @Override
-    protected int defineOffsetIndexNumberBlock() { return 39; }
+    protected int defineSizeIndexColumnBlock() { return 39; }
+    @Override
+    protected int defineSizeIndexInfoBlock() { return 20; }
            
     @Override
     protected int defineOffsetColumnType() { return 0; }
@@ -685,7 +690,7 @@ public abstract class JetFormat {
     @Override
     protected int defineMaxRowSize() { return 4060; }
     @Override
-    protected int definePageInitialFreeSpace() { return PAGE_SIZE - 14; }
+    protected int defineDataPageInitialFreeSpace() { return PAGE_SIZE - 14; }
     
     @Override
     protected int defineOffsetMaskedHeader() { return 24; }
@@ -725,7 +730,9 @@ public abstract class JetFormat {
     protected int defineOffsetIndexDefBlock() { return 63; }
 
     @Override
-    protected int defineOffsetIndexNumberBlock() { return 52; }
+    protected int defineSizeIndexColumnBlock() { return 52; }
+    @Override
+    protected int defineSizeIndexInfoBlock() { return 28; }
     
     @Override
     protected int defineOffsetColumnType() { return 0; }
index 42d803075869017da9fd592cfb16bbc7e94a822e..7a662e76c99afd1495d8199cf1d4027fc1bdfa8c 100644 (file)
@@ -28,6 +28,7 @@ King of Prussia, PA 19406
 package com.healthmarketscience.jackcess;
 
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
 
 
@@ -35,7 +36,12 @@ import java.util.List;
  * Simple implementation of an Access table index
  * @author Tim McCune
  */
-public class SimpleIndexData extends IndexData {
+public class SimpleIndexData extends IndexData 
+{
+
+  static final DataPage NEW_ROOT_DATA_PAGE = 
+    new SimpleDataPage(0, true, Collections.<Entry>emptyList());
+
 
   /** data for the single index page.  if this data came from multiple pages,
       the index is read-only. */
@@ -131,7 +137,6 @@ public class SimpleIndexData extends IndexData {
   {
     throw new UnsupportedOperationException();
   }
-  
 
   /**
    * Simple implementation of a DataPage
@@ -145,7 +150,14 @@ public class SimpleIndexData extends IndexData {
     private List<Entry> _entries;
 
     private SimpleDataPage(int pageNumber) {
+      this(pageNumber, false, null);
+    }
+
+    private SimpleDataPage(int pageNumber, boolean leaf, List<Entry> entries)
+    {
       _pageNumber = pageNumber;
+      _leaf = leaf;
+      _entries = entries;
     }
     
     @Override
@@ -208,8 +220,7 @@ public class SimpleIndexData extends IndexData {
     }
     
     @Override
-    public void setEntries(List<Entry> entries) {
-      
+    public void setEntries(List<Entry> entries) {      
       _entries = entries;
     }
 
index c25934b350acee8787986fdbab67e955c5fba972..0f3a076ecb9f5b6814178472d13e109588278d19 100644 (file)
@@ -62,6 +62,8 @@ public class Table
   
   private static final short OVERFLOW_ROW_MASK = (short)0x4000;
 
+  static final int MAGIC_TABLE_NUMBER = 1625;
+
   private static final int MAX_BYTE = 256;
 
   /** Table type code for system tables */
@@ -777,51 +779,91 @@ public class Table
   }
 
   /**
-   * Writes a new table defined by the given columns to the database.
+   * Writes a new table defined by the given columns and indexes to the
+   * database.
    * @return the first page of the new table's definition
    */
   public static int writeTableDefinition(
-      List<Column> columns, PageChannel pageChannel, JetFormat format,
-      Charset charset)
+      List<Column> columns, List<IndexBuilder> indexes,
+      PageChannel pageChannel, JetFormat format, Charset charset)
     throws IOException
   {
+    int indexCount = 0;
+    int logicalIndexCount = 0;
+    if(!indexes.isEmpty()) {
+      // sort out index numbers.  for now, these values will always match
+      // (until we support writing foreign key indexes)
+      for(IndexBuilder idx : indexes) {
+        idx.setIndexNumber(logicalIndexCount++);
+        idx.setIndexDataNumber(indexCount++);
+      }
+    }
+
+    // allocate first table def page
+    int tdefPageNumber = pageChannel.allocateNewPage();
+
     // first, create the usage map page
-    int usageMapPageNumber = pageChannel.writeNewPage(
-        createUsageMapDefinitionBuffer(pageChannel, format));
+    int usageMapPageNumber = 
+        createUsageMapDefinitionBuffer(indexes, pageChannel, format);
 
     // next, determine how big the table def will be (in case it will be more
     // than one page)
+    int idxDataLen = (indexCount * (format.SIZE_INDEX_DEFINITION + 
+                                    format.SIZE_INDEX_COLUMN_BLOCK)) + 
+      (logicalIndexCount * format.SIZE_INDEX_INFO_BLOCK);
     int totalTableDefSize = format.SIZE_TDEF_HEADER +
-      (format.SIZE_COLUMN_DEF_BLOCK * columns.size()) +
+      (format.SIZE_COLUMN_DEF_BLOCK * columns.size()) + idxDataLen +
       format.SIZE_TDEF_TRAILER;
+
+    // total up the amount of space used by the column and index names (2
+    // bytes per char + 2 bytes for the length)
     for(Column col : columns) {
-      // we add the number of bytes for the column name and 2 bytes for the
-      // length of the column name
       int nameByteLen = (col.getName().length() *
                          JetFormat.TEXT_FIELD_UNIT_SIZE);
       totalTableDefSize += nameByteLen + 2;
     }
     
+    for(IndexBuilder idx : indexes) {
+      int nameByteLen = (idx.getName().length() *
+                         JetFormat.TEXT_FIELD_UNIT_SIZE);
+      totalTableDefSize += nameByteLen + 2;
+    }
+    
+
     // now, create the table definition
     ByteBuffer buffer = pageChannel.createBuffer(Math.max(totalTableDefSize,
                                                           format.PAGE_SIZE));
     writeTableDefinitionHeader(buffer, columns, usageMapPageNumber,
-                               totalTableDefSize, format);
-    writeColumnDefinitions(buffer, columns, format, charset); 
+                               totalTableDefSize, indexCount, 
+                               logicalIndexCount, format);
+
+    if(indexCount > 0) {
+      // index row counts
+      IndexData.writeRowCountDefinitions(buffer, indexCount, format);
+    }
+
+    // column definitions
+    Column.writeDefinitions(buffer, columns, format, charset); 
     
+    if(indexCount > 0) {
+      // index and index data definitions
+      IndexData.writeDefinitions(buffer, columns, indexes, tdefPageNumber,
+                                 pageChannel, format);
+      Index.writeDefinitions(buffer, indexes, charset);
+    }
+
     //End of tabledef
     buffer.put((byte) 0xff);
     buffer.put((byte) 0xff);
 
     // write table buffer to database
-    int tdefPageNumber = PageChannel.INVALID_PAGE_NUMBER;
     if(totalTableDefSize <= format.PAGE_SIZE) {
       
       // easy case, fits on one page
       buffer.putShort(format.OFFSET_FREE_SPACE,
                       (short)(buffer.remaining() - 8)); // overwrite page free space
       // Write the tdef page to disk.
-      tdefPageNumber = pageChannel.writeNewPage(buffer);
+      pageChannel.writePage(buffer, tdefPageNumber);
       
     } else {
 
@@ -834,11 +876,10 @@ public class Table
         // reset for next write
         partialTdef.clear();
         
-        if(tdefPageNumber == PageChannel.INVALID_PAGE_NUMBER) {
+        if(nextTdefPageNumber == PageChannel.INVALID_PAGE_NUMBER) {
           
           // this is the first page.  note, the first page already has the
           // page header, so no need to write it here
-          tdefPageNumber = pageChannel.allocateNewPage();
           nextTdefPageNumber = tdefPageNumber;
           
         } else {
@@ -879,15 +920,14 @@ public class Table
    */
   private static void writeTableDefinitionHeader(
       ByteBuffer buffer, List<Column> columns,
-      int usageMapPageNumber, int totalTableDefSize, JetFormat format)
+      int usageMapPageNumber, int totalTableDefSize,
+      int indexCount, int logicalIndexCount, JetFormat format)
     throws IOException
   {
     //Start writing the tdef
     writeTablePageHeader(buffer);
     buffer.putInt(totalTableDefSize);  //Length of table def
-    buffer.put((byte) 0x59);  //Unknown
-    buffer.put((byte) 0x06);  //Unknown
-    buffer.putShort((short) 0); //Unknown
+    buffer.putInt(MAGIC_TABLE_NUMBER); // seemingly constant magic value
     buffer.putInt(0);  //Number of rows
     buffer.putInt(0); //Last Autonumber
     buffer.put((byte) 1); // this makes autonumbering work in access
@@ -898,8 +938,8 @@ public class Table
     buffer.putShort((short) columns.size()); //Max columns a row will have
     buffer.putShort(Column.countVariableLength(columns));  //Number of variable columns in table
     buffer.putShort((short) columns.size()); //Number of columns in table
-    buffer.putInt(0);  //Number of indexes in table
-    buffer.putInt(0);  //Number of indexes in table
+    buffer.putInt(logicalIndexCount);  //Number of logical indexes in table
+    buffer.putInt(indexCount);  //Number of indexes in table
     buffer.put((byte) 0); //Usage map row number
     ByteUtil.put3ByteInt(buffer, usageMapPageNumber);  //Usage map page number
     buffer.put((byte) 1); //Free map row number
@@ -926,117 +966,41 @@ public class Table
     buffer.putInt(0);  //Next TDEF page pointer
   }
   
-  /**
-   * @param buffer Buffer to write to
-   * @param columns List of Columns to write definitions for
-   */
-  private static void writeColumnDefinitions(
-      ByteBuffer buffer, List<Column> columns, JetFormat format,
-      Charset charset)
-    throws IOException
-  {
-    short columnNumber = (short) 0;
-    short fixedOffset = (short) 0;
-    short variableOffset = (short) 0;
-    // we specifically put the "long variable" values after the normal
-    // variable length values so that we have a better chance of fitting it
-    // all (because "long variable" values can go in separate pages)
-    short longVariableOffset =
-      Column.countNonLongVariableLength(columns);
-    for (Column col : columns) {
-      int position = buffer.position();
-      buffer.put(col.getType().getValue());
-      buffer.put((byte) 0x59);  //Unknown
-      buffer.put((byte) 0x06);  //Unknown
-      buffer.putShort((short) 0); //Unknown
-      buffer.putShort(columnNumber);  //Column Number
-      if (col.isVariableLength()) {
-        if(!col.getType().isLongValue()) {
-          buffer.putShort(variableOffset++);
-        } else {
-          buffer.putShort(longVariableOffset++);
-        }          
-      } else {
-        buffer.putShort((short) 0);
-      }
-      buffer.putShort(columnNumber); //Column Number again
-      if(col.getType().getHasScalePrecision()) {
-        buffer.put(col.getPrecision());  // numeric precision
-        buffer.put(col.getScale());  // numeric scale
-      } else {
-        buffer.put((byte) 0x00); //unused
-        buffer.put((byte) 0x00); //unused
-      }
-      buffer.putShort((short) 0); //Unknown
-      buffer.put(getColumnBitFlags(col)); // misc col flags
-      if (col.isCompressedUnicode()) {  //Compressed
-        buffer.put((byte) 1);
-      } else {
-        buffer.put((byte) 0);
-      }
-      buffer.putInt(0); //Unknown, but always 0.
-      //Offset for fixed length columns
-      if (col.isVariableLength()) {
-        buffer.putShort((short) 0);
-      } else {
-        buffer.putShort(fixedOffset);
-        fixedOffset += col.getType().getFixedSize(col.getLength());
-      }
-      if(!col.getType().isLongValue()) {
-        buffer.putShort(col.getLength()); //Column length
-      } else {
-        buffer.putShort((short)0x0000); // unused
-      }
-      columnNumber++;
-      if (LOG.isDebugEnabled()) {
-        LOG.debug("Creating new column def block\n" + ByteUtil.toHexString(
-            buffer, position, format.SIZE_COLUMN_DEF_BLOCK));
-      }
-    }
-    for (Column col : columns) {
-      writeName(buffer, col.getName(), charset);
-    }
-  }
-
   /**
    * Writes the given name into the given buffer in the format as expected by
    * {@link #readName}.
    */
-  private static void writeName(ByteBuffer buffer, String name,
-                                Charset charset)
+  static void writeName(ByteBuffer buffer, String name, Charset charset)
   {
       ByteBuffer encName = Column.encodeUncompressedText(name, charset);
       buffer.putShort((short) encName.remaining());
       buffer.put(encName);
   }
-
-  /**
-   * Constructs a byte containing the flags for the given column.
-   */
-  private static byte getColumnBitFlags(Column col) {
-    byte flags = Column.UNKNOWN_FLAG_MASK;
-    if(!col.isVariableLength()) {
-      flags |= Column.FIXED_LEN_FLAG_MASK;
-    }
-    if(col.isAutoNumber()) {
-      flags |= col.getAutoNumberGenerator().getColumnFlags();
-    }
-    return flags;
-  }
   
   /**
    * Create the usage map definition page buffer.  The "used pages" map is in
-   * row 0, the "pages with free space" map is in row 1.
+   * row 0, the "pages with free space" map is in row 1.  Index usage maps are
+   * in subsequent rows.
    */
-  private static ByteBuffer createUsageMapDefinitionBuffer(
-      PageChannel pageChannel, JetFormat format)
+  private static int createUsageMapDefinitionBuffer(
+      List<IndexBuilder> indexes, PageChannel pageChannel, JetFormat format)
     throws IOException
   {
+    // 2 table usage maps plus 1 for each index
+    int umapNum = 2 + indexes.size();
+
     int usageMapRowLength = format.OFFSET_USAGE_MAP_START +
       format.USAGE_MAP_TABLE_BYTE_LENGTH;
-    int freeSpace = format.PAGE_INITIAL_FREE_SPACE
-      - (2 * getRowSpaceUsage(usageMapRowLength, format));
+    int freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE
+      - (umapNum * getRowSpaceUsage(usageMapRowLength, format));
     
+    // for now, don't handle writing that many indexes
+    if(freeSpace < 0) {
+      throw new IOException("FIXME attempting to write too many indexes");
+    }
+
+    int umapPageNumber = pageChannel.allocateNewPage();
+
     ByteBuffer rtn = pageChannel.createPageBuffer();
     rtn.put(PageTypes.DATA);
     rtn.put((byte) 0x1);  //Unknown
@@ -1045,7 +1009,7 @@ public class Table
     rtn.putInt(0); //Unknown
     rtn.putShort((short) 2); //Number of records on this page
 
-    // write two rows of usage map definitions
+    // write two rows of usage map definitions for the table
     int rowStart = findRowEnd(rtn, 0, format) - usageMapRowLength;
     for(int i = 0; i < 2; ++i) {
       rtn.putShort(getRowStartOffset(i, format), (short)rowStart);
@@ -1058,8 +1022,34 @@ public class Table
       }
       rowStart -= usageMapRowLength;
     }
-        
-    return rtn;
+
+    if(!indexes.isEmpty()) {
+      
+      for(int i = 0; i < indexes.size(); ++i) {
+        IndexBuilder idx = indexes.get(i);
+
+        // allocate root page for the index
+        int rootPageNumber = pageChannel.allocateNewPage();
+        int umapRowNum = i + 2;
+
+        // stash info for later use
+        idx.setRootPageNumber(rootPageNumber);
+        idx.setUmapRowNumber((byte)umapRowNum);
+        idx.setUmapPageNumber(umapPageNumber);
+
+        // index map definition, including initial root page
+        rtn.putShort(getRowStartOffset(umapRowNum, format), (short)rowStart);
+        rtn.put(rowStart, UsageMap.MAP_TYPE_INLINE);
+        rtn.putInt(rowStart + 1, rootPageNumber);
+        rtn.put(rowStart + 5, (byte)1);
+
+        rowStart -= usageMapRowLength;
+      }      
+    }
+
+    pageChannel.writePage(rtn, umapPageNumber);
+
+    return umapPageNumber;
   }
     
   /**
@@ -1091,12 +1081,7 @@ public class Table
     _freeSpacePages = UsageMap.read(getDatabase(), pageNum, rowNum, false);
     
     for (int i = 0; i < _indexCount; i++) {
-      int uniqueEntryCountOffset =
-        (getFormat().OFFSET_INDEX_DEF_BLOCK +
-         (i * getFormat().SIZE_INDEX_DEFINITION) + 4);
-      int uniqueEntryCount = tableBuffer.getInt(uniqueEntryCountOffset);
-      _indexDatas.add(createIndexData(i, uniqueEntryCount, 
-                                      uniqueEntryCountOffset));
+      _indexDatas.add(IndexData.create(this, tableBuffer, i, getFormat()));
     }
     
     int colOffset = getFormat().OFFSET_INDEX_DEF_BLOCK +
@@ -1132,22 +1117,12 @@ public class Table
 
     // read index column information
     for (int i = 0; i < _indexCount; i++) {
-      ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX); //Forward past Unknown
       _indexDatas.get(i).read(tableBuffer, _columns);
     }
 
     // read logical index info (may be more logical indexes than index datas)
     for (int i = 0; i < _logicalIndexCount; i++) {
-
-      ByteUtil.forward(tableBuffer, getFormat().SKIP_BEFORE_INDEX_SLOT); //Forward past Unknown
-      int indexNumber = tableBuffer.getInt();
-      int indexDataNumber = tableBuffer.getInt();
-      ByteUtil.forward(tableBuffer, 11);
-      byte indexType = tableBuffer.get();
-      ByteUtil.forward(tableBuffer, getFormat().SKIP_AFTER_INDEX_SLOT); //Skip past Unknown
-
-      IndexData indexData = _indexDatas.get(indexDataNumber);
-      _indexes.add(new Index(indexData, indexNumber, indexType));
+      _indexes.add(new Index(tableBuffer, _indexDatas, getFormat()));
     }
 
     // read logical index names
@@ -1162,20 +1137,6 @@ public class Table
       Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR);
     }
   }
-
-  /**
-   * Creates an index with the given initial info.
-   */
-  private IndexData createIndexData(int indexDataNumber,
-                                    int uniqueEntryCount, 
-                                    int uniqueEntryCountOffset)
-  {
-    return(_useBigIndex ?
-           new BigIndexData(this, indexDataNumber, uniqueEntryCount,
-                            uniqueEntryCountOffset) :
-           new SimpleIndexData(this, indexDataNumber, uniqueEntryCount, 
-                               uniqueEntryCountOffset));
-  }
   
   /**
    * Writes the given page data to the given page number, clears any other
@@ -1209,15 +1170,6 @@ public class Table
                                          getDatabase().getCharset());
   }
   
-  /**
-   * Skips past a name int the buffer at the current position.  The
-   * expected name format is the same as that for {@link #readName}.
-   */
-  private void skipName(ByteBuffer buffer) {
-       int nameLength = readNameLength(buffer);
-    ByteUtil.forward(buffer, nameLength);
-  }
-  
   /**
    * Returns a name length read from the buffer at the current position.
    */
@@ -1571,7 +1523,7 @@ public class Table
     ByteBuffer dataPage = _addRowBufferH.setNewPage(getPageChannel());
     dataPage.put(PageTypes.DATA); //Page type
     dataPage.put((byte) 1); //Unknown
-    dataPage.putShort((short)getFormat().PAGE_INITIAL_FREE_SPACE); //Free space in this page
+    dataPage.putShort((short)getFormat().DATA_PAGE_INITIAL_FREE_SPACE); //Free space in this page
     dataPage.putInt(_tableDefPageNumber); //Page pointer to table definition
     dataPage.putInt(0); //Unknown
     dataPage.putShort((short)0); //Number of rows on this page
@@ -1761,7 +1713,8 @@ public class Table
                rtn.append("\nName: " + _name);
     rtn.append("\nRow count: " + _rowCount);
     rtn.append("\nColumn count: " + _columns.size());
-    rtn.append("\nIndex count: " + _indexCount);
+    rtn.append("\nIndex (data) count: " + _indexCount);
+    rtn.append("\nLogical Index count: " + _logicalIndexCount);
     rtn.append("\nColumns:\n");
     for(Column col : _columns) {
       rtn.append(col);
index 13ff1dfc74d3d0df9db4eab25c341b4383c0b16b..740a68464109dd3ada6e20a8fe4661b26f7717c8 100644 (file)
@@ -42,8 +42,11 @@ public class TableBuilder {
   private String _name;
   /** columns for the new table */
   private List<Column> _columns = new ArrayList<Column>();
-  /** whether or not table/columns names are automatically escaped */
+  /** indexes for the new table */
+  private List<IndexBuilder> _indexes = new ArrayList<IndexBuilder>();
+  /** whether or not table/column/index names are automatically escaped */
   private boolean _escapeIdentifiers;
+  
 
   public TableBuilder(String name) {
     this(name, false);
@@ -76,6 +79,20 @@ public class TableBuilder {
     return addColumn(columnBuilder.toColumn());
   }
 
+  /**
+   * Adds an IndexBuilder to the new table.
+   */
+  public TableBuilder addIndex(IndexBuilder index) {
+    if(_escapeIdentifiers) {
+      index.setName(Database.escapeIdentifier(index.getName()));
+      for(IndexBuilder.Column col : index.getColumns()) {
+        col.setName(Database.escapeIdentifier(col.getName()));
+      }
+    }
+    _indexes.add(index);
+    return this;
+  }
+
   /**
    * Sets whether or not subsequently added columns will have their names
    * automatically escaped
@@ -101,7 +118,7 @@ public class TableBuilder {
   public Table toTable(Database db)
     throws IOException
   {
-    db.createTable(_name, _columns);
+    db.createTable(_name, _columns, _indexes);
     return db.getTable(_name);
   }
   
index a2f00ca173a9ecff6374c36c483df6e9b1673c35..5ce7174ec1100832fdbd5ca25f7a7536415d0b0b 100644 (file)
@@ -38,6 +38,7 @@ import java.util.TreeSet;
 
 import junit.framework.TestCase;
 
+import static com.healthmarketscience.jackcess.Database.*;
 import static com.healthmarketscience.jackcess.DatabaseTest.*;
 import static com.healthmarketscience.jackcess.JetFormatTest.*;
 
@@ -422,6 +423,46 @@ public class IndexTest extends TestCase {
       db.close();
     }
   }
+
+  public void testIndexCreation() throws Exception
+  {
+    for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
+      Database db = create(fileFormat);
+
+      Table t = new TableBuilder("TestTable")
+        .addColumn(new ColumnBuilder("id", DataType.LONG))
+        .addColumn(new ColumnBuilder("data", DataType.TEXT))
+        .addIndex(new IndexBuilder(IndexBuilder.PRIMARY_KEY_NAME)
+                  .addColumns("id").setPrimaryKey())
+        .toTable(db);
+
+      assertEquals(1, t.getIndexes().size());
+      Index idx = t.getIndexes().get(0);
+      
+      assertEquals(IndexBuilder.PRIMARY_KEY_NAME, idx.getName());       
+      assertEquals(1, idx.getColumns().size());
+      assertEquals("id", idx.getColumns().get(0).getName());
+      assertTrue(idx.getColumns().get(0).isAscending());
+      assertTrue(idx.isPrimaryKey());
+      assertTrue(idx.isUnique());
+      assertFalse(idx.shouldIgnoreNulls());
+      assertNull(idx.getReference());
+
+      t.addRow(2, "row2");
+      t.addRow(1, "row1");
+      t.addRow(3, "row3");
+
+      Cursor c = new CursorBuilder(t)
+        .setIndexByName(IndexBuilder.PRIMARY_KEY_NAME).toCursor();
+
+      for(int i = 1; i <= 3; ++i) {
+        Map<String,Object> row = c.getNextRow();
+        assertEquals(i, row.get("id"));
+        assertEquals("row" + i, row.get("data"));
+      }
+      assertFalse(c.moveToNextRow());
+    }    
+  }
   
   private void checkIndexColumns(Table table, String... idxInfo)
     throws Exception