Class McpDates
The generated MCP server used to carry this itself, as
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss") inlined in two places and a comment asking
whoever edited one to remember the other. That arrangement had five separate defects and only the
first of them was visible:
1990-01-01was REFUSED, though the tool schema said "ISO-8601 string" and that is ISO-8601. It is also the form a model reaches for when a column is calledHIRE_DATE, so it was the one people hit first.- A trailing zone was SILENTLY DROPPED.
SimpleDateFormat.parse(String)stops at the end of its pattern and ignores whatever follows, so...T22:44:00+05:30and...T22:44:00Zboth parsed as local time — no error, wrong instant, no signal. - Fractional seconds went the same way, which costs a DATE nothing and costs a TIMESTAMP real data.
- Parsing was LENIENT, so
2003-13-45came back as2004-02-14rather than as an error: confident nonsense. - Nothing carried a zone at all, so an accepted string meant different instants on different servers, and — because daylight saving moves — on different dates on the same server.
Items 2 to 4 were the dangerous ones. Item 1 failed closed; those three produced a plausible wrong answer and told nobody.
What is accepted now
A date, optionally a time, optionally a zone offset:
1990-01-01 midnight, server zone 1990-01-01T09:30 seconds optional 1990-01-01T09:30:00 1990-01-01T09:30:00.500 fractional seconds kept 1990-01-01T09:30:00Z honoured, not ignored 1990-01-01T09:30:00+05:30 honoured, not ignored
Anything else is REFUSED with a message naming the accepted forms rather than merely echoing the input. That matters more than politeness: an agent given "Unparseable date" retries blind, and a retry loop against a failing tool churns pooled connections.
An offset is converted, not recorded. An Oracle DATE has no zone to store one in, so
09:30Z becomes whatever 09:30 UTC is on the server's clock. That is the correct reading of
the input and it is a real change from the old behaviour, which kept the wall clock and threw the
offset away.
Where there is no offset the server's own zone is used. That is not ideal — it is what an Oracle DATE can represent, and inventing UTC instead would silently shift every value that works today.
This lives in pub rather than in emitted source for two reasons: it can be unit-tested
here and cannot be tested there, and the generated Jackson configuration can reference
ISO_PATTERN instead of repeating the literal — a library constant is not the illegal
forward reference that stopped the emitted constant being shared.
Copyright 2003-2026 ATB Consultancy Services Ltd
(formerly Orinda Software Ltd, Dublin, Ireland)
- Since:
- 2.0.0
- Author:
- [email protected]
-
Field Summary
Fields -
Method Summary
Modifier and TypeMethodDescriptionstatic StringRender a date the way every generated server always has.static StringRender whichever of the two this actually is.static StringformatTimestamp(Timestamp theTimestamp) Render a timestamp, keeping the fractional seconds aDATEdoes not have.static StringfromOracleTimestampText(String theOracleText) The reverse: what Oracle handed back, as the ISO text MCP crosses.static DateRead one of the accepted forms.static TimestampparseSqlTimestamp(Object theValue) Read aTimestampfrom either the JDBC escape form"yyyy-mm-dd hh:mm:ss[.f...]"or one of the ISO formsparse(java.lang.Object)accepts.static TimestampparseTimestamp(Object theValue) The same asparse(java.lang.Object), as aTimestamp.static StringtoOracleDateText(String theIsoText) ISO text to the form the Oracle DATE mask matches, for a DATE index-by element crossing MCP.static StringtoOracleTimestampText(String theIsoText) ISO text to the form an Oracle format mask matches, for a zoned timestamp crossing MCP.static StringtoOracleUnzonedTimestampText(String theIsoText) ISO text to the form the unzoned Oracle TIMESTAMP mask matches.
-
Field Details
-
ISO_PATTERN
How a DATE is rendered on the way out, and what Jackson is configured with.Deliberately unchanged from what generated servers have always emitted: altering it would change every date field every existing client reads. Fractional seconds are added on the way out only for a
Timestamp, which is the type that actually carries them — seeformatTimestamp(java.sql.Timestamp).- See Also:
-
ISO_PATTERN_MILLIS
With fractional seconds, for the type that has them.- See Also:
-
-
Method Details
-
format
-
formatTimestamp
-
formatAny
Render whichever of the two this actually is.The generated code reaches every date value through
instanceof java.util.Date, andTimestampextendsjava.util.Date— so without this dispatch a timestamp renders through the DATE pattern and drops the fractional seconds that are the only reason the type differs. Sending it here rather than wideningISO_PATTERNkeeps a plain DATE rendering byte-for-byte as it always has.- Parameters:
theDate- the value, or null- Returns:
- the ISO-8601 text, with milliseconds only for a
Timestamp
-
toOracleTimestampText
ISO text to the form an Oracle format mask matches, for a zoned timestamp crossing MCP.Why this exists rather than a reformat. MCP speaks ISO-8601, where a
Tseparates the date from the time. The generated PL/SQL converts a zoned index-by element with'yyyy-mm-dd hh24:mi:ss.ff9 TZR', which has a SPACE there — measured, an ISOTagainst that mask raises ORA-01858. The separator before the ZONE is flexible (with or without a space, offset or region name, all accepted), so this is the only difference that matters.It is a TEXTUAL swap, deliberately, and not a parse-and-reformat. Parsing would yield an instant and lose the caller's zone, so rendering it again would substitute the server's — silently turning
+05:30into whatever the server runs on, which is the exact defect this whole area was fixed for.- Parameters:
theIsoText- the caller's value, or null- Returns:
- the same value with the date/time separator Oracle's mask expects
-
fromOracleTimestampText
-
toOracleDateText
ISO text to the form the Oracle DATE mask matches, for a DATE index-by element crossing MCP.Two differences from
toOracleTimestampText(java.lang.String), both MEASURED against 12c rather than reasoned about — which is the rule this area exists under, every wrong claim in its history having come from reasoning about a format that oneTO_DATEcall would have settled:- the
Tbecomes a space, exactly as it does for a zoned timestamp; and - any fractional seconds are REMOVED.
PlsqlIndexByTable2.ORACLE_DATE_TO_CHAR_MASKis'yyyy-mm-dd hh24:mi:ss'with noFFelement, andTO_DATE('2019-03-01 14:25:36.123', ...)raises ORA-01830.
A bare
'2019-03-01'is accepted by that mask and means midnight, so a date-only value needs no padding and gets none.Dropping the fraction loses precision, and that is deliberate. An Oracle DATE has no sub-second component to store it in, so the alternative is not fidelity but ORA-01830 — and the SCALAR date path already drops it silently, by binding a
Datethat Oracle truncates on arrival. Refusing here would make an index-by DATE stricter than a plain DATE parameter beside it, for nothing. The emitted tool description says so out loud, because an accepted spelling that a caller cannot discover is precisely the defect a live MCP session reported against this area once already.Nothing else is touched. A value carrying a zone, or one that is not a timestamp at all, is passed through to be refused by Oracle in Oracle's own words rather than quietly reshaped into something that parses — the same division of labour
PlsqlIndexByTable2.ensureFractionalSeconds()keeps.- Parameters:
theIsoText- the caller's value, or null- Returns:
- the same value in the form the DATE mask accepts
- Since:
- 2.0.6
- the
-
toOracleUnzonedTimestampText
ISO text to the form the unzoned Oracle TIMESTAMP mask matches.The
Tbecomes a space, and a fraction longer than 8 digits is truncated. Measured on 12c against'yyyy-mm-dd hh24:mi:ss.ff8': a MISSING fraction is accepted, one to eight digits are accepted, and nine raises ORA-01830.That first point is why there is no counterpart to
PlsqlIndexByTable2.ensureFractionalSeconds()here. That method exists because the ZONED mask stops tolerating a missing fraction once it names a zone; the unzoned mask never stopped, so padding would be work with no effect.Truncating rather than refusing, because a caller sending a ninth digit is already sending more precision than the column will keep: Oracle's TIMESTAMP tops out at 9 and DEFAULTS to 6, and rounds
.12345678to.123457on arrival whatever we hand it.- Parameters:
theIsoText- the caller's value, or null- Returns:
- the same value in the form the unzoned TIMESTAMP mask accepts
- Since:
- 2.0.6
-
parse
Read one of the accepted forms.- Parameters:
theValue- the caller's value;toStringis used, so a JSON string arrives here as itself- Returns:
- the parsed value, or null for a null input
- Throws:
IllegalArgumentException- naming the accepted forms, when the text is not one of them
-
parseTimestamp
The same asparse(java.lang.Object), as aTimestamp.- Parameters:
theValue- the caller's value, or null- Returns:
- the parsed value, or null for a null input
-
parseSqlTimestamp
Read aTimestampfrom either the JDBC escape form"yyyy-mm-dd hh:mm:ss[.f...]"or one of the ISO formsparse(java.lang.Object)accepts.This exists because a generated table's TIMESTAMP column crosses MCP as the FIRST of those and a PL/SQL TIMESTAMP parameter crosses as the second, and one surface -- a table's index-lookup tool -- has to take a value the caller most likely read back off the other table tools. A generated row exposes such a column through
set<Col>(String)/get<Col>String(), both of which speakjava.sql.Timestamp's owntoString/valueOfform, so that is what aget_by_pkresult carries and what aninsertaccepts. Refusing it in the lookup tool alone would mean a value this server had just emitted was not a value it would take back.The JDBC form is read with
Timestamp.valueOf(java.lang.String), not throughparse(java.lang.Object):parsereturns aDateand would truncate to milliseconds, while an OracleTIMESTAMP(6)carries microseconds andvalueOfkeeps all nine digits of the nanosecond field.- Parameters:
theValue- the caller's value, or null- Returns:
- the parsed value, or null for a null (or empty) input
- Throws:
IllegalArgumentException- naming the accepted ISO forms, when the text is neither
-