aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Ahlborn <jtahlborn@yahoo.com>2016-09-08 12:45:38 +0000
committerJames Ahlborn <jtahlborn@yahoo.com>2016-09-08 12:45:38 +0000
commit819953ac72f4e8ba7e070e53ee12e9ba1decda5a (patch)
treebbc5df96955b5a052c037831988a93e7444486f9
parent70eb4cc43cfc18a65aa50506dfeaf1d8a2e3a3f4 (diff)
parent4de28cb4f6595148baaf36a57875e20c32faeda2 (diff)
downloadjackcess-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
-rw-r--r--TODO.txt12
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java20
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java32
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java45
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/Relationship.java8
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java184
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/TableBuilder.java58
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java15
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java179
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/DBMutator.java68
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java267
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java22
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java122
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java60
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java2
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java17
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java13
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java346
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java38
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TableCreator.java319
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java773
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TableMutator.java250
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java332
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java9
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/IndexTest.java51
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/TableUpdaterTest.java177
26 files changed, 2872 insertions, 547 deletions
diff --git a/TODO.txt b/TODO.txt
index e3a12d6..fdae514 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -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;
+ }
+ }
+ }
+}