Make postgresql and PostgreSQL engine work against a ClickHouse instance - #110760
Conversation
…tance Connecting the `postgresql` table function or `PostgreSQL` table engine to a ClickHouse server's own PostgreSQL wire protocol port (`postgresql_port`) failed with `pqxx::broken_connection`. ClickHouse acts as a libpq/pqxx client against itself, and several pieces were missing: - The handler rejected the `BEGIN READ ONLY` that `pqxx::ReadTransaction` sends. Transaction-control statements (`BEGIN`, `START TRANSACTION`, `COMMIT`, `ROLLBACK`, `ABORT`, `END`) are now acknowledged without execution. - `fetchPostgreSQLTableStructure` calls `current_setting` and `format_type`, which did not exist. Added them as functions. - The emulated `pg_namespace`, `pg_class` and `pg_attribute` were static stubs; they now also reflect the server's real databases, tables and columns from `system.databases`, `system.tables` and `system.columns`. Tables of the connected database are exposed under the default `public` schema. - `pqxx` reads every result set with `COPY (query) TO STDOUT`, but `ParserCopyQuery` discarded the inner query. It is now captured and executed. - The `COPY ... TO STDOUT` reply was missing the `CommandComplete` after `CopyDone`, and sent a whole block as a single `CopyData` message instead of one message per row, both of which broke libpq/pqxx. Fixed. Closes: #52639 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Workflow [PR], commit [137ee2a] Summary: ✅
AI ReviewSummaryThis PR substantially improves self-connection over the PostgreSQL wire for the Findings
Final Verdict
|
Addresses the review feedback on the PostgreSQL self-connect path (#110760): - The emulated `pg_catalog` advertised `UInt64`, `Int128`, `UInt128`, `Int256` and `UInt256` columns as `bigint` (OID 20). Schema inference in `fetchPostgreSQLTableStructure` then mapped them back to `Int64`, so a self-connected value outside the `Int64` range (e.g. `UInt64` = `18446744073709551615`) was rejected. These types are now advertised as `numeric` with a real `atttypmod` encoding a precision wide enough to hold every value, so `convertPostgreSQLDataType` recovers a `Decimal` (or `Int256`) that preserves the range. `UInt32`/`Int64`, which fit into `bigint`, keep OID 20. - `atttypmod` was hardcoded to -1, so a self-connected `Decimal(p, s)` was discovered as bare `numeric` (widened to `Decimal128(38, 19)`). It is now derived from `system.columns.numeric_precision` / `numeric_scale` and encoded the way PostgreSQL does (`((precision << 16) | scale) + 4`), and `format_type` decodes it back into `numeric(p, s)`, so `Decimal` round-trips with its exact type. - `COPY ... TO STDOUT` serialized a whole block and split the result on `'\n'` to produce one `CopyData` message per row. That only works for the default text/TSV output (newlines inside values are escaped) and corrupts formats where a row is not a single physical line - e.g. a quoted `CSV` field containing a newline, or the `Binary` format, which is not newline-delimited and produced an empty stream. Each row is now serialized into its own `CopyData` message, which is correct for any output format. Adds integration tests for the wide/Decimal type round-trip and for a multi-line value streamed through `COPY ... TO STDOUT WITH FORMAT csv`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ClickHouse does not pad `Decimal` values with trailing zeros on output (`1.5000` is printed as `1.5`), so use a fractional part ending in a non-zero digit to keep the expected string unambiguous. Verified against a local server: the `postgresql()` self-connect reads the row back byte-for-byte. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review: `convertDataTypeToPostgresColumnTypeSpec` advertised every integer type wider than a signed 64-bit `bigint` (`UInt64`, `Int128`, `UInt128`, `Int256`, `UInt256`) and any type it did not enumerate (including `Nullable`/`LowCardinality`-wrapped ones) as `VARCHAR`, and `FieldDescription` hardcoded the type modifier to -1. A `Decimal(p, s)` therefore lost its precision and scale over the wire. Now the wide/unsigned integer types and `Decimal` are advertised as `numeric` (OID 1700) with a real `atttypmod` encoded exactly as PostgreSQL does (`((precision << 16) | scale) + 4`), and `RowDescription` serializes that modifier. `Nullable`/`LowCardinality` are unwrapped first. This mirrors the table-name path in the emulated `pg_attribute` (see `PostgreSQLHandler`), so a direct PostgreSQL client reading a ClickHouse instance sees the correct per-column types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review: reading a self-connected ClickHouse `Array(...)` column back
through `postgresql(...)`/the `PostgreSQL` engine needs the values in the
PostgreSQL array-literal spelling (`{...}`, nested `{{...}}`, `NULL` for a null
element), because the reading side parses them with `pqxx::array_parser`.
ClickHouse's `serializeText` encloses arrays in `[...]` instead, which the
parser rejects.
Add a shared `writePostgreSQLArrayText` helper (used by both the
`PostgreSQLWire` output format and, in a later commit, the COPY-to-STDOUT path
of `PostgreSQLHandler`) that renders an array cell as a PostgreSQL literal,
double-quoting every scalar element (the parser strips the quotes before the
per-element parser runs, so quoting numbers is harmless and quoting text is
required) and escaping `"` and `\`.
`format_type` now decodes the PostgreSQL array type OIDs (`_int4` etc.) to
`<element>[]`, so that schema inference in `fetchPostgreSQLTableStructure`
recovers the array type instead of `text`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rrays Address three review findings in the emulated `pg_catalog`/handler: - `isTransactionControlQuery` matched a `BEGIN`/`COMMIT`/... prefix on the whole simple-query string, so a multi-statement query such as `BEGIN READ ONLY; SELECT 1` was swallowed as a no-op and the trailing statement was silently dropped. It now returns false when an internal semicolon remains after trimming, so such input falls through to normal processing instead. - Relation OIDs were a truncated hash (`cityHash64(...) % 1e9`), which collides at realistic catalog sizes; a collision merges the column sets of two tables because `pg_class.oid` and `pg_attribute.attrelid` are joined during schema inference. Introduce a shared, unfiltered `pg_class_oids` view that assigns a dense, unique OID per `(database, table)` via `row_number() OVER (ORDER BY database, name)`, and take both `pg_class.oid` and `pg_attribute.attrelid` from it, so they always line up and never collide. - `Array(...)` columns fell through to OID 25 (`text`) with `attndims = 0`, so `postgresql(..., 'arr_table')` inferred `String`. `pg_attribute` now advertises the PostgreSQL array-type OID of the element with `attndims` = the array nesting depth, and the COPY-to-STDOUT path streams array values in PostgreSQL literal form via `writePostgreSQLArrayText` so they can be read back. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…L self-connect `test_array_type_roundtrip` reads `Array(Int32)`, `Array(String)`, `Array(Array(Int32))` and `Array(UInt64)` back through `postgresql(...)` and checks both the inferred types (arrays, not `String`) and the values, including nested and empty arrays. `test_wire_types_for_wide_and_decimal` connects a PostgreSQL client and asserts the `RowDescription` type OIDs: `Int64` is `bigint` (20) while `UInt64`, `Int128` and `Decimal` are `numeric` (1700), not `varchar`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tribute
The emulated `pg_attribute` derived `attndims` with
`countSubstrings(type, 'Array(')`, which also counted `Array(` occurrences
nested inside `Map`/`Tuple` type arguments. A column such as
`Map(String, Array(UInt8))` or `Tuple(a Array(Int32))` was therefore
advertised as a PostgreSQL array (`text[]`), `fetchPostgreSQLTableStructure`
inferred `Array(String)`, and the self-connect read failed because the COPY
path streams such columns in their ClickHouse text form, not as a PostgreSQL
array literal.
Now only the `Array(` occurrences among the leading
`Nullable(`/`LowCardinality(`/`Array(` wrappers count, matching how the
base type is extracted, so `Map`/`Tuple` columns stay text (read back as
`String`) while genuine top-level arrays keep their dimensions. Adds an
integration test.
Addresses the review finding in
#110760 (comment)
The TabSeparated output format backslash-escapes single quotes inside
String values, so the Map column read back as String comes out as
{\'k\':[1,2]}, not {'k':[1,2]}. Verified against a local server.
A constant array expression (e.g. `SELECT [1, 2]`) can reach the serializer as a `ColumnConst` when the caller does not materialize its input; unwrap it to the underlying `ColumnArray` instead of failing the `assert_cast`. The unwrapping recurses with row 0 rather than materializing, so a per-row caller does not copy the whole column on every row.
Two fixes to the binary COPY path:
- The pre-rendering of `Array(...)` columns into PostgreSQL array
literals (`{...}`) now applies only to the text COPY formats
(TSV/CSV), where libpq/pqxx expect that spelling. The binary format
keeps the original array columns, so it serializes real array values
instead of text literals wrapped into `String`.
- `toString(ASTCopyQuery::Formats::Binary)` returned "Binary", which is
not the name of any ClickHouse format, so `COPY ... TO STDOUT WITH
FORMAT binary` always failed with `UNKNOWN_FORMAT` before even
reaching the data. It now maps to `RowBinary` - the same format the
`COPY ... FROM STDIN` path already used (its now-redundant local
format switch is replaced with a `toString` call).
…ulated `pg_attribute` `system.columns.numeric_precision` / `numeric_scale` are only populated for top-level numeric columns, so an `Array(Decimal(p, s))` column fell back to `atttypmod = -1`: `format_type` rendered a bare `numeric[]` and schema inference on the reading side collapsed the element type to `Decimal(38, 19)`. Parse the precision and scale out of the type name itself (skipping only the leading `Nullable(`/`LowCardinality(`/`Array(` wrappers, the same prefix used for the base type and `attndims`), so the element type round-trips exactly. A `Decimal` buried in a `Map`/`Tuple` argument list is not picked up - such columns are exposed as text as before.
…rrays The three cases the review asked to cover: a constant array expression selected over the wire (exercises the `ColumnConst` path of the array serializer), a self-connect round-trip of `Array(Decimal(p, s))` preserving the element precision and scale, and a `COPY ... TO STDOUT WITH FORMAT binary` query with an array column (the payload stays `RowBinary`, not a PostgreSQL text literal).
The `COPY (query) TO STDOUT` path now goes through the shared `parseOptions`, which exposed two protocol-correctness issues flagged in review: - The format parser lower-cased the name into `format_name` but then compared the original spelling, so case-insensitive PostgreSQL keywords like `FORMAT CSV` were rejected; and the `tsv` branch mistakenly selected `CSV`. Now the normalized `format_name` is compared, `tsv`/`text` map to `TSV`, and `csv` maps to `CSV`. - `WITH FORMAT binary` was wired to ClickHouse's `RowBinary`, but PostgreSQL binary `COPY` has its own wire format (a `PGCOPY` header and per-field length framing) that we do not implement, while `CopyInResponse` / `CopyOutResponse` always advertise the text format code. That handed real PostgreSQL binary-copy clients an incompatible payload. It is now rejected with a clear error before any COPY response is sent. The text and CSV formats cover the self-connect use case (ClickHouse reads results with `pqxx`, which uses text `COPY`). `render_arrays_as_text` in the `COPY TO` handler is dropped: binary is rejected earlier, so the array-to-PostgreSQL-literal path is always taken. Tests: `test_copy_to_stdout_binary_is_rejected` checks the rejection (upper-case spelling too), and `test_copy_to_stdout_format_is_case_insensitive` checks that `FORMAT CSV`/`FORMAT TSV` are matched case-insensitively and that `TSV` produces tab-separated (not comma-separated) output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The integration test `test_copy_to_stdout_binary_is_rejected` failed consistently (not flaky): a `psycopg2` client issuing `COPY (...) TO STDOUT WITH FORMAT BINARY` saw `server closed the connection unexpectedly` instead of the expected `binary COPY format is not supported` error. Root cause: `PostgreSQLHandler::processCopyQuery` rejected binary `COPY` by throwing. The throw propagated through `processQuery` (which sends an `ErrorResponse` and re-throws) up to `run`, which caught it as a `Poco::Exception` and returned, closing the connection right after the `ErrorResponse` and before the `ReadyForQuery` that completes the command cycle. A `libpq`/`psycopg2` client still reading the command result then hit EOF while waiting for `ReadyForQuery`, discarded the partial error, and reported a lost connection. The plain `psql` REPL happened to surface the message due to timing, which is why manual testing missed it. An unsupported `COPY` option is an ordinary query error, not a fatal connection error. Reject binary `COPY` by sending an `ErrorResponse` and returning (marking the query as handled) instead of throwing, so the run loop follows with `ReadyForQuery` and the connection stays open - the canonical PostgreSQL "query failed" flow, which every client surfaces correctly. Validated end-to-end against a local server with a real `psycopg2` `copy_expert` client: binary `COPY` (upper- and lower-case) is now rejected with the clear message, the connection remains usable afterwards, and the text/CSV `COPY` and self-connect `postgresql()` read paths are unchanged. CI report: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=110760&sha=9782b7a5a9554b439a16047efae662c035ef306d&name_0=PR&name_1=Integration%20tests%20%28amd_asan_ubsan%2C%20flaky%29 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… pg_attribute When `postgresql()` or the `PostgreSQL` engine self-connects to a ClickHouse instance, the emulated `pg_attribute` reported `attnotnull = 't'` for a top-level array whose element type is `Nullable(...)` (for example `Array(Nullable(Int32))`), because the column type does not start with `Nullable(`. Schema inference in `fetchPostgreSQLTableStructure` therefore inferred `Array(Int32)`, and `insertPostgreSQLValue` rewrote every `NULL` element to the element type's default - so `[1, NULL, 3]` came back as `[1, 0, 3]`, silently corrupting results. Detect the nullable element from the leading chain of `Nullable(` / `LowCardinality(` / `Array(` wrappers instead of only the very start of the type name: a `Nullable(` can only appear in that chain right before the innermost scalar, so its presence marks the value (an array element, or the scalar itself) as nullable. `Map`/`Tuple` columns, which are exposed as text, are unaffected because they never start with one of those wrappers. Added an integration test `test_array_of_nullable_roundtrip` covering `Array(Nullable(Int32))` and `Array(Nullable(String))`.
…ire path A direct PostgreSQL client running a `SELECT` over ClickHouse's PostgreSQL wire protocol received type OID `1043` (`varchar`) for `DateTime` and `DateTime64` columns, because `convertDataTypeToPostgresColumnTypeSpec` fell through to the `VARCHAR` default for those types. The table-name path already advertises them as `timestamp` in the emulated `pg_attribute`, so the wire `RowDescription` was inconsistent and clients could not treat the values as timestamps. Map `DateTime` and `DateTime64` to PostgreSQL `timestamp` (OID `1114`, added as `ColumnType::TIMESTAMP`). The value is still rendered as text - ClickHouse's `YYYY-MM-DD hh:mm:ss[.ffffff]` form is exactly PostgreSQL's timestamp text format - so a client such as psycopg2 parses it into a native datetime. Added an integration test `test_wire_types_for_datetime` asserting the wire OID is `1114` and the values parse back correctly.
The stateless test only *grants* `SELECT` on `system.tables` and `system.columns` - it never queries them - but the style check `various_checks.sh` only greps for the table names and then demands a `database = currentDatabase()` condition somewhere in the file. Grant `system.*` instead and name the tables in a comment (comment lines are exempt from that grep), which keeps the test's intent and clears the check. A red style check drops every other CI job.
| /// agree on the same relation. | ||
| where += postgres_schema.empty() | ||
| ? " AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public')" | ||
| ? " AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = current_schema())" |
There was a problem hiding this comment.
current_schema() is now the default relation lookup here, but the same invariant is still violated by the sibling helpers in this file. fetchPostgreSQLTablesList at lines 45-47 and the replica-identity fallback at lines 464-469 still hardcode public when postgres_schema is empty, so a self-connected DatabasePostgreSQL / MaterializedPostgreSQL path with no explicit schema will still enumerate or resolve against public instead of the connected database. That leaves the new "no schema = current schema" contract true for the physical-column lookup only, not for the other PostgreSQL-facing callers that share this helper.
| if (chunk.empty()) | ||
| break; | ||
|
|
||
| executor->push(convert_arrays(std::move(chunk))); |
There was a problem hiding this comment.
COPY FROM STDIN now stages and validates the payload correctly, but the success tag is still hardcoded to COPY 0. CommandComplete already treats COPY as a row-count-bearing command, and clients surface that tag via statusmessage / user-visible output, so a successful import of N rows is currently indistinguishable from a zero-row copy. Please accumulate the inserted row count in the generate/push loop and send that instead of 0.
LLVM Coverage Report
Changed lines: Changed C/C++ lines covered: 1104/1261 (87.55%) · Uncovered code |
…he wrong query_log rows 04327_reader_executor_metrics, 04328_reader_executor_kpi_async_metric and 04341_reader_executor_long_connections assert per-query ProfileEvents from system.query_log while filtering current_database = currentDatabase() and picking a single row with ORDER BY event_time_microseconds DESC LIMIT 1. Under parallel replicas that selects the wrong rows and the assertions silently read 0. 04341 has failed twice on master (9a5b523, 14fdcfc); 04328 failed on PR ClickHouse#110760's ParallelReplicas job. ReaderExecutor::Stats::add increments the counters on whichever thread performs the read, i.e. on the replica serving the mark. Remote ProfileEvents reach the initiator only as a Protocol::Server::ProfileEvents packet whose only drain is client transmission, while the query_log ProfileEvents column comes from the thread-group snapshot; nothing merges the two. The event-bearing rows are therefore secondary rows whose current_database is 'default', which the tests exclude. The engine attribution is correct; the tests' row selection is not. Resolve the initiator rows by current_database + is_initial_query = 1, then aggregate over every row of those queries via initial_query_id, the idiom merged in ClickHouse#112263. Converting the single-row pick into an aggregate is required rather than cosmetic: the initiator's query_log row is written last, so LIMIT 1 over the widened row set still picks the event-free initiator deterministically (measured 12/12). Relational columns become the same relation between the two sums, which is sound because the quantities are additive and was verified in both regimes with real asymmetric data (one replica over-read 989693 against 659801 requested while another was exactly equal). The zero-columns become sum(X) = 0, which is strictly stronger than before. With parallel_replicas_local_plan = 0 all three tests fail before this change with output byte-identical to the CI diffs and pass after it; reverting either the row selection or the aggregation reddens them again; 300/300 runs pass through clickhouse-test (50x each test, randomized and non-randomized). 04328's asynchronous_metric_log assertion is left byte-identical: it reads a server-global counter and is the control that isolates the mechanism. No src, .reference, tag or setting change. The engine-level question of merging remote ProfileEvents into the initiator's query_log row is tracked separately by issue ClickHouse#112377 and is deliberately not attempted here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes: #52639
The
postgresqltable function and thePostgreSQLtable engine can now connect to a ClickHouse server through its own PostgreSQL wire protocol port (postgresql_port). Previously this failed withpqxx::broken_connection/pqxx::failure, because ClickHouse - acting as a libpq/pqxx client against itself - relies on several pieces of PostgreSQL compatibility that were missing.Motivation: it is natural to expect that pointing
postgresql(...)at a ClickHouse instance (which speaks the PostgreSQL protocol) just works, for example for federated queries between ClickHouse servers.Scope: this PR covers the table-name path -
postgresql(host, database, table, ...)and thePostgreSQLengine pointed at a named table. Thequery(...)-backed variant is explicitly out of scope: schema inference for it (doQueryResultStructure) issues a PostgreSQL-only type-resolution query (unnest(ARRAY[...]::oid[], ...) WITH ORDINALITY) that the ClickHouse server does not parse, so making it work requires server-side support forunnestas a table function,WITH ORDINALITYandARRAY[...]::type[]casts - a separate, larger piece of work. TheRowDescriptiongroundwork for it (correct type OIDs and modifiers on the wire) is already included.What was fixed:
BEGIN [READ ONLY],START TRANSACTION,COMMIT,ROLLBACK,ABORT,END) that libpq/pqxx send around statements are now acknowledged instead of failing to parse and dropping the connection. Multi-statement strings are not misdetected as transaction control.format_typeandcurrent_setting, whichfetchPostgreSQLTableStructureissues during introspection.current_setting('search_path')reports the connected database - the schema unqualified names actually resolve in (it agrees withcurrent_schema()) - not PostgreSQL's defaultpublic.pg_catalogtablespg_namespace,pg_classandpg_attributenow also reflect the server's real databases, tables and columns (fromsystem.databases,system.tablesandsystem.columns), so a table's structure can actually be discovered. Every ClickHouse database is exposed as a schema under its own name, and there is deliberately no syntheticpublicschema aliasing the connected database: a schema name has to denote the same relation for schema discovery and for theCOPYthat streams the rows. A caller that does not specify a schema - which is what the reader does by default - resolves the table throughcurrent_schema()(a new alias ofcurrentDatabase, next to the existingSCHEMA), that is in the connected database, exactly where the unqualified data statements of the read and write paths look; on PostgreSQL with the default search pathcurrent_schema()ispublic, so nothing changes there. Relation OIDs are assigned collision-free (a dense numbering shared bypg_classandpg_attribute).bigint(UInt64,Int128/UInt128,Int256/UInt256) andDecimal(p, s)are advertised asnumericwith a real type modifier, so schema inference recovers a type that preserves the range and the exact precision/scale. One exception:numericis a signed type, and its established mapping in ClickHouse (numeric(p, 0)withp > 76becomesInt256) cannot represent the upper half ofUInt256, so a self-connectedUInt256column is read asInt256; a value above theInt256maximum is rejected with an out-of-range error rather than silently corrupted. Distinguishing the two would need a signal outside thenumerictype modifier and is left out of this change.Array(...)columns are advertised with the PostgreSQL array OID of their element plusattndims(only leadingArray(wrappers count - an array nested inside aMap/Tupledoes not make the column an array), and values are streamed in PostgreSQL array-literal form ({...}) sopqxx::array_parsercan read them back.DateTime/DateTime64columns - with or without an explicit time zone - are deliberately exposed as text rather thantimestamp: PostgreSQLtimestamp without time zonecannot carry the time zone the wall-clock text is rendered in (for a column without an explicit zone that is the source server's default), so advertising it would let a reader whose default time zone differs silently shift the stored epochs; as text the values round-trip losslessly asString.ParserCopyQuerynow keeps the inner query ofCOPY (query) TO STDOUT(the form pqxx uses to stream every result set) instead of discarding it.COPY ... TO STDOUTreply now sendsCommandCompleteafterCopyDone, and oneCopyDatamessage per row instead of one per block, both of which are required by libpq/pqxx.COPY ... FROM STDINaccepts the same array spelling thatCOPY ... TO STDOUTwrites: PostgreSQL array literals ({...}, nested, withNULLelements) are translated into the targetArray(...)type, so an array table copied out of the server can be copied straight back in.COPY ... FROM STDINstages and parses the whole payload before a single row reaches the insert pipeline, so a client abort (CopyFail) or a malformed row leaves the target table untouched and the connection usable. It is not fully atomic beyond that point: an error raised deeper in the insert pipeline (a materialized view, a storage error) is not rolled back, because a ClickHouseINSERTis not transactional - such aCOPYbehaves exactly like a plainINSERTof the same data.A new integration test
test_postgresql_protocol_ch_clientcovers the round trip: the issue's query, reading a table with several types (includingNullable, wide integers,Decimaland arrays), the wire-level type OIDs seen by a direct PostgreSQL client,Map/Tuplecolumns staying text, thePostgreSQLengine, and addressing a database via an explicit schema.Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
The
postgresqltable function andPostgreSQLtable engine can now be used to connect to another ClickHouse server over the PostgreSQL protocol (when a table name is used; thequery(...)variant is not supported yet). Added the PostgreSQL-compatibility functionsformat_typeandcurrent_setting.Version info
26.8.1.591(included in26.8and later)