All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Added
- Telemetry is actually emitted.
InfluxElixir.Telemetrydocumented[:influx_elixir, :write | :query, ...]events, but nothing in the library called it.Write.Writer.write/3(henceInfluxElixir.write/3and everyBatchWriterflush) now emits the write span withdatabase,bytesandpoint_count;InfluxElixir.query_sql/3,execute_sql/3,query_influxql/3andquery_flux/3emit the query span withdatabase,transport(the client module) and, for list results,row_count.:stopmetadata carriesresult: :ok | :error. BatchWriterandWrite.Writeraccept a:clientoption to write with a specific client module instead of the configured one.InfluxElixir.Configknows:timeout,:batch_writerand:finch_name.
Fixed
Client.HTTPnever set Finch'spool_timeout(#14), so every request waited at most Finch's default 5 s to check a connection out of the pool no matter how generous:timeoutwas, and against a slow multi-node endpoint failed with a transport:timeoutat five seconds. A:pool_timeoutoption now resolves like:timeout(per-call opt → connection → 5_000) and is passed on every request, including the streaming query. Verified against InfluxDB 3 Core with a size-1 pool held by a sleeping stream — which also showed that Finch raises on a checkout timeout rather than returning an error, so the exception used to escapequery_sql/3. It is now{:error, {:connection_error, :pool_timeout}}, and the streaming query raisesInfluxElixir.StreamErrorwithreason: :pool_timeout.- Flight and HTTP now return the same
timevalues.Flight.Readerdecoded Timestamp columns to raw integers while the HTTP path yieldsDateTime; the reader now reads the ArrowTimeUnitand converts.Query.ResponseParseralso treated InfluxDB 3's zone-less JSON timestamps ("2023-11-14T22:13:20.123456789") as opaque strings becauseDateTime.from_iso8601/1rejects them; they are now parsed as UTC. Verified against InfluxDB 3 Core: identical rows on both transports. transport: :flightwas documented but ignored by the facade,Query.SQLand the usage rules; every query went over HTTP.Client.HTTP.query_sql/3now dispatches toFlight.Clientwhentransport: :flightis given, using the connection's host/token, the resolved database, andflight_port(opt, connection, or 443).params:are rejected over Flight instead of being dropped. Verified against InfluxDB 3 Core's Flight endpoint.BatchWriterretried 4xx responses. The discard clause matched{:error, {:http_error, status}}, a shape no client produces, so a rejected batch (bad line protocol, unknown database) was retried with backoff untilmax_retriesran out. It now matches the clients'%{status: 4xx}and drops the batch on the first response. The retry path is covered by a real transport error against a closed port.ConnectionSupervisorhanded the batch writer the raw config instead of the initialised connection, so abatch_writer:underClient.Localcrashed on its first flush. Covered by a supervisor-level test.ConnectionSupervisorvalidates HTTP connection config. A typo such asdefault_database:(which the facade and application docs themselves used) was silently ignored; withClient.HTTPit now fails at startup with aNimbleOptions.ValidationError.Client.Localconfigs are not validated.
Changed
timeis aDateTimeon every client and transport.Client.LocalreturnedtimeandDATE_BINbuckets as ISO 8601 strings, while the HTTP path (once its zone-less parsing was fixed, see below) and Flight returnDateTime. All three now returnDateTimewith microsecond precision, so the contract suite asserts one instant across Local, HTTP and Flight. Code that compared Local'stimeto a string must use aDateTime(six-digit sigil orDateTime.compare/2).InfluxElixir.write/3goes throughWrite.Writer, so payloads over 1 KB are gzipped likeBatchWriterflushes already were.Flight.Readerdecodes fixed-width columns with binary comprehensions (one pass, no per-element slicing) instead of indexedbinary_part/3.Client.Local.query_flux/3returns the long row shape real Flux returns: one row per field with_field/_value,_measurement,_time(aDateTime), the tags,resultand a per-seriestableindex, ordered by table then time.filter(fn: (r) => r._field == "...")is honoured. The old wide rows (%{"_measurement", "<field>" => v, "time"}) could not exercise consumer Flux handling; verified against InfluxDB 2.7.Client.HTTP.query_flux/3requests#datatypeannotations so CSV cells come back typed (double,long,unsignedLong,boolean, RFC3339 →DateTime) instead of as strings.Query.ResponseParser.coerce_types/1also converts_time,_startand_stop.parse/2returns{:error, {:unexpected_json, term}}for a JSON scalar body instead of raisingCaseClauseError.
Added
api_version: :v2 | :v3connection option (InfluxElixir.Config). Required for InfluxDB 2.x: a v2 server answers200to the v3 write path without storing anything, so writes silently vanished and malformed line protocol or an unknown bucket reported success. With:v2the client usesPOST /api/v2/write?org=&bucket=&precision=ns|us|ms|s.
Fixed
Client.HTTP.create_bucket/3works against real InfluxDB v2. It sent"orgID": "", which v2 rejects (id must have a length of 16 bytes). The org ID is now resolved from the connection's:orgname (org_id:overrides). Creating a bucket that already exists is treated as success, matchingClient.Local.Client.HTTP.delete_bucket/2accepts a bucket name. v2 deletes by ID; the name is resolved viaGET /api/v2/buckets?name=, so the same call works againstClient.Local. A 16-hex argument is used as an ID directly.- Flux CSV parsing uses NimbleCSV. The hand-rolled splitter left
\ron every last cell and header, turned the blank line between tables into a row and the next table's header into data, and broke quoted cells containing commas. All were observed against InfluxDB 2.7. LineProtocolfloats no longer lose precision.{:decimals, 17}formatting wrote1.0e-20as0.0; the shortest round-trip form is used (1.0e-20,2.5e-7), which InfluxDB 3 Core accepts and reads back exactly.Telemetry.write_start/1andquery_start/1emit wall-clocksystem_time. They emittedSystem.monotonic_time/0under that key, an arbitrary offset that is useless as a timestamp. Amonotonic_timemeasurement is emitted alongside, matching:telemetry.span/3.Flight.Client.query/3closes the gRPC channel whenDoGetfails; it was only disconnected on success.Client.Local.stop/1no longer races the owner process's ETS cleanup. Called from anon_exitafter the test process had exited, the:ets.info/1guard could pass and:ets.delete/1then raiseArgumentError, failing the test intermittently.Flight.Readerrow assembly is linear in the batch's row count. Cells were read withEnum.at/2on the column lists for every row, which made decoding a record batch quadratic; columns are now tuples read withelem/2.
Changed
Client.HTTProutes every request through onerequest/7helper that maps the status to{:ok, response}/{:error, %{status, body}}/{:error, {:connection_error, reason}}, replacing fourteen copies of the same three-clausecase. No behavioural change.
[0.1.20] - 2026-09-10
Changed
Client.Localordered aggregates now use the InfluxDB v3 SQL spelling (#13).first_value(field ORDER BY col [ASC|DESC])andlast_value(field ORDER BY col [ASC|DESC])are parsed and executed, includingGROUP BY <columns>for "latest value per group" queries. The InfluxQL-styleFIRST(field, time)/LAST(field, time)the double previously accepted are rejected: InfluxDB v3 fails planning on them (Invalid function 'last'), so accepting them let a query pass tests and 400 in production. The rejection names the v3 spelling.first_value/last_valuewithout an innerORDER BYare also rejected — DataFusion returns an arbitrary group member in that case, which the double cannot reproduce. Verified against a live InfluxDB 3 Core; the shared contract suite now passes against the real engine (it previously failed on the twoFIRST/LASTtests).Client.Localparser rejections are prefixedClient.Local:so anunsupported column expressionerror reads as a limitation of the test double rather than of InfluxDB. Plain aggregates (AVG,SUM,COUNT,MIN,MAX) now reject a second argument, as the real engine does.Client.Localreports a missing table the way the real engine does.query_sql/3on an unknown measurement returned{:error, {:table_not_found, name}}whileClient.HTTPreturns{:error, %{status: 400, body: "Error during planning: table ... not found"}}, and the streaming path mapped it to a 404. Both now produce the 400 planning error, so consumer code that matches%{status: 400}can be exercised against the double. Code matching the old tuple must be updated.
Fixed
BatchWriternow honours its:databaseoption. The value was stored in state and never forwarded to the write, so every flush landed in the connection's default database. It is now the write target unless:write_optsnames a:databaseexplicitly.Client.Local(:v2profile) accepts writes to buckets created withcreate_bucket/3. Writes only checked thedatabases:seeded at start, so a bucket created through the API returned404 database not found.Client.Localno longer re-types quoted string literals (#12). A bound string param or quoted literal such as'08338636'was parsed back throughInteger.parse, dropping the leading zero and changing the type, soWHERE repcode = $rc/IN ($rc)over zero-padded identifiers never matched while real InfluxDB v3 matched correctly. Quoted literals are now strings; only bare literals are typed. Comparing a string literal against a numeric field compares the field's text rendering, which is what DataFusion does (amount >= '1000.00'is lexical and matches500.0on the real engine too), so that footgun now fails in tests the same way it fails in production.- Linear-time accumulation in
Client.LocalWHERE parsing andFlight.Readerbatch decoding. Both appended with++inside a reduce, which is quadratic in the number of clauses / record batches. Client.Localparam substitution is whole-placeholder and single-pass.$hwas previously replaced inside$hmin, and a substituted string value containing another placeholder's name could be re-substituted.
[0.1.19] - 2026-07-08
Fixed
InfluxElixir.Client.Local.query_sql_stream/3now mirrors the HTTP client's error semantics (#11).Client.Localis the documented drop-in test double forClient.HTTP, but it still returned an empty stream on a query error or an unsupported operation whileClient.HTTPraised — so consumer code that rescuesInfluxElixir.StreamError(to avoid treating an outage as "no data") could not be exercised against the test double. It now raisesInfluxElixir.StreamErroron enumeration for both cases, matchingClient.HTTP.
Added
- Tests covering the Local
:http_status/:unsupportedstream-error paths and lazy (deferred) raising.
[0.1.18] - 2026-07-08
Fixed
query_sql_stream/3(HTTP transport) now truly streams and no longer swallows errors (#10). Previously it usedFinch.request/3, which buffered the entire response body and eagerly decoded every JSONL line before yielding — giving zero memory benefit overquery_sql/3— and it halted to an empty list on non-2xx statuses, transport errors, and unresolved databases, so every failure class looked like "zero rows". It now consumes the response withFinch.stream/5, decoding JSONL line-by-line with back-pressure (constant memory), and raises anInfluxElixir.StreamErroron a missing database, a non-success HTTP status, or a transport error when the stream is enumerated.
Added
InfluxElixir.StreamErrorexception, raised while consuming a streaming query that cannot produce rows. Carries a:kind(:no_database | :http_status | :transport | :decode | :unsupported) plus:status/:body/:reasoncontext.InfluxElixir.StreamError.stream/1builds anEnumerable.t()that defers the raise to enumeration, shared by both client implementations.- Tests covering the HTTP
:no_database/:transportpaths (real Finch pool, no mocking) andStreamErrormessage construction.
[0.1.17] - 2026-06-30
Changed
- Loosened
decimalconstraint to~> 2.0 or ~> 3.0(#9). Unblocks downstream apps from upgrading pastdecimal 2.4.1(EEF-CVE-2026-32686) and from picking upecto ~> 3.14 → ash ~> 3.29chains. Surface used (Decimal.to_string/2,%Decimal{}pattern) is stable across 2 → 3. - Loosened
grpcconstraint to~> 0.11 or ~> 1.0(#9). Unblocks downstream apps from upgrading pastgrpc 0.11.5(5 CVEs including EEF-CVE-2026-48853). - Defaulted the Flight client to the Mint gRPC adapter so the library doesn't
pull in
:gun, which becameoptionalingrpc 1.0. Mint is already available viafinch. InfluxElixir.Supervisornow skips addingGRPC.Client.Supervisoras a child whengrpc 1.0+is present (1.0 auto-starts it via its ownApplication).
Fixed
LocalClientnow supportsCOUNT(*)as a scalar and DATE_BIN-bucketed aggregate.LocalClientWHERE-clause parser now returns a 400 error for unrecognised clauses (e.g.LIKE) instead of silently matching all rows.
Added
- Regression tests for
COUNT(*), explicit column-listSELECT,INoperator narrowing, write-timestamp preservation, and silent WHERE drop.
Earlier releases
- Initial project setup with module stubs
- CI pipeline with quality checks and auto-publish to Hex.pm