Values
Andromeda keeps what you write. Numbers keep their exact value, times keep every microsecond, unit codes stay the codes you sent, and Relationship targets stay spelled the way you spelled them. A few kinds of value do come back in a different spelling, always one canonical form for each. This page covers which ones, and why the canonical form loses nothing.
Numbers
Every JSON number you write keeps its exact decimal value. It never passes through a binary floating-point number, not on the way into storage, in a q comparison, in a temporal aggregation, in a notification or in the temporal history, and not on the way back out. Two numbers that differ only in their twentieth digit stay different.
What is kept is the value, not the way you wrote it. Equivalent spellings come back in one form.
| You write | You get back |
|---|---|
1.0 | 1 |
1e2 | 100 |
-0 | 0 |
0.1 | 0.1 |
0.0000001 | 1E-7 |
So 1.0 doesn't come back as 1.0. A number carries its value, and its trailing zeros aren't part of it. Numbers inside JSON that Andromeda carries as data, such as a JsonProperty json or an @json literal, are spelled the same way.
A number can have up to 131,072 digits before the decimal point and 16,383 after it. A larger one gets 400 BadRequestData, and the error points at it with a JSON Pointer. Zero is accepted in any spelling, whatever its exponent.
A few numbers aren't attribute values, and Andromeda holds them differently.
- Geometry coordinates and
bboxnumbers are double-precision floats. Each comes back as the shortest decimal that reads back as its double, spelled like every other number. So13.0becomes13,14.550000000000000001becomes14.55, and13.123456789012345678becomes13.123456789012346. Precision explains why that loses nothing measurable. GeoJSON written inside a plainPropertyvalue isn't a geometry, and its numbers are kept like any other. - A subscription's
timeIntervalandthrottlingare double-precision floats too. - Counts, page sizes and similar parameters are integers.
Declared numbers and Booleans
A string's type is its declared datatype when it has one, and its JSON type when it has none. The Property's valueType decides first, then the literal's own @type.
A string declared as xsd:decimal, xsd:float, xsd:double or one of the integer datatypes derived from xsd:decimal is a number. Those integer datatypes are xsd:integer, xsd:long, xsd:int, xsd:short, xsd:byte, xsd:nonNegativeInteger, xsd:positiveInteger, xsd:nonPositiveInteger, xsd:negativeInteger, xsd:unsignedLong, xsd:unsignedInt, xsd:unsignedShort and xsd:unsignedByte. A string declared as xsd:boolean is a Boolean. Either way the string has to be a valid value of its datatype, or the write gets 400 BadRequestData.
A declared value is still a typed literal. It comes back as a string with its @type, or beside its valueType, never as a bare JSON number or boolean. Its spelling is the canonical one of its datatype.
| Declared as | You write | You get back |
|---|---|---|
xsd:integer | "01" | "1" |
xsd:decimal | "+1.50" | "1.5" |
xsd:decimal | "1.0" | "1" |
xsd:double | "1.0E2" | "100" |
xsd:double | "0.0000001" | "1E-7" |
xsd:double | "INF", "-INF", "NaN" | "INF", "-INF", "NaN" |
xsd:boolean | "1" | "true" |
A decimal or an integer comes back in plain decimal notation, and a float or a double the way every other number does (see Numbers). Writing the same value in another spelling is therefore not a change. A literal declared with any other XSD datatype keeps the text you wrote.
Declared values in q, orderBy and aggregations
In a q term, a declared number compares with number operands and a declared Boolean with Boolean ones, never with a quoted string. With "reading": {"@value": "4", "@type": "xsd:integer"}, reading==4 and reading>3 match, and reading=="4" doesn't.
INFis greater than every number and-INFless, so>3matchesINFand<3matches-INF, and neither equals any operand.NaNfails==and every ordering, and passes!=.orderBysorts declared numbers among the numbers, with-INFfirst, thenINF, thenNaN.- The temporal aggregations
sum,avg,min,max,stddevandsumsqcount declared numbers and skipINF,-INFandNaN, because their result is a JSON number.
A Property holding several values, a ListProperty valueList and a sub-attribute holding several values are compared one value at a time, each with its own type. The valueType declares every one of them, and it comes before a value's own @type. With "reading": {"type": "Property", "value": ["4", "7"], "valueType": "xsd:integer"}, reading==4 matches, and reading!=4 and reading=="4" don't. A reading holding [{"@value": "4", "@type": "xsd:integer"}, {"@value": "7", "@type": "xsd:integer"}] behaves the same, while the plain ["4", "7"] matches reading=="4" and reading!=4. Declared Booleans and declared times among several values work the same way. An array still has no order, so >3 matches none of them.
A JsonProperty is different. Its json is one JSON value, kept as written and never read as typed values, so a valueType beside it declares nothing. An equality such as config==4 never matches it, and config!=4 always does.
Date-times
A value is a time only when something says so. That can be its JSON-LD @type, which your @context can set for you, or the valueType of the Property it belongs to. Anything else is a string, even if it looks like a date. A string is stored and returned byte for byte, and q compares it as a string.
Take a write with one declared time and one plain string.
{
"testedAt": {
"type": "Property",
"value": {
"@type": "DateTime",
"@value": "2018-12-04T12:00:00,5Z"
}
},
"note": {
"type": "Property",
"value": "2018-12-04T12:00:00,5Z"
}
}
Reading the entity back gives this.
{
"testedAt": {
"type": "Property",
"value": {
"type": "DateTime",
"@value": "2018-12-04T12:00:00.500Z"
}
},
"note": {
"type": "Property",
"value": "2018-12-04T12:00:00,5Z"
}
}
testedAt is declared as a DateTime, so its time comes back in the one served spelling. The core @context maps type to @type, which is why @type comes back as type. note is a plain string, so it comes back exactly as written.
Precision and range
Every instant is held to the microsecond. NGSI-LD allows no more than six fractional digits, so nothing is lost. The instants Andromeda stamps itself, such as createdAt, modifiedAt and deletedAt, are cut to the microsecond before they are stored or returned, so the answer to a write always matches a later read of it.
Years run from 0000 to 9999. Sometimes Andromeda computes an instant itself, such as an expiry worked out from a lifetime. If the result falls outside the range it can store, the request is refused rather than the instant clamped.
How time values are served
Every time value comes back in one spelling, however you wrote it.
- A
DateTimeis in UTC with aZ. A whole second has no fraction, a whole millisecond has three digits, and anything else has six. So2024-01-01T00:00:00,5Zcomes back as2024-01-01T00:00:00.500Z. - A
Timefollows the same fraction rule, so12:00:00.000Zcomes back as12:00:00Z. - A
Datehas only one spelling anyway.
This holds everywhere a time value appears. That means system times, observedAt, expiresAt and deletedAt, responses, notifications, the Content-Range header and forwarded requests. It covers a single value, each value of a multi-valued Property, each list item, and each typed literal inside a structured value, on the Core API and in the temporal history. So writing the same time in another spelling isn't a change (see Writes).
The exception is a literal typed with an XSD datatype, xsd:dateTime included. It keeps the text you wrote, because in XSD the time zone offset is part of the value.
Time values in q
Whether you quote the operand decides what it matches.
q=reading=="2024-01-01T00:00:00Z"is a string. It matches areadingholding that untyped string, and never a declaredDateTime.q=reading==2024-01-01T00:00:00Zis a time. It matches areadingdeclared as thatDateTime, and never a string.
The grammar a DateTime, Date or Time has to follow is under Date-times in Strictness.
Units
A unitCode is a UN/CEFACT Recommendation 20 common code, such as KGM, MTR or CEL. Case matters, so kgm is refused.
Andromeda accepts every code listed in any revision of Recommendation 20 from 4 to 17. That is all 2,091 codes of Revision 9, which NGSI-LD cites, plus the 45 added since. A code that a later revision deprecated or deleted is still accepted. Recommendation 20 also counts every Recommendation 21 package code with an X prefix as a common code, so those are accepted too.
You get back the unit you sent. Andromeda never converts between units. NGSI-LD doesn't ask it to, and Recommendation 20's conversion factors are only informative.
One attribute can hold instances in different units, for example two datasetId instances, or two points in its history. Each keeps its own unit, and nothing is normalised. The one place mixed units are refused is temporal aggregation, because an average over two units means nothing. Temporal aggregation explains when that happens.
Identifiers and references
Any identifier you choose can be up to 1,024 bytes, counted in UTF-8 as Andromeda stores it. Every identifier is a database index key, which is where the limit comes from. A non-ASCII character takes more than one byte, so a non-ASCII identifier reaches the limit sooner than its length suggests.
The limit applies to the following.
- Entity ids, wherever an entity is created, addressed or selected.
- The ids of subscriptions, registrations, Snapshots and EntityMaps, and the ids and URLs of stored
@contexts. datasetId, and aninstanceIdyou supply.- Attribute names and entity type names, measured after expansion to their full IRI.
- The
idPatternof a registration's entity selector.
Because names are measured after expansion, a short attribute name can go over the limit once your @context expands it under a long @vocab.
A longer identifier is refused with 400 BadRequestData before anything is stored, and the error gives the length and the limit. In a batch, the refusal is reported against that one entity in a 207, and the rest of the batch goes ahead.
A reference has no limit of its own. That covers a Relationship object and each member of a ListRelationship objectList, which only the request body limit bounds. A reference longer than 1,024 bytes still can't name an entity Andromeda stores, though, so following it with a traversal or a join finds nothing.
Long identifiers can also trip up proxies and load balancers. A 1,024-byte non-ASCII identifier grows to about 3 KB once it is percent-encoded in a path, and many proxies cap the request line at 4 to 8 KB. To query with long identifiers or references, use POST /ngsi-ld/v1/entityOperations/query, which carries the query in the body.
Relationship targets
A Relationship's object and previousObject, and a ListRelationship's objectList and previousObjectList, come back exactly as you wrote them. That holds in every representation, in notifications, in entities forwarded to or relayed from other brokers, and in the temporal history.
Andromeda never percent-decodes a target. urn:ngsi-ld%3ABuilding%3A1 and urn:ngsi-ld:Building:1 are two different identifiers, and decoding the first would point the Relationship at an entity you never named.
The only rewriting is normalisation, which never decodes an escape and leaves the identifier meaning the same thing.
- In a URN, the
urn:scheme and the namespace are lowercased, and the hex digits of a percent-encoding are uppercased. - In an
httporhttpsURL, the scheme and the host are lowercased, a default port is dropped,.and..segments are resolved, and an empty path becomes/.
Comparisons follow the same rule.
- Linked entity retrieval looks a target up by its exact identifier. An encoded target only reaches an entity whose id is spelled the same way.
qcompares a Relationship's object with the operand byte for byte, in queries and in subscriptions. A single URI operand is also compared in its normalised spelling, soq=owner=="HTTP://Example.com/a"finds the Relationship stored ashttp://example.com/a. With!=, both spellings are excluded, since they name one resource.- The temporal history records the target as written, so the current entity and its history agree on the spelling.
Where Andromeda interprets the specification
The rule for numbers goes further than the specification requires. A broker may drop insignificant digits, and Andromeda doesn't. Geometry precision is left to the implementation.
A value is a time, a number or a Boolean only when it is declared as one, and an undeclared value comes back as written. Fractions of a second are limited to six digits. The difference between quoted and bare q operands comes from the q grammar.
NGSI-LD cites Recommendation 20 Revision 9 for unit codes. Andromeda also accepts the codes the other revisions from 4 to 17 list.
The specification sets no length for a URI, so the 1,024-byte limit is Andromeda's own (see Andromeda's own bounds).
Percent-encoded and decoded targets are different resources. The URN normalisation is the one RFC 8141 allows, which RFC 3986 treats as an equivalent spelling. The URL normalisation is the syntax-based and scheme-based normalisation of RFC 3986. The specification says "URI comparison should be performed so that the number of false negatives is minimized, as recommended by IETF RFC 3986 [5], section 6", which is why a q operand is compared in both spellings.