]> source.dussan.org Git - jackcess.git/commitdiff
Add support for linked odbc tables. TableMetaData provides access to connection...
authorJames Ahlborn <jtahlborn@yahoo.com>
Fri, 26 Aug 2022 21:02:28 +0000 (21:02 +0000)
committerJames Ahlborn <jtahlborn@yahoo.com>
Fri, 26 Aug 2022 21:02:28 +0000 (21:02 +0000)
git-svn-id: https://svn.code.sf.net/p/jackcess/code/jackcess/trunk@1379 f203690c-595d-4dc9-a70b-905162fa7fd2

14 files changed:
src/changes/changes.xml
src/main/java/com/healthmarketscience/jackcess/ColumnBuilder.java
src/main/java/com/healthmarketscience/jackcess/IndexBuilder.java
src/main/java/com/healthmarketscience/jackcess/Table.java
src/main/java/com/healthmarketscience/jackcess/TableDefinition.java [new file with mode: 0644]
src/main/java/com/healthmarketscience/jackcess/TableMetaData.java
src/main/java/com/healthmarketscience/jackcess/impl/DatabaseImpl.java
src/main/java/com/healthmarketscience/jackcess/impl/TableDefinitionImpl.java [new file with mode: 0644]
src/main/java/com/healthmarketscience/jackcess/impl/TableImpl.java
src/test/data/V1997/test2V1997.mdb
src/test/data/V2007/odbcLinkerTestV2007.accdb [new file with mode: 0644]
src/test/java/com/healthmarketscience/jackcess/DatabaseTest.java
src/test/java/com/healthmarketscience/jackcess/LinkedTableTest.java [new file with mode: 0644]
src/test/java/com/healthmarketscience/jackcess/impl/JetFormatTest.java

index 6698a2dd3f0b2175c4ee2f02873850067c941de9..5656044a25d79e5e6a82c39e360775e662969d15 100644 (file)
@@ -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">
index e063448081321a5a9d2ec51c34340caa92c9a38f..9253d0677cc5b9c47fa438cf9f1aaa53afdbb956 100644 (file)
@@ -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);
   }
 
index d10a6fb1fdbd5668c92c9c0e8cb6d5566c2aba2c..9ef480d1d4794e294f0cab5a4abbc03335e49b00 100644 (file)
@@ -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);
   }
 
index 5462e8089b26a522f9eac215a617d78a08970d25..539baae9f0e8dec67ff55d08b83ddb3c1de24162 100644 (file)
@@ -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 (file)
index 0000000..76dff60
--- /dev/null
@@ -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);
+}
index 250932b78c54964de698c423b42e01aab09b0fea..991d0261292dcd320f1c0e6f7651c0de9c1d6b17 100644 (file)
@@ -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.
    */
@@ -55,8 +64,20 @@ 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;
 }
index 80799ef2780712f38444052336756ff6d6d77ebe..7370626e472636e8262065143e5378181f21eeb6 100644 (file)
@@ -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
@@ -2308,11 +2353,21 @@ public class DatabaseImpl implements Database, DateTimeContext
       return null;
     }
 
+    @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")
@@ -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 (file)
index 0000000..6c918bd
--- /dev/null
@@ -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"));
+  }
+}
index d05f640fd6fc078cc5d27ac598f6fcb3253a8acb..c01c1a520eb5149f63b63d4e19ca546928b7efd7 100644 (file)
@@ -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()) {
index b8b2ca204c4ec9773348e960f9d79bc2d8f467bf..8a6c47a8f3a4ccf7f0f43c769ab2d5f08454d1f6 100644 (file)
Binary files a/src/test/data/V1997/test2V1997.mdb and b/src/test/data/V1997/test2V1997.mdb differ
diff --git a/src/test/data/V2007/odbcLinkerTestV2007.accdb b/src/test/data/V2007/odbcLinkerTestV2007.accdb
new file mode 100644 (file)
index 0000000..b610534
Binary files /dev/null and b/src/test/data/V2007/odbcLinkerTestV2007.accdb differ
index 5f5a4886560bffd57dfcf7e90147b03b488a1631..3b905459d06c1198588467e21fd35cb818be993c 100644 (file)
@@ -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 (file)
index 0000000..5763285
--- /dev/null
@@ -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();
+    }
+  }
+
+}
index 6f01bcafec63299c6ad51a3cb61c5473c46ab43d..0f6c88918cb25692f65af8f6fe555e6f30475453 100644 (file)
@@ -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"),