Thursday, November 17. 2011
State of the Company – Now Closed
I am sorry to inform you that Digital Engine Software is no more. Priorities change, new opportunities arise, and we have decided to take advantage of them. Peter landed an amazing job working for Riot Games in Southern California, Mike is pursuing his Masters in Economics at the University of Vermont, and I am currently finishing the remaining obligations for Digital Engine Software with exciting opportunities on the horizon.
It has been a privilege working with Mike and Peter, and with all of our clients and business partners. It has also been a great enjoyment, both during work and engaging with all of you outside of work, particularly at our LAN events. I wish Mike and Peter the best of luck in their pursuits and I look forward to seeing you all wherever our paths may cross.
I will continue to be reachable through all of the usual methods for the foreseeable future to answer any questions and address any issues (both Digital Engine-related and otherwise). It has been a great run and I expect a lot more greatness from everyone involved in the years to come. Farewell Digital Engine Software.
Friday, November 11. 2011
DOM Event Model Compatibility (or Why fireEvent Doesn't Trigger addEventListener)
I've recently been updating an intranet web application that I wrote to fix compatibility issues with Internet Explorer 9 and came across some interesting behavior. IE9 introduces support for DOM Level 2 Events (and to some extent Level 3), a feature that many of us have been hoping to see in IE for a long time. Hurrah! Unfortunately, the addition of this feature is not as smooth as we might like. Presumably due to the incompatibilities between the Microsoft Event Model and the DOM Level 2 Event Model, Microsoft has chosen to implement the DOM model separately from their existing event model. This has some serious implications for web developers, and particularly for JavaScript framework developers.
So, what is the impact of this decision for web developers? IE9 and later versions, in Standards Mode, now support 3 event models: DOM Level 2 Events, the Microsoft Event Model, and the so-called DOM Level 0 or legacy
event model. But to what extent are these models compatible? It turns out that both DOM2+ and MS events will be received by DOM0 handlers, but that DOM2+ events are not received by MS handlers or vice versa.
An Example
To illustrate the problem, consider the following example fragment:
element.addEventListener('click', function () { alert('DOM Level 2 Event'); }, false);
element.onclick = function () { alert('DOM Level 0 Event'); };
Now consider what happens if the user clicks on the element. What happens if element.fireEvent('onclick')
is called? What about element.dispatchEvent(clickevent)
? If the user clicks the element, each of the three alert boxes appears in the order it was attached, exactly what you would expect. If element.fireEvent('onclick')
is called, the DOM0 alert box appears first followed by the MS alert box and the DOM2+ alert never appears, regardless of the order in which they were registered. If element.dispatchEvent(clickevent)
is called, the DOM0 alert box and the DOM2+ alert boxes appear in the order they were attached and the MS alert box never appears.
The Takeaway Message
Ok, that's interesting, what are web developers supposed to do about it? The answer is simple, never mix event models. Yet, this simple advice is not so easily taken for several reasons. First, in order to support IE8 and its predecessors, code must be written for the MS event model. In this simple case, all that is required is to ensure that feature tests are performed with the same precedence order everywhere. If addEventListener
is present, always use both addEventListener
and dispatchEvent
, if not use attachEvent
and fireEvent
. The real problem with sticking to a single event model appears when we consider using JavaScript frameworks and other library code and the decision of which event model to use, and even knowledge of which will be used, is not available to the developer.
JavaScript libraries must choose a single event model in which to trigger events, since triggering DOM0 listeners twice is almost always unacceptable. Keeping in mind that if one library uses the DOM2+ event model and the other uses the MS model they won't receive each other's events, I strongly urge all libraries to use addEventListener
in preference to attachEvent
, both because it is standardized and because attachEvent
throws errors on pages served as XHTML. Unfortunately, this is not standard practice for many Microsoft and IE-centric libraries (e.g. the ASP.NET AJAX Control Toolkit, which spurred my original inquiry) and without a de facto standard for event model preference, it will be difficult to convince such projects to change and risk breaking user code (often IE-only) which uses attachEvent
.
Further Testing
For the curious, I have coded up a test page to test event model compatibility on different browsers and different compatibility levels. The Event Model Compatibility Test Page tests each of the native event creation and handling functions as well as the event functions of several popular JavaScript frameworks. I hope you find it useful, or at least a bit interesting.
An Update
For anyone in the unenviable position of needing to use/interoperate with frameworks that send events in both the Microsoft Event Model and the DOM Level 2 event model, my suggestion is to register event listeners using the legacy DOM Level 0 event model on Internet Explorer (note that the Dojo Toolkit appears to be the only framework which does this). This is the only event model which receives all events and by using it (rather than using both the DOM2+ and MS event models) events are only received once. I typically use code similar to the following:
element.addEventListener(evtType, listener);
} else {
addLegacyEventListener(element, evtType, listener);
}
In the above snippet, addLegacyEventListener
can be any function that hooks up the listener to the DOM Level 0 event model. It can be as simple as element['on' + evtType] = listener
, if there is ever at most one listener, or it can chain listeners in whatever style is preferred. Here is an example of an addLegacyEventListener function that I commonly use.
Saturday, October 22. 2011
Problems with NULL Large Object Data Types and 64-bit Replication
I recently encountered an interesting problem where constraint checks on VARBINARY(MAX)
columns were failing during synchronization for merge-replicated clients. After a lot of investigation, I have found that when a 64-bit SQL Server 2008 R2 subscriber synchronizes a table which contains large object data types on an article published with @stream_blob_columns = N'true'
any constraint checks which depend on the values being NULL will fail. Note that 32-bit clients work properly and that without the constraint checks the final value would be NULL, but apparently during synchronization it is in a temporary state of being non-NULL and fails before reaching that point.
Although the release notes in KB2463333 do not mention a fix for this problem, it appears that Service Pack 1 for SQL Server 2008 R2 at least contains a workaround. When testing with SP1, the insert fails due to the constraint violation but the retry succeeds. However, even with the workaround, performance problems related to the retry are a legitimate concern. I would suggest considering disabling @stream_blob_columns
on the affected articles for anyone encountering this problem.
Sunday, September 25. 2011
Problems using SpamPD as before-queue filter
I have recently configured a Postfix mail server to use SpamAssassin as a before-content filter using SpamPD as described on the SpamAssassin Wiki. I have been quite impressed with how well it is working, with one exception. A few email messages have been getting rejected with the following error message:
After a bit of investigation I found that the problem is due to a few mail servers that include the AUTH parameter to the MAIL FROM command, even when not authenticated. However, this server is configured to allow authentication only over TLS/SSL. The problem is that the AUTH parameter is passed through SpamPD to the receiving instance of postfix, which rejects the MAIL FROM command because the connection from SpamPD is not using TLS/SSL. To solve the problem, add "-o smtpd_tls_auth_only=no" to the postfix receiving entry in master.cf from the example as follows:
Friday, September 9. 2011
Problems with XDMCP/VNC and GDM on a Headless Server
I was recently assisting a colleague to setup VNC on a headless server for remote graphical administration. I had followed one of the many VNC+GDM tutorials on the Internet and everything was working great. Then, after a reboot, it stopped. Here's what happened:
Symptoms
The server is running Debian Squeeze and is configured similarly to the settings in this HOWTO on the Ubuntu Forums. After working quite well for several months, the server was rebooted and it ceased to work at all. Whether connecting using VNC or directly over XDMCP I was presented with the generic black X cursor over a black/white/grey patterned screen and the GDM greeter was nowhere in sight.
After much poking and prodding and technical assistance I determined the key symptom. Everything works well when the server is booted with a monitor connected. When started without a monitor, X would fail to start with the following error:
(EE) open /dev/fb0: No such file or directory (EE) intel(0): No modes. (EE) Screen(s) found, but none have a usable configuration. Fatal server error: no screens found
Although this shouldn't be a problem for the VNC session (which doesn't require an X display on the video hardware), it was.
Solution
Fixing this problem simply required disabling the GDM server instance running on the display hardware. With this server disabled in the GDM configuration, everything works smoothly regardless of whether or not a monitor is connected. On Debian, the default server can be disabled by editing /usr/share/gdm/defaults.conf
and commenting out the following line (around line 604 in my version):
0=Standard device=/dev/console
Bug Report?
I'm not knowledgeable enough about how GDM and XDMCP are supposed to work to know if this is a bug or expected behavior. Any feedback on this point would be greatly appreciated.
Wednesday, July 13. 2011
String Concatenation with XML and Entity Encoding Problems
The Problem
Unfortunately, SQL Server does not provide a built-in string concatenation aggregate operator, a la GROUP_CONCAT
from MySQL. Because of this deficiency, many workarounds, hacks, and custom code has been written to accomplish this goal. An excellent survey of some of the available methods was compiled by Anith Sen in his post Concatenating Row Values in Transact-SQL. For anyone using "The blackbox XML methods" to concatenate strings, there is at least one more important caveat to be aware of: XML Entity Encoding. Consider the following example:
INSERT INTO @Strings VALUES ('x < y when: ');
INSERT INTO @Strings VALUES ('x = 1 &&');
INSERT INTO @Strings VALUES ('y = 2');
SELECT Text + ' '
FROM @Strings
FOR XML PATH('')
This results in the text x < y when: x = 1 && y = 2
. But why? The answer is that when an xml value is cast to a string (as it is implicitly in the above query, because the TYPE directive in the FOR XML statement is omitted) it is entity-encoded for interpretation by an XML parser. Without entity encoding, the string would not parse into equivalent XML, if it was even valid XML at all.
A Solution
To avoid this problem, we can query the value of the resulting XML expression in a way that avoids entity encoding. This can be done using the value() method of the xml type as follows:
INSERT INTO @Strings VALUES ('x < y when: ');
INSERT INTO @Strings VALUES ('x = 1 &&');
INSERT INTO @Strings VALUES ('y = 2');
SELECT (SELECT Text + ' '
FROM @Strings
FOR XML PATH(''), TYPE).VALUE('.', 'NVARCHAR(4000)')
Note the addition of the TYPE directive to the FOR XML statement, it is required. Also note that NVARCHAR(4000)
can be replaced with NVARCHAR(MAX)
or any other SQL data type, as appropriate. Since the value method does not escape its output, the text is returned unchanged.
For the curious, I have attempted to benchmark the differences between these two methods and encountered inconsistent and conflicting results. In most scenarios using the value method was slightly slower than the alternative, but the difference seems to be overwhelmed by other minor differences in test configuration. As always, your mileage may vary.
Wednesday, June 8. 2011
IPv6 Not Forwarding Through Linux Box
To join the fun of World IPv6 Day, I decided to setup IPv6 connectivity to the Digital Engine office. This was accomplished using Hurricane Electric's awesome (free) tunnelbroker.net service. All went smoothly, except that the office gateway, running Debian Squeeze (Linux), would not forward IPv6 traffic from computers on the network. Traffic originating at the gateway worked fine, traffic originating on the local network was silently discarded. For others facing a similar problem, here are some things to check:
- Check that IPv6 connectivity is working from the gateway computer (
ping6 ipv6.google.com
or similar). - Check that network computers receive appropriate IPv6 addresses (that radvd or dhcp6 are working properly) and that they can connect/ping to the gateway over IPv6.
- Check that forwarding is enabled for IPv6 on all (or at least both local and public) interfaces in the kernel (
sysctl net.ipv6.conf.all.forwarding
). This can be set permanently in /etc/sysctl.conf. - Check that the gateway computer has an explicit default route. This is important! If an IPv6 gateway address is not specified, traffic will not be forwarded. (Make sure there is a gateway statement in /etc/network/interfaces and/or that
route -6
lists a global default route with a gateway address.) - Check that there are no firewall rules blocking traffic and that the default FORWARD policy is ACCEPT (
ip6tables -L
). - Check that the internal network address of the gateway has a global IPv6 address assigned. A link-local address is not sufficient and Linux will not forward traffic from interfaces with only link-local addresses!
For the curious, it was the last point which was causing my problem. As a reference, here is a snippet from /etc/network/interfaces which I used to configure the tunnel:
iface eth1 inet static address 192.168.z.z netmask 255.255.255.0 iface eth1 inet6 static address 2001:470:y:xxx::1 netmask 64 auto he-ipv6 iface he-ipv6 inet6 v4tunnel endpoint 216.218.226.238 local z.z.z.z address 2001:470:x:xxx::2 netmask 64 gateway 2001:470:x:xxx::1
Friday, May 6. 2011
Corrupted MSmerge_contents90_forall.bcp in Dynamic Snapshot
Executive Summary
A problem with dynamic snapshot generation may cause some BCP files to contain stale data at the end of the file. Re-creation of the dynamic snapshot does not solve the problem. Removing the old dynamic snapshot before re-creating it may solve the problem temporarily.
Background
I am using Merge Replication with SQL Server 2008 R2 (server and clients) and recently encountered the following error message during synchronization on one of the subscribers:
This message only appeared on one of the subscribers. It was completely reproducible and occurred every time the subscriber attempted to synchronize. Reinitializing the subscriber and recreating the snapshot had no effect. The error message would also appear on a test machine set to use the same partition (via setting the HOST_NAME property to match the subscriber with the errors).
Note that this problem appears to be the same as the one reported in Microsoft Connect #646157, that was closed as a duplicate of #646156... which appears to be inaccessible. How very frustrating!
Investigation Technical Details
One of the initial steps that I used to try to isolate the problem was to copy MSmerge_contents90_forall.bcp (which is listed in the bcp invocation in the error message) to a test machine and attempt to load it. This can be done using the SQL BULK INSERT statement, or using the bcp utility. I tried the following SQL:
FROM 'C:\File\Path\MSmerge_contents90_forall.bcp'
WITH (DATAFILETYPE = 'widenative')
Which produced the following output:
It confirms that the file is invalid and we have a row number that may be close to where the error occurs in the file, but not enough to isolate the problem yet. So, next a try with bcp. Unfortunately, the bcp command in the synchronization error message is not applicable to replication configurations using snapshots in native format (the default). Instead, I used the following command:
A description of the meaning of these command options can be found on the bcp Utility page on MSDN. The command produced the following output:
At least some of the rows were successfully loaded. The rows saved to errfile.dat, which could not be loaded, do not appear to be sane (e.g. negative values in the tablenick column), suggesting some sort of data corruption. But again, no real indication of what is happening.
At this point I was lost. I looked at SQL Profiler traces during snapshot creation and poked around in the data without success. I decided to write a bcp file parser to determine the exact source and nature of the corruption. What I found was 11 bytes which were invalid:
001c820: ffff ff10 48a2 984a 33cb c046 a44e 1d9d ....H..J3..F.N.. 001c830: b826 5368 ffff ff84 c1b3 d97a 56cd e5ff .&Sh.......zV... 001c840: ffff d088 0b05 109b 6e58 9e16 c611 e0ba ........nX...... 001c850: 8800 145e 281b 87ed 0c00 0000 0000 0008 ...^(........... 001c860: 700c 0000 0000 0000 0b00 ece6 e53e de7e p............>.~ 001c870: 0400 0000 ffff ff10 7482 32a1 50b6 4d4a ........t.2.P.MJ
If these bytes were removed, the file would parse completely without error. Now we are closer to a real cause, and perhaps a solution.
Next I recreated the dynamic snapshot for the partition while running Process Monitor (a tool I highly recommend) and looked for accesses to MSmerge_contents90_forall.bcp, particularly at the offset where the corruption begins. What I found is that data is written up to the byte before this offset, but not at this byte or after it (by any process). Looking back further in the log revealed that the file was opened with disposition OpenIf, rather than CreateIf, meaning that it was not truncated. Also, there is no call to SetEndOfFileInformationFile/SetAllocationInformationFile made to truncate the file or resize it after writing. Eureka! We've found the problem!
If the size of MSmerge_contents90_forall.bcp (or any other bcp file) shrinks between the previous snapshot and the current snapshot, stale data will be left at the end of the file and errors will occur (unless it happens to end on a row boundary, in which case the stale data will be loaded, potentially causing future errors). The workaround was simple: Delete the folder for the dynamic partition (or individual bcp files with errors) and recreate the snapshot.
Best of luck in solving your problems.
Sunday, March 20. 2011
Problems with UT2004 and Windows Firewall
At our recent LAN Party, I ran into a problem playing Unreal Tournament 2004 (old, I know...) on Windows 7. After clicking "Join Game" or "Host Game" I am presented with the following error message:
It has been determined that the Windows Firewall is enabled and that UT2004 is not yet authorized to connect to the internet. Authorization is required in order to use the online components of the game. Please refer to the README.TXT for more information.
There are a few different solutions that I came across. The first is to change permissions on a firewall policy list registry key, which doesn't appear to exist in Windows 7 (although it may solve the problem, if that is where UT2004 wants to write...). The other, which does work for me, is to edit the UT2004 configuration to ignore the Windows firewall. The complete solution was to edit ut2004.ini (stored in the System directory inside the UT2004 installation directory) and add the following lines:
IgnoreSP2=1
Note that in many configurations on Vista/7, ut2004.ini is saved/loaded from underneath %LOCALAPPDATA%\VirtualStore (usually C:\Users\username\AppData\Local\VirtualStore) due to UAC File Virtualization. Edit ut2004.ini in the VirtualStore as it is loaded preferentially.
Tuesday, February 22. 2011
Troubleshooting OpenSwan with NETKEY
While attempting to setup OpenSwan on OpenWRT, I encountered the following message in my system log (heavily redacted):
date machinename authpriv.warn pluto[pid]: "connname" #4: ERROR: netlink response for Add SA esp.uid@xxx.xxx.xxx.xxx included errno 2: No such file or directory 003 "connname" #2: ERROR: netlink response for Add SA esp.uid@xxx.xxx.xxx.xxx included errno 2: No such file or directory
After a lot of digging, I found that the in-kernel netlink code is returning -ENOENT
in response to the ADD_SA
request. This is being propagated from crypto_larval_wait
at crypto/api.c:171 as a result of the following call chain:
xfrm_add_sa in net/xfrm/xfrm_user.c xfrm_state_construct in net/xfrm/xfrm_user.c xfrm_init_state in net/xfrm/xfrm_state.c esp_init_state in net/ipv4/esp4.c esp_init_authenc in net/ipv4/esp4.c crypto_alloc_aead in crypto/aead.c crypto_lookup_aead in crypto/aead.c crypto_alg_mod_lookup in crypto/api.c crypto_larval_wait in crypto/api.c
This is due to the kernel failing to load a requested crypto module (obviously...). To figure out which modules are failing to load, do the following (taken from Documentation/debugging-modules.txt in the Linux kernel source tree):
echo '#! /bin/sh' > /tmp/modprobe
echo 'echo "$@" >> /tmp/modprobe.log' >> /tmp/modprobe
echo 'exec /sbin/modprobe "$@"' >> /tmp/modprobe
chmod a+x /tmp/modprobe
echo /tmp/modprobe > /proc/sys/kernel/modprobe
Then try the connection (or do whatever provokes the error message) and read /tmp/modprobe.log to determine which modules failed to load.
I'm hoping to get a few patches into the OpenWRT source tree to allow selecting the necessary modules (hopefully with a meta-option for all typical IPSec modules). But until then, and on non-OpenWRT systems, the above process should work to figure out which modules are failing to load. Best of luck!
Update: This same problem can also manifest with the following message:
003 "connname" #2: ERROR: netlink response for Add SA esp.uid@xxx.xxx.xxx.xxx included errno 89: Function not implemented
Sunday, February 6. 2011
Paradox and Microsoft Access 2007 on Windows 7
First, my sympathies to anyone who still has to deal with Paradox databases. I recently setup Microsoft Access 2007 on a Windows 7 64-bit computer to manipulate data in an old Boreland Paradox database (from a limited user account). I consider this to be the computer equivalent of putting the engine from an old VW Beetle into a new Porche 914 (rather than the other way around)... but nonetheless, it has been done. The only serious problem was the following error message, which appeared upon opening the database from Access:
The cause was a series of permissions errors caused by the more strict access controls in Windows Vista/7. To fix the error perform the following steps:
- Grant write permissions for the user/group who will be running Access on
HKEY_LOCAL_MACHINE\SOFTWARE[\Wow6432Node]\Borland\Database Engine\Settings
in the Windows Registry. - Change the BDE configuration to avoid writing to C:\PDOXUSRS.NET
- Run "C:\Program Files (x86)\Common Files\Borland Shared\Bde\BDEADMIN.exe" as an Administrator
- Navigate to Configuration | Drivers | Native | Paradox
- Change the NET DIR parameter to somewhere writable (and shared between users). I created a folder in %systemdrive%\ProgramData.
- If necessary, grant write permissions on
HKEY_CLASSES_ROOT\.html
in the Widnows Registry. I don't believe this was strictly required, but at one point I had enabled it during testing and am not completely certain if it was ruled out as a cause of the problem.
After performing the above steps, the error should be resolved. If not, I suggest using Process Monitor to look for permissions errors and attempt to resolve them (which is what I did). Best of luck.
Additional Information: Also, for those looking to the future, Access 2010 and later have dropped support Paradox 3-7, so further workarounds may be required.
Very Slow Data Repartitioning in SQL Server Replication with Precomputed Partitions
Background
I am using Merge Replication with SQL Server 2008 R2 (server and clients). I have setup a publication with a rather complex filtering hierarchy rooted in a table with Parameterized Row Filters extended many times through Join Filters. Making modifications to data in a table near the root of this hierarchy would trigger repartitioning of the replicated data which would never complete and would cause deadlock errors in the other connections to the database while it ran (I let it run overnight once in single user mode, but had to kill it after 13 hours...).
Investigation Technical Details
After a lot of watching in SQL Profiler and digging around in the triggers/procedures which perform the repartitioning I found the culprit. The replication DML trigger on the table (MSMerge_op_GUID
) called the repartitioning procedure (MSmerge_expand_sp_GUID
) which included several queries with the following subquery:
FROM dbo.MSmerge_current_partition_mappings cpm WITH (ROWLOCK)
INNER JOIN dbo.MSmerge_contents mc2 WITH (ROWLOCK) ON cpm.rowguid = mc2.rowguid AND mc2.marker = @marker
Looking at the execution plan for any of the queries showed that this subquery was responsible for at least 40% of the total query cost. Both of these tables are quite large (~800k and ~425k rows respectively in my DB) and neither had indexes to cover this (sub-)query.
Solution
So, of course, I added the following indexes (with naming conventions to match the existing indexes):
ON dbo.MSmerge_current_partition_mappings (rowguid, partition_id);
CREATE INDEX nc6MSmerge_contents
ON dbo.MSmerge_contents (marker, rowguid);
After adding these indexes, the repartitioning operations completed in under 20 minutes!
Caveat
Both of these tables are heavily used and often modified (depending on the workload), so adding more indexes may not be the best solution for databases with high-performance requirements where repartitioning is rare and non-repartitioning operations are the norm. If that is the case, I suggest creating the above indexes before major repartitioning operations and removing them once the repartition is complete. However, that being said, I have been able to leave these indexes on the tables with no noticeable performance impact and a significant reduction in execution time for data modifications which involve repartitioning data.
Report Menu Disappeared from Visual Studio 2005
Recently, the "Report Menu" stopped appearing in the Main Menu in Visual Studio 2005 after focusing on the report designer surface (in an ASP.NET project). This is a rather significant problem for me, since I do not know of another way to modify report parameters and data sources....
I did a bit of digging through the activity log (as described in Troubleshooting Extensions with the Activity Log on the Visual Studio Blog, but found that the Microsoft Report Designer Package was loading without error. The real breakthrough came after watching devenv.exe in Process Monitor and watching it load the Visual Studio 2008 versions of Microsoft.ReportDesignerUI.dll and several others....
My guess is that the cause of the problem was installing Business Intelligence Development Studio with the "Advanced Services" skew of SQL Server 2008 R2 Express, which uses Visual Studio 2008 and a version of the Microsoft Report Designer Package designed for that version of VS. However, I have not confirmed this absolutely, because I need the newer version for another project. So, instead, I will bite the bullet and upgrade all of my Report Viewer 2005 reports to Report Viewer 2008. At least then I can edit them in Visual Studio 2010 (did I mention Report Designer 2008 - in VS 2010 - won't edit/save 2005 files?).
In case this sounds like a familiar gripe, I had similar problems with incompatible library versions in Microsoft Access.
Friday, February 4. 2011
1and1 Blocking Port 25
After a bit of frustration at Joomla! for discarding important error information, and phpMailer for not providing a method of retaining that error information, I have discovered that my client's web host, 1and1, blocks outbound connections on port 25 to all hosts except smtp.1and1.com. This wouldn't be a significant problem if I hadn't setup SPF on their domain.... Looks like it is time to open up an alternate port on the mail server....
Update: 1and1 is blocking ports 465 and 587 in addition to port 25. Great....
Tuesday, November 23. 2010
Indexes on Linked Tables/Views in Access
Access has 2 distinct notions of an index on a linked table/view. The first is a "true" index, which resides in the linked database and is not used/modified directly by Jet/Access. These "true" indexes can be used by the database engine for executing query plans and are enforced during data modification (e.g. for uniqueness). The second is a "pseudo-index" which is "a dynamic cross-reference" that Jet/Access creates/maintains to facilitate data modification on linked tables/views which lack a unique index in the linked database. Constraints in pseudo-indexes can not be enforced outside of Access (and I am unsure if they are even enforced within Access) and are not used during query execution on the linked database (of course, although I have no knowledge of how they are used within Access...).
How to add indexes to linked tables/views
Create the indexes in the linked database (e.g. SQL Server) and refresh the table/view link (using Linked Table Manager or programmatically with TableDef.RefreshLink).
How to add pseudo-indexes to linked tables/views
When a link to a table/view without an index is created, Access prompts the user with a "Select Unique Record Identifier" dialog box (which is also triggered from code using DoCmd.TransferDatabase
to create the link). Simply select the fields to be included in the index. Note that when creating the table using DAO/ADOX methods, this dialog is not presented and the pseudo-index is not created. Also, attempting to add an index to a linked table using DAO/ADOX results in an error (similarly to attempting to add an index through the table designer). To create the pseudo-index programmatically use Database.Execute
to run a CREATE INDEX
statement on the linked table/view. Jet will decide that the index must be a pseudo-index and create one.
Why pseudo-indexes?
There are several situations where an index in the linked database is not desirable. Particularly on updateable views where the underlying table(s) are updated frequently. Creating an indexed view has the side effect of materializing the table on disk, which requires every update to the underlying tables to involve multiple writes to disk as both the original table and the indexed view are updated. Yet, without a pseudo-index, the view would not be updateable from Access. So in these situations a pseudo-index is required.
Caveats
Unfortunately, pseudo-indexes are not preserved when the linked table connection is updated. So for any scripts which modify the connection (e.g. to re-point a table to a different server) must recreate the indexes after refreshing the link. The following code is a general outline for what is required (note that index direction is not currently preserved). The code should be run on the index before the connection is updated, then the returned SQL run after the connection is changed and the link is refreshed:
function indexToSQL(index, tableName) {
var e, query;
query = new Array();
query.push("CREATE ");
if (index.Unique) {
query.push("UNIQUE ");
}
query.push("INDEX ");
query.push(index.Name);
query.push(" ON ");
query.push(tableName);
query.push(" (");
for (e = new Enumerator(index.Fields); !e.atEnd(); e.moveNext()) {
query.push(e.item().Name);
query.push(", ");
}
query.pop();
query.push(") ");
if (index.IgnoreNulls || index.Primary || index.Required) {
query.push("WITH ");
if (index.IgnoreNulls) {
query.push("IGNORE NULL ");
}
if (index.Primary) {
query.push("PRIMARY ");
}
if (index.Required) {
query.push("DISALLOW NULL ");
}
}
return query.join("");
}