aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java
diff options
context:
space:
mode:
authorJames Ahlborn <jtahlborn@yahoo.com>2016-04-28 23:04:04 +0000
committerJames Ahlborn <jtahlborn@yahoo.com>2016-04-28 23:04:04 +0000
commit595044d1a5f38da203a83d7c870bb5001aea1a59 (patch)
tree91cef7a1327f3acf1b1da444325088ab02842aa7 /src/main/java
parent631d2c1901f2a4ed066b8ad3091ae6d46a793f56 (diff)
downloadjackcess-595044d1a5f38da203a83d7c870bb5001aea1a59.tar.gz
jackcess-595044d1a5f38da203a83d7c870bb5001aea1a59.zip
some initial code for mutation support
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/branches/mutateops@983 f203690c-595d-4dc9-a70b-905162fa7fd2
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/TableModBuilder.java53
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java16
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java148
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/DBMutator.java185
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java2
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java13
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TableCreator.java116
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java342
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TableMutator.java102
9 files changed, 748 insertions, 229 deletions
diff --git a/src/main/java/com/healthmarketscience/jackcess/TableModBuilder.java b/src/main/java/com/healthmarketscience/jackcess/TableModBuilder.java
new file mode 100644
index 0000000..e7a654a
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/TableModBuilder.java
@@ -0,0 +1,53 @@
+/*
+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 com.healthmarketscience.jackcess.impl.TableImpl;
+import com.healthmarketscience.jackcess.impl.TableMutator;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class TableModBuilder
+{
+ private Table _table;
+
+ public TableModBuilder(Table table) {
+ _table = table;
+ }
+
+ public AddColumn addColumn(ColumnBuilder column) {
+ return new AddColumn(column);
+ }
+
+ public class AddColumn
+ {
+ private ColumnBuilder _column;
+
+ private AddColumn(ColumnBuilder column) {
+ _column = column;
+ }
+
+ public Column add() throws IOException
+ {
+ return new TableMutator((TableImpl)_table).addColumn(_column);
+ }
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java b/src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java
index 6b28b22..96285e8 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java
@@ -397,6 +397,22 @@ 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.out.println("FOO insert " + pos + " " + len + " " + limit + " " + buffer.capacity());
+ 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..24c53ad 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
@@ -1405,22 +1405,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,87 +1569,89 @@ 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(
+ DBMutator mutator, ColumnBuilder col, ByteBuffer buffer)
+ throws IOException
+ {
+ DBMutator.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
+
+ if(col.isVariableLength()) {
+ buffer.putShort(colOffsets.getNextVariableOffset(col));
+ } else {
+ buffer.putShort((short) 0);
+ }
- buffer.put(getColumnBitFlags(col)); // misc col flags
+ buffer.putShort(col.getColumnNumber()); //Column Number again
- // 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);
+ 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);
+ }
- 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.putInt(0); //Unknown, but always 0.
+ //Offset for fixed length columns
+ if(col.isVariableLength()) {
+ buffer.putShort((short) 0);
+ } else {
+ buffer.putShort(colOffsets.getNextFixedOffset(col));
}
- for (ColumnBuilder col : columns) {
- TableImpl.writeName(buffer, col.getName(), creator.getCharset());
+
+ 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
}
}
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..e3825d2
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/DBMutator.java
@@ -0,0 +1,185 @@
+/*
+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;
+import java.util.Set;
+
+import com.healthmarketscience.jackcess.ColumnBuilder;
+import com.healthmarketscience.jackcess.DataType;
+
+/**
+ * Helper class used to maintain state during database mutation.
+ *
+ * @author James Ahlborn
+ */
+abstract class DBMutator
+{
+ private final DatabaseImpl _database;
+ private ColumnOffsets _colOffsets;
+
+ 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 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 static int calculateNameLength(String name) {
+ return (name.length() * JetFormat.TEXT_FIELD_UNIT_SIZE) + 2;
+ }
+
+ 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(
+ "Complex column creation is not yet implemented");
+ }
+
+ column.validate(getFormat());
+ if(!colNames.add(column.getName().toUpperCase())) {
+ throw new IllegalArgumentException("duplicate column name: " +
+ column.getName());
+ }
+
+ setColumnSortOrder(column);
+ }
+
+ protected void validateAutoNumberColumn(Set<DataType> autoTypes,
+ ColumnBuilder column)
+ {
+ if(!column.getType().isMultipleAutoNumberAllowed() &&
+ !autoTypes.add(column.getType())) {
+ throw new IllegalArgumentException(
+ "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());
+ }
+ }
+
+ private ColumnImpl.SortOrder getDbSortOrder() {
+ try {
+ return _database.getDefaultSortOrder();
+ } catch(IOException e) {
+ // ignored, just use the jet format default
+ }
+ return null;
+ }
+
+ /**
+ * 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 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;
+ }
+ }
+}
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/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/TableCreator.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableCreator.java
index d20ae38..89da310 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;
@@ -37,9 +36,8 @@ import com.healthmarketscience.jackcess.IndexBuilder;
* @author James Ahlborn
* @usage _advanced_class_
*/
-class TableCreator
+class TableCreator extends DBMutator
{
- private final DatabaseImpl _database;
private final String _name;
private final List<ColumnBuilder> _columns;
private final List<IndexBuilder> _indexes;
@@ -53,9 +51,9 @@ class TableCreator
private int _indexCount;
private int _logicalIndexCount;
- public TableCreator(DatabaseImpl database, String name, List<ColumnBuilder> columns,
- List<IndexBuilder> indexes) {
- _database = database;
+ public TableCreator(DatabaseImpl database, String name,
+ List<ColumnBuilder> columns, List<IndexBuilder> indexes) {
+ super(database);
_name = name;
_columns = columns;
_indexes = ((indexes != null) ? indexes :
@@ -66,22 +64,6 @@ class TableCreator
return _name;
}
- 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 getTdefPageNumber() {
return _tdefPageNumber;
}
@@ -114,10 +96,6 @@ class TableCreator
return _indexStates.get(idx);
}
- public int reservePageNumber() throws IOException {
- return getPageChannel().allocateNewPage();
- }
-
public ColumnState getColumnState(ColumnBuilder col) {
return _columnStates.get(col);
}
@@ -127,6 +105,22 @@ class TableCreator
}
/**
+ * @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_
*/
@@ -167,7 +161,8 @@ 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);
} finally {
getPageChannel().finishWrite();
@@ -192,33 +187,10 @@ class TableCreator
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,12 +198,7 @@ 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);
}
}
@@ -327,39 +294,4 @@ class TableCreator
}
}
- /**
- * 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;
- }
- }
}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
index 85991cb..fd55628 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
@@ -117,9 +117,9 @@ public class TableImpl implements Table
/** 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 +198,7 @@ public class TableImpl implements Table
}
_maxColumnCount = (short)_columns.size();
_maxVarColumnCount = (short)_varColumns.size();
- getAutoNumberColumns();
+ initAutoNumberColumns();
_fkEnforcer = null;
_flags = 0;
@@ -224,8 +224,13 @@ public class TableImpl implements Table
_name = name;
_flags = flags;
+ System.out.println("FOO " + _name + " tdefLen " + tableBuffer.getInt(8) +
+ " free " +
+ tableBuffer.getShort(database.getFormat().OFFSET_FREE_SPACE));
+
// 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,36 +258,13 @@ 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 ...
}
+ System.out.println("FOO done " + tableBuffer.position());
+
// re-sort columns if necessary
if(getDatabase().getColumnOrder() != ColumnOrder.DATA) {
Collections.sort(_columns, DISPLAY_ORDER_COMPARATOR);
@@ -512,6 +494,10 @@ public class TableImpl implements Table
return _logicalIndexCount;
}
+ List<ColumnImpl> getAutoNumberColumns() {
+ return _autoNumColumns;
+ }
+
public CursorImpl getDefaultCursor() {
if(_defaultCursor == null) {
_defaultCursor = CursorImpl.createCursor(this);
@@ -983,15 +969,11 @@ 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());
}
@@ -1032,19 +1014,46 @@ public class TableImpl implements Table
//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,
+ DBMutator mutator, List<Integer> reservedPages)
+ throws IOException
+ {
+ buffer.rewind();
+ int totalTableDefSize = buffer.remaining();
+ System.out.println("FOO writing tdef to " + tdefPageNumber + " and " +
+ reservedPages + " tot size " + 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 {
+ System.out.println("FOO splitting tdef");
+
// need to split across multiple pages
+
ByteBuffer partialTdef = pageChannel.createPageBuffer();
buffer.rewind();
int nextTdefPageNumber = PageChannel.INVALID_PAGE_NUMBER;
@@ -1057,7 +1066,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,20 +1082,197 @@ 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 TableMutator to this table.
+ * @usage _advanced_method_
+ */
+ protected ColumnImpl mutateAddColumn(TableMutator 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
+ int addedLen = 0;
+
+ if(isLongVal) {
+ addedLen += 10;
+ }
+
+ addedLen += format.SIZE_COLUMN_DEF_BLOCK;
+
+ int nameByteLen = DBMutator.calculateNameLength(column.getName());
+ addedLen += nameByteLen;
+
+ // load current table definition and add space for new info
+ List<Integer> nextPages = new ArrayList<Integer>(1);
+ ByteBuffer tableBuffer = loadCompleteTableDefinitionBufferForUpdate(
+ nextPages, addedLen);
+ int origTdefLen = tableBuffer.limit();
+
+ // 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
+ for(int i = 0; i < _columns.size(); ++i) {
+ ByteUtil.forward(tableBuffer, tableBuffer.getShort());
+ }
+ ByteUtil.insertEmptyData(tableBuffer, nameByteLen);
+ writeName(tableBuffer, column.getName(), mutator.getCharset());
+
+ int umapPos = -1;
+ if(isLongVal) {
+
+ // skip past index defs
+ ByteUtil.forward(tableBuffer, (_indexDatas.size() *
+ (format.SIZE_INDEX_DEFINITION +
+ format.SIZE_INDEX_COLUMN_BLOCK)));
+ ByteUtil.forward(tableBuffer,
+ (_indexes.size() * format.SIZE_INDEX_INFO_BLOCK));
+ for(int i = 0; i < _indexes.size(); ++i) {
+ ByteUtil.forward(tableBuffer, tableBuffer.getShort());
+ }
+
+ // FIXME add usage maps...
+ }
+
+ // sanity check the updates
+ if((origTdefLen + addedLen) != tableBuffer.limit()) {
+ throw new IllegalStateException(
+ withErrorContext("Failed update table definition"));
+ }
+
+ // before writing the new table def, create the column
+ ColumnImpl 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,
+ nextPages);
+
+
+ // 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);
+ }
+
+ // lastly, may need to clear table def buffer
+ _tableDefBufferH.possiblyInvalidate(_tableDefPageNumber, tableBuffer);
+
+ // update modification count so any active RowStates can keep themselves
+ // up-to-date
+ ++_modCount;
+
+ return newCol;
+ }
+
+ private ByteBuffer loadCompleteTableDefinitionBufferForUpdate(
+ List<Integer> nextPages, int addedLen)
+ throws IOException
+ {
+ // load complete table definition
+ ByteBuffer tableBuffer = _tableDefBufferH.setPage(getPageChannel(),
+ _tableDefPageNumber);
+ tableBuffer = loadCompleteTableDefinitionBuffer(tableBuffer, nextPages);
+
+ // make sure the table buffer has enough room for the new info
+ int origTdefLen = tableBuffer.getInt(8);
+ int newTdefLen = origTdefLen + addedLen;
+ System.out.println("FOO new " + newTdefLen + " add " + 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;
}
/**
@@ -1180,7 +1366,7 @@ public class TableImpl implements Table
} else {
// need another umap page
umapPageNumber = creator.reservePageNumber();
- }
+ }
freeSpace = format.DATA_PAGE_INITIAL_FREE_SPACE;
@@ -1247,7 +1433,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 +1442,7 @@ public class TableImpl implements Table
} else {
// lval column "free space pages" usage map (always on same page)
colState.setUmapFreeRowNumber((byte)umapRowNum);
- }
+ }
}
rowStart -= umapRowLength;
@@ -1278,27 +1464,36 @@ public class TableImpl implements Table
* 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;
+ System.out.println("FOO next page free " + nextPageBuffer.getShort(getFormat().OFFSET_FREE_SPACE));
+ 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 +1521,7 @@ public class TableImpl implements Table
}
Collections.sort(_columns);
- getAutoNumberColumns();
+ initAutoNumberColumns();
// setup the data index for the columns
int colIdx = 0;
@@ -1364,6 +1559,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 +2753,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 +2855,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 +2910,10 @@ public class TableImpl implements Table
reset();
_headerRowBufferH.invalidate();
_overflowRowBufferH.invalidate();
+ if(TableImpl.this._maxColumnCount != _rowValues.length) {
+ // columns added or removed from table
+ _rowValues = new Object[TableImpl.this._maxColumnCount];
+ }
_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..fe1500f
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableMutator.java
@@ -0,0 +1,102 @@
+/*
+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.EnumSet;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.healthmarketscience.jackcess.ColumnBuilder;
+import com.healthmarketscience.jackcess.DataType;
+
+/**
+ * Helper class used to maintain state during table mutation.
+ *
+ * @author James Ahlborn
+ * @usage _advanced_class_
+ */
+public class TableMutator extends DBMutator
+{
+ private final TableImpl _table;
+
+ private ColumnBuilder _column;
+ private ColumnState _columnState;
+
+ public TableMutator(TableImpl table) {
+ super(table.getDatabase());
+ _table = table;
+ }
+
+ public ColumnBuilder getColumn() {
+ return _column;
+ }
+
+ public ColumnImpl addColumn(ColumnBuilder column) throws IOException
+ {
+ _column = column;
+
+ validateAddColumn();
+
+ // assign column numbers and do some assorted column bookkeeping
+ short columnNumber = (short)_table.getMaxColumnCount();
+ _column.setColumnNumber(columnNumber);
+ if(_column.getType().isLongValue()) {
+ // only lval columns need extra state
+ _columnState = new ColumnState();
+ }
+
+ getPageChannel().startExclusiveWrite();
+ try {
+
+ return _table.mutateAddColumn(this);
+
+ } finally {
+ getPageChannel().finishWrite();
+ }
+ }
+
+ private void validateAddColumn()
+ {
+ if(_column == null) {
+ throw new IllegalArgumentException("Cannot add column with no column");
+ }
+ if((_table.getColumnCount() + 1) > getFormat().MAX_COLUMNS_PER_TABLE) {
+ throw new IllegalArgumentException(
+ "Cannot add column to table with " +
+ getFormat().MAX_COLUMNS_PER_TABLE + " columns");
+ }
+
+ Set<String> colNames = new HashSet<String>();
+ // next, validate the column definitions
+ for(ColumnImpl column : _table.getColumns()) {
+ colNames.add(column.getName().toUpperCase());
+ }
+
+ 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);
+ }
+ }
+}