diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2016-09-08 12:45:38 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2016-09-08 12:45:38 +0000 |
commit | 819953ac72f4e8ba7e070e53ee12e9ba1decda5a (patch) | |
tree | bbc5df96955b5a052c037831988a93e7444486f9 | |
parent | 70eb4cc43cfc18a65aa50506dfeaf1d8a2e3a3f4 (diff) | |
parent | 4de28cb4f6595148baaf36a57875e20c32faeda2 (diff) | |
download | jackcess-819953ac72f4e8ba7e070e53ee12e9ba1decda5a.tar.gz jackcess-819953ac72f4e8ba7e070e53ee12e9ba1decda5a.zip |
merge branch mutateops changes through r1030
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1031 f203690c-595d-4dc9-a70b-905162fa7fd2
26 files changed, 2872 insertions, 547 deletions
@@ -2,8 +2,6 @@ Missing pieces: - fix long text index entries (for new general sort order) * ??? -- implement foreign key index creation & relationship creation - * MEDIUM - implement table creation w/ complex columns * MEDIUM - implement table, column, index renaming @@ -19,13 +17,3 @@ Rename: - Table - update table def, queries, relationships, complex tables? - Column - update table def, queries, relationships, complex tables? - Index - update table def - -Index add (fk impl) -- Database.addIndex(IndexBuilder) - use TableCreator internal - - add indexes separately from adding fk info, (backing indexes need to be - added first) - - require baking indexes to be created first (does MSAccess?) - - need to populate index after creation! populate first, then add? - - add relationships -- flush all non-system tables from DbImpl._tableCache (references to old table - impls) diff --git a/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java b/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java index cb4b554..76a1783 100644 --- a/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java @@ -25,12 +25,16 @@ import com.healthmarketscience.jackcess.impl.ColumnImpl; import com.healthmarketscience.jackcess.impl.DatabaseImpl; import com.healthmarketscience.jackcess.impl.JetFormat; import com.healthmarketscience.jackcess.impl.PropertyMapImpl; +import com.healthmarketscience.jackcess.impl.TableImpl; +import com.healthmarketscience.jackcess.impl.TableUpdater; /** * Builder style class for constructing a {@link Column}. See {@link - * TableBuilder} for example usage. + * TableBuilder} for example usage. Additionally, a Column can be added to an + * existing Table using the {@link #addToTable(Table)} method. * * @author James Ahlborn + * @see TableBuilder * @usage _general_class_ */ public class ColumnBuilder { @@ -381,12 +385,12 @@ public class ColumnBuilder { * @usage _advanced_method_ */ public void validate(JetFormat format) { - if(getType() == null) { - throw new IllegalArgumentException(withErrorContext("must have type")); - } DatabaseImpl.validateIdentifierName( getName(), format.MAX_COLUMN_NAME_LENGTH, "column"); + if(getType() == null) { + throw new IllegalArgumentException(withErrorContext("must have type")); + } if(getType().isUnsupported()) { throw new IllegalArgumentException(withErrorContext( "Cannot create column with unsupported type " + getType())); @@ -472,6 +476,14 @@ public class ColumnBuilder { return this; } + /** + * Adds a new Column to the given Table with the currently configured + * attributes. + */ + public Column addToTable(Table table) throws IOException { + return new TableUpdater((TableImpl)table).addColumn(this); + } + private String withErrorContext(String msg) { return msg + "(Column=" + getName() + ")"; } diff --git a/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java b/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java index f545366..688eb9e 100644 --- a/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java @@ -19,8 +19,6 @@ package com.healthmarketscience.jackcess; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; import java.util.List; import java.util.Map; @@ -147,37 +145,13 @@ public class CursorBuilder { * Searches for an index with the given column names. */ private CursorBuilder setIndexByColumns(List<String> searchColumns) { - boolean found = false; - for(IndexImpl index : _table.getIndexes()) { - - Collection<? extends Index.Column> indexColumns = index.getColumns(); - if(indexColumns.size() != searchColumns.size()) { - continue; - } - Iterator<String> sIter = searchColumns.iterator(); - Iterator<? extends Index.Column> iIter = indexColumns.iterator(); - boolean matches = true; - while(sIter.hasNext()) { - String sColName = sIter.next(); - String iColName = iIter.next().getName(); - if((sColName != iColName) && - ((sColName == null) || !sColName.equalsIgnoreCase(iColName))) { - matches = false; - break; - } - } - - if(matches) { - _index = index; - found = true; - break; - } - } - if(!found) { + IndexImpl index = _table.findIndexForColumns(searchColumns, false); + if(index == null) { throw new IllegalArgumentException("Index with columns " + searchColumns + " does not exist in table " + _table); } + _index = index; return this; } diff --git a/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java b/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java index 9c9f584..d10a6fb 100644 --- a/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java @@ -16,6 +16,7 @@ limitations under the License. package com.healthmarketscience.jackcess; +import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -25,12 +26,16 @@ import com.healthmarketscience.jackcess.impl.DatabaseImpl; import com.healthmarketscience.jackcess.impl.IndexData; import com.healthmarketscience.jackcess.impl.IndexImpl; import com.healthmarketscience.jackcess.impl.JetFormat; +import com.healthmarketscience.jackcess.impl.TableImpl; +import com.healthmarketscience.jackcess.impl.TableUpdater; /** * Builder style class for constructing an {@link Index}. See {@link - * TableBuilder} for example usage. + * TableBuilder} for example usage. Additionally, an Index can be added to an + * existing Table using the {@link #addToTable(Table)} method. * * @author James Ahlborn + * @see TableBuilder * @usage _general_class_ */ public class IndexBuilder @@ -47,6 +52,8 @@ public class IndexBuilder private byte _flags = IndexData.UNKNOWN_INDEX_FLAG; /** the names and orderings of the indexed columns */ private final List<Column> _columns = new ArrayList<Column>(); + /** 0-based index number */ + private int _indexNumber; public IndexBuilder(String name) { _name = name; @@ -118,6 +125,14 @@ public class IndexBuilder } /** + * @usage _advanced_method_ + */ + public IndexBuilder setType(byte type) { + _type = type; + return this; + } + + /** * Sets this index to enforce uniqueness. */ public IndexBuilder setUnique() { @@ -141,6 +156,26 @@ public class IndexBuilder return this; } + /** + * @usage _advanced_method_ + */ + public int getIndexNumber() { + return _indexNumber; + } + + /** + * @usage _advanced_method_ + */ + public void setIndexNumber(int newIndexNumber) { + _indexNumber = newIndexNumber; + } + + /** + * Checks that this index definition is valid. + * + * @throws IllegalArgumentException if this index definition is invalid. + * @usage _advanced_method_ + */ public void validate(Set<String> tableColNames, JetFormat format) { DatabaseImpl.validateIdentifierName( @@ -169,6 +204,14 @@ public class IndexBuilder } } + /** + * Adds a new Index to the given Table with the currently configured + * attributes. + */ + public Index addToTable(Table table) throws IOException { + return new TableUpdater((TableImpl)table).addIndex(this); + } + private String withErrorContext(String msg) { return msg + "(Index=" + getName() + ")"; } diff --git a/src/main/java/com/healthmarketscience/jackcess/Relationship.java b/src/main/java/com/healthmarketscience/jackcess/Relationship.java index 615f5f7..4200e57 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Relationship.java +++ b/src/main/java/com/healthmarketscience/jackcess/Relationship.java @@ -27,6 +27,10 @@ import java.util.List; */ public interface Relationship { + public enum JoinType { + INNER, LEFT_OUTER, RIGHT_OUTER; + } + public String getName(); public Table getFromTable(); @@ -45,7 +49,11 @@ public interface Relationship public boolean cascadeDeletes(); + public boolean cascadeNullOnDelete(); + public boolean isLeftOuterJoin(); public boolean isRightOuterJoin(); + + public JoinType getJoinType(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java b/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java new file mode 100644 index 0000000..8ba9bb1 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java @@ -0,0 +1,184 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.healthmarketscience.jackcess.impl.DatabaseImpl; +import com.healthmarketscience.jackcess.impl.RelationshipCreator; +import com.healthmarketscience.jackcess.impl.RelationshipImpl; + +/** + * Builder style class for constructing a {@link Relationship}, and, + * optionally, the associated backing foreign key (if referential integrity + * enforcement is enabled). A Relationship can only be constructed for + * {@link Table}s which already exist in the {@link Database}. Additionally, + * if integrity enforcement is enabled, there must already be a unique index + * on the "from" Table for the relevant columns (same requirement as MS + * Access). + * <p/> + * Example: + * <pre> + * Relationship rel = new RelationshipBuilder("FromTable", "ToTable") + * .addColumns("ID", "FK_ID") + * .setReferentialIntegrity() + * .setCascadeDeletes() + * .toRelationship(db); + * </pre> + * + * @author James Ahlborn + * @see TableBuilder + * @usage _general_class_ + */ +public class RelationshipBuilder +{ + private static final int JOIN_FLAGS = + RelationshipImpl.LEFT_OUTER_JOIN_FLAG | + RelationshipImpl.RIGHT_OUTER_JOIN_FLAG; + + /** relationship flags (default to "don't enforce") */ + private int _flags = RelationshipImpl.NO_REFERENTIAL_INTEGRITY_FLAG; + private String _fromTable; + private String _toTable; + private List<String> _fromCols = new ArrayList<String>(); + private List<String> _toCols = new ArrayList<String>(); + + public RelationshipBuilder(String fromTable, String toTable) + { + _fromTable = fromTable; + _toTable = toTable; + } + + /** + * Adds a pair of columns to the relationship. + */ + public RelationshipBuilder addColumns(String fromCol, String toCol) { + _fromCols.add(fromCol); + _toCols.add(toCol); + return this; + } + + /** + * Enables referential integrity enforcement for this relationship. + * + * Note, this requires the "from" table to have an existing unique index on + * the relevant columns. + */ + public RelationshipBuilder setReferentialIntegrity() { + return clearFlag(RelationshipImpl.NO_REFERENTIAL_INTEGRITY_FLAG); + } + + /** + * Enables deletes to be cascaded from the "from" table to the "to" table. + * + * Note, this requires referential integrity to be enforced. + */ + public RelationshipBuilder setCascadeDeletes() { + return setFlag(RelationshipImpl.CASCADE_DELETES_FLAG); + } + + /** + * Enables updates to be cascaded from the "from" table to the "to" table. + * + * Note, this requires referential integrity to be enforced. + */ + public RelationshipBuilder setCascadeUpdates() { + return setFlag(RelationshipImpl.CASCADE_UPDATES_FLAG); + } + + /** + * Enables deletes in the "from" table to be cascaded as "null" to the "to" + * table. + * + * Note, this requires referential integrity to be enforced. + */ + public RelationshipBuilder setCascadeNullOnDelete() { + return setFlag(RelationshipImpl.CASCADE_NULL_FLAG); + } + + /** + * Sets the preferred join type for this relationship. + */ + public RelationshipBuilder setJoinType(Relationship.JoinType joinType) { + clearFlag(JOIN_FLAGS); + switch(joinType) { + case INNER: + // nothing to do + break; + case LEFT_OUTER: + _flags |= RelationshipImpl.LEFT_OUTER_JOIN_FLAG; + break; + case RIGHT_OUTER: + _flags |= RelationshipImpl.RIGHT_OUTER_JOIN_FLAG; + break; + default: + throw new RuntimeException("unexpected join type " + joinType); + } + return this; + } + + public boolean hasReferentialIntegrity() { + return !hasFlag(RelationshipImpl.NO_REFERENTIAL_INTEGRITY_FLAG); + } + + public int getFlags() { + return _flags; + } + + public String getFromTable() { + return _fromTable; + } + + public String getToTable() { + return _toTable; + } + + public List<String> getFromColumns() { + return _fromCols; + } + + public List<String> getToColumns() { + return _toCols; + } + + /** + * Creates a new Relationship in the given Database with the currently + * configured attributes. + */ + public Relationship toRelationship(Database db) + throws IOException + { + return new RelationshipCreator((DatabaseImpl)db).createRelationship(this); + } + + private RelationshipBuilder setFlag(int flagMask) { + _flags |= flagMask; + return this; + } + + private RelationshipBuilder clearFlag(int flagMask) { + _flags &= ~flagMask; + return this; + } + + private boolean hasFlag(int flagMask) { + return((_flags & flagMask) != 0); + } + +} diff --git a/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java b/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java index 8e5272e..31aa3a0 100644 --- a/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java +++ b/src/main/java/com/healthmarketscience/jackcess/TableBuilder.java @@ -28,6 +28,7 @@ import java.util.Set; import com.healthmarketscience.jackcess.impl.DatabaseImpl; import com.healthmarketscience.jackcess.impl.PropertyMapImpl; +import com.healthmarketscience.jackcess.impl.TableCreator; /** * Builder style class for constructing a {@link Table}. @@ -44,6 +45,9 @@ import com.healthmarketscience.jackcess.impl.PropertyMapImpl; * </pre> * * @author James Ahlborn + * @see ColumnBuilder + * @see IndexBuilder + * @see RelationshipBuilder * @usage _general_class_ */ public class TableBuilder { @@ -116,6 +120,9 @@ public class TableBuilder { } } + public String getName() { + return _name; + } /** * Adds a Column to the new table. @@ -140,6 +147,10 @@ public class TableBuilder { return this; } + public List<ColumnBuilder> getColumns() { + return _columns; + } + /** * Adds an IndexBuilder to the new table. */ @@ -155,6 +166,22 @@ public class TableBuilder { } /** + * Adds the Indexes to the new table. + */ + public TableBuilder addIndexes(Collection<? extends IndexBuilder> indexes) { + if(indexes != null) { + for(IndexBuilder col : indexes) { + addIndex(col); + } + } + return this; + } + + public List<IndexBuilder> getIndexes() { + return _indexes; + } + + /** * Sets whether or not subsequently added columns will have their names * automatically escaped */ @@ -202,35 +229,16 @@ public class TableBuilder { return this; } + public Map<String,PropertyMap.Property> getProperties() { + return _props; + } + /** * Creates a new Table in the given Database with the currently configured * attributes. */ - public Table toTable(Database db) - throws IOException - { - ((DatabaseImpl)db).createTable(_name, _columns, _indexes); - Table table = db.getTable(_name); - - boolean addedProps = false; - if(_props != null) { - table.getProperties().putAll(_props.values()); - addedProps = true; - } - for(ColumnBuilder cb : _columns) { - Map<String,PropertyMap.Property> colProps = cb.getProperties(); - if(colProps != null) { - table.getColumn(cb.getName()).getProperties().putAll(colProps.values()); - addedProps = true; - } - } - - // all table and column props are saved together - if(addedProps) { - table.getProperties().save(); - } - - return table; + public Table toTable(Database db) throws IOException { + return new TableCreator(((DatabaseImpl)db)).createTable(this); } /** diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java index 6b28b22..efb5587 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java @@ -397,6 +397,21 @@ public final class ByteUtil { } return -1; } + + /** + * Inserts empty data of the given length at the current position of the + * given buffer (moving existing data forward the given length). The limit + * of the buffer is adjusted by the given length. The buffer is expecting + * to have the required capacity available. + */ + public static void insertEmptyData(ByteBuffer buffer, int len) { + byte[] buf = buffer.array(); + int pos = buffer.position(); + int limit = buffer.limit(); + System.arraycopy(buf, pos, buf, pos + len, limit - pos); + Arrays.fill(buf, pos, pos + len, (byte)0); + buffer.limit(limit + len); + } /** * Convert a byte buffer to a hexadecimal string for display diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index 8db672f..998e80a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -33,6 +33,7 @@ import java.sql.Blob; import java.sql.Clob; import java.sql.SQLException; import java.util.Calendar; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map; @@ -309,13 +310,17 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { return new ColumnImpl(args); } - /** + /** * Sets the usage maps for this column. */ void setUsageMaps(UsageMap ownedPages, UsageMap freeSpacePages) { // base does nothing } + void collectUsageMapPages(Collection<Integer> pages) { + // base does nothing + } + /** * Secondary column initialization after the table is fully loaded. */ @@ -1405,22 +1410,6 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { } /** - * @param columns A list of columns in a table definition - * @return The number of variable length columns which are not long values - * found in the list - * @usage _advanced_method_ - */ - private static short countNonLongVariableLength(List<ColumnBuilder> columns) { - short rtn = 0; - for (ColumnBuilder col : columns) { - if (col.isVariableLength() && !col.getType().isLongValue()) { - rtn++; - } - } - return rtn; - } - - /** * @return an appropriate BigDecimal representation of the given object. * <code>null</code> is returned as 0 and Numbers are converted * using their double representation. @@ -1585,90 +1574,118 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { protected static void writeDefinitions(TableCreator creator, ByteBuffer buffer) throws IOException { - List<ColumnBuilder> columns = creator.getColumns(); - 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 = countNonLongVariableLength(columns); - for (ColumnBuilder col : columns) { + int longVariableOffset = creator.countNonLongVariableLength(); + creator.setColumnOffsets(0, 0, longVariableOffset); - buffer.put(col.getType().getValue()); - buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); //constant magic number - buffer.putShort(col.getColumnNumber()); //Column Number + for (ColumnBuilder col : creator.getColumns()) { + writeDefinition(creator, col, buffer); + } - if(col.isVariableLength()) { - if(!col.getType().isLongValue()) { - buffer.putShort(variableOffset++); - } else { - buffer.putShort(longVariableOffset++); - } - } else { - buffer.putShort((short) 0); - } + for (ColumnBuilder col : creator.getColumns()) { + TableImpl.writeName(buffer, col.getName(), creator.getCharset()); + } + } - buffer.putShort(col.getColumnNumber()); //Column Number again + protected static void writeDefinition( + TableMutator mutator, ColumnBuilder col, ByteBuffer buffer) + throws IOException + { + TableMutator.ColumnOffsets colOffsets = mutator.getColumnOffsets(); - if(col.getType().isTextual()) { - // this will write 4 bytes (note we don't support writing dbs which - // use the text code page) - writeSortOrder(buffer, col.getTextSortOrder(), creator.getFormat()); - } else { - // note scale/precision not stored for calculated numeric fields - if(col.getType().getHasScalePrecision() && !col.isCalculated()) { - 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(col.getType().getValue()); + buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); //constant magic number + buffer.putShort(col.getColumnNumber()); //Column Number - buffer.put(getColumnBitFlags(col)); // misc col flags + if(col.isVariableLength()) { + buffer.putShort(colOffsets.getNextVariableOffset(col)); + } else { + buffer.putShort((short) 0); + } - // note access doesn't seem to allow unicode compression for calced fields - if(col.isCalculated()) { - buffer.put(CALCULATED_EXT_FLAG_MASK); - } else if (col.isCompressedUnicode()) { //Compressed - buffer.put(COMPRESSED_UNICODE_EXT_FLAG_MASK); + buffer.putShort(col.getColumnNumber()); //Column Number again + + if(col.getType().isTextual()) { + // this will write 4 bytes (note we don't support writing dbs which + // use the text code page) + writeSortOrder(buffer, col.getTextSortOrder(), mutator.getFormat()); + } else { + // note scale/precision not stored for calculated numeric fields + if(col.getType().getHasScalePrecision() && !col.isCalculated()) { + buffer.put(col.getPrecision()); // numeric precision + buffer.put(col.getScale()); // numeric scale } else { - buffer.put((byte)0); + buffer.put((byte) 0x00); //unused + buffer.put((byte) 0x00); //unused } + buffer.putShort((short) 0); //Unknown + } - buffer.putInt(0); //Unknown, but always 0. + buffer.put(getColumnBitFlags(col)); // misc col flags - //Offset for fixed length columns - if(col.isVariableLength()) { - buffer.putShort((short) 0); - } else { - buffer.putShort(fixedOffset); - fixedOffset += col.getType().getFixedSize(col.getLength()); - } + // note access doesn't seem to allow unicode compression for calced fields + if(col.isCalculated()) { + buffer.put(CALCULATED_EXT_FLAG_MASK); + } else if (col.isCompressedUnicode()) { //Compressed + buffer.put(COMPRESSED_UNICODE_EXT_FLAG_MASK); + } else { + buffer.put((byte)0); + } + + buffer.putInt(0); //Unknown, but always 0. - if(!col.getType().isLongValue()) { - short length = col.getLength(); - if(col.isCalculated()) { - // calced columns have additional value overhead - if(!col.getType().isVariableLength() || - col.getType().getHasScalePrecision()) { - length = CalculatedColumnUtil.CALC_FIXED_FIELD_LEN; - } else { - length += CalculatedColumnUtil.CALC_EXTRA_DATA_LEN; - } + //Offset for fixed length columns + if(col.isVariableLength()) { + buffer.putShort((short) 0); + } else { + buffer.putShort(colOffsets.getNextFixedOffset(col)); + } + + if(!col.getType().isLongValue()) { + short length = col.getLength(); + if(col.isCalculated()) { + // calced columns have additional value overhead + if(!col.getType().isVariableLength() || + col.getType().getHasScalePrecision()) { + length = CalculatedColumnUtil.CALC_FIXED_FIELD_LEN; + } else { + length += CalculatedColumnUtil.CALC_EXTRA_DATA_LEN; } - buffer.putShort(length); //Column length - } else { - buffer.putShort((short)0x0000); // unused } - + buffer.putShort(length); //Column length + } else { + buffer.putShort((short)0x0000); // unused } - for (ColumnBuilder col : columns) { - TableImpl.writeName(buffer, col.getName(), creator.getCharset()); + } + + protected static void writeColUsageMapDefinitions( + TableCreator creator, ByteBuffer buffer) + throws IOException + { + // write long value column usage map references + for(ColumnBuilder lvalCol : creator.getLongValueColumns()) { + writeColUsageMapDefinition(creator, lvalCol, buffer); } } + protected static void writeColUsageMapDefinition( + TableMutator creator, ColumnBuilder lvalCol, ByteBuffer buffer) + throws IOException + { + TableMutator.ColumnState colState = creator.getColumnState(lvalCol); + + buffer.putShort(lvalCol.getColumnNumber()); + + // owned pages umap (both are on same page) + buffer.put(colState.getUmapOwnedRowNumber()); + ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); + // free space pages umap + buffer.put(colState.getUmapFreeRowNumber()); + ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); + } + /** * Reads the sort order info from the given buffer from the given position. */ diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DBMutator.java b/src/main/java/com/healthmarketscience/jackcess/impl/DBMutator.java new file mode 100644 index 0000000..b16a058 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DBMutator.java @@ -0,0 +1,68 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl; + +import java.io.IOException; +import java.nio.charset.Charset; + + +/** + * Common helper class used to maintain state during database mutation. + * + * @author James Ahlborn + */ +abstract class DBMutator +{ + private final DatabaseImpl _database; + + protected DBMutator(DatabaseImpl database) { + _database = database; + } + + public DatabaseImpl getDatabase() { + return _database; + } + + public JetFormat getFormat() { + return _database.getFormat(); + } + + public PageChannel getPageChannel() { + return _database.getPageChannel(); + } + + public Charset getCharset() { + return _database.getCharset(); + } + + public int reservePageNumber() throws IOException { + return getPageChannel().allocateNewPage(); + } + + public static int calculateNameLength(String name) { + return (name.length() * JetFormat.TEXT_FIELD_UNIT_SIZE) + 2; + } + + protected ColumnImpl.SortOrder getDbSortOrder() { + try { + return _database.getDefaultSortOrder(); + } catch(IOException e) { + // ignored, just use the jet format default + } + return null; + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index 99500dd..8ed8f57 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -53,6 +53,7 @@ import com.healthmarketscience.jackcess.CursorBuilder; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.Database; import com.healthmarketscience.jackcess.DatabaseBuilder; +import com.healthmarketscience.jackcess.Index; import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.IndexCursor; import com.healthmarketscience.jackcess.PropertyMap; @@ -60,6 +61,7 @@ import com.healthmarketscience.jackcess.Relationship; import com.healthmarketscience.jackcess.Row; import com.healthmarketscience.jackcess.RuntimeIOException; import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableBuilder; import com.healthmarketscience.jackcess.TableMetaData; import com.healthmarketscience.jackcess.impl.query.QueryImpl; import com.healthmarketscience.jackcess.query.Query; @@ -85,11 +87,8 @@ public class DatabaseImpl implements Database /** this is the default "userId" used if we cannot find existing info. this seems to be some standard "Admin" userId for access files */ - private static final byte[] SYS_DEFAULT_SID = new byte[2]; - static { - SYS_DEFAULT_SID[0] = (byte) 0xA6; - SYS_DEFAULT_SID[1] = (byte) 0x33; - } + private static final byte[] SYS_DEFAULT_SID = new byte[] { + (byte) 0xA6, (byte) 0x33}; /** the default value for the resource path used to load classpath * resources. @@ -204,9 +203,7 @@ public class DatabaseImpl implements Database /** Name of the system object that is the parent of all databases */ private static final String SYSTEM_OBJECT_NAME_DATABASES = "Databases"; /** Name of the system object that is the parent of all relationships */ - @SuppressWarnings("unused") - private static final String SYSTEM_OBJECT_NAME_RELATIONSHIPS = - "Relationships"; + private static final String SYSTEM_OBJECT_NAME_RELATIONSHIPS = "Relationships"; /** Name of the table that contains system access control entries */ private static final String TABLE_SYSTEM_ACES = "MSysACEs"; /** Name of the table that contains table relationships */ @@ -227,6 +224,8 @@ public class DatabaseImpl implements Database private static final Short TYPE_QUERY = 5; /** System object type for linked table definitions */ private static final Short TYPE_LINKED_TABLE = 6; + /** System object type for relationships */ + private static final Short TYPE_RELATIONSHIP = 8; /** max number of table lookups to cache */ private static final int MAX_CACHED_LOOKUP_TABLES = 50; @@ -281,6 +280,10 @@ public class DatabaseImpl implements Database private TableFinder _tableFinder; /** System access control entries table (initialized on first use) */ private TableImpl _accessControlEntries; + /** ID of the Relationships system object */ + private Integer _relParentId; + /** SIDs to use for the ACEs added for new relationships */ + private final List<byte[]> _newRelSIDs = new ArrayList<byte[]>(); /** System relationships table (initialized on first use) */ private TableImpl _relationships; /** System queries table (initialized on first use) */ @@ -315,6 +318,8 @@ public class DatabaseImpl implements Database private PropertyMaps.Handler _propsHandler; /** ID of the Databases system object */ private Integer _dbParentId; + /** owner of objects we create */ + private byte[] _newObjOwner; /** core database properties */ private PropertyMaps _dbPropMaps; /** summary properties */ @@ -1016,8 +1021,9 @@ public class DatabaseImpl implements Database * Create a new table in this database * @param name Name of the table to create * @param columns List of Columns in the table - * @usage _general_method_ + * @deprecated use {@link TableBuilder} instead */ + @Deprecated public void createTable(String name, List<ColumnBuilder> columns) throws IOException { @@ -1029,18 +1035,17 @@ public class DatabaseImpl implements 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 - * @usage _general_method_ + * @deprecated use {@link TableBuilder} instead */ + @Deprecated public void createTable(String name, List<ColumnBuilder> columns, List<IndexBuilder> indexes) throws IOException { - if(lookupTable(name) != null) { - throw new IllegalArgumentException(withErrorContext( - "Cannot create table with name of existing table '" + name + "'")); - } - - new TableCreator(this, name, columns, indexes).createTable(); + new TableBuilder(name) + .addColumns(columns) + .addIndexes(indexes) + .toTable(this); } public void createLinkedTable(String name, String linkedDbName, @@ -1085,8 +1090,8 @@ public class DatabaseImpl implements Database //Add this table to system tables addToSystemCatalog(name, tdefPageNumber, type, linkedDbName, - linkedTableName); - addToAccessControlEntries(tdefPageNumber); + linkedTableName, _tableParentId); + addToAccessControlEntries(tdefPageNumber, _tableParentId, _newTableSIDs); } public List<Relationship> getRelationships(Table table1, Table table2) @@ -1143,20 +1148,17 @@ public class DatabaseImpl implements Database TableImpl table1, TableImpl table2, boolean includeSystemTables) throws IOException { - // the relationships table does not get loaded until first accessed - if(_relationships == null) { - _relationships = getRequiredSystemTable(TABLE_SYSTEM_RELATIONSHIPS); - } - + initRelationships(); + List<Relationship> relationships = new ArrayList<Relationship>(); if(table1 != null) { - Cursor cursor = createCursorWithOptionalIndex( - _relationships, REL_COL_FROM_TABLE, table1.getName()); + Cursor cursor = createCursorWithOptionalIndex( + _relationships, REL_COL_FROM_TABLE, table1.getName()); collectRelationships(cursor, table1, table2, relationships, includeSystemTables); - cursor = createCursorWithOptionalIndex( - _relationships, REL_COL_TO_TABLE, table1.getName()); + cursor = createCursorWithOptionalIndex( + _relationships, REL_COL_TO_TABLE, table1.getName()); collectRelationships(cursor, table2, table1, relationships, includeSystemTables); } else { @@ -1167,6 +1169,114 @@ public class DatabaseImpl implements Database return relationships; } + RelationshipImpl writeRelationship(RelationshipCreator creator) + throws IOException + { + initRelationships(); + + String name = createRelationshipName(creator); + RelationshipImpl newRel = creator.createRelationshipImpl(name); + + ColumnImpl ccol = _relationships.getColumn(REL_COL_COLUMN_COUNT); + ColumnImpl flagCol = _relationships.getColumn(REL_COL_FLAGS); + ColumnImpl icol = _relationships.getColumn(REL_COL_COLUMN_INDEX); + ColumnImpl nameCol = _relationships.getColumn(REL_COL_NAME); + ColumnImpl fromTableCol = _relationships.getColumn(REL_COL_FROM_TABLE); + ColumnImpl fromColCol = _relationships.getColumn(REL_COL_FROM_COLUMN); + ColumnImpl toTableCol = _relationships.getColumn(REL_COL_TO_TABLE); + ColumnImpl toColCol = _relationships.getColumn(REL_COL_TO_COLUMN); + + int numCols = newRel.getFromColumns().size(); + List<Object[]> rows = new ArrayList<Object[]>(numCols); + for(int i = 0; i < numCols; ++i) { + Object[] row = new Object[_relationships.getColumnCount()]; + ccol.setRowValue(row, numCols); + flagCol.setRowValue(row, newRel.getFlags()); + icol.setRowValue(row, i); + nameCol.setRowValue(row, name); + fromTableCol.setRowValue(row, newRel.getFromTable().getName()); + fromColCol.setRowValue(row, newRel.getFromColumns().get(i).getName()); + toTableCol.setRowValue(row, newRel.getToTable().getName()); + toColCol.setRowValue(row, newRel.getToColumns().get(i).getName()); + rows.add(row); + } + + getPageChannel().startWrite(); + try { + + int relObjId = _tableFinder.getNextFreeSyntheticId(); + _relationships.addRows(rows); + addToSystemCatalog(name, relObjId, TYPE_RELATIONSHIP, null, null, + _relParentId); + addToAccessControlEntries(relObjId, _relParentId, _newRelSIDs); + + } finally { + getPageChannel().finishWrite(); + } + + return newRel; + } + + private void initRelationships() throws IOException { + // the relationships table does not get loaded until first accessed + if(_relationships == null) { + // need the parent id of the relationships objects + _relParentId = _tableFinder.findObjectId(DB_PARENT_ID, + SYSTEM_OBJECT_NAME_RELATIONSHIPS); + _relationships = getRequiredSystemTable(TABLE_SYSTEM_RELATIONSHIPS); + } + } + + private String createRelationshipName(RelationshipCreator creator) + throws IOException + { + // ensure that the final identifier name does not get too long + // - the primary name is limited to ((max / 2) - 3) + // - the total name is limited to (max - 3) + int maxIdLen = getFormat().MAX_INDEX_NAME_LENGTH; + int limit = (maxIdLen / 2) - 3; + String origName = creator.getPrimaryTable().getName(); + if(origName.length() > limit) { + origName = origName.substring(0, limit); + } + limit = maxIdLen - 3; + origName += creator.getSecondaryTable().getName(); + if(origName.length() > limit) { + origName = origName.substring(0, limit); + } + + // now ensure name is unique + Set<String> names = new HashSet<String>(); + + // collect the names of all relationships for uniqueness check + for(Row row : + CursorImpl.createCursor(_systemCatalog).newIterable().setColumnNames( + SYSTEM_CATALOG_COLUMNS)) + { + String name = row.getString(CAT_COL_NAME); + if (name != null && TYPE_RELATIONSHIP.equals(row.get(CAT_COL_TYPE))) { + names.add(toLookupName(name)); + } + } + + if(creator.hasReferentialIntegrity()) { + // relationship name will also be index name in secondary table, so must + // check those names as well + for(Index idx : creator.getSecondaryTable().getIndexes()) { + names.add(toLookupName(idx.getName())); + } + } + + String baseName = toLookupName(origName); + String name = baseName; + int i = 0; + while(names.contains(name)) { + name = baseName + (++i); + } + + return ((i == 0) ? origName : (origName + i)); + } + public List<Query> getQueries() throws IOException { // the queries table does not get loaded until first accessed @@ -1270,14 +1380,9 @@ public class DatabaseImpl implements Database return readProperties(propsBytes, objectId, rowId); } - /** - * @return property group for the given "database" object - */ - private PropertyMaps getPropertiesForDbObject(String dbName) - throws IOException - { + private Integer getDbParentId() throws IOException { if(_dbParentId == null) { - // need the parent if of the databases objects + // need the parent id of the databases objects _dbParentId = _tableFinder.findObjectId(DB_PARENT_ID, SYSTEM_OBJECT_NAME_DATABASES); if(_dbParentId == null) { @@ -1285,9 +1390,36 @@ public class DatabaseImpl implements Database "Did not find required parent db id")); } } + return _dbParentId; + } + + private byte[] getNewObjectOwner() throws IOException { + if(_newObjOwner == null) { + // there doesn't seem to be any obvious way to find the main "owner" of + // an access db, but certain db objects seem to have the common db + // owner. we attempt to grab the db properties object and use its + // owner. + Row msysDbRow = _tableFinder.getObjectRow( + getDbParentId(), OBJECT_NAME_DB_PROPS, + Collections.singleton(CAT_COL_OWNER)); + byte[] owner = null; + if(msysDbRow != null) { + owner = msysDbRow.getBytes(CAT_COL_OWNER); + } + _newObjOwner = (((owner != null) && (owner.length > 0)) ? + owner : SYS_DEFAULT_SID); + } + return _newObjOwner; + } + /** + * @return property group for the given "database" object + */ + private PropertyMaps getPropertiesForDbObject(String dbName) + throws IOException + { Row objectRow = _tableFinder.getObjectRow( - _dbParentId, dbName, SYSTEM_CATALOG_PROPS_COLUMNS); + getDbParentId(), dbName, SYSTEM_CATALOG_PROPS_COLUMNS); byte[] propsBytes = null; int objectId = -1; RowIdImpl rowId = null; @@ -1420,12 +1552,14 @@ public class DatabaseImpl implements Database /** * Add a new table to the system catalog * @param name Table name - * @param pageNumber Page number that contains the table definition + * @param objectId id of the new object */ - private void addToSystemCatalog(String name, int pageNumber, Short type, - String linkedDbName, String linkedTableName) + private void addToSystemCatalog(String name, int objectId, Short type, + String linkedDbName, String linkedTableName, + Integer parentId) throws IOException { + byte[] owner = getNewObjectOwner(); Object[] catalogRow = new Object[_systemCatalog.getColumnCount()]; int idx = 0; Date creationTime = new Date(); @@ -1434,7 +1568,7 @@ public class DatabaseImpl implements Database { ColumnImpl col = iter.next(); if (CAT_COL_ID.equals(col.getName())) { - catalogRow[idx] = Integer.valueOf(pageNumber); + catalogRow[idx] = Integer.valueOf(objectId); } else if (CAT_COL_NAME.equals(col.getName())) { catalogRow[idx] = name; } else if (CAT_COL_TYPE.equals(col.getName())) { @@ -1443,14 +1577,11 @@ public class DatabaseImpl implements Database CAT_COL_DATE_UPDATE.equals(col.getName())) { catalogRow[idx] = creationTime; } else if (CAT_COL_PARENT_ID.equals(col.getName())) { - catalogRow[idx] = _tableParentId; + catalogRow[idx] = parentId; } else if (CAT_COL_FLAGS.equals(col.getName())) { catalogRow[idx] = Integer.valueOf(0); } else if (CAT_COL_OWNER.equals(col.getName())) { - byte[] owner = new byte[2]; catalogRow[idx] = owner; - owner[0] = (byte) 0xcf; - owner[1] = (byte) 0x5f; } else if (CAT_COL_DATABASE.equals(col.getName())) { catalogRow[idx] = linkedDbName; } else if (CAT_COL_FOREIGN_NAME.equals(col.getName())) { @@ -1459,15 +1590,16 @@ public class DatabaseImpl implements Database } _systemCatalog.addRow(catalogRow); } - + /** - * Add a new table to the system's access control entries - * @param pageNumber Page number that contains the table definition + * Adds a new object to the system's access control entries */ - private void addToAccessControlEntries(int pageNumber) throws IOException { - - if(_newTableSIDs.isEmpty()) { - initNewTableSIDs(); + private void addToAccessControlEntries( + Integer objectId, Integer parentId, List<byte[]> sids) + throws IOException + { + if(sids.isEmpty()) { + collectNewObjectSIDs(parentId, sids); } TableImpl acEntries = getAccessControlEntries(); @@ -1476,14 +1608,13 @@ public class DatabaseImpl implements Database ColumnImpl objIdCol = acEntries.getColumn(ACE_COL_OBJECT_ID); ColumnImpl sidCol = acEntries.getColumn(ACE_COL_SID); - // construct a collection of ACE entries mimicing those of our parent, the - // "Tables" system object - List<Object[]> aceRows = new ArrayList<Object[]>(_newTableSIDs.size()); - for(byte[] sid : _newTableSIDs) { + // construct a collection of ACE entries + List<Object[]> aceRows = new ArrayList<Object[]>(sids.size()); + for(byte[] sid : sids) { Object[] aceRow = new Object[acEntries.getColumnCount()]; acmCol.setRowValue(aceRow, SYS_FULL_ACCESS_ACM); inheritCol.setRowValue(aceRow, Boolean.FALSE); - objIdCol.setRowValue(aceRow, Integer.valueOf(pageNumber)); + objIdCol.setRowValue(aceRow, objectId); sidCol.setRowValue(aceRow, sid); aceRows.add(aceRow); } @@ -1491,25 +1622,26 @@ public class DatabaseImpl implements Database } /** - * Determines the collection of SIDs which need to be added to new tables. + * Find collection of SIDs for the given parent id. */ - private void initNewTableSIDs() throws IOException + private void collectNewObjectSIDs(Integer parentId, List<byte[]> sids) + throws IOException { - // search for ACEs matching the tableParentId. use the index on the + // search for ACEs matching the given parentId. use the index on the // objectId column if found (should be there) Cursor cursor = createCursorWithOptionalIndex( - getAccessControlEntries(), ACE_COL_OBJECT_ID, _tableParentId); + getAccessControlEntries(), ACE_COL_OBJECT_ID, parentId); for(Row row : cursor) { Integer objId = row.getInt(ACE_COL_OBJECT_ID); - if(_tableParentId.equals(objId)) { - _newTableSIDs.add(row.getBytes(ACE_COL_SID)); + if(parentId.equals(objId)) { + sids.add(row.getBytes(ACE_COL_SID)); } } - if(_newTableSIDs.isEmpty()) { + if(sids.isEmpty()) { // if all else fails, use the hard-coded default - _newTableSIDs.add(SYS_DEFAULT_SID); + sids.add(SYS_DEFAULT_SID); } } @@ -1582,6 +1714,15 @@ public class DatabaseImpl implements Database } _pageChannel.close(); } + + public void validateNewTableName(String name) throws IOException { + if(lookupTable(name) != null) { + throw new IllegalArgumentException(withErrorContext( + "Cannot create table with name of existing table '" + name + "'")); + } + + validateIdentifierName(name, getFormat().MAX_TABLE_NAME_LENGTH, "table"); + } /** * Validates an identifier name. diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java b/src/main/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java index d2d4c50..d29836a 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java @@ -50,7 +50,7 @@ final class FKEnforcer CaseInsensitiveColumnMatcher.INSTANCE; private final TableImpl _table; - private final List<ColumnImpl> _cols; + private List<ColumnImpl> _cols; private List<Joiner> _primaryJoinersChkUp; private List<Joiner> _primaryJoinersChkDel; private List<Joiner> _primaryJoinersDoUp; @@ -62,6 +62,10 @@ final class FKEnforcer _table = table; // at this point, only init the index columns + initColumns(); + } + + private void initColumns() { Set<ColumnImpl> cols = new TreeSet<ColumnImpl>(); for(IndexImpl idx : _table.getIndexes()) { IndexImpl.ForeignKeyReference ref = idx.getReference(); @@ -79,6 +83,22 @@ final class FKEnforcer } /** + * Resets the internals of this FKEnforcer (for post-table modification) + */ + void reset() { + // columns to enforce may have changed + initColumns(); + + // clear any existing joiners (will be re-created on next use) + _primaryJoinersChkUp = null; + _primaryJoinersChkDel = null; + _primaryJoinersDoUp = null; + _primaryJoinersDoDel = null; + _primaryJoinersDoNull = null; + _secondaryJoiners = null; + } + + /** * Does secondary initialization, if necessary. */ private void initialize() throws IOException { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java index 3cff2e7..a55259e 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java @@ -21,12 +21,12 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; -import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.ConstraintViolationException; import com.healthmarketscience.jackcess.Index; import com.healthmarketscience.jackcess.IndexBuilder; @@ -372,6 +372,10 @@ public class IndexData { _ownedPages.addPageNumber(pageNumber); } + void collectUsageMapPages(Collection<Integer> pages) { + pages.add(_ownedPages.getTablePageNumber()); + } + /** * Used by unit tests to validate the internal status of the index. * @usage _advanced_method_ @@ -478,8 +482,20 @@ public class IndexData { protected static void writeRowCountDefinitions( TableCreator creator, ByteBuffer buffer) { + writeRowCountDefinitions(creator, buffer, creator.getIndexCount()); + } + + /** + * Writes the index row count definitions into a table definition buffer. + * @param creator description of the indexes to write + * @param buffer Buffer to write to + * @param idxCount num indexes to write + */ + protected static void writeRowCountDefinitions( + TableMutator creator, ByteBuffer buffer, int idxCount) + { // index row counts (empty data) - ByteUtil.forward(buffer, (creator.getIndexCount() * + ByteUtil.forward(buffer, (idxCount * creator.getFormat().SIZE_INDEX_DEFINITION)); } @@ -492,60 +508,78 @@ public class IndexData { TableCreator creator, ByteBuffer buffer) throws IOException { - ByteBuffer rootPageBuffer = creator.getPageChannel().createPageBuffer(); - writeDataPage(rootPageBuffer, NEW_ROOT_DATA_PAGE, - creator.getTdefPageNumber(), creator.getFormat()); + ByteBuffer rootPageBuffer = createRootPageBuffer(creator); - for(IndexBuilder idx : creator.getIndexes()) { - buffer.putInt(MAGIC_INDEX_NUMBER); // seemingly constant magic value + for(TableMutator.IndexDataState idxDataState : creator.getIndexDataStates()) { + writeDefinition(creator, buffer, idxDataState, rootPageBuffer); + } + } - // write column information (always MAX_COLUMNS entries) - List<IndexBuilder.Column> idxColumns = idx.getColumns(); - for(int i = 0; i < MAX_COLUMNS; ++i) { + /** + * Writes the index definitions into a table definition buffer. + * @param creator description of the indexes to write + * @param buffer Buffer to write to + */ + protected static void writeDefinition( + TableMutator creator, ByteBuffer buffer, + TableMutator.IndexDataState idxDataState, ByteBuffer rootPageBuffer) + throws IOException + { + if(rootPageBuffer == null) { + rootPageBuffer = createRootPageBuffer(creator); + } - short columnNumber = COLUMN_UNUSED; - byte flags = 0; + buffer.putInt(MAGIC_INDEX_NUMBER); // seemingly constant magic value - if(i < idxColumns.size()) { + // write column information (always MAX_COLUMNS entries) + IndexBuilder idx = idxDataState.getFirstIndex(); + List<IndexBuilder.Column> idxColumns = idx.getColumns(); + for(int i = 0; i < MAX_COLUMNS; ++i) { - // determine column info - IndexBuilder.Column idxCol = idxColumns.get(i); - flags = idxCol.getFlags(); + short columnNumber = COLUMN_UNUSED; + byte flags = 0; - // find actual table column number - for(ColumnBuilder col : creator.getColumns()) { - 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( - withErrorContext( - "Column with name " + idxCol.getName() + " not found", - creator.getDatabase(), creator.getName(), idx.getName())); - } + if(i < idxColumns.size()) { + + // determine column info + IndexBuilder.Column idxCol = idxColumns.get(i); + flags = idxCol.getFlags(); + + // find actual table column number + columnNumber = creator.getColumnNumber(idxCol.getName()); + if(columnNumber == COLUMN_UNUSED) { + // should never happen as this is validated before + throw new IllegalArgumentException( + withErrorContext( + "Column with name " + idxCol.getName() + " not found", + creator.getDatabase(), creator.getTableName(), idx.getName())); } - - buffer.putShort(columnNumber); // table column number - buffer.put(flags); // column flags (e.g. ordering) } + + buffer.putShort(columnNumber); // table column number + buffer.put(flags); // column flags (e.g. ordering) + } - TableCreator.IndexState idxState = creator.getIndexState(idx); + buffer.put(idxDataState.getUmapRowNumber()); // umap row + ByteUtil.put3ByteInt(buffer, idxDataState.getUmapPageNumber()); // umap page - buffer.put(idxState.getUmapRowNumber()); // umap row - ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); // umap page + // write empty root index page + creator.getPageChannel().writePage(rootPageBuffer, + idxDataState.getRootPageNumber()); - // write empty root index page - creator.getPageChannel().writePage(rootPageBuffer, - idxState.getRootPageNumber()); + buffer.putInt(idxDataState.getRootPageNumber()); + buffer.putInt(0); // unknown + buffer.put(idx.getFlags()); // index flags (unique, etc.) + ByteUtil.forward(buffer, 5); // unknown + } - buffer.putInt(idxState.getRootPageNumber()); - buffer.putInt(0); // unknown - buffer.put(idx.getFlags()); // index flags (unique, etc.) - ByteUtil.forward(buffer, 5); // unknown - } + private static ByteBuffer createRootPageBuffer(TableMutator creator) + throws IOException + { + ByteBuffer rootPageBuffer = creator.getPageChannel().createPageBuffer(); + writeDataPage(rootPageBuffer, NEW_ROOT_DATA_PAGE, + creator.getTdefPageNumber(), creator.getFormat()); + return rootPageBuffer; } /** diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java index f3fe868..8cdb54c 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java @@ -54,7 +54,9 @@ public class IndexImpl implements Index, Comparable<IndexImpl> private static final byte CASCADE_NULL_FLAG = (byte)2; /** index table type for the "primary" table in a foreign key index */ - private static final byte PRIMARY_TABLE_TYPE = (byte)1; + static final byte FK_PRIMARY_TABLE_TYPE = (byte)1; + /** index table type for the "secondary" table in a foreign key index */ + static final byte FK_SECONDARY_TABLE_TYPE = (byte)2; /** indicate an invalid index number for foreign key field */ private static final int INVALID_INDEX_NUMBER = -1; @@ -75,7 +77,6 @@ public class IndexImpl implements Index, Comparable<IndexImpl> JetFormat format) throws IOException { - ByteUtil.forward(tableBuffer, format.SKIP_BEFORE_INDEX_SLOT); //Forward past Unknown _indexNumber = tableBuffer.getInt(); int indexDataNumber = tableBuffer.getInt(); @@ -342,17 +343,7 @@ public class IndexImpl implements Index, Comparable<IndexImpl> { // write logical index information for(IndexBuilder idx : creator.getIndexes()) { - TableCreator.IndexState idxState = creator.getIndexState(idx); - buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def - buffer.putInt(idxState.getIndexNumber()); // index num - buffer.putInt(idxState.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.getType()); // index type flags - buffer.putInt(0); // unknown + writeDefinition(creator, idx, buffer); } // write index names @@ -361,6 +352,47 @@ public class IndexImpl implements Index, Comparable<IndexImpl> } } + protected static void writeDefinition( + TableMutator mutator, IndexBuilder idx, ByteBuffer buffer) + throws IOException + { + TableMutator.IndexDataState idxDataState = mutator.getIndexDataState(idx); + + // write logical index information + buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def + buffer.putInt(idx.getIndexNumber()); // index num + buffer.putInt(idxDataState.getIndexDataNumber()); // index data num + + byte idxType = idx.getType(); + if(idxType != FOREIGN_KEY_INDEX_TYPE) { + 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 + } else { + ForeignKeyReference reference = mutator.getForeignKey(idx); + buffer.put(reference.getTableType()); // related table type + buffer.putInt(reference.getOtherIndexNumber()); // related index num + buffer.putInt(reference.getOtherTablePageNumber()); // related table definition page number + byte updateFlags = 0; + if(reference.isCascadeUpdates()) { + updateFlags |= CASCADE_UPDATES_FLAG; + } + byte deleteFlags = 0; + if(reference.isCascadeDeletes()) { + deleteFlags |= CASCADE_DELETES_FLAG; + } + if(reference.isCascadeNullOnDelete()) { + deleteFlags |= CASCADE_NULL_FLAG; + } + buffer.put(updateFlags); // cascade updates flag + buffer.put(deleteFlags); // cascade deletes flag + } + buffer.put(idxType); // index type flags + buffer.putInt(0); // unknown + } + private String withErrorContext(String msg) { return withErrorContext(msg, getTable().getDatabase(), getName()); } @@ -401,7 +433,7 @@ public class IndexImpl implements Index, Comparable<IndexImpl> } public boolean isPrimaryTable() { - return(getTableType() == PRIMARY_TABLE_TYPE); + return(getTableType() == FK_PRIMARY_TABLE_TYPE); } public int getOtherIndexNumber() { diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java b/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java index eed8832..ba848e4 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java @@ -672,7 +672,7 @@ public abstract class JetFormat { @Override protected int defineMaxCompressedUnicodeSize() { return 1024; } @Override - protected int defineSizeTdefHeader() { return 63; } + protected int defineSizeTdefHeader() { return 43; } @Override protected int defineSizeTdefTrailer() { return 2; } @Override diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java index a42da54..8a137ea 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java @@ -17,8 +17,10 @@ limitations under the License. package com.healthmarketscience.jackcess.impl; import java.io.IOException; +import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.Collection; /** @@ -70,6 +72,11 @@ class LongValueColumnImpl extends ColumnImpl } @Override + void collectUsageMapPages(Collection<Integer> pages) { + _lvalBufferH.collectUsageMapPages(pages); + } + + @Override void postTableLoadInit() throws IOException { if(_lvalBufferH == null) { _lvalBufferH = new LegacyLongValueBufferHolder(); @@ -440,6 +447,10 @@ class LongValueColumnImpl extends ColumnImpl getBufferHolder().clear(); } + public void collectUsageMapPages(Collection<Integer> pages) { + // base does nothing + } + protected abstract TempPageHolder getBufferHolder(); } @@ -516,5 +527,11 @@ class LongValueColumnImpl extends ColumnImpl } super.clear(); } + + @Override + public void collectUsageMapPages(Collection<Integer> pages) { + pages.add(_ownedPages.getTablePageNumber()); + pages.add(_freeSpacePages.getTablePageNumber()); + } } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java b/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java index cf06e49..00dabbe 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java @@ -135,6 +135,19 @@ public class PageChannel implements Channel, Flushable { } /** + * Begins an exclusive "logical" write operation (throws an exception if + * another write operation is outstanding). See {@link #finishWrite} for + * more details. + */ + public void startExclusiveWrite() { + if(_writeCount != 0) { + throw new IllegalArgumentException( + "Another write operation is currently in progress"); + } + startWrite(); + } + + /** * Completes a "logical" write operation. This method should be called in * finally block which wraps a logical write operation (which is preceded by * a {@link #startWrite} call). Logical write operations may be nested. If diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java b/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java new file mode 100644 index 0000000..aee71ae --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java @@ -0,0 +1,346 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.healthmarketscience.jackcess.ConstraintViolationException; +import com.healthmarketscience.jackcess.IndexBuilder; +import com.healthmarketscience.jackcess.IndexCursor; +import com.healthmarketscience.jackcess.RelationshipBuilder; +import com.healthmarketscience.jackcess.Row; + +/** + * Helper class used to maintain state during relationship creation. + * + * @author James Ahlborn + */ +public class RelationshipCreator extends DBMutator +{ + private final static int CASCADE_FLAGS = + RelationshipImpl.CASCADE_DELETES_FLAG | + RelationshipImpl.CASCADE_UPDATES_FLAG | + RelationshipImpl.CASCADE_NULL_FLAG; + + // for the purposes of choosing a backing index for a foreign key, there are + // certain index flags that can be ignored (we don't care how they are set) + private final static byte IGNORED_PRIMARY_INDEX_FLAGS = + IndexData.IGNORE_NULLS_INDEX_FLAG | IndexData.REQUIRED_INDEX_FLAG; + private final static byte IGNORED_SECONDARY_INDEX_FLAGS = + IGNORED_PRIMARY_INDEX_FLAGS | IndexData.UNIQUE_INDEX_FLAG; + + private TableImpl _primaryTable; + private TableImpl _secondaryTable; + private RelationshipBuilder _relationship; + private List<ColumnImpl> _primaryCols; + private List<ColumnImpl> _secondaryCols; + private int _flags; + private String _name; + + public RelationshipCreator(DatabaseImpl database) + { + super(database); + } + + public TableImpl getPrimaryTable() { + return _primaryTable; + } + + public TableImpl getSecondaryTable() { + return _secondaryTable; + } + + public boolean hasReferentialIntegrity() { + return _relationship.hasReferentialIntegrity(); + } + + public RelationshipImpl createRelationshipImpl(String name) { + _name = name; + RelationshipImpl newRel = new RelationshipImpl( + name, _primaryTable, _secondaryTable, _flags, + _primaryCols, _secondaryCols); + return newRel; + } + + /** + * Creates the relationship in the database. + * @usage _advanced_method_ + */ + public RelationshipImpl createRelationship(RelationshipBuilder relationship) + throws IOException + { + _relationship = relationship; + + validate(); + + _flags = _relationship.getFlags(); + // need to determine the one-to-one flag on our own + if(isOneToOne()) { + _flags |= RelationshipImpl.ONE_TO_ONE_FLAG; + } + + getPageChannel().startExclusiveWrite(); + try { + + RelationshipImpl newRel = getDatabase().writeRelationship(this); + + if(hasReferentialIntegrity()) { + addPrimaryIndex(); + addSecondaryIndex(); + } + + return newRel; + + } finally { + getPageChannel().finishWrite(); + } + } + + private void addPrimaryIndex() throws IOException { + TableUpdater updater = new TableUpdater(_primaryTable); + updater.setForeignKey(createFKReference(true)); + updater.addIndex(createPrimaryIndex(), true, + IGNORED_PRIMARY_INDEX_FLAGS, (byte)0); + } + + private void addSecondaryIndex() throws IOException { + TableUpdater updater = new TableUpdater(_secondaryTable); + updater.setForeignKey(createFKReference(false)); + updater.addIndex(createSecondaryIndex(), true, + IGNORED_SECONDARY_INDEX_FLAGS, (byte)0); + } + + private IndexImpl.ForeignKeyReference createFKReference(boolean isPrimary) { + byte tableType = 0; + int otherTableNum = 0; + int otherIdxNum = 0; + if(isPrimary) { + tableType = IndexImpl.FK_PRIMARY_TABLE_TYPE; + otherTableNum = _secondaryTable.getTableDefPageNumber(); + // we create the primary index first, so the secondary index does not + // exist yet + otherIdxNum = _secondaryTable.getLogicalIndexCount(); + } else { + tableType = IndexImpl.FK_SECONDARY_TABLE_TYPE; + otherTableNum = _primaryTable.getTableDefPageNumber(); + // at this point, we've already created the primary index, it's the last + // one on the primary table + otherIdxNum = _primaryTable.getLogicalIndexCount() - 1; + } + boolean cascadeUpdates = ((_flags & RelationshipImpl.CASCADE_UPDATES_FLAG) != 0); + boolean cascadeDeletes = ((_flags & RelationshipImpl.CASCADE_DELETES_FLAG) != 0); + boolean cascadeNull = ((_flags & RelationshipImpl.CASCADE_NULL_FLAG) != 0); + + return new IndexImpl.ForeignKeyReference( + tableType, otherIdxNum, otherTableNum, cascadeUpdates, cascadeDeletes, + cascadeNull); + } + + private void validate() throws IOException { + + _primaryTable = getDatabase().getTable(_relationship.getFromTable()); + _secondaryTable = getDatabase().getTable(_relationship.getToTable()); + + if((_primaryTable == null) || (_secondaryTable == null)) { + throw new IllegalArgumentException(withErrorContext( + "Two valid tables are required in relationship")); + } + + _primaryCols = getColumns(_primaryTable, _relationship.getFromColumns()); + _secondaryCols = getColumns(_secondaryTable, _relationship.getToColumns()); + + if((_primaryCols == null) || (_primaryCols.isEmpty()) || + (_secondaryCols == null) || (_secondaryCols.isEmpty())) { + throw new IllegalArgumentException(withErrorContext( + "Missing columns in relationship")); + } + + if(_primaryCols.size() != _secondaryCols.size()) { + throw new IllegalArgumentException(withErrorContext( + "Must have same number of columns on each side of relationship")); + } + + for(int i = 0; i < _primaryCols.size(); ++i) { + ColumnImpl pcol = _primaryCols.get(i); + ColumnImpl scol = _primaryCols.get(i); + + if(pcol.getType() != scol.getType()) { + throw new IllegalArgumentException(withErrorContext( + "Matched columns must have the same data type")); + } + } + + if(!hasReferentialIntegrity()) { + + if((_relationship.getFlags() & CASCADE_FLAGS) != 0) { + throw new IllegalArgumentException(withErrorContext( + "Cascade flags cannot be enabled if referential integrity is not enforced")); + } + + return; + } + + // for now, we will require the unique index on the primary table (just + // like access does). we could just create it auto-magically... + IndexImpl primaryIdx = getPrimaryUniqueIndex(); + if(primaryIdx == null) { + throw new IllegalArgumentException(withErrorContext( + "Missing unique index on primary table required to enforce integrity")); + } + + // while relationships can have "dupe" columns, indexes (and therefore + // integrity enforced relationships) cannot + if((new HashSet<String>(getColumnNames(_primaryCols)).size() != + _primaryCols.size()) || + (new HashSet<String>(getColumnNames(_secondaryCols)).size() != + _secondaryCols.size())) { + throw new IllegalArgumentException(withErrorContext( + "Cannot have duplicate columns in an integrity enforced relationship")); + } + + // TODO: future, check for enforce cycles? + + // check referential integrity + IndexCursor primaryCursor = primaryIdx.newCursor().toIndexCursor(); + Object[] entryValues = new Object[_secondaryCols.size()]; + for(Row row : _secondaryTable.newCursor().toCursor() + .newIterable().addColumns(_secondaryCols)) { + // grab the secondary table values + boolean hasValues = false; + for(int i = 0; i < _secondaryCols.size(); ++i) { + entryValues[i] = _secondaryCols.get(i).getRowValue(row); + hasValues = hasValues || (entryValues[i] != null); + } + + if(!hasValues) { + // we can ignore null entries + continue; + } + + // check that they exist in the primary table + if(!primaryCursor.findFirstRowByEntry(entryValues)) { + throw new ConstraintViolationException(withErrorContext( + "Integrity constraint violation found for relationship")); + } + } + + } + + private IndexBuilder createPrimaryIndex() { + String name = createPrimaryIndexName(); + return createIndex(name, _primaryCols) + .setUnique() + .setType(IndexImpl.FOREIGN_KEY_INDEX_TYPE); + } + + private IndexBuilder createSecondaryIndex() { + // secondary index uses relationship name + return createIndex(_name, _secondaryCols) + .setType(IndexImpl.FOREIGN_KEY_INDEX_TYPE); + } + + private static IndexBuilder createIndex(String name, List<ColumnImpl> cols) { + IndexBuilder idx = new IndexBuilder(name); + for(ColumnImpl col : cols) { + idx.addColumns(col.getName()); + } + return idx; + } + + private String createPrimaryIndexName() { + Set<String> idxNames = TableUpdater.getIndexNames(_primaryTable, null); + + // primary naming scheme: ".rB", .rC", ".rD", "rE" ... + String baseName = ".r"; + String suffix = "B"; + + while(true) { + String idxName = baseName + suffix; + if(!idxNames.contains(DatabaseImpl.toLookupName(idxName))) { + return idxName; + } + + char c = (char)(suffix.charAt(0) + 1); + if(c == '[') { + c = 'a'; + } + suffix = "" + c; + } + } + + private static List<ColumnImpl> getColumns(TableImpl table, + List<String> colNames) { + List<ColumnImpl> cols = new ArrayList<ColumnImpl>(); + for(String colName : colNames) { + cols.add(table.getColumn(colName)); + } + return cols; + } + + private static List<String> getColumnNames(List<ColumnImpl> cols) { + List<String> colNames = new ArrayList<String>(); + for(ColumnImpl col : cols) { + colNames.add(col.getName()); + } + return colNames; + } + + private boolean isOneToOne() { + // a relationship is one to one if the two sides of the relationship have + // unique indexes on the relevant columns + if(getPrimaryUniqueIndex() == null) { + return false; + } + IndexImpl idx = _secondaryTable.findIndexForColumns( + getColumnNames(_secondaryCols), true); + return (idx != null); + } + + private IndexImpl getPrimaryUniqueIndex() { + return _primaryTable.findIndexForColumns(getColumnNames(_primaryCols), true); + } + + private static String getTableErrorContext( + TableImpl table, List<ColumnImpl> cols, + String tableName, Collection<String> colNames) { + if(table != null) { + tableName = table.getName(); + } + if(cols != null) { + colNames = getColumnNames(cols); + } + + return CustomToStringStyle.valueBuilder(tableName) + .append(null, cols) + .toString(); + } + + private String withErrorContext(String msg) { + return msg + "(Rel=" + + getTableErrorContext(_primaryTable, _primaryCols, + _relationship.getFromTable(), + _relationship.getFromColumns()) + " -> " + + getTableErrorContext(_secondaryTable, _secondaryCols, + _relationship.getToTable(), + _relationship.getToColumns()) + ")"; + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java index 8775448..0cc2b90 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java @@ -33,20 +33,20 @@ public class RelationshipImpl implements Relationship { /** flag indicating one-to-one relationship */ - private static final int ONE_TO_ONE_FLAG = 0x00000001; + public static final int ONE_TO_ONE_FLAG = 0x00000001; /** flag indicating no referential integrity */ - private static final int NO_REFERENTIAL_INTEGRITY_FLAG = 0x00000002; + public static final int NO_REFERENTIAL_INTEGRITY_FLAG = 0x00000002; /** flag indicating cascading updates (requires referential integrity) */ - private static final int CASCADE_UPDATES_FLAG = 0x00000100; + public static final int CASCADE_UPDATES_FLAG = 0x00000100; /** flag indicating cascading deletes (requires referential integrity) */ - private static final int CASCADE_DELETES_FLAG = 0x00001000; + public static final int CASCADE_DELETES_FLAG = 0x00001000; /** flag indicating cascading null on delete (requires referential integrity) */ - private static final int CASCADE_NULL_FLAG = 0x00002000; + public static final int CASCADE_NULL_FLAG = 0x00002000; /** flag indicating left outer join */ - private static final int LEFT_OUTER_JOIN_FLAG = 0x01000000; + public static final int LEFT_OUTER_JOIN_FLAG = 0x01000000; /** flag indicating right outer join */ - private static final int RIGHT_OUTER_JOIN_FLAG = 0x02000000; + public static final int RIGHT_OUTER_JOIN_FLAG = 0x02000000; /** the name of this relationship */ private final String _name; @@ -66,13 +66,20 @@ public class RelationshipImpl implements Relationship public RelationshipImpl(String name, Table fromTable, Table toTable, int flags, int numCols) { + this(name, fromTable, toTable, flags, + Collections.nCopies(numCols, (Column)null), + Collections.nCopies(numCols, (Column)null)); + } + + public RelationshipImpl(String name, Table fromTable, Table toTable, int flags, + List<? extends Column> fromCols, + List<? extends Column> toCols) + { _name = name; _fromTable = fromTable; - _fromColumns = new ArrayList<Column>( - Collections.nCopies(numCols, (Column)null)); + _fromColumns = new ArrayList<Column>(fromCols); _toTable = toTable; - _toColumns = new ArrayList<Column>( - Collections.nCopies(numCols, (Column)null)); + _toColumns = new ArrayList<Column>(toCols); _flags = flags; } @@ -127,6 +134,15 @@ public class RelationshipImpl implements Relationship public boolean isRightOuterJoin() { return hasFlag(RIGHT_OUTER_JOIN_FLAG); } + + public JoinType getJoinType() { + if(isLeftOuterJoin()) { + return JoinType.LEFT_OUTER; + } else if(isRightOuterJoin()) { + return JoinType.RIGHT_OUTER; + } + return JoinType.INNER; + } private boolean hasFlag(int flagMask) { return((getFlags() & flagMask) != 0); diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableCreator.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableCreator.java index d20ae38..cb7d129 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableCreator.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableCreator.java @@ -17,7 +17,6 @@ limitations under the License. package com.healthmarketscience.jackcess.impl; import java.io.IOException; -import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; @@ -30,6 +29,8 @@ import java.util.Set; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.IndexBuilder; +import com.healthmarketscience.jackcess.PropertyMap; +import com.healthmarketscience.jackcess.TableBuilder; /** * Helper class used to maintain state during table creation. @@ -37,14 +38,13 @@ import com.healthmarketscience.jackcess.IndexBuilder; * @author James Ahlborn * @usage _advanced_class_ */ -class TableCreator +public class TableCreator extends TableMutator { - private final DatabaseImpl _database; - private final String _name; - private final List<ColumnBuilder> _columns; - private final List<IndexBuilder> _indexes; - private final Map<IndexBuilder,IndexState> _indexStates = - new IdentityHashMap<IndexBuilder,IndexState>(); + private String _name; + private List<ColumnBuilder> _columns; + private List<IndexBuilder> _indexes; + private final List<IndexDataState> _indexDataStates = + new ArrayList<IndexDataState>(); private final Map<ColumnBuilder,ColumnState> _columnStates = new IdentityHashMap<ColumnBuilder,ColumnState>(); private final List<ColumnBuilder> _lvalCols = new ArrayList<ColumnBuilder>(); @@ -53,35 +53,20 @@ class TableCreator private int _indexCount; private int _logicalIndexCount; - public TableCreator(DatabaseImpl database, String name, List<ColumnBuilder> columns, - List<IndexBuilder> indexes) { - _database = database; - _name = name; - _columns = columns; - _indexes = ((indexes != null) ? indexes : - Collections.<IndexBuilder>emptyList()); + public TableCreator(DatabaseImpl database) { + super(database); } public String getName() { return _name; } - public DatabaseImpl getDatabase() { - return _database; + @Override + String getTableName() { + return getName(); } - - public JetFormat getFormat() { - return _database.getFormat(); - } - - public PageChannel getPageChannel() { - return _database.getPageChannel(); - } - - public Charset getCharset() { - return _database.getCharset(); - } - + + @Override public int getTdefPageNumber() { return _tdefPageNumber; } @@ -110,14 +95,24 @@ class TableCreator return _logicalIndexCount; } - public IndexState getIndexState(IndexBuilder idx) { - return _indexStates.get(idx); + @Override + public IndexDataState getIndexDataState(IndexBuilder idx) { + for(IndexDataState idxDataState : _indexDataStates) { + for(IndexBuilder curIdx : idxDataState.getIndexes()) { + if(idx == curIdx) { + return idxDataState; + } + } + } + throw new IllegalStateException(withErrorContext( + "could not find state for index")); } - public int reservePageNumber() throws IOException { - return getPageChannel().allocateNewPage(); + public List<IndexDataState> getIndexDataStates() { + return _indexDataStates; } + @Override public ColumnState getColumnState(ColumnBuilder col) { return _columnStates.get(col); } @@ -126,11 +121,44 @@ class TableCreator return _lvalCols; } + @Override + short getColumnNumber(String colName) { + for(ColumnBuilder col : _columns) { + if(col.getName().equalsIgnoreCase(colName)) { + return col.getColumnNumber(); + } + } + return IndexData.COLUMN_UNUSED; + } + + /** + * @return The number of variable length columns which are not long values + * found in the list + * @usage _advanced_method_ + */ + public short countNonLongVariableLength() { + short rtn = 0; + for (ColumnBuilder col : _columns) { + if (col.isVariableLength() && !col.getType().isLongValue()) { + rtn++; + } + } + return rtn; + } + + /** * Creates the table in the database. * @usage _advanced_method_ */ - public void createTable() throws IOException { + public TableImpl createTable(TableBuilder table) throws IOException { + + _name = table.getName(); + _columns = table.getColumns(); + _indexes = table.getIndexes(); + if(_indexes == null) { + _indexes = Collections.<IndexBuilder>emptyList(); + } validate(); @@ -146,17 +174,14 @@ class TableCreator } if(hasIndexes()) { - // sort out index numbers. for now, these values will always match - // (until we support writing foreign key indexes) + // sort out index numbers (and backing index data). for(IndexBuilder idx : _indexes) { - IndexState idxState = new IndexState(); - idxState.setIndexNumber(_logicalIndexCount++); - idxState.setIndexDataNumber(_indexCount++); - _indexStates.put(idx, idxState); + idx.setIndexNumber(_logicalIndexCount++); + findIndexDataState(idx); } } - getPageChannel().startWrite(); + getPageChannel().startExclusiveWrite(); try { // reserve some pages @@ -167,58 +192,79 @@ class TableCreator TableImpl.writeTableDefinition(this); // update the database with the new table info - _database.addNewTable(_name, _tdefPageNumber, DatabaseImpl.TYPE_TABLE, null, null); + getDatabase().addNewTable(_name, _tdefPageNumber, DatabaseImpl.TYPE_TABLE, + null, null); + + TableImpl newTable = getDatabase().getTable(_name); + + // add any table properties + boolean addedProps = false; + Map<String,PropertyMap.Property> props = table.getProperties(); + if(props != null) { + newTable.getProperties().putAll(props.values()); + addedProps = true; + } + for(ColumnBuilder cb : _columns) { + Map<String,PropertyMap.Property> colProps = cb.getProperties(); + if(colProps != null) { + newTable.getColumn(cb.getName()).getProperties() + .putAll(colProps.values()); + addedProps = true; + } + } + + // all table and column props are saved together + if(addedProps) { + newTable.getProperties().save(); + } + + return newTable; } finally { getPageChannel().finishWrite(); } } + private IndexDataState findIndexDataState(IndexBuilder idx) { + + // search for an index which matches the given index (in terms of the + // backing data) + for(IndexDataState idxDataState : _indexDataStates) { + if(sameIndexData(idxDataState.getFirstIndex(), idx)) { + idxDataState.addIndex(idx); + return idxDataState; + } + } + + // no matches found, need new index data state + IndexDataState idxDataState = new IndexDataState(); + idxDataState.setIndexDataNumber(_indexCount++); + idxDataState.addIndex(idx); + _indexDataStates.add(idxDataState); + return idxDataState; + } + /** * Validates the new table information before attempting creation. */ - private void validate() { + private void validate() throws IOException { - DatabaseImpl.validateIdentifierName( - _name, getFormat().MAX_TABLE_NAME_LENGTH, "table"); + getDatabase().validateNewTableName(_name); if((_columns == null) || _columns.isEmpty()) { - throw new IllegalArgumentException( - "Cannot create table with no columns"); + throw new IllegalArgumentException(withErrorContext( + "Cannot create table with no columns")); } if(_columns.size() > getFormat().MAX_COLUMNS_PER_TABLE) { - throw new IllegalArgumentException( + throw new IllegalArgumentException(withErrorContext( "Cannot create table with more than " + - getFormat().MAX_COLUMNS_PER_TABLE + " columns"); + getFormat().MAX_COLUMNS_PER_TABLE + " columns")); } - ColumnImpl.SortOrder dbSortOrder = null; - try { - dbSortOrder = _database.getDefaultSortOrder(); - } catch(IOException e) { - // ignored, just use the jet format default - } - Set<String> colNames = new HashSet<String>(); // next, validate the column definitions for(ColumnBuilder column : _columns) { - - // FIXME for now, we can't create complex columns - if(column.getType() == DataType.COMPLEX_TYPE) { - throw new UnsupportedOperationException( - "Complex column creation is not yet implemented"); - } - - column.validate(getFormat()); - if(!colNames.add(column.getName().toUpperCase())) { - throw new IllegalArgumentException("duplicate column name: " + - column.getName()); - } - - // set the sort order to the db default (if unspecified) - if(column.getType().isTextual() && (column.getTextSortOrder() == null)) { - column.setTextSortOrder(dbSortOrder); - } + validateColumn(colNames, column); } List<ColumnBuilder> autoCols = getAutoNumberColumns(); @@ -226,39 +272,23 @@ class TableCreator // for most autonumber types, we can only have one of each type Set<DataType> autoTypes = EnumSet.noneOf(DataType.class); for(ColumnBuilder c : autoCols) { - if(!c.getType().isMultipleAutoNumberAllowed() && - !autoTypes.add(c.getType())) { - throw new IllegalArgumentException( - "Can have at most one AutoNumber column of type " + c.getType() + - " per table"); - } + validateAutoNumberColumn(autoTypes, c); } } if(hasIndexes()) { if(_indexes.size() > getFormat().MAX_INDEXES_PER_TABLE) { - throw new IllegalArgumentException( + throw new IllegalArgumentException(withErrorContext( "Cannot create table with more than " + - getFormat().MAX_INDEXES_PER_TABLE + " indexes"); + getFormat().MAX_INDEXES_PER_TABLE + " indexes")); } // now, validate the indexes Set<String> idxNames = new HashSet<String>(); - boolean foundPk = false; + boolean foundPk[] = new boolean[1]; for(IndexBuilder index : _indexes) { - index.validate(colNames, getFormat()); - if(!idxNames.add(index.getName().toUpperCase())) { - throw new IllegalArgumentException("duplicate index name: " + - index.getName()); - } - if(index.isPrimaryKey()) { - if(foundPk) { - throw new IllegalArgumentException( - "found second primary key index: " + index.getName()); - } - foundPk = true; - } + validateIndex(colNames, idxNames, foundPk, index); } } } @@ -274,92 +304,37 @@ class TableCreator return autoCols; } - /** - * Maintains additional state used during index creation. - * @usage _advanced_class_ - */ - static final class IndexState - { - private int _indexNumber; - private int _indexDataNumber; - private byte _umapRowNumber; - private int _umapPageNumber; - private int _rootPageNumber; - - public int getIndexNumber() { - return _indexNumber; - } - - public void setIndexNumber(int newIndexNumber) { - _indexNumber = newIndexNumber; - } - - public int getIndexDataNumber() { - return _indexDataNumber; - } - - public void setIndexDataNumber(int newIndexDataNumber) { - _indexDataNumber = newIndexDataNumber; - } - - public byte getUmapRowNumber() { - return _umapRowNumber; + private static boolean sameIndexData(IndexBuilder idx1, IndexBuilder idx2) { + // index data can be combined if flags match and columns (and col flags) + // match + if(idx1.getFlags() != idx2.getFlags()) { + return false; } - public void setUmapRowNumber(byte newUmapRowNumber) { - _umapRowNumber = newUmapRowNumber; - } - - public int getUmapPageNumber() { - return _umapPageNumber; - } - - public void setUmapPageNumber(int newUmapPageNumber) { - _umapPageNumber = newUmapPageNumber; + if(idx1.getColumns().size() != idx2.getColumns().size()) { + return false; } + + for(int i = 0; i < idx1.getColumns().size(); ++i) { + IndexBuilder.Column col1 = idx1.getColumns().get(i); + IndexBuilder.Column col2 = idx2.getColumns().get(i); - public int getRootPageNumber() { - return _rootPageNumber; + if(!sameIndexData(col1, col2)) { + return false; + } } - public void setRootPageNumber(int newRootPageNumber) { - _rootPageNumber = newRootPageNumber; - } + return true; } - - /** - * Maintains additional state used during column creation. - * @usage _advanced_class_ - */ - static final class ColumnState - { - private byte _umapOwnedRowNumber; - private byte _umapFreeRowNumber; - // we always put both usage maps on the same page - private int _umapPageNumber; - public byte getUmapOwnedRowNumber() { - return _umapOwnedRowNumber; + private static boolean sameIndexData( + IndexBuilder.Column col1, IndexBuilder.Column col2) { + return (col1.getName().equals(col2.getName()) && + (col1.getFlags() == col2.getFlags())); } - public void setUmapOwnedRowNumber(byte newUmapOwnedRowNumber) { - _umapOwnedRowNumber = newUmapOwnedRowNumber; -} - - public byte getUmapFreeRowNumber() { - return _umapFreeRowNumber; - } - - public void setUmapFreeRowNumber(byte newUmapFreeRowNumber) { - _umapFreeRowNumber = newUmapFreeRowNumber; - } - - public int getUmapPageNumber() { - return _umapPageNumber; - } - - public void setUmapPageNumber(int newUmapPageNumber) { - _umapPageNumber = newUmapPageNumber; - } + @Override + protected String withErrorContext(String msg) { + return msg + "(Table=" + getName() + ")"; } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java index 85991cb..e1b0d1e 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -22,6 +22,7 @@ import java.io.StringWriter; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -33,12 +34,14 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import com.healthmarketscience.jackcess.BatchUpdateException; import com.healthmarketscience.jackcess.Column; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.ConstraintViolationException; import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.Index; import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.JackcessException; import com.healthmarketscience.jackcess.PropertyMap; @@ -111,15 +114,15 @@ public class TableImpl implements Table /** Type of the table (either TYPE_SYSTEM or TYPE_USER) */ private final byte _tableType; /** Number of actual indexes on the table */ - private final int _indexCount; + private int _indexCount; /** Number of logical indexes for the table */ - private final int _logicalIndexCount; + private int _logicalIndexCount; /** page number of the definition of this table */ private final int _tableDefPageNumber; /** max Number of columns in the table (includes previous deletions) */ - private final short _maxColumnCount; + private short _maxColumnCount; /** max Number of variable columns in the table */ - private final short _maxVarColumnCount; + private short _maxVarColumnCount; /** List of columns in this table, ordered by column number */ private final List<ColumnImpl> _columns = new ArrayList<ColumnImpl>(); /** List of variable length columns in this table, ordered by offset */ @@ -198,7 +201,7 @@ public class TableImpl implements Table } _maxColumnCount = (short)_columns.size(); _maxVarColumnCount = (short)_varColumns.size(); - getAutoNumberColumns(); + initAutoNumberColumns(); _fkEnforcer = null; _flags = 0; @@ -225,7 +228,8 @@ public class TableImpl implements Table _flags = flags; // read table definition - tableBuffer = loadCompleteTableDefinitionBuffer(tableBuffer); + tableBuffer = loadCompleteTableDefinitionBuffer(tableBuffer, null); + _rowCount = tableBuffer.getInt(getFormat().OFFSET_NUM_ROWS); _lastLongAutoNumber = tableBuffer.getInt(getFormat().OFFSET_NEXT_AUTO_NUMBER); if(getFormat().OFFSET_NEXT_COMPLEX_AUTO_NUMBER >= 0) { @@ -253,34 +257,9 @@ public class TableImpl implements Table readIndexDefinitions(tableBuffer); // read column usage map info - while(tableBuffer.remaining() >= 2) { - - short umapColNum = tableBuffer.getShort(); - if(umapColNum == IndexData.COLUMN_UNUSED) { - break; - } - - int pos = tableBuffer.position(); - UsageMap colOwnedPages = null; - UsageMap colFreeSpacePages = null; - try { - colOwnedPages = UsageMap.read(getDatabase(), tableBuffer, false); - colFreeSpacePages = UsageMap.read(getDatabase(), tableBuffer, false); - } catch(IllegalStateException e) { - // ignore invalid usage map info - colOwnedPages = null; - colFreeSpacePages = null; - tableBuffer.position(pos + 8); - LOG.warn(withErrorContext("Invalid column " + umapColNum + - " usage map definition: " + e)); - } - - for(ColumnImpl col : _columns) { - if(col.getColumnNumber() == umapColNum) { - col.setUsageMaps(colOwnedPages, colFreeSpacePages); - break; - } - } + while((tableBuffer.remaining() >= 2) && + readColumnUsageMaps(tableBuffer)) { + // keep reading ... } // re-sort columns if necessary @@ -512,6 +491,43 @@ public class TableImpl implements Table return _logicalIndexCount; } + int getIndexCount() { + return _indexCount; + } + + public IndexImpl findIndexForColumns(Collection<String> searchColumns, + boolean uniqueOnly) { + for(IndexImpl index : _indexes) { + + Collection<? extends Index.Column> indexColumns = index.getColumns(); + if(indexColumns.size() != searchColumns.size()) { + continue; + } + Iterator<String> sIter = searchColumns.iterator(); + Iterator<? extends Index.Column> iIter = indexColumns.iterator(); + boolean matches = true; + while(sIter.hasNext()) { + String sColName = sIter.next(); + String iColName = iIter.next().getName(); + if((sColName != iColName) && + ((sColName == null) || !sColName.equalsIgnoreCase(iColName))) { + matches = false; + break; + } + } + + if(matches && (!uniqueOnly || index.isUnique())) { + return index; + } + } + + return null; + } + + List<ColumnImpl> getAutoNumberColumns() { + return _autoNumColumns; + } + public CursorImpl getDefaultCursor() { if(_defaultCursor == null) { _defaultCursor = CursorImpl.createCursor(this); @@ -983,20 +999,15 @@ public class TableImpl implements Table // total up the amount of space used by the column and index names (2 // bytes per char + 2 bytes for the length) for(ColumnBuilder col : creator.getColumns()) { - int nameByteLen = (col.getName().length() * - JetFormat.TEXT_FIELD_UNIT_SIZE); - totalTableDefSize += nameByteLen + 2; + totalTableDefSize += DBMutator.calculateNameLength(col.getName()); } for(IndexBuilder idx : creator.getIndexes()) { - int nameByteLen = (idx.getName().length() * - JetFormat.TEXT_FIELD_UNIT_SIZE); - totalTableDefSize += nameByteLen + 2; + totalTableDefSize += DBMutator.calculateNameLength(idx.getName()); } // now, create the table definition - PageChannel pageChannel = creator.getPageChannel(); ByteBuffer buffer = PageChannel.createBuffer(Math.max(totalTableDefSize, format.PAGE_SIZE)); writeTableDefinitionHeader(creator, buffer, totalTableDefSize); @@ -1015,36 +1026,46 @@ public class TableImpl implements Table IndexImpl.writeDefinitions(creator, buffer); } - // write long value column usage map references - for(ColumnBuilder lvalCol : creator.getLongValueColumns()) { - buffer.putShort(lvalCol.getColumnNumber()); - TableCreator.ColumnState colState = - creator.getColumnState(lvalCol); - - // owned pages umap (both are on same page) - buffer.put(colState.getUmapOwnedRowNumber()); - ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); - // free space pages umap - buffer.put(colState.getUmapFreeRowNumber()); - ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber()); - } + // column usage map references + ColumnImpl.writeColUsageMapDefinitions(creator, buffer); //End of tabledef buffer.put((byte) 0xff); buffer.put((byte) 0xff); + buffer.flip(); + + // write table buffer to database + writeTableDefinitionBuffer(buffer, creator.getTdefPageNumber(), creator, + Collections.<Integer>emptyList()); + } + + private static void writeTableDefinitionBuffer( + ByteBuffer buffer, int tdefPageNumber, + TableMutator mutator, List<Integer> reservedPages) + throws IOException + { + buffer.rewind(); + int totalTableDefSize = buffer.remaining(); + JetFormat format = mutator.getFormat(); + PageChannel pageChannel = mutator.getPageChannel(); // write table buffer to database if(totalTableDefSize <= format.PAGE_SIZE) { // easy case, fits on one page + + // overwrite page free space buffer.putShort(format.OFFSET_FREE_SPACE, - (short)(buffer.remaining() - 8)); // overwrite page free space + (short)(Math.max( + format.PAGE_SIZE - totalTableDefSize - 8, 0))); // Write the tdef page to disk. - pageChannel.writePage(buffer, creator.getTdefPageNumber()); + buffer.clear(); + pageChannel.writePage(buffer, tdefPageNumber); } else { // need to split across multiple pages + ByteBuffer partialTdef = pageChannel.createPageBuffer(); buffer.rewind(); int nextTdefPageNumber = PageChannel.INVALID_PAGE_NUMBER; @@ -1057,7 +1078,7 @@ public class TableImpl implements Table // this is the first page. note, the first page already has the // page header, so no need to write it here - nextTdefPageNumber = creator.getTdefPageNumber(); + nextTdefPageNumber = tdefPageNumber; } else { @@ -1073,23 +1094,554 @@ public class TableImpl implements Table if(buffer.hasRemaining()) { // need a next page - nextTdefPageNumber = pageChannel.allocateNewPage(); + if(reservedPages.isEmpty()) { + nextTdefPageNumber = pageChannel.allocateNewPage(); + } else { + nextTdefPageNumber = reservedPages.remove(0); + } partialTdef.putInt(format.OFFSET_NEXT_TABLE_DEF_PAGE, nextTdefPageNumber); } // update page free space partialTdef.putShort(format.OFFSET_FREE_SPACE, - (short)(partialTdef.remaining() - 8)); // overwrite page free space + (short)(Math.max( + partialTdef.remaining() - 8, 0))); // write partial page to disk pageChannel.writePage(partialTdef, curTdefPageNumber); } } + + } + + /** + * Writes a column defined by the given TableUpdater to this table. + * @usage _advanced_method_ + */ + protected ColumnImpl mutateAddColumn(TableUpdater mutator) throws IOException + { + ColumnBuilder column = mutator.getColumn(); + JetFormat format = mutator.getFormat(); + boolean isVarCol = column.isVariableLength(); + boolean isLongVal = column.getType().isLongValue(); + + //// + // calculate how much more space we need in the table def + if(isLongVal) { + mutator.addTdefLen(10); + } + + mutator.addTdefLen(format.SIZE_COLUMN_DEF_BLOCK); + + int nameByteLen = DBMutator.calculateNameLength(column.getName()); + mutator.addTdefLen(nameByteLen); + + //// + // load current table definition and add space for new info + ByteBuffer tableBuffer = loadCompleteTableDefinitionBufferForUpdate( + mutator); + + ColumnImpl newCol = null; + int umapPos = -1; + boolean success = false; + try { + + //// + // update various bits of the table def + ByteUtil.forward(tableBuffer, 29); + tableBuffer.putShort((short)(_maxColumnCount + 1)); + short varColCount = (short)(_varColumns.size() + (isVarCol ? 1 : 0)); + tableBuffer.putShort(varColCount); + tableBuffer.putShort((short)(_columns.size() + 1)); + + // move to end of column def blocks + tableBuffer.position(format.SIZE_TDEF_HEADER + + (_indexCount * format.SIZE_INDEX_DEFINITION) + + (_columns.size() * format.SIZE_COLUMN_DEF_BLOCK)); + + // figure out the data offsets for the new column + int fixedOffset = 0; + int varOffset = 0; + if(column.isVariableLength()) { + // find the variable offset + for(ColumnImpl col : _varColumns) { + if(col.getVarLenTableIndex() >= varOffset) { + varOffset = col.getVarLenTableIndex() + 1; + } + } + } else { + // find the fixed offset + for(ColumnImpl col : _columns) { + if(!col.isVariableLength() && + (col.getFixedDataOffset() >= fixedOffset)) { + fixedOffset = col.getFixedDataOffset() + + col.getType().getFixedSize(col.getLength()); + } + } + } + + mutator.setColumnOffsets(fixedOffset, varOffset, varOffset); + + // insert space for the column definition and write it + int colDefPos = tableBuffer.position(); + ByteUtil.insertEmptyData(tableBuffer, format.SIZE_COLUMN_DEF_BLOCK); + ColumnImpl.writeDefinition(mutator, column, tableBuffer); + + // skip existing column names and write new name + skipNames(tableBuffer, _columns.size()); + ByteUtil.insertEmptyData(tableBuffer, nameByteLen); + writeName(tableBuffer, column.getName(), mutator.getCharset()); + + if(isLongVal) { + + // allocate usage maps for the long value col + Map.Entry<Integer,Integer> umapInfo = addUsageMaps(2, null); + TableMutator.ColumnState colState = mutator.getColumnState(column); + colState.setUmapPageNumber(umapInfo.getKey()); + byte rowNum = umapInfo.getValue().byteValue(); + colState.setUmapOwnedRowNumber(rowNum); + colState.setUmapFreeRowNumber((byte)(rowNum + 1)); + + // skip past index defs + ByteUtil.forward(tableBuffer, (_indexCount * + format.SIZE_INDEX_COLUMN_BLOCK)); + ByteUtil.forward(tableBuffer, + (_logicalIndexCount * format.SIZE_INDEX_INFO_BLOCK)); + skipNames(tableBuffer, _logicalIndexCount); + + // skip existing usage maps + while(tableBuffer.remaining() >= 2) { + if(tableBuffer.getShort() == IndexData.COLUMN_UNUSED) { + // found end of tdef, we want to insert before this + ByteUtil.forward(tableBuffer, -2); + break; + } + + ByteUtil.forward(tableBuffer, 8); + + // keep reading ... + } + + // write new column usage map info + umapPos = tableBuffer.position(); + ByteUtil.insertEmptyData(tableBuffer, 10); + ColumnImpl.writeColUsageMapDefinition( + mutator, column, tableBuffer); + } + + // sanity check the updates + validateTableDefUpdate(mutator, tableBuffer); + + // before writing the new table def, create the column + newCol = ColumnImpl.create(this, tableBuffer, colDefPos, + column.getName(), _columns.size()); + newCol.setColumnIndex(_columns.size()); + + //// + // write updated table def back to the database + writeTableDefinitionBuffer(tableBuffer, _tableDefPageNumber, mutator, + mutator.getNextPages()); + success = true; + + } finally { + if(!success) { + // need to discard modified table buffer + _tableDefBufferH.invalidate(); + } + } + + //// + // now, update current TableImpl + + _columns.add(newCol); + ++_maxColumnCount; + if(newCol.isVariableLength()) { + _varColumns.add(newCol); + ++_maxVarColumnCount; + } + if(newCol.isAutoNumber()) { + _autoNumColumns.add(newCol); + } + + if(umapPos >= 0) { + // read column usage map + tableBuffer.position(umapPos); + readColumnUsageMaps(tableBuffer); + } + + newCol.postTableLoadInit(); + + if(!isSystem()) { + // after fully constructed, allow column validator to be configured (but + // only for user tables) + newCol.setColumnValidator(null); + } + + // save any column properties + Map<String,PropertyMap.Property> colProps = column.getProperties(); + if(colProps != null) { + newCol.getProperties().putAll(colProps.values()); + getProperties().save(); + } + + completeTableMutation(tableBuffer); + + return newCol; } /** + * Writes a index defined by the given TableUpdater to this table. + * @usage _advanced_method_ + */ + protected IndexData mutateAddIndexData(TableUpdater mutator) throws IOException + { + IndexBuilder index = mutator.getIndex(); + JetFormat format = mutator.getFormat(); + + //// + // calculate how much more space we need in the table def + mutator.addTdefLen(format.SIZE_INDEX_DEFINITION + + format.SIZE_INDEX_COLUMN_BLOCK); + + //// + // load current table definition and add space for new info + ByteBuffer tableBuffer = loadCompleteTableDefinitionBufferForUpdate( + mutator); + + IndexData newIdxData = null; + boolean success = false; + try { + + //// + // update various bits of the table def + ByteUtil.forward(tableBuffer, 39); + tableBuffer.putInt(_indexCount + 1); + + // move to end of index data def blocks + tableBuffer.position(format.SIZE_TDEF_HEADER + + (_indexCount * format.SIZE_INDEX_DEFINITION)); + + // write index row count definition (empty initially) + ByteUtil.insertEmptyData(tableBuffer, format.SIZE_INDEX_DEFINITION); + IndexData.writeRowCountDefinitions(mutator, tableBuffer, 1); + + // skip columns and column names + ByteUtil.forward(tableBuffer, + (_columns.size() * format.SIZE_COLUMN_DEF_BLOCK)); + skipNames(tableBuffer, _columns.size()); + + // move to end of current index datas + ByteUtil.forward(tableBuffer, (_indexCount * + format.SIZE_INDEX_COLUMN_BLOCK)); + + // allocate usage maps and root page + TableMutator.IndexDataState idxDataState = mutator.getIndexDataState(index); + int rootPageNumber = getPageChannel().allocateNewPage(); + Map.Entry<Integer,Integer> umapInfo = addUsageMaps(1, rootPageNumber); + idxDataState.setRootPageNumber(rootPageNumber); + idxDataState.setUmapPageNumber(umapInfo.getKey()); + idxDataState.setUmapRowNumber(umapInfo.getValue().byteValue()); + + // write index data def + int idxDataDefPos = tableBuffer.position(); + ByteUtil.insertEmptyData(tableBuffer, format.SIZE_INDEX_COLUMN_BLOCK); + IndexData.writeDefinition(mutator, tableBuffer, idxDataState, null); + + // sanity check the updates + validateTableDefUpdate(mutator, tableBuffer); + + // before writing the new table def, create the index data + tableBuffer.position(0); + newIdxData = IndexData.create( + this, tableBuffer, idxDataState.getIndexDataNumber(), format); + tableBuffer.position(idxDataDefPos); + newIdxData.read(tableBuffer, _columns); + + //// + // write updated table def back to the database + writeTableDefinitionBuffer(tableBuffer, _tableDefPageNumber, mutator, + mutator.getNextPages()); + success = true; + + } finally { + if(!success) { + // need to discard modified table buffer + _tableDefBufferH.invalidate(); + } + } + + //// + // now, update current TableImpl + + for(IndexData.ColumnDescriptor iCol : newIdxData.getColumns()) { + _indexColumns.add(iCol.getColumn()); + } + + ++_indexCount; + _indexDatas.add(newIdxData); + + completeTableMutation(tableBuffer); + + // don't forget to populate the new index + populateIndexData(newIdxData); + + return newIdxData; + } + + private void populateIndexData(IndexData idxData) + throws IOException + { + // grab the columns involved in this index + List<ColumnImpl> idxCols = new ArrayList<ColumnImpl>(); + for(IndexData.ColumnDescriptor col : idxData.getColumns()) { + idxCols.add(col.getColumn()); + } + + // iterate through all the rows and add them to the index + Object[] rowVals = new Object[_columns.size()]; + for(Row row : getDefaultCursor().newIterable().addColumns(idxCols)) { + for(Column col : idxCols) { + col.setRowValue(rowVals, col.getRowValue(row)); + } + + IndexData.commitAll( + idxData.prepareAddRow(rowVals, (RowIdImpl)row.getId(), null)); + } + + updateTableDefinition(0); + } + + /** + * Writes a index defined by the given TableUpdater to this table. + * @usage _advanced_method_ + */ + protected IndexImpl mutateAddIndex(TableUpdater mutator) throws IOException + { + IndexBuilder index = mutator.getIndex(); + JetFormat format = mutator.getFormat(); + + //// + // calculate how much more space we need in the table def + mutator.addTdefLen(format.SIZE_INDEX_INFO_BLOCK); + + int nameByteLen = DBMutator.calculateNameLength(index.getName()); + mutator.addTdefLen(nameByteLen); + + //// + // load current table definition and add space for new info + ByteBuffer tableBuffer = loadCompleteTableDefinitionBufferForUpdate( + mutator); + + IndexImpl newIdx = null; + boolean success = false; + try { + + //// + // update various bits of the table def + ByteUtil.forward(tableBuffer, 35); + tableBuffer.putInt(_logicalIndexCount + 1); + + // move to end of index data def blocks + tableBuffer.position(format.SIZE_TDEF_HEADER + + (_indexCount * format.SIZE_INDEX_DEFINITION)); + + // skip columns and column names + ByteUtil.forward(tableBuffer, + (_columns.size() * format.SIZE_COLUMN_DEF_BLOCK)); + skipNames(tableBuffer, _columns.size()); + + // move to end of current index datas + ByteUtil.forward(tableBuffer, (_indexCount * + format.SIZE_INDEX_COLUMN_BLOCK)); + // move to end of current indexes + ByteUtil.forward(tableBuffer, (_logicalIndexCount * + format.SIZE_INDEX_INFO_BLOCK)); + + int idxDefPos = tableBuffer.position(); + ByteUtil.insertEmptyData(tableBuffer, format.SIZE_INDEX_INFO_BLOCK); + IndexImpl.writeDefinition(mutator, index, tableBuffer); + + // skip existing index names and write new name + skipNames(tableBuffer, _logicalIndexCount); + ByteUtil.insertEmptyData(tableBuffer, nameByteLen); + writeName(tableBuffer, index.getName(), mutator.getCharset()); + + // sanity check the updates + validateTableDefUpdate(mutator, tableBuffer); + + // before writing the new table def, create the index + tableBuffer.position(idxDefPos); + newIdx = new IndexImpl(tableBuffer, _indexDatas, format); + newIdx.setName(index.getName()); + + //// + // write updated table def back to the database + writeTableDefinitionBuffer(tableBuffer, _tableDefPageNumber, mutator, + mutator.getNextPages()); + success = true; + + } finally { + if(!success) { + // need to discard modified table buffer + _tableDefBufferH.invalidate(); + } + } + + //// + // now, update current TableImpl + + ++_logicalIndexCount; + _indexes.add(newIdx); + + completeTableMutation(tableBuffer); + + return newIdx; + } + + private void validateTableDefUpdate(TableUpdater mutator, ByteBuffer tableBuffer) + throws IOException + { + if(!mutator.validateUpdatedTdef(tableBuffer)) { + throw new IllegalStateException( + withErrorContext("Failed updating table definition (unexpected length)")); + } + } + + private void completeTableMutation(ByteBuffer tableBuffer) throws IOException + { + // lastly, may need to clear table def buffer + _tableDefBufferH.possiblyInvalidate(_tableDefPageNumber, tableBuffer); + + // update any foreign key enforcing + _fkEnforcer.reset(); + + // update modification count so any active RowStates can keep themselves + // up-to-date + ++_modCount; + } + + /** + * Skips the given number of names in the table buffer. + */ + private static void skipNames(ByteBuffer tableBuffer, int count) { + for(int i = 0; i < count; ++i) { + ByteUtil.forward(tableBuffer, tableBuffer.getShort()); + } + } + + private ByteBuffer loadCompleteTableDefinitionBufferForUpdate( + TableUpdater mutator) + throws IOException + { + // load complete table definition + ByteBuffer tableBuffer = _tableDefBufferH.setPage(getPageChannel(), + _tableDefPageNumber); + tableBuffer = loadCompleteTableDefinitionBuffer( + tableBuffer, mutator.getNextPages()); + + // make sure the table buffer has enough room for the new info + int addedLen = mutator.getAddedTdefLen(); + int origTdefLen = tableBuffer.getInt(8); + mutator.setOrigTdefLen(origTdefLen); + int newTdefLen = origTdefLen + addedLen; + while(newTdefLen > tableBuffer.capacity()) { + tableBuffer = expandTableBuffer(tableBuffer); + tableBuffer.flip(); + } + + tableBuffer.limit(origTdefLen); + + // set new tdef length + tableBuffer.position(8); + tableBuffer.putInt(newTdefLen); + + return tableBuffer; + } + + private Map.Entry<Integer,Integer> addUsageMaps( + int numMaps, Integer firstUsedPage) + throws IOException + { + JetFormat format = getFormat(); + PageChannel pageChannel = getPageChannel(); + int umapRowLength = format.OFFSET_USAGE_MAP_START + + format.USAGE_MAP_TABLE_BYTE_LENGTH; + int totalUmapSpaceUsage = getRowSpaceUsage(umapRowLength, format) * numMaps; + int umapPageNumber = PageChannel.INVALID_PAGE_NUMBER; + int firstRowNum = -1; + int freeSpace = 0; + + // search currently known usage map buffers to find one with enough free + // space (the numMaps should always be small enough to put them all on one + // page). pages will free space will probaby be newer pages (higher + // numbers), so we sort in reverse order. + Set<Integer> knownPages = new TreeSet<Integer>(Collections.reverseOrder()); + collectUsageMapPages(knownPages); + + ByteBuffer umapBuf = pageChannel.createPageBuffer(); + for(Integer pageNum : knownPages) { + pageChannel.readPage(umapBuf, pageNum); + freeSpace = umapBuf.getShort(format.OFFSET_FREE_SPACE); + if(freeSpace >= totalUmapSpaceUsage) { + // found a page! + umapPageNumber = pageNum; + firstRowNum = getRowsOnDataPage(umapBuf, format); + break; + } + } + + if(umapPageNumber == PageChannel.INVALID_PAGE_NUMBER) { + + // didn't find any existing pages, need to create a new one + freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE; + firstRowNum = 0; + umapBuf = createUsageMapDefPage(pageChannel, freeSpace); + } + + // write the actual usage map defs + int rowStart = findRowEnd(umapBuf, firstRowNum, format) - umapRowLength; + int umapRowNum = firstRowNum; + for(int i = 0; i < numMaps; ++i) { + umapBuf.putShort(getRowStartOffset(umapRowNum, format), (short)rowStart); + umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE); + + if(firstUsedPage != null) { + // fill in the first used page of the usage map + umapBuf.putInt(rowStart + 1, firstUsedPage); + umapBuf.put(rowStart + 5, (byte)1); + } + + rowStart -= umapRowLength; + ++umapRowNum; + } + + // finish the page + freeSpace -= totalUmapSpaceUsage; + umapBuf.putShort(format.OFFSET_FREE_SPACE, (short)freeSpace); + umapBuf.putShort(format.OFFSET_NUM_ROWS_ON_DATA_PAGE, + (short)umapRowNum); + pageChannel.writePage(umapBuf, umapPageNumber); + + return new AbstractMap.SimpleImmutableEntry<Integer,Integer>( + umapPageNumber, firstRowNum); + } + + void collectUsageMapPages(Collection<Integer> pages) { + pages.add(_ownedPages.getTablePageNumber()); + pages.add(_freeSpacePages.getTablePageNumber()); + + for(IndexData idx : _indexDatas) { + idx.collectUsageMapPages(pages); + } + + for(ColumnImpl col : _columns) { + col.collectUsageMapPages(pages); + } + } + + /** * @param buffer Buffer to write to */ private static void writeTableDefinitionHeader( @@ -1180,17 +1732,11 @@ public class TableImpl implements Table } else { // need another umap page umapPageNumber = creator.reservePageNumber(); - } + } freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE; - umapBuf = pageChannel.createPageBuffer(); - umapBuf.put(PageTypes.DATA); - umapBuf.put((byte) 0x1); //Unknown - umapBuf.putShort((short)freeSpace); //Free space in page - umapBuf.putInt(0); //Table definition - umapBuf.putInt(0); //Unknown - umapBuf.putShort((short)0); //Number of records on this page + umapBuf = createUsageMapDefPage(pageChannel, freeSpace); rowStart = findRowEnd(umapBuf, 0, format) - umapRowLength; umapRowNum = 0; @@ -1212,16 +1758,16 @@ public class TableImpl implements Table // index umap int indexIdx = i - 2; - IndexBuilder idx = creator.getIndexes().get(indexIdx); + TableMutator.IndexDataState idxDataState = + creator.getIndexDataStates().get(indexIdx); // allocate root page for the index int rootPageNumber = pageChannel.allocateNewPage(); // stash info for later use - TableCreator.IndexState idxState = creator.getIndexState(idx); - idxState.setRootPageNumber(rootPageNumber); - idxState.setUmapRowNumber((byte)umapRowNum); - idxState.setUmapPageNumber(umapPageNumber); + idxDataState.setRootPageNumber(rootPageNumber); + idxDataState.setUmapRowNumber((byte)umapRowNum); + idxDataState.setUmapPageNumber(umapPageNumber); // index map definition, including initial root page umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE); @@ -1236,7 +1782,7 @@ public class TableImpl implements Table lvalColIdx /= 2; ColumnBuilder lvalCol = lvalCols.get(lvalColIdx); - TableCreator.ColumnState colState = + TableMutator.ColumnState colState = creator.getColumnState(lvalCol); umapBuf.put(rowStart, UsageMap.MAP_TYPE_INLINE); @@ -1247,7 +1793,7 @@ public class TableImpl implements Table // data page, so just discard the previous one we wrote --i; umapType = 0; - } + } if(umapType == 0) { // lval column "owned pages" usage map @@ -1256,7 +1802,7 @@ public class TableImpl implements Table } else { // lval column "free space pages" usage map (always on same page) colState.setUmapFreeRowNumber((byte)umapRowNum); - } + } } rowStart -= umapRowLength; @@ -1274,31 +1820,52 @@ public class TableImpl implements Table } } + private static ByteBuffer createUsageMapDefPage( + PageChannel pageChannel, int freeSpace) + { + ByteBuffer umapBuf = pageChannel.createPageBuffer(); + umapBuf.put(PageTypes.DATA); + umapBuf.put((byte) 0x1); //Unknown + umapBuf.putShort((short)freeSpace); //Free space in page + umapBuf.putInt(0); //Table definition + umapBuf.putInt(0); //Unknown + umapBuf.putShort((short)0); //Number of records on this page + return umapBuf; + } + /** * Returns a single ByteBuffer which contains the entire table definition * (which may span multiple database pages). */ - private ByteBuffer loadCompleteTableDefinitionBuffer(ByteBuffer tableBuffer) + private ByteBuffer loadCompleteTableDefinitionBuffer( + ByteBuffer tableBuffer, List<Integer> pages) throws IOException { int nextPage = tableBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE); ByteBuffer nextPageBuffer = null; while (nextPage != 0) { + if(pages != null) { + pages.add(nextPage); + } if (nextPageBuffer == null) { nextPageBuffer = getPageChannel().createPageBuffer(); } getPageChannel().readPage(nextPageBuffer, nextPage); nextPage = nextPageBuffer.getInt(getFormat().OFFSET_NEXT_TABLE_DEF_PAGE); - ByteBuffer newBuffer = PageChannel.createBuffer( - tableBuffer.capacity() + getFormat().PAGE_SIZE - 8); - newBuffer.put(tableBuffer); - newBuffer.put(nextPageBuffer.array(), 8, getFormat().PAGE_SIZE - 8); - tableBuffer = newBuffer; + tableBuffer = expandTableBuffer(tableBuffer); + tableBuffer.put(nextPageBuffer.array(), 8, getFormat().PAGE_SIZE - 8); tableBuffer.flip(); } return tableBuffer; } + private ByteBuffer expandTableBuffer(ByteBuffer tableBuffer) { + ByteBuffer newBuffer = PageChannel.createBuffer( + tableBuffer.capacity() + getFormat().PAGE_SIZE - 8); + newBuffer.put(tableBuffer); + return newBuffer; + } + private void readColumnDefinitions(ByteBuffer tableBuffer, short columnCount) throws IOException { @@ -1326,7 +1893,7 @@ public class TableImpl implements Table } Collections.sort(_columns); - getAutoNumberColumns(); + initAutoNumberColumns(); // setup the data index for the columns int colIdx = 0; @@ -1364,6 +1931,39 @@ public class TableImpl implements Table Collections.sort(_indexes); } + private boolean readColumnUsageMaps(ByteBuffer tableBuffer) + throws IOException + { + short umapColNum = tableBuffer.getShort(); + if(umapColNum == IndexData.COLUMN_UNUSED) { + return false; + } + + int pos = tableBuffer.position(); + UsageMap colOwnedPages = null; + UsageMap colFreeSpacePages = null; + try { + colOwnedPages = UsageMap.read(getDatabase(), tableBuffer, false); + colFreeSpacePages = UsageMap.read(getDatabase(), tableBuffer, false); + } catch(IllegalStateException e) { + // ignore invalid usage map info + colOwnedPages = null; + colFreeSpacePages = null; + tableBuffer.position(pos + 8); + LOG.warn(withErrorContext("Invalid column " + umapColNum + + " usage map definition: " + e)); + } + + for(ColumnImpl col : _columns) { + if(col.getColumnNumber() == umapColNum) { + col.setUsageMaps(colOwnedPages, colFreeSpacePages); + break; + } + } + + return true; + } + /** * Writes the given page data to the given page number, clears any other * relevant buffers. @@ -2525,7 +3125,7 @@ public class TableImpl implements Table return rowSize + format.SIZE_ROW_LOCATION; } - private void getAutoNumberColumns() { + private void initAutoNumberColumns() { for(ColumnImpl c : _columns) { if(c.isAutoNumber()) { _autoNumColumns.add(c); @@ -2627,7 +3227,7 @@ public class TableImpl implements Table /** true if the row values array has data */ private boolean _haveRowValues; /** values read from the last row */ - private final Object[] _rowValues; + private Object[] _rowValues; /** null mask for the last row */ private NullMask _nullMask; /** last modification count seen on the table we track this so that the @@ -2682,6 +3282,11 @@ public class TableImpl implements Table reset(); _headerRowBufferH.invalidate(); _overflowRowBufferH.invalidate(); + int colCount = TableImpl.this.getColumnCount(); + if(colCount != _rowValues.length) { + // columns added or removed from table + _rowValues = new Object[colCount]; + } _lastModCount = TableImpl.this._modCount; } } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableMutator.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableMutator.java new file mode 100644 index 0000000..65bb8dc --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableMutator.java @@ -0,0 +1,250 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.IndexBuilder; + +/** + * Common helper class used to maintain state during table mutation. + * + * @author James Ahlborn + * @usage _advanced_class_ + */ +public abstract class TableMutator extends DBMutator +{ + private ColumnOffsets _colOffsets; + + protected TableMutator(DatabaseImpl database) { + super(database); + } + + public void setColumnOffsets( + int fixedOffset, int varOffset, int longVarOffset) { + if(_colOffsets == null) { + _colOffsets = new ColumnOffsets(); + } + _colOffsets.set(fixedOffset, varOffset, longVarOffset); + } + + public ColumnOffsets getColumnOffsets() { + return _colOffsets; + } + + public IndexImpl.ForeignKeyReference getForeignKey(IndexBuilder idx) { + return null; + } + + protected void validateColumn(Set<String> colNames, ColumnBuilder column) { + + // FIXME for now, we can't create complex columns + if(column.getType() == DataType.COMPLEX_TYPE) { + throw new UnsupportedOperationException(withErrorContext( + "Complex column creation is not yet implemented")); + } + + column.validate(getFormat()); + if(!colNames.add(DatabaseImpl.toLookupName(column.getName()))) { + throw new IllegalArgumentException(withErrorContext( + "duplicate column name: " + column.getName())); + } + + setColumnSortOrder(column); + } + + protected void validateIndex(Set<String> colNames, Set<String> idxNames, + boolean[] foundPk, IndexBuilder index) { + + index.validate(colNames, getFormat()); + if(!idxNames.add(DatabaseImpl.toLookupName(index.getName()))) { + throw new IllegalArgumentException(withErrorContext( + "duplicate index name: " + index.getName())); + } + if(index.isPrimaryKey()) { + if(foundPk[0]) { + throw new IllegalArgumentException(withErrorContext( + "found second primary key index: " + index.getName())); + } + foundPk[0] = true; + } else if(index.getType() == IndexImpl.FOREIGN_KEY_INDEX_TYPE) { + if(getForeignKey(index) == null) { + throw new IllegalArgumentException(withErrorContext( + "missing foreign key info for " + index.getName())); + } + } + } + + protected void validateAutoNumberColumn(Set<DataType> autoTypes, + ColumnBuilder column) + { + if(!column.getType().isMultipleAutoNumberAllowed() && + !autoTypes.add(column.getType())) { + throw new IllegalArgumentException(withErrorContext( + "Can have at most one AutoNumber column of type " + column.getType() + + " per table")); + } + } + + private void setColumnSortOrder(ColumnBuilder column) { + // set the sort order to the db default (if unspecified) + if(column.getType().isTextual() && (column.getTextSortOrder() == null)) { + column.setTextSortOrder(getDbSortOrder()); + } + } + + abstract String getTableName(); + + public abstract int getTdefPageNumber(); + + abstract short getColumnNumber(String colName); + + public abstract ColumnState getColumnState(ColumnBuilder col); + + public abstract IndexDataState getIndexDataState(IndexBuilder idx); + + protected abstract String withErrorContext(String msg); + + /** + * Maintains additional state used during column writing. + * @usage _advanced_class_ + */ + static final class ColumnOffsets + { + private short _fixedOffset; + private short _varOffset; + private short _longVarOffset; + + public void set(int fixedOffset, int varOffset, int longVarOffset) { + _fixedOffset = (short)fixedOffset; + _varOffset = (short)varOffset; + _longVarOffset = (short)longVarOffset; + } + + public short getNextVariableOffset(ColumnBuilder col) { + if(!col.getType().isLongValue()) { + return _varOffset++; + } + return _longVarOffset++; + } + + public short getNextFixedOffset(ColumnBuilder col) { + short offset = _fixedOffset; + _fixedOffset += col.getType().getFixedSize(col.getLength()); + return offset; + } + } + + /** + * Maintains additional state used during column creation. + * @usage _advanced_class_ + */ + static final class ColumnState + { + private byte _umapOwnedRowNumber; + private byte _umapFreeRowNumber; + // we always put both usage maps on the same page + private int _umapPageNumber; + + public byte getUmapOwnedRowNumber() { + return _umapOwnedRowNumber; + } + + public void setUmapOwnedRowNumber(byte newUmapOwnedRowNumber) { + _umapOwnedRowNumber = newUmapOwnedRowNumber; + } + + public byte getUmapFreeRowNumber() { + return _umapFreeRowNumber; + } + + public void setUmapFreeRowNumber(byte newUmapFreeRowNumber) { + _umapFreeRowNumber = newUmapFreeRowNumber; + } + + public int getUmapPageNumber() { + return _umapPageNumber; + } + + public void setUmapPageNumber(int newUmapPageNumber) { + _umapPageNumber = newUmapPageNumber; + } + } + + /** + * Maintains additional state used during index data creation. + * @usage _advanced_class_ + */ + static final class IndexDataState + { + private final List<IndexBuilder> _indexes = new ArrayList<IndexBuilder>(); + private int _indexDataNumber; + private byte _umapRowNumber; + private int _umapPageNumber; + private int _rootPageNumber; + + public IndexBuilder getFirstIndex() { + // all indexes which have the same backing IndexDataState will have + // equivalent columns and flags. + return _indexes.get(0); + } + + public List<IndexBuilder> getIndexes() { + return _indexes; + } + + public void addIndex(IndexBuilder idx) { + _indexes.add(idx); + } + + public int getIndexDataNumber() { + return _indexDataNumber; + } + + public void setIndexDataNumber(int newIndexDataNumber) { + _indexDataNumber = newIndexDataNumber; + } + + public byte getUmapRowNumber() { + return _umapRowNumber; + } + + public void setUmapRowNumber(byte newUmapRowNumber) { + _umapRowNumber = newUmapRowNumber; + } + + public int getUmapPageNumber() { + return _umapPageNumber; + } + + public void setUmapPageNumber(int newUmapPageNumber) { + _umapPageNumber = newUmapPageNumber; + } + + public int getRootPageNumber() { + return _rootPageNumber; + } + + public void setRootPageNumber(int newRootPageNumber) { + _rootPageNumber = newRootPageNumber; + } + } +} diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java new file mode 100644 index 0000000..8d8f348 --- /dev/null +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java @@ -0,0 +1,332 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess.impl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.IndexBuilder; + +/** + * Helper class used to maintain state during table mutation. + * + * @author James Ahlborn + * @usage _advanced_class_ + */ +public class TableUpdater extends TableMutator +{ + private final TableImpl _table; + + private ColumnBuilder _column; + private IndexBuilder _index; + private int _origTdefLen; + private int _addedTdefLen; + private List<Integer> _nextPages = new ArrayList<Integer>(1); + private ColumnState _colState; + private IndexDataState _idxDataState; + private IndexImpl.ForeignKeyReference _fkReference; + + public TableUpdater(TableImpl table) { + super(table.getDatabase()); + _table = table; + } + + public ColumnBuilder getColumn() { + return _column; + } + + public IndexBuilder getIndex() { + return _index; + } + + @Override + String getTableName() { + return _table.getName(); + } + + @Override + public int getTdefPageNumber() { + return _table.getTableDefPageNumber(); + } + + @Override + short getColumnNumber(String colName) { + for(ColumnImpl col : _table.getColumns()) { + if(col.getName().equalsIgnoreCase(colName)) { + return col.getColumnNumber(); + } + } + return IndexData.COLUMN_UNUSED; + } + + @Override + public ColumnState getColumnState(ColumnBuilder col) { + return ((col == _column) ? _colState : null); + } + + @Override + public IndexDataState getIndexDataState(IndexBuilder idx) { + return ((idx == _index) ? _idxDataState : null); + } + + void setForeignKey(IndexImpl.ForeignKeyReference fkReference) { + _fkReference = fkReference; + } + + @Override + public IndexImpl.ForeignKeyReference getForeignKey(IndexBuilder idx) { + return ((idx == _index) ? _fkReference : null); + } + + int getAddedTdefLen() { + return _addedTdefLen; + } + + void addTdefLen(int add) { + _addedTdefLen += add; + } + + void setOrigTdefLen(int len) { + _origTdefLen = len; + } + + List<Integer> getNextPages() { + return _nextPages; + } + + void resetTdefInfo() { + _addedTdefLen = 0; + _origTdefLen = 0; + _nextPages.clear(); + } + + public ColumnImpl addColumn(ColumnBuilder column) throws IOException { + + _column = column; + + validateAddColumn(); + + // assign column number and do some assorted column bookkeeping + short columnNumber = (short)_table.getMaxColumnCount(); + _column.setColumnNumber(columnNumber); + if(_column.getType().isLongValue()) { + _colState = new ColumnState(); + } + + getPageChannel().startExclusiveWrite(); + try { + + return _table.mutateAddColumn(this); + + } finally { + getPageChannel().finishWrite(); + } + } + + public IndexImpl addIndex(IndexBuilder index) throws IOException { + return addIndex(index, false, (byte)0, (byte)0); + } + + IndexImpl addIndex(IndexBuilder index, boolean isInternal, byte ignoreIdxFlags, + byte ignoreColFlags) + throws IOException + { + _index = index; + + if(!isInternal) { + validateAddIndex(); + } + + // assign index number and do some assorted index bookkeeping + int indexNumber = _table.getLogicalIndexCount(); + _index.setIndexNumber(indexNumber); + + // initialize backing index state + initIndexDataState(ignoreIdxFlags, ignoreColFlags); + + if(!isInternal) { + getPageChannel().startExclusiveWrite(); + } else { + // if "internal" update, this is part of a larger operation which + // already holds an exclusive write lock + getPageChannel().startWrite(); + } + try { + + if(_idxDataState.getIndexDataNumber() == _table.getIndexCount()) { + // we need a new backing index data + _table.mutateAddIndexData(this); + + // we need to modify the table def again when adding the Index, so reset + resetTdefInfo(); + } + + return _table.mutateAddIndex(this); + + } finally { + getPageChannel().finishWrite(); + } + } + + boolean validateUpdatedTdef(ByteBuffer tableBuffer) { + // sanity check the updates + return((_origTdefLen + _addedTdefLen) == tableBuffer.limit()); + } + + private void validateAddColumn() { + + if(_column == null) { + throw new IllegalArgumentException(withErrorContext( + "Cannot add column with no column")); + } + if((_table.getColumnCount() + 1) > getFormat().MAX_COLUMNS_PER_TABLE) { + throw new IllegalArgumentException(withErrorContext( + "Cannot add column to table with " + + getFormat().MAX_COLUMNS_PER_TABLE + " columns")); + } + + Set<String> colNames = getColumnNames(); + // next, validate the column definition + validateColumn(colNames, _column); + + if(_column.isAutoNumber()) { + // for most autonumber types, we can only have one of each type + Set<DataType> autoTypes = EnumSet.noneOf(DataType.class); + for(ColumnImpl column : _table.getAutoNumberColumns()) { + autoTypes.add(column.getType()); + } + + validateAutoNumberColumn(autoTypes, _column); + } + } + + private void validateAddIndex() { + + if(_index == null) { + throw new IllegalArgumentException(withErrorContext( + "Cannot add index with no index")); + } + if((_table.getLogicalIndexCount() + 1) > getFormat().MAX_INDEXES_PER_TABLE) { + throw new IllegalArgumentException(withErrorContext( + "Cannot add index to table with " + + getFormat().MAX_INDEXES_PER_TABLE + " indexes")); + } + + boolean foundPk[] = new boolean[1]; + Set<String> idxNames = getIndexNames(_table, foundPk); + // next, validate the index definition + validateIndex(getColumnNames(), idxNames, foundPk, _index); + } + + private Set<String> getColumnNames() { + Set<String> colNames = new HashSet<String>(); + for(ColumnImpl column : _table.getColumns()) { + colNames.add(DatabaseImpl.toLookupName(column.getName())); + } + return colNames; + } + + static Set<String> getIndexNames(TableImpl table, boolean[] foundPk) { + Set<String> idxNames = new HashSet<String>(); + for(IndexImpl index : table.getIndexes()) { + idxNames.add(DatabaseImpl.toLookupName(index.getName())); + if(index.isPrimaryKey() && (foundPk != null)) { + foundPk[0] = true; + } + } + return idxNames; + } + + private void initIndexDataState(byte ignoreIdxFlags, byte ignoreColFlags) { + + _idxDataState = new IndexDataState(); + _idxDataState.addIndex(_index); + + // search for an existing index which matches the given index (in terms of + // the backing data) + IndexData idxData = findIndexData( + _index, _table, ignoreIdxFlags, ignoreColFlags); + + int idxDataNumber = ((idxData != null) ? + idxData.getIndexDataNumber() : + _table.getIndexCount()); + + _idxDataState.setIndexDataNumber(idxDataNumber); + } + + static IndexData findIndexData(IndexBuilder idx, TableImpl table, + byte ignoreIdxFlags, byte ignoreColFlags) + { + for(IndexData idxData : table.getIndexDatas()) { + if(sameIndexData(idx, idxData, ignoreIdxFlags, ignoreColFlags)) { + return idxData; + } + } + return null; + } + + private static boolean sameIndexData(IndexBuilder idx1, IndexData idx2, + byte ignoreIdxFlags, byte ignoreColFlags) { + // index data can be combined if flags match and columns (and col flags) + // match + if((idx1.getFlags() | ignoreIdxFlags) != + (idx2.getIndexFlags() | ignoreIdxFlags)) { + return false; + } + + if(idx1.getColumns().size() != idx2.getColumns().size()) { + return false; + } + + for(int i = 0; i < idx1.getColumns().size(); ++i) { + IndexBuilder.Column col1 = idx1.getColumns().get(i); + IndexData.ColumnDescriptor col2 = idx2.getColumns().get(i); + + if(!sameIndexData(col1, col2, ignoreColFlags)) { + return false; + } + } + + return true; + } + + private static boolean sameIndexData( + IndexBuilder.Column col1, IndexData.ColumnDescriptor col2, + int ignoreColFlags) { + return (col1.getName().equals(col2.getName()) && + ((col1.getFlags() | ignoreColFlags) == + (col2.getFlags() | ignoreColFlags))); + } + + @Override + protected String withErrorContext(String msg) { + String objStr = ""; + if(_column != null) { + objStr = ";Column=" + _column.getName(); + } else if(_index != null) { + objStr = ";Index=" + _index.getName(); + } + return msg + "(Table=" + _table.getName() + objStr + ")"; + } +} diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java index 60e8bea..6a6fd34 100644 --- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -25,7 +25,6 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; -import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashMap; @@ -63,7 +62,7 @@ public class DatabaseTest extends TestCase Database db = create(fileFormat); try { - ((DatabaseImpl)db).createTable("test", Collections.<ColumnBuilder>emptyList()); + new TableBuilder("test").toTable(db); fail("created table with no columns?"); } catch(IllegalArgumentException e) { // success @@ -598,9 +597,9 @@ public class DatabaseTest extends TestCase columns.add(new ColumnBuilder(colName, DataType.TEXT).toColumn()); } - ((DatabaseImpl)db).createTable("test", columns); - - Table t = db.getTable("test"); + Table t = new TableBuilder("test") + .addColumns(columns) + .toTable(db); List<String> row = new ArrayList<String>(); Map<String,Object> expectedRowData = new LinkedHashMap<String, Object>(); diff --git a/src/test/java/com/healthmarketscience/jackcess/IndexTest.java b/src/test/java/com/healthmarketscience/jackcess/IndexTest.java index 28e2ff9..47f7b89 100644 --- a/src/test/java/com/healthmarketscience/jackcess/IndexTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/IndexTest.java @@ -462,6 +462,57 @@ public class IndexTest extends TestCase { } } + public void testIndexCreationSharedData() 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()) + .addIndex(new IndexBuilder("Index1").addColumns("id")) + .addIndex(new IndexBuilder("Index2").addColumns("id")) + .addIndex(new IndexBuilder("Index3").addColumns(false, "id")) + .toTable(db); + + assertEquals(4, t.getIndexes().size()); + IndexImpl idx = (IndexImpl)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()); + + IndexImpl idx1 = (IndexImpl)t.getIndexes().get(1); + IndexImpl idx2 = (IndexImpl)t.getIndexes().get(2); + IndexImpl idx3 = (IndexImpl)t.getIndexes().get(3); + + assertNotSame(idx.getIndexData(), idx1.getIndexData()); + assertSame(idx1.getIndexData(), idx2.getIndexData()); + assertNotSame(idx2.getIndexData(), idx3.getIndexData()); + + t.addRow(2, "row2"); + t.addRow(1, "row1"); + t.addRow(3, "row3"); + + Cursor c = t.newCursor() + .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()); + } + } + public void testGetForeignKeyIndex() throws Exception { for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX, true)) { diff --git a/src/test/java/com/healthmarketscience/jackcess/TableUpdaterTest.java b/src/test/java/com/healthmarketscience/jackcess/TableUpdaterTest.java new file mode 100644 index 0000000..160f71c --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/TableUpdaterTest.java @@ -0,0 +1,177 @@ +/* +Copyright (c) 2016 James Ahlborn + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package com.healthmarketscience.jackcess; + +import java.util.Arrays; + +import com.healthmarketscience.jackcess.Database.FileFormat; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import com.healthmarketscience.jackcess.impl.DatabaseImpl; +import com.healthmarketscience.jackcess.impl.TableImpl; +import junit.framework.TestCase; +import static com.healthmarketscience.jackcess.TestUtil.*; + +/** + * + * @author James Ahlborn + */ +public class TableUpdaterTest extends TestCase +{ + + public TableUpdaterTest(String name) throws Exception { + super(name); + } + + public void testTableUpdating() throws Exception { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = create(fileFormat); + + doTestUpdating(db, false, true); + + db.close(); + } + } + + public void testTableUpdatingOneToOne() throws Exception { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = create(fileFormat); + + doTestUpdating(db, true, true); + // FIXME, add one-to-one, add no enforce rel + + db.close(); + } + } + + public void testTableUpdatingNoEnforce() throws Exception { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = create(fileFormat); + + doTestUpdating(db, false, false); + + db.close(); + } + } + + private void doTestUpdating(Database db, boolean oneToOne, boolean enforce) + throws Exception + { + Table t1 = new TableBuilder("TestTable") + .addColumn(new ColumnBuilder("id", DataType.LONG)) + .toTable(db); + + Table t2 = new TableBuilder("TestTable2") + .addColumn(new ColumnBuilder("id2", DataType.LONG)) + .toTable(db); + + int t1idxs = 1; + new IndexBuilder(IndexBuilder.PRIMARY_KEY_NAME) + .addColumns("id").setPrimaryKey() + .addToTable(t1); + new ColumnBuilder("data", DataType.TEXT) + .addToTable(t1); + new ColumnBuilder("bigdata", DataType.MEMO) + .addToTable(t1); + + new ColumnBuilder("data2", DataType.TEXT) + .addToTable(t2); + new ColumnBuilder("bigdata2", DataType.MEMO) + .addToTable(t2); + + int t2idxs = 0; + if(oneToOne) { + ++t2idxs; + new IndexBuilder(IndexBuilder.PRIMARY_KEY_NAME) + .addColumns("id2").setPrimaryKey() + .addToTable(t2); + } + + RelationshipBuilder rb = new RelationshipBuilder("TestTable", "TestTable2") + .addColumns("id", "id2"); + if(enforce) { + ++t1idxs; + ++t2idxs; + rb.setReferentialIntegrity() + .setCascadeDeletes(); + } + + Relationship rel = rb.toRelationship(db); + + assertEquals("TestTableTestTable2", rel.getName()); + assertSame(t1, rel.getFromTable()); + assertEquals(Arrays.asList(t1.getColumn("id")), rel.getFromColumns()); + assertSame(t2, rel.getToTable()); + assertEquals(Arrays.asList(t2.getColumn("id2")), rel.getToColumns()); + assertEquals(oneToOne, rel.isOneToOne()); + assertEquals(enforce, rel.hasReferentialIntegrity()); + assertEquals(enforce, rel.cascadeDeletes()); + assertFalse(rel.cascadeUpdates()); + assertEquals(Relationship.JoinType.INNER, rel.getJoinType()); + + assertEquals(t1idxs, t1.getIndexes().size()); + assertEquals(1, ((TableImpl)t1).getIndexDatas().size()); + + assertEquals(t2idxs, t2.getIndexes().size()); + assertEquals((t2idxs > 0 ? 1 : 0), ((TableImpl)t2).getIndexDatas().size()); + + ((DatabaseImpl)db).getPageChannel().startWrite(); + try { + + for(int i = 0; i < 10; ++i) { + t1.addRow(i, "row" + i, "row-data" + i); + } + + for(int i = 0; i < 10; ++i) { + t2.addRow(i, "row2_" + i, "row-data2_" + i); + } + + } finally { + ((DatabaseImpl)db).getPageChannel().finishWrite(); + } + + try { + t2.addRow(10, "row10", "row-data10"); + if(enforce) { + fail("ConstraintViolationException should have been thrown"); + } + } catch(ConstraintViolationException cv) { + // success + if(!enforce) { throw cv; } + } + + Row r1 = CursorBuilder.findRowByPrimaryKey(t1, 5); + t1.deleteRow(r1); + + int id = 0; + for(Row r : t1) { + assertEquals(id, r.get("id")); + ++id; + if(id == 5) { + ++id; + } + } + + id = 0; + for(Row r : t2) { + assertEquals(id, r.get("id2")); + ++id; + if(enforce && (id == 5)) { + ++id; + } + } + } +} |