Browse Source

merge branch mutateops changes through r1030

git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1031 f203690c-595d-4dc9-a70b-905162fa7fd2
tags/jackcess-2.1.5
James Ahlborn 7 years ago
parent
commit
819953ac72
26 changed files with 2872 additions and 547 deletions
  1. 0
    12
      TODO.txt
  2. 16
    4
      src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java
  3. 3
    29
      src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java
  4. 44
    1
      src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java
  5. 8
    0
      src/main/java/com/healthmarketscience/jackcess/Relationship.java
  6. 184
    0
      src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java
  7. 33
    25
      src/main/java/com/healthmarketscience/jackcess/TableBuilder.java
  8. 15
    0
      src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java
  9. 98
    81
      src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java
  10. 68
    0
      src/main/java/com/healthmarketscience/jackcess/impl/DBMutator.java
  11. 204
    63
      src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
  12. 21
    1
      src/main/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java
  13. 78
    44
      src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java
  14. 46
    14
      src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java
  15. 1
    1
      src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java
  16. 17
    0
      src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java
  17. 13
    0
      src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java
  18. 346
    0
      src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java
  19. 27
    11
      src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java
  20. 147
    172
      src/main/java/com/healthmarketscience/jackcess/impl/TableCreator.java
  21. 689
    84
      src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
  22. 250
    0
      src/main/java/com/healthmarketscience/jackcess/impl/TableMutator.java
  23. 332
    0
      src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java
  24. 4
    5
      src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java
  25. 51
    0
      src/test/java/com/healthmarketscience/jackcess/IndexTest.java
  26. 177
    0
      src/test/java/com/healthmarketscience/jackcess/TableUpdaterTest.java

+ 0
- 12
TODO.txt View File

@@ -2,8 +2,6 @@ Missing pieces:

- fix long text index entries (for new general sort order)
* ???
- implement foreign key index creation & relationship creation
* MEDIUM
- implement table creation w/ complex columns
* MEDIUM
- implement table, column, index renaming
@@ -19,13 +17,3 @@ Rename:
- Table - update table def, queries, relationships, complex tables?
- Column - update table def, queries, relationships, complex tables?
- Index - update table def

Index add (fk impl)
- Database.addIndex(IndexBuilder) - use TableCreator internal
- add indexes separately from adding fk info, (backing indexes need to be
added first)
- require baking indexes to be created first (does MSAccess?)
- need to populate index after creation! populate first, then add?
- add relationships
- flush all non-system tables from DbImpl._tableCache (references to old table
impls)

+ 16
- 4
src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java View File

@@ -25,12 +25,16 @@ import com.healthmarketscience.jackcess.impl.ColumnImpl;
import com.healthmarketscience.jackcess.impl.DatabaseImpl;
import com.healthmarketscience.jackcess.impl.JetFormat;
import com.healthmarketscience.jackcess.impl.PropertyMapImpl;
import com.healthmarketscience.jackcess.impl.TableImpl;
import com.healthmarketscience.jackcess.impl.TableUpdater;

/**
* Builder style class for constructing a {@link Column}. See {@link
* TableBuilder} for example usage.
* TableBuilder} for example usage. Additionally, a Column can be added to an
* existing Table using the {@link #addToTable(Table)} method.
*
* @author James Ahlborn
* @see TableBuilder
* @usage _general_class_
*/
public class ColumnBuilder {
@@ -381,12 +385,12 @@ public class ColumnBuilder {
* @usage _advanced_method_
*/
public void validate(JetFormat format) {
if(getType() == null) {
throw new IllegalArgumentException(withErrorContext("must have type"));
}
DatabaseImpl.validateIdentifierName(
getName(), format.MAX_COLUMN_NAME_LENGTH, "column");

if(getType() == null) {
throw new IllegalArgumentException(withErrorContext("must have type"));
}
if(getType().isUnsupported()) {
throw new IllegalArgumentException(withErrorContext(
"Cannot create column with unsupported type " + getType()));
@@ -472,6 +476,14 @@ public class ColumnBuilder {
return this;
}

/**
* Adds a new Column to the given Table with the currently configured
* attributes.
*/
public Column addToTable(Table table) throws IOException {
return new TableUpdater((TableImpl)table).addColumn(this);
}

private String withErrorContext(String msg) {
return msg + "(Column=" + getName() + ")";
}

+ 3
- 29
src/main/java/com/healthmarketscience/jackcess/CursorBuilder.java View File

@@ -19,8 +19,6 @@ package com.healthmarketscience.jackcess;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

@@ -147,37 +145,13 @@ public class CursorBuilder {
* Searches for an index with the given column names.
*/
private CursorBuilder setIndexByColumns(List<String> searchColumns) {
boolean found = false;
for(IndexImpl index : _table.getIndexes()) {
Collection<? extends Index.Column> indexColumns = index.getColumns();
if(indexColumns.size() != searchColumns.size()) {
continue;
}
Iterator<String> sIter = searchColumns.iterator();
Iterator<? extends Index.Column> iIter = indexColumns.iterator();
boolean matches = true;
while(sIter.hasNext()) {
String sColName = sIter.next();
String iColName = iIter.next().getName();
if((sColName != iColName) &&
((sColName == null) || !sColName.equalsIgnoreCase(iColName))) {
matches = false;
break;
}
}

if(matches) {
_index = index;
found = true;
break;
}
}
if(!found) {
IndexImpl index = _table.findIndexForColumns(searchColumns, false);
if(index == null) {
throw new IllegalArgumentException("Index with columns " +
searchColumns +
" does not exist in table " + _table);
}
_index = index;
return this;
}


+ 44
- 1
src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java View File

@@ -16,6 +16,7 @@ limitations under the License.

package com.healthmarketscience.jackcess;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -25,12 +26,16 @@ import com.healthmarketscience.jackcess.impl.DatabaseImpl;
import com.healthmarketscience.jackcess.impl.IndexData;
import com.healthmarketscience.jackcess.impl.IndexImpl;
import com.healthmarketscience.jackcess.impl.JetFormat;
import com.healthmarketscience.jackcess.impl.TableImpl;
import com.healthmarketscience.jackcess.impl.TableUpdater;

/**
* Builder style class for constructing an {@link Index}. See {@link
* TableBuilder} for example usage.
* TableBuilder} for example usage. Additionally, an Index can be added to an
* existing Table using the {@link #addToTable(Table)} method.
*
* @author James Ahlborn
* @see TableBuilder
* @usage _general_class_
*/
public class IndexBuilder
@@ -47,6 +52,8 @@ public class IndexBuilder
private byte _flags = IndexData.UNKNOWN_INDEX_FLAG;
/** the names and orderings of the indexed columns */
private final List<Column> _columns = new ArrayList<Column>();
/** 0-based index number */
private int _indexNumber;

public IndexBuilder(String name) {
_name = name;
@@ -117,6 +124,14 @@ public class IndexBuilder
return setUnique();
}

/**
* @usage _advanced_method_
*/
public IndexBuilder setType(byte type) {
_type = type;
return this;
}

/**
* Sets this index to enforce uniqueness.
*/
@@ -141,6 +156,26 @@ public class IndexBuilder
return this;
}

/**
* @usage _advanced_method_
*/
public int getIndexNumber() {
return _indexNumber;
}

/**
* @usage _advanced_method_
*/
public void setIndexNumber(int newIndexNumber) {
_indexNumber = newIndexNumber;
}

/**
* Checks that this index definition is valid.
*
* @throws IllegalArgumentException if this index definition is invalid.
* @usage _advanced_method_
*/
public void validate(Set<String> tableColNames, JetFormat format) {

DatabaseImpl.validateIdentifierName(
@@ -169,6 +204,14 @@ public class IndexBuilder
}
}

/**
* Adds a new Index to the given Table with the currently configured
* attributes.
*/
public Index addToTable(Table table) throws IOException {
return new TableUpdater((TableImpl)table).addIndex(this);
}

private String withErrorContext(String msg) {
return msg + "(Index=" + getName() + ")";
}

+ 8
- 0
src/main/java/com/healthmarketscience/jackcess/Relationship.java View File

@@ -27,6 +27,10 @@ import java.util.List;
*/
public interface Relationship
{
public enum JoinType {
INNER, LEFT_OUTER, RIGHT_OUTER;
}
public String getName();
public Table getFromTable();
@@ -45,7 +49,11 @@ public interface Relationship
public boolean cascadeDeletes();

public boolean cascadeNullOnDelete();

public boolean isLeftOuterJoin();

public boolean isRightOuterJoin();

public JoinType getJoinType();
}

+ 184
- 0
src/main/java/com/healthmarketscience/jackcess/RelationshipBuilder.java View File

@@ -0,0 +1,184 @@
/*
Copyright (c) 2016 James Ahlborn

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.healthmarketscience.jackcess;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import com.healthmarketscience.jackcess.impl.DatabaseImpl;
import com.healthmarketscience.jackcess.impl.RelationshipCreator;
import com.healthmarketscience.jackcess.impl.RelationshipImpl;

/**
* Builder style class for constructing a {@link Relationship}, and,
* optionally, the associated backing foreign key (if referential integrity
* enforcement is enabled). A Relationship can only be constructed for
* {@link Table}s which already exist in the {@link Database}. Additionally,
* if integrity enforcement is enabled, there must already be a unique index
* on the "from" Table for the relevant columns (same requirement as MS
* Access).
* <p/>
* Example:
* <pre>
* Relationship rel = new RelationshipBuilder("FromTable", "ToTable")
* .addColumns("ID", "FK_ID")
* .setReferentialIntegrity()
* .setCascadeDeletes()
* .toRelationship(db);
* </pre>
*
* @author James Ahlborn
* @see TableBuilder
* @usage _general_class_
*/
public class RelationshipBuilder
{
private static final int JOIN_FLAGS =
RelationshipImpl.LEFT_OUTER_JOIN_FLAG |
RelationshipImpl.RIGHT_OUTER_JOIN_FLAG;

/** relationship flags (default to "don't enforce") */
private int _flags = RelationshipImpl.NO_REFERENTIAL_INTEGRITY_FLAG;
private String _fromTable;
private String _toTable;
private List<String> _fromCols = new ArrayList<String>();
private List<String> _toCols = new ArrayList<String>();

public RelationshipBuilder(String fromTable, String toTable)
{
_fromTable = fromTable;
_toTable = toTable;
}

/**
* Adds a pair of columns to the relationship.
*/
public RelationshipBuilder addColumns(String fromCol, String toCol) {
_fromCols.add(fromCol);
_toCols.add(toCol);
return this;
}

/**
* Enables referential integrity enforcement for this relationship.
*
* Note, this requires the "from" table to have an existing unique index on
* the relevant columns.
*/
public RelationshipBuilder setReferentialIntegrity() {
return clearFlag(RelationshipImpl.NO_REFERENTIAL_INTEGRITY_FLAG);
}

/**
* Enables deletes to be cascaded from the "from" table to the "to" table.
*
* Note, this requires referential integrity to be enforced.
*/
public RelationshipBuilder setCascadeDeletes() {
return setFlag(RelationshipImpl.CASCADE_DELETES_FLAG);
}
/**
* Enables updates to be cascaded from the "from" table to the "to" table.
*
* Note, this requires referential integrity to be enforced.
*/
public RelationshipBuilder setCascadeUpdates() {
return setFlag(RelationshipImpl.CASCADE_UPDATES_FLAG);
}

/**
* Enables deletes in the "from" table to be cascaded as "null" to the "to"
* table.
*
* Note, this requires referential integrity to be enforced.
*/
public RelationshipBuilder setCascadeNullOnDelete() {
return setFlag(RelationshipImpl.CASCADE_NULL_FLAG);
}

/**
* Sets the preferred join type for this relationship.
*/
public RelationshipBuilder setJoinType(Relationship.JoinType joinType) {
clearFlag(JOIN_FLAGS);
switch(joinType) {
case INNER:
// nothing to do
break;
case LEFT_OUTER:
_flags |= RelationshipImpl.LEFT_OUTER_JOIN_FLAG;
break;
case RIGHT_OUTER:
_flags |= RelationshipImpl.RIGHT_OUTER_JOIN_FLAG;
break;
default:
throw new RuntimeException("unexpected join type " + joinType);
}
return this;
}

public boolean hasReferentialIntegrity() {
return !hasFlag(RelationshipImpl.NO_REFERENTIAL_INTEGRITY_FLAG);
}

public int getFlags() {
return _flags;
}

public String getFromTable() {
return _fromTable;
}

public String getToTable() {
return _toTable;
}
public List<String> getFromColumns() {
return _fromCols;
}

public List<String> getToColumns() {
return _toCols;
}
/**
* Creates a new Relationship in the given Database with the currently
* configured attributes.
*/
public Relationship toRelationship(Database db)
throws IOException
{
return new RelationshipCreator((DatabaseImpl)db).createRelationship(this);
}

private RelationshipBuilder setFlag(int flagMask) {
_flags |= flagMask;
return this;
}

private RelationshipBuilder clearFlag(int flagMask) {
_flags &= ~flagMask;
return this;
}

private boolean hasFlag(int flagMask) {
return((_flags & flagMask) != 0);
}

}

+ 33
- 25
src/main/java/com/healthmarketscience/jackcess/TableBuilder.java View File

@@ -28,6 +28,7 @@ import java.util.Set;

import com.healthmarketscience.jackcess.impl.DatabaseImpl;
import com.healthmarketscience.jackcess.impl.PropertyMapImpl;
import com.healthmarketscience.jackcess.impl.TableCreator;

/**
* Builder style class for constructing a {@link Table}.
@@ -44,6 +45,9 @@ import com.healthmarketscience.jackcess.impl.PropertyMapImpl;
* </pre>
*
* @author James Ahlborn
* @see ColumnBuilder
* @see IndexBuilder
* @see RelationshipBuilder
* @usage _general_class_
*/
public class TableBuilder {
@@ -116,6 +120,9 @@ public class TableBuilder {
}
}

public String getName() {
return _name;
}

/**
* Adds a Column to the new table.
@@ -140,6 +147,10 @@ public class TableBuilder {
return this;
}

public List<ColumnBuilder> getColumns() {
return _columns;
}
/**
* Adds an IndexBuilder to the new table.
*/
@@ -154,6 +165,22 @@ public class TableBuilder {
return this;
}

/**
* Adds the Indexes to the new table.
*/
public TableBuilder addIndexes(Collection<? extends IndexBuilder> indexes) {
if(indexes != null) {
for(IndexBuilder col : indexes) {
addIndex(col);
}
}
return this;
}

public List<IndexBuilder> getIndexes() {
return _indexes;
}
/**
* Sets whether or not subsequently added columns will have their names
* automatically escaped
@@ -202,35 +229,16 @@ public class TableBuilder {
return this;
}

public Map<String,PropertyMap.Property> getProperties() {
return _props;
}

/**
* Creates a new Table in the given Database with the currently configured
* attributes.
*/
public Table toTable(Database db)
throws IOException
{
((DatabaseImpl)db).createTable(_name, _columns, _indexes);
Table table = db.getTable(_name);

boolean addedProps = false;
if(_props != null) {
table.getProperties().putAll(_props.values());
addedProps = true;
}
for(ColumnBuilder cb : _columns) {
Map<String,PropertyMap.Property> colProps = cb.getProperties();
if(colProps != null) {
table.getColumn(cb.getName()).getProperties().putAll(colProps.values());
addedProps = true;
}
}

// all table and column props are saved together
if(addedProps) {
table.getProperties().save();
}
return table;
public Table toTable(Database db) throws IOException {
return new TableCreator(((DatabaseImpl)db)).createTable(this);
}

/**

+ 15
- 0
src/main/java/com/healthmarketscience/jackcess/impl/ByteUtil.java View File

@@ -397,6 +397,21 @@ public final class ByteUtil {
}
return -1;
}

/**
* Inserts empty data of the given length at the current position of the
* given buffer (moving existing data forward the given length). The limit
* of the buffer is adjusted by the given length. The buffer is expecting
* to have the required capacity available.
*/
public static void insertEmptyData(ByteBuffer buffer, int len) {
byte[] buf = buffer.array();
int pos = buffer.position();
int limit = buffer.limit();
System.arraycopy(buf, pos, buf, pos + len, limit - pos);
Arrays.fill(buf, pos, pos + len, (byte)0);
buffer.limit(limit + len);
}
/**
* Convert a byte buffer to a hexadecimal string for display

+ 98
- 81
src/main/java/com/healthmarketscience/jackcess/impl/ColumnImpl.java View File

@@ -33,6 +33,7 @@ import java.sql.Blob;
import java.sql.Clob;
import java.sql.SQLException;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -309,13 +310,17 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
return new ColumnImpl(args);
}

/**
/**
* Sets the usage maps for this column.
*/
void setUsageMaps(UsageMap ownedPages, UsageMap freeSpacePages) {
// base does nothing
}

void collectUsageMapPages(Collection<Integer> pages) {
// base does nothing
}
/**
* Secondary column initialization after the table is fully loaded.
*/
@@ -1404,22 +1409,6 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
return rtn;
}

/**
* @param columns A list of columns in a table definition
* @return The number of variable length columns which are not long values
* found in the list
* @usage _advanced_method_
*/
private static short countNonLongVariableLength(List<ColumnBuilder> columns) {
short rtn = 0;
for (ColumnBuilder col : columns) {
if (col.isVariableLength() && !col.getType().isLongValue()) {
rtn++;
}
}
return rtn;
}
/**
* @return an appropriate BigDecimal representation of the given object.
* <code>null</code> is returned as 0 and Numbers are converted
@@ -1585,90 +1574,118 @@ public class ColumnImpl implements Column, Comparable<ColumnImpl> {
protected static void writeDefinitions(TableCreator creator, ByteBuffer buffer)
throws IOException
{
List<ColumnBuilder> columns = creator.getColumns();
short fixedOffset = (short) 0;
short variableOffset = (short) 0;
// we specifically put the "long variable" values after the normal
// variable length values so that we have a better chance of fitting it
// all (because "long variable" values can go in separate pages)
short longVariableOffset = countNonLongVariableLength(columns);
for (ColumnBuilder col : columns) {
int longVariableOffset = creator.countNonLongVariableLength();
creator.setColumnOffsets(0, 0, longVariableOffset);

buffer.put(col.getType().getValue());
buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); //constant magic number
buffer.putShort(col.getColumnNumber()); //Column Number
for (ColumnBuilder col : creator.getColumns()) {
writeDefinition(creator, col, buffer);
}

if(col.isVariableLength()) {
if(!col.getType().isLongValue()) {
buffer.putShort(variableOffset++);
} else {
buffer.putShort(longVariableOffset++);
}
} else {
buffer.putShort((short) 0);
}
for (ColumnBuilder col : creator.getColumns()) {
TableImpl.writeName(buffer, col.getName(), creator.getCharset());
}
}

buffer.putShort(col.getColumnNumber()); //Column Number again
protected static void writeDefinition(
TableMutator mutator, ColumnBuilder col, ByteBuffer buffer)
throws IOException
{
TableMutator.ColumnOffsets colOffsets = mutator.getColumnOffsets();

if(col.getType().isTextual()) {
// this will write 4 bytes (note we don't support writing dbs which
// use the text code page)
writeSortOrder(buffer, col.getTextSortOrder(), creator.getFormat());
} else {
// note scale/precision not stored for calculated numeric fields
if(col.getType().getHasScalePrecision() && !col.isCalculated()) {
buffer.put(col.getPrecision()); // numeric precision
buffer.put(col.getScale()); // numeric scale
} else {
buffer.put((byte) 0x00); //unused
buffer.put((byte) 0x00); //unused
}
buffer.putShort((short) 0); //Unknown
}
buffer.put(col.getType().getValue());
buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); //constant magic number
buffer.putShort(col.getColumnNumber()); //Column Number

buffer.put(getColumnBitFlags(col)); // misc col flags
if(col.isVariableLength()) {
buffer.putShort(colOffsets.getNextVariableOffset(col));
} else {
buffer.putShort((short) 0);
}

// note access doesn't seem to allow unicode compression for calced fields
if(col.isCalculated()) {
buffer.put(CALCULATED_EXT_FLAG_MASK);
} else if (col.isCompressedUnicode()) { //Compressed
buffer.put(COMPRESSED_UNICODE_EXT_FLAG_MASK);
buffer.putShort(col.getColumnNumber()); //Column Number again

if(col.getType().isTextual()) {
// this will write 4 bytes (note we don't support writing dbs which
// use the text code page)
writeSortOrder(buffer, col.getTextSortOrder(), mutator.getFormat());
} else {
// note scale/precision not stored for calculated numeric fields
if(col.getType().getHasScalePrecision() && !col.isCalculated()) {
buffer.put(col.getPrecision()); // numeric precision
buffer.put(col.getScale()); // numeric scale
} else {
buffer.put((byte)0);
buffer.put((byte) 0x00); //unused
buffer.put((byte) 0x00); //unused
}
buffer.putShort((short) 0); //Unknown
}

buffer.putInt(0); //Unknown, but always 0.
buffer.put(getColumnBitFlags(col)); // misc col flags

//Offset for fixed length columns
if(col.isVariableLength()) {
buffer.putShort((short) 0);
} else {
buffer.putShort(fixedOffset);
fixedOffset += col.getType().getFixedSize(col.getLength());
}
// note access doesn't seem to allow unicode compression for calced fields
if(col.isCalculated()) {
buffer.put(CALCULATED_EXT_FLAG_MASK);
} else if (col.isCompressedUnicode()) { //Compressed
buffer.put(COMPRESSED_UNICODE_EXT_FLAG_MASK);
} else {
buffer.put((byte)0);
}

buffer.putInt(0); //Unknown, but always 0.

if(!col.getType().isLongValue()) {
short length = col.getLength();
if(col.isCalculated()) {
// calced columns have additional value overhead
if(!col.getType().isVariableLength() ||
col.getType().getHasScalePrecision()) {
length = CalculatedColumnUtil.CALC_FIXED_FIELD_LEN;
} else {
length += CalculatedColumnUtil.CALC_EXTRA_DATA_LEN;
}
//Offset for fixed length columns
if(col.isVariableLength()) {
buffer.putShort((short) 0);
} else {
buffer.putShort(colOffsets.getNextFixedOffset(col));
}

if(!col.getType().isLongValue()) {
short length = col.getLength();
if(col.isCalculated()) {
// calced columns have additional value overhead
if(!col.getType().isVariableLength() ||
col.getType().getHasScalePrecision()) {
length = CalculatedColumnUtil.CALC_FIXED_FIELD_LEN;
} else {
length += CalculatedColumnUtil.CALC_EXTRA_DATA_LEN;
}
buffer.putShort(length); //Column length
} else {
buffer.putShort((short)0x0000); // unused
}

buffer.putShort(length); //Column length
} else {
buffer.putShort((short)0x0000); // unused
}
for (ColumnBuilder col : columns) {
TableImpl.writeName(buffer, col.getName(), creator.getCharset());
}

protected static void writeColUsageMapDefinitions(
TableCreator creator, ByteBuffer buffer)
throws IOException
{
// write long value column usage map references
for(ColumnBuilder lvalCol : creator.getLongValueColumns()) {
writeColUsageMapDefinition(creator, lvalCol, buffer);
}
}

protected static void writeColUsageMapDefinition(
TableMutator creator, ColumnBuilder lvalCol, ByteBuffer buffer)
throws IOException
{
TableMutator.ColumnState colState = creator.getColumnState(lvalCol);

buffer.putShort(lvalCol.getColumnNumber());
// owned pages umap (both are on same page)
buffer.put(colState.getUmapOwnedRowNumber());
ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber());
// free space pages umap
buffer.put(colState.getUmapFreeRowNumber());
ByteUtil.put3ByteInt(buffer, colState.getUmapPageNumber());
}

/**
* Reads the sort order info from the given buffer from the given position.
*/

+ 68
- 0
src/main/java/com/healthmarketscience/jackcess/impl/DBMutator.java View File

@@ -0,0 +1,68 @@
/*
Copyright (c) 2016 James Ahlborn

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.healthmarketscience.jackcess.impl;

import java.io.IOException;
import java.nio.charset.Charset;


/**
* Common helper class used to maintain state during database mutation.
*
* @author James Ahlborn
*/
abstract class DBMutator
{
private final DatabaseImpl _database;

protected DBMutator(DatabaseImpl database) {
_database = database;
}

public DatabaseImpl getDatabase() {
return _database;
}

public JetFormat getFormat() {
return _database.getFormat();
}

public PageChannel getPageChannel() {
return _database.getPageChannel();
}

public Charset getCharset() {
return _database.getCharset();
}

public int reservePageNumber() throws IOException {
return getPageChannel().allocateNewPage();
}

public static int calculateNameLength(String name) {
return (name.length() * JetFormat.TEXT_FIELD_UNIT_SIZE) + 2;
}

protected ColumnImpl.SortOrder getDbSortOrder() {
try {
return _database.getDefaultSortOrder();
} catch(IOException e) {
// ignored, just use the jet format default
}
return null;
}
}

+ 204
- 63
src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java View File

@@ -53,6 +53,7 @@ import com.healthmarketscience.jackcess.CursorBuilder;
import com.healthmarketscience.jackcess.DataType;
import com.healthmarketscience.jackcess.Database;
import com.healthmarketscience.jackcess.DatabaseBuilder;
import com.healthmarketscience.jackcess.Index;
import com.healthmarketscience.jackcess.IndexBuilder;
import com.healthmarketscience.jackcess.IndexCursor;
import com.healthmarketscience.jackcess.PropertyMap;
@@ -60,6 +61,7 @@ import com.healthmarketscience.jackcess.Relationship;
import com.healthmarketscience.jackcess.Row;
import com.healthmarketscience.jackcess.RuntimeIOException;
import com.healthmarketscience.jackcess.Table;
import com.healthmarketscience.jackcess.TableBuilder;
import com.healthmarketscience.jackcess.TableMetaData;
import com.healthmarketscience.jackcess.impl.query.QueryImpl;
import com.healthmarketscience.jackcess.query.Query;
@@ -85,11 +87,8 @@ public class DatabaseImpl implements Database

/** this is the default "userId" used if we cannot find existing info. this
seems to be some standard "Admin" userId for access files */
private static final byte[] SYS_DEFAULT_SID = new byte[2];
static {
SYS_DEFAULT_SID[0] = (byte) 0xA6;
SYS_DEFAULT_SID[1] = (byte) 0x33;
}
private static final byte[] SYS_DEFAULT_SID = new byte[] {
(byte) 0xA6, (byte) 0x33};

/** the default value for the resource path used to load classpath
* resources.
@@ -204,9 +203,7 @@ public class DatabaseImpl implements Database
/** Name of the system object that is the parent of all databases */
private static final String SYSTEM_OBJECT_NAME_DATABASES = "Databases";
/** Name of the system object that is the parent of all relationships */
@SuppressWarnings("unused")
private static final String SYSTEM_OBJECT_NAME_RELATIONSHIPS =
"Relationships";
private static final String SYSTEM_OBJECT_NAME_RELATIONSHIPS = "Relationships";
/** Name of the table that contains system access control entries */
private static final String TABLE_SYSTEM_ACES = "MSysACEs";
/** Name of the table that contains table relationships */
@@ -227,6 +224,8 @@ public class DatabaseImpl implements Database
private static final Short TYPE_QUERY = 5;
/** System object type for linked table definitions */
private static final Short TYPE_LINKED_TABLE = 6;
/** System object type for relationships */
private static final Short TYPE_RELATIONSHIP = 8;

/** max number of table lookups to cache */
private static final int MAX_CACHED_LOOKUP_TABLES = 50;
@@ -281,6 +280,10 @@ public class DatabaseImpl implements Database
private TableFinder _tableFinder;
/** System access control entries table (initialized on first use) */
private TableImpl _accessControlEntries;
/** ID of the Relationships system object */
private Integer _relParentId;
/** SIDs to use for the ACEs added for new relationships */
private final List<byte[]> _newRelSIDs = new ArrayList<byte[]>();
/** System relationships table (initialized on first use) */
private TableImpl _relationships;
/** System queries table (initialized on first use) */
@@ -315,6 +318,8 @@ public class DatabaseImpl implements Database
private PropertyMaps.Handler _propsHandler;
/** ID of the Databases system object */
private Integer _dbParentId;
/** owner of objects we create */
private byte[] _newObjOwner;
/** core database properties */
private PropertyMaps _dbPropMaps;
/** summary properties */
@@ -1016,8 +1021,9 @@ public class DatabaseImpl implements Database
* Create a new table in this database
* @param name Name of the table to create
* @param columns List of Columns in the table
* @usage _general_method_
* @deprecated use {@link TableBuilder} instead
*/
@Deprecated
public void createTable(String name, List<ColumnBuilder> columns)
throws IOException
{
@@ -1029,18 +1035,17 @@ public class DatabaseImpl implements Database
* @param name Name of the table to create
* @param columns List of Columns in the table
* @param indexes List of IndexBuilders describing indexes for the table
* @usage _general_method_
* @deprecated use {@link TableBuilder} instead
*/
@Deprecated
public void createTable(String name, List<ColumnBuilder> columns,
List<IndexBuilder> indexes)
throws IOException
{
if(lookupTable(name) != null) {
throw new IllegalArgumentException(withErrorContext(
"Cannot create table with name of existing table '" + name + "'"));
}

new TableCreator(this, name, columns, indexes).createTable();
new TableBuilder(name)
.addColumns(columns)
.addIndexes(indexes)
.toTable(this);
}

public void createLinkedTable(String name, String linkedDbName,
@@ -1085,8 +1090,8 @@ public class DatabaseImpl implements Database
//Add this table to system tables
addToSystemCatalog(name, tdefPageNumber, type, linkedDbName,
linkedTableName);
addToAccessControlEntries(tdefPageNumber);
linkedTableName, _tableParentId);
addToAccessControlEntries(tdefPageNumber, _tableParentId, _newTableSIDs);
}

public List<Relationship> getRelationships(Table table1, Table table2)
@@ -1143,20 +1148,17 @@ public class DatabaseImpl implements Database
TableImpl table1, TableImpl table2, boolean includeSystemTables)
throws IOException
{
// the relationships table does not get loaded until first accessed
if(_relationships == null) {
_relationships = getRequiredSystemTable(TABLE_SYSTEM_RELATIONSHIPS);
}

initRelationships();
List<Relationship> relationships = new ArrayList<Relationship>();

if(table1 != null) {
Cursor cursor = createCursorWithOptionalIndex(
_relationships, REL_COL_FROM_TABLE, table1.getName());
Cursor cursor = createCursorWithOptionalIndex(
_relationships, REL_COL_FROM_TABLE, table1.getName());
collectRelationships(cursor, table1, table2, relationships,
includeSystemTables);
cursor = createCursorWithOptionalIndex(
_relationships, REL_COL_TO_TABLE, table1.getName());
cursor = createCursorWithOptionalIndex(
_relationships, REL_COL_TO_TABLE, table1.getName());
collectRelationships(cursor, table2, table1, relationships,
includeSystemTables);
} else {
@@ -1167,6 +1169,114 @@ public class DatabaseImpl implements Database
return relationships;
}

RelationshipImpl writeRelationship(RelationshipCreator creator)
throws IOException
{
initRelationships();
String name = createRelationshipName(creator);
RelationshipImpl newRel = creator.createRelationshipImpl(name);

ColumnImpl ccol = _relationships.getColumn(REL_COL_COLUMN_COUNT);
ColumnImpl flagCol = _relationships.getColumn(REL_COL_FLAGS);
ColumnImpl icol = _relationships.getColumn(REL_COL_COLUMN_INDEX);
ColumnImpl nameCol = _relationships.getColumn(REL_COL_NAME);
ColumnImpl fromTableCol = _relationships.getColumn(REL_COL_FROM_TABLE);
ColumnImpl fromColCol = _relationships.getColumn(REL_COL_FROM_COLUMN);
ColumnImpl toTableCol = _relationships.getColumn(REL_COL_TO_TABLE);
ColumnImpl toColCol = _relationships.getColumn(REL_COL_TO_COLUMN);

int numCols = newRel.getFromColumns().size();
List<Object[]> rows = new ArrayList<Object[]>(numCols);
for(int i = 0; i < numCols; ++i) {
Object[] row = new Object[_relationships.getColumnCount()];
ccol.setRowValue(row, numCols);
flagCol.setRowValue(row, newRel.getFlags());
icol.setRowValue(row, i);
nameCol.setRowValue(row, name);
fromTableCol.setRowValue(row, newRel.getFromTable().getName());
fromColCol.setRowValue(row, newRel.getFromColumns().get(i).getName());
toTableCol.setRowValue(row, newRel.getToTable().getName());
toColCol.setRowValue(row, newRel.getToColumns().get(i).getName());
rows.add(row);
}

getPageChannel().startWrite();
try {
int relObjId = _tableFinder.getNextFreeSyntheticId();
_relationships.addRows(rows);
addToSystemCatalog(name, relObjId, TYPE_RELATIONSHIP, null, null,
_relParentId);
addToAccessControlEntries(relObjId, _relParentId, _newRelSIDs);
} finally {
getPageChannel().finishWrite();
}

return newRel;
}

private void initRelationships() throws IOException {
// the relationships table does not get loaded until first accessed
if(_relationships == null) {
// need the parent id of the relationships objects
_relParentId = _tableFinder.findObjectId(DB_PARENT_ID,
SYSTEM_OBJECT_NAME_RELATIONSHIPS);
_relationships = getRequiredSystemTable(TABLE_SYSTEM_RELATIONSHIPS);
}
}

private String createRelationshipName(RelationshipCreator creator)
throws IOException
{
// ensure that the final identifier name does not get too long
// - the primary name is limited to ((max / 2) - 3)
// - the total name is limited to (max - 3)
int maxIdLen = getFormat().MAX_INDEX_NAME_LENGTH;
int limit = (maxIdLen / 2) - 3;
String origName = creator.getPrimaryTable().getName();
if(origName.length() > limit) {
origName = origName.substring(0, limit);
}
limit = maxIdLen - 3;
origName += creator.getSecondaryTable().getName();
if(origName.length() > limit) {
origName = origName.substring(0, limit);
}

// now ensure name is unique
Set<String> names = new HashSet<String>();
// collect the names of all relationships for uniqueness check
for(Row row :
CursorImpl.createCursor(_systemCatalog).newIterable().setColumnNames(
SYSTEM_CATALOG_COLUMNS))
{
String name = row.getString(CAT_COL_NAME);
if (name != null && TYPE_RELATIONSHIP.equals(row.get(CAT_COL_TYPE))) {
names.add(toLookupName(name));
}
}

if(creator.hasReferentialIntegrity()) {
// relationship name will also be index name in secondary table, so must
// check those names as well
for(Index idx : creator.getSecondaryTable().getIndexes()) {
names.add(toLookupName(idx.getName()));
}
}

String baseName = toLookupName(origName);
String name = baseName;
int i = 0;
while(names.contains(name)) {
name = baseName + (++i);
}

return ((i == 0) ? origName : (origName + i));
}
public List<Query> getQueries() throws IOException
{
// the queries table does not get loaded until first accessed
@@ -1270,14 +1380,9 @@ public class DatabaseImpl implements Database
return readProperties(propsBytes, objectId, rowId);
}

/**
* @return property group for the given "database" object
*/
private PropertyMaps getPropertiesForDbObject(String dbName)
throws IOException
{
private Integer getDbParentId() throws IOException {
if(_dbParentId == null) {
// need the parent if of the databases objects
// need the parent id of the databases objects
_dbParentId = _tableFinder.findObjectId(DB_PARENT_ID,
SYSTEM_OBJECT_NAME_DATABASES);
if(_dbParentId == null) {
@@ -1285,9 +1390,36 @@ public class DatabaseImpl implements Database
"Did not find required parent db id"));
}
}
return _dbParentId;
}

private byte[] getNewObjectOwner() throws IOException {
if(_newObjOwner == null) {
// there doesn't seem to be any obvious way to find the main "owner" of
// an access db, but certain db objects seem to have the common db
// owner. we attempt to grab the db properties object and use its
// owner.
Row msysDbRow = _tableFinder.getObjectRow(
getDbParentId(), OBJECT_NAME_DB_PROPS,
Collections.singleton(CAT_COL_OWNER));
byte[] owner = null;
if(msysDbRow != null) {
owner = msysDbRow.getBytes(CAT_COL_OWNER);
}
_newObjOwner = (((owner != null) && (owner.length > 0)) ?
owner : SYS_DEFAULT_SID);
}
return _newObjOwner;
}

/**
* @return property group for the given "database" object
*/
private PropertyMaps getPropertiesForDbObject(String dbName)
throws IOException
{
Row objectRow = _tableFinder.getObjectRow(
_dbParentId, dbName, SYSTEM_CATALOG_PROPS_COLUMNS);
getDbParentId(), dbName, SYSTEM_CATALOG_PROPS_COLUMNS);
byte[] propsBytes = null;
int objectId = -1;
RowIdImpl rowId = null;
@@ -1420,12 +1552,14 @@ public class DatabaseImpl implements Database
/**
* Add a new table to the system catalog
* @param name Table name
* @param pageNumber Page number that contains the table definition
* @param objectId id of the new object
*/
private void addToSystemCatalog(String name, int pageNumber, Short type,
String linkedDbName, String linkedTableName)
private void addToSystemCatalog(String name, int objectId, Short type,
String linkedDbName, String linkedTableName,
Integer parentId)
throws IOException
{
byte[] owner = getNewObjectOwner();
Object[] catalogRow = new Object[_systemCatalog.getColumnCount()];
int idx = 0;
Date creationTime = new Date();
@@ -1434,7 +1568,7 @@ public class DatabaseImpl implements Database
{
ColumnImpl col = iter.next();
if (CAT_COL_ID.equals(col.getName())) {
catalogRow[idx] = Integer.valueOf(pageNumber);
catalogRow[idx] = Integer.valueOf(objectId);
} else if (CAT_COL_NAME.equals(col.getName())) {
catalogRow[idx] = name;
} else if (CAT_COL_TYPE.equals(col.getName())) {
@@ -1443,14 +1577,11 @@ public class DatabaseImpl implements Database
CAT_COL_DATE_UPDATE.equals(col.getName())) {
catalogRow[idx] = creationTime;
} else if (CAT_COL_PARENT_ID.equals(col.getName())) {
catalogRow[idx] = _tableParentId;
catalogRow[idx] = parentId;
} else if (CAT_COL_FLAGS.equals(col.getName())) {
catalogRow[idx] = Integer.valueOf(0);
} else if (CAT_COL_OWNER.equals(col.getName())) {
byte[] owner = new byte[2];
catalogRow[idx] = owner;
owner[0] = (byte) 0xcf;
owner[1] = (byte) 0x5f;
} else if (CAT_COL_DATABASE.equals(col.getName())) {
catalogRow[idx] = linkedDbName;
} else if (CAT_COL_FOREIGN_NAME.equals(col.getName())) {
@@ -1459,15 +1590,16 @@ public class DatabaseImpl implements Database
}
_systemCatalog.addRow(catalogRow);
}
/**
* Add a new table to the system's access control entries
* @param pageNumber Page number that contains the table definition
* Adds a new object to the system's access control entries
*/
private void addToAccessControlEntries(int pageNumber) throws IOException {

if(_newTableSIDs.isEmpty()) {
initNewTableSIDs();
private void addToAccessControlEntries(
Integer objectId, Integer parentId, List<byte[]> sids)
throws IOException
{
if(sids.isEmpty()) {
collectNewObjectSIDs(parentId, sids);
}

TableImpl acEntries = getAccessControlEntries();
@@ -1476,14 +1608,13 @@ public class DatabaseImpl implements Database
ColumnImpl objIdCol = acEntries.getColumn(ACE_COL_OBJECT_ID);
ColumnImpl sidCol = acEntries.getColumn(ACE_COL_SID);

// construct a collection of ACE entries mimicing those of our parent, the
// "Tables" system object
List<Object[]> aceRows = new ArrayList<Object[]>(_newTableSIDs.size());
for(byte[] sid : _newTableSIDs) {
// construct a collection of ACE entries
List<Object[]> aceRows = new ArrayList<Object[]>(sids.size());
for(byte[] sid : sids) {
Object[] aceRow = new Object[acEntries.getColumnCount()];
acmCol.setRowValue(aceRow, SYS_FULL_ACCESS_ACM);
inheritCol.setRowValue(aceRow, Boolean.FALSE);
objIdCol.setRowValue(aceRow, Integer.valueOf(pageNumber));
objIdCol.setRowValue(aceRow, objectId);
sidCol.setRowValue(aceRow, sid);
aceRows.add(aceRow);
}
@@ -1491,25 +1622,26 @@ public class DatabaseImpl implements Database
}

/**
* Determines the collection of SIDs which need to be added to new tables.
* Find collection of SIDs for the given parent id.
*/
private void initNewTableSIDs() throws IOException
private void collectNewObjectSIDs(Integer parentId, List<byte[]> sids)
throws IOException
{
// search for ACEs matching the tableParentId. use the index on the
// search for ACEs matching the given parentId. use the index on the
// objectId column if found (should be there)
Cursor cursor = createCursorWithOptionalIndex(
getAccessControlEntries(), ACE_COL_OBJECT_ID, _tableParentId);
getAccessControlEntries(), ACE_COL_OBJECT_ID, parentId);
for(Row row : cursor) {
Integer objId = row.getInt(ACE_COL_OBJECT_ID);
if(_tableParentId.equals(objId)) {
_newTableSIDs.add(row.getBytes(ACE_COL_SID));
if(parentId.equals(objId)) {
sids.add(row.getBytes(ACE_COL_SID));
}
}

if(_newTableSIDs.isEmpty()) {
if(sids.isEmpty()) {
// if all else fails, use the hard-coded default
_newTableSIDs.add(SYS_DEFAULT_SID);
sids.add(SYS_DEFAULT_SID);
}
}

@@ -1582,6 +1714,15 @@ public class DatabaseImpl implements Database
}
_pageChannel.close();
}

public void validateNewTableName(String name) throws IOException {
if(lookupTable(name) != null) {
throw new IllegalArgumentException(withErrorContext(
"Cannot create table with name of existing table '" + name + "'"));
}

validateIdentifierName(name, getFormat().MAX_TABLE_NAME_LENGTH, "table");
}
/**
* Validates an identifier name.

+ 21
- 1
src/main/java/com/healthmarketscience/jackcess/impl/FKEnforcer.java View File

@@ -50,7 +50,7 @@ final class FKEnforcer
CaseInsensitiveColumnMatcher.INSTANCE;

private final TableImpl _table;
private final List<ColumnImpl> _cols;
private List<ColumnImpl> _cols;
private List<Joiner> _primaryJoinersChkUp;
private List<Joiner> _primaryJoinersChkDel;
private List<Joiner> _primaryJoinersDoUp;
@@ -62,6 +62,10 @@ final class FKEnforcer
_table = table;

// at this point, only init the index columns
initColumns();
}

private void initColumns() {
Set<ColumnImpl> cols = new TreeSet<ColumnImpl>();
for(IndexImpl idx : _table.getIndexes()) {
IndexImpl.ForeignKeyReference ref = idx.getReference();
@@ -78,6 +82,22 @@ final class FKEnforcer
Collections.<ColumnImpl>emptyList();
}

/**
* Resets the internals of this FKEnforcer (for post-table modification)
*/
void reset() {
// columns to enforce may have changed
initColumns();

// clear any existing joiners (will be re-created on next use)
_primaryJoinersChkUp = null;
_primaryJoinersChkDel = null;
_primaryJoinersDoUp = null;
_primaryJoinersDoDel = null;
_primaryJoinersDoNull = null;
_secondaryJoiners = null;
}

/**
* Does secondary initialization, if necessary.
*/

+ 78
- 44
src/main/java/com/healthmarketscience/jackcess/impl/IndexData.java View File

@@ -21,12 +21,12 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;

import com.healthmarketscience.jackcess.ColumnBuilder;
import com.healthmarketscience.jackcess.ConstraintViolationException;
import com.healthmarketscience.jackcess.Index;
import com.healthmarketscience.jackcess.IndexBuilder;
@@ -372,6 +372,10 @@ public class IndexData {
_ownedPages.addPageNumber(pageNumber);
}

void collectUsageMapPages(Collection<Integer> pages) {
pages.add(_ownedPages.getTablePageNumber());
}
/**
* Used by unit tests to validate the internal status of the index.
* @usage _advanced_method_
@@ -477,9 +481,21 @@ public class IndexData {
*/
protected static void writeRowCountDefinitions(
TableCreator creator, ByteBuffer buffer)
{
writeRowCountDefinitions(creator, buffer, creator.getIndexCount());
}

/**
* Writes the index row count definitions into a table definition buffer.
* @param creator description of the indexes to write
* @param buffer Buffer to write to
* @param idxCount num indexes to write
*/
protected static void writeRowCountDefinitions(
TableMutator creator, ByteBuffer buffer, int idxCount)
{
// index row counts (empty data)
ByteUtil.forward(buffer, (creator.getIndexCount() *
ByteUtil.forward(buffer, (idxCount *
creator.getFormat().SIZE_INDEX_DEFINITION));
}

@@ -492,60 +508,78 @@ public class IndexData {
TableCreator creator, ByteBuffer buffer)
throws IOException
{
ByteBuffer rootPageBuffer = creator.getPageChannel().createPageBuffer();
writeDataPage(rootPageBuffer, NEW_ROOT_DATA_PAGE,
creator.getTdefPageNumber(), creator.getFormat());
ByteBuffer rootPageBuffer = createRootPageBuffer(creator);

for(IndexBuilder idx : creator.getIndexes()) {
buffer.putInt(MAGIC_INDEX_NUMBER); // seemingly constant magic value
for(TableMutator.IndexDataState idxDataState : creator.getIndexDataStates()) {
writeDefinition(creator, buffer, idxDataState, rootPageBuffer);
}
}

// write column information (always MAX_COLUMNS entries)
List<IndexBuilder.Column> idxColumns = idx.getColumns();
for(int i = 0; i < MAX_COLUMNS; ++i) {
/**
* Writes the index definitions into a table definition buffer.
* @param creator description of the indexes to write
* @param buffer Buffer to write to
*/
protected static void writeDefinition(
TableMutator creator, ByteBuffer buffer,
TableMutator.IndexDataState idxDataState, ByteBuffer rootPageBuffer)
throws IOException
{
if(rootPageBuffer == null) {
rootPageBuffer = createRootPageBuffer(creator);
}

short columnNumber = COLUMN_UNUSED;
byte flags = 0;
buffer.putInt(MAGIC_INDEX_NUMBER); // seemingly constant magic value

if(i < idxColumns.size()) {
// write column information (always MAX_COLUMNS entries)
IndexBuilder idx = idxDataState.getFirstIndex();
List<IndexBuilder.Column> idxColumns = idx.getColumns();
for(int i = 0; i < MAX_COLUMNS; ++i) {

// determine column info
IndexBuilder.Column idxCol = idxColumns.get(i);
flags = idxCol.getFlags();
short columnNumber = COLUMN_UNUSED;
byte flags = 0;

// find actual table column number
for(ColumnBuilder col : creator.getColumns()) {
if(col.getName().equalsIgnoreCase(idxCol.getName())) {
columnNumber = col.getColumnNumber();
break;
}
}
if(columnNumber == COLUMN_UNUSED) {
// should never happen as this is validated before
throw new IllegalArgumentException(
withErrorContext(
"Column with name " + idxCol.getName() + " not found",
creator.getDatabase(), creator.getName(), idx.getName()));
}
if(i < idxColumns.size()) {
// determine column info
IndexBuilder.Column idxCol = idxColumns.get(i);
flags = idxCol.getFlags();
// find actual table column number
columnNumber = creator.getColumnNumber(idxCol.getName());
if(columnNumber == COLUMN_UNUSED) {
// should never happen as this is validated before
throw new IllegalArgumentException(
withErrorContext(
"Column with name " + idxCol.getName() + " not found",
creator.getDatabase(), creator.getTableName(), idx.getName()));
}
buffer.putShort(columnNumber); // table column number
buffer.put(flags); // column flags (e.g. ordering)
}
buffer.putShort(columnNumber); // table column number
buffer.put(flags); // column flags (e.g. ordering)
}

TableCreator.IndexState idxState = creator.getIndexState(idx);
buffer.put(idxDataState.getUmapRowNumber()); // umap row
ByteUtil.put3ByteInt(buffer, idxDataState.getUmapPageNumber()); // umap page

buffer.put(idxState.getUmapRowNumber()); // umap row
ByteUtil.put3ByteInt(buffer, creator.getUmapPageNumber()); // umap page
// write empty root index page
creator.getPageChannel().writePage(rootPageBuffer,
idxDataState.getRootPageNumber());

// write empty root index page
creator.getPageChannel().writePage(rootPageBuffer,
idxState.getRootPageNumber());
buffer.putInt(idxDataState.getRootPageNumber());
buffer.putInt(0); // unknown
buffer.put(idx.getFlags()); // index flags (unique, etc.)
ByteUtil.forward(buffer, 5); // unknown
}

buffer.putInt(idxState.getRootPageNumber());
buffer.putInt(0); // unknown
buffer.put(idx.getFlags()); // index flags (unique, etc.)
ByteUtil.forward(buffer, 5); // unknown
}
private static ByteBuffer createRootPageBuffer(TableMutator creator)
throws IOException
{
ByteBuffer rootPageBuffer = creator.getPageChannel().createPageBuffer();
writeDataPage(rootPageBuffer, NEW_ROOT_DATA_PAGE,
creator.getTdefPageNumber(), creator.getFormat());
return rootPageBuffer;
}

/**

+ 46
- 14
src/main/java/com/healthmarketscience/jackcess/impl/IndexImpl.java View File

@@ -54,7 +54,9 @@ public class IndexImpl implements Index, Comparable<IndexImpl>
private static final byte CASCADE_NULL_FLAG = (byte)2;

/** index table type for the "primary" table in a foreign key index */
private static final byte PRIMARY_TABLE_TYPE = (byte)1;
static final byte FK_PRIMARY_TABLE_TYPE = (byte)1;
/** index table type for the "secondary" table in a foreign key index */
static final byte FK_SECONDARY_TABLE_TYPE = (byte)2;

/** indicate an invalid index number for foreign key field */
private static final int INVALID_INDEX_NUMBER = -1;
@@ -75,7 +77,6 @@ public class IndexImpl implements Index, Comparable<IndexImpl>
JetFormat format)
throws IOException
{

ByteUtil.forward(tableBuffer, format.SKIP_BEFORE_INDEX_SLOT); //Forward past Unknown
_indexNumber = tableBuffer.getInt();
int indexDataNumber = tableBuffer.getInt();
@@ -342,17 +343,7 @@ public class IndexImpl implements Index, Comparable<IndexImpl>
{
// write logical index information
for(IndexBuilder idx : creator.getIndexes()) {
TableCreator.IndexState idxState = creator.getIndexState(idx);
buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def
buffer.putInt(idxState.getIndexNumber()); // index num
buffer.putInt(idxState.getIndexDataNumber()); // index data num
buffer.put((byte)0); // related table type
buffer.putInt(INVALID_INDEX_NUMBER); // related index num
buffer.putInt(0); // related table definition page number
buffer.put((byte)0); // cascade updates flag
buffer.put((byte)0); // cascade deletes flag
buffer.put(idx.getType()); // index type flags
buffer.putInt(0); // unknown
writeDefinition(creator, idx, buffer);
}

// write index names
@@ -361,6 +352,47 @@ public class IndexImpl implements Index, Comparable<IndexImpl>
}
}

protected static void writeDefinition(
TableMutator mutator, IndexBuilder idx, ByteBuffer buffer)
throws IOException
{
TableMutator.IndexDataState idxDataState = mutator.getIndexDataState(idx);

// write logical index information
buffer.putInt(TableImpl.MAGIC_TABLE_NUMBER); // seemingly constant magic value which matches the table def
buffer.putInt(idx.getIndexNumber()); // index num
buffer.putInt(idxDataState.getIndexDataNumber()); // index data num

byte idxType = idx.getType();
if(idxType != FOREIGN_KEY_INDEX_TYPE) {
buffer.put((byte)0); // related table type
buffer.putInt(INVALID_INDEX_NUMBER); // related index num
buffer.putInt(0); // related table definition page number
buffer.put((byte)0); // cascade updates flag
buffer.put((byte)0); // cascade deletes flag
} else {
ForeignKeyReference reference = mutator.getForeignKey(idx);
buffer.put(reference.getTableType()); // related table type
buffer.putInt(reference.getOtherIndexNumber()); // related index num
buffer.putInt(reference.getOtherTablePageNumber()); // related table definition page number
byte updateFlags = 0;
if(reference.isCascadeUpdates()) {
updateFlags |= CASCADE_UPDATES_FLAG;
}
byte deleteFlags = 0;
if(reference.isCascadeDeletes()) {
deleteFlags |= CASCADE_DELETES_FLAG;
}
if(reference.isCascadeNullOnDelete()) {
deleteFlags |= CASCADE_NULL_FLAG;
}
buffer.put(updateFlags); // cascade updates flag
buffer.put(deleteFlags); // cascade deletes flag
}
buffer.put(idxType); // index type flags
buffer.putInt(0); // unknown
}

private String withErrorContext(String msg) {
return withErrorContext(msg, getTable().getDatabase(), getName());
}
@@ -401,7 +433,7 @@ public class IndexImpl implements Index, Comparable<IndexImpl>
}

public boolean isPrimaryTable() {
return(getTableType() == PRIMARY_TABLE_TYPE);
return(getTableType() == FK_PRIMARY_TABLE_TYPE);
}

public int getOtherIndexNumber() {

+ 1
- 1
src/main/java/com/healthmarketscience/jackcess/impl/JetFormat.java View File

@@ -672,7 +672,7 @@ public abstract class JetFormat {
@Override
protected int defineMaxCompressedUnicodeSize() { return 1024; }
@Override
protected int defineSizeTdefHeader() { return 63; }
protected int defineSizeTdefHeader() { return 43; }
@Override
protected int defineSizeTdefTrailer() { return 2; }
@Override

+ 17
- 0
src/main/java/com/healthmarketscience/jackcess/impl/LongValueColumnImpl.java View File

@@ -17,8 +17,10 @@ limitations under the License.
package com.healthmarketscience.jackcess.impl;

import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Collection;


/**
@@ -69,6 +71,11 @@ class LongValueColumnImpl extends ColumnImpl
_lvalBufferH = new UmapLongValueBufferHolder(ownedPages, freeSpacePages);
}

@Override
void collectUsageMapPages(Collection<Integer> pages) {
_lvalBufferH.collectUsageMapPages(pages);
}
@Override
void postTableLoadInit() throws IOException {
if(_lvalBufferH == null) {
@@ -440,6 +447,10 @@ class LongValueColumnImpl extends ColumnImpl
getBufferHolder().clear();
}

public void collectUsageMapPages(Collection<Integer> pages) {
// base does nothing
}

protected abstract TempPageHolder getBufferHolder();
}

@@ -516,5 +527,11 @@ class LongValueColumnImpl extends ColumnImpl
}
super.clear();
}

@Override
public void collectUsageMapPages(Collection<Integer> pages) {
pages.add(_ownedPages.getTablePageNumber());
pages.add(_freeSpacePages.getTablePageNumber());
}
}
}

+ 13
- 0
src/main/java/com/healthmarketscience/jackcess/impl/PageChannel.java View File

@@ -134,6 +134,19 @@ public class PageChannel implements Channel, Flushable {
++_writeCount;
}

/**
* Begins an exclusive "logical" write operation (throws an exception if
* another write operation is outstanding). See {@link #finishWrite} for
* more details.
*/
public void startExclusiveWrite() {
if(_writeCount != 0) {
throw new IllegalArgumentException(
"Another write operation is currently in progress");
}
startWrite();
}

/**
* Completes a "logical" write operation. This method should be called in
* finally block which wraps a logical write operation (which is preceded by

+ 346
- 0
src/main/java/com/healthmarketscience/jackcess/impl/RelationshipCreator.java View File

@@ -0,0 +1,346 @@
/*
Copyright (c) 2016 James Ahlborn

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.healthmarketscience.jackcess.impl;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.healthmarketscience.jackcess.ConstraintViolationException;
import com.healthmarketscience.jackcess.IndexBuilder;
import com.healthmarketscience.jackcess.IndexCursor;
import com.healthmarketscience.jackcess.RelationshipBuilder;
import com.healthmarketscience.jackcess.Row;

/**
* Helper class used to maintain state during relationship creation.
*
* @author James Ahlborn
*/
public class RelationshipCreator extends DBMutator
{
private final static int CASCADE_FLAGS =
RelationshipImpl.CASCADE_DELETES_FLAG |
RelationshipImpl.CASCADE_UPDATES_FLAG |
RelationshipImpl.CASCADE_NULL_FLAG;

// for the purposes of choosing a backing index for a foreign key, there are
// certain index flags that can be ignored (we don't care how they are set)
private final static byte IGNORED_PRIMARY_INDEX_FLAGS =
IndexData.IGNORE_NULLS_INDEX_FLAG | IndexData.REQUIRED_INDEX_FLAG;
private final static byte IGNORED_SECONDARY_INDEX_FLAGS =
IGNORED_PRIMARY_INDEX_FLAGS | IndexData.UNIQUE_INDEX_FLAG;
private TableImpl _primaryTable;
private TableImpl _secondaryTable;
private RelationshipBuilder _relationship;
private List<ColumnImpl> _primaryCols;
private List<ColumnImpl> _secondaryCols;
private int _flags;
private String _name;
public RelationshipCreator(DatabaseImpl database)
{
super(database);
}

public TableImpl getPrimaryTable() {
return _primaryTable;
}

public TableImpl getSecondaryTable() {
return _secondaryTable;
}

public boolean hasReferentialIntegrity() {
return _relationship.hasReferentialIntegrity();
}

public RelationshipImpl createRelationshipImpl(String name) {
_name = name;
RelationshipImpl newRel = new RelationshipImpl(
name, _primaryTable, _secondaryTable, _flags,
_primaryCols, _secondaryCols);
return newRel;
}

/**
* Creates the relationship in the database.
* @usage _advanced_method_
*/
public RelationshipImpl createRelationship(RelationshipBuilder relationship)
throws IOException
{
_relationship = relationship;
validate();

_flags = _relationship.getFlags();
// need to determine the one-to-one flag on our own
if(isOneToOne()) {
_flags |= RelationshipImpl.ONE_TO_ONE_FLAG;
}

getPageChannel().startExclusiveWrite();
try {

RelationshipImpl newRel = getDatabase().writeRelationship(this);

if(hasReferentialIntegrity()) {
addPrimaryIndex();
addSecondaryIndex();
}

return newRel;

} finally {
getPageChannel().finishWrite();
}
}

private void addPrimaryIndex() throws IOException {
TableUpdater updater = new TableUpdater(_primaryTable);
updater.setForeignKey(createFKReference(true));
updater.addIndex(createPrimaryIndex(), true,
IGNORED_PRIMARY_INDEX_FLAGS, (byte)0);
}

private void addSecondaryIndex() throws IOException {
TableUpdater updater = new TableUpdater(_secondaryTable);
updater.setForeignKey(createFKReference(false));
updater.addIndex(createSecondaryIndex(), true,
IGNORED_SECONDARY_INDEX_FLAGS, (byte)0);
}

private IndexImpl.ForeignKeyReference createFKReference(boolean isPrimary) {
byte tableType = 0;
int otherTableNum = 0;
int otherIdxNum = 0;
if(isPrimary) {
tableType = IndexImpl.FK_PRIMARY_TABLE_TYPE;
otherTableNum = _secondaryTable.getTableDefPageNumber();
// we create the primary index first, so the secondary index does not
// exist yet
otherIdxNum = _secondaryTable.getLogicalIndexCount();
} else {
tableType = IndexImpl.FK_SECONDARY_TABLE_TYPE;
otherTableNum = _primaryTable.getTableDefPageNumber();
// at this point, we've already created the primary index, it's the last
// one on the primary table
otherIdxNum = _primaryTable.getLogicalIndexCount() - 1;
}
boolean cascadeUpdates = ((_flags & RelationshipImpl.CASCADE_UPDATES_FLAG) != 0);
boolean cascadeDeletes = ((_flags & RelationshipImpl.CASCADE_DELETES_FLAG) != 0);
boolean cascadeNull = ((_flags & RelationshipImpl.CASCADE_NULL_FLAG) != 0);

return new IndexImpl.ForeignKeyReference(
tableType, otherIdxNum, otherTableNum, cascadeUpdates, cascadeDeletes,
cascadeNull);
}

private void validate() throws IOException {

_primaryTable = getDatabase().getTable(_relationship.getFromTable());
_secondaryTable = getDatabase().getTable(_relationship.getToTable());
if((_primaryTable == null) || (_secondaryTable == null)) {
throw new IllegalArgumentException(withErrorContext(
"Two valid tables are required in relationship"));
}

_primaryCols = getColumns(_primaryTable, _relationship.getFromColumns());
_secondaryCols = getColumns(_secondaryTable, _relationship.getToColumns());
if((_primaryCols == null) || (_primaryCols.isEmpty()) ||
(_secondaryCols == null) || (_secondaryCols.isEmpty())) {
throw new IllegalArgumentException(withErrorContext(
"Missing columns in relationship"));
}

if(_primaryCols.size() != _secondaryCols.size()) {
throw new IllegalArgumentException(withErrorContext(
"Must have same number of columns on each side of relationship"));
}

for(int i = 0; i < _primaryCols.size(); ++i) {
ColumnImpl pcol = _primaryCols.get(i);
ColumnImpl scol = _primaryCols.get(i);

if(pcol.getType() != scol.getType()) {
throw new IllegalArgumentException(withErrorContext(
"Matched columns must have the same data type"));
}
}

if(!hasReferentialIntegrity()) {

if((_relationship.getFlags() & CASCADE_FLAGS) != 0) {
throw new IllegalArgumentException(withErrorContext(
"Cascade flags cannot be enabled if referential integrity is not enforced"));
}
return;
}

// for now, we will require the unique index on the primary table (just
// like access does). we could just create it auto-magically...
IndexImpl primaryIdx = getPrimaryUniqueIndex();
if(primaryIdx == null) {
throw new IllegalArgumentException(withErrorContext(
"Missing unique index on primary table required to enforce integrity"));
}

// while relationships can have "dupe" columns, indexes (and therefore
// integrity enforced relationships) cannot
if((new HashSet<String>(getColumnNames(_primaryCols)).size() !=
_primaryCols.size()) ||
(new HashSet<String>(getColumnNames(_secondaryCols)).size() !=
_secondaryCols.size())) {
throw new IllegalArgumentException(withErrorContext(
"Cannot have duplicate columns in an integrity enforced relationship"));
}
// TODO: future, check for enforce cycles?

// check referential integrity
IndexCursor primaryCursor = primaryIdx.newCursor().toIndexCursor();
Object[] entryValues = new Object[_secondaryCols.size()];
for(Row row : _secondaryTable.newCursor().toCursor()
.newIterable().addColumns(_secondaryCols)) {
// grab the secondary table values
boolean hasValues = false;
for(int i = 0; i < _secondaryCols.size(); ++i) {
entryValues[i] = _secondaryCols.get(i).getRowValue(row);
hasValues = hasValues || (entryValues[i] != null);
}

if(!hasValues) {
// we can ignore null entries
continue;
}

// check that they exist in the primary table
if(!primaryCursor.findFirstRowByEntry(entryValues)) {
throw new ConstraintViolationException(withErrorContext(
"Integrity constraint violation found for relationship"));
}
}

}

private IndexBuilder createPrimaryIndex() {
String name = createPrimaryIndexName();
return createIndex(name, _primaryCols)
.setUnique()
.setType(IndexImpl.FOREIGN_KEY_INDEX_TYPE);
}
private IndexBuilder createSecondaryIndex() {
// secondary index uses relationship name
return createIndex(_name, _secondaryCols)
.setType(IndexImpl.FOREIGN_KEY_INDEX_TYPE);
}
private static IndexBuilder createIndex(String name, List<ColumnImpl> cols) {
IndexBuilder idx = new IndexBuilder(name);
for(ColumnImpl col : cols) {
idx.addColumns(col.getName());
}
return idx;
}

private String createPrimaryIndexName() {
Set<String> idxNames = TableUpdater.getIndexNames(_primaryTable, null);

// primary naming scheme: ".rB", .rC", ".rD", "rE" ...
String baseName = ".r";
String suffix = "B";

while(true) {
String idxName = baseName + suffix;
if(!idxNames.contains(DatabaseImpl.toLookupName(idxName))) {
return idxName;
}

char c = (char)(suffix.charAt(0) + 1);
if(c == '[') {
c = 'a';
}
suffix = "" + c;
}
}

private static List<ColumnImpl> getColumns(TableImpl table,
List<String> colNames) {
List<ColumnImpl> cols = new ArrayList<ColumnImpl>();
for(String colName : colNames) {
cols.add(table.getColumn(colName));
}
return cols;
}

private static List<String> getColumnNames(List<ColumnImpl> cols) {
List<String> colNames = new ArrayList<String>();
for(ColumnImpl col : cols) {
colNames.add(col.getName());
}
return colNames;
}

private boolean isOneToOne() {
// a relationship is one to one if the two sides of the relationship have
// unique indexes on the relevant columns
if(getPrimaryUniqueIndex() == null) {
return false;
}
IndexImpl idx = _secondaryTable.findIndexForColumns(
getColumnNames(_secondaryCols), true);
return (idx != null);
}

private IndexImpl getPrimaryUniqueIndex() {
return _primaryTable.findIndexForColumns(getColumnNames(_primaryCols), true);
}

private static String getTableErrorContext(
TableImpl table, List<ColumnImpl> cols,
String tableName, Collection<String> colNames) {
if(table != null) {
tableName = table.getName();
}
if(cols != null) {
colNames = getColumnNames(cols);
}

return CustomToStringStyle.valueBuilder(tableName)
.append(null, cols)
.toString();
}
private String withErrorContext(String msg) {
return msg + "(Rel=" +
getTableErrorContext(_primaryTable, _primaryCols,
_relationship.getFromTable(),
_relationship.getFromColumns()) + " -> " +
getTableErrorContext(_secondaryTable, _secondaryCols,
_relationship.getToTable(),
_relationship.getToColumns()) + ")";
}
}

+ 27
- 11
src/main/java/com/healthmarketscience/jackcess/impl/RelationshipImpl.java View File

@@ -33,20 +33,20 @@ public class RelationshipImpl implements Relationship
{

/** flag indicating one-to-one relationship */
private static final int ONE_TO_ONE_FLAG = 0x00000001;
public static final int ONE_TO_ONE_FLAG = 0x00000001;
/** flag indicating no referential integrity */
private static final int NO_REFERENTIAL_INTEGRITY_FLAG = 0x00000002;
public static final int NO_REFERENTIAL_INTEGRITY_FLAG = 0x00000002;
/** flag indicating cascading updates (requires referential integrity) */
private static final int CASCADE_UPDATES_FLAG = 0x00000100;
public static final int CASCADE_UPDATES_FLAG = 0x00000100;
/** flag indicating cascading deletes (requires referential integrity) */
private static final int CASCADE_DELETES_FLAG = 0x00001000;
public static final int CASCADE_DELETES_FLAG = 0x00001000;
/** flag indicating cascading null on delete (requires referential
integrity) */
private static final int CASCADE_NULL_FLAG = 0x00002000;
public static final int CASCADE_NULL_FLAG = 0x00002000;
/** flag indicating left outer join */
private static final int LEFT_OUTER_JOIN_FLAG = 0x01000000;
public static final int LEFT_OUTER_JOIN_FLAG = 0x01000000;
/** flag indicating right outer join */
private static final int RIGHT_OUTER_JOIN_FLAG = 0x02000000;
public static final int RIGHT_OUTER_JOIN_FLAG = 0x02000000;

/** the name of this relationship */
private final String _name;
@@ -65,14 +65,21 @@ public class RelationshipImpl implements Relationship

public RelationshipImpl(String name, Table fromTable, Table toTable, int flags,
int numCols)
{
this(name, fromTable, toTable, flags,
Collections.nCopies(numCols, (Column)null),
Collections.nCopies(numCols, (Column)null));
}

public RelationshipImpl(String name, Table fromTable, Table toTable, int flags,
List<? extends Column> fromCols,
List<? extends Column> toCols)
{
_name = name;
_fromTable = fromTable;
_fromColumns = new ArrayList<Column>(
Collections.nCopies(numCols, (Column)null));
_fromColumns = new ArrayList<Column>(fromCols);
_toTable = toTable;
_toColumns = new ArrayList<Column>(
Collections.nCopies(numCols, (Column)null));
_toColumns = new ArrayList<Column>(toCols);
_flags = flags;
}

@@ -127,6 +134,15 @@ public class RelationshipImpl implements Relationship
public boolean isRightOuterJoin() {
return hasFlag(RIGHT_OUTER_JOIN_FLAG);
}

public JoinType getJoinType() {
if(isLeftOuterJoin()) {
return JoinType.LEFT_OUTER;
} else if(isRightOuterJoin()) {
return JoinType.RIGHT_OUTER;
}
return JoinType.INNER;
}
private boolean hasFlag(int flagMask) {
return((getFlags() & flagMask) != 0);

+ 147
- 172
src/main/java/com/healthmarketscience/jackcess/impl/TableCreator.java View File

@@ -17,7 +17,6 @@ limitations under the License.
package com.healthmarketscience.jackcess.impl;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
@@ -30,6 +29,8 @@ import java.util.Set;
import com.healthmarketscience.jackcess.ColumnBuilder;
import com.healthmarketscience.jackcess.DataType;
import com.healthmarketscience.jackcess.IndexBuilder;
import com.healthmarketscience.jackcess.PropertyMap;
import com.healthmarketscience.jackcess.TableBuilder;

/**
* Helper class used to maintain state during table creation.
@@ -37,14 +38,13 @@ import com.healthmarketscience.jackcess.IndexBuilder;
* @author James Ahlborn
* @usage _advanced_class_
*/
class TableCreator
public class TableCreator extends TableMutator
{
private final DatabaseImpl _database;
private final String _name;
private final List<ColumnBuilder> _columns;
private final List<IndexBuilder> _indexes;
private final Map<IndexBuilder,IndexState> _indexStates =
new IdentityHashMap<IndexBuilder,IndexState>();
private String _name;
private List<ColumnBuilder> _columns;
private List<IndexBuilder> _indexes;
private final List<IndexDataState> _indexDataStates =
new ArrayList<IndexDataState>();
private final Map<ColumnBuilder,ColumnState> _columnStates =
new IdentityHashMap<ColumnBuilder,ColumnState>();
private final List<ColumnBuilder> _lvalCols = new ArrayList<ColumnBuilder>();
@@ -53,35 +53,20 @@ class TableCreator
private int _indexCount;
private int _logicalIndexCount;

public TableCreator(DatabaseImpl database, String name, List<ColumnBuilder> columns,
List<IndexBuilder> indexes) {
_database = database;
_name = name;
_columns = columns;
_indexes = ((indexes != null) ? indexes :
Collections.<IndexBuilder>emptyList());
public TableCreator(DatabaseImpl database) {
super(database);
}

public String getName() {
return _name;
}

public DatabaseImpl getDatabase() {
return _database;
@Override
String getTableName() {
return getName();
}

public JetFormat getFormat() {
return _database.getFormat();
}

public PageChannel getPageChannel() {
return _database.getPageChannel();
}

public Charset getCharset() {
return _database.getCharset();
}

@Override
public int getTdefPageNumber() {
return _tdefPageNumber;
}
@@ -110,14 +95,24 @@ class TableCreator
return _logicalIndexCount;
}

public IndexState getIndexState(IndexBuilder idx) {
return _indexStates.get(idx);
@Override
public IndexDataState getIndexDataState(IndexBuilder idx) {
for(IndexDataState idxDataState : _indexDataStates) {
for(IndexBuilder curIdx : idxDataState.getIndexes()) {
if(idx == curIdx) {
return idxDataState;
}
}
}
throw new IllegalStateException(withErrorContext(
"could not find state for index"));
}

public int reservePageNumber() throws IOException {
return getPageChannel().allocateNewPage();
public List<IndexDataState> getIndexDataStates() {
return _indexDataStates;
}

@Override
public ColumnState getColumnState(ColumnBuilder col) {
return _columnStates.get(col);
}
@@ -126,11 +121,44 @@ class TableCreator
return _lvalCols;
}

@Override
short getColumnNumber(String colName) {
for(ColumnBuilder col : _columns) {
if(col.getName().equalsIgnoreCase(colName)) {
return col.getColumnNumber();
}
}
return IndexData.COLUMN_UNUSED;
}

/**
* @return The number of variable length columns which are not long values
* found in the list
* @usage _advanced_method_
*/
public short countNonLongVariableLength() {
short rtn = 0;
for (ColumnBuilder col : _columns) {
if (col.isVariableLength() && !col.getType().isLongValue()) {
rtn++;
}
}
return rtn;
}

/**
* Creates the table in the database.
* @usage _advanced_method_
*/
public void createTable() throws IOException {
public TableImpl createTable(TableBuilder table) throws IOException {

_name = table.getName();
_columns = table.getColumns();
_indexes = table.getIndexes();
if(_indexes == null) {
_indexes = Collections.<IndexBuilder>emptyList();
}

validate();

@@ -146,17 +174,14 @@ class TableCreator
}

if(hasIndexes()) {
// sort out index numbers. for now, these values will always match
// (until we support writing foreign key indexes)
// sort out index numbers (and backing index data).
for(IndexBuilder idx : _indexes) {
IndexState idxState = new IndexState();
idxState.setIndexNumber(_logicalIndexCount++);
idxState.setIndexDataNumber(_indexCount++);
_indexStates.put(idx, idxState);
idx.setIndexNumber(_logicalIndexCount++);
findIndexDataState(idx);
}
}

getPageChannel().startWrite();
getPageChannel().startExclusiveWrite();
try {
// reserve some pages
@@ -167,58 +192,79 @@ class TableCreator
TableImpl.writeTableDefinition(this);

// update the database with the new table info
_database.addNewTable(_name, _tdefPageNumber, DatabaseImpl.TYPE_TABLE, null, null);
getDatabase().addNewTable(_name, _tdefPageNumber, DatabaseImpl.TYPE_TABLE,
null, null);

TableImpl newTable = getDatabase().getTable(_name);

// add any table properties
boolean addedProps = false;
Map<String,PropertyMap.Property> props = table.getProperties();
if(props != null) {
newTable.getProperties().putAll(props.values());
addedProps = true;
}
for(ColumnBuilder cb : _columns) {
Map<String,PropertyMap.Property> colProps = cb.getProperties();
if(colProps != null) {
newTable.getColumn(cb.getName()).getProperties()
.putAll(colProps.values());
addedProps = true;
}
}

// all table and column props are saved together
if(addedProps) {
newTable.getProperties().save();
}

return newTable;

} finally {
getPageChannel().finishWrite();
}
}

private IndexDataState findIndexDataState(IndexBuilder idx) {

// search for an index which matches the given index (in terms of the
// backing data)
for(IndexDataState idxDataState : _indexDataStates) {
if(sameIndexData(idxDataState.getFirstIndex(), idx)) {
idxDataState.addIndex(idx);
return idxDataState;
}
}

// no matches found, need new index data state
IndexDataState idxDataState = new IndexDataState();
idxDataState.setIndexDataNumber(_indexCount++);
idxDataState.addIndex(idx);
_indexDataStates.add(idxDataState);
return idxDataState;
}

/**
* Validates the new table information before attempting creation.
*/
private void validate() {
private void validate() throws IOException {

DatabaseImpl.validateIdentifierName(
_name, getFormat().MAX_TABLE_NAME_LENGTH, "table");
getDatabase().validateNewTableName(_name);
if((_columns == null) || _columns.isEmpty()) {
throw new IllegalArgumentException(
"Cannot create table with no columns");
throw new IllegalArgumentException(withErrorContext(
"Cannot create table with no columns"));
}
if(_columns.size() > getFormat().MAX_COLUMNS_PER_TABLE) {
throw new IllegalArgumentException(
throw new IllegalArgumentException(withErrorContext(
"Cannot create table with more than " +
getFormat().MAX_COLUMNS_PER_TABLE + " columns");
getFormat().MAX_COLUMNS_PER_TABLE + " columns"));
}
ColumnImpl.SortOrder dbSortOrder = null;
try {
dbSortOrder = _database.getDefaultSortOrder();
} catch(IOException e) {
// ignored, just use the jet format default
}

Set<String> colNames = new HashSet<String>();
// next, validate the column definitions
for(ColumnBuilder column : _columns) {

// FIXME for now, we can't create complex columns
if(column.getType() == DataType.COMPLEX_TYPE) {
throw new UnsupportedOperationException(
"Complex column creation is not yet implemented");
}
column.validate(getFormat());
if(!colNames.add(column.getName().toUpperCase())) {
throw new IllegalArgumentException("duplicate column name: " +
column.getName());
}

// set the sort order to the db default (if unspecified)
if(column.getType().isTextual() && (column.getTextSortOrder() == null)) {
column.setTextSortOrder(dbSortOrder);
}
validateColumn(colNames, column);
}

List<ColumnBuilder> autoCols = getAutoNumberColumns();
@@ -226,39 +272,23 @@ class TableCreator
// for most autonumber types, we can only have one of each type
Set<DataType> autoTypes = EnumSet.noneOf(DataType.class);
for(ColumnBuilder c : autoCols) {
if(!c.getType().isMultipleAutoNumberAllowed() &&
!autoTypes.add(c.getType())) {
throw new IllegalArgumentException(
"Can have at most one AutoNumber column of type " + c.getType() +
" per table");
}
validateAutoNumberColumn(autoTypes, c);
}
}

if(hasIndexes()) {

if(_indexes.size() > getFormat().MAX_INDEXES_PER_TABLE) {
throw new IllegalArgumentException(
throw new IllegalArgumentException(withErrorContext(
"Cannot create table with more than " +
getFormat().MAX_INDEXES_PER_TABLE + " indexes");
getFormat().MAX_INDEXES_PER_TABLE + " indexes"));
}

// now, validate the indexes
Set<String> idxNames = new HashSet<String>();
boolean foundPk = false;
boolean foundPk[] = new boolean[1];
for(IndexBuilder index : _indexes) {
index.validate(colNames, getFormat());
if(!idxNames.add(index.getName().toUpperCase())) {
throw new IllegalArgumentException("duplicate index name: " +
index.getName());
}
if(index.isPrimaryKey()) {
if(foundPk) {
throw new IllegalArgumentException(
"found second primary key index: " + index.getName());
}
foundPk = true;
}
validateIndex(colNames, idxNames, foundPk, index);
}
}
}
@@ -274,92 +304,37 @@ class TableCreator
return autoCols;
}

/**
* Maintains additional state used during index creation.
* @usage _advanced_class_
*/
static final class IndexState
{
private int _indexNumber;
private int _indexDataNumber;
private byte _umapRowNumber;
private int _umapPageNumber;
private int _rootPageNumber;

public int getIndexNumber() {
return _indexNumber;
}

public void setIndexNumber(int newIndexNumber) {
_indexNumber = newIndexNumber;
}

public int getIndexDataNumber() {
return _indexDataNumber;
}

public void setIndexDataNumber(int newIndexDataNumber) {
_indexDataNumber = newIndexDataNumber;
}

public byte getUmapRowNumber() {
return _umapRowNumber;
private static boolean sameIndexData(IndexBuilder idx1, IndexBuilder idx2) {
// index data can be combined if flags match and columns (and col flags)
// match
if(idx1.getFlags() != idx2.getFlags()) {
return false;
}

public void setUmapRowNumber(byte newUmapRowNumber) {
_umapRowNumber = newUmapRowNumber;
}

public int getUmapPageNumber() {
return _umapPageNumber;
}

public void setUmapPageNumber(int newUmapPageNumber) {
_umapPageNumber = newUmapPageNumber;
if(idx1.getColumns().size() != idx2.getColumns().size()) {
return false;
}
for(int i = 0; i < idx1.getColumns().size(); ++i) {
IndexBuilder.Column col1 = idx1.getColumns().get(i);
IndexBuilder.Column col2 = idx2.getColumns().get(i);

public int getRootPageNumber() {
return _rootPageNumber;
if(!sameIndexData(col1, col2)) {
return false;
}
}

public void setRootPageNumber(int newRootPageNumber) {
_rootPageNumber = newRootPageNumber;
}
return true;
}
/**
* Maintains additional state used during column creation.
* @usage _advanced_class_
*/
static final class ColumnState
{
private byte _umapOwnedRowNumber;
private byte _umapFreeRowNumber;
// we always put both usage maps on the same page
private int _umapPageNumber;

public byte getUmapOwnedRowNumber() {
return _umapOwnedRowNumber;
private static boolean sameIndexData(
IndexBuilder.Column col1, IndexBuilder.Column col2) {
return (col1.getName().equals(col2.getName()) &&
(col1.getFlags() == col2.getFlags()));
}

public void setUmapOwnedRowNumber(byte newUmapOwnedRowNumber) {
_umapOwnedRowNumber = newUmapOwnedRowNumber;
}

public byte getUmapFreeRowNumber() {
return _umapFreeRowNumber;
}

public void setUmapFreeRowNumber(byte newUmapFreeRowNumber) {
_umapFreeRowNumber = newUmapFreeRowNumber;
}

public int getUmapPageNumber() {
return _umapPageNumber;
}

public void setUmapPageNumber(int newUmapPageNumber) {
_umapPageNumber = newUmapPageNumber;
}
@Override
protected String withErrorContext(String msg) {
return msg + "(Table=" + getName() + ")";
}
}

+ 689
- 84
src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
File diff suppressed because it is too large
View File


+ 250
- 0
src/main/java/com/healthmarketscience/jackcess/impl/TableMutator.java View File

@@ -0,0 +1,250 @@
/*
Copyright (c) 2016 James Ahlborn

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.healthmarketscience.jackcess.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import com.healthmarketscience.jackcess.ColumnBuilder;
import com.healthmarketscience.jackcess.DataType;
import com.healthmarketscience.jackcess.IndexBuilder;

/**
* Common helper class used to maintain state during table mutation.
*
* @author James Ahlborn
* @usage _advanced_class_
*/
public abstract class TableMutator extends DBMutator
{
private ColumnOffsets _colOffsets;

protected TableMutator(DatabaseImpl database) {
super(database);
}

public void setColumnOffsets(
int fixedOffset, int varOffset, int longVarOffset) {
if(_colOffsets == null) {
_colOffsets = new ColumnOffsets();
}
_colOffsets.set(fixedOffset, varOffset, longVarOffset);
}

public ColumnOffsets getColumnOffsets() {
return _colOffsets;
}

public IndexImpl.ForeignKeyReference getForeignKey(IndexBuilder idx) {
return null;
}

protected void validateColumn(Set<String> colNames, ColumnBuilder column) {

// FIXME for now, we can't create complex columns
if(column.getType() == DataType.COMPLEX_TYPE) {
throw new UnsupportedOperationException(withErrorContext(
"Complex column creation is not yet implemented"));
}
column.validate(getFormat());
if(!colNames.add(DatabaseImpl.toLookupName(column.getName()))) {
throw new IllegalArgumentException(withErrorContext(
"duplicate column name: " + column.getName()));
}

setColumnSortOrder(column);
}

protected void validateIndex(Set<String> colNames, Set<String> idxNames,
boolean[] foundPk, IndexBuilder index) {
index.validate(colNames, getFormat());
if(!idxNames.add(DatabaseImpl.toLookupName(index.getName()))) {
throw new IllegalArgumentException(withErrorContext(
"duplicate index name: " + index.getName()));
}
if(index.isPrimaryKey()) {
if(foundPk[0]) {
throw new IllegalArgumentException(withErrorContext(
"found second primary key index: " + index.getName()));
}
foundPk[0] = true;
} else if(index.getType() == IndexImpl.FOREIGN_KEY_INDEX_TYPE) {
if(getForeignKey(index) == null) {
throw new IllegalArgumentException(withErrorContext(
"missing foreign key info for " + index.getName()));
}
}
}

protected void validateAutoNumberColumn(Set<DataType> autoTypes,
ColumnBuilder column)
{
if(!column.getType().isMultipleAutoNumberAllowed() &&
!autoTypes.add(column.getType())) {
throw new IllegalArgumentException(withErrorContext(
"Can have at most one AutoNumber column of type " + column.getType() +
" per table"));
}
}

private void setColumnSortOrder(ColumnBuilder column) {
// set the sort order to the db default (if unspecified)
if(column.getType().isTextual() && (column.getTextSortOrder() == null)) {
column.setTextSortOrder(getDbSortOrder());
}
}

abstract String getTableName();

public abstract int getTdefPageNumber();

abstract short getColumnNumber(String colName);

public abstract ColumnState getColumnState(ColumnBuilder col);

public abstract IndexDataState getIndexDataState(IndexBuilder idx);

protected abstract String withErrorContext(String msg);

/**
* Maintains additional state used during column writing.
* @usage _advanced_class_
*/
static final class ColumnOffsets
{
private short _fixedOffset;
private short _varOffset;
private short _longVarOffset;

public void set(int fixedOffset, int varOffset, int longVarOffset) {
_fixedOffset = (short)fixedOffset;
_varOffset = (short)varOffset;
_longVarOffset = (short)longVarOffset;
}
public short getNextVariableOffset(ColumnBuilder col) {
if(!col.getType().isLongValue()) {
return _varOffset++;
}
return _longVarOffset++;
}

public short getNextFixedOffset(ColumnBuilder col) {
short offset = _fixedOffset;
_fixedOffset += col.getType().getFixedSize(col.getLength());
return offset;
}
}

/**
* Maintains additional state used during column creation.
* @usage _advanced_class_
*/
static final class ColumnState
{
private byte _umapOwnedRowNumber;
private byte _umapFreeRowNumber;
// we always put both usage maps on the same page
private int _umapPageNumber;

public byte getUmapOwnedRowNumber() {
return _umapOwnedRowNumber;
}

public void setUmapOwnedRowNumber(byte newUmapOwnedRowNumber) {
_umapOwnedRowNumber = newUmapOwnedRowNumber;
}

public byte getUmapFreeRowNumber() {
return _umapFreeRowNumber;
}

public void setUmapFreeRowNumber(byte newUmapFreeRowNumber) {
_umapFreeRowNumber = newUmapFreeRowNumber;
}

public int getUmapPageNumber() {
return _umapPageNumber;
}

public void setUmapPageNumber(int newUmapPageNumber) {
_umapPageNumber = newUmapPageNumber;
}
}

/**
* Maintains additional state used during index data creation.
* @usage _advanced_class_
*/
static final class IndexDataState
{
private final List<IndexBuilder> _indexes = new ArrayList<IndexBuilder>();
private int _indexDataNumber;
private byte _umapRowNumber;
private int _umapPageNumber;
private int _rootPageNumber;

public IndexBuilder getFirstIndex() {
// all indexes which have the same backing IndexDataState will have
// equivalent columns and flags.
return _indexes.get(0);
}

public List<IndexBuilder> getIndexes() {
return _indexes;
}

public void addIndex(IndexBuilder idx) {
_indexes.add(idx);
}

public int getIndexDataNumber() {
return _indexDataNumber;
}

public void setIndexDataNumber(int newIndexDataNumber) {
_indexDataNumber = newIndexDataNumber;
}

public byte getUmapRowNumber() {
return _umapRowNumber;
}

public void setUmapRowNumber(byte newUmapRowNumber) {
_umapRowNumber = newUmapRowNumber;
}

public int getUmapPageNumber() {
return _umapPageNumber;
}

public void setUmapPageNumber(int newUmapPageNumber) {
_umapPageNumber = newUmapPageNumber;
}

public int getRootPageNumber() {
return _rootPageNumber;
}

public void setRootPageNumber(int newRootPageNumber) {
_rootPageNumber = newRootPageNumber;
}
}
}

+ 332
- 0
src/main/java/com/healthmarketscience/jackcess/impl/TableUpdater.java View File

@@ -0,0 +1,332 @@
/*
Copyright (c) 2016 James Ahlborn

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.healthmarketscience.jackcess.impl;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.healthmarketscience.jackcess.ColumnBuilder;
import com.healthmarketscience.jackcess.DataType;
import com.healthmarketscience.jackcess.IndexBuilder;

/**
* Helper class used to maintain state during table mutation.
*
* @author James Ahlborn
* @usage _advanced_class_
*/
public class TableUpdater extends TableMutator
{
private final TableImpl _table;

private ColumnBuilder _column;
private IndexBuilder _index;
private int _origTdefLen;
private int _addedTdefLen;
private List<Integer> _nextPages = new ArrayList<Integer>(1);
private ColumnState _colState;
private IndexDataState _idxDataState;
private IndexImpl.ForeignKeyReference _fkReference;

public TableUpdater(TableImpl table) {
super(table.getDatabase());
_table = table;
}

public ColumnBuilder getColumn() {
return _column;
}

public IndexBuilder getIndex() {
return _index;
}

@Override
String getTableName() {
return _table.getName();
}
@Override
public int getTdefPageNumber() {
return _table.getTableDefPageNumber();
}

@Override
short getColumnNumber(String colName) {
for(ColumnImpl col : _table.getColumns()) {
if(col.getName().equalsIgnoreCase(colName)) {
return col.getColumnNumber();
}
}
return IndexData.COLUMN_UNUSED;
}

@Override
public ColumnState getColumnState(ColumnBuilder col) {
return ((col == _column) ? _colState : null);
}

@Override
public IndexDataState getIndexDataState(IndexBuilder idx) {
return ((idx == _index) ? _idxDataState : null);
}

void setForeignKey(IndexImpl.ForeignKeyReference fkReference) {
_fkReference = fkReference;
}

@Override
public IndexImpl.ForeignKeyReference getForeignKey(IndexBuilder idx) {
return ((idx == _index) ? _fkReference : null);
}

int getAddedTdefLen() {
return _addedTdefLen;
}

void addTdefLen(int add) {
_addedTdefLen += add;
}

void setOrigTdefLen(int len) {
_origTdefLen = len;
}

List<Integer> getNextPages() {
return _nextPages;
}

void resetTdefInfo() {
_addedTdefLen = 0;
_origTdefLen = 0;
_nextPages.clear();
}

public ColumnImpl addColumn(ColumnBuilder column) throws IOException {

_column = column;

validateAddColumn();
// assign column number and do some assorted column bookkeeping
short columnNumber = (short)_table.getMaxColumnCount();
_column.setColumnNumber(columnNumber);
if(_column.getType().isLongValue()) {
_colState = new ColumnState();
}

getPageChannel().startExclusiveWrite();
try {

return _table.mutateAddColumn(this);

} finally {
getPageChannel().finishWrite();
}
}

public IndexImpl addIndex(IndexBuilder index) throws IOException {
return addIndex(index, false, (byte)0, (byte)0);
}

IndexImpl addIndex(IndexBuilder index, boolean isInternal, byte ignoreIdxFlags,
byte ignoreColFlags)
throws IOException
{
_index = index;

if(!isInternal) {
validateAddIndex();
}

// assign index number and do some assorted index bookkeeping
int indexNumber = _table.getLogicalIndexCount();
_index.setIndexNumber(indexNumber);

// initialize backing index state
initIndexDataState(ignoreIdxFlags, ignoreColFlags);

if(!isInternal) {
getPageChannel().startExclusiveWrite();
} else {
// if "internal" update, this is part of a larger operation which
// already holds an exclusive write lock
getPageChannel().startWrite();
}
try {

if(_idxDataState.getIndexDataNumber() == _table.getIndexCount()) {
// we need a new backing index data
_table.mutateAddIndexData(this);

// we need to modify the table def again when adding the Index, so reset
resetTdefInfo();
}

return _table.mutateAddIndex(this);

} finally {
getPageChannel().finishWrite();
}
}

boolean validateUpdatedTdef(ByteBuffer tableBuffer) {
// sanity check the updates
return((_origTdefLen + _addedTdefLen) == tableBuffer.limit());
}

private void validateAddColumn() {

if(_column == null) {
throw new IllegalArgumentException(withErrorContext(
"Cannot add column with no column"));
}
if((_table.getColumnCount() + 1) > getFormat().MAX_COLUMNS_PER_TABLE) {
throw new IllegalArgumentException(withErrorContext(
"Cannot add column to table with " +
getFormat().MAX_COLUMNS_PER_TABLE + " columns"));
}
Set<String> colNames = getColumnNames();
// next, validate the column definition
validateColumn(colNames, _column);
if(_column.isAutoNumber()) {
// for most autonumber types, we can only have one of each type
Set<DataType> autoTypes = EnumSet.noneOf(DataType.class);
for(ColumnImpl column : _table.getAutoNumberColumns()) {
autoTypes.add(column.getType());
}

validateAutoNumberColumn(autoTypes, _column);
}
}

private void validateAddIndex() {
if(_index == null) {
throw new IllegalArgumentException(withErrorContext(
"Cannot add index with no index"));
}
if((_table.getLogicalIndexCount() + 1) > getFormat().MAX_INDEXES_PER_TABLE) {
throw new IllegalArgumentException(withErrorContext(
"Cannot add index to table with " +
getFormat().MAX_INDEXES_PER_TABLE + " indexes"));
}
boolean foundPk[] = new boolean[1];
Set<String> idxNames = getIndexNames(_table, foundPk);
// next, validate the index definition
validateIndex(getColumnNames(), idxNames, foundPk, _index);
}

private Set<String> getColumnNames() {
Set<String> colNames = new HashSet<String>();
for(ColumnImpl column : _table.getColumns()) {
colNames.add(DatabaseImpl.toLookupName(column.getName()));
}
return colNames;
}

static Set<String> getIndexNames(TableImpl table, boolean[] foundPk) {
Set<String> idxNames = new HashSet<String>();
for(IndexImpl index : table.getIndexes()) {
idxNames.add(DatabaseImpl.toLookupName(index.getName()));
if(index.isPrimaryKey() && (foundPk != null)) {
foundPk[0] = true;
}
}
return idxNames;
}

private void initIndexDataState(byte ignoreIdxFlags, byte ignoreColFlags) {

_idxDataState = new IndexDataState();
_idxDataState.addIndex(_index);
// search for an existing index which matches the given index (in terms of
// the backing data)
IndexData idxData = findIndexData(
_index, _table, ignoreIdxFlags, ignoreColFlags);

int idxDataNumber = ((idxData != null) ?
idxData.getIndexDataNumber() :
_table.getIndexCount());

_idxDataState.setIndexDataNumber(idxDataNumber);
}

static IndexData findIndexData(IndexBuilder idx, TableImpl table,
byte ignoreIdxFlags, byte ignoreColFlags)
{
for(IndexData idxData : table.getIndexDatas()) {
if(sameIndexData(idx, idxData, ignoreIdxFlags, ignoreColFlags)) {
return idxData;
}
}
return null;
}
private static boolean sameIndexData(IndexBuilder idx1, IndexData idx2,
byte ignoreIdxFlags, byte ignoreColFlags) {
// index data can be combined if flags match and columns (and col flags)
// match
if((idx1.getFlags() | ignoreIdxFlags) !=
(idx2.getIndexFlags() | ignoreIdxFlags)) {
return false;
}

if(idx1.getColumns().size() != idx2.getColumns().size()) {
return false;
}
for(int i = 0; i < idx1.getColumns().size(); ++i) {
IndexBuilder.Column col1 = idx1.getColumns().get(i);
IndexData.ColumnDescriptor col2 = idx2.getColumns().get(i);

if(!sameIndexData(col1, col2, ignoreColFlags)) {
return false;
}
}

return true;
}

private static boolean sameIndexData(
IndexBuilder.Column col1, IndexData.ColumnDescriptor col2,
int ignoreColFlags) {
return (col1.getName().equals(col2.getName()) &&
((col1.getFlags() | ignoreColFlags) ==
(col2.getFlags() | ignoreColFlags)));
}

@Override
protected String withErrorContext(String msg) {
String objStr = "";
if(_column != null) {
objStr = ";Column=" + _column.getName();
} else if(_index != null) {
objStr = ";Index=" + _index.getName();
}
return msg + "(Table=" + _table.getName() + objStr + ")";
}
}

+ 4
- 5
src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java View File

@@ -25,7 +25,6 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap;
@@ -63,7 +62,7 @@ public class DatabaseTest extends TestCase
Database db = create(fileFormat);

try {
((DatabaseImpl)db).createTable("test", Collections.<ColumnBuilder>emptyList());
new TableBuilder("test").toTable(db);
fail("created table with no columns?");
} catch(IllegalArgumentException e) {
// success
@@ -598,9 +597,9 @@ public class DatabaseTest extends TestCase
columns.add(new ColumnBuilder(colName, DataType.TEXT).toColumn());
}

((DatabaseImpl)db).createTable("test", columns);
Table t = db.getTable("test");
Table t = new TableBuilder("test")
.addColumns(columns)
.toTable(db);

List<String> row = new ArrayList<String>();
Map<String,Object> expectedRowData = new LinkedHashMap<String, Object>();

+ 51
- 0
src/test/java/com/healthmarketscience/jackcess/IndexTest.java View File

@@ -462,6 +462,57 @@ public class IndexTest extends TestCase {
}
}
public void testIndexCreationSharedData() throws Exception
{
for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
Database db = create(fileFormat);

Table t = new TableBuilder("TestTable")
.addColumn(new ColumnBuilder("id", DataType.LONG))
.addColumn(new ColumnBuilder("data", DataType.TEXT))
.addIndex(new IndexBuilder(IndexBuilder.PRIMARY_KEY_NAME)
.addColumns("id").setPrimaryKey())
.addIndex(new IndexBuilder("Index1").addColumns("id"))
.addIndex(new IndexBuilder("Index2").addColumns("id"))
.addIndex(new IndexBuilder("Index3").addColumns(false, "id"))
.toTable(db);

assertEquals(4, t.getIndexes().size());
IndexImpl idx = (IndexImpl)t.getIndexes().get(0);
assertEquals(IndexBuilder.PRIMARY_KEY_NAME, idx.getName());
assertEquals(1, idx.getColumns().size());
assertEquals("id", idx.getColumns().get(0).getName());
assertTrue(idx.getColumns().get(0).isAscending());
assertTrue(idx.isPrimaryKey());
assertTrue(idx.isUnique());
assertFalse(idx.shouldIgnoreNulls());
assertNull(idx.getReference());

IndexImpl idx1 = (IndexImpl)t.getIndexes().get(1);
IndexImpl idx2 = (IndexImpl)t.getIndexes().get(2);
IndexImpl idx3 = (IndexImpl)t.getIndexes().get(3);

assertNotSame(idx.getIndexData(), idx1.getIndexData());
assertSame(idx1.getIndexData(), idx2.getIndexData());
assertNotSame(idx2.getIndexData(), idx3.getIndexData());

t.addRow(2, "row2");
t.addRow(1, "row1");
t.addRow(3, "row3");

Cursor c = t.newCursor()
.setIndexByName(IndexBuilder.PRIMARY_KEY_NAME).toCursor();

for(int i = 1; i <= 3; ++i) {
Map<String,Object> row = c.getNextRow();
assertEquals(i, row.get("id"));
assertEquals("row" + i, row.get("data"));
}
assertFalse(c.moveToNextRow());
}
}
public void testGetForeignKeyIndex() throws Exception
{
for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.INDEX, true)) {

+ 177
- 0
src/test/java/com/healthmarketscience/jackcess/TableUpdaterTest.java View File

@@ -0,0 +1,177 @@
/*
Copyright (c) 2016 James Ahlborn

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package com.healthmarketscience.jackcess;

import java.util.Arrays;

import com.healthmarketscience.jackcess.Database.FileFormat;
import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
import com.healthmarketscience.jackcess.impl.DatabaseImpl;
import com.healthmarketscience.jackcess.impl.TableImpl;
import junit.framework.TestCase;
import static com.healthmarketscience.jackcess.TestUtil.*;

/**
*
* @author James Ahlborn
*/
public class TableUpdaterTest extends TestCase
{

public TableUpdaterTest(String name) throws Exception {
super(name);
}

public void testTableUpdating() throws Exception {
for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
Database db = create(fileFormat);

doTestUpdating(db, false, true);
db.close();
}
}

public void testTableUpdatingOneToOne() throws Exception {
for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
Database db = create(fileFormat);

doTestUpdating(db, true, true);
// FIXME, add one-to-one, add no enforce rel
db.close();
}
}

public void testTableUpdatingNoEnforce() throws Exception {
for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
Database db = create(fileFormat);

doTestUpdating(db, false, false);
db.close();
}
}

private void doTestUpdating(Database db, boolean oneToOne, boolean enforce)
throws Exception
{
Table t1 = new TableBuilder("TestTable")
.addColumn(new ColumnBuilder("id", DataType.LONG))
.toTable(db);
Table t2 = new TableBuilder("TestTable2")
.addColumn(new ColumnBuilder("id2", DataType.LONG))
.toTable(db);

int t1idxs = 1;
new IndexBuilder(IndexBuilder.PRIMARY_KEY_NAME)
.addColumns("id").setPrimaryKey()
.addToTable(t1);
new ColumnBuilder("data", DataType.TEXT)
.addToTable(t1);
new ColumnBuilder("bigdata", DataType.MEMO)
.addToTable(t1);

new ColumnBuilder("data2", DataType.TEXT)
.addToTable(t2);
new ColumnBuilder("bigdata2", DataType.MEMO)
.addToTable(t2);

int t2idxs = 0;
if(oneToOne) {
++t2idxs;
new IndexBuilder(IndexBuilder.PRIMARY_KEY_NAME)
.addColumns("id2").setPrimaryKey()
.addToTable(t2);
}

RelationshipBuilder rb = new RelationshipBuilder("TestTable", "TestTable2")
.addColumns("id", "id2");
if(enforce) {
++t1idxs;
++t2idxs;
rb.setReferentialIntegrity()
.setCascadeDeletes();
}

Relationship rel = rb.toRelationship(db);

assertEquals("TestTableTestTable2", rel.getName());
assertSame(t1, rel.getFromTable());
assertEquals(Arrays.asList(t1.getColumn("id")), rel.getFromColumns());
assertSame(t2, rel.getToTable());
assertEquals(Arrays.asList(t2.getColumn("id2")), rel.getToColumns());
assertEquals(oneToOne, rel.isOneToOne());
assertEquals(enforce, rel.hasReferentialIntegrity());
assertEquals(enforce, rel.cascadeDeletes());
assertFalse(rel.cascadeUpdates());
assertEquals(Relationship.JoinType.INNER, rel.getJoinType());

assertEquals(t1idxs, t1.getIndexes().size());
assertEquals(1, ((TableImpl)t1).getIndexDatas().size());

assertEquals(t2idxs, t2.getIndexes().size());
assertEquals((t2idxs > 0 ? 1 : 0), ((TableImpl)t2).getIndexDatas().size());

((DatabaseImpl)db).getPageChannel().startWrite();
try {

for(int i = 0; i < 10; ++i) {
t1.addRow(i, "row" + i, "row-data" + i);
}

for(int i = 0; i < 10; ++i) {
t2.addRow(i, "row2_" + i, "row-data2_" + i);
}

} finally {
((DatabaseImpl)db).getPageChannel().finishWrite();
}

try {
t2.addRow(10, "row10", "row-data10");
if(enforce) {
fail("ConstraintViolationException should have been thrown");
}
} catch(ConstraintViolationException cv) {
// success
if(!enforce) { throw cv; }
}

Row r1 = CursorBuilder.findRowByPrimaryKey(t1, 5);
t1.deleteRow(r1);
int id = 0;
for(Row r : t1) {
assertEquals(id, r.get("id"));
++id;
if(id == 5) {
++id;
}
}
id = 0;
for(Row r : t2) {
assertEquals(id, r.get("id2"));
++id;
if(enforce && (id == 5)) {
++id;
}
}
}
}

Loading…
Cancel
Save