Why does Ignition runNamedQuery throw a ClassCastException?

Stefan Weidner13 min read
B&R AutomationOther TopicTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Where does the request stop when runNamedQuery throws a ClassCastException?

Follow the packet. A step lookup in Vision crosses five hops, and each one has its own failure signature. The script reads a tag, hands a parameter object to system.db.runNamedQuery, the gateway binds the values into the named query, JDBC carries the SELECT to PostgreSQL, and a dataset comes back to the client. Identify the hop where the request dies before changing any code.

Hop Component What happens here Failure signature at this hop
1 Script (Designer script console or Vision client) system.tag.readBlocking returns a list of QualifiedValue objects AttributeError: 'java.util.ArrayList' object has no attribute 'value'
2 Client scripting layer (PyArgumentMap) Coerces the parameter argument to java.util.Map java.lang.ClassCastException: Cannot coerce value 'set([...])' into type: interface java.util.Map
3 Gateway named query execution Matches map keys to declared query parameters by name No error; unmatched parameter leaves the WHERE clause matching nothing
4 JDBC driver to PostgreSQL Executes the SELECT and maps column types to Java types Numeric and padded character columns arrive as types the script or expression does not expect
5 Result returned to client Dataset converted to PyDataSet <PyDataset rows:0 cols:12>

Read the traceback bottom-up. The frames run ClientDBUtilities.runNamedQuery into PyArgumentMap.interpretPyArgs, then PyArgumentMap.coerce, then TypeUtilities.coerce. The exception fires inside argument interpretation on the client. The request never left the client, so the gateway, the database connection, and the query text are not suspects yet. Local layer first: fix hops 1 and 2, then move outward.

Check 1: What does the tag read actually hand the script?

Take the reading by printing the raw return of the tag read before it goes anywhere near the query.

result = system.tag.readBlocking(["[default]MLWinTags/localVars/currentStepNumber"])
print result
print type(result)

In Ignition 8.1, system.tag.readBlocking is the recommended read function; system.tag.read is the legacy call. readBlocking expects a list of paths, even for a single tag, and returns a list with one QualifiedValue per path. A QualifiedValue prints as three fields:

Printed field Example from this lookup Meaning
Value 20 The step number the query needs
Quality Good Tag quality code
Timestamp Fri Dec 27 21:23:36 EST 2024 (1735352616123) Last value change time

Branch on the output:

Output Meaning Next action
[[20, Good, ...]] List containing one QualifiedValue Index the list, then take .value: result[0].value
AttributeError: 'java.util.ArrayList' object has no attribute 'value' .value applied to the list instead of its element Insert the [0] index before .value
20 Bare value extracted Proceed to Check 2

Appending .value to the tag path string (currentStepNumber.value) reads the tag's value property, but readBlocking still wraps the result in a QualifiedValue inside a list. The path suffix does not unwrap anything. The working form is:

qv = system.tag.readBlocking(["[default]MLWinTags/localVars/currentStepNumber"])[0]
if not qv.quality.isGood():
    raise ValueError("Step number tag quality: %s" % qv.quality)
stepNumber = qv.value
print stepNumber   # expect 20, not [20, Good, ...]

Check 2: Is the parameter argument a dict or a set?

The exception text names the object it received: set([[20, Good, ...]]). That is a Python set containing a QualifiedValue list. The call was written as:

system.db.runNamedQuery('GetNextStep', {mystepnumber})

In Jython, braces without colons build a set literal, not a dictionary. The client scripting layer tries to coerce the second argument to java.util.Map, a set has no key/value structure, and the coercion throws. Two near-miss forms produce the same failure class:

Literal written Jython type Result
{mystepnumber} set ClassCastException to java.util.Map
{'mystepnumber', mystepnumber} set (comma, not colon) ClassCastException to java.util.Map
{"mystepnumber": mystepnumber} dict Coerces to Map; request proceeds to the gateway

Take the reading with print type(params) immediately before the call. The output must be <type 'dict'>. If it is a set, the colon is missing. Once the argument is a dict, the ClassCastException disappears and the request reaches hop 3.

Check 3: Do the dictionary keys match the named query parameter names?

After fixing Checks 1 and 2, the call ran cleanly and printed <PyDataset rows:0 cols:12>. Read that result carefully. Twelve columns means the query executed on the database and returned the table's schema. Zero rows means the WHERE clause matched nothing. The fault is now at hop 3.

The named query text was:

SELECT * FROM tasks WHERE stepnumber = :myCurrStepNum

The script passed {"mystepnumber": mystepnumber}. The gateway looks up each declared query parameter by its exact name in the map. Named query parameter names are case-sensitive, and mystepnumber is a different string from myCurrStepNum. The extra key is ignored, the declared parameter myCurrStepNum receives no value, and a comparison of stepnumber against a missing (null) value matches no rows in SQL. No error is raised anywhere on the path.

The same mechanism explains two other symptoms seen on this lookup:

  • Calling the query with no parameter at all ran without error and returned an empty dataset.
  • A Vision propertyChange script that fell back to a default string when dataset.rowCount was zero displayed only Unknown Step in the runtime, because its dictionary also used the key mystepnumber.

Fix it by copying the parameter name from the named query's parameter table into the script, character for character:

params = {"myCurrStepNum": stepNumber}
ds = system.db.runNamedQuery("GetNextStep", params)
print ds.getRowCount(), ds.getColumnCount()   # expect 1 and 12

If the row count is still zero with a matching key, move to Check 4. If the script runs in gateway scope rather than a client or Designer, system.db.runNamedQuery also needs the project name as its first argument; the traceback here shows the client scope function, so the two-argument form applies.

Check 4: Does the parameter type match the database column type?

The PostgreSQL column definition for tasks shows stepnumber as numeric, with a taskid column alongside it. Two problems follow from that schema.

Type crossing JDBC. PostgreSQL numeric is an arbitrary-precision decimal. It arrives in Ignition as a Java BigDecimal, not an integer, so it does not come through JDBC cleanly. Outbound, declare the named query parameter myCurrStepNum with an integer data type in the Named Query editor so the gateway binds a whole number; PostgreSQL compares an integer against a numeric column without trouble. Inbound, any stepnumber value read back from the dataset is a BigDecimal. Wrap it in int() before arithmetic or equality tests in script, or change the column to an integer type if it only ever holds whole step numbers.

Row identity. A table with both taskid and stepnumber normally repeats step numbers across tasks. Filtering on stepnumber alone returns every task's step 20, and getValueAt(0, ...) silently picks whichever row the database returns first. The returned row for this lookup began with the values 6 and 20, which is exactly the pair the WHERE clause should pin down:

SELECT *
FROM tasks
WHERE taskid = :myTaskId
  AND stepnumber = :myCurrStepNum

Declare myTaskId as a second parameter in the named query and add it to the dict with the identical spelling. The parameter name myTaskId is a placeholder; use whatever name you declare.

Reading Outcome Next
getRowCount() = 0 with matching keys Type or value mismatch in WHERE Run the same SELECT with literal values in the Designer database browser
getRowCount() > 1 WHERE does not identify a unique step Add taskid to the filter
getRowCount() = 1 Lookup resolved Check 5: decide where the query should run

Check 5: Should this query run in a script or a Named Query binding?

In Vision, component event scripts execute on the client's UI thread. system.db.runNamedQuery called from a script is a synchronous round trip: client to gateway, gateway to database, and back. The window cannot repaint or accept input until that trip completes, so every slow query shows up as a frozen screen. Perspective does not share this constraint; Vision does.

Attribute Scripted runNamedQuery in propertyChange Named Query binding on a custom property
Thread Blocks the UI until the result returns Runs asynchronously; UI stays responsive
Designer design mode Does not run Runs
Designer preview mode Runs only when the bound tag changes value Runs
Client runtime Runs on each property change Runs on each parameter change
Empty-result handling if dataset.rowCount: guard in script try() in downstream expressions

Resolve with a binding. Configure it on the component or root container:

  1. Right-click the component, open Customizers > Custom Properties, and add myStepNumber as an Integer.
  2. Bind myStepNumber with a direct Tag binding to [default]MLWinTags/localVars/currentStepNumber.
  3. Add a second custom property of type Dataset (for example stepData) and give it a Named Query binding to GetNextStep.
  4. In the binding's parameter table, point myCurrStepNum at the myStepNumber property reference, and myTaskId at its source if you added it.
  5. Bind display fields to stepData with expressions wrapped in try(), so an empty dataset shows a fallback instead of an error overlay:
try({Root Container.stepData}[0, "safety"], "Unknown Step")

If a script is unavoidable, keep it in propertyChange, filter on the property name, and keep the work minimal:

if event.propertyName == 'myStepNumber' and event.newValue is not None:
    params = {"myCurrStepNum": event.newValue}   # exact, case-sensitive name
    dataset = system.db.runNamedQuery("GetNextStep", params)
    if dataset.rowCount:
        event.source.text = dataset.getValueAt(0, 'safety')
    else:
        event.source.text = 'Unknown Step'

If a binding shows intermittent overlays, diagnose the binding (quality of the parameter source, query errors in the gateway log) rather than moving the query into a blocking script. The script trades a visible overlay for a frozen window.

How do I pull individual columns out of the returned row?

A one-row result needs no loop. Convert once, then address columns by name. Column names come from the database schema; print them before writing any lookups, because a misspelled column name raises an error while a misspelled parameter name does not.

pds = system.dataset.toPyDataSet(ds)
print pds.getColumnNames()

if len(pds) == 1:
    row = pds[0]
    taskDescription = row["task_description"]
    safety          = row["safety"]
    packType        = row["pack_type"]
    stepNum         = int(row["stepnumber"])   # numeric arrives as BigDecimal

    # Or capture every column in one dict
    stepRow = dict((c, row[c]) for c in pds.getColumnNames())
    print stepRow
else:
    print "Expected 1 row, got", len(pds)

For the returned step, task_description held the inspection instruction text, safety held the PPE requirement, and pack_type held both. The same values are reachable without conversion through ds.getValueAt(0, "safety") on the raw dataset, and in expressions through {Root Container.stepData}[0, "safety"].

Do not fan the row out into tags. Tags are shared across every client and add a write/read round trip through the gateway for each field. Hold intermediate values in window or component custom properties, and write to tags or the database only when the operator saves or presses Next.

Why does a scanned part number not equal the database value?

The validation expression compared a trimmed operator input against a root container property populated from the row:

((trim({[default]MLWinTags/localVars/input_string.value})) = {Root Container.partnum})

The scan read 123A4B, the task called for 123A4B, and the expression still returned false. When two strings display identically but compare unequal, the difference lives in characters or types you cannot see. The usual causes:

Cause Mechanism Fix
Fixed-width character column PostgreSQL pads fixed-length character types with trailing spaces; trim() was applied only to the input side Trim both sides, change the column to a variable-length type, or trim in the SELECT
Scanner suffix Scanners commonly append carriage return, line feed, or tab to each read trim() on the input, or strip the suffix in the scanner configuration
Type mismatch Property holds a non-string type from JDBC (numeric, for example) and the comparison is not string-to-string Match the custom property type to the column; cast explicitly
Case difference String equality is case-sensitive Normalize with upper() on both sides if case is not meaningful

Take the reading instead of guessing. Open the one-row result in the dataset property editor, copy it to the clipboard, and paste it into a plain text editor. The paste exposes the data type of every column as it came through JDBC, and trailing padding becomes visible. In script, print repr(value) and len(value) for both strings; a length difference on identical-looking text confirms padding or a hidden suffix. The symmetric version of the expression, with the input held in a component property instead of a tag:

trim({Root Container.scanInput.text}) = trim({Root Container.partnum})

What makes the step lookup feel laggy?

Running the query from a script does not make the database faster. It only moves the wait onto the UI thread. Measure where the time goes, hop by hop:

  1. Run the exact SELECT, with literal values for taskid and stepnumber, in the Designer's database query browser. If it is slow there, the delay is in the database, not in Vision.
  2. Check for an index covering the WHERE columns. Without one, PostgreSQL scans the whole tasks table on every step change. An index on (taskid, stepnumber) serves the two-column lookup directly.
  3. If the browser query is fast but the screen lags, confirm the lookup runs as an asynchronous Named Query binding (Check 5), and that the display is not chained through tag writes that each add a gateway round trip.
CREATE INDEX IF NOT EXISTS tasks_taskid_stepnumber_idx
    ON tasks (taskid, stepnumber);

A single-row indexed lookup returns faster than an operator can move between screens, which removes the reason to prefetch the next step before it is needed.

How do I wire the step sequence and verify it end to end?

The target flow: on Next, write out the captures for the current step, advance the step number, and let the binding fetch the new row. With the lookup indexed and bound, the step number is the only state the button must change.

  1. Keep myStepNumber as the single source of the current step, bound to the step tag or held as a window property.
  2. Keep stepData bound to GetNextStep with myCurrStepNum (and myTaskId) pointed at window properties.
  3. Hold operator input and scan results in component properties on the window, not in tags.
  4. In the Next button's actionPerformed script, validate the scan against partnum; on pass, write the capture record for the current step with a named query or system.db insert, then increment the step number.
  5. Let the binding react to the new step number; the display expressions refresh from stepData without further script.
# Next button actionPerformed (Vision)
root = event.source.parent
scan = (root.getComponent('scanInput').text or '').strip()
part = (root.partnum or '').strip()

if scan != part:
    system.gui.messageBox("Wrong part for this step")
else:
    # write capture for current step here (insert named query), then advance
    system.tag.writeBlocking(
        ["[default]MLWinTags/localVars/currentStepNumber"],
        [int(root.myStepNumber) + 1])

Verify each hop in order before trusting the screen:

  1. Script console: print system.tag.readBlocking(["[default]MLWinTags/localVars/currentStepNumber"])[0].value returns a bare integer such as 20, not a bracketed QualifiedValue.
  2. print type(params) returns <type 'dict'>, and every key matches a named query parameter name exactly, including case.
  3. runNamedQuery returns a dataset with row count 1 and column count 12 for a known task and step.
  4. The dataset property editor paste shows the expected type for stepnumber and no trailing padding on the part number column.
  5. In preview mode, change the step tag and confirm stepData and the display fields update without the window freezing.
  6. Scan the correct part: the comparison returns true and Next advances the step. Scan a wrong part: the comparison returns false, no capture is written, and the step number does not change.

FAQ

Does system.tag.readBlocking need a list even for one tag?

Yes. Pass the path inside brackets and index the result, because it returns a list of QualifiedValue objects: system.tag.readBlocking([path])[0].value. Calling .value on the list raises AttributeError: 'java.util.ArrayList' object has no attribute 'value'.

Can I pass a QualifiedValue straight into runNamedQuery parameters?

No. Extract the bare value first; a QualifiedValue carries value, quality, and timestamp, and wrapping it in braces without a colon produces a set, which fails coercion to java.util.Map. Build the parameters as a dict such as {"myCurrStepNum": qv.value}.

Does a misspelled named query parameter raise an error?

No. The query executes with the declared parameter unset and returns zero rows with the full column set, for example <PyDataset rows:0 cols:12>. Parameter names are case-sensitive, so mystepnumber does not satisfy :myCurrStepNum.

Back to blog