Просмотр исходного кода

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
tags/jackcess-2.1.1
James Ahlborn 9 лет назад
Родитель
Сommit
e8616e6476

+ 6
- 0
src/changes/changes.xml Просмотреть файл

Load linked table info from system table when reading databases with Load linked table info from system table when reading databases with
unsupported sort orders. unsupported sort orders.
</action> </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>
<release version="2.1.0" date="2015-04-16" <release version="2.1.0" date="2015-04-16"
description="Relicense to Apache License"> description="Relicense to Apache License">

+ 32
- 0
src/main/java/com/healthmarketscience/jackcess/Database.java Просмотреть файл

public static final String FK_ENFORCE_PROPERTY = public static final String FK_ENFORCE_PROPERTY =
"com.healthmarketscience.jackcess.enforceForeignKeys"; "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. * Enum which indicates which version of Access created the database.
* @usage _general_class_ * @usage _general_class_
*/ */
public void setEnforceForeignKeys(Boolean newEnforceForeignKeys); 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}). * Gets currently configured ColumnValidatorFactory (always non-{@code null}).
* @usage _intermediate_method_ * @usage _intermediate_method_

+ 14
- 0
src/main/java/com/healthmarketscience/jackcess/Table.java Просмотреть файл

*/ */
public void setErrorHandler(ErrorHandler newErrorHandler); 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) * @return All of the columns in this table (unmodifiable List)
* @usage _general_method_ * @usage _general_method_

+ 96
- 13
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java Просмотреть файл

private static final char MIN_COMPRESS_CHAR = 1; private static final char MIN_COMPRESS_CHAR = 1;
private static final char MAX_COMPRESS_CHAR = 0xFF; private static final char MAX_COMPRESS_CHAR = 0xFF;


/** auto numbers must be > 0 */
static final int INVALID_AUTO_NUMBER = 0;


/** owning table */ /** owning table */
private final TableImpl _table; private final TableImpl _table;
/** Whether or not the column is of variable length */ /** Whether or not the column is of variable length */
* <i>Warning, calling this externally will result in this value being * <i>Warning, calling this externally will result in this value being
* "lost" for the table.</i> * "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. * Restores a previous autonumber generated by this generator.
} }


@Override @Override
public Object getNext(Object prevRowValue) {
public Object getNext(TableImpl.WriteRowState writeRowState) {
// the table stores the last long autonumber used // the table stores the last long autonumber used
return getTable().getNextLongAutoNumber(); 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 @Override
public void restoreLast(Object last) { public void restoreLast(Object last) {
if(last instanceof Integer) { if(last instanceof Integer) {
} }


@Override @Override
public Object getNext(Object prevRowValue) {
public Object getNext(TableImpl.WriteRowState writeRowState) {
// format guids consistently w/ Column.readGUIDValue() // format guids consistently w/ Column.readGUIDValue()
_lastAutoNumber = "{" + UUID.randomUUID() + "}"; _lastAutoNumber = "{" + UUID.randomUUID() + "}";
return _lastAutoNumber; return _lastAutoNumber;
} }


@Override
public Object handleInsert(TableImpl.WriteRowState writeRowState,
Object inRowValue)
throws IOException
{
_lastAutoNumber = toCharSequence(inRowValue);
return _lastAutoNumber;
}

@Override @Override
public void restoreLast(Object last) { public void restoreLast(Object last) {
_lastAutoNumber = null; _lastAutoNumber = null;
} }


@Override @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 @Override
} }


@Override @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(); throw new UnsupportedOperationException();
} }



+ 30
- 0
src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java Просмотреть файл

private Table.ColumnOrder _columnOrder; private Table.ColumnOrder _columnOrder;
/** whether or not enforcement of foreign-keys is enabled */ /** whether or not enforcement of foreign-keys is enabled */
private boolean _enforceForeignKeys; private boolean _enforceForeignKeys;
/** whether or not auto numbers can be directly inserted by the user */
private boolean _allowAutoNumInsert;
/** factory for ColumnValidators */ /** factory for ColumnValidators */
private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE; private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE;
/** cache of in-use tables */ /** cache of in-use tables */
_charset = ((charset == null) ? getDefaultCharset(_format) : charset); _charset = ((charset == null) ? getDefaultCharset(_format) : charset);
_columnOrder = getDefaultColumnOrder(); _columnOrder = getDefaultColumnOrder();
_enforceForeignKeys = getDefaultEnforceForeignKeys(); _enforceForeignKeys = getDefaultEnforceForeignKeys();
_allowAutoNumInsert = getDefaultAllowAutoNumberInsert();
_fileFormat = fileFormat; _fileFormat = fileFormat;
_pageChannel = new PageChannel(channel, closeChannel, _format, autoSync); _pageChannel = new PageChannel(channel, closeChannel, _format, autoSync);
_timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone); _timeZone = ((timeZone == null) ? getDefaultTimeZone() : timeZone);
_enforceForeignKeys = newEnforceForeignKeys; _enforceForeignKeys = newEnforceForeignKeys;
} }


public boolean isAllowAutoNumberInsert() {
return _allowAutoNumInsert;
}

public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert) {
if(allowAutoNumInsert == null) {
allowAutoNumInsert = getDefaultAllowAutoNumberInsert();
}
_allowAutoNumInsert = allowAutoNumInsert;
}


public ColumnValidatorFactory getColumnValidatorFactory() { public ColumnValidatorFactory getColumnValidatorFactory() {
return _validatorFactory; return _validatorFactory;
} }
return true; 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 * Copies the given InputStream to the given channel using the most
* efficient means possible. * efficient means possible.

+ 140
- 48
src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java Просмотреть файл

import com.healthmarketscience.jackcess.ColumnBuilder; import com.healthmarketscience.jackcess.ColumnBuilder;
import com.healthmarketscience.jackcess.ConstraintViolationException; import com.healthmarketscience.jackcess.ConstraintViolationException;
import com.healthmarketscience.jackcess.CursorBuilder; import com.healthmarketscience.jackcess.CursorBuilder;
import com.healthmarketscience.jackcess.DataType;
import com.healthmarketscience.jackcess.IndexBuilder; import com.healthmarketscience.jackcess.IndexBuilder;
import com.healthmarketscience.jackcess.JackcessException; import com.healthmarketscience.jackcess.JackcessException;
import com.healthmarketscience.jackcess.PropertyMap; import com.healthmarketscience.jackcess.PropertyMap;
private PropertyMap _props; private PropertyMap _props;
/** properties group for this table (and columns) */ /** properties group for this table (and columns) */
private PropertyMaps _propertyMaps; 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 */ /** foreign-key enforcer for this table */
private final FKEnforcer _fkEnforcer; private final FKEnforcer _fkEnforcer;


return _tableDefPageNumber; return _tableDefPageNumber;
} }


public boolean isAllowAutoNumberInsert() {
return ((_allowAutoNumInsert != null) ? (boolean)_allowAutoNumInsert :
getDatabase().isAllowAutoNumberInsert());
}

public void setAllowAutoNumberInsert(Boolean allowAutoNumInsert) {
_allowAutoNumInsert = allowAutoNumInsert;
}

/** /**
* @usage _advanced_method_ * @usage _advanced_method_
*/ */


/** /**
* @param buffer Buffer to write to * @param buffer Buffer to write to
* @param columns List of Columns in the table
*/ */
private static void writeTableDefinitionHeader( private static void writeTableDefinitionHeader(
TableCreator creator, ByteBuffer buffer, int totalTableDefSize) TableCreator creator, ByteBuffer buffer, int totalTableDefSize)
/** /**
* Add multiple rows to this table, only writing to disk after all * Add multiple rows to this table, only writing to disk after all
* rows have been written, and every time a data page is filled. * 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, private List<? extends Object[]> addRows(List<? extends Object[]> rows,
final boolean isBatchWrite) final boolean isBatchWrite)
int pageNumber = PageChannel.INVALID_PAGE_NUMBER; int pageNumber = PageChannel.INVALID_PAGE_NUMBER;
int updateCount = 0; int updateCount = 0;
int autoNumAssignCount = 0; int autoNumAssignCount = 0;
WriteRowState writeRowState =
(!_autoNumColumns.isEmpty() ? new WriteRowState() : null);
try { try {


List<Object[]> dupeRows = null; List<Object[]> dupeRows = null;
} }


// fill in autonumbers // fill in autonumbers
handleAutoNumbersForAdd(row);
handleAutoNumbersForAdd(row, writeRowState);
++autoNumAssignCount; ++autoNumAssignCount;
// write the row of data to a temporary buffer // write the row of data to a temporary buffer


// handle various value massaging activities // handle various value massaging activities
for(ColumnImpl column : _columns) { for(ColumnImpl column : _columns) {
Object rowValue = null;

if(column.isAutoNumber()) { 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); column.setRowValue(row, rowValue);
} }


// fill in autonumbers
handleAutoNumbersForUpdate(row, rowBuffer, rowState);

// generate new row bytes // generate new row bytes
ByteBuffer newRowData = createRow( ByteBuffer newRowData = createRow(
row, _writeRowBufferH.getPageBuffer(getPageChannel()), oldRowSize, row, _writeRowBufferH.getPageBuffer(getPageChannel()), oldRowSize,
} }


/** /**
* 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 throws IOException
{ {
if(_autoNumColumns.isEmpty()) { if(_autoNumColumns.isEmpty()) {
return; return;
} }
Object complexAutoNumber = null;

boolean enableInsert = isAllowAutoNumberInsert();
writeRowState.resetAutoNumber();
for(ColumnImpl col : _autoNumColumns) { 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(); 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); 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. * Restores all autonumber column values from a failed add row.
*/ */
return _lastLongAutoNumber; return _lastLongAutoNumber;
} }


void adjustLongAutoNumber(int inLongAutoNumber) {
if(inLongAutoNumber > _lastLongAutoNumber) {
_lastLongAutoNumber = inLongAutoNumber;
}
}

void restoreLastLongAutoNumber(int lastLongAutoNumber) { void restoreLastLongAutoNumber(int lastLongAutoNumber) {
// restores the last used auto number // restores the last used auto number
_lastLongAutoNumber = lastLongAutoNumber - 1; _lastLongAutoNumber = lastLongAutoNumber - 1;
return _lastComplexTypeAutoNumber; return _lastComplexTypeAutoNumber;
} }


void adjustComplexTypeAutoNumber(int inComplexTypeAutoNumber) {
if(inComplexTypeAutoNumber > _lastComplexTypeAutoNumber) {
_lastComplexTypeAutoNumber = inComplexTypeAutoNumber;
}
}

void restoreLastComplexTypeAutoNumber(int lastComplexTypeAutoNumber) { void restoreLastComplexTypeAutoNumber(int lastComplexTypeAutoNumber) {
// restores the last used auto number // restores the last used auto number
_lastComplexTypeAutoNumber = lastComplexTypeAutoNumber - 1; _lastComplexTypeAutoNumber = lastComplexTypeAutoNumber - 1;
} }


/** /**
* 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_ * @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 */ /** Buffer used for reading the header row data pages */
private final TempPageHolder _headerRowBufferH; private final TempPageHolder _headerRowBufferH;
} }
public void reset() { public void reset() {
resetAutoNumber();
_finalRowId = null; _finalRowId = null;
_finalRowBuffer = null; _finalRowBuffer = null;
_rowsOnHeaderPage = 0; _rowsOnHeaderPage = 0;

+ 0
- 76
src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java Просмотреть файл

} }
} }


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 { public void testWriteAndReadDate() throws Exception {
for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) { for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
Database db = createMem(fileFormat); Database db = createMem(fileFormat);

+ 3
- 3
src/test/java/com/healthmarketscience/jackcess/TestUtil.java Просмотреть файл

return Arrays.<Row>asList(rows); return Arrays.<Row>asList(rows);
} }
static void dumpDatabase(Database mdb) throws Exception {
public static void dumpDatabase(Database mdb) throws Exception {
dumpDatabase(mdb, false); dumpDatabase(mdb, false);
} }


static void dumpDatabase(Database mdb, boolean systemTables)
public static void dumpDatabase(Database mdb, boolean systemTables)
throws Exception throws Exception
{ {
dumpDatabase(mdb, systemTables, new PrintWriter(System.out, true)); 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)); dumpTable(table, new PrintWriter(System.out, true));
} }



+ 499
- 0
src/test/java/com/healthmarketscience/jackcess/impl/AutoNumberTest.java Просмотреть файл

/*
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();
}
}

}

Загрузка…
Отмена
Сохранить