git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@941 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-2.1.1
@@ -9,6 +9,12 @@ | |||
Load linked table info from system table when reading databases with | |||
unsupported sort orders. | |||
</action> | |||
<action dev="jahlborn" type="update" system="SourceForge2Features" | |||
issue="32"> | |||
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. | |||
</action> | |||
</release> | |||
<release version="2.1.0" date="2015-04-16" | |||
description="Relicense to Apache License"> |
@@ -117,6 +117,13 @@ public interface Database extends Iterable<Table>, 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<Table>, 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 | |||
* <i>enabling this feature should be done with care</i> 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_ |
@@ -98,6 +98,20 @@ public interface Table extends Iterable<Row> | |||
*/ | |||
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_ |
@@ -151,7 +151,10 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> { | |||
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<ColumnImpl> { | |||
* <i>Warning, calling this externally will result in this value being | |||
* "lost" for the table.</i> | |||
*/ | |||
public abstract Object getNext(Object prevRowValue); | |||
public abstract Object getNext(TableImpl.WriteRowState writeRowState); | |||
/** | |||
* Returns a valid autonumber for this generator. | |||
* <p> | |||
* <i>Warning, calling this externally may result in this value being | |||
* "lost" for the table.</i> | |||
*/ | |||
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<ColumnImpl> { | |||
} | |||
@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<ColumnImpl> { | |||
} | |||
@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<ColumnImpl> { | |||
} | |||
@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<ColumnImpl> { | |||
} | |||
@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(); | |||
} | |||
@@ -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. |
@@ -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<? extends Object[]> addRows(List<? extends Object[]> 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<Object[]> 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; |
@@ -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<? extends Map<String, Object>> 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); |
@@ -254,17 +254,17 @@ public class TestUtil | |||
return Arrays.<Row>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)); | |||
} | |||
@@ -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<? extends Map<String, Object>> 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(); | |||
} | |||
} | |||
} |