<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Konstantin Semenenkov</title>
    <description>The latest articles on DEV Community by Konstantin Semenenkov (@konstantin_semenenkov).</description>
    <link>https://dev.to/konstantin_semenenkov</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1579859%2Fda5c1db2-2173-4bea-96b7-f884ea868a2c.jpg</url>
      <title>DEV Community: Konstantin Semenenkov</title>
      <link>https://dev.to/konstantin_semenenkov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/konstantin_semenenkov"/>
    <language>en</language>
    <item>
      <title>Using Mapping Tables to Merge Data with Auto-Number Keys Referenced by Other Tables</title>
      <dc:creator>Konstantin Semenenkov</dc:creator>
      <pubDate>Thu, 05 Sep 2024 16:26:50 +0000</pubDate>
      <link>https://dev.to/konstantin_semenenkov/using-mapping-tables-to-merge-data-with-auto-number-keys-referenced-by-other-tables-25p9</link>
      <guid>https://dev.to/konstantin_semenenkov/using-mapping-tables-to-merge-data-with-auto-number-keys-referenced-by-other-tables-25p9</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The problem and solution can be demonstrated with the following picture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0deqov31v7q4sfn77hd.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff0deqov31v7q4sfn77hd.jpg" alt="Fig. 1 Expected result" width="800" height="405"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig. 1 Expected result&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We have two instances of databases with similar structures that were not designed to be combined. The tables use auto-number primary keys, which are also used as foreign keys — a common model that works fine until the data needs to be merged.&lt;/p&gt;

&lt;p&gt;Now, we want to combine all this data into a target database. The goal is to retain both the original target parents with their children, plus the source parents with their children. Since we use an auto-number primary key, each source row in the target DB will get new IDs (e.g., 3 and 4), and these new values must be used for corresponding child rows.&lt;/p&gt;

&lt;p&gt;We’ll discuss a solution based on source-ID-to-target-ID mapping tables. This approach was recently implemented in the ID-Remap Merge feature of &lt;a href="https://ksdbmerge.tools/" rel="noopener noreferrer"&gt;KS DB Merge Tools&lt;/a&gt;. We’ll also briefly cover alternative solutions. The implementation details will focus on several popular relational DBMSs — Oracle, MySQL, SQL Server, PostgreSQL, and SQLite — but the approach can generally be applied to any relational DBMS.&lt;/p&gt;
&lt;h2&gt;
  
  
  Other Solutions
&lt;/h2&gt;

&lt;p&gt;It’s clear that auto-number primary and foreign keys are not suitable for such operations. A more natural solution would be to use GUIDs, which are unique across all databases and can be copied between databases as-is, preserving referential integrity and avoiding conflicts with existing data:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4druon9wcx274famuj5.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk4druon9wcx274famuj5.jpg" alt="Fig. 2 Using GUIDs" width="800" height="676"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig. 2 Using GUIDs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;However, this solution is not suitable for our task because we can’t change the database design. It may be a legacy database, or there may be other reasons why we can’t modify the structure of the target database.&lt;/p&gt;

&lt;p&gt;Another approach requiring a redesign would be to change the key structure by adding a &lt;code&gt;DatabaseID&lt;/code&gt; value and changing keys to be composite keys consisting of that &lt;code&gt;DatabaseID&lt;/code&gt; and an auto-number:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fntc4mevsmz4qo9h4gkfs.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fntc4mevsmz4qo9h4gkfs.jpg" alt="Fig. 3 Composite keys" width="800" height="572"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig. 3 Composite keys&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We also don’t consider some tricky ways, like to import source records with negative IDs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0pgsxjzrl84y2ysr007.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk0pgsxjzrl84y2ysr007.jpg" alt="Fig. 4 Negative IDs" width="800" height="411"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig. 4 Negative IDs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Or shifting ID values to some number space not used by target IDs:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcym2xklmqm71t68c5hts.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcym2xklmqm71t68c5hts.jpg" alt="Fig. 5 Shifting IDs" width="800" height="411"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig. 5 Shifting IDs&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These methods can work under certain circumstances, but they are generally less applicable. Negative IDs can be used only once, and shifting IDs creates gaps in ID values, reducing the range of available IDs.&lt;/p&gt;
&lt;h2&gt;
  
  
  Our Solution
&lt;/h2&gt;

&lt;p&gt;When we insert source parent rows into the target database, we need to keep a mapping of the original parent ID from the source DB to its new value in the target DB. Then, we use this mapping to assign the correct parent ID values for child rows.&lt;/p&gt;

&lt;p&gt;The examples below are provided for SQL Server, followed by clarifications for other DBMSs.&lt;/p&gt;

&lt;p&gt;First, let’s create a mapping table in the target DB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE dbo.MapParent (
  SourceID int primary key,
  TargetID int not null);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we loop through source Parent rows and generate &lt;code&gt;INSERT&lt;/code&gt; statements for the target DB, keeping new parent ID values in the mapping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSERT INTO dbo.Parent (Name)
OUTPUT
  1, -- source ParentID
  INSERTED.ParentID
INTO dbo.MapParent
VALUES ('Source parent #1');
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result of this statement in the target DB will be a new Parent table row with new auto-number ID and new row in the dbo.MapParent table:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8hkcsaic3lugc72m1yck.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8hkcsaic3lugc72m1yck.jpg" alt="Fig. 6 MapParent table" width="232" height="60"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Fig. 6 MapParent table&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;OUTPUT&lt;/code&gt; statement to extract the inserted ID value is specific to SQL Server. For other DBMSs, we can use the following methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Oracle:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DECLARE target_id NUMBER(19);
BEGIN
  INSERT INTO "Parent" ("Name")
  VALUES ('Source parent #1')
  RETURNING "ParentID" INTO target_id;

  INSERT INTO "MapParent" VALUES (1, target_id);

  COMMIT;
END;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;MySQL / MariaDB:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSERT INTO `Parent` (`Name`)
  VALUES ('Source parent #1');

INSERT INTO `MapParent`
  VALUES (2, LAST_INSERT_ID());

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;PostgreSQL:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSERT INTO "Parent" ("Name")
 VALUES ('Source parent #1');

INSERT INTO "MapParent"
 VALUES (2, lastval());

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;SQLite:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSERT INTO "Parent" ("Name")
  VALUES ('Source parent #1');
INSERT INTO "MapParent"
  VALUES (2, last_insert_rowid());
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Going Further
&lt;/h2&gt;

&lt;p&gt;Since we use auto-numbers for primary keys in our database, our &lt;code&gt;Child&lt;/code&gt; primary key is most likely also an auto-number. But what if our &lt;code&gt;Child&lt;/code&gt; primary key is used as a foreign key for another table, say &lt;code&gt;GrandChild&lt;/code&gt;? We need to use the same approach: create a &lt;code&gt;MapChild&lt;/code&gt; table with &lt;code&gt;SourceID&lt;/code&gt; and &lt;code&gt;TargetID&lt;/code&gt; columns to keep the mapping for child rows and populate it during &lt;code&gt;INSERT INTO Child&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In real-world applications, we may have complex objects at the application level that are represented in the database as many rows in many related tables linked by foreign keys. In our Parent-Child-GrandChild example, we must ensure that we merge a parent row with all its children, and all sub-children of each child, and so on. We can obtain this information from the database metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQL Server: &lt;code&gt;sys.foreign_keys&lt;/code&gt; and &lt;code&gt;sys.foreign_key_columns&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Oracle: &lt;code&gt;SYS.ALL_CONSTRAINTS&lt;/code&gt; and &lt;code&gt;SYS.ALL_CONS_COLUMNS&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;MySQL/MariaDB: &lt;code&gt;INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS&lt;/code&gt; and &lt;code&gt;INFORMATION_SCHEMA.KEY_COLUMN_USAGE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;PostgreSQL: &lt;code&gt;INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS&lt;/code&gt; and &lt;code&gt;INFORMATION_SCHEMA.KEY_COLUMN_USAGE&lt;/code&gt; views or underlying &lt;code&gt;pg_constraint&lt;/code&gt; and other system catalogs&lt;/li&gt;
&lt;li&gt;SQLite: &lt;code&gt;PRAGMA foreign_key_list(table-name)&lt;/code&gt; can be used to get parent relations; child relations can be retrieved based on parent relations for all tables, and all tables can be taken from the &lt;code&gt;sqlite_master&lt;/code&gt; table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  General Algorithm for Merging a Parent with All Levels of Children
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Set the “current” table to the source parent that we want to merge with all children. Later, the current table will be replaced with child tables, but the steps for the current table will not change; this is a recursive algorithm.&lt;/li&gt;
&lt;li&gt;Create a mapping table for the current table.&lt;/li&gt;
&lt;li&gt;Read the required rows from the source DB for the current table. If the current table is a parent table, “required” means the rows needed for the business goal (e.g., importing a specific parent or parents matching certain criteria). Otherwise, if the current table is not a parent table, we deal with its children and need to process only those children related to our parent. This requires temporary storage of a list of IDs for each processed table. For example, if &lt;code&gt;ParentID&lt;/code&gt; 2 is referenced by &lt;code&gt;ChildIDs&lt;/code&gt; 3 and 4, then when processing the &lt;code&gt;GrandChild&lt;/code&gt; table, we only need its rows with &lt;code&gt;ChildID&lt;/code&gt; 3 and 4.&lt;/li&gt;
&lt;li&gt;For each row, generate &lt;code&gt;INSERT&lt;/code&gt; statements, populating the target table and mapping table from step 2. If the current table is one of the child tables, then foreign key values referencing previously processed parents must be replaced with a &lt;code&gt;SELECT TargetID FROM Mapping&lt;/code&gt; subquery.&lt;/li&gt;
&lt;li&gt;Find all direct child tables (those with foreign keys to the current table).&lt;/li&gt;
&lt;li&gt;For each table from step 5, change the current table to that child and repeat all steps starting from step 2.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a result, we will generate a SQL script that creates mapping tables and inserts all required source data from the parent and all its children. Executing such a script will give us the expected result shown in Fig. 1. If necessary, we can add &lt;code&gt;DROP TABLE&lt;/code&gt; statements for the mapping tables because they are no longer needed for &lt;code&gt;INSERT&lt;/code&gt;s. However, these tables can be useful for further troubleshooting, they can be used to identify rows merged from the source database. They can also be used to divide a complex merge into parts, for example, merging reference data first, verifying the merge result, and then proceeding with the rest of the data using the mapping tables created for the reference data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Circular References&lt;/strong&gt;: Unfortunately, foreign key dependencies may form cycles. For example, our schema could evolve as follows (pseudo-SQL without data types, to demonstrate relations between tables and the historical order of schema changes):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;CREATE TABLE Parent (ParentID PRIMARY KEY, Name);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CREATE TABLE Child (ChildID PRIMARY KEY, ParentID NOT NULL REFERENCES Parent(ParentID), Name);&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ALTER TABLE Parent ADD COLUMN FavoriteNephewChildID NULL REFERENCES Child(ChildID);&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The word “Nephew” in the column name is intentional to emphasize that this ID should belong to the child of another parent. Our algorithm can’t be used here because when we process the &lt;code&gt;Parent&lt;/code&gt; table, the &lt;code&gt;Child&lt;/code&gt; mapping does not exist yet. And that's not just a mistake in the algorithm. Logically, it may happen that we need to merge a parent referencing a child that is not yet merged, and vice versa. The solution is to split the &lt;code&gt;Parent&lt;/code&gt; table merge into two steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, merge &lt;code&gt;Parent&lt;/code&gt; rows without &lt;code&gt;FavoriteNephewChildID&lt;/code&gt; (set it to &lt;code&gt;NULL&lt;/code&gt;) and generate a mapping for &lt;code&gt;Parent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Next, merge &lt;code&gt;Child&lt;/code&gt; rows using the &lt;code&gt;Parent&lt;/code&gt; mapping and generate a mapping for &lt;code&gt;Child&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Finally, update &lt;code&gt;Parent.FavoriteNephewChildID&lt;/code&gt; using the &lt;code&gt;Parent&lt;/code&gt; and &lt;code&gt;Child&lt;/code&gt; mappings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this example, we have two tables referencing each other, but in general, there could be longer cycles, such as &lt;code&gt;T2&lt;/code&gt; referencing &lt;code&gt;T1&lt;/code&gt;, &lt;code&gt;T3&lt;/code&gt; referencing &lt;code&gt;T2&lt;/code&gt;, &lt;code&gt;T4&lt;/code&gt;referencing &lt;code&gt;T3&lt;/code&gt;, and finally &lt;code&gt;T1&lt;/code&gt; referencing &lt;code&gt;T4&lt;/code&gt;. The solution will be the same: merge one of the nullable foreign key columns as &lt;code&gt;NULL&lt;/code&gt; first and then update it in an additional step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other Considerations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Triggers&lt;/strong&gt;: Processed tables in the target database may have &lt;code&gt;INSERT&lt;/code&gt; triggers that change related data in other tables. We need to decide whether this is desired. If not (e.g., we want to import these related tables separately), additional actions will be required, depending on the DBMS, which may or may not support trigger disabling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calculated Columns&lt;/strong&gt;: Calculated columns need to be excluded from &lt;code&gt;INSERT&lt;/code&gt; statements generated in step 4 of our algorithm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changed Columns&lt;/strong&gt;: It may happen that the source and target databases use slightly different schemas (e.g., different versions of applications), and the processed target tables are missing columns present in the source. In this case, we need to generate &lt;code&gt;INSERT&lt;/code&gt;s only for common columns. Of course, if the source table is missing a &lt;code&gt;NOT NULL&lt;/code&gt; column present in the target, the entire merge operation may not be possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Combining databases with auto-number primary keys presents a number of challenges. By using the recursive algorithm discussed in this article, you can confidently merge parent-child relationships and handle more complex data hierarchies without losing integrity or causing conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/3824639/how-do-you-merge-tables-with-autonumber-primary-keys" rel="noopener noreferrer"&gt;How do you merge tables with autonumber primary keys?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/playlist?list=PLC6q4oJciOHPVGDJuC_JuEEASGtXB-nff" rel="noopener noreferrer"&gt;How to Merge Tables With Autonumber Primary Keys Referenced by Foreign Keys&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Implementing UI Automation Testing for Desktop Applications Dealing With Different DBMS</title>
      <dc:creator>Konstantin Semenenkov</dc:creator>
      <pubDate>Wed, 05 Jun 2024 20:22:39 +0000</pubDate>
      <link>https://dev.to/konstantin_semenenkov/implementing-ui-automation-testing-for-desktop-applications-dealing-with-different-dbms-1fkg</link>
      <guid>https://dev.to/konstantin_semenenkov/implementing-ui-automation-testing-for-desktop-applications-dealing-with-different-dbms-1fkg</guid>
      <description>&lt;h2&gt;
  
  
  This article can be useful for:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Those who participate in UI automation testing of desktop apps. Perhaps someone will be interested in the real experience of building a testing system.&lt;/li&gt;
&lt;li&gt;Someone who is making software that needs to use different relational database management systems (DBMSs).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A brief history of the topic
&lt;/h2&gt;

&lt;p&gt;Just a few words about the &lt;a href="https://ksdbmerge.tools/"&gt;project&lt;/a&gt; discussed here. This section does not contain any technical details, so it can be skipped if you're in a hurry. The only technical information in this section is the list of DBMSs related to our discussion.&lt;/p&gt;

&lt;p&gt;The project is a number of diff and merge tools for popular relational DBMSs. Initially created for internal use in Access development, it was primarily used to compare module and form code between different versions of the same Access project. Later, a fork was made for SQL Server, using the same UI to compare schema, data, and programming stuff. Further forks were developed for MySQL, SQLite, and PostgreSQL. With tools for working with metadata from different DBMSs, a tool was created for Cross-DBMS scenarios, focusing mainly on data. Having created a tool for Cross-DBMS, I realized that it sorely lacks Oracle support. Thus, for Cross-DBMS a kernel for working with Oracle was implemented, along with a separate tool for Oracle Database.&lt;/p&gt;

&lt;p&gt;All tools are made using Visual Studio Community Edition and utilize the .NET Framework and WPF.&lt;/p&gt;

&lt;h2&gt;
  
  
  About the tests
&lt;/h2&gt;

&lt;p&gt;After one of the disastrous releases, where changes in one area unexpectedly broke another, it became clear that UI automation tests were necessary. Although there were unit tests for some components, they did not check the functionality of the entire application. Manually testing all the functionality of each release would be too time-consuming, and besides, humans (at least I) are lazy and can make mistakes. If it can be automated, then it should be automated. The first tests were built using the &lt;a href="https://github.com/TestStack/White"&gt;TestStack.White&lt;/a&gt; library. Now, since this library is no longer supported, there is a smooth migration to the &lt;a href="https://github.com/FlaUI/FlaUI"&gt;FlaUI&lt;/a&gt; library.&lt;/p&gt;

&lt;p&gt;It was decided to use the &lt;a href="https://specflow.org/"&gt;SpecFlow&lt;/a&gt; BDD framework, which conveniently describes the steps for using the application and the expected results. A typical test looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Scenario: 01 Sample data diff test
  Given [System] Set template folder to '04_DataDiff'
    And [App] Start with 'dd1;dd2' loaded
   When [HomeTab] Click on the 'left' panel summary 'Total' for 'Table definitions'
    And [ObjectList] On the 'left' panel click on 'Compare data (all records)' for 'Client'
   Then [DataDiff] 'right' panel totals is '2;0;1;1', per page '2;0;1;1'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For each step, code is written that uses the UI automation library to check and manipulate the UI. However, over time, there was a complication with parameterized tests, where the same steps needed to be used for different cases. SpecFlow allows describing parameterized tests using Scenario Outline, where each case is described by a single row in the Examples table. Thus, for one case, we can only a set of scalar values. However, this was insufficient to describe the expected result of a complex UI consisting of multiple grids, each needing to be described by a separate table. For such tests, another approach was developed: each case is described by a row in an Excel table, with columns describing the actions and the expected UI state. Since an Excel cell can contain multiline text, a special syntax was adopted to describe multiple UI grids in one cell, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Columns (Name; Nullability)]
- id; NOT NULL
- name; NULL

[Constraints (Type; Columns)]
- PRIMARY KEY; id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From a one-dimensional list of parameters in SpecFlow, we moved to a four-dimensional set of parameters: a set of Excel cells, each containing a set of tables, with each table being a set of rows and columns. Using Excel or its alternatives is convenient as there are ready-to-use tools for viewing and editing. But there are significant drawbacks associated with the xlsx file format, such as difficulty tracking test history, comparing them, and understanding the volume of existing tests. Therefore, in the future, these scenarios are planned to be moved from Excel to some text format, most likely XML, requiring the development of a UI for editing these scenarios.&lt;/p&gt;

&lt;p&gt;A typical test proceeds as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prepare one or two database instances. Only one instance is used if the test checks the application's behavior when only one database is opened, or when the same database is compared to itself. In most cases, databases are described as scripts. For tests described using Excel, the script is assembled from spreadsheet content on the fly. For applications working with Access or SQLite, ready-made DB files are sometimes used instead of scripts. Specifically for Access, not all database objects can be created with an SQL script, even when only dealing with tables. For SQLite, ready-made files are used to perform a more complete end-to-end testing cycle, which is particularly important for protected files.
Where possible:

&lt;ul&gt;
&lt;li&gt;Databases are created in parallel to speed up the process.&lt;/li&gt;
&lt;li&gt;Database files are created on a RAM Drive, sometimes with the entire database server placed there. Using a RAM Drive not only speeds up test execution but also prolongs the life of SSDs or HDDs.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The application is launched and opens the databases. If possible, and if not crucial for the test, specifying the database is done via the command line to save time clicking through database open dialogs.&lt;/li&gt;
&lt;li&gt;The test clicks through the application and checks UI elements according to the scenario steps. When the application generates and executes SQL scripts, the text of these scripts is extracted and goes to the output of the test execution console. This often helping to understand the problem without debugging the test.&lt;/li&gt;
&lt;li&gt;If the test fails, a screenshot of the application is saved.&lt;/li&gt;
&lt;li&gt;The application is closed, and the databases are deleted. In the most cases database deletion occurs in the background, saving some time. Deletion is controlled by a switch; sometimes databases are retained for further analysis and debugging. Tests for AccdbMerge also check for any non-closed Access processes, which are used by the application to process database files.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1 and 2 can take significant time. Creating one database with two tables for two different cases and launching the application once can be significantly faster than creating a database with one table twice and launching the application twice (once for each database). Therefore, some cases are combined, if possible, so that one database is created for all cases, and the application is launched only once.&lt;/p&gt;

&lt;h2&gt;
  
  
  DBMS-specific things
&lt;/h2&gt;

&lt;p&gt;All of the listed relational DBMSs deal with tables; tables consist of columns and rows. SQL is everywhere. It would seem that I can easily take some existing test from one application and use it for another. Fortunately, sometimes this is almost possible. If a new feature is being developed, which is subsequently planned to be implemented in tools for different DBMSs, then the test for it is usually written first for SQLite, since these tests are the fastest. Often such a test and scripts for it can be reused for other products with minimal changes. But, as we know, the devil is in the details. Different data types, different database management capabilities, different SQL. As a result, the tests still have significant differences. Let's talk about them. The following will list the features for each DBMS separately, in the order these tests were created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Microsoft Access
&lt;/h2&gt;

&lt;p&gt;Possibly the biggest issue for my tests is the limited SQL in Access, both for DDL and DML. Access has Queries similar to Views in other DBMS, and there is a CREATE VIEW statement described in the documentation, but &lt;a href="https://stackoverflow.com/questions/11367959/create-view-in-ms-access-2007"&gt;it does not work&lt;/a&gt;. There are many data types that cannot be used either in CREATE TABLE or INSERT. As a result, using a script, you can create a database file with tables using simple data types like numbers or strings. But for something more complex, pre-prepared mdb and accdb files often have to be used. However, even if we have prepared database templates, sometimes simply copying them is not enough. A common practice in Access development is to split the database into a backend and frontend, resulting in linked tables in the frontend that need to update their links to the backend after copying.&lt;/p&gt;

&lt;p&gt;Another problem with interacting with Access is that it updates from time to time, causing some tests to stop working. My application stops working even though nothing has changed in it — only Access has changed. They have &lt;a href="https://techcommunity.microsoft.com/t5/access-blog/breaking-ace-out-of-the-bubble/bc-p/2606712/highlight/true#M213"&gt;broken&lt;/a&gt; &lt;a href="https://techcommunity.microsoft.com/t5/access-blog/breaking-ace-out-of-the-bubble/bc-p/3641817/highlight/true#M289"&gt;twice&lt;/a&gt; the availability of DAO libraries from the outside.&lt;/p&gt;

&lt;p&gt;The most frequent support request I receive is that AccdbMerge cannot connect to Access, which is always fixed only by restoring Office, without changing anything in AccdbMerge.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQL Server
&lt;/h2&gt;

&lt;p&gt;I started my programming career in the early 2000s, and SQL Server was the first database engine I worked with. So, SQL Server is a sort of standard DBMS for me, and for a long time, all other DBMSs were learned through comparison with SQL Server.&lt;/p&gt;

&lt;p&gt;Perhaps the most interesting result of making tests for SQL Server was some incompatibility with .NET data types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The SQL Server &lt;strong&gt;decimal&lt;/strong&gt; data type can exceed the capacity of the .NET &lt;strong&gt;decimal&lt;/strong&gt; data type&lt;/li&gt;
&lt;li&gt;The SQL Server &lt;strong&gt;uniqueidentifier&lt;/strong&gt; and the .NET &lt;strong&gt;Guid&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql/comparing-guid-and-uniqueidentifier-values"&gt;have different internal presentation and sorting rules&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  MySQL and MariaDB
&lt;/h2&gt;

&lt;p&gt;The main problem for me during my work on these tools was the frequency of new releases of these DBMSs, each new release needs to be checked for compatibility with the application. At first, this mostly concerned MariaDB; MySQL stayed on version 8.0 for a long time. However, it recently released versions 8.1 to 8.4 within a short interval.&lt;/p&gt;

&lt;p&gt;From my experience working with different DBMSs, MySQL ranks second after Access in terms of the number of bugs. And sometimes they don't get fixed for years. One example, which in particular is one of the reasons for the differences between tests, is non-working inline foreign keys. There are a lot of questions on this topic on StackOverflow and &lt;a href="https://stackoverflow.com/questions/24313143/mysql-how-to-declare-foreign-key-inline"&gt;one of them&lt;/a&gt; contains links to a bunch of related bugs in MySQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  SQLite
&lt;/h2&gt;

&lt;p&gt;SQLite has several specific features, but the most distinctive for my tools and their tests is handling of data types. Not only can it simply be missing, but even if they are specified, by default you can easily put the value of the different data type. Fortunately, version 3.37.0 introduced the concept of STRICT tables to fix this, but it does not eliminate the need to maintain regular tables.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.sqlite.org/formatchng.html"&gt;compatibility&lt;/a&gt; between database files and library versions is admirable. However, knowing about this super-compatibility, it was doubly strange to face &lt;a href="https://sqlite.org/forum/forumpost/d2c637bafbbff69d"&gt;the end of support&lt;/a&gt; for one of the methods for protecting database files.&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Similar to SQL Server, I did not have any significant issues with PostgreSQL. The main difficulty from a development point of view was the huge number of data types and a wide variety of types of database objects and their properties. And it was new to me to met NaN and +/- infinity values for some numeric and timestamp data types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Oracle
&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, before running the test, we must first create a database. For SQL Server, MySQL, and PostgreSQL, this is simply done by sending the CREATE DATABASE command to the database server. But not for Oracle. It doesn't work there. You have the database server - and that's your database. And don't ask for another one. Instead of a database, I had to create a user which is created with a new schema. Then the test interacts with objects within this schema. Since the schema name is displayed in the application as part of the table name, and this schema is different each time, special variables had to be entered and processed. For other DBMS tests, the table is referred to as TableName or dbo.TableName or public.TableName, and this is the real name of the object. But for Oracle tests, I had to write $schema.TABLENAME and then in the code replace $schema with the real name of the schema before looking for this one object on the application UI. At first, I thought that maybe I just didn’t know something about Oracle, but then I came across the source code of one of the DB Fiddles - its authors did the same thing.&lt;/p&gt;

&lt;p&gt;Another feature of Oracle is that an empty string is always treated as NULL. Unlike any other DBMS, it is impossible to save an empty string as non-NULL.&lt;/p&gt;

&lt;p&gt;I use Windows, and all my desktop apps work only on Windows. Another feature of working with Oracle was the need to use Docker Desktop since I could not find a Windows version for the latest versions of Oracle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other
&lt;/h2&gt;

&lt;p&gt;In addition to the listed differences, there are several more points that are specific to each DBMS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Case-sensitivity and quoted identifiers work differently for object names. Some DBMSs will leave the CamelCase object name as is, others will convert it to lowercase, and others to uppercase. For some DBMSs, CamelCase can be preserved if you surround the name with double quotes, but double quotes are not used by all DBMSs. MySQL and MariaDB use grave accents (`), and SQL Server (by default) and Access use square brackets ([]).&lt;/li&gt;
&lt;li&gt;Creating database objects takes time, and sometimes it can be speeded up if you wrap all statements in a transaction. However, not every DBMS supports transactions for DDL statements. Additionally, some DDL commands sometimes cannot be used together with others and must be sent to the DBMS server as a separate request. &lt;/li&gt;
&lt;li&gt;When filling tables with data, it is convenient to use multi-row INSERT statements like this:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSERT INTO TableName
  (ID, Name)
VALUES
  (1, 'Smith'),
  (2, 'Doe')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But this syntax is not supported by all DBMSs, or some DBMSs may not support it for all versions. In order for the test to work on all the required DBMS versions, we have to split the script into separate INSERTs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article, we discussed the practical experience of building a UI automation testing system for desktop applications that interact with various relational DBMSs. The necessity of automated UI testing became evident after a release highlighted the risks of manual testing. Each DBMS presented unique challenges, from Access's limited SQL capabilities and frequent updates to Oracle's unconventional database creation requirements. The experience highlighted the importance of flexibility and adaptability in automated testing to accommodate the nuances of different DBMSs.&lt;/p&gt;

&lt;h2&gt;
  
  
  P. S.
&lt;/h2&gt;

&lt;p&gt;According to the publishing rules, I have to note that I have used AI for title, summary and proofreading of the rest of the text. But the text itself was handwritten, including the SQL for the cover image :).&lt;/p&gt;

</description>
      <category>testing</category>
      <category>database</category>
      <category>sql</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
