git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1031 f203690c-595d-4dc9-a70b-905162fa7fd2tags/jackcess-2.1.5
@@ -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) |
@@ -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() + ")"; | |||
} |
@@ -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; | |||
} | |||
@@ -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() + ")"; | |||
} |
@@ -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(); | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} | |||
/** |
@@ -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 |
@@ -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. | |||
*/ |
@@ -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; | |||
} | |||
} |
@@ -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. |
@@ -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. | |||
*/ |
@@ -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; | |||
} | |||
/** |
@@ -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() { |
@@ -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,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()); | |||
} | |||
} | |||
} |
@@ -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 |
@@ -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()) + ")"; | |||
} | |||
} |
@@ -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); |
@@ -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() + ")"; | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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 + ")"; | |||
} | |||
} |
@@ -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>(); |
@@ -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)) { |
@@ -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; | |||
} | |||
} | |||
} | |||
} |