Create INSERT Statements as Target Output

Overview

This guide describes how to use a Row Level to Icebox task to generate SQL INSERT statements instead of loading the selected data into a target database. This approach is useful when the target database cannot be reached or configured as an XDM target connection. In that case, XDM is used to select the required subset of data, follow the configured data relations, and write the selected rows as SQL statements to an XDM file. The resulting file can then be downloaded and executed outside XDM. The example in this guide uses the sample environment from the tutorial as the source environment. The approach itself is generic: it can be adapted to other source databases, provided that the generated SQL syntax is valid for the database where the statements will be executed.

The configuration consists of the following parts:

  • A custom parameter that points to the XDM file used as output.

  • An XDM file that stores the generated SQL text.

  • A row-based Groovy Modification Method that creates INSERT statements for the selected rows.

  • A Groovy Task Stage Hook that collects the generated SQL files and writes them to the XDM file.

  • A Row Level to Icebox task that selects the data and runs the Modification Method and Task Stage Hook.

  • The task does not use the Icebox generation as the main result. Instead, the relevant result is the generated SQL file.

The generated SQL file must be reviewed before it is executed on an external database. Pay special attention to table order, date and timestamp formats, identity columns, generated columns, and database-specific data types.

Prerequisites

Before you start, the following objects must already exist:

  • A source connection with credentials.

  • An application model with at least one version.

  • A start table configured on the application model version.

  • Data relation rules that describe which related rows XDM must collect.

  • A source environment with at least one installed application.

  • A start model configured on the source environment.

  • Required permissions to read the source objects and execute the task.

  • For the tutorial-based example, use the sample environment and the application model setup from the Row Level Processing tutorial.

The target database is not configured as an XDM target in this guide. The generated SQL script is executed outside XDM.

Steps

Step 1: Create the output file

Create an XDM file that will store the generated SQL text.

Use the following settings:

Setting

Value

Name

InsertOutput

File type

SQL

Content

Empty

InsertOutput is the final output file.

Step 2: Create the Custom Parameter

Create a custom parameter for the output file.

Use the following settings:

Setting

Value

Name

Insert Output File

Variable name

INSERT_OUTPUT_FILE

Type

FILE

Default value

InsertOutput

Requirement level

NONE

This custom parameter allows the task or workflow configuration to reference the XDM file that receives the generated SQL content.

Step 3: Create the Modification Method

In this guide, the method is named Create Insert Statements.

Use the following settings:

Setting

Value

Name

Create Insert Statements

Script language

GROOVY

Input type

ROW

Mapping table access mode

READ

The Modification Method is used during source processing. It prepares one SQL output file per processed table and writes one SQL INSERT statement for each selected row.

import de.ubs.xdm.utils.modification.ModificationContext

// Class-level variables
String insertPrefix            // "INSERT INTO schema.table (COL1, COL2, ...) VALUES ("
List<String> columnNames       // Column names in column order
List<String> columnTypesLocal  // Column types in column order
File outputFile                // Output file for the INSERT statements

/**
 * Initialization phase of the Modification Method.
 */
def init() {
    // 1. Retrieve table information.
    // getCurrentTableSchema() returns the schema, getCurrentTableName() returns the table name.
    String schemaName = ctx.getCurrentTableSchema()
    String tableName  = ctx.getCurrentTableName()

    String fullTableName = (schemaName != null && !schemaName.trim().isEmpty())
            ? "${schemaName}.${tableName}"
            : tableName

    // 2. Retrieve column information.
    def cols = ctx.getDataColumns()

    // Sort columns by column position if required.
    // cols = cols.sort { it.getColumnNo() }

    columnNames = cols.collect { def c ->
        c.getName()
    }

    columnTypesLocal = cols.collect { def c ->
        String typeName = c.getTypeName()
        return typeName != null ? typeName.toUpperCase() : null
    }

    // 3. Build the INSERT statement prefix.
    String columnList = columnNames.collect { "\"${it}\"" }.join(", ")
    insertPrefix = "INSERT INTO ${fullTableName} (${columnList}) VALUES ("

    // 4. Retrieve root_dir from the task runtime properties.
    String rootDirPath = ctx.getTaskRuntimeProperty("root_dir")

    if (!rootDirPath || rootDirPath.trim().isEmpty()) {
        throw new IllegalStateException("Task runtime property 'root_dir' is not set.")
    }

    File rootDir = new File(rootDirPath)

    if (!rootDir.exists()) {
        rootDir.mkdirs()
    }

    // 5. Prepare the output file for this table.
    String safeTableName = fullTableName.replace('.', '_')
    outputFile = new File(rootDir, "insert_${safeTableName}.sql")

    if (outputFile.exists()) {
        outputFile.delete()
    }

    outputFile.createNewFile()

    return null
}

/**
 * Apply phase.
 * This method is executed once for each row.
 */
def apply() {
    List<String> values = []

    for (int i = 0; i < data.length; i++) {
        Object value = data[i]
        String type = (i < columnTypesLocal.size()) ? columnTypesLocal[i] : null

        if (value == null) {
            values << "NULL"
            continue
        }

        String upperType = type != null ? type.toUpperCase() : ""

        switch (upperType) {
            case ["CHAR", "VARCHAR", "CLOB", "GRAPHIC", "VARGRAPHIC"]:
            case { it.startsWith("CHAR(") || it.startsWith("VARCHAR(") || it.contains("CHAR") }:
                // Character-based values are written as SQL string literals.
                values << toStringLiteral(value.toString())
                break

            case "DATE":
                // Expected format: YYYY-MM-DD.
                // Add formatting here if the input value uses a different format.
                String dateString = value.toString()
                values << "DATE '${dateString}'"
                break

            case ["TIMESTAMP", "TIMESTMP"]:
                // Expected Db2 format: YYYY-MM-DD-HH.MM.SS[.ffffff].
                String timestampString = value.toString()
                values << "TIMESTAMP '${timestampString}'"
                break

            default:
                // Numeric and other values are written without additional formatting.
                values << value.toString()
        }
    }

    String valueList = values.join(", ")
    String statement = "${insertPrefix}${valueList});"

    // Append the statement to the output file.
    outputFile << (statement + System.lineSeparator())

    // Do not pass the record to further task processing.
    return false

    // Use this if the record should continue in the task processing chain.
    // return true
}

/**
 * Close phase of the Modification Method.
 */
def close() {
}

/**
 * Converts a string value into a Db2 SQL string literal.
 */
private String toStringLiteral(String value) {
    String escapedValue = value.replace("'", "''")
    return "'${escapedValue}'"
}

The method performs the following steps:

  1. During initialization, it determines the current schema and table name.

  2. It reads the column names and data types of the current table.

  3. It creates an INSERT INTO …​ VALUES …​ prefix for the current table.

  4. It creates one intermediate SQL file per table in the task runtime directory.

  5. For each selected row, it formats the row values as one SQL INSERT statement.

  6. It writes the statement to the table-specific SQL file.

  7. It returns false so that the row is not passed on to the normal Icebox output.

Because the method returns false, the task may show no rows written to the Icebox. This is expected for this configuration.

Step 4: Create the Modification Set

Create a modification set that contains a modification rule for the Create Insert Statements method.

Example settings for the modification set:

Setting

Value

Name

Generate INSERT statements

Active

Enabled

Script language

GROOVY

Add a modification rule with the following settings:

Setting Value

Active

Enabled

Modification Method

Create Insert Statements

Scope

SOURCE

Schema pattern

%

Name pattern

%

Column pattern

%

Modification propagation

Disabled

With these patterns, the rule applies to all selected source tables. Restrict the schema or table pattern if only specific tables should be exported as SQL statements.

Step 5: Create the Task Stage Hook

Create a Groovy Task Stage Hook. In this guide, the hook is named Collect INSERT Files.

Use the following settings:

Setting

Value

Name

Collect INSERT Files

Code type

GROOVY

Add a hook parameter for the output file:

Setting

Value

Parameter name

StatementOutputFile

Data type

FILE

Default value

InsertOutput

import java.nio.file.*

/**
* Task Stage Hook (POST Stage 2)
*
* Expected input:
* - properties["root_dir"]: Directory that contains the insert_*.sql files.
* - taskStageHook.StatementOutputFile: File parameter of the hook.
*   - taskStageHook.StatementOutputFile.content is populated with the complete SQL text.
*
* The content of all insert_*.sql files is collected in an internal string variable
* and written to taskStageHook.StatementOutputFile.content at the end.
*/

// 1. Determine root_dir.
String rootDirPath = properties?.get("root_dir") as String

if (!rootDirPath || rootDirPath.trim().isEmpty()) {
throw new IllegalStateException("Runtime property 'root_dir' is not set.")
}

File rootDir = new File(rootDirPath)

if (!rootDir.exists() || !rootDir.isDirectory()) {
throw new IllegalStateException("root_dir '${rootDirPath}' does not exist or is not a directory.")
}

// 2. Collect insert_*.sql files.
List<File> insertFiles = rootDir.listFiles(new FilenameFilter() {
@Override
boolean accept(File dir, String name) {
return name.startsWith("insert_") && name.endsWith(".sql")
}
})?.toList() ?: []

// 3. Define the processing order.
// Default option: sort alphabetically by file name.
// insertFiles.sort { it.name }

// Alternative option: use an explicit table order if required.
List<String> tableOrder = [
"production.employees",
"production.titles",
"production.salaries"
]

List<File> orderedFiles = []

tableOrder.each { String tableName ->
String fileName = "insert_${tableName.replace('.', '_')}.sql"
File file = insertFiles.find { it.name == fileName }

    if (file) {
        orderedFiles << file
    }
}

orderedFiles.addAll(insertFiles - orderedFiles)
insertFiles = orderedFiles

// 4. Collect the file contents in memory.
StringBuilder contentBuilder = new StringBuilder()

insertFiles.each { File file ->
contentBuilder.append("-- BEGIN ").append(file.name).append(System.lineSeparator())
contentBuilder.append(file.getText("UTF-8"))
contentBuilder.append(System.lineSeparator())
contentBuilder.append("-- END ").append(file.name).append(System.lineSeparator())
}

// 5. Write the collected content to the XDM file.
// The hook parameter is a File parameter, and the content is assigned as a string.
String fullContent = contentBuilder.toString()

if (taskStageHook?.StatementOutputFile == null) {
throw new IllegalStateException("Hook parameter 'StatementOutputFile' (File) is not configured.")
}

taskStageHook.StatementOutputFile.content = fullContent

// Optional: store the output size in properties if the calling workflow needs it.
// properties.put("INSERT_OUTPUT_SIZE", fullContent.length().toString())

The Task Stage Hook runs after Stage2. It performs the following steps:

  1. It reads the task runtime directory from root_dir.

  2. It searches for files matching insert_*.sql.

  3. It sorts the files.

  4. It optionally applies an explicit table order.

  5. It reads the content of all SQL files.

  6. It writes the combined SQL text to StatementOutputFile.

The parameter name used in the hook definition and the parameter name used in the Groovy code must be identical. If the parameter is named StatementOutputFile, the script must access it as taskStageHook.StatementOutputFile.

Step 6: Configure the Row Level to Icebox task

Create or edit a Row Level to Icebox task template.

Use the sample source environment from the tutorial, or another source environment with an application model and data relation rules.

Configure the task template as follows:

Setting

Value

Source environment

The environment that contains the source application model.

Row selection mode

QUERY

Start condition

A condition or query that selects the initial rows from the start table.

Modification set

Generate INSERT statements

Custom parameter

Insert Output File.

Insert output file

Select InsertOutput.

The exact start condition depends on the start table of the application model.

Example:

"emp_no" < 10017

During execution, the Row Level to Icebox task uses the start condition on the start table. It then follows the configured data relation rules and selects related parent and child rows.

Step 7: Add the Task Stage Hook Usage

Add a Task Stage Hook Usage to the Row Level to Icebox task template.

Use the following settings:

Setting

Value

Active

Enabled

Stage

Stage2

Hook phase

Post

Task stage hook

Collect INSERT Files

StatementOutputFile

InsertOutput

The hook must run as a POST hook for Stage2, because the intermediate insert_*.sql files are created during Stage2.

Step 8: Execute the task

Execute the Row Level to Icebox task.

After a successful execution:

The Modification Method has created intermediate insert_*.sql files in the task runtime directory. The Task Stage Hook has collected these files. The XDM file InsertOutput contains the combined SQL script. Example output:

-- BEGIN insert_production_employees.sql
INSERT INTO production.employees ("emp_no", "first_name", "last_name")
VALUES (10001, 'Georgi', 'Facello');
-- END insert_production_employees.sql
-- BEGIN insert_production_titles.sql
INSERT INTO production.titles ("emp_no", "title", "from_date", "to_date")
VALUES (10001, 'Senior Engineer', DATE '2006-06-26', DATE '9999-01-01');
-- END insert_production_titles.sql

You can download the generated SQL file and execute it outside XDM.

Benefits

This approach provides the following benefits:

  • It allows XDM to prepare data for target databases that cannot be configured as XDM target connections.

  • It keeps the target database outside the XDM execution process. The generated SQL file can be reviewed before it is executed.

  • It creates a single XDM file that can be downloaded.

  • It separates data selection from data loading. XDM selects and formats the data, while the final execution is handled by an external database tool.

  • It makes the generated data transparent, because the result is plain SQL text.

  • It can be adapted to different source databases, as long as the generated SQL syntax is valid for the database where the statements are executed.

Conclusion

With this configuration, a Row Level to Icebox task can be used to generate SQL INSERT statements instead of producing an Icebox generation as the main result. The Modification Method creates table-specific SQL files during source processing. The POST Stage2 Task Stage Hook collects these files and writes the combined SQL script to the XDM file InsertOutput. The resulting file can be reviewed, downloaded, and executed outside XDM against a database that is not configured as an XDM target connection. This pattern is useful when XDM should still control the data selection, relationship traversal, and row processing, but the final data load must happen outside XDM.