diff options
author | James Ahlborn <jtahlborn@yahoo.com> | 2016-04-28 23:04:04 +0000 |
---|---|---|
committer | James Ahlborn <jtahlborn@yahoo.com> | 2016-04-28 23:04:04 +0000 |
commit | 595044d1a5f38da203a83d7c870bb5001aea1a59 (patch) | |
tree | 91cef7a1327f3acf1b1da444325088ab02842aa7 /src/main/java | |
parent | 631d2c1901f2a4ed066b8ad3091ae6d46a793f56 (diff) | |
download | jackcess-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')
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); + } + } +} |