<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">
{
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');
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).
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;
*/
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");
}
}
}
+
+ 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));
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;
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 */
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() {
return _indexType == FOREIGN_KEY_INDEX_TYPE;
}
+ public ForeignKeyReference getReference() {
+ return _reference;
+ }
+
/**
* Whether or not {@code null} values are actually recorded in the 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();
}
}
+ /**
+ * 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();
+ }
+ }
}
--- /dev/null
+/*
+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;
+ }
+ }
+
+}
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);
_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;
}
}
/**
- * 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();
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>
@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);
}
}
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
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()) {
}
// 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()));
}
/**
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;
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;
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();
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();
protected abstract long defineMaxDatabaseSize();
protected abstract int defineMaxRowSize();
- protected abstract int definePageInitialFreeSpace();
+ protected abstract int defineDataPageInitialFreeSpace();
protected abstract int defineOffsetMaskedHeader();
protected abstract byte[] defineHeaderMask();
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();
@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; }
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; }
@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; }
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; }
package com.healthmarketscience.jackcess;
import java.io.IOException;
+import java.util.Collections;
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. */
{
throw new UnsupportedOperationException();
}
-
/**
* Simple implementation of a DataPage
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
}
@Override
- public void setEntries(List<Entry> entries) {
-
+ public void setEntries(List<Entry> entries) {
_entries = entries;
}
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 */
}
/**
- * 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 {
// 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 {
*/
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
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
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
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);
}
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;
}
/**
_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 +
// 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
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
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.
*/
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
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);
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);
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
public Table toTable(Database db)
throws IOException
{
- db.createTable(_name, _columns);
+ db.createTable(_name, _columns, _indexes);
return db.getTable(_name);
}
import junit.framework.TestCase;
+import static com.healthmarketscience.jackcess.Database.*;
import static com.healthmarketscience.jackcess.DatabaseTest.*;
import static com.healthmarketscience.jackcess.JetFormatTest.*;
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