From: James Ahlborn Date: Fri, 1 May 2015 00:45:53 +0000 (+0000) Subject: Allow optional direct insert/update of autonumber values. This is disabled by defaul... X-Git-Tag: jackcess-2.1.1~5 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=e8616e647606956573e6b33bfd0c098c63f3886e;p=jackcess.git Allow optional direct insert/update of autonumber values. This is disabled by default, but can be selectively enabled per-jvm (using system property), per-database, and per-table. fixes feature #32 git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@941 f203690c-595d-4dc9-a70b-905162fa7fd2 --- diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7d52603..0068167 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -9,6 +9,12 @@ Load linked table info from system table when reading databases with unsupported sort orders. + + Allow optional direct insert/update of autonumber values. This is + disabled by default, but can be selectively enabled per-jvm (using + system property), per-database, and per-table. + diff --git a/src/main/java/com/healthmarketscience/jackcess/Database.java b/src/main/java/com/healthmarketscience/jackcess/Database.java index 410ce0d..1e6d6cf 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Database.java +++ b/src/main/java/com/healthmarketscience/jackcess/Database.java @@ -117,6 +117,13 @@ public interface Database extends Iterable, Closeable, Flushable public static final String FK_ENFORCE_PROPERTY = "com.healthmarketscience.jackcess.enforceForeignKeys"; + /** system property which can be used to set the default allow auto number + * insert policy. Defaults to {@code false}. + * @usage _general_field_ + */ + public static final String ALLOW_AUTONUM_INSERT_PROPERTY = + "com.healthmarketscience.jackcess.allowAutoNumberInsert"; + /** * Enum which indicates which version of Access created the database. * @usage _general_class_ @@ -392,6 +399,31 @@ public interface Database extends Iterable
, Closeable, Flushable */ public void setEnforceForeignKeys(Boolean newEnforceForeignKeys); + /** + * Gets current allow auto number insert policy. By default, jackcess does + * not allow auto numbers to be inserted or updated directly (they are + * always handled internally by the Table). Setting this policy to {@code + * true} allows the caller to optionally set the value explicitly when + * adding or updating rows (if a value is not provided, it will still be + * handled internally by the Table). This value can be set database-wide + * using {@link #setAllowAutoNumberInsert} and/or on a per-table basis using + * {@link Table#setAllowAutoNumberInsert} (and/or on a jvm-wide using the + * {@link #ALLOW_AUTONUM_INSERT_PROPERTY} system property). Note that + * enabling this feature should be done with care to reduce the + * chances of screwing up the database. + * + * @usage _intermediate_method_ + */ + public boolean isAllowAutoNumberInsert(); + + /** + * Sets the new auto number insert policy for the database (unless + * overridden at the Table level). If {@code null}, resets to the default + * value. + * @usage _intermediate_method_ + */ + public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert); + /** * Gets currently configured ColumnValidatorFactory (always non-{@code null}). * @usage _intermediate_method_ diff --git a/src/main/java/com/healthmarketscience/jackcess/Table.java b/src/main/java/com/healthmarketscience/jackcess/Table.java index 6604491..bbb6885 100644 --- a/src/main/java/com/healthmarketscience/jackcess/Table.java +++ b/src/main/java/com/healthmarketscience/jackcess/Table.java @@ -98,6 +98,20 @@ public interface Table extends Iterable */ public void setErrorHandler(ErrorHandler newErrorHandler); + /** + * Gets the currently configured auto number insert policy. + * @see Database#isAllowAutoNumberInsert + * @usage _intermediate_method_ + */ + public boolean isAllowAutoNumberInsert(); + + /** + * Sets the new auto number insert policy for the Table. If {@code null}, + * resets to using the policy configured at the Database level. + * @usage _intermediate_method_ + */ + public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert); + /** * @return All of the columns in this table (unmodifiable List) * @usage _general_method_ diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java index d6c0908..b4b7cc2 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java @@ -151,7 +151,10 @@ public class ColumnImpl implements Column, Comparable { private static final char MIN_COMPRESS_CHAR = 1; private static final char MAX_COMPRESS_CHAR = 0xFF; - + /** auto numbers must be > 0 */ + static final int INVALID_AUTO_NUMBER = 0; + + /** owning table */ private final TableImpl _table; /** Whether or not the column is of variable length */ @@ -1843,7 +1846,17 @@ public class ColumnImpl implements Column, Comparable { * Warning, calling this externally will result in this value being * "lost" for the table. */ - public abstract Object getNext(Object prevRowValue); + public abstract Object getNext(TableImpl.WriteRowState writeRowState); + + /** + * Returns a valid autonumber for this generator. + *

+ * Warning, calling this externally may result in this value being + * "lost" for the table. + */ + public abstract Object handleInsert( + TableImpl.WriteRowState writeRowState, Object inRowValue) + throws IOException; /** * Restores a previous autonumber generated by this generator. @@ -1867,11 +1880,26 @@ public class ColumnImpl implements Column, Comparable { } @Override - public Object getNext(Object prevRowValue) { + public Object getNext(TableImpl.WriteRowState writeRowState) { // the table stores the last long autonumber used return getTable().getNextLongAutoNumber(); } + @Override + public Object handleInsert(TableImpl.WriteRowState writeRowState, + Object inRowValue) + throws IOException + { + int inAutoNum = toNumber(inRowValue).intValue(); + if(inAutoNum <= INVALID_AUTO_NUMBER) { + throw new IOException(withErrorContext( + "Invalid auto number value " + inAutoNum)); + } + // the table stores the last long autonumber used + getTable().adjustLongAutoNumber(inAutoNum); + return inAutoNum; + } + @Override public void restoreLast(Object last) { if(last instanceof Integer) { @@ -1897,12 +1925,21 @@ public class ColumnImpl implements Column, Comparable { } @Override - public Object getNext(Object prevRowValue) { + public Object getNext(TableImpl.WriteRowState writeRowState) { // format guids consistently w/ Column.readGUIDValue() _lastAutoNumber = "{" + UUID.randomUUID() + "}"; return _lastAutoNumber; } + @Override + public Object handleInsert(TableImpl.WriteRowState writeRowState, + Object inRowValue) + throws IOException + { + _lastAutoNumber = toCharSequence(inRowValue); + return _lastAutoNumber; + } + @Override public void restoreLast(Object last) { _lastAutoNumber = null; @@ -1925,14 +1962,54 @@ public class ColumnImpl implements Column, Comparable { } @Override - public Object getNext(Object prevRowValue) { - int nextComplexAutoNum = - ((prevRowValue == null) ? - // the table stores the last ComplexType autonumber used - getTable().getNextComplexTypeAutoNumber() : - // same value is shared across all ComplexType values in a row - ((ComplexValueForeignKey)prevRowValue).get()); - return new ComplexValueForeignKeyImpl(ColumnImpl.this, nextComplexAutoNum); + public Object getNext(TableImpl.WriteRowState writeRowState) { + // same value is shared across all ComplexType values in a row + int nextComplexAutoNum = writeRowState.getComplexAutoNumber(); + if(nextComplexAutoNum <= INVALID_AUTO_NUMBER) { + // the table stores the last ComplexType autonumber used + nextComplexAutoNum = getTable().getNextComplexTypeAutoNumber(); + writeRowState.setComplexAutoNumber(nextComplexAutoNum); + } + return new ComplexValueForeignKeyImpl(ColumnImpl.this, + nextComplexAutoNum); + } + + @Override + public Object handleInsert(TableImpl.WriteRowState writeRowState, + Object inRowValue) + throws IOException + { + ComplexValueForeignKey inComplexFK = null; + if(inRowValue instanceof ComplexValueForeignKey) { + inComplexFK = (ComplexValueForeignKey)inRowValue; + } else { + inComplexFK = new ComplexValueForeignKeyImpl( + ColumnImpl.this, toNumber(inRowValue).intValue()); + } + + if(inComplexFK.getColumn() != ColumnImpl.this) { + throw new IOException(withErrorContext( + "Wrong column for complex value foreign key, found " + + inComplexFK.getColumn().getName())); + } + if(inComplexFK.get() < 1) { + throw new IOException(withErrorContext( + "Invalid complex value foreign key value " + inComplexFK.get())); + } + // same value is shared across all ComplexType values in a row + int prevRowValue = writeRowState.getComplexAutoNumber(); + if(prevRowValue <= INVALID_AUTO_NUMBER) { + writeRowState.setComplexAutoNumber(inComplexFK.get()); + } else if(prevRowValue != inComplexFK.get()) { + throw new IOException(withErrorContext( + "Inconsistent complex value foreign key values: found " + + prevRowValue + ", given " + inComplexFK)); + } + + // the table stores the last ComplexType autonumber used + getTable().adjustComplexTypeAutoNumber(inComplexFK.get()); + + return inComplexFK; } @Override @@ -1963,7 +2040,13 @@ public class ColumnImpl implements Column, Comparable { } @Override - public Object getNext(Object prevRowValue) { + public Object getNext(TableImpl.WriteRowState writeRowState) { + throw new UnsupportedOperationException(); + } + + @Override + public Object handleInsert(TableImpl.WriteRowState writeRowState, + Object inRowValue) { throw new UnsupportedOperationException(); } diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java index 47acb10..8cf809d 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java @@ -301,6 +301,8 @@ public class DatabaseImpl implements Database private Table.ColumnOrder _columnOrder; /** whether or not enforcement of foreign-keys is enabled */ private boolean _enforceForeignKeys; + /** whether or not auto numbers can be directly inserted by the user */ + private boolean _allowAutoNumInsert; /** factory for ColumnValidators */ private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE; /** cache of in-use tables */ @@ -501,6 +503,7 @@ public class DatabaseImpl implements Database _charset = ((charset == null) ? getDefaultCharset(_format) : charset); _columnOrder = getDefaultColumnOrder(); _enforceForeignKeys = getDefaultEnforceForeignKeys(); + _allowAutoNumInsert = getDefaultAllowAutoNumberInsert(); _fileFormat = fileFormat; _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone); @@ -662,6 +665,18 @@ public class DatabaseImpl implements Database _enforceForeignKeys = newEnforceForeignKeys; } + public boolean isAllowAutoNumberInsert() { + return _allowAutoNumInsert; + } + + public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert) { + if(allowAutoNumInsert == null) { + allowAutoNumInsert = getDefaultAllowAutoNumberInsert(); + } + _allowAutoNumInsert = allowAutoNumInsert; + } + + public ColumnValidatorFactory getColumnValidatorFactory() { return _validatorFactory; } @@ -1743,6 +1758,21 @@ public class DatabaseImpl implements Database return true; } + /** + * Returns the default allow auto number insert policy. This defaults to + * {@code false}, but can be overridden using the system + * property {@value com.healthmarketscience.jackcess.Database#ALLOW_AUTONUM_INSERT_PROPERTY}. + * @usage _advanced_method_ + */ + public static boolean getDefaultAllowAutoNumberInsert() + { + String prop = System.getProperty(ALLOW_AUTONUM_INSERT_PROPERTY); + if(prop != null) { + return Boolean.TRUE.toString().equalsIgnoreCase(prop); + } + return false; + } + /** * Copies the given InputStream to the given channel using the most * efficient means possible. diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java index 0e0488d..e295316 100644 --- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java +++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java @@ -38,7 +38,6 @@ import com.healthmarketscience.jackcess.Column; import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.ConstraintViolationException; import com.healthmarketscience.jackcess.CursorBuilder; -import com.healthmarketscience.jackcess.DataType; import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.JackcessException; import com.healthmarketscience.jackcess.PropertyMap; @@ -166,6 +165,9 @@ public class TableImpl implements Table private PropertyMap _props; /** properties group for this table (and columns) */ private PropertyMaps _propertyMaps; + /** optional flag indicating whether or not auto numbers can be directly + inserted by the user */ + private Boolean _allowAutoNumInsert; /** foreign-key enforcer for this table */ private final FKEnforcer _fkEnforcer; @@ -356,6 +358,15 @@ public class TableImpl implements Table return _tableDefPageNumber; } + public boolean isAllowAutoNumberInsert() { + return ((_allowAutoNumInsert != null) ? (boolean)_allowAutoNumInsert : + getDatabase().isAllowAutoNumberInsert()); + } + + public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert) { + _allowAutoNumInsert = allowAutoNumInsert; + } + /** * @usage _advanced_method_ */ @@ -1079,7 +1090,6 @@ public class TableImpl implements Table /** * @param buffer Buffer to write to - * @param columns List of Columns in the table */ private static void writeTableDefinitionHeader( TableCreator creator, ByteBuffer buffer, int totalTableDefSize) @@ -1499,7 +1509,7 @@ public class TableImpl implements Table /** * Add multiple rows to this table, only writing to disk after all * rows have been written, and every time a data page is filled. - * @param inRows List of Object[] row values + * @param rows List of Object[] row values */ private List addRows(List rows, final boolean isBatchWrite) @@ -1516,6 +1526,8 @@ public class TableImpl implements Table int pageNumber = PageChannel.INVALID_PAGE_NUMBER; int updateCount = 0; int autoNumAssignCount = 0; + WriteRowState writeRowState = + (!_autoNumColumns.isEmpty() ? new WriteRowState() : null); try { List dupeRows = null; @@ -1549,7 +1561,7 @@ public class TableImpl implements Table } // fill in autonumbers - handleAutoNumbersForAdd(row); + handleAutoNumbersForAdd(row, writeRowState); ++autoNumAssignCount; // write the row of data to a temporary buffer @@ -1758,45 +1770,44 @@ public class TableImpl implements Table // handle various value massaging activities for(ColumnImpl column : _columns) { - - Object rowValue = null; + if(column.isAutoNumber()) { - - // fill in any auto-numbers (we don't allow autonumber values to be - // modified) - rowValue = getRowColumn(getFormat(), rowBuffer, column, rowState, null); - - } else { + // handle these separately (below) + continue; + } - rowValue = column.getRowValue(row); - if(rowValue == Column.KEEP_VALUE) { + Object rowValue = column.getRowValue(row); + if(rowValue == Column.KEEP_VALUE) { - // fill in any "keep value" fields (restore old value) - rowValue = getRowColumn(getFormat(), rowBuffer, column, rowState, - keepRawVarValues); + // fill in any "keep value" fields (restore old value) + rowValue = getRowColumn(getFormat(), rowBuffer, column, rowState, + keepRawVarValues); - } else { - - // set oldValue to something that could not possibly be a real value - Object oldValue = Column.KEEP_VALUE; - if(_indexColumns.contains(column)) { - // read (old) row value to help update indexes - oldValue = getRowColumn(getFormat(), rowBuffer, column, rowState, null); - } else { - oldValue = rowState.getRowCacheValue(column.getColumnIndex()); - } + } else { - // if the old value was passed back in, we don't need to validate - if(oldValue != rowValue) { - // pass input value through column validator - rowValue = column.validate(rowValue); - } + // set oldValue to something that could not possibly be a real value + Object oldValue = Column.KEEP_VALUE; + if(_indexColumns.contains(column)) { + // read (old) row value to help update indexes + oldValue = getRowColumn(getFormat(), rowBuffer, column, rowState, + null); + } else { + oldValue = rowState.getRowCacheValue(column.getColumnIndex()); } + + // if the old value was passed back in, we don't need to validate + if(oldValue != rowValue) { + // pass input value through column validator + rowValue = column.validate(rowValue); + } } column.setRowValue(row, rowValue); } + // fill in autonumbers + handleAutoNumbersForUpdate(row, rowBuffer, rowState); + // generate new row bytes ByteBuffer newRowData = createRow( row, _writeRowBufferH.getPageBuffer(getPageChannel()), oldRowSize, @@ -2178,32 +2189,79 @@ public class TableImpl implements Table } /** - * Fill in all autonumber column values. + * Fill in all autonumber column values for add. */ - private void handleAutoNumbersForAdd(Object[] row) + private void handleAutoNumbersForAdd(Object[] row, WriteRowState writeRowState) throws IOException { if(_autoNumColumns.isEmpty()) { return; } - - Object complexAutoNumber = null; + + boolean enableInsert = isAllowAutoNumberInsert(); + writeRowState.resetAutoNumber(); for(ColumnImpl col : _autoNumColumns) { - // ignore given row value, use next autonumber + + // ignore input row value, use original row value (unless explicitly + // enabled) + Object inRowValue = getInputAutoNumberRowValue(enableInsert, col, row); + ColumnImpl.AutoNumberGenerator autoNumGen = col.getAutoNumberGenerator(); - Object rowValue = null; - if(autoNumGen.getType() != DataType.COMPLEX_TYPE) { - rowValue = autoNumGen.getNext(null); - } else { - // complex type auto numbers are shared across all complex columns - // in the row - complexAutoNumber = autoNumGen.getNext(complexAutoNumber); - rowValue = complexAutoNumber; - } + Object rowValue = ((inRowValue == null) ? + autoNumGen.getNext(writeRowState) : + autoNumGen.handleInsert(writeRowState, inRowValue)); + col.setRowValue(row, rowValue); } } + /** + * Fill in all autonumber column values for update. + */ + private void handleAutoNumbersForUpdate(Object[] row, ByteBuffer rowBuffer, + RowState rowState) + throws IOException + { + if(_autoNumColumns.isEmpty()) { + return; + } + + boolean enableInsert = isAllowAutoNumberInsert(); + rowState.resetAutoNumber(); + for(ColumnImpl col : _autoNumColumns) { + + // ignore input row value, use original row value (unless explicitly + // enabled) + Object inRowValue = getInputAutoNumberRowValue(enableInsert, col, row); + + Object rowValue = + ((inRowValue == null) ? + getRowColumn(getFormat(), rowBuffer, col, rowState, null) : + col.getAutoNumberGenerator().handleInsert(rowState, inRowValue)); + + col.setRowValue(row, rowValue); + } + } + + /** + * Optionally get the input autonumber row value for the given column from + * the given row if one was provided. + */ + private static Object getInputAutoNumberRowValue( + boolean enableInsert, ColumnImpl col, Object[] row) + { + if(!enableInsert) { + return null; + } + + Object inRowValue = col.getRowValue(row); + if((inRowValue == Column.KEEP_VALUE) || (inRowValue == Column.AUTO_NUMBER)) { + // these "special" values both behave like nothing was given + inRowValue = null; + } + return inRowValue; + } + /** * Restores all autonumber column values from a failed add row. */ @@ -2246,6 +2304,12 @@ public class TableImpl implements Table return _lastLongAutoNumber; } + void adjustLongAutoNumber(int inLongAutoNumber) { + if(inLongAutoNumber > _lastLongAutoNumber) { + _lastLongAutoNumber = inLongAutoNumber; + } + } + void restoreLastLongAutoNumber(int lastLongAutoNumber) { // restores the last used auto number _lastLongAutoNumber = lastLongAutoNumber - 1; @@ -2261,6 +2325,12 @@ public class TableImpl implements Table return _lastComplexTypeAutoNumber; } + void adjustComplexTypeAutoNumber(int inComplexTypeAutoNumber) { + if(inComplexTypeAutoNumber > _lastComplexTypeAutoNumber) { + _lastComplexTypeAutoNumber = inComplexTypeAutoNumber; + } + } + void restoreLastComplexTypeAutoNumber(int lastComplexTypeAutoNumber) { // restores the last used auto number _lastComplexTypeAutoNumber = lastComplexTypeAutoNumber - 1; @@ -2501,10 +2571,31 @@ public class TableImpl implements Table } /** - * Maintains the state of reading a row of data. + * Maintains state for writing a new row of data. + */ + protected static class WriteRowState + { + private int _complexAutoNumber = ColumnImpl.INVALID_AUTO_NUMBER; + + public int getComplexAutoNumber() { + return _complexAutoNumber; + } + + public void setComplexAutoNumber(int complexAutoNumber) { + _complexAutoNumber = complexAutoNumber; + } + + public void resetAutoNumber() { + _complexAutoNumber = ColumnImpl.INVALID_AUTO_NUMBER; + } + } + + /** + * Maintains the state of reading/updating a row of data. * @usage _advanced_class_ */ - public final class RowState implements ErrorHandler.Location + public final class RowState extends WriteRowState + implements ErrorHandler.Location { /** Buffer used for reading the header row data pages */ private final TempPageHolder _headerRowBufferH; @@ -2560,6 +2651,7 @@ public class TableImpl implements Table } public void reset() { + resetAutoNumber(); _finalRowId = null; _finalRowBuffer = null; _rowsOnHeaderPage = 0; diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java index 6deb3b9..918271a 100644 --- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java +++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java @@ -619,82 +619,6 @@ public class DatabaseTest extends TestCase } } - public void testAutoNumber() throws Exception { - for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { - Database db = createMem(fileFormat); - - Table table = new TableBuilder("test") - .addColumn(new ColumnBuilder("a", DataType.LONG) - .setAutoNumber(true)) - .addColumn(new ColumnBuilder("b", DataType.TEXT)) - .toTable(db); - - doTestAutoNumber(table); - - db.close(); - } - } - - public void testAutoNumberPK() throws Exception { - for (final TestDB testDB : SUPPORTED_DBS_TEST) { - Database db = openMem(testDB); - - Table table = db.getTable("Table3"); - - doTestAutoNumber(table); - - db.close(); - } - } - - private void doTestAutoNumber(Table table) throws Exception - { - Object[] row = {null, "row1"}; - assertSame(row, table.addRow(row)); - assertEquals(1, ((Integer)row[0]).intValue()); - row = table.addRow(13, "row2"); - assertEquals(2, ((Integer)row[0]).intValue()); - row = table.addRow("flubber", "row3"); - assertEquals(3, ((Integer)row[0]).intValue()); - - table.reset(); - - row = table.addRow(Column.AUTO_NUMBER, "row4"); - assertEquals(4, ((Integer)row[0]).intValue()); - row = table.addRow(Column.AUTO_NUMBER, "row5"); - assertEquals(5, ((Integer)row[0]).intValue()); - - Object[] smallRow = {Column.AUTO_NUMBER}; - row = table.addRow(smallRow); - assertNotSame(row, smallRow); - assertEquals(6, ((Integer)row[0]).intValue()); - - table.reset(); - - List> expectedRows = - createExpectedTable( - createExpectedRow( - "a", 1, - "b", "row1"), - createExpectedRow( - "a", 2, - "b", "row2"), - createExpectedRow( - "a", 3, - "b", "row3"), - createExpectedRow( - "a", 4, - "b", "row4"), - createExpectedRow( - "a", 5, - "b", "row5"), - createExpectedRow( - "a", 6, - "b", null)); - - assertTable(expectedRows, table); - } - public void testWriteAndReadDate() throws Exception { for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { Database db = createMem(fileFormat); diff --git a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java index f7a24f0..c6856bc 100644 --- a/src/test/java/com/healthmarketscience/jackcess/TestUtil.java +++ b/src/test/java/com/healthmarketscience/jackcess/TestUtil.java @@ -254,17 +254,17 @@ public class TestUtil return Arrays.asList(rows); } - static void dumpDatabase(Database mdb) throws Exception { + public static void dumpDatabase(Database mdb) throws Exception { dumpDatabase(mdb, false); } - static void dumpDatabase(Database mdb, boolean systemTables) + public static void dumpDatabase(Database mdb, boolean systemTables) throws Exception { dumpDatabase(mdb, systemTables, new PrintWriter(System.out, true)); } - static void dumpTable(Table table) throws Exception { + public static void dumpTable(Table table) throws Exception { dumpTable(table, new PrintWriter(System.out, true)); } diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/AutoNumberTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/AutoNumberTest.java new file mode 100644 index 0000000..37eec77 --- /dev/null +++ b/src/test/java/com/healthmarketscience/jackcess/impl/AutoNumberTest.java @@ -0,0 +1,499 @@ +/* +Copyright (c) 2015 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.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.healthmarketscience.jackcess.Column; +import com.healthmarketscience.jackcess.ColumnBuilder; +import com.healthmarketscience.jackcess.CursorBuilder; +import com.healthmarketscience.jackcess.DataType; +import com.healthmarketscience.jackcess.Database; +import static com.healthmarketscience.jackcess.Database.*; +import com.healthmarketscience.jackcess.Row; +import com.healthmarketscience.jackcess.Table; +import com.healthmarketscience.jackcess.TableBuilder; +import static com.healthmarketscience.jackcess.TestUtil.*; +import com.healthmarketscience.jackcess.complex.ComplexValueForeignKey; +import static com.healthmarketscience.jackcess.impl.JetFormatTest.*; +import junit.framework.TestCase; + +/** + * + * @author James Ahlborn + */ +public class AutoNumberTest extends TestCase +{ + + public AutoNumberTest(String name) throws Exception { + super(name); + } + + + public void testAutoNumber() throws Exception + { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("a", DataType.LONG) + .setAutoNumber(true)) + .addColumn(new ColumnBuilder("b", DataType.TEXT)) + .toTable(db); + + doTestAutoNumber(table); + + db.close(); + } + } + + public void testAutoNumberPK() throws Exception + { + for (final TestDB testDB : SUPPORTED_DBS_TEST) { + Database db = openMem(testDB); + + Table table = db.getTable("Table3"); + + doTestAutoNumber(table); + + db.close(); + } + } + + private static void doTestAutoNumber(Table table) throws Exception + { + Object[] row = {null, "row1"}; + assertSame(row, table.addRow(row)); + assertEquals(1, ((Integer)row[0]).intValue()); + row = table.addRow(13, "row2"); + assertEquals(2, ((Integer)row[0]).intValue()); + row = table.addRow("flubber", "row3"); + assertEquals(3, ((Integer)row[0]).intValue()); + + table.reset(); + + row = table.addRow(Column.AUTO_NUMBER, "row4"); + assertEquals(4, ((Integer)row[0]).intValue()); + row = table.addRow(Column.AUTO_NUMBER, "row5"); + assertEquals(5, ((Integer)row[0]).intValue()); + + Object[] smallRow = {Column.AUTO_NUMBER}; + row = table.addRow(smallRow); + assertNotSame(row, smallRow); + assertEquals(6, ((Integer)row[0]).intValue()); + + table.reset(); + + List> expectedRows = + createExpectedTable( + createExpectedRow( + "a", 1, + "b", "row1"), + createExpectedRow( + "a", 2, + "b", "row2"), + createExpectedRow( + "a", 3, + "b", "row3"), + createExpectedRow( + "a", 4, + "b", "row4"), + createExpectedRow( + "a", 5, + "b", "row5"), + createExpectedRow( + "a", 6, + "b", null)); + + assertTable(expectedRows, table); + } + + public void testAutoNumberGuid() throws Exception + { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("a", DataType.GUID) + .setAutoNumber(true)) + .addColumn(new ColumnBuilder("b", DataType.TEXT)) + .toTable(db); + + Object[] row = {null, "row1"}; + assertSame(row, table.addRow(row)); + assertTrue(ColumnImpl.isGUIDValue(row[0])); + row = table.addRow(13, "row2"); + assertTrue(ColumnImpl.isGUIDValue(row[0])); + row = table.addRow("flubber", "row3"); + assertTrue(ColumnImpl.isGUIDValue(row[0])); + + Object[] smallRow = {Column.AUTO_NUMBER}; + row = table.addRow(smallRow); + assertNotSame(row, smallRow); + assertTrue(ColumnImpl.isGUIDValue(row[0])); + + db.close(); + } + } + + public void testInsertLongAutoNumber() throws Exception + { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("a", DataType.LONG) + .setAutoNumber(true)) + .addColumn(new ColumnBuilder("b", DataType.TEXT)) + .toTable(db); + + doTestInsertLongAutoNumber(table); + + db.close(); + } + } + + public void testInsertLongAutoNumberPK() throws Exception + { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("a", DataType.LONG) + .setAutoNumber(true)) + .addColumn(new ColumnBuilder("b", DataType.TEXT)) + .setPrimaryKey("a") + .toTable(db); + + doTestInsertLongAutoNumber(table); + + db.close(); + } + } + + private static void doTestInsertLongAutoNumber(Table table) throws Exception + { + assertFalse(table.getDatabase().isAllowAutoNumberInsert()); + assertFalse(table.isAllowAutoNumberInsert()); + + Object[] row = {null, "row1"}; + assertSame(row, table.addRow(row)); + assertEquals(1, ((Integer)row[0]).intValue()); + row = table.addRow(13, "row2"); + assertEquals(2, ((Integer)row[0]).intValue()); + row = table.addRow("flubber", "row3"); + assertEquals(3, ((Integer)row[0]).intValue()); + + table.reset(); + + table.setAllowAutoNumberInsert(true); + assertFalse(table.getDatabase().isAllowAutoNumberInsert()); + assertTrue(table.isAllowAutoNumberInsert()); + + Row row2 = CursorBuilder.findRow( + table, Collections.singletonMap("a", 2)); + assertEquals("row2", row2.getString("b")); + + table.deleteRow(row2); + + row = table.addRow(Column.AUTO_NUMBER, "row4"); + assertEquals(4, ((Integer)row[0]).intValue()); + + assertEquals(4, ((TableImpl)table).getLastLongAutoNumber()); + + row = table.addRow(2, "row2-redux"); + assertEquals(2, ((Integer)row[0]).intValue()); + + assertEquals(4, ((TableImpl)table).getLastLongAutoNumber()); + + row2 = CursorBuilder.findRow( + table, Collections.singletonMap("a", 2)); + assertEquals("row2-redux", row2.getString("b")); + + row = table.addRow(13, "row13-mindthegap"); + assertEquals(13, ((Integer)row[0]).intValue()); + + assertEquals(13, ((TableImpl)table).getLastLongAutoNumber()); + + try { + table.addRow("not a number", "nope"); + fail("NumberFormatException should have been thrown"); + } catch(NumberFormatException e) { + // success + } + + assertEquals(13, ((TableImpl)table).getLastLongAutoNumber()); + + try { + table.addRow(-10, "uh-uh"); + fail("IOException should have been thrown"); + } catch(IOException e) { + // success + } + + row = table.addRow(Column.AUTO_NUMBER, "row14"); + assertEquals(14, ((Integer)row[0]).intValue()); + + Row row13 = CursorBuilder.findRow( + table, Collections.singletonMap("a", 13)); + assertEquals("row13-mindthegap", row13.getString("b")); + + row13.put("a", "45"); + row13 = table.updateRow(row13); + assertEquals(45, row13.get("a")); + + assertEquals(45, ((TableImpl)table).getLastLongAutoNumber()); + + row13.put("a", -1); + + try { + table.updateRow(row13); + fail("IOException should have been thrown"); + } catch(IOException e) { + // success + } + + assertEquals(45, ((TableImpl)table).getLastLongAutoNumber()); + + row13.put("a", 55); + + table.setAllowAutoNumberInsert(null); + + row13 = table.updateRow(row13); + assertEquals(45, row13.get("a")); + + assertEquals(45, ((TableImpl)table).getLastLongAutoNumber()); + + } + + public void testInsertComplexAutoNumber() throws Exception + { + for(final TestDB testDB : TestDB.getSupportedForBasename(Basename.COMPLEX)) { + + Database db = openMem(testDB); + + Table t1 = db.getTable("Table1"); + + assertFalse(t1.isAllowAutoNumberInsert()); + + int lastAutoNum = ((TableImpl)t1).getLastComplexTypeAutoNumber(); + + Object[] row = t1.addRow("arow"); + ++lastAutoNum; + checkAllComplexAutoNums(lastAutoNum, row); + + assertEquals(lastAutoNum, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + db.setAllowAutoNumberInsert(true); + assertTrue(db.isAllowAutoNumberInsert()); + assertTrue(t1.isAllowAutoNumberInsert()); + + row = t1.addRow("anotherrow"); + ++lastAutoNum; + checkAllComplexAutoNums(lastAutoNum, row); + + assertEquals(lastAutoNum, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + row = t1.addRow("row5", 5, null, null, 5, 5); + checkAllComplexAutoNums(5, row); + + assertEquals(lastAutoNum, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + row = t1.addRow("row13", 13, null, null, 13, 13); + checkAllComplexAutoNums(13, row); + + assertEquals(13, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + try { + t1.addRow("nope", "not a number"); + fail("NumberFormatException should have been thrown"); + } catch(NumberFormatException e) { + // success + } + + assertEquals(13, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + try { + t1.addRow("uh-uh", -10); + fail("IOException should have been thrown"); + } catch(IOException e) { + // success + } + + assertEquals(13, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + try { + t1.addRow("wut", 6, null, null, 40, 42); + fail("IOException should have been thrown"); + } catch(IOException e) { + // success + } + + row = t1.addRow("morerows"); + checkAllComplexAutoNums(14, row); + + assertEquals(14, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + Row row13 = CursorBuilder.findRow( + t1, Collections.singletonMap("id", "row13")); + + row13.put("VersionHistory_F5F8918F-0A3F-4DA9-AE71-184EE5012880", "45"); + row13.put("multi-value-data", "45"); + row13.put("attach-data", "45"); + row13 = t1.updateRow(row13); + checkAllComplexAutoNums(45, row13); + + assertEquals(45, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + row13.put("attach-data", -1); + + try { + t1.updateRow(row13); + fail("IOException should have been thrown"); + } catch(IOException e) { + // success + } + + assertEquals(45, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + row13.put("attach-data", 55); + + try { + t1.updateRow(row13); + fail("IOException should have been thrown"); + } catch(IOException e) { + // success + } + + assertEquals(45, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + row13.put("VersionHistory_F5F8918F-0A3F-4DA9-AE71-184EE5012880", 55); + row13.put("multi-value-data", 55); + + db.setAllowAutoNumberInsert(null); + + row13 = t1.updateRow(row13); + checkAllComplexAutoNums(45, row13); + + assertEquals(45, ((TableImpl)t1).getLastComplexTypeAutoNumber()); + + db.close(); + } + } + + private static void checkAllComplexAutoNums(int expected, Object[] row) + { + assertEquals(expected, ((ComplexValueForeignKey)row[1]).get()); + assertEquals(expected, ((ComplexValueForeignKey)row[4]).get()); + assertEquals(expected, ((ComplexValueForeignKey)row[5]).get()); + } + + private static void checkAllComplexAutoNums(int expected, Row row) + { + assertEquals(expected, ((Number)row.get("VersionHistory_F5F8918F-0A3F-4DA9-AE71-184EE5012880")).intValue()); + assertEquals(expected, ((Number)row.get("multi-value-data")).intValue()); + assertEquals(expected, ((Number)row.get("attach-data")).intValue()); + } + + public void testInsertGuidAutoNumber() throws Exception + { + for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { + Database db = createMem(fileFormat); + + Table table = new TableBuilder("test") + .addColumn(new ColumnBuilder("a", DataType.GUID) + .setAutoNumber(true)) + .addColumn(new ColumnBuilder("b", DataType.TEXT)) + .toTable(db); + + db.setAllowAutoNumberInsert(true); + table.setAllowAutoNumberInsert(false); + assertFalse(table.isAllowAutoNumberInsert()); + + Object[] row = {null, "row1"}; + assertSame(row, table.addRow(row)); + assertTrue(ColumnImpl.isGUIDValue(row[0])); + row = table.addRow(13, "row2"); + assertTrue(ColumnImpl.isGUIDValue(row[0])); + row = table.addRow("flubber", "row3"); + assertTrue(ColumnImpl.isGUIDValue(row[0])); + + Object[] smallRow = {Column.AUTO_NUMBER}; + row = table.addRow(smallRow); + assertNotSame(row, smallRow); + assertTrue(ColumnImpl.isGUIDValue(row[0])); + + table.setAllowAutoNumberInsert(null); + assertTrue(table.isAllowAutoNumberInsert()); + + Row row2 = CursorBuilder.findRow( + table, Collections.singletonMap("b", "row2")); + assertEquals("row2", row2.getString("b")); + + String row2Guid = row2.getString("a"); + table.deleteRow(row2); + + row = table.addRow(Column.AUTO_NUMBER, "row4"); + assertTrue(ColumnImpl.isGUIDValue(row[0])); + + row = table.addRow(row2Guid, "row2-redux"); + assertEquals(row2Guid, row[0]); + + row2 = CursorBuilder.findRow( + table, Collections.singletonMap("a", row2Guid)); + assertEquals("row2-redux", row2.getString("b")); + + try { + table.addRow("not a guid", "nope"); + fail("IOException should have been thrown"); + } catch(IOException e) { + // success + } + + row = table.addRow(Column.AUTO_NUMBER, "row5"); + assertTrue(ColumnImpl.isGUIDValue(row[0])); + + row2Guid = UUID.randomUUID().toString(); + row2.put("a", row2Guid); + + row2 = table.updateRow(row2); + assertEquals(row2Guid, row2.get("a")); + + row2.put("a", "not a guid"); + + try { + table.updateRow(row2); + fail("IOException should have been thrown"); + } catch(IOException e) { + // success + } + + table.setAllowAutoNumberInsert(false); + + row2 = table.updateRow(row2); + assertTrue(ColumnImpl.isGUIDValue(row2.get("a"))); + assertFalse(row2Guid.equals(row2.get("a"))); + + db.close(); + } + } + +}