Skip to content

Add table-model query support to the Grafana data source plugin#114

Merged
CritasWang merged 3 commits into
apache:masterfrom
PDGGK:feature/grafana-table-model
Jul 23, 2026
Merged

Add table-model query support to the Grafana data source plugin#114
CritasWang merged 3 commits into
apache:masterfrom
PDGGK:feature/grafana-table-model

Conversation

@PDGGK

@PDGGK PDGGK commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

What

Adds table-model query support to the IoTDB Grafana data source plugin — the first slice of #18258.

The plugin previously only spoke the tree model: both QueryEditor modes build root.* paths and the backend calls /grafana/v1/query/expression, which has no table-model equivalent, so table-model users could not visualize their data.

How

  • New QueryEditor mode "SQL: Table Model" — a database field, a SQL editor and a result format option, alongside the existing "SQL: Full Customized" and "SQL: Drop-down List" modes.
  • Native-client backend (per review): queries run through apache/iotdb-client-go v2.0.8 (TableSession) against the RPC port, served by a lazily created per-datasource TableSessionPool. A new optional rpc address datasource setting configures the endpoint (defaults to the URL's host with port 6667). Tree-model queries and the health check keep using the REST service for now.
  • Typed, nullable fields driven by the result set's data types — TIMESTAMP → time (converted by the client with the server-reported timestamp precision), INT32/INT64 → int64, FLOAT/DOUBLE → float64, BOOLEAN → bool, DATE/BLOB/TEXT/others → string — with INT64 precision preserved end-to-end (native values, no JSON).
  • Multi-device results render as separate series: in the default Time series format, rows are sorted by the first TIMESTAMP column and a long-shaped result (time + tag columns + value columns) is pivoted with the SDK's data.LongToWide, turning tag columns into field labels — SELECT time, device_id, temperature FROM t draws one line per device instead of one interleaved series. The Table format returns rows untouched (preserves a user's ORDER BY). If the pivot cannot apply (e.g. null timestamps), the plain frame is returned.
  • $__timeFilter(col) / $__timeFrom / $__timeTo macros expand to ISO 8601 UTC timestamp literals for the panel's range, which the server parses under any configured timestamp_precision; the function forms $__timeFrom() / $__timeTo() familiar from Grafana's SQL data sources are accepted too, and $__timeFilter allows one level of nested parentheses.
  • README: documents the new mode, the rpc address setting, the macros, the format option and the LIMIT recommendation for wide scans.

Testing

  • Backend: go build, go vet, and go test ./pkg/plugin (13 unit tests) pass — covering macro expansion (incl. function forms and nested parentheses), the result-set fetch path (behind a narrow interface, incl. nulls and error propagation), per-type conversion incl. DATE/BLOB rendering and INT64 precision, the long-to-wide pivot for multi-device results (incl. unsorted input and the null-timestamp fallback), Table-format row-order preservation, and RPC endpoint resolution.
  • Frontend: type-checked with tsc --noEmit against the @grafana types.
  • The client contract was verified against the iotdb-client-go v2.0.8 sources (TableSession, SessionDataSet typed getters, server-reported time precision) and the server's relational planner (ignoreTimestamp semantics). It has not yet been smoke-tested against a live server.

Follow-ups (same issue)

The @grafana/toolkit@grafana/create-plugin toolchain migration (next PR, as discussed), a database → table → column browser, a visual query builder, frontend Jest coverage for the new mode, path-picker performance, and moving the tree-model path and health check to the native client as part of the broader modernization.

The Grafana data source plugin only understood the tree model: both
QueryEditor modes build root.* paths and the backend calls
/grafana/v1/query/expression, which has no table-model equivalent, so
table-model users could not visualize their data through the plugin.

This adds a third QueryEditor mode, "SQL: Table Model", that runs standard
SQL against a database and renders the result. It reuses IoTDB's existing
table REST endpoint (POST /rest/table/v1/query) rather than adding a new
server API: the backend sends {database, sql} and turns the row-major
QueryDataSet into a Grafana frame with one typed, nullable field per
column, driven by the response's data_types so column types are exact
instead of guessed. The body is decoded with UseNumber so INT64 values
beyond 2^53 keep their precision.

In the default Time series format, rows are sorted by the first TIMESTAMP
column and a long-shaped result (time + tag columns + value columns) is
pivoted into one labeled series per tag combination, so a multi-device
query renders as separate lines; the Table format preserves the server's
row order. A $__timeFilter(col) / $__timeFrom / $__timeTo macro set
expands to the panel's range, accepting the $__timeFrom() / $__timeTo()
function forms as well. Because the table REST endpoint returns TIMESTAMP
values (and compares integer time literals) in the server's configured
timestamp_precision -- unlike the tree-model /grafana/v1 endpoints, which
normalize to milliseconds server-side -- a new data source "time
precision" option (ms default / us / ns) scales both the macro bounds and
the TIMESTAMP rendering, so the mode also works on us/ns servers.

Follow-up work for the same issue: a database -> table -> column browser,
a visual query builder, a default row limit to bound large SELECTs, and
the Grafana plugin SDK / Go backend modernization.

Signed-off-by: Zihan Dai <99155080+PDGGK@users.noreply.github.com>
Comment on lines +204 to +210
this.setState({
isDropDownList: this.props.query.isDropDownList,
sqlType: this.props.query.sqlType,
database: this.props.query.database ?? '',
sql: this.props.query.sql ?? '',
format: this.props.query.format ?? tableFormats[0],
});

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds table-model SQL query support to the IoTDB Grafana data source plugin by introducing a new “SQL: Table Model” editor mode and a Go backend implementation that queries IoTDB’s /rest/table/v1/query endpoint, with timestamp-precision-aware macro expansion and typed/nullable result frames.

Changes:

  • Add a new QueryEditor mode (“SQL: Table Model”) with database + SQL inputs and a result format selector (Time series vs Table).
  • Implement a Go backend table-model query path that expands Grafana time macros, parses row-major QueryDataSet responses with UseNumber, and builds typed Grafana frames (including optional long-to-wide pivot).
  • Add a datasource setting for timestampPrecision (ms/us/ns) and document the new mode/macros/precision behavior in the README.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
connectors/grafana-plugin/src/types.ts Extends query/options types to carry table-model SQL/database/format and timestamp precision setting.
connectors/grafana-plugin/src/QueryEditor.tsx Adds “SQL: Table Model” UI mode (database + SQL editor + format).
connectors/grafana-plugin/src/datasource.ts Expands template variables for table-model sql and database.
connectors/grafana-plugin/src/ConfigEditor.tsx Adds a “time precision” datasource setting (ms/us/ns).
connectors/grafana-plugin/README.md Documents the new table-model mode, macros, format behavior, and timestamp precision option.
connectors/grafana-plugin/pkg/plugin/table_query.go Implements backend table-model querying, macro expansion, typed frame building, and pivoting.
connectors/grafana-plugin/pkg/plugin/table_query_test.go Adds unit tests covering macro expansion, typing/nulls, precision, sorting, pivoting, and error paths.
connectors/grafana-plugin/pkg/plugin/plugin.go Wires new query fields + timestamp precision into the backend query flow and instance model.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread connectors/grafana-plugin/pkg/plugin/table_query.go Outdated
Comment thread connectors/grafana-plugin/pkg/plugin/table_query.go Outdated
Comment thread connectors/grafana-plugin/src/QueryEditor.tsx
Comment thread connectors/grafana-plugin/src/datasource.ts
@CritasWang

Copy link
Copy Markdown
Contributor

Thanks for the PR! Two requests before we move forward with the review:

  1. Please try switching the backend from the REST API to apache/iotdb-client-go — see the discussion in [Feature request] Grafana plugin: no table model support and poor tree model UX — needs overhaul | Grafana iotdb#18258 (comment by @HTHou). The native client should perform better than REST, and the latest release (v2.0.8) already provides table-model support (TableSession / PooledTableSession). It would also give us typed values directly instead of decoding JSON and coercing interface{} (the UseNumber / toInt64 workarounds in table_query.go), and the server-side rest_query_default_row_size_limit cap would no longer apply.

  2. Please also modernize the frontend build toolchain in this effort — the plugin still builds with @grafana/toolkit (pinned to latest in package.json), which has been deprecated and archived by Grafana. The recommended path is migrating to @grafana/create-plugin. If that makes this PR too large, it's fine to do it as an immediate follow-up PR, but we should not keep adding features on top of a dead toolchain for long.

The frontend part of this PR (the new QueryEditor mode, macros, format/precision options) looks like it can stay mostly as-is regardless of which transport the backend uses.

Per review: query table-model data through apache/iotdb-client-go
(v2.0.8, TableSession) on the RPC port instead of POST
/rest/table/v1/query.

- A lazily created, per-datasource TableSessionPool serves the queries;
  a new "rpc address" datasource option sets the endpoint, defaulting to
  the URL's host with the default RPC port 6667. Tree-model queries and
  the health check keep using the REST service for now.
- Values arrive as typed Go values instead of JSON, removing the
  UseNumber/json.Number coercion layer; INT64 precision is inherent, and
  the client converts TIMESTAMP/DATE values with the server-reported
  timestamp precision, so the short-lived "time precision" datasource
  option is removed again.
- The time macros now expand to ISO 8601 UTC timestamp literals
  (e.g. 2020-09-13T12:26:40.000+00:00) rather than epoch-ms integers,
  which the server parses correctly under any configured
  timestamp_precision.
- The per-query database is applied with USE on the pooled session;
  DATE renders as yyyy-MM-dd and BLOB as 0x-prefixed hex, matching the
  REST transport's output.
- The server-side REST row cap (rest_query_default_row_size_limit) no
  longer applies; the README now recommends a LIMIT clause for wide
  scans.
- Frame building, the Time series/Table formats, the long-to-wide pivot
  and the row sorting are unchanged; the fetch path sits behind a narrow
  result-set interface so it stays unit-testable. 13 unit tests, go vet
  and tsc --noEmit pass.

Signed-off-by: Zihan Dai <99155080+PDGGK@users.noreply.github.com>
@PDGGK

PDGGK commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @CritasWang @HTHou — done: the table-model backend now goes through apache/iotdb-client-go v2.0.8 (TableSession) instead of the REST API (bb4ffad).

Notes on what the switch changed:

  • The native client connects to the RPC port rather than the REST port, so the datasource gained an optional rpc address setting; when left empty the plugin uses the URL's host with the default port 6667. Sessions come from a lazily created per-datasource TableSessionPool, so datasources that only use the tree model never open an RPC connection. Tree-model queries and the health check still use the REST service — moving those over is probably best folded into the SDK/toolchain modernization.
  • Values now arrive as typed Go values, which removed the whole JSON UseNumber/interface{} coercion layer, and the client converts TIMESTAMP values with the server-reported timestamp precision. That also let me simplify the time macros: $__timeFilter / $__timeFrom / $__timeTo now expand to ISO 8601 UTC literals (e.g. 2020-09-13T12:26:40.000+00:00), which parse correctly under any server timestamp_precision, instead of epoch-ms integers.
  • The per-query database is applied with USE on the pooled session; DATE/BLOB render the same as before (yyyy-MM-dd / 0x… hex). And as you said, the REST row cap no longer applies — the README now recommends a LIMIT clause for wide scans.
  • The stale "column-major" comments Copilot flagged went away with the rewrite.

On the toolchain: I'll take the @grafana/toolkit@grafana/create-plugin migration as the immediate follow-up PR, as suggested — that keeps this one reviewable on its own. It's also the natural place for the remaining frontend bot notes (Jest coverage for the new mode, and the pre-existing QueryEditor props-mutation / state-update patterns CodeQL flags), since the editor code gets reworked there anyway.

@CritasWang

Copy link
Copy Markdown
Contributor

Thanks for the quick turnaround on the native-client switch — the new backend reads well, and deferring the toolchain migration (plus the remaining frontend bot notes) to the follow-up PR is exactly what we agreed. Two things before this can be merged:

1. USE state leaks across pooled sessions

queryTableModel executes USE <database> on a pooled session and then returns it to the pool. In iotdb-client-go v2.0.8, PooledTableSession.Close() only restores the database when the pool itself has one configured (tablesessionpool.go):

if s.session.config.Database != s.sessionPool.config.Database && s.sessionPool.config.Database != "" {
	err := s.session.ExecuteNonQueryStatement("use " + s.sessionPool.config.Database)

The pool created in getTablePool sets only Host/Port/UserName/Password, so config.Database is empty and this reset branch never runs. Consequence: query A runs USE db1 and returns the session; query B with an empty database field may then pick up that "dirty" session and silently query db1 — nondeterministically, depending on pool scheduling. Any dashboard mixing panels with different (or empty) database values on the same datasource will hit this.

Either fix works for me:

  • make the database field required for the table-model mode (frontend validation + reject empty in verifyQuery) — simplest and most predictable; or
  • reset the session's database state on every checkout before running the query.

2. Please smoke-test against a live server before merge

The PR description notes the new path "has not yet been smoke-tested against a live server", and the 13 unit tests all sit behind the mocked result-set interface — the actual RPC path (connect, auth, USE, timeouts, timestamp precision) hasn't been exercised end-to-end. Please run the new mode against a real IoTDB 2.x instance with a Grafana panel and post the result here (a screenshot or a short list of the queries you ran is fine). A multi-panel / multi-database scenario would also directly verify the fix for point 1.

Per review: queryTableModel runs USE <database> on a pooled session, and
PooledTableSession.Close() only restores the database when the pool has
one configured — which this pool does not — so a session's USE state
survived release. A query with an empty database field could then
nondeterministically inherit whichever database a previous panel had
selected on that session.

Make the database required for the table-model mode: verifyQuery rejects
an empty database, the editor marks the field required, and the README
documents it. Every query therefore always runs USE for its own database
on the session it checked out, so leftover session state can never
influence a result regardless of pool scheduling. Fully-qualified
database.table references keep working and take precedence over the
session database.

Verified against a live IoTDB 2.0.8: alternating panels on two databases
sharing one session pool each saw exactly their own data across
repeated rounds.

Signed-off-by: Zihan Dai <99155080+PDGGK@users.noreply.github.com>
@PDGGK

PDGGK commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Both points addressed:

1. USE state leak — fixed by making the database required. verifyQuery now rejects an empty database in table-model mode ("Input error, DATABASE is required"), the editor marks the field required, and the README documents it. Since every query now always runs USE <database> on its checked-out session before executing, leftover session state can never influence a result regardless of pool scheduling. Fully-qualified db.table references still work and take precedence over the session database.

2. Live smoke test — ran the new mode end-to-end against a real IoTDB 2.0.8 (apache/iotdb:2.0.8-standalone, RPC 6667), driving queryTableModel — the exact entry point Grafana calls — with real panel-range parameters over the native RPC path (connect, session-open auth as root, per-query USE, timeout, fetch):

  • SELECT time, device, temperature FROM env WHERE $__timeFilter(time) (database smoke1, 2 devices) → pivoted into 2 labeled series (device=d1, device=d2), 2 points each
  • the same query in Table format → 3 plain columns, 4 rows, server row order preserved
  • WHERE time >= $__timeFrom() AND time <= $__timeTo() → the ISO-8601 literal expansion parsed and filtered correctly on the live server
  • all-types round trip: TIMESTAMP / TEXT / FLOAT / INT32 / INT64 (9007199254740993, above 2^53, exact) / BOOLEAN / DATE (2025-07-14) / BLOB (0xcafebabe)
  • the point-1 scenario: 3 alternating rounds of smoke1 (4 rows) / smoke2 (1 row, device d9) panels sharing one session pool — each panel saw exactly its own database every round
  • a bad query → the server error surfaces as the panel error

One honest caveat: this drove the backend query path directly rather than a rendered Grafana panel — the current @grafana/toolkit toolchain can't produce a dev build on modern Node, which is the follow-up PR we agreed on. I'll post an in-Grafana screenshot of the same dashboards as part of that toolchain-migration PR, where the frontend becomes buildable again. The smoke harness is a small build-tagged Go test; happy to include it in this PR if you'd find it useful for CI or reproduction.

@CritasWang

Copy link
Copy Markdown
Contributor

Both points look good to me.

The verifyQuery fix is correct: requiring the database means every table-model query always runs USE <database> on its checked-out session, so there is no path by which a session's leftover state from a previous query can influence a result — regardless of pool scheduling or how many panels share the datasource. The comment in the code explains the reasoning clearly.

The smoke-test caveat (backend path only, not through a rendered Grafana panel) is acceptable given the agreed toolchain situation. The scenarios you covered — multi-database pool sharing, all-types round-trip including INT64 above 2^53, ISO-8601 macro expansion on a live server, error surfacing — are exactly what matters for this PR. The in-Grafana screenshot can follow with the toolchain-migration PR as discussed.

LGTM from my side. Waiting for CI on the latest commit to go green, then this is ready to merge.

@CritasWang
CritasWang merged commit 85c87a4 into apache:master Jul 23, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants