From 334c5ff3e738e3b715198493ca79c3c6421a3b08 Mon Sep 17 00:00:00 2001 From: James Ahlborn Date: Fri, 4 Mar 2011 12:58:55 +0000 Subject: [PATCH] add support for creating indexes (except foreign key indexes) on a table when a table is created git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@515 f203690c-595d-4dc9-a70b-905162fa7fd2 --- src/changes/changes.xml | 6 + .../healthmarketscience/jackcess/Column.java | 90 +++++- .../jackcess/Database.java | 34 ++- .../healthmarketscience/jackcess/Index.java | 160 ++++++++++- .../jackcess/IndexBuilder.java | 236 +++++++++++++++ .../jackcess/IndexData.java | 148 ++++++++-- .../jackcess/JetFormat.java | 27 +- .../jackcess/SimpleIndexData.java | 19 +- .../healthmarketscience/jackcess/Table.java | 269 ++++++++---------- .../jackcess/TableBuilder.java | 21 +- .../jackcess/IndexTest.java | 41 +++ 11 files changed, 844 insertions(+), 207 deletions(-) create mode 100644 src/java/com/healthmarketscience/jackcess/IndexBuilder.java diff --git a/src/changes/changes.xml b/src/changes/changes.xml index e4f2b9c..e65bfde 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -12,10 +12,16 @@ More fixes related to reading and interpreting index information. Handle multiple logical indexes backed by the same index data. + Interpret foreign key constraint information. Allow MSISAM files to be written (experimental). + + 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. + diff --git a/src/java/com/healthmarketscience/jackcess/Column.java b/src/java/com/healthmarketscience/jackcess/Column.java index 4c164ad..9f963be 100644 --- a/src/java/com/healthmarketscience/jackcess/Column.java +++ b/src/java/com/healthmarketscience/jackcess/Column.java @@ -1022,7 +1022,7 @@ public class Column implements Comparable { { 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 { 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 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). diff --git a/src/java/com/healthmarketscience/jackcess/Database.java b/src/java/com/healthmarketscience/jackcess/Database.java index b8dcc94..cc14cf5 100644 --- a/src/java/com/healthmarketscience/jackcess/Database.java +++ b/src/java/com/healthmarketscience/jackcess/Database.java @@ -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 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 columns, + List 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 idxNames = new HashSet(); + 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)); diff --git a/src/java/com/healthmarketscience/jackcess/Index.java b/src/java/com/healthmarketscience/jackcess/Index.java index f5cb874..9ba24cc 100644 --- a/src/java/com/healthmarketscience/jackcess/Index.java +++ b/src/java/com/healthmarketscience/jackcess/Index.java @@ -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 { 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 { 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 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 { 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 { @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 { } } + /** + * 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 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 index 0000000..4799abf --- /dev/null +++ b/src/java/com/healthmarketscience/jackcess/IndexBuilder.java @@ -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 _columns = new ArrayList(); + + // 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 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 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 idxColNames = new HashSet(); + 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; + } + } + +} diff --git a/src/java/com/healthmarketscience/jackcess/IndexData.java b/src/java/com/healthmarketscience/jackcess/IndexData.java index 995e308..fa2f67e 100644 --- a/src/java/com/healthmarketscience/jackcess/IndexData.java +++ b/src/java/com/healthmarketscience/jackcess/IndexData.java @@ -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 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 columns, List 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 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 *

@@ -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())); } /** diff --git a/src/java/com/healthmarketscience/jackcess/JetFormat.java b/src/java/com/healthmarketscience/jackcess/JetFormat.java index 723ac5e..d512659 100644 --- a/src/java/com/healthmarketscience/jackcess/JetFormat.java +++ b/src/java/com/healthmarketscience/jackcess/JetFormat.java @@ -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; } diff --git a/src/java/com/healthmarketscience/jackcess/SimpleIndexData.java b/src/java/com/healthmarketscience/jackcess/SimpleIndexData.java index 42d8030..7a662e7 100644 --- a/src/java/com/healthmarketscience/jackcess/SimpleIndexData.java +++ b/src/java/com/healthmarketscience/jackcess/SimpleIndexData.java @@ -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.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 _entries; private SimpleDataPage(int pageNumber) { + this(pageNumber, false, null); + } + + private SimpleDataPage(int pageNumber, boolean leaf, List entries) + { _pageNumber = pageNumber; + _leaf = leaf; + _entries = entries; } @Override @@ -208,8 +220,7 @@ public class SimpleIndexData extends IndexData { } @Override - public void setEntries(List entries) { - + public void setEntries(List entries) { _entries = entries; } diff --git a/src/java/com/healthmarketscience/jackcess/Table.java b/src/java/com/healthmarketscience/jackcess/Table.java index c25934b..0f3a076 100644 --- a/src/java/com/healthmarketscience/jackcess/Table.java +++ b/src/java/com/healthmarketscience/jackcess/Table.java @@ -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 columns, PageChannel pageChannel, JetFormat format, - Charset charset) + List columns, List 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 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 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 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); diff --git a/src/java/com/healthmarketscience/jackcess/TableBuilder.java b/src/java/com/healthmarketscience/jackcess/TableBuilder.java index 13ff1df..740a684 100644 --- a/src/java/com/healthmarketscience/jackcess/TableBuilder.java +++ b/src/java/com/healthmarketscience/jackcess/TableBuilder.java @@ -42,8 +42,11 @@ public class TableBuilder { private String _name; /** columns for the new table */ private List _columns = new ArrayList(); - /** whether or not table/columns names are automatically escaped */ + /** indexes for the new table */ + private List _indexes = new ArrayList(); + /** 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); } diff --git a/test/src/java/com/healthmarketscience/jackcess/IndexTest.java b/test/src/java/com/healthmarketscience/jackcess/IndexTest.java index a2f00ca..5ce7174 100644 --- a/test/src/java/com/healthmarketscience/jackcess/IndexTest.java +++ b/test/src/java/com/healthmarketscience/jackcess/IndexTest.java @@ -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 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 -- 2.39.5