Visitar URL original
INSERT INTO a TIMESERIES type still coerces a number into a BOOLEAN or STRING column, where the line protocol, gRPC and Prometheus refuse the same point · Issue #10137 · ArcadeData/arcadedb · GitHub
Skip to content

INSERT INTO a TIMESERIES type still coerces a number into a BOOLEAN or STRING column, where the line protocol, gRPC and Prometheus refuse the same point #10137

Closed

Description

@ruispereira

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 = 1 is stored as true; bo = 0 as false; bo = 7 as true.
  • s = 42 is stored as "42", s = 4.25 as "4.25", s = true as "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

  1. added
    engineCore engine: document/schema API
    severity:minorCosmetic, documentation, edge case, or a workaround exists
    on Oct 16, 2026
  2. added this to the 26.11.1 milestone on Oct 16, 2026
  3. self-assigned this
    on Oct 17, 2026
  4. added a commit that references this issue on Oct 18, 2026
  5. robfrank commented on Oct 18, 2026

    @robfrank
    Collaborator

    Fixed by #10236. A SQL INSERT into a TIMESERIES type converted each value to the column type before the time series check ran, so 7 written to a BOOLEAN column was stored as true and 42 written 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, through SET, CONTENT and VALUES. An out-of-range number (for example 300 into a BYTE column) now fails with a clear message naming the column instead of a bare IllegalArgumentException.

    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:


    ❤️ 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.

  6. removed
    in progressThe team is actively working on this issue
    on Oct 18, 2026
  7. added a commit that references this issue on Oct 18, 2026
  8. added 2 commits that reference this issue on Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugengineCore engine: document/schema APIseverity:minorCosmetic, documentation, edge case, or a workaround existssqltimeseriesTime series module

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions