EN Queries - AlanDevise/TsGate GitHub Wiki

Home · GitHub

English | 简体中文

2.1.3 is published through the GitHub source/tag and Maven Central; same-SHA CI and public artifact verification passed. Current coordinates: 2.1.3; see 2.1.3 release notes.

2.1.3 corrections

Version 2.1.3 tightens structured-filter validation, merges IoTDB physical table-name case variants before whole-batch checks, verifies backend registration in tests/CI, rejects duplicate IoTDB/InfluxDB 3 result keys, and checks direct-adapter batch counts before copying inputs. The public API, Template → SPI → adapter architecture, Java 17/client/server baselines and configuration defaults remain unchanged. 2.1.3 release notes.

All four built-in adapters share stricter structured-filter validation before database I/O. EQ, NE, GT, GE, LT and LE require exactly one operand; BETWEEN requires exactly two; IN requires at least one. A directly constructed QueryFilter must have a nonblank column and an operator, and its operands must not contain null, Float.NaN / Double.NaN, or positive/negative Float/Double infinity. Malformed filters produce ARGUMENT_ERROR instead of silently dropping extra operands, treating null as the string 'null', or submitting a non-finite numeric literal. The strings "null", "NaN" and "Infinity" remain ordinary literal strings; backend numeric precision and range rules are unchanged.

The fluent builder's optional-filter behavior is preserved: missing optional inputs and empty collections are skipped, null elements are removed, and an all-null collection adds no filter. A filter that remains after this normalization must satisfy the operand rules above. Direct model input is validated as supplied. The checks apply to common query and count, including fluent list() and offset-page count requests; count() still ignores pagination and cursor settings in its private copy. Native SQL is unchanged.

Within the shared argument-validation stage, invalid filters produce ARGUMENT_ERROR before optional capability checks, including a query that also requests an unsupported strict cursor. Valid unsupported operations still produce their existing UNSUPPORTED_OPERATION errors. Existing entry-point, lifecycle and backend-specific validation ordering is otherwise preserved.

Returned duplicate names in 2.1.3

IoTDB rejects exact duplicate SDK output column names before reading any row, including duplicate metadata on an empty result. InfluxDB 3 rejects an exact duplicate JSON object key in a successful query response, including when its earlier value is null or the duplicate appears in a later row. These errors produce QUERY_ERROR and fail the entire query; no partial rows or successful count are returned. Common query, count and adapter executeQuery(sql) use the guarded decoders.

Returned names that differ in case remain distinct at decoding; this does not change IoTDB’s lowercase identity rules for structured-query inputs or writes. Native SQL is passed through unchanged: the server may reject duplicate aliases first, and the decoder rejects ambiguous output if the server returns it. Direct use of a borrowed native client is outside the adapter decoder. Existing InfluxDB 1.x/openGemini duplicate-column and tag/field collision checks remain unchanged. See 2.1.3 release notes for release scope and validation status.

Aggregate output names in 2.1.2

Structured aggregate outputs must have distinct names after the selected backend's column normalization. The checked set contains grouping tags, every aggregate alias, and the generated window_start only when groupByTime emits that column. Duplicate aliases, repeated grouping tags, tag/alias collisions and generated-window collisions fail with ARGUMENT_ERROR before any HTTP request or session borrow. The same rule applies to direct TSDBAdapter.query / count, fluent list and offset-page totals.

InfluxDB 1.x, InfluxDB 3 and OpenGemini preserve quoted identifier case: total and TOTAL remain distinct. IoTDB uses lowercase physical identity, so these collide. Without a generated window, window_start may be an ordinary aggregate alias. Existing unsupported cursor, sorting and timezone checks retain their error classification and priority; this validation neither adds those capabilities nor rewrites native SQL. Use unique aliases such as temperature_avg and temperature_max when aggregating a field more than once.

The validation, configuration-snapshot and startup-failure-policy fixes documented here are included in 2.1.0. See 2.1.0 change history.

InfluxDB 1.x and OpenGemini aggregate responses also include the implicit physical time column, so an exact aggregate alias or grouping tag named time is rejected before I/O. Case-distinct Time remains available. This protocol-specific reservation does not add a reserved time alias to IoTDB or InfluxDB 3.

Basic fluent queries

TGTemplate.query(Class<T>) returns com.alandevise.tsgate.core.TGQueryBuilder<T>. The examples use the injected TGTemplate variable tgTemplate. Create a new builder for each query; the builder is mutable and is not intended to be shared across threads.

import com.alandevise.tsgate.core.TGQueryBuilder;

TGQueryBuilder<AccrueRecord> query = tgTemplate.query(AccrueRecord.class);
List<AccrueRecord> rows = tgTemplate.query(AccrueRecord.class)
        .database("tsdb")
        .whereTag("device_code", "device_001")
        .timeRange(startTime, endTime)
        .orderByTimeDesc()
        .limit(20)
        .list();

Direct adapter query validation

The same basic validation applies to TSDBAdapter.query(database, query) for all four backends, including caller-created TSDBQuery models. A null query, nonpositive explicit limit, negative offset, or startTime > endTime produces ARGUMENT_ERROR before database I/O. Invalid pagination is never silently discarded. A null limit leaves SQL unpaged while backend result limits still apply; offset zero and equal time bounds are valid. Native executeQuery(sql) retains its separate SQL and result-limit contract.

count() continues to ignore pagination and cursor settings before counting; reversed time bounds are rejected. The shared SPI default count(null) returns ARGUMENT_ERROR rather than an unchecked null-pointer failure, including for third-party adapters that use the default implementation. Fluent page sizes, pagination-probe allowances and backend-specific unsupported operations retain their existing contracts.

For the OpenGemini adapter, only the exact backend query error measurement not found becomes a successful empty result in structured common query(...), or zero in count(...), whose measurement is explicit. Database, retention-policy, permission and other query errors retain their errors. Native executeQuery(...) preserves query errors, since one statement can involve multiple measurements or subqueries; the borrowed compatible org.influxdb.InfluxDB also retains the server's error DTO. A new measurement or tag series may be temporarily invisible after a write acknowledgment; business read-side polling must have an explicit deadline and match the expected point. Each adapter query remains one request. See OpenGemini integration.

Column projections

public class ValueStatusResult {
    private Double value;
    private Long status;
}

List<ValueStatusResult> rows = tgTemplate.query(AccrueRecord.class)
        .database("tsdb")
        .select("value", "status")
        .whereTag("source", "annotation-pojo")
        .timeRange(startTime, endTime)
        .orderByTimeAsc()
        .limit(100)
        .list(ValueStatusResult.class);

FIELD and multi-column sorting

FIELD sorting applies to IoTDB and InfluxDB 3; InfluxDB 1.x supports time sorting only. A FIELD may be the first sort key or follow time in a multi-column ordering. Calls produce SQL ORDER BY keys in declaration order; later keys are compared only when earlier values are equal.

List<AccrueRecord> rows = tgTemplate.query(AccrueRecord.class)
        .database("tsdb")
        .timeRange(startTime, endTime)
        .orderByTimeAsc()
        .thenByFieldDesc("value")
        .thenByFieldAsc("status")
        .limit(100)
        .list();

The example produces ORDER BY time ASC, value DESC, status ASC. Use orderByFieldAsc(field) or orderByFieldDesc(field) to make a FIELD the first key. orderBy... replaces the current ordering, while thenByField... appends or updates secondary keys.

FIELD sorting works with list(), limit/offset pagination through page(pageNum, pageSize) and strictCursorPage(). The single-time-cursor page() and aggregate queries reject FIELD sorting explicitly. Strict cursors construct lexicographic conditions using each SortSpec direction, supporting mixed ordering such as ORDER BY time DESC, device_code ASC.

Strict pagination appends any missing time + all @TGTag columns to custom ordering. Appended keys inherit the first key's direction; explicitly declared keys retain their own direction. The POJO must declare the table's complete tag-key set. Sort and cursor values must currently be non-null.

Aggregation

public class AccrueAggregateResult {
    private Long windowStart;
    private String deviceCode;
    private Double avgValue;
    private Double maxValue;
    private Long sampleCount;
}

List<AccrueAggregateResult> rows = tgTemplate.query(AccrueRecord.class)
        .database("tsdb")
        .whereTagIn("device_code", List.of("device_001", "device_002"))
        .where("value", OperatorEnum.GE, 0)
        .timeRange(startTime, endTime)
        .groupByTime("5m")
        .groupByTag("device_code")
        .aggregate("value", AggregationFunctionEnum.AVG, "avg_value")
        .aggregate("value", AggregationFunctionEnum.MAX, "max_value")
        .aggregate("value", AggregationFunctionEnum.COUNT, "sample_count")
        .timeZone("+08:00")
        .orderByTimeAsc()
        .limit(100)
        .list(AccrueAggregateResult.class);

groupByTime accepts only a positive integer with ms/s/m/h/d units, such as 5m; a unitless integer means milliseconds. Fractional, negative, zero, compound or overflowing durations outside the long millisecond range are rejected before SQL generation.

Fluent window aggregation returns window_start as an epoch-millisecond timestamp, which may map to Long, Instant or Date. If InfluxDB returns a window timestamp without a timezone suffix, the adapter interprets it as UTC independently of the application's default JVM timezone.

IoTDB / InfluxDB 3 window timezone rules, with InfluxDB 1.x restrictions described in the backend capability table:

  • UTC is the default. Fixed offsets such as UTC and +08:00 use fixed-duration windows and do not require a time range.
  • Regional zones such as America/New_York and Asia/Shanghai require an explicit bounded timeRange(start, end).
  • A regional 1d window follows local midnight on the queried dates. A DST transition day can contain 23 or 25 hours. Nd aligns every N local calendar days from the local date 1970-01-01, using each boundary date's actual offset.
  • For both backends, the adapter computes calendar boundaries and generates bounded CASE grouping. It does not change the borrowed IoTDB Session's timezone or depend on a function unique to a recent database release. More than 10000 generated windows produces ARGUMENT_ERROR.
  • Regional ms/s/m/h windows use the offset at the query start. A range crossing an offset change produces UNSUPPORTED_OPERATION. Use UTC, a fixed offset or calendar-day windows for such a range.
  • The query still filters with start <= time <= end; window boundaries are left-inclusive and right-exclusive. To exclude the next local midnight, set the end to one millisecond before that boundary.

For example, .timeRange(start, end).groupByTime("1d").timeZone("America/New_York") uses 23-hour and 25-hour calendar days on 2026-03-08 and 2026-11-01 respectively, rather than applying a fixed offset taken from 1970.

Fluent-query notes

  • Most fluent methods collect query settings and have no strict overall ordering requirement. Terminal calls such as list(), page() and page(pageNum, pageSize) execute the query and belong at the end.
  • Later calls to settings such as database, select, timeRange, limit and orderByTimeAsc/Desc replace earlier values.
  • Calls to where, whereTag, whereTagIn, groupByTag and aggregate accumulate. Ordinary predicates currently combine with AND.
  • Use page() for time-cursor pagination, with page size from limit(). Use page(pageNum, pageSize) for offset pagination, without additionally mixing in limit() and offset().
  • Create a new builder with tgTemplate.query(...) for each query. Do not share builders across threads or requests.

Stable default aggregate aliases

An omitted aggregate alias uses the field name plus an underscore and the function name lowercased with Locale.ROOT: for example, MIN(value) receives value_min regardless of the JVM's default locale. Explicit aliases are unchanged.


← Writes, models and errors · Pagination and native queries →

OpenGemini in 2.1.0

TsGate 2.1.0 includes the independent OpenGemini adapter and starter. See OpenGemini integration for configuration, default-engine limits and exact-version testing, and 2.1.0 release notes for release changes.