aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Ahlborn <jtahlborn@yahoo.com>2022-08-26 21:02:28 +0000
committerJames Ahlborn <jtahlborn@yahoo.com>2022-08-26 21:02:28 +0000
commitb5aa0575e557d975a062fb7eb34f1ecb5b6a5a29 (patch)
tree97e9f362b8701f0462ccb2b35fcc37a7550eaa02
parentf1403cdbb47ce8cb1839cd3700ab00c8c8a416bc (diff)
downloadjackcess-b5aa0575e557d975a062fb7eb34f1ecb5b6a5a29.tar.gz
jackcess-b5aa0575e557d975a062fb7eb34f1ecb5b6a5a29.zip
Add support for linked odbc tables. TableMetaData provides access to connection string and local TableDefinition if available. Fixes #45
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1379 f203690c-595d-4dc9-a70b-905162fa7fd2
-rw-r--r--src/changes/changes.xml6
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java8
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java16
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/Table.java16
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/TableDefinition.java125
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/TableMetaData.java25
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java248
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TableDefinitionImpl.java60
-rw-r--r--src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java4
-rw-r--r--src/test/data/V1997/test2V1997.mdbbin122880 -> 137216 bytes
-rw-r--r--src/test/data/V2007/odbcLinkerTestV2007.accdbbin0 -> 610304 bytes
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java126
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/LinkedTableTest.java198
-rw-r--r--src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java1
14 files changed, 627 insertions, 206 deletions
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 6698a2d..5656044 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -6,7 +6,11 @@
<body>
<release version="4.0.2" date="TBD">
<action dev="jahlborn" type="update">
- Add Table methods to access the creation and last modified dates.
+ Add Table methods to get the creation and last modified dates.
+ </action>
+ <action dev="jahlborn" type="update" issue="45">
+ Add support for linked odbc tables. TableMetaData provides access to
+ connection string and local TableDefinition if available.
</action>
</release>
<release version="4.0.1" date="2021-06-21">
diff --git a/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java b/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java
index e063448..9253d06 100644
--- a/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java
+++ b/src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java
@@ -491,6 +491,14 @@ public class ColumnBuilder {
* attributes.
*/
public Column addToTable(Table table) throws IOException {
+ return addToTableDefinition(table);
+ }
+
+ /**
+ * Adds a new Column to the given TableDefinition with the currently
+ * configured attributes.
+ */
+ public Column addToTableDefinition(TableDefinition table) throws IOException {
return new TableUpdater((TableImpl)table).addColumn(this);
}
diff --git a/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java b/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java
index d10a6fb..9ef480d 100644
--- a/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java
+++ b/src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java
@@ -38,7 +38,7 @@ import com.healthmarketscience.jackcess.impl.TableUpdater;
* @see TableBuilder
* @usage _general_class_
*/
-public class IndexBuilder
+public class IndexBuilder
{
/** name typically used by MS Access for the primary key index */
public static final String PRIMARY_KEY_NAME = "PrimaryKey";
@@ -138,7 +138,7 @@ public class IndexBuilder
public IndexBuilder setUnique() {
_flags |= IndexData.UNIQUE_INDEX_FLAG;
return this;
- }
+ }
/**
* Sets this index to encforce required.
@@ -146,7 +146,7 @@ public class IndexBuilder
public IndexBuilder setRequired() {
_flags |= IndexData.REQUIRED_INDEX_FLAG;
return this;
- }
+ }
/**
* Sets this index to ignore null values.
@@ -154,7 +154,7 @@ public class IndexBuilder
public IndexBuilder setIgnoreNulls() {
_flags |= IndexData.IGNORE_NULLS_INDEX_FLAG;
return this;
- }
+ }
/**
* @usage _advanced_method_
@@ -209,6 +209,14 @@ public class IndexBuilder
* attributes.
*/
public Index addToTable(Table table) throws IOException {
+ return addToTableDefinition(table);
+ }
+
+ /**
+ * Adds a new Index to the given TableDefinition with the currently
+ * configured attributes.
+ */
+ public Index addToTableDefinition(TableDefinition table) throws IOException {
return new TableUpdater((TableImpl)table).addIndex(this);
}
diff --git a/src/main/java/com/healthmarketscience/jackcess/Table.java b/src/main/java/com/healthmarketscience/jackcess/Table.java
index 5462e80..539baae 100644
--- a/src/main/java/com/healthmarketscience/jackcess/Table.java
+++ b/src/main/java/com/healthmarketscience/jackcess/Table.java
@@ -44,7 +44,7 @@ import com.healthmarketscience.jackcess.util.OleBlob;
* @author James Ahlborn
* @usage _general_class_
*/
-public interface Table extends Iterable<Row>
+public interface Table extends Iterable<Row>, TableDefinition
{
/**
* enum which controls the ordering of the columns in a table.
@@ -63,28 +63,33 @@ public interface Table extends Iterable<Row>
* @return The name of the table
* @usage _general_method_
*/
+ @Override
public String getName();
/**
* Whether or not this table has been marked as hidden.
* @usage _general_method_
*/
+ @Override
public boolean isHidden();
/**
* Whether or not this table is a system (internal) table.
* @usage _general_method_
*/
+ @Override
public boolean isSystem();
/**
* @usage _general_method_
*/
+ @Override
public int getColumnCount();
/**
* @usage _general_method_
*/
+ @Override
public Database getDatabase();
/**
@@ -120,24 +125,28 @@ public interface Table extends Iterable<Row>
* @return All of the columns in this table (unmodifiable List)
* @usage _general_method_
*/
+ @Override
public List<? extends Column> getColumns();
/**
* @return the column with the given name
* @usage _general_method_
*/
+ @Override
public Column getColumn(String name);
/**
* @return the properties for this table
* @usage _general_method_
*/
+ @Override
public PropertyMap getProperties() throws IOException;
/**
* @return the created date for this table if available
* @usage _general_method_
*/
+ @Override
public LocalDateTime getCreatedDate() throws IOException;
/**
@@ -147,12 +156,14 @@ public interface Table extends Iterable<Row>
* @return the last updated date for this table if available
* @usage _general_method_
*/
+ @Override
public LocalDateTime getUpdatedDate() throws IOException;
/**
* @return All of the Indexes on this table (unmodifiable List)
* @usage _intermediate_method_
*/
+ @Override
public List<? extends Index> getIndexes();
/**
@@ -160,6 +171,7 @@ public interface Table extends Iterable<Row>
* @throws IllegalArgumentException if there is no index with the given name
* @usage _intermediate_method_
*/
+ @Override
public Index getIndex(String name);
/**
@@ -168,6 +180,7 @@ public interface Table extends Iterable<Row>
* table
* @usage _intermediate_method_
*/
+ @Override
public Index getPrimaryKeyIndex();
/**
@@ -176,6 +189,7 @@ public interface Table extends Iterable<Row>
* table and the given table
* @usage _intermediate_method_
*/
+ @Override
public Index getForeignKeyIndex(Table otherTable);
/**
diff --git a/src/main/java/com/healthmarketscience/jackcess/TableDefinition.java b/src/main/java/com/healthmarketscience/jackcess/TableDefinition.java
new file mode 100644
index 0000000..76dff60
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/TableDefinition.java
@@ -0,0 +1,125 @@
+/*
+Copyright (c) 2022 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.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * The definition of a single database table. A TableDefinition instance is
+ * retrieved from a {@link TableMetaData} instance. The TableDefinition
+ * instance only provides access to the table metadata, but no table data.
+ * <p>
+ * A TableDefinition instance is not thread-safe (see {@link Database} for
+ * more thread-safety details).
+ *
+ * @author James Ahlborn
+ * @usage _intermediate_class_
+ */
+public interface TableDefinition
+{
+ /**
+ * @return The name of the table
+ * @usage _general_method_
+ */
+ public String getName();
+
+ /**
+ * Whether or not this table has been marked as hidden.
+ * @usage _general_method_
+ */
+ public boolean isHidden();
+
+ /**
+ * Whether or not this table is a system (internal) table.
+ * @usage _general_method_
+ */
+ public boolean isSystem();
+
+ /**
+ * @usage _general_method_
+ */
+ public int getColumnCount();
+
+ /**
+ * @usage _general_method_
+ */
+ public Database getDatabase();
+
+ /**
+ * @return All of the columns in this table (unmodifiable List)
+ * @usage _general_method_
+ */
+ public List<? extends Column> getColumns();
+
+ /**
+ * @return the column with the given name
+ * @usage _general_method_
+ */
+ public Column getColumn(String name);
+
+ /**
+ * @return the properties for this table
+ * @usage _general_method_
+ */
+ public PropertyMap getProperties() throws IOException;
+
+ /**
+ * @return the created date for this table if available
+ * @usage _general_method_
+ */
+ public LocalDateTime getCreatedDate() throws IOException;
+
+ /**
+ * Note: jackcess <i>does not automatically update the modified date of a
+ * Table</i>.
+ *
+ * @return the last updated date for this table if available
+ * @usage _general_method_
+ */
+ public LocalDateTime getUpdatedDate() throws IOException;
+
+ /**
+ * @return All of the Indexes on this table (unmodifiable List)
+ * @usage _intermediate_method_
+ */
+ public List<? extends Index> getIndexes();
+
+ /**
+ * @return the index with the given name
+ * @throws IllegalArgumentException if there is no index with the given name
+ * @usage _intermediate_method_
+ */
+ public Index getIndex(String name);
+
+ /**
+ * @return the primary key index for this table
+ * @throws IllegalArgumentException if there is no primary key index on this
+ * table
+ * @usage _intermediate_method_
+ */
+ public Index getPrimaryKeyIndex();
+
+ /**
+ * @return the foreign key index joining this table to the given other table
+ * @throws IllegalArgumentException if there is no relationship between this
+ * table and the given table
+ * @usage _intermediate_method_
+ */
+ public Index getForeignKeyIndex(Table otherTable);
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/TableMetaData.java b/src/main/java/com/healthmarketscience/jackcess/TableMetaData.java
index 250932b..991d026 100644
--- a/src/main/java/com/healthmarketscience/jackcess/TableMetaData.java
+++ b/src/main/java/com/healthmarketscience/jackcess/TableMetaData.java
@@ -26,8 +26,17 @@ import java.io.IOException;
* @author James Ahlborn
* @usage _intermediate_class_
*/
-public interface TableMetaData
+public interface TableMetaData
{
+ public enum Type {
+ LOCAL, LINKED, LINKED_ODBC;
+ }
+
+ /**
+ * The type of table
+ */
+ public Type getType();
+
/**
* The name of the table (as it is stored in the database)
*/
@@ -37,7 +46,7 @@ public interface TableMetaData
* {@code true} if this is a linked table, {@code false} otherwise.
*/
public boolean isLinked();
-
+
/**
* {@code true} if this is a system table, {@code false} otherwise.
*/
@@ -56,7 +65,19 @@ public interface TableMetaData
public String getLinkedDbName();
/**
+ * The connection of this the linked database if this is a linked ODBC
+ * table, {@code null} otherwise.
+ */
+ public String getConnectionName();
+
+ /**
* Opens this table from the given Database instance.
*/
public Table open(Database db) throws IOException;
+
+ /**
+ * Gets the local table definition from the given Database instance if
+ * available. Only useful for linked ODBC tables.
+ */
+ public TableDefinition getTableDefinition(Database db) throws IOException;
}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
index 80799ef..7370626 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
@@ -67,6 +67,7 @@ import com.healthmarketscience.jackcess.Row;
import com.healthmarketscience.jackcess.RuntimeIOException;
import com.healthmarketscience.jackcess.Table;
import com.healthmarketscience.jackcess.TableBuilder;
+import com.healthmarketscience.jackcess.TableDefinition;
import com.healthmarketscience.jackcess.TableMetaData;
import com.healthmarketscience.jackcess.expr.EvalConfig;
import com.healthmarketscience.jackcess.impl.query.QueryImpl;
@@ -186,6 +187,8 @@ public class DatabaseImpl implements Database, DateTimeContext
private static final String CAT_COL_DATABASE = "Database";
/** System catalog column name of the remote table name */
private static final String CAT_COL_FOREIGN_NAME = "ForeignName";
+ /** System catalog column name of the remote connection name */
+ private static final String CAT_COL_CONNECT_NAME = "Connect";
/** top-level parentid for a database */
private static final int DB_PARENT_ID = 0xF000000;
@@ -236,6 +239,8 @@ public class DatabaseImpl implements Database, DateTimeContext
private static final String OBJECT_NAME_USERDEF_PROPS = "UserDefined";
/** System object type for table definitions */
static final Short TYPE_TABLE = 1;
+ /** System object type for linked odbc tables */
+ private static final Short TYPE_LINKED_ODBC_TABLE = 4;
/** System object type for query definitions */
private static final Short TYPE_QUERY = 5;
/** System object type for linked table definitions */
@@ -254,7 +259,8 @@ public class DatabaseImpl implements Database, DateTimeContext
private static Collection<String> SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS =
new HashSet<String>(Arrays.asList(CAT_COL_NAME, CAT_COL_TYPE, CAT_COL_ID,
CAT_COL_FLAGS, CAT_COL_PARENT_ID,
- CAT_COL_DATABASE, CAT_COL_FOREIGN_NAME));
+ CAT_COL_DATABASE, CAT_COL_FOREIGN_NAME,
+ CAT_COL_CONNECT_NAME));
/** the columns to read when getting object propertyes */
private static Collection<String> SYSTEM_CATALOG_PROPS_COLUMNS =
new HashSet<String>(Arrays.asList(CAT_COL_ID, CAT_COL_PROPS));
@@ -267,6 +273,9 @@ public class DatabaseImpl implements Database, DateTimeContext
private static final Pattern INVALID_IDENTIFIER_CHARS =
Pattern.compile("[\\p{Cntrl}.!`\\]\\[]");
+ /** regex to match a password in an ODBC string */
+ private static final Pattern ODBC_PWD_PATTERN = Pattern.compile("\\bPWD=[^;]+");
+
/** the File of the database */
private final Path _file;
/** the simple name of the database */
@@ -332,7 +341,7 @@ public class DatabaseImpl implements Database, DateTimeContext
private boolean _evaluateExpressions;
/** factory for ColumnValidators */
private ColumnValidatorFactory _validatorFactory = SimpleColumnValidatorFactory.INSTANCE;
- /** cache of in-use tables */
+ /** cache of in-use tables (or table definitions) */
private final TableCache _tableCache = new TableCache();
/** handler for reading/writing properteies */
private PropertyMaps.Handler _propsHandler;
@@ -670,9 +679,10 @@ public class DatabaseImpl implements Database, DateTimeContext
// common case, local table name == remote table name
TableInfo tableInfo = lookupTable(table.getName());
- if((tableInfo != null) && tableInfo.isLinked() &&
- matchesLinkedTable(table, ((LinkedTableInfo)tableInfo).linkedTableName,
- ((LinkedTableInfo)tableInfo).linkedDbName)) {
+ if((tableInfo != null) &&
+ (tableInfo.getType() == TableMetaData.Type.LINKED) &&
+ matchesLinkedTable(table, tableInfo.getLinkedTableName(),
+ tableInfo.getLinkedDbName())) {
return true;
}
@@ -983,8 +993,8 @@ public class DatabaseImpl implements Database, DateTimeContext
* Read the system catalog
*/
private void readSystemCatalog() throws IOException {
- _systemCatalog = readTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG,
- SYSTEM_OBJECT_FLAGS);
+ _systemCatalog = loadTable(TABLE_SYSTEM_CATALOG, PAGE_SYSTEM_CATALOG,
+ SYSTEM_OBJECT_FLAGS, TYPE_TABLE);
try {
_tableFinder = new DefaultTableFinder(
@@ -1096,24 +1106,7 @@ public class DatabaseImpl implements Database, DateTimeContext
* @usage _advanced_method_
*/
public TableImpl getTable(int tableDefPageNumber) throws IOException {
-
- // first, check for existing table
- TableImpl table = _tableCache.get(tableDefPageNumber);
- if(table != null) {
- return table;
- }
-
- // lookup table info from system catalog
- Row objectRow = _tableFinder.getObjectRow(
- tableDefPageNumber, SYSTEM_CATALOG_COLUMNS);
- if(objectRow == null) {
- return null;
- }
-
- String name = objectRow.getString(CAT_COL_NAME);
- int flags = objectRow.getInt(CAT_COL_FLAGS);
-
- return readTable(name, tableDefPageNumber, flags);
+ return loadTable(null, tableDefPageNumber, 0, null);
}
/**
@@ -1147,14 +1140,14 @@ public class DatabaseImpl implements Database, DateTimeContext
private TableImpl getTable(TableInfo tableInfo, boolean includeSystemTables)
throws IOException
{
- if(tableInfo.isLinked()) {
+ if(tableInfo.getType() == TableMetaData.Type.LINKED) {
if(_linkedDbs == null) {
_linkedDbs = new HashMap<String,Database>();
}
- String linkedDbName = ((LinkedTableInfo)tableInfo).linkedDbName;
- String linkedTableName = ((LinkedTableInfo)tableInfo).linkedTableName;
+ String linkedDbName = tableInfo.getLinkedDbName();
+ String linkedTableName = tableInfo.getLinkedTableName();
Database linkedDb = _linkedDbs.get(linkedDbName);
if(linkedDb == null) {
linkedDb = getLinkResolver().resolveLinkedDatabase(this, linkedDbName);
@@ -1165,8 +1158,8 @@ public class DatabaseImpl implements Database, DateTimeContext
includeSystemTables);
}
- return readTable(tableInfo.tableName, tableInfo.pageNumber,
- tableInfo.flags);
+ return loadTable(tableInfo.tableName, tableInfo.pageNumber,
+ tableInfo.flags, tableInfo.tableType);
}
/**
@@ -1384,9 +1377,7 @@ public class DatabaseImpl implements Database, DateTimeContext
}
}
- private String createRelationshipName(RelationshipCreator creator)
- throws IOException
- {
+ private String createRelationshipName(RelationshipCreator creator) {
// 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)
@@ -1833,7 +1824,7 @@ public class DatabaseImpl implements Database, DateTimeContext
/**
* Reads a table with the given name from the given pageNumber.
*/
- private TableImpl readTable(String name, int pageNumber, int flags)
+ private TableImpl loadTable(String name, int pageNumber, int flags, Short type)
throws IOException
{
// first, check for existing table
@@ -1842,6 +1833,30 @@ public class DatabaseImpl implements Database, DateTimeContext
return table;
}
+ if(name == null) {
+ // lookup table info from system catalog
+ Row objectRow = _tableFinder.getObjectRow(
+ pageNumber, SYSTEM_CATALOG_COLUMNS);
+ if(objectRow == null) {
+ return null;
+ }
+
+ name = objectRow.getString(CAT_COL_NAME);
+ flags = objectRow.getInt(CAT_COL_FLAGS);
+ type = objectRow.getShort(CAT_COL_TYPE);
+ }
+
+ // need to load table from db
+ return _tableCache.put(readTable(name, pageNumber, flags, type));
+ }
+
+ /**
+ * Reads a table with the given name from the given pageNumber.
+ */
+ private TableImpl readTable(
+ String name, int pageNumber, int flags, Short type)
+ throws IOException
+ {
ByteBuffer buffer = takeSharedBuffer();
try {
// need to load table from db
@@ -1852,8 +1867,9 @@ public class DatabaseImpl implements Database, DateTimeContext
"Looking for " + name + " at page " + pageNumber +
", but page type is " + pageType));
}
- return _tableCache.put(
- new TableImpl(this, buffer, pageNumber, name, flags));
+ return (!TYPE_LINKED_ODBC_TABLE.equals(type) ?
+ new TableImpl(this, buffer, pageNumber, name, flags) :
+ new TableDefinitionImpl(this, buffer, pageNumber, name, flags));
} finally {
releaseSharedBuffer(buffer);
}
@@ -1991,23 +2007,39 @@ public class DatabaseImpl implements Database, DateTimeContext
{
_tableLookup.put(toLookupName(tableName),
createTableInfo(tableName, pageNumber, 0, type,
- linkedDbName, linkedTableName));
+ linkedDbName, linkedTableName, null));
// clear this, will be created next time needed
_tableNames = null;
}
+ private static TableInfo createTableInfo(
+ String tableName, Short type, Row row) {
+
+ Integer pageNumber = row.getInt(CAT_COL_ID);
+ int flags = row.getInt(CAT_COL_FLAGS);
+ String linkedDbName = row.getString(CAT_COL_DATABASE);
+ String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME);
+ String connectName = row.getString(CAT_COL_CONNECT_NAME);
+
+ return createTableInfo(tableName, pageNumber, flags, type, linkedDbName,
+ linkedTableName, connectName);
+ }
+
/**
* Creates a TableInfo instance appropriate for the given table data.
*/
private static TableInfo createTableInfo(
String tableName, Integer pageNumber, int flags, Short type,
- String linkedDbName, String linkedTableName)
+ String linkedDbName, String linkedTableName, String connectName)
{
if(TYPE_LINKED_TABLE.equals(type)) {
- return new LinkedTableInfo(pageNumber, tableName, flags, linkedDbName,
- linkedTableName);
+ return new LinkedTableInfo(pageNumber, tableName, flags, type,
+ linkedDbName, linkedTableName);
+ } else if(TYPE_LINKED_ODBC_TABLE.equals(type)) {
+ return new LinkedODBCTableInfo(pageNumber, tableName, flags, type,
+ connectName, linkedTableName);
}
- return new TableInfo(pageNumber, tableName, flags);
+ return new TableInfo(pageNumber, tableName, flags, type);
}
/**
@@ -2224,7 +2256,7 @@ public class DatabaseImpl implements Database, DateTimeContext
}
private static boolean isTableType(Short objType) {
- return(TYPE_TABLE.equals(objType) || TYPE_LINKED_TABLE.equals(objType));
+ return(TYPE_TABLE.equals(objType) || isAnyLinkedTableType(objType));
}
public static FileFormatDetails getFileFormatDetails(FileFormat fileFormat) {
@@ -2268,6 +2300,11 @@ public class DatabaseImpl implements Database, DateTimeContext
return defaultValue;
}
+ private static boolean isAnyLinkedTableType(Short type) {
+ return (TYPE_LINKED_TABLE.equals(type) ||
+ TYPE_LINKED_ODBC_TABLE.equals(type));
+ }
+
/**
* Utility class for storing table page number and actual name.
*/
@@ -2276,11 +2313,19 @@ public class DatabaseImpl implements Database, DateTimeContext
public final Integer pageNumber;
public final String tableName;
public final int flags;
+ public final Short tableType;
- private TableInfo(Integer newPageNumber, String newTableName, int newFlags) {
+ private TableInfo(Integer newPageNumber, String newTableName, int newFlags,
+ Short newTableType) {
pageNumber = newPageNumber;
tableName = newTableName;
flags = newFlags;
+ tableType = newTableType;
+ }
+
+ @Override
+ public Type getType() {
+ return Type.LOCAL;
}
@Override
@@ -2309,11 +2354,21 @@ public class DatabaseImpl implements Database, DateTimeContext
}
@Override
+ public String getConnectionName() {
+ return null;
+ }
+
+ @Override
public Table open(Database db) throws IOException {
return ((DatabaseImpl)db).getTable(this, true);
}
@Override
+ public TableDefinition getTableDefinition(Database db) throws IOException {
+ return null;
+ }
+
+ @Override
public String toString() {
ToStringBuilder sb = CustomToStringStyle.valueBuilder("TableMetaData")
.append("name", getName());
@@ -2323,10 +2378,17 @@ public class DatabaseImpl implements Database, DateTimeContext
if(isLinked()) {
sb.append("isLinked", isLinked())
.append("linkedTableName", getLinkedTableName())
- .append("linkedDbName", getLinkedDbName());
+ .append("linkedDbName", getLinkedDbName())
+ .append("connectionName", maskPassword(getConnectionName()));
}
return sb.toString();
}
+
+ private static String maskPassword(String connectionName) {
+ return ((connectionName != null) ?
+ ODBC_PWD_PATTERN.matcher(connectionName).replaceAll("PWD=XXXXXX") :
+ null);
+ }
}
/**
@@ -2334,15 +2396,21 @@ public class DatabaseImpl implements Database, DateTimeContext
*/
private static class LinkedTableInfo extends TableInfo
{
- private final String linkedDbName;
- private final String linkedTableName;
+ private final String _linkedDbName;
+ private final String _linkedTableName;
private LinkedTableInfo(Integer newPageNumber, String newTableName,
- int newFlags, String newLinkedDbName,
+ int newFlags, Short newTableType,
+ String newLinkedDbName,
String newLinkedTableName) {
- super(newPageNumber, newTableName, newFlags);
- linkedDbName = newLinkedDbName;
- linkedTableName = newLinkedTableName;
+ super(newPageNumber, newTableName, newFlags, newTableType);
+ _linkedDbName = newLinkedDbName;
+ _linkedTableName = newLinkedTableName;
+ }
+
+ @Override
+ public Type getType() {
+ return Type.LINKED;
}
@Override
@@ -2352,12 +2420,62 @@ public class DatabaseImpl implements Database, DateTimeContext
@Override
public String getLinkedTableName() {
- return linkedTableName;
+ return _linkedTableName;
}
@Override
public String getLinkedDbName() {
- return linkedDbName;
+ return _linkedDbName;
+ }
+ }
+
+ /**
+ * Utility class for storing linked ODBC table info
+ */
+ private static class LinkedODBCTableInfo extends TableInfo
+ {
+ private final String _linkedTableName;
+ private final String _connectionName;
+
+ private LinkedODBCTableInfo(Integer newPageNumber, String newTableName,
+ int newFlags, Short newTableType,
+ String connectName,
+ String newLinkedTableName) {
+ super(newPageNumber, newTableName, newFlags, newTableType);
+ _linkedTableName = newLinkedTableName;
+ _connectionName = connectName;
+ }
+
+ @Override
+ public Type getType() {
+ return Type.LINKED_ODBC;
+ }
+
+ @Override
+ public boolean isLinked() {
+ return true;
+ }
+
+ @Override
+ public String getLinkedTableName() {
+ return _linkedTableName;
+ }
+
+ @Override
+ public String getConnectionName() {
+ return _connectionName;
+ }
+
+ @Override
+ public Table open(Database db) throws IOException {
+ return null;
+ }
+
+ @Override
+ public TableDefinition getTableDefinition(Database db) throws IOException {
+ return (((pageNumber != null) && (pageNumber > 0)) ?
+ ((DatabaseImpl)db).getTable(this, true) :
+ null);
}
}
@@ -2448,7 +2566,7 @@ public class DatabaseImpl implements Database, DateTimeContext
} else if(systemTables) {
tableNames.add(tableName);
}
- } else if(TYPE_LINKED_TABLE.equals(type) && linkedTables) {
+ } else if(linkedTables && isAnyLinkedTableType(type)) {
tableNames.add(tableName);
}
}
@@ -2524,13 +2642,8 @@ public class DatabaseImpl implements Database, DateTimeContext
}
String realName = row.getString(CAT_COL_NAME);
- Integer pageNumber = row.getInt(CAT_COL_ID);
- int flags = row.getInt(CAT_COL_FLAGS);
- String linkedDbName = row.getString(CAT_COL_DATABASE);
- String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME);
- return createTableInfo(realName, pageNumber, flags, type, linkedDbName,
- linkedTableName);
+ return createTableInfo(realName, type, row);
}
return null;
@@ -2595,20 +2708,15 @@ public class DatabaseImpl implements Database, DateTimeContext
Row row = _systemCatalogCursor.getCurrentRow(
SYSTEM_CATALOG_TABLE_DETAIL_COLUMNS);
- Integer pageNumber = row.getInt(CAT_COL_ID);
- String realName = row.getString(CAT_COL_NAME);
- int flags = row.getInt(CAT_COL_FLAGS);
Short type = row.getShort(CAT_COL_TYPE);
if(!isTableType(type)) {
return null;
}
- String linkedDbName = row.getString(CAT_COL_DATABASE);
- String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME);
+ String realName = row.getString(CAT_COL_NAME);
- return createTableInfo(realName, pageNumber, flags, type, linkedDbName,
- linkedTableName);
+ return createTableInfo(realName, type, row);
}
@Override
@@ -2686,13 +2794,7 @@ public class DatabaseImpl implements Database, DateTimeContext
continue;
}
- Integer pageNumber = row.getInt(CAT_COL_ID);
- int flags = row.getInt(CAT_COL_FLAGS);
- String linkedDbName = row.getString(CAT_COL_DATABASE);
- String linkedTableName = row.getString(CAT_COL_FOREIGN_NAME);
-
- return createTableInfo(realName, pageNumber, flags, type, linkedDbName,
- linkedTableName);
+ return createTableInfo(realName, type, row);
}
return null;
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableDefinitionImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableDefinitionImpl.java
new file mode 100644
index 0000000..6c918bd
--- /dev/null
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableDefinitionImpl.java
@@ -0,0 +1,60 @@
+/*
+Copyright (c) 2022 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.List;
+
+/**
+ * A database table definition which does not allow any actual data
+ * interaction (read or write).
+ * <p>
+ * Note, in an ideal world, TableImpl would extend TableDefinitionImpl.
+ * However, since TableDefinitionImpl came later, it was easier to do it this
+ * way and avoid a lot of unnecessary code shuffling.
+ * <p>
+ * Is not thread-safe.
+ *
+ * @author James Ahlborn
+ * @usage _advanced_class_
+ */
+public class TableDefinitionImpl extends TableImpl
+{
+ protected TableDefinitionImpl(DatabaseImpl database, ByteBuffer tableBuffer,
+ int pageNumber, String name, int flags)
+ throws IOException {
+ super(database, tableBuffer, pageNumber, name, flags);
+ }
+
+ @Override
+ protected List<? extends Object[]> addRows(List<? extends Object[]> rows,
+ final boolean isBatchWrite)
+ throws IOException {
+ // all row additions eventually flow through this method
+ throw new UnsupportedOperationException(
+ withErrorContext("TableDefinition has no data access"));
+ }
+
+ @Override
+ public RowState createRowState() {
+ // RowState is needed for all traversal operations, so this kills any data
+ // reading as well as update/delete methods
+ throw new UnsupportedOperationException(
+ withErrorContext("TableDefinition has no data access"));
+ }
+}
diff --git a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
index d05f640..c01c1a5 100644
--- a/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
+++ b/src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
@@ -2231,8 +2231,8 @@ public class TableImpl implements Table, PropertyMaps.Owner
* rows have been written, and every time a data page is filled.
* @param rows List of Object[] row values
*/
- private List<? extends Object[]> addRows(List<? extends Object[]> rows,
- final boolean isBatchWrite)
+ protected List<? extends Object[]> addRows(List<? extends Object[]> rows,
+ final boolean isBatchWrite)
throws IOException
{
if(rows.isEmpty()) {
diff --git a/src/test/data/V1997/test2V1997.mdb b/src/test/data/V1997/test2V1997.mdb
index b8b2ca2..8a6c47a 100644
--- a/src/test/data/V1997/test2V1997.mdb
+++ b/src/test/data/V1997/test2V1997.mdb
Binary files differ
diff --git a/src/test/data/V2007/odbcLinkerTestV2007.accdb b/src/test/data/V2007/odbcLinkerTestV2007.accdb
new file mode 100644
index 0000000..b610534
--- /dev/null
+++ b/src/test/data/V2007/odbcLinkerTestV2007.accdb
Binary files differ
diff --git a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java
index 5f5a488..3b90545 100644
--- a/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java
+++ b/src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java
@@ -38,17 +38,16 @@ import java.util.UUID;
import java.util.stream.Collectors;
import static com.healthmarketscience.jackcess.Database.*;
+import static com.healthmarketscience.jackcess.DatabaseBuilder.*;
+import static com.healthmarketscience.jackcess.TestUtil.*;
import com.healthmarketscience.jackcess.impl.ColumnImpl;
import com.healthmarketscience.jackcess.impl.DatabaseImpl;
import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
import com.healthmarketscience.jackcess.impl.RowIdImpl;
import com.healthmarketscience.jackcess.impl.RowImpl;
import com.healthmarketscience.jackcess.impl.TableImpl;
-import com.healthmarketscience.jackcess.util.LinkResolver;
import com.healthmarketscience.jackcess.util.RowFilterTest;
import junit.framework.TestCase;
-import static com.healthmarketscience.jackcess.TestUtil.*;
-import static com.healthmarketscience.jackcess.DatabaseBuilder.*;
/**
@@ -865,123 +864,7 @@ public class DatabaseTest extends TestCase
}
}
- public void testLinkedTables() throws Exception {
- for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.LINKED)) {
- Database db = openCopy(testDB);
-
- try {
- db.getTable("Table2");
- fail("FileNotFoundException should have been thrown");
- } catch(FileNotFoundException e) {
- // success
- }
-
- TableMetaData tmd = db.getTableMetaData("Table2");
- assertEquals("Table2", tmd.getName());
- assertTrue(tmd.isLinked());
- assertFalse(tmd.isSystem());
- assertEquals("Table1", tmd.getLinkedTableName());
- assertEquals("Z:\\jackcess_test\\linkeeTest.accdb", tmd.getLinkedDbName());
-
- tmd = db.getTableMetaData("FooTable");
- assertNull(tmd);
-
- assertTrue(db.getLinkedDatabases().isEmpty());
-
- final String linkeeDbName = "Z:\\jackcess_test\\linkeeTest.accdb";
- final File linkeeFile = new File("src/test/data/linkeeTest.accdb");
- db.setLinkResolver(new LinkResolver() {
- public Database resolveLinkedDatabase(Database linkerdb, String dbName)
- throws IOException {
- assertEquals(linkeeDbName, dbName);
- return DatabaseBuilder.open(linkeeFile);
- }
- });
-
- Table t2 = db.getTable("Table2");
-
- assertEquals(1, db.getLinkedDatabases().size());
- Database linkeeDb = db.getLinkedDatabases().get(linkeeDbName);
- assertNotNull(linkeeDb);
- assertEquals(linkeeFile, linkeeDb.getFile());
- assertEquals("linkeeTest.accdb", ((DatabaseImpl)linkeeDb).getName());
-
- List<? extends Map<String, Object>> expectedRows =
- createExpectedTable(
- createExpectedRow(
- "ID", 1,
- "Field1", "bar"));
-
- assertTable(expectedRows, t2);
-
- db.createLinkedTable("FooTable", linkeeDbName, "Table2");
-
- tmd = db.getTableMetaData("FooTable");
- assertEquals("FooTable", tmd.getName());
- assertTrue(tmd.isLinked());
- assertFalse(tmd.isSystem());
- assertEquals("Table2", tmd.getLinkedTableName());
- assertEquals("Z:\\jackcess_test\\linkeeTest.accdb", tmd.getLinkedDbName());
-
- Table t3 = db.getTable("FooTable");
-
- assertEquals(1, db.getLinkedDatabases().size());
-
- expectedRows =
- createExpectedTable(
- createExpectedRow(
- "ID", 1,
- "Field1", "buzz"));
-
- assertTable(expectedRows, t3);
-
- tmd = db.getTableMetaData("Table1");
- assertEquals("Table1", tmd.getName());
- assertFalse(tmd.isLinked());
- assertFalse(tmd.isSystem());
- assertNull(tmd.getLinkedTableName());
- assertNull(tmd.getLinkedDbName());
-
- Table t1 = tmd.open(db);
-
- assertFalse(db.isLinkedTable(null));
- assertTrue(db.isLinkedTable(t2));
- assertTrue(db.isLinkedTable(t3));
- assertFalse(db.isLinkedTable(t1));
-
- List<Table> tables = getTables(db.newIterable());
- assertEquals(3, tables.size());
- assertTrue(tables.contains(t1));
- assertTrue(tables.contains(t2));
- assertTrue(tables.contains(t3));
- assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog()));
-
- tables = getTables(db.newIterable().setIncludeNormalTables(false));
- assertEquals(2, tables.size());
- assertFalse(tables.contains(t1));
- assertTrue(tables.contains(t2));
- assertTrue(tables.contains(t3));
- assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog()));
-
- tables = getTables(db.newIterable().withLocalUserTablesOnly());
- assertEquals(1, tables.size());
- assertTrue(tables.contains(t1));
- assertFalse(tables.contains(t2));
- assertFalse(tables.contains(t3));
- assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog()));
-
- tables = getTables(db.newIterable().withSystemTablesOnly());
- assertTrue(tables.size() > 5);
- assertFalse(tables.contains(t1));
- assertFalse(tables.contains(t2));
- assertFalse(tables.contains(t3));
- assertTrue(tables.contains(((DatabaseImpl)db).getSystemCatalog()));
-
- db.close();
- }
- }
-
- private static List<Table> getTables(Iterable<Table> tableIter)
+ static List<Table> getTables(Iterable<Table> tableIter)
{
List<Table> tableList = new ArrayList<Table>();
for(Table t : tableIter) {
@@ -1102,9 +985,6 @@ public class DatabaseTest extends TestCase
expectedCreateDate = "2004-05-28T17:51:48.701";
expectedUpdateDate = "2006-07-24T09:56:19.701";
}
- System.out.println("FOO " + testDB.getExpectedFileFormat() + " " +
- table.getCreatedDate() + " " +
- table.getUpdatedDate());
assertEquals(expectedCreateDate, table.getCreatedDate().toString());
assertEquals(expectedUpdateDate, table.getUpdatedDate().toString());
}
diff --git a/src/test/java/com/healthmarketscience/jackcess/LinkedTableTest.java b/src/test/java/com/healthmarketscience/jackcess/LinkedTableTest.java
new file mode 100644
index 0000000..5763285
--- /dev/null
+++ b/src/test/java/com/healthmarketscience/jackcess/LinkedTableTest.java
@@ -0,0 +1,198 @@
+/*
+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.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static com.healthmarketscience.jackcess.TestUtil.*;
+import com.healthmarketscience.jackcess.impl.DatabaseImpl;
+import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
+import com.healthmarketscience.jackcess.util.LinkResolver;
+import junit.framework.TestCase;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class LinkedTableTest extends TestCase
+{
+
+ public void testLinkedTables() throws Exception {
+ for (final TestDB testDB : TestDB.getSupportedForBasename(Basename.LINKED)) {
+ Database db = openCopy(testDB);
+
+ try {
+ db.getTable("Table2");
+ fail("FileNotFoundException should have been thrown");
+ } catch(FileNotFoundException e) {
+ // success
+ }
+
+ TableMetaData tmd = db.getTableMetaData("Table2");
+ assertEquals("Table2", tmd.getName());
+ assertTrue(tmd.isLinked());
+ assertFalse(tmd.isSystem());
+ assertEquals("Table1", tmd.getLinkedTableName());
+ assertNull(tmd.getConnectionName());
+ assertEquals(TableMetaData.Type.LINKED, tmd.getType());
+ assertEquals("Z:\\jackcess_test\\linkeeTest.accdb", tmd.getLinkedDbName());
+ assertNull(tmd.getTableDefinition(db));
+
+ tmd = db.getTableMetaData("FooTable");
+ assertNull(tmd);
+
+ assertTrue(db.getLinkedDatabases().isEmpty());
+
+ final String linkeeDbName = "Z:\\jackcess_test\\linkeeTest.accdb";
+ final File linkeeFile = new File("src/test/data/linkeeTest.accdb");
+ db.setLinkResolver(new LinkResolver() {
+ @Override
+ public Database resolveLinkedDatabase(Database linkerdb, String dbName)
+ throws IOException {
+ assertEquals(linkeeDbName, dbName);
+ return DatabaseBuilder.open(linkeeFile);
+ }
+ });
+
+ Table t2 = db.getTable("Table2");
+
+ assertEquals(1, db.getLinkedDatabases().size());
+ Database linkeeDb = db.getLinkedDatabases().get(linkeeDbName);
+ assertNotNull(linkeeDb);
+ assertEquals(linkeeFile, linkeeDb.getFile());
+ assertEquals("linkeeTest.accdb", ((DatabaseImpl)linkeeDb).getName());
+
+ List<? extends Map<String, Object>> expectedRows =
+ createExpectedTable(
+ createExpectedRow(
+ "ID", 1,
+ "Field1", "bar"));
+
+ assertTable(expectedRows, t2);
+
+ db.createLinkedTable("FooTable", linkeeDbName, "Table2");
+
+ tmd = db.getTableMetaData("FooTable");
+ assertEquals("FooTable", tmd.getName());
+ assertTrue(tmd.isLinked());
+ assertFalse(tmd.isSystem());
+ assertEquals("Table2", tmd.getLinkedTableName());
+ assertEquals("Z:\\jackcess_test\\linkeeTest.accdb", tmd.getLinkedDbName());
+
+ Table t3 = db.getTable("FooTable");
+
+ assertEquals(1, db.getLinkedDatabases().size());
+
+ expectedRows =
+ createExpectedTable(
+ createExpectedRow(
+ "ID", 1,
+ "Field1", "buzz"));
+
+ assertTable(expectedRows, t3);
+
+ tmd = db.getTableMetaData("Table1");
+ assertEquals("Table1", tmd.getName());
+ assertFalse(tmd.isLinked());
+ assertFalse(tmd.isSystem());
+ assertNull(tmd.getLinkedTableName());
+ assertNull(tmd.getLinkedDbName());
+
+ Table t1 = tmd.open(db);
+
+ assertFalse(db.isLinkedTable(null));
+ assertTrue(db.isLinkedTable(t2));
+ assertTrue(db.isLinkedTable(t3));
+ assertFalse(db.isLinkedTable(t1));
+
+ List<Table> tables = DatabaseTest.getTables(db.newIterable());
+ assertEquals(3, tables.size());
+ assertTrue(tables.contains(t1));
+ assertTrue(tables.contains(t2));
+ assertTrue(tables.contains(t3));
+ assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog()));
+
+ tables = DatabaseTest.getTables(db.newIterable().setIncludeNormalTables(false));
+ assertEquals(2, tables.size());
+ assertFalse(tables.contains(t1));
+ assertTrue(tables.contains(t2));
+ assertTrue(tables.contains(t3));
+ assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog()));
+
+ tables = DatabaseTest.getTables(db.newIterable().withLocalUserTablesOnly());
+ assertEquals(1, tables.size());
+ assertTrue(tables.contains(t1));
+ assertFalse(tables.contains(t2));
+ assertFalse(tables.contains(t3));
+ assertFalse(tables.contains(((DatabaseImpl)db).getSystemCatalog()));
+
+ tables = DatabaseTest.getTables(db.newIterable().withSystemTablesOnly());
+ assertTrue(tables.size() > 5);
+ assertFalse(tables.contains(t1));
+ assertFalse(tables.contains(t2));
+ assertFalse(tables.contains(t3));
+ assertTrue(tables.contains(((DatabaseImpl)db).getSystemCatalog()));
+
+ db.close();
+ }
+ }
+
+ public void testOdbcLinkedTables() throws Exception {
+ for (final TestDB testDB :
+ TestDB.getSupportedForBasename(Basename.LINKED_ODBC)) {
+ Database db = openCopy(testDB);
+
+ TableMetaData tmd = db.getTableMetaData("Ordrar");
+ assertEquals(TableMetaData.Type.LINKED_ODBC, tmd.getType());
+ assertEquals("dbo.Ordrar", tmd.getLinkedTableName());
+ assertNull(tmd.getLinkedDbName());
+ assertEquals("DSN=Magnapinna;Description=Safexit;UID=safexit;PWD=DummyPassword;APP=Microsoft Office;DATABASE=safexit", tmd.getConnectionName());
+ assertFalse(tmd.toString().contains("DummyPassword"));
+
+ TableDefinition t = tmd.getTableDefinition(db);
+
+ List<? extends Column> cols = t.getColumns();
+ assertEquals(20, cols.size());
+
+ List<? extends Index> idxs = t.getIndexes();
+ assertEquals(5, idxs.size());
+
+ Table tbl = db.getTable("Ordrar");
+
+ try {
+ tbl.iterator();
+ fail("UnsupportedOperationException should have been thrown");
+ } catch(UnsupportedOperationException expected) {
+ // expected
+ }
+
+ try {
+ tbl.addRow(1L,"bar");
+ fail("UnsupportedOperationException should have been thrown");
+ } catch(UnsupportedOperationException expected) {
+ // expected
+ }
+
+ db.close();
+ }
+ }
+
+}
diff --git a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java
index 6f01bca..0f6c889 100644
--- a/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java
+++ b/src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java
@@ -53,6 +53,7 @@ public class JetFormatTest extends TestCase {
COMPLEX("complexDataTest"),
UNSUPPORTED("unsupportedFieldsTest"),
LINKED("linkerTest"),
+ LINKED_ODBC("odbcLinkerTest"),
BLOB("testOle"),
CALC_FIELD("calcFieldTest"),
BINARY_INDEX("binIdxTest"),