Repository navigation
Description
TIMESERIES. The only hit is #10064 ("INSERT INTO a TIMESERIES type converts values with its own convertValue
instead of ColumnDefinition.storableValue", CLOSED), which this reopens from the other side, and #10107 (OPEN,
the JSONL importer), which is a different caller. No issue covers the document-layer conversion running first.
| surface | route | converts through ColumnDefinition.storableValue |
|---|---|---|
line protocol (POST /ts/write) |
TimeSeriesGateway.write -> TimeSeriesStorableRows.convert |
yes |
gRPC TimeSeriesWrite / TimeSeriesWriteStream |
TimeSeriesGateway.write -> TimeSeriesStorableRows.convert |
yes |
| Prometheus remote write | PostPrometheusWriteHandler -> TimeSeriesStorableRows.convert |
yes |
SQL INSERT INTO <ts type> |
SaveElementStep.saveToTimeSeries |
calls it, but only after MutableDocument has already converted the value |
| JSONL import | JsonlImporterFormat.loadTimeSeriesSamples |
no (already filed, #10107) |
Summary
#10064's contract is that every ingestion surface reads a column value through
ColumnDefinition.storableValue, whose documented rule is: "a BOOLEAN column keeps a boolean and parses
true/false (any case) ... any other text, and a number, is refused rather than stored as false" and
"a STRING column keeps a string".
On the SQL path the value never reaches that rule unchanged. A TIMESERIES type declares its columns as schema
properties, so UpdateSetStep -> UpdateItem.applyOperation -> UpdatableResult.setProperty ->
MutableDocument.set -> MutableDocument.convertValueToSchemaType -> Type.convert has already coerced the
value to the declared Java type by the time saveToTimeSeries calls storableValue, which then sees a value of
the right class and passes it. The guard landed at the call site (saveToTimeSeries) and not at the authority
(the row the INSERT builds), so the two layers agree only where they happen to refuse the same thing.
Where they disagree, the SQL statement stores a sample the other three surfaces refuse and report:
bo = 1is stored astrue;bo = 0asfalse;bo = 7astrue.s = 42is stored as"42",s = 4.25as"4.25",s = trueas"true".
That is the exact case the contract was written to stop ("not stored as false"). A fleet that writes the same
measurement from an agent (line protocol) and from SQL gets a 400 with
M.bo (field): Cannot convert a value of type Long to the BOOLEAN column 'bo' from one and a silent success
from the other.
Secondary, same cause: when the document layer refuses first it throws a bare
java.lang.IllegalArgumentException ("Cannot convert type 'class java.lang.Integer' to 'BYTE' found in property
'b'"), not the CommandExecutionException carrying storableValue's message that saveToTimeSeries wraps.
Issue10064TimeSeriesInsertStorableValueTest.aTextBeyondTheRowLimitIsRefusedByTheStatement asserts
CommandExecutionException; every range refusal next to it asserts only RuntimeException, because it is the
other exception.
Where (HEAD ee6a5b8)
engine/src/main/java/com/arcadedb/query/sql/executor/SaveElementStep.java:281-col.storableValue(...), reached with an already-converted value.engine/src/main/java/com/arcadedb/database/MutableDocument.java:668(convertValueToSchemaType) <-putProperty:501<-set:241, the conversion that runs first.engine/src/main/java/com/arcadedb/engine/timeseries/ColumnDefinition.java:342-storableValue, whose javadoc states the rule.engine/src/test/java/com/arcadedb/engine/timeseries/Issue10064TimeSeriesInsertStorableValueTest.java:53- the test javadoc already records "The document layer has already converted each property to its declared type by the time the step runs", but every case it pins is one where both layers refuse.
Repro
Embedded, no server needed.
db.command("sql", "CREATE TIMESERIES TYPE M TIMESTAMP ts FIELDS (i INTEGER, d DOUBLE, s STRING, bo BOOLEAN) SHARDS 1");
// SQL: stored
db.command("sql", "INSERT INTO M SET ts=7000, i=1, d=1, s=42, bo=7");
// -> SELECT * FROM M WHERE ts=7000 => {"i":1,"d":1.0,"s":"42","bo":true}
// line protocol / gRPC / Prometheus, same point: refused
TimeSeriesGateway.write(db, LineProtocolParser.parse("M i=1i,d=1,s=42i,bo=7i 7500",
LineProtocolParser.Precision.MILLISECONDS));
// -> WriteReport.dropped()==1,
// unreadableValues=[M.bo (field): Cannot convert a value of type Long to the BOOLEAN column 'bo',
// M.s (field): Cannot convert a value of type Long to the STRING column 's']Full matrix run against HEAD (** marks a disagreement):
** number 1 -> BOOLEAN SQL=stored(true) GATEWAY=REFUSED
** number 0 -> BOOLEAN SQL=stored(false) GATEWAY=REFUSED
** number 7 -> BOOLEAN SQL=stored(true) GATEWAY=REFUSED
** 42 -> STRING SQL=stored("42") GATEWAY=REFUSED
** 4.25 -> STRING SQL=stored("4.25") GATEWAY=REFUSED
** true -> STRING SQL=stored("true") GATEWAY=REFUSED
true -> INTEGER SQL=REFUSED GATEWAY=REFUSED
true -> DOUBLE SQL=REFUSED GATEWAY=REFUSED
'2.5' -> DOUBLE SQL=stored(2.5) GATEWAY=stored(2.5)
'TRUE' -> BOOLEAN SQL=stored(true) GATEWAY=stored(true)
300 -> BYTE SQL=REFUSED(bare IllegalArgumentException) GATEWAY=REFUSED(named reason)
The same split shows the other way round on a DATETIME field column, where storableValue documents that
"text of digits is that same count":
** '1700000000000' -> DATETIME SQL=REFUSED(bare IllegalArgumentException)
GATEWAY=stored (2023-11-14 22:13:20)
'2023-11-14 22:13:20' -> DATETIME SQL=stored GATEWAY=stored (agree)
0 -> DATETIME SQL=stored GATEWAY=stored (agree)
Suggested fix
Read the raw expression value for a TIMESERIES target instead of letting the document layer convert it:
saveToTimeSeries already has the row, so the INSERT into a LocalTimeSeriesType should build its row from the
unconverted projection values (or declare the TIMESERIES columns so that the document layer does not coerce) and
hand each one to ColumnDefinition.storableValue, wrapping its IllegalArgumentException as it already does.
Guarantee that stops it coming back: a table-driven test that, for one list of (column type, raw value) pairs,
asserts the SQL INSERT and TimeSeriesStorableRows.convert reach the same outcome - both store the same
value, or both refuse - rather than asserting each surface on its own. The present test file asserts each path
separately, which is why a pair where they differ was never written down.
Scope / confidence
Reproduced on an embedded database built from HEAD. The matrix above is the whole set of (declared kind, value
kind) pairs I ran; I did not run DATE/DATETIME columns, so the table is "at least these", not exhaustive.
Activity
fix(#10137): INSERT INTO a TIMESERIES type no longer coerces a number…
robfrank commentedon Oct 18, 2026
Fixed by #10236. A SQL
INSERTinto a TIMESERIES type converted each value to the column type before the time series check ran, so7written to a BOOLEAN column was stored astrueand42written to a STRING column as"42", while the line protocol, gRPC and Prometheus remote write refuse the same point. SQL now refuses those values the same way, throughSET,CONTENTandVALUES. An out-of-range number (for example 300 into a BYTE column) now fails with a clear message naming the column instead of a bareIllegalArgumentException.This is a breaking change: a SQL insert that used to store a coerced number in a BOOLEAN or STRING time series column now fails.
Still open:
INSERT ... FROM SELECTinto a TIMESERIES type fails outright (pre-existing, unrelated to the values).ALTER PROPERTYaccepts MAX, REGEXP and MANDATORY on a time series column, but no write path enforces them (pre-existing).❤️ Thanks for helping make ArcadeDB better! ArcadeDB is free and open source, sustained by its community. If it's useful to you or your company, please consider becoming a sponsor to keep development going.
fix(#10238,#10233,#10232): TIMESERIES column constraints enforced on …
fix(#10137): INSERT INTO a TIMESERIES type no longer coerces a number…
fix(#10238,#10233,#10232): TIMESERIES column constraints enforced on …