Search docs.molo17.com
    Configuration guide Pipelines

    Write and test a UDF to convert dates, times, and names

    When a source column cannot be copied to the target as it is, a built-in field function covers the simple cases and a User Defined Function (UDF) covers the rest. This guide shows how to choose between them, how to write a UDF in the Control Plane that parses text into dates in several formats, writes a TIME as text with a pattern, feeds one source field into two target columns, and handles nulls and bad values, and how to test it before it touches real data.

    In this guide

    You will learn how to decide between a built-in field function, a Custom Field Function, and a UDF; map source fields to target columns with arrows; apply the built-in functions that fit; write, compile, and test a UDF in the editor, including how to read and fix a compile error and a wrong-type error; attach it to an entity; run a snapshot and a change data capture (CDC) change; and verify the target values with SQL.

    Quick information

    Goal

    Convert text to dates, a TIME to text, and one field into two columns with a tested UDF, and keep nulls and bad values under control

    Audience

    Data engineers and integration developers who can read basic Java

    Level

    Intermediate

    Time required

    About 45 minutes

    Area

    Control Plane → Pipelines → entity Options → Edit mapping → UDF

    Applies to

    Gluesync 2.3

    Before you begin

    Make sure that:

    1. A pipeline already has a source and a target that Core Hub can read, for example PostgreSQL to PostgreSQL, and you can edit its entities;

    2. The source table holds the values to convert and the target table already has columns of the types you need. In this guide the source is shop_udf.events, with a date stored as text in several formats, a TIME column, a full name, and a code with stray spaces, and the target is shop_udf.events with DATE and varchar columns (the tables are in the optional first step);

    3. The entity is not started, or you accept that saving its mapping stops it;

    4. You can open Query Studio, or any SQL client, on the target database.

    The UDF editor in the Control Plane writes Java: the language label under the editor reads Java. The code in this guide uses only the standard Java library and the classes that the generated code already imports.

    Choose a field function, a Custom Field Function, or a UDF

    Gluesync offers three ways to change a value on its way to the target. Pick the lightest one that does the job.

    Use When What you saw in this guide

    Built-in field function

    One source column feeds one target column (1:1) and the transformation is in the catalog: trim a string, format a TIME or a date as text with one pattern, parse text with one pattern, mask a value.

    Convert Time to String with the pattern HH:mm on end_time_text, and Trim String on code. Both are chosen from the fx button of the target column, with no code.

    Custom Field Function

    Your own logic for one target column, reused by several entities, or one target built from several source columns (N:1).

    For a target that is fed by a source field that also feeds another column (1:N), the fx panel lists no built-in function and offers Add a custom field function only.

    UDF

    You need logic that a catalog function cannot express: try several input formats, split one value into two columns, choose a default for a null or a bad value, or decide row by row.

    Parse four date spellings, split a full name, write the same TIME as 14:05 and 02:05 PM, and keep a bad date from stopping the entity.

    A built-in field function is always the first choice. In this guide a UDF is needed because the date arrives in four spellings and because one source field feeds two target columns.

    The three can be combined on one entity. The UDF runs first, then the field functions run on what the UDF returned. The next steps show what that means for the columns that have a field function.

    Guide steps

    Optional: create the demo tables

    SQL

    Skip this step if you use your own tables. Otherwise create the source table and the target table, and add the pipeline UDF demo with an entity that maps shop_udf.events to shop_udf.events.

    1. In the source database, run:

      CREATE SCHEMA IF NOT EXISTS shop_udf;
      CREATE TABLE shop_udf.events (
        id              integer PRIMARY KEY,
        full_name       varchar(100),
        event_date_text varchar(20),
        start_time      time,
        code            varchar(30),
        end_time        time
      );
      INSERT INTO shop_udf.events (id, full_name, event_date_text, start_time, code, end_time) VALUES
       (1, 'Maria Rossi',      '20260131',   '09:30:00', '  ab-001 ', '11:00:00'),
       (2, 'Luca de Angelis',  '31/01/2026', '14:05:30', 'cd-002',    '15:35:30'),
       (3, 'Cher',             '2026-02-15', '23:59:59', '  ef-003',  '01:29:59'),
       (4, 'Anna Maria Verdi', '15.02.2026', NULL,       'gh-004  ',  NULL),
       (5, 'Paul Smith',       'not a date', '08:00:00', 'ij-005',    '09:30:00'),
       (6, NULL,               NULL,         '00:00:00', NULL,        '01:30:00');
    2. In the target database, run:

      CREATE SCHEMA IF NOT EXISTS shop_udf;
      CREATE TABLE shop_udf.events (
        id               integer PRIMARY KEY,
        first_name       varchar(60),
        last_name        varchar(60),
        event_date       date,
        event_date_label varchar(20),
        start_time_24h   varchar(20),
        start_time_12h   varchar(20),
        code             varchar(30),
        end_time_text    varchar(20)
      );

    The source columns and what each target column receives:

    Source column Target column Conversion

    id

    id

    Copied

    full_name

    first_name and last_name

    UDF: one field into two columns

    event_date_text

    event_date and event_date_label

    UDF: text in four formats into a DATE, and the same date as text such as 31 Jan 2026

    start_time

    start_time_24h and start_time_12h

    UDF: a TIME as 14:05 and as 02:05 PM

    end_time

    end_time_text

    Built-in field function Convert Time to String, pattern HH:mm

    code

    code

    Built-in field function Trim String

    Map the fields with arrows

    UI

    1. Open the pipeline UDF demo;

    2. On the row of the entity events, select the three-dot Options menu, and then select Edit mapping. The Field mapping dialog (the Fields Editor) opens;

    3. Check the source field end_time if it is not checked;

    4. Drag from the dot at the right of each source field to the dot at the left of its target field: full_name to first_name and to last_name, event_date_text to event_date and to event_date_label, start_time to start_time_24h and to start_time_12h, and end_time to end_time_text. The arrows id to id and code to code are already drawn.

    Field mapping dialog with the Incompatible data types banner marked 1
    Figure 1. Three target fields are marked with a type error (2) and the banner says Incompatible data types (1). The UDF button (3) is at the top right of the target table.
    Expected result

    The dialog lists the errors: date cannot take text, and a varchar cannot take a TIME. Confirm stays disabled until the errors are resolved. A banner also says that the save adds columns and that the entity will be stopped and needs a resync; that is expected when you add target columns to an entity.

    A second banner appears as soon as one source field feeds two target fields: without a custom field function or the UDF, the value of the source column is copied to each of its target columns.

    Apply the built-in functions that fit

    UI

    1. In the target table, select the fx button at the end of the row end_time_text. The Apply a field function panel opens, with the chips Built-in (1) and My functions (0);

    2. Open the Functions collection list and select Convert Time to String;

    3. Open Select map to expression and select the pattern HH:mm;

    4. Select the fx button of the row code, open Functions collection, and select Trim String.

    Apply a field function panel on end_time_text with the fx button marked 1
    Figure 2. The fx button (1), the function (2), and the pattern (3) of end_time_text.

    Now select the fx button of the row start_time_24h.

    Panel of start_time_24h reading No field function found
    Figure 3. A target that shares its source with another target (1) lists no built-in function (2).
    Expected result

    end_time_text and code show a blue fx and end_time_text no longer has an error. start_time_24h reports No field function found because start_time also feeds start_time_12h: built-in functions are offered on 1:1 arrows only. The UDF in the next steps converts these columns.

    Open the UDF editor

    UI

    1. In the target table toolbar, select UDF. The dialog User defined function (UDF) for shop_udf.events opens;

    2. Read the generated code. It is Java, and it has one method with this signature:

      public Pair<MappingFunctionOperation, Map<String, Object>> onChange(Map<String, Object> newValues, Map<String, Object> oldValues, MappingFunctionOperation operation, boolean isSnapshot, Logger logger)
    UDF editor with the generated Java code
    Figure 4. Compile (1) and Test (2) are at the top right of the editor; Row origin (3) tells the UDF whether a row comes from CDC or from a snapshot.
    Expected result

    The editor shows a generated class named UDF_ followed by an identifier, with one targetValues.put(…​) line per target column. newValues is keyed by the source column names, and the map you return is keyed by the target column names. The right side shows Input data with a random sample value for each source column, and an empty Results panel.

    Do not keep the generated line for a column that has a field function. For end_time_text the generated code converts the TIME to a String with String.valueOf, and the entity then fails at run time because the field function expects a TIME. The code in the next step passes the original value instead.

    Paste the UDF and fix a compile error

    UI

    1. Select all the text in the editor and replace it with the code below. The class name does not matter: Core Hub renames the class to the generated name when you compile;

    2. Select Compile.

    import java.time.LocalDate;
    import java.time.LocalTime;
    import java.time.format.DateTimeFormatter;
    import java.time.format.DateTimeParseException;
    import java.time.format.ResolverStyle;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Locale;
    import java.util.Map;
    import kotlin.Pair;
    import com.molo17.gluesync.commons.model.api.MappingFunctionOperation;
    import org.slf4j.Logger;
    
    public class UDF_events {
    
        // Accepted spellings of the date, tried in order. STRICT rejects impossible dates such as 31/02/2026.
        private static final List<DateTimeFormatter> DATE_INPUTS = List.of(
                strict("uuuuMMdd"),      // 20260131
                strict("dd/MM/uuuu"),    // 31/01/2026
                strict("uuuu-MM-dd"),    // 2026-01-31
                strict("dd.MM.uuuu"));   // 31.01.2026
    
        private static final DateTimeFormatter DATE_LABEL = DateTimeFormatter.ofPattern("dd MMM uuuu", Locale.ENGLISH);
        private static final DateTimeFormatter TIME_24H = DateTimeFormatter.ofPattern("HH:mm");
        private static final DateTimeFormatter TIME_12H = DateTimeFormatter.ofPattern("hh:mm a", Locale.ENGLISH);
    
        private static DateTimeFormatter strict(String pattern) {
            return DateTimeFormatter.ofPattern(pattern).withResolverStyle(ResolverStyle.STRICT);
        }
    
        public Pair<MappingFunctionOperation, Map<String, Object>> onChange(Map<String, Object> newValues, Map<String, Object> oldValues, MappingFunctionOperation operation, boolean isSnapshot, Logger logger) {
            Map<String, Object> targetValues = new HashMap<>();
            targetValues.put("id", newValues.get("id"));
    
            // Columns that also have a field function (Trim String on code, Convert Time to String on
            // end_time_text) receive the untouched source value: the field function runs after the UDF
            // and expects the original type, a LocalTime for end_time_text, not a String.
            targetValues.put("code", newValues.get("code"));
            targetValues.put("end_time_text", newValues.get("end_time"));
    
            // One source column, two target columns: split the full name.
            String[] name = splitName(newValues.get("full_name"));
            targetValues.put("first_name", name[0]);
            targetValues.put("last_name", name[1]);
    
            // One source column, two target columns: the same date as a DATE and as a label.
            LocalDate eventDate = parseDate(newValues.get("event_date_text"), logger);
            targetValues.put("event_date", eventDate);
            targetValues.put("event_date_label", eventDate == null ? null : eventDate.format(DATE_LABEL));
    
            // One source column, two target columns: the same TIME in two text patterns.
            targetValues.put("start_time_24h", formatTime(newValues.get("start_time"), TIME_24H, logger));
            targetValues.put("start_time_12h", formatTime(newValues.get("start_time"), TIME_12H, logger));
    
            return new Pair<>(operation, targetValues);
        }
    
        private static LocalDate parseDate(Object value, Logger logger) {
            if (value == null) {
                return null;
            }
            if (value instanceof LocalDate) {
                return (LocalDate) value;
            }
            String text = value.toString().trim();
            if (text.isEmpty()) {
                return null;
            }
            for (DateTimeFormatter format : DATE_INPUTS) {
                try {
                    return LocalDate.parse(text, format);
                } catch (DateTimeParseException ignored) {
                    // try the next format
                }
            }
            logger.warn("event_date_text '{}' matches no accepted date format: event_date left null", text);
            return null;
        }
    
        private static String formatTime(Object value, DateTimeFormatter format, Logger logger) {
            if (value == null) {
                return null;
            }
            if (value instanceof LocalTime) {
                return ((LocalTime) value).format(format);
            }
            try {
                return LocalTime.parse(value.toString().trim()).format(format);
            } catch (DateTimeParseException e) {
                logger.warn("start_time '{}' is not a time: left null", value);
                return null;
            }
        }
    
        // First word = first name, the rest = last name. A single word is a first name only.
        private static String[] splitName(Object value) {
            if (value == null) {
                return new String[] {null, null};
            }
            String text = value.toString().trim().replaceAll("\\s+", " ");
            if (text.isEmpty()) {
                return new String[] {null, null};
            }
            int cut = text.indexOf(' ');
            if (cut < 0) {
                return new String[] {text, null};
            }
            return new String[] {text.substring(0, cut), text.substring(cut + 1)};
        }
    }

    To see how a compile error looks, change the line targetValues.put("code", newValues.get("code")); into the two lines below, and select Compile:

    String code = newValues.get("code");
    targetValues.put("code", code);
    UDF editor with the Compile failed banner marked 1 and the offending line String code equals newValues.get marked 2
    Figure 5. The banner reports the Java error (1); it does not give a line number, so look for the statement it describes (2).

    The values of the maps are Object, so Java refuses to assign one to a String. Restore the line targetValues.put("code", newValues.get("code")); and select Compile again.

    Expected result

    After the error, the banner reads Compile failed followed by MappingFunction compilation error: incompatible types: java.lang.Object cannot be converted to java.lang.String. After the fix, no banner is shown and the hint under the editor changes to Test the UDF by clicking.

    Test the UDF with sample rows

    UI

    1. In Input data, under New values, replace the random values with a row of your own. The first test uses id 2, full_name Luca de Angelis, event_date_text 31/01/2026, start_time 14:05:30, code ` cd-002 , and `end_time 15:35:30;

    2. Leave Row origin on CDC, and select Test;

    3. Repeat with the other spellings and the bad value: 20260131, 2026-02-15, 15.02.2026, 31/02/2026, and not a date.

    UDF editor after a test
    Figure 6. Edit the sample values (3), then read the three columns of the result (4): the UDF runs once for an insert, an update, and a delete.
    Expected result

    For the first row, the three result columns show Luca and de Angelis, 2026-01-31 and 31 Jan 2026, 14:05 and 02:05 PM, and the code. The four spellings all give a date. 31/02/2026 and not a date give an empty event_date and event_date_label, because the UDF returns null for a value it cannot read.

    Now try a wrong type on purpose. Replace the line targetValues.put("event_date", eventDate); with targetValues.put("event_date", newValues.get("event_date_text"));, compile, and select Test.

    Test failed banner reading Incorrect data type for column event_date must be of type LOCAL_DATE
    Figure 7. A text value for a DATE column is refused by the test (1), before anything is written.

    Restore the original line, compile, and test again.

    The test panel shows what the UDF returns, before the field functions. In the result, code is not trimmed yet and end_time_text is the raw time, 15:35:30. The functions apply when the entity runs. A blank sample value is an empty string, not a null: a blank TIME fails the test with Text '' could not be parsed at index 0. Check nulls with real rows, as in the last step.

    Save the UDF and confirm the mapping

    UI

    1. Select Save in the UDF dialog. Save is enabled after a successful test;

    2. In Field mapping, check that Error shows (0), and select Confirm;

    3. On the entity page, select Confirm and continue.

    Field mapping dialog with the UDF button showing a dot
    Figure 8. The UDF button now has a dot (1) and Error shows (0) (2). Confirm is at the bottom of the dialog.
    Expected result

    The type errors are gone. The warning Data type mismatch detected remains, because the source and target types still differ; it is expected, since the UDF converts the values. In the entity list, the entity shows the tags FF (field functions) and UDF.

    Run a snapshot

    UI

    1. On the pipeline page, select Run one-time snapshot in the actions of the entity events. The dialog Snapshot (copy-only) opens;

    2. Under Data already in the target, keep Keep and merge, the default;

    3. Select Start snapshot.

    Snapshot copy-only dialog with Keep and merge marked 1 and the Start snapshot button marked 2
    Figure 9. Keep and merge (1) overwrites rows with the same key. Start snapshot (2) starts the copy.

    Check the target with this SQL in a SQL client. Query Studio masks the two name columns, as the last step explains:

    SELECT id, first_name, last_name, event_date, event_date_label,
           start_time_24h, start_time_12h, code, end_time_text
    FROM shop_udf.events
    ORDER BY id;
    Expected result

    Six rows, with NULL shown as an empty cell:

    id first_name last_name event_date event_date_label start_time_24h start_time_12h code end_time_text

    1

    Maria

    Rossi

    2026-01-31

    31 Jan 2026

    09:30

    09:30 AM

    ab-001

    11:00

    2

    Luca

    de Angelis

    2026-01-31

    31 Jan 2026

    14:05

    02:05 PM

    cd-002

    15:35

    3

    Cher

    2026-02-15

    15 Feb 2026

    23:59

    11:59 PM

    ef-003

    01:29

    4

    Anna

    Maria Verdi

    2026-02-15

    15 Feb 2026

    gh-004

    5

    Paul

    Smith

    08:00

    08:00 AM

    ij-005

    09:30

    6

    00:00

    12:00 AM

    01:30

    The code is trimmed by Trim String, the end time is cut to HH:mm by Convert Time to String, and row 5, whose date text is not a date, has no date.

    Start CDC and change the source

    UI

    1. On the pipeline page, select the Start sync button of the entity events. The dialog Sync changes (CDC) opens;

    2. Keep Where it stopped and select Start entity;

    3. In the source database, run an insert, an update, and a delete:

      INSERT INTO shop_udf.events (id, full_name, event_date_text, start_time, code, end_time)
      VALUES (7, 'Grace  Murray Hopper', '20261224', '07:15:00', '  gm-007 ', '08:45:00');
      UPDATE shop_udf.events SET event_date_text = '31/12/2026', start_time = NULL WHERE id = 2;
      DELETE FROM shop_udf.events WHERE id = 6;
    Pipeline page with the entity events running
    Figure 10. The entity is running (1); the Features column shows the UDF and the field functions (2).
    Expected result

    After a few seconds the target has the new row 7 (Grace, Murray Hopper, 2026-12-24, 24 Dec 2026, 07:15, 07:15 AM, gm-007, 08:45), the row 2 has event_date 2026-12-31 and no start time, and the row 6 is gone. The double space in the name did not matter.

    Check nulls and bad values on live changes

    SQL

    1. In the source database, insert a row with a bad date and a row of nulls:

      INSERT INTO shop_udf.events (id, full_name, event_date_text, start_time, code, end_time)
      VALUES (8, 'Bad Date', 'tomorrow', '10:00:00', 'bd-008', '11:00:00'),
             (9, NULL, NULL, NULL, NULL, NULL);
    2. In Query Studio, open a session on UDF demo-target (TARGET) (select Query Studio in the left sidebar, then the + next to the session tabs, and choose the pipeline UDF demo and its target), paste the query below, and select Run:

      SELECT id, event_date, event_date_label, start_time_24h, start_time_12h, code, end_time_text
      FROM shop_udf.events
      ORDER BY id;
    Query Studio with the query on shop_udf.events marked 1
    Figure 11. The query (1), Run (2), and the result (3).
    Expected result

    Eight rows. Row 8 has no date, because tomorrow is not a date, but it has its times and its code: the entity keeps running. Row 9 is all NULL apart from its key. The entity is still Active on the pipeline page.

    Query Studio masks the columns first_name and last_name, because Core Hub tags them as personal data (PII:name), so they are not in this query. To check the names, run SELECT id, first_name, last_name FROM shop_udf.events ORDER BY id; in a SQL client; the Query Studio guide in the related guides explains how a user is granted the right to see personal data. After the changes above, the names are Maria Rossi, Luca de Angelis, Cher and nothing, Anna Maria Verdi, Paul Smith, Grace Murray Hopper, Bad Date, and two nulls for row 9.

    Optional, for advanced users

    You do not need the log to follow this guide. The UDF writes a warning for every date it cannot read, and the Core Hub log keeps it, for example event_date_text 'tomorrow' matches no accepted date format: event_date left null.

    How the conversions work

    Every snippet below is part of the UDF you compiled, tested, and ran.

    Text to a date in several formats

    The date arrives as String. List the accepted spellings once, and try them in order. The STRICT resolver rejects impossible dates such as 31/02/2026 instead of moving them to March.

        // Accepted spellings of the date, tried in order. STRICT rejects impossible dates such as 31/02/2026.
        private static final List<DateTimeFormatter> DATE_INPUTS = List.of(
                strict("uuuuMMdd"),      // 20260131
                strict("dd/MM/uuuu"),    // 31/01/2026
                strict("uuuu-MM-dd"),    // 2026-01-31
                strict("dd.MM.uuuu"));   // 31.01.2026
    
        private static DateTimeFormatter strict(String pattern) {
            return DateTimeFormatter.ofPattern(pattern).withResolverStyle(ResolverStyle.STRICT);
        }
    
        private static LocalDate parseDate(Object value, Logger logger) {
            if (value == null) {
                return null;
            }
            if (value instanceof LocalDate) {
                return (LocalDate) value;
            }
            String text = value.toString().trim();
            if (text.isEmpty()) {
                return null;
            }
            for (DateTimeFormatter format : DATE_INPUTS) {
                try {
                    return LocalDate.parse(text, format);
                } catch (DateTimeParseException ignored) {
                    // try the next format
                }
            }
            logger.warn("event_date_text '{}' matches no accepted date format: event_date left null", text);
            return null;
        }

    The pattern letters are uuuu for the year, MM for the month, and dd for the day. Put the most likely format first: the first format that parses wins, so a text such as 01/02/2026 is read with the first matching pattern. The method also returns a value that is already a java.time.LocalDate as it is.

    A TIME to text with a pattern

    A TIME column arrives as java.time.LocalTime. Format it with a DateTimeFormatter; do not cast it to String.

        private static final DateTimeFormatter DATE_LABEL = DateTimeFormatter.ofPattern("dd MMM uuuu", Locale.ENGLISH);
        private static final DateTimeFormatter TIME_24H = DateTimeFormatter.ofPattern("HH:mm");
        private static final DateTimeFormatter TIME_12H = DateTimeFormatter.ofPattern("hh:mm a", Locale.ENGLISH);
    
        private static String formatTime(Object value, DateTimeFormatter format, Logger logger) {
            if (value == null) {
                return null;
            }
            if (value instanceof LocalTime) {
                return ((LocalTime) value).format(format);
            }
            try {
                return LocalTime.parse(value.toString().trim()).format(format);
            } catch (DateTimeParseException e) {
                logger.warn("start_time '{}' is not a time: left null", value);
                return null;
            }
        }

    Use HH:mm for 24 hours and hh:mm a for 12 hours with AM and PM. Pass Locale.ENGLISH to get AM and PM whatever the language of the machine. If the column needs only one pattern and nothing else, use the built-in Convert Time to String instead, as the guide does for end_time_text.

    One source field, two target columns

    Draw two arrows from the same source field, then put a value for each target column. The same date becomes a DATE and a label; the same TIME becomes two texts; one full name becomes two names.

            // One source column, two target columns: split the full name.
            String[] name = splitName(newValues.get("full_name"));
            targetValues.put("first_name", name[0]);
            targetValues.put("last_name", name[1]);
            // One source column, two target columns: the same date as a DATE and as a label.
            LocalDate eventDate = parseDate(newValues.get("event_date_text"), logger);
            targetValues.put("event_date", eventDate);
            targetValues.put("event_date_label", eventDate == null ? null : eventDate.format(DATE_LABEL));
            // One source column, two target columns: the same TIME in two text patterns.
            targetValues.put("start_time_24h", formatTime(newValues.get("start_time"), TIME_24H, logger));
            targetValues.put("start_time_12h", formatTime(newValues.get("start_time"), TIME_12H, logger));
    
        // First word = first name, the rest = last name. A single word is a first name only.
        private static String[] splitName(Object value) {
            if (value == null) {
                return new String[] {null, null};
            }
            String text = value.toString().trim().replaceAll("\\s+", " ");
            if (text.isEmpty()) {
                return new String[] {null, null};
            }
            int cut = text.indexOf(' ');
            if (cut < 0) {
                return new String[] {text, null};
            }
            return new String[] {text.substring(0, cut), text.substring(cut + 1)};
        }

    The first word is the first name and the rest is the last name, so Luca de Angelis gives Luca and de Angelis, and Cher gives a first name only. Change the rule to match your data.

    Nulls and bad values

    Decide for every column what a null and a bad value should become, then write it down in the code. The UDF in this guide returns null for a null, and null plus a warning for a value it cannot read.

    Policy Code What happens

    Write null and keep going (used in this guide)

    logger.warn(…​) and return null; in parseDate

    The row is written with a NULL date. The entity stays Active, as the live check above shows.

    Stop on a bad value

    Replace those two lines of parseDate with throw new IllegalArgumentException("event_date_text '" + text + "' matches no accepted date format");

    The test fails with that message. At run time the entity goes to Error with Target write failed and Error invoking mapping function: java.lang.IllegalArgumentException: event_date_text 'tomorrow' matches no accepted date format, and the row is not written. After you save the corrected UDF, the entity resumes and writes the row.

    Time zones

    A TIME has no time zone, and a timestamp without time zone arrives as java.time.LocalDateTime, also with no zone: the UDF formats them as they are. A timestamp with time zone arrives as java.time.OffsetDateTime. To write it in a zone of your choice, convert it first. This expression was tested in the editor on a timestamptz source column:

    String rome = ((OffsetDateTime) newValues.get("logged_at"))
            .atZoneSameInstant(ZoneId.of("Europe/Rome")).toLocalDateTime().toString();

    With the sample value 2013-09-03T05:10:38.747+02:45, it returns 2013-09-03T04:25:38.747. Import java.time.OffsetDateTime and java.time.ZoneId.

    Rules the UDF follows

    Rule Why

    Keep the signature onChange(newValues, oldValues, operation, isSnapshot, Logger logger)

    Core Hub calls this exact method. Code written for the earlier signature, without isSnapshot, is refused when you compile.

    Read from newValues by source column name; return values by target column name

    The generated code does this. The test ignores a name that matches no target column, and at run time a column that the UDF does not put is written as NULL, so a typo leaves that column empty.

    Return a value for every target column

    A column that the UDF does not put is written as NULL. This includes the columns that have a field function: end_time_text was empty in the target until the UDF put it.

    Return a value of the type of the target column

    DATE takes java.time.LocalDate, and varchar takes String. The test refuses other types with Incorrect data type for column …​.

    For a column that has a field function, return the original source value

    The field function runs after the UDF and expects the source type. A String for end_time_text fails with class java.lang.String cannot be cast to class java.time.LocalTime.

    Handle nulls yourself

    A null source value arrives as null. Calling a method on it raises a NullPointerException.

    The types seen in this guide are java.lang.Integer for integer, java.lang.String for varchar, java.time.LocalTime for time, java.time.LocalDateTime for timestamp, and java.time.OffsetDateTime for timestamptz. To check any other column, return its class name in a text column and select Test. This probe was compiled and run in the editor; it shows java.time.LocalTime for start_time:

    targetValues.put("last_name", typeOf(newValues.get("start_time")));
    
    private static String typeOf(Object value) {
        return value == null ? "null" : value.getClass().getName();
    }
    Verify the result

    You have successfully converted the values with a UDF if:

    • Compile succeeds and Test shows the expected values for the four date spellings, the bad date, and the times;

    • the Error count in Field mapping is 0 and the entity shows the UDF and FF tags;

    • after the snapshot, the target has the six rows listed in the guide, with trimmed codes and HH:mm end times;

    • after the CDC changes, the new row, the updated row, and the deleted row are right in the target;

    • a bad date writes the row with a NULL date, and the entity stays Active.

    Troubleshooting

    Problem Possible cause What to do

    Compile failed: incompatible types: java.lang.Object cannot be converted to java.lang.String

    The maps hold Object values, and the code assigns one to a String without a conversion

    Use String.valueOf(value) or value.toString() after a null check. The message has no line number; search the code for the statement it describes

    Compile failed: must declare public Pair<…​> onChange(…​ boolean isSnapshot, Logger logger): add the isSnapshot parameter before the logger

    The method uses the older signature without isSnapshot

    Copy the signature from the generated code, with isSnapshot before logger

    Test failed: Incorrect data type for column event_date must be of type LOCAL_DATE, but we received a String

    The UDF returns text for a DATE column

    Parse the text to a java.time.LocalDate and return that

    Test failed: Incorrect data type for column last_name must be of type STRING, but we received a Integer (or LocalTime)

    The UDF returns a number or a time for a varchar column

    Convert it with String.valueOf(…​) or a DateTimeFormatter

    Test failed: class java.time.LocalTime cannot be cast to class java.lang.String

    The code casts a TIME value to String; it arrives as java.time.LocalTime

    Format it with a DateTimeFormatter, as formatTime does

    Test failed: Text '' could not be parsed at index 0

    A TIME sample value was left blank, and a blank is an empty string, not a null

    Type a time in the sample, and test nulls with real rows

    The test passes, but a target column is NULL

    The UDF did not put that column, or the key does not match a target column name. The test ignores unknown keys

    Compare the keys of targetValues with the target column names, letter for letter

    The entity shows Target write failed: class java.lang.String cannot be cast to class java.time.LocalTime

    The UDF returned a String for a column that has Convert Time to String, which runs after the UDF

    Put newValues.get("end_time") as it is, and let the field function format it

    The entity shows Target write failed: Error invoking mapping function: java.lang.IllegalArgumentException: …​

    The UDF throws on a bad value

    Fix the source value, or return null instead of throwing, then save the UDF: the entity resumes

    UDF is no longer valid — re-define the UDF, or UDF is invalid: Source or target columns have changed since the UDF was saved

    You checked or unchecked source fields, or changed target fields, after saving the UDF

    Open the UDF, adjust the code, compile, test, and save it again

    A timestamp comes out hours away from what you expect

    A timestamp with time zone arrives as OffsetDateTime, with the offset of the source

    Convert it with atZoneSameInstant(ZoneId.of("…​")) before formatting. A TIME and a timestamp carry no zone, so they are never shifted

    A date such as 01/02/2026 is read the wrong way round

    Two formats in DATE_INPUTS can read it, for example dd/MM/uuuu and MM/dd/uuuu; the first one that parses wins

    Put the format you expect first in DATE_INPUTS, or accept one format per source

    end_time_text shows 15:35:30 in the test but 15:35 in the target

    The test shows what the UDF returns; field functions apply when the entity runs

    Check the result in the target, as in the snapshot step

    Contact MOLO17 Support if you need assistance with UDFs.