User-Defined Fields in Veson Data Lake: Configuration, Querying, and Limitations

If you query IMOS data in Snowflake, your User-Defined Fields (UDFs) appear in Data Lake as columns you can report on. This article explains where to find them, which objects to query, why row counts differ between those objects, and what happens in Snowflake when you add, rename, or deactivate a UDF or clear its value in IMOS.


What a User-Defined Field is

A User-Defined Field, or UDF, is an extra field your administrators create in the IMOS Data Center against a particular kind of record: an extra reference on a Cargo, a counterparty code on an Address Book entry, a settlement note on a Time Charter contract. Each one is called a prompt. You can add as many as you need, at any time, without involving Veson.

IMOS stores prompts and their values separately from the records they hang off. Data Lake reassembles them. For every kind of record that supports UDFs, Data Lake publishes a companion object whose columns are your prompts and whose rows are your records. If your administrator adds a prompt called TRN No to the Address Book, a column called TRN No appears in AddressBookUserDefinedFields.

That is the whole idea: your configuration becomes the schema. It also explains most of the behavior described on this page, because a schema that follows your configuration has to cope with prompts being added, renamed, cleared and deactivated while queries are running against it.


Which records support UDFs

Ten record types are published today. Each object is built from the IMOS table that holds the record and is keyed on that record's business key rather than an internal row number, so the keys match what you see in IMOS.

Data Lake object

IMOS record

Source table

Business key

AddressBookUserDefinedFields

Address Book entry

cabv

ShortName, Type

BerthUserDefinedFields

Berth

berth

PortNo, ShortName

COAUserDefinedFields

COA / contract

gcontrct

CoaNo, ContractType

CargoUserDefinedFields

Cargo

gcargo

CargoID

CargoItineraryUserDefinedFields

Cargo itinerary line

tuitin

CargoID, SeqNo

CargoNameUserDefinedFields

Cargo name

cargo

ShortName

TimeCharterContractUserDefinedFields

Time Charter contract

tcform

TcCode

TimeCharterTcFormUserDefinedFields

Time Charter form

tcform

TcCode

VesselUserDefinedFields

Vessel

gvsl

VesselCode

VoyageUserDefinedFields

Voyage

voyage

VesselCode, VoyageNo

Not yet available

UDFs on Financial Invoices are configurable in IMOS and visible in Report Designer, but they are not published to Data Lake. This is a recognized gap with an open enhancement request. If you depend on invoice UDFs for month-end reporting, raise it with your Veson contact so it can be weighted against other demand.


Where UDFs live in Snowflake

Three objects can carry the same UDF name. They are built to different rules on purpose, and only one of them is the surface to build on.

Object

Status

What it holds

REPORTING."<X>UserDefinedFields"

Use this

Current values. One row per record that has at least one UDF value. Columns follow your prompts automatically.

REPORTING_HIST."hist_<X>UserDefinedFields"

Change history

Every revision of every record, including records with no UDF values at all, and including prompts that have since been deactivated.

TRANSACTIONAL."<X>UserDefinedFields"

Retiring

A legacy compatibility view kept during the earlier migration. Its column list was frozen at deployment, so prompts added since will never appear in it.

Action

If any of your queries, views or BI models still read TRANSACTIONAL."<X>UserDefinedFields", migrate them to REPORTING. The transactional views are scheduled for removal and may still be present in your environment in the meantime.


How prompts become columns

The column name is the prompt name

Column names are taken verbatim from the prompt text in IMOS, including spaces and case. Quote them in SQL: SELECT "TRN No" FROM .... Environments configured for Quoted Identifiers Ignore Casing behave as you would expect for case.

Duplicate prompt names are disambiguated by field ID

IMOS permits two prompts on the same record type to share a name, most often when an old prompt has been deactivated and a new one created with the same label. A single column cannot serve both, so where names collide the column is prefixed with the prompt's internal field ID, giving column names of the form 23:NOMINATION and 89:NOMINATION.

If you see a prefixed column where you expected a plain one, it means a second prompt with that name exists on that record type. Checking the Data Center for a deactivated duplicate is usually the quickest way to work out which is which.

Deactivated prompts

Deactivating a prompt in IMOS does not erase the values already recorded against it. The history object keeps deactivated prompts as columns so past values stay readable, while the live REPORTING object reflects your current configuration.


Adding, renaming and removing prompts

You do not raise a request with Veson to add a UDF. Prompts are yours to create in IMOS, and Data Lake picks them up on its own.

Action

What happens in Data Lake

You add a prompt

A new column appears on the relevant objects at the next ingestion cycle. No deployment, no ticket, no downtime.

You rename a prompt

The column is renamed to match at the next cycle. Update any query or BI model that referenced the old name, as it will no longer resolve.

You deactivate a prompt

The column is not dropped. Historic values stay queryable rather than disappearing from under existing reports.

You change a value

The current value updates in REPORTING, and a new revision is written to the history object recording the full row as it stood at the time of the change.

UDF objects refresh on the reporting cadence, which is a 30-minute cycle. On environments where transactional data is delivered on a faster chain, the UDF objects still follow the 30-minute reporting cycle rather than the faster one.

Practical consequence

A prompt added in IMOS a few minutes ago will not be in Snowflake yet. Allow a full cycle before treating an absent column as a fault.


Why row counts differ

The most common question about UDFs is why the history object returns more rows than the live one. This is expected, and it follows directly from the two objects answering different questions.

  IMOS source
  ───────────────────────────────────────────────────
   cabv              userdata            userfield
   (records)         (values)            (prompt names)
      │                  │                    │
      └──────────────────┴────────────────────┘
                         │
          ┌──────────────┴───────────────┐
          │                              │
     INNER JOIN                     LEFT JOIN
  "must have a value"           "keep every record"
          ▼                              ▼
  ┌───────────────────────┐   ┌────────────────────────────┐
  │ REPORTING.            │   │ REPORTING_HIST.            │
  │ AddressBookUser...    │   │ hist_AddressBookUser...    │
  │                       │   │                            │
  │        9,879 rows     │   │        10,256 rows         │
  └───────────────────────┘   └────────────────────────────┘
   records that have             every record, values or not
   a value                        = 9,879 with values
                                  +   377 with none yet

Example figures from a production Address Book. The difference is not missing or duplicated data: it is exactly the number of address book entries that currently carry no UDF value at all.

REPORTING uses an inner join. A record with no UDF values has nothing to show, so it does not appear. REPORTING_HIST uses a left join, so every record gets a row whether it has values or not.

That asymmetry is deliberate. History has to be able to record a field going from populated to empty. If empty records were dropped, clearing a value would look identical to deleting the record, and the change you are watching for would be invisible.

  Time ──────────────────────────────────────────────►
  Record "ACME SHIPPING, GB"
    rev 1   TRN No = "GB123456"      ← recorded
    rev 2   TRN No = (cleared)       ← still recorded, because
                                       empty rows are kept

Rule of thumb

Do not expect REPORTING and REPORTING_HIST row counts to match. By design they will not. Compare each against the source record count instead, as shown below.

A comparison that does mean something

Read both sides in a single statement. Environments ingest every 30 minutes, so counts taken as separate queries can be read moments apart and drift by a row or two on their own.

-- Substitute your own database name for <DB>
SELECT
    base_rows,
    base_without_udf,
    reporting_rows,
    reporting_rows - (base_rows - base_without_udf) AS residual
FROM (
  SELECT
    (SELECT COUNT(*) FROM <DB>.TRANSACTIONAL."cabv") AS base_rows,
    (SELECT COUNT(*) FROM <DB>.TRANSACTIONAL."cabv" b
      WHERE NOT EXISTS (SELECT 1 FROM <DB>.TRANSACTIONAL."userdata" u
                         WHERE u."objID"   = b."_sqlid"
                           AND u."objType" = 'cabv')) AS base_without_udf,
    (SELECT COUNT(*) FROM <DB>.REPORTING."AddressBookUserDefinedFields") AS reporting_rows
);

Expect residual = 0. That is the check that matters.

A residual of zero confirms the live object holds every record that has at least one value. The base_without_udf figure is the legitimate gap between the live object and history, and it is the 377 in the diagram above.


Querying UDFs

Current values

Join the UDF object back to the main reporting object on the business key.

SELECT
    ab."ShortName",
    ab."FullName",
    udf."TRN No",
    udf."Credit Terms"
FROM REPORTING."AddressBook" ab
JOIN REPORTING."AddressBookUserDefinedFields" udf
  ON udf."ShortName" = ab."ShortName"
 AND udf."Type"      = ab."Type";

The latest state, including records with no values

Take the highest revision per record from the history object. This is also how you pick up records that have never had a UDF value.

SELECT *
FROM REPORTING_HIST."hist_AddressBookUserDefinedFields"
QUALIFY ROW_NUMBER() OVER (PARTITION BY "_ID"
                            ORDER BY "_REVISION" DESC) = 1;

What changed, and when

Every revision is a full row, not a delta: when one prompt value changes, the whole row is recorded as it stood at that moment. Comparing consecutive revisions therefore tells you exactly which prompt moved.

-- Revisions for one address book entry, newest first
SELECT "_REVISION", "TRN No", "Credit Terms"
FROM REPORTING_HIST."hist_AddressBookUserDefinedFields"
WHERE "_ID":"ShortName"::VARCHAR = 'ACME SHIPPING'
  AND "_ID":"Type"::VARCHAR      = 'V'
ORDER BY "_REVISION" DESC;

A cleared value appears as an empty revision, not as a missing row.


History columns

Alongside your prompt columns, every history object carries a small set of control columns.

Column

Meaning

"_ID"

The record's business key, held as a JSON object. Read one component with "_ID":"ShortName"::VARCHAR. Records are identified by business key, not by an internal row number.

"_BASE_ID"

The internal row identifier of the source record, for tracing back to the transactional layer.

"_REVISION"

Increments with each recorded change. The highest revision for an "_ID" is its current state.

"_ACTIVE"

Intended to mark a record as removed. Read the first limitation below before relying on it.


Current limitations

Three behaviors are worth knowing before you build change detection on the history objects. All three affect history only. The live REPORTING objects are unaffected.

Deleted records are not yet marked inactive

When a record is deleted in IMOS, its last revision stays in the history object still flagged "_ACTIVE" = 1. In practice a WHERE "_ACTIVE" = 1 filter currently excludes nothing, and history counts drift gradually upward as records are deleted over time. Volumes are small: a recent production measurement found single figures to low tens of retained rows per object. A fix is in progress and the existing rows will be corrected when it lands.

Renaming a business key creates a new identity

Because records are identified by their business key, renaming that key in IMOS, for example changing an Address Book short name, produces a new "_ID" starting again at revision 1. The revision trail under the old key is left in place rather than carried across. Treat a business key rename as a new record for change-detection purposes.

Configured default values are not exposed

Where a prompt is configured with a default value in IMOS, the default itself is not published to Data Lake. Values actually recorded against a record are published as normal. Exposing configured defaults is a recognized request and is not yet scheduled.


Requesting a change

Situation

What to do

New prompt

No request needed. Create it in the IMOS Data Center and the column appears within one 30-minute cycle.

Column missing

Confirm the prompt exists and is active in the Data Center, allow one full cycle, and check you are querying REPORTING rather than the retiring transactional view. If it is still absent, raise it through the Veson Help Center with the record type and prompt name.

New record type

Publishing UDFs for a record type not listed above, Financial Invoice being the live example, is a product enhancement. Raise it with your Client Success contact so it can be prioritized against other demand.

Access

UDF objects, live and historic, are granted to the CLIENT_ADMIN role. If a query returns an insufficient privileges error, check the role you are connected as before raising a ticket.