Uh oh!
There was an error while loading. Please reload this page.
[feat](catalog) support ADBC catalog that reads external sources over Arrow - #66331
Conversation
hello-stephen
commented
Jul 31, 2026
Thank you for your contribution to Apache Doris. Please clearly describe your PR:
|
77ee3bd to
a6e8d66Comparemorningman
commented
Aug 2, 2026
run buildall |
hello-stephen
commented
Aug 2, 2026
Cloud UT Coverage ReportIncrement line coverage Increment coverage report
|
hello-stephen
commented
Aug 2, 2026
FE UT Coverage ReportIncrement line coverage |
hello-stephen
commented
Aug 2, 2026
BE UT Coverage ReportIncrement line coverage Increment coverage report
|
hello-stephen
commented
Aug 2, 2026
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
hello-stephen
commented
Aug 2, 2026
FE Regression Coverage ReportIncrement line coverage |
### What problem does this PR solve? Related Issue: #65615 Related PR: #66331 Problem Summary: Split out of #66331, which adds an `adbc` catalog type that reads an external source through an [Arrow Database Connectivity](https://arrow.apache.org/adbc/) driver. This PR carries only that PR's `thirdparty/` half, so the dependency can be reviewed and the build-env image rebuilt before the code that links against it lands. Nothing in the tree consumes these artifacts yet -- this PR adds one package to the thirdparty build and declares its license, and changes nothing else. **What comes out of it** | Artifact | How | Used by | |---|---|---| | `libadbc_driver_manager.a` | built from source | to be statically linked into `doris_be` (#66331) | | `libadbc_driver_jni.so` | built from source | to be loaded by the FE ADBC connector (#66331) | | `libadbc_driver_sqlite.so` | built from source | tests only, **not shipped** | | `libadbc_driver_flightsql.so` | prebuilt, from the official release wheel | tests only, **not shipped** | Doris ships no ADBC driver to users; a deployment supplies its own. The two drivers above exist so the ADBC code paths can be tested at all. **Three things upstream does that do not carry over** - *The SQLite driver needs a system SQLite3 development package*, which Doris does not ship and most build hosts lack. The source tree vendors the amalgamation but never references it from CMake, so it is compiled here into a scratch static library, handed to `FindSQLite3`, and dropped afterwards. It ends up statically inside the driver, leaving no sqlite artifacts in thirdparty. - *The JNI bridge header is generated by shelling out to Maven* (`java/driver/jni/CMakeLists.txt` runs `mvn -Pjni,javah compile`). Doing that would make this the first thirdparty package to require Maven, a Maven Central connection and a JDK 11+, while the build-env image runs this script with `JAVA_HOME` on JDK 8. The `javah` output is checked in as a patch instead and `jni_wrapper.cc` is compiled against it directly, needing nothing but `jni.h`. The patch header records how to regenerate it on a version bump. The prebuilt JNI binary inside upstream's Maven jar is not used either: it requires `GLIBC_2.34` and `GLIBCXX_3.4.31`, which excludes CentOS 7/8, Rocky 8 and Ubuntu 20.04. - *The Flight SQL driver is written in Go and no bare shared library is published*, so it is taken from the official release wheel -- a zip the existing download step already knows how to unpack -- rather than adding a Go toolchain to the thirdparty build. It is skipped on platforms upstream publishes no prebuilt binary for, the same way hyperscan is. **On the version pin** The source tree is tag `apache-arrow-adbc-24`, which is release C/Go 1.12.0 (the tag carries neither number). The prebuilt Flight SQL driver is pinned to that same release, and that is not cosmetic: ADBC partition descriptors are driver-private bytes, so every process that hands one to another must have loaded the very same driver build. `dist/LICENSE-dist.txt` gets the corresponding Apache-2.0 entry. It is the only file outside `thirdparty/` here.
a6e8d66 to
4315a84Comparemorningman
commented
Aug 3, 2026
run buildall |
hello-stephen
commented
Aug 3, 2026
FE UT Coverage ReportIncrement line coverage |
hello-stephen
commented
Aug 3, 2026
TPC-H: Total hot run time: 28798 ms |
hello-stephen
commented
Aug 3, 2026
TPC-DS: Total hot run time: 169203 ms |
hello-stephen
commented
Aug 3, 2026
ClickBench: Total hot run time: 25.15 s |
hello-stephen
commented
Aug 3, 2026
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
hello-stephen
commented
Aug 3, 2026
FE Regression Coverage ReportIncrement line coverage |
Nothing references the ADBC symbols yet, so the archive contributes no objects to the binary until AdbcDriverRegistry lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Doris serdes only accept the Arrow variants Doris itself emits: the string serde takes STRING, BINARY and FIXED_SIZE_BINARY and nothing else. Third-party ADBC drivers emit others -- DuckDB emits string_view, Go-based drivers may emit large_* and dictionary. Convert those before materialization, and fail with the offending type named when no Doris column can hold it, because silently wrong data is far worse than a loud error. Normalization loops rather than converting once: decoding dictionary<int32, large_utf8> leaves large_utf8 behind, which still needs converting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The driver manager dlopens on every AdbcDatabaseInit and keeps no cache of its own, so without this every scan range would reload the driver. Load each resolved path at most once and never dlclose: drivers carry global state and background threads -- Go runtimes especially -- so unloading one is a use-after-free hazard. Failed loads are cached too, so a bad path does not retry the dlopen once per scan range, and the failure message carries the path the user configured. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows the string-map shape jdbc_params and es_params already established. The partition descriptor is opaque binary, so it travels base64-encoded rather than as a typed field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors remote_doris_reader: the Arrow-stream-to-Block materialization is identical, only the stream source changes from Arrow Flight to ADBC. Each column is normalized before materialization, since third-party drivers emit Arrow variants the serdes reject. The reader drives the driver's own function table rather than the driver manager's free functions. Those re-dlopen on every AdbcDatabaseInit, which would defeat AdbcDriverRegistry. Databases are not pooled across scan ranges. That is a throughput optimization which only pays off once multiple partitions run concurrently, and adding an untestable caching layer now would only obscure the functional path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ADBC only has a v2 reader, and enable_file_scanner_v2 is a fuzzy=true session variable the regression harness flips at random, so honoring it would make ADBC queries fail on a coin flip. Force adbc onto v2, mirroring the existing transactional_hive force-to-v1. Loads are excluded: there is no ADBC load path, so widening the rule to cover them would only route them somewhere they still cannot run. is_supported also has to accept adbc under FORMAT_ARROW, otherwise the scanner would refuse the very ranges the operator forces onto it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things the unit tests cannot reach, because each needs the engine sitting in front of the connector. A table created behind Doris's back, by the sqlite3 CLI the fixture is already built with, must be queryable with no REFRESH at all. That works only if the connector answers the engine's re-list from the source; it is the end-to-end half of the rule, and it fails against a connector that serves that listing from memory. REFRESH CATALOG must reach the connector, asserted through a schema change rather than a new table: the engine clears its own schema copy either way, so a stale answer after the refresh can only have come from the connector. Without the hook the catalog would serve the schema it first read until the TTL expired, and REFRESH CATALOG does not rebuild the connector. The cache knobs, both directions: a catalog with the cache turned off still works, and an unparseable ttl fails at CREATE CATALOG. The second doubles as proof that the deployed plugin is the new one -- an older build ignores an unknown property, so the CREATE would succeed and the assertion would fail rather than pass quietly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1z6M8Gj9F9pErafGdfdjx
driver_checksum has been a property name with nothing behind it since the connector was written: a user could set it and Doris would ignore it. It is now checked at CREATE CATALOG, by MD5, against the file this FE resolves. It earns its place from what this catalog type asks of an operator. Doris ships no ADBC driver, so the library is placed by hand on every node and the copies have to stay identical -- and a wrong or stale copy announces nothing. It loads, it answers, and whatever it does differently arrives later as a query failure that never mentions a file. The property remains optional and stays honest about its reach: it sees this FE's copy and no BE's, so it does not verify that the nodes agree. What it gives an operator is a way to state which build the catalog was written for and have one node check itself against that. A checksum that cannot be computed fails rather than passes, or the property would be quietly optional on exactly the node that could not read the file. Not routed through the validation context's own checksum service: that one resolves against jdbc_drivers_dir and enforces a .jar grammar, neither of which applies here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1z6M8Gj9F9pErafGdfdjx
When a source refuses the pushed-down statement, its answer arrives in its own words -- typically a syntax error pointing at a quote character. Nothing in that says Doris wrote the statement, and nothing says which SQL Doris writes is a catalog property. That is the first wall anyone pointing this connector at something other than Doris walks into, because the default dialect is conservative ANSI and a source that wants something else rejects the very first query. The planning-time failure now names the dialect the statement was generated in and the property that changes it. Only the partitioned path can carry this: asking the driver to split a statement executes it, so FE is where that rejection lands. The single-statement path runs on BE and its error is BE's to improve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1z6M8Gj9F9pErafGdfdjx
…ment A TIMESTAMPTZ literal reaches the dialect already converted to UTC: in a +08:00 session, ts > '2023-01-01 00:00:00' arrives as the wall clock 2022-12-31 16:00:00. Standard SQL's TIMESTAMP '...' spelling carries no zone, so the source reads that UTC wall clock as its own local time and compares it against its column. On a source east of UTC that only widens the match and Doris narrows it again on what comes back. On one west of UTC it NARROWS the match, and the rows the source drops are rows the query wanted -- a scan cannot ask for rows the source never sent. There is no portable spelling that says which instant is meant, so the comparison stays with Doris, for the same reason NaN, null-safe equality and LIKE already do. Also records why this connector does not consult the catalog property enable.mapping.timestamp_tz when mapping a zoned arrow timestamp: ExternalCatalog stamps that property as "false" into every external catalog that does not name it, so reading it cannot tell a user who asked for wall clocks from one who said nothing, and honouring it would force DATETIMEV2 on every adbc catalog. This connector's default is TIMESTAMPTZ; making the property settable needs fe-core to let a connector supply its own default first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1z6M8Gj9F9pErafGdfdjx
Twenty suites over a live catalog: type mapping and semantics, complex and binary types, scan edges, predicate pushdown, column pruning, query shapes, source table models, metadata operations, nested catalogs, cross source joins, large data, outfile, MTMV, and negative cases. Several assertions are written against what the planner actually delivers rather than against what the SQL says, because Nereids normalises the predicate before any connector sees it: a one-column disjunction becomes an IN list, NOT is pushed through comparisons and connectives by De Morgan, column-plus-literal arithmetic is folded, and <=> against a non-null literal becomes =. Pinning the SQL spelling would have been asserting on the optimizer, so each of those keeps a shape the optimizer cannot rewrite -- a two-column OR, two-column arithmetic, two nullable columns of one type -- to hold the connector's own behaviour. Likewise count(*) is asserted as one narrow column, not zero: pruning an empty scan tuple puts the smallest slot back. Three expectations record losses this connector cannot avoid. IPV4 arrives as the address's 32 bits read signed, because Doris encodes it as int32 on both sides of the wire. A source datetime arrives as TIMESTAMPTZ, so comparisons cast it back before checking the instant. DBL_MAX cannot reach the test client at all -- Doris prints a double with 16 significant digits, which parses back as infinity -- so that row is compared inside Doris with a null-safe join instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F1z6M8Gj9F9pErafGdfdjx
Both of this connector's deployment-level settings move off fe.conf and into the plugin's own adbc.conf -- the channel apache#66347 introduced -- as drivers_dir and driver_secure_path, read through ConnectorConf.get. Neither has an fe.conf half. The two @ConfFields they used to be (adbc_drivers_dir, adbc_driver_secure_path) and the two DefaultConnectorContext env entries that forwarded them are removed rather than kept as a fallback: this connector has never shipped, so no deployment configured them anywhere else, and a key in fe-core is an engine change per connector setting -- which is what that channel exists to stop. The default drivers directory is computed in the connector from the doris_home the engine already publishes, so it stays <DORIS_HOME>/plugins/adbc_drivers. build.sh needs no change: it seeds a live <name>.conf from any *.conf.template found in a plugin zip. AdbcConnectorConfTest pins the template's name against ConnectorProvider.name() -- a template under any other name deploys a file the engine never opens, with every setting in it silently ignored -- and pins that an environment still carrying the old fe.conf keys does not resurrect a channel fe-core no longer feeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RgJjuW5w4jEKF9HorENTur
4315a84 to
1bc74d1Comparemorningman
commented
Aug 4, 2026
run buildall |
hello-stephen
commented
Aug 4, 2026
Cloud UT Coverage ReportIncrement line coverage Increment coverage report
|
hello-stephen
commented
Aug 4, 2026
FE UT Coverage ReportIncrement line coverage |
hello-stephen
commented
Aug 4, 2026
TPC-H: Total hot run time: 29211 ms |
hello-stephen
commented
Aug 4, 2026
TPC-DS: Total hot run time: 166274 ms |
hello-stephen
commented
Aug 4, 2026
ClickBench: Total hot run time: 29.16 s |
hello-stephen
commented
Aug 4, 2026
FE Regression Coverage ReportIncrement line coverage |
morningman
commented
Aug 4, 2026
run check_coverage |
PR approved by at least one committer and no changes requested. |
PR approved by anyone and no changes requested. |
Uh oh!
There was an error while loading. Please reload this page.
## Summary Adds user documentation for the ADBC Catalog introduced in apache/doris#66331. An ADBC Catalog reads an external data source through an [Arrow Database Connectivity](https://arrow.apache.org/adbc/) driver. Data is transferred in Arrow format instead of being converted value by value as in a JDBC catalog, and one scan is split by the driver's own result partitions so several BE nodes read it in parallel. Phase one targets Arrow Flight SQL sources, including another Doris cluster. The feature is experimental and ships in 5.0.0. ## What the doc covers - Overview, differences from a JDBC Catalog, applicable scenarios and a feature matrix - Driver deployment. Doris ships no ADBC driver, and the FE resolves `driver_url` into an absolute path that it sends to the BEs unchanged, so the same driver build must sit at the same absolute path on the FE and on every BE. The `adbc.conf` settings (`drivers_dir`, `driver_secure_path`) are documented alongside. - Catalog properties, organized as an unordered list in the same style as the Iceberg doc: `driver_url`, `uri`, `{DriverProperties}`, `{ConnectionProperties}`, `{ReadProperties}`, `{DriverOptions}`, `{CommonProperties}` - Examples: querying another Doris cluster, pinning the driver build with `driver_checksum`, requiring parallel reads, disabling parallel reads - Namespace mapping from ADBC's three naming levels onto the two a Doris external table has - Column type mapping from Arrow types, including unsigned widening and the behavior for unmappable types - Query operations: column pruning, predicate pushdown, `LIMIT` pushdown, `COUNT(*)`, inspecting the generated remote SQL, parallel reads - Metadata cache (`meta.cache.adbc.metadata.*`), manual refresh and observability - Limitations and FAQ ## Notes - Both the English page and the Chinese translation are included, and only the `current` version is touched. - `sidebars.ts` lists the new page after `doris-catalog`, since the ADBC catalog is the intended replacement for it. - Content was verified against the source on the feature branch rather than the PR description alone. Two places where the two disagree follow the code: `user` and `password` are documented as optional, because only `driver_url` and `uri` are actually required; and `drivers_dir` / `driver_secure_path` are documented as `adbc.conf` settings rather than `fe.conf` keys. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: morningman <moringman@apache.org> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
### What problem does this PR solve? Related Issue: apache#65615 Related PR: apache#66331 Problem Summary: Split out of apache#66331, which adds an `adbc` catalog type that reads an external source through an [Arrow Database Connectivity](https://arrow.apache.org/adbc/) driver. This PR carries only that PR's `thirdparty/` half, so the dependency can be reviewed and the build-env image rebuilt before the code that links against it lands. Nothing in the tree consumes these artifacts yet -- this PR adds one package to the thirdparty build and declares its license, and changes nothing else. **What comes out of it** | Artifact | How | Used by | |---|---|---| | `libadbc_driver_manager.a` | built from source | to be statically linked into `doris_be` (apache#66331) | | `libadbc_driver_jni.so` | built from source | to be loaded by the FE ADBC connector (apache#66331) | | `libadbc_driver_sqlite.so` | built from source | tests only, **not shipped** | | `libadbc_driver_flightsql.so` | prebuilt, from the official release wheel | tests only, **not shipped** | Doris ships no ADBC driver to users; a deployment supplies its own. The two drivers above exist so the ADBC code paths can be tested at all. **Three things upstream does that do not carry over** - *The SQLite driver needs a system SQLite3 development package*, which Doris does not ship and most build hosts lack. The source tree vendors the amalgamation but never references it from CMake, so it is compiled here into a scratch static library, handed to `FindSQLite3`, and dropped afterwards. It ends up statically inside the driver, leaving no sqlite artifacts in thirdparty. - *The JNI bridge header is generated by shelling out to Maven* (`java/driver/jni/CMakeLists.txt` runs `mvn -Pjni,javah compile`). Doing that would make this the first thirdparty package to require Maven, a Maven Central connection and a JDK 11+, while the build-env image runs this script with `JAVA_HOME` on JDK 8. The `javah` output is checked in as a patch instead and `jni_wrapper.cc` is compiled against it directly, needing nothing but `jni.h`. The patch header records how to regenerate it on a version bump. The prebuilt JNI binary inside upstream's Maven jar is not used either: it requires `GLIBC_2.34` and `GLIBCXX_3.4.31`, which excludes CentOS 7/8, Rocky 8 and Ubuntu 20.04. - *The Flight SQL driver is written in Go and no bare shared library is published*, so it is taken from the official release wheel -- a zip the existing download step already knows how to unpack -- rather than adding a Go toolchain to the thirdparty build. It is skipped on platforms upstream publishes no prebuilt binary for, the same way hyperscan is. **On the version pin** The source tree is tag `apache-arrow-adbc-24`, which is release C/Go 1.12.0 (the tag carries neither number). The prebuilt Flight SQL driver is pinned to that same release, and that is not cosmetic: ADBC partition descriptors are driver-private bytes, so every process that hands one to another must have loaded the very same driver build. `dist/LICENSE-dist.txt` gets the corresponding Apache-2.0 entry. It is the only file outside `thirdparty/` here.
… Arrow (apache#66331) ### What problem does this PR solve? Related Issue: apache#65615 Problem Summary: Doris reads an external database through JDBC today: the source's rows become JDBC objects and then Doris columns, one value at a time, across JNI. The source is also read by a single backend, because JDBC has no notion of how a result is partitioned. This PR adds a new catalog type, `adbc`, that reads an external source through an [Arrow Database Connectivity](https://arrow.apache.org/adbc/) driver. Data arrives as Arrow record batches and is handed to BE's scanner without a per-value conversion, and a scan is split into the driver's own result partitions so that N backends read N partitions in parallel. Neither is reachable through JDBC at all. ```sql CREATE CATALOG remote_source PROPERTIES ( "type" = "adbc", "driver_url" = "libadbc_driver_flightsql.so", "uri" = "grpc://remote-doris:8070", "user" = "root", "password" = "" ); SELECT count(*) FROM remote_source.some_db.some_table; ``` Phase one targets Arrow Flight SQL sources, which makes this the intended replacement for the `remote_doris` catalog. Other sources need only their driver's `.so` and, where their SQL differs from ANSI, a dialect implementation -- no architectural change. ### Supported features What an ADBC catalog gives you from the outside -- what you can do with it and what it does for you without being asked. The boundaries are in *Known limits* further down. | Capability | What it means when you use it | Control (default) | |---|---|---| | ADBC catalog | `CREATE CATALOG ... PROPERTIES("type" = "adbc", ...)`, then query the source's tables like any other external table. Phase one targets Arrow Flight SQL sources, including another Doris cluster. | `uri`, `user`, `password` | | Arrow-native transfer | Rows arrive as Arrow batches and go straight into the scanner, with no per-value conversion on the way in -- which is where a JDBC catalog spends much of a wide or high-volume scan. | Always on | | Parallel read across backends | One scan is split into the source's own result partitions and read by several BEs at once, instead of a single BE on a single connection. Falls back to one statement when the source cannot partition. | `partitioned_read` = `auto` / `disabled` / `required` (`auto`), `max_partitions` (`1024`) | | Column pruning | Only the columns the query actually needs are requested from the source, and `EXPLAIN` prints that same statement. | Always on | | Predicate pushdown | `=`, `!=`, `<`, `<=`, `>`, `>=`, `IS [NOT] NULL`, `[NOT] IN` and `AND` / `OR` / `NOT` over them become the remote `WHERE`. Anything else -- functions, arithmetic, `LIKE`, `BETWEEN` -- stays in Doris. Doris re-applies every predicate regardless, so pushdown changes speed only, never the rows you get. | Always on | | `LIMIT` pushdown | Pushed once the whole `WHERE` was pushed, so the source never truncates ahead of a filter Doris still has to apply. | Always on | | `COUNT(*)` without column values | A count reads no column data from the source at all. | Always on | | Metadata browsing | `SHOW DATABASES`, `SHOW TABLES`, `DESC`, `SHOW CREATE TABLE` and `information_schema` over the source's own databases, tables and columns. | -- | | Automatic type mapping | Source columns arrive as Doris types, including `ARRAY` / `MAP` / `STRUCT`, `DECIMAL`, date, and timestamp with and without a zone. | -- | | Metadata cache and `REFRESH` | Name resolution and table schemas are remembered per catalog, so a query stops paying several remote round trips before it is even planned. Listings are still read live, so a table created on the source is visible with no refresh at all, and `REFRESH CATALOG` / `DATABASE` / `TABLE` drops what is remembered. | `meta.cache.adbc.metadata.enable` / `.ttl-second` (`600`) / `.capacity` (`1000`) | | The usual query surface | Joins against internal tables and against other catalogs, aggregation, `ORDER BY`, `UNION`, subqueries, `SELECT ... INTO OUTFILE`, and an MTMV built on an ADBC table. | -- | | Source SQL you can steer | The generated SQL is conservative ANSI. The connector asks the driver which vendor it is talking to, and you can override that when the answer is unhelpful. | `sql_dialect` (auto-detected, `ansi` fallback; `doris` provided) | | Driver options passed through | Anything the driver itself understands can be set on the catalog, e.g. `"adbc.adbc.snowflake.sql.db" = "..."`. | `adbc.*` properties | | Driver placement and pinning | You place the driver library (Doris ships none); a bare file name resolves under the drivers directory, and a wrong or stale build is reported at `CREATE CATALOG` rather than as a puzzling query failure later. | `driver_url`, `driver_checksum`, `driver_entrypoint` | ### Design decisions worth knowing before reviewing - **FE and BE load the same driver `.so`.** FE goes through the ADBC JNI bridge, which wraps the same C driver manager BE links statically, and BE dlopens the identical file. This is a measured constraint, not a preference: partition descriptors are driver-private bytes, and two official implementations of the same protocol already serialize incompatible messages and mis-parse each other silently rather than erroring. - **Doris ships no ADBC driver.** The library is placed by the operator, under `adbc_drivers_dir` on FE and `be/plugins/adbc_drivers` on every BE. `driver_url` therefore accepts local references only (bare name, `file://`, or an absolute path); remote schemes are rejected because a per-node download cannot promise the nodes agree. "Driver file not found" is written as a first-class error for that reason, and `driver_checksum` can pin the build. - **The SQL sent to a source is conservative ANSI by default**, generated through a dialect interface a source can claim by vendor name or by the `sql_dialect` property. Predicates are pushed all-or-nothing per conjunct from a whitelist; BE re-applies every predicate regardless, so pushdown is pure acceleration. - **Planning has side effects on a Flight SQL source**: asking the driver to partition a statement executes it. That makes this the first connector for which `EXPLAIN` had to be told not to plan for real -- see the SPI addition below. ### Changes outside the connector | Change | Why | |---|---| | `ConnectorScanRequest.isExplainOnly()` (SPI) + `PluginDrivenScanNode` fills it | `EXPLAIN` reaches `planScan` for real (its explain level is `NORMAL`, so `NereidsPlanner.distribute()` does not return early). For this connector that would execute the very query it was asked only to describe. | | `PluginDrivenScanNode` re-asks for the display statement with the current columns | The connector properties are cached in `init()`, before Nereids prunes the scan tuple, so `EXPLAIN`'s `QUERY:` line named more columns than the statement that actually runs. Affects any connector that renders remote SQL from the column list (adbc, jdbc). What goes to BE is unchanged. | | `PluginDrivenScanNode.mapFileFormatType()` learns `"arrow"` | Routes an ADBC range to BE's Arrow reader. | | `TTableFormatFileDesc.adbc_params` | The scan range's parameters; BE's ADBC reader does not read the JDBC field. | | `be/src/vec/exec/format/adbc/*` + `file_scanner_v2` gate | The BE reader. | | `FlightSqlSchemaHelper` (FE, Arrow Flight SQL server) | `GetTables` described a `DATEV2` column as date64 while BE writes date32, and described array/map/struct with placeholder children. A client that types its columns from that schema fails on the first batch. Visible to every Arrow Flight SQL client, not only this connector. | | `DataTypeTimeStampTzSerDe::read_column_from_arrow` (BE) | It never overrode the numeric serde's fixed-width path, which memcpy'd Arrow's int64 epochs into a column that stores packed date/time values -- same width, so every row was silently wrong. | | `thirdparty`: `arrow-adbc` | The C driver manager is linked into `doris_be`; the JNI bridge is built for FE (upstream's prebuilt one needs GLIBC 2.34). The SQLite and Flight SQL drivers are fetched for tests only and are **not shipped**. | | `Config.adbc_drivers_dir` / `adbc_driver_secure_path`, `conf/fe.conf` JVM options, `build.sh` | Driver placement and the FE-side plugin build/deploy wiring. | ### Behaviour to be aware of - A view on the source is not listed as a table. A Doris source ignores the base-table filter ADBC sends, so the filter is applied where the answer is read. - `partitioned_read` is `auto` by default: split the scan when the driver can, read it as one statement when it cannot. `required` forbids that downgrade, which is what keeps a test from going green while quietly exercising the fallback; `disabled` is the escape hatch. - Metadata is cached per catalog for 10 minutes by default (`meta.cache.adbc.metadata.*`), and every `REFRESH` statement drops it. A newly created remote table is reachable without any refresh. - Read only: `INSERT`, `CREATE`/`DROP TABLE` and the other write statements against an ADBC catalog are rejected. An MTMV over an ADBC table does build and refresh. ### Test coverage - `fe-connector-adbc`: 186 unit tests, no skips. About 25 of them drive the real SQLite ADBC driver through the same Java -> JNI -> C driver manager -> driver `.so` path FE takes in production; `fe-connector-api` covers the new SPI field. - BE: unit tests for the ADBC reader, the driver registry, the Arrow variant normalizer, the scanner gate and the TIMESTAMPTZ serde. - End-to-end (`regression-test/suites/external_table_p0/adbc/`): 20 suites against a live catalog -- type mapping and semantics, complex and binary types, scan edges, predicate pushdown, column pruning, query shapes, source table models, metadata operations, nested catalogs, cross-source joins, large data, outfile, MTMV, negative cases, and partitioned vs. single-statement reads compared row for row. Each suite returns early when the driver is absent, so a cluster without one does not fail. ### Known limits - Read only. No writes, DDL, statistics or aggregate pushdown. - Partitions are verified to be read completely and without duplication, but **spreading them over several backends has only been reasoned from the code**, not observed -- the test environment has one backend, and `test_adbc_multi_backend` returns early there. - `IPV4` from a Doris source arrives as a bare `INT` (both sides encode it as int32), and a source `DATETIME` arrives as `TIMESTAMPTZ`. Both are asserted, not worked around. - The FE and BE copies of the driver are not checked against each other; `driver_checksum` verifies FE's copy only. ### Release note Support ADBC catalog, which reads an external source through an Arrow Database Connectivity driver: data is transferred as Arrow record batches and a scan is split across backends by the driver's own result partitions. Phase one targets Arrow Flight SQL sources. The driver library is supplied by the operator; Doris ships none. Also fixes two defects on the Arrow Flight SQL path that are visible to existing clients: `GetTables` reported the wrong Arrow type for `DATEV2` and placeholder children for `ARRAY`/`MAP`/`STRUCT`, and a `TIMESTAMPTZ` column read from Arrow was decoded by copying its bits instead of converting the epoch.
What problem does this PR solve?
Related Issue: #65615
Problem Summary:
Doris reads an external database through JDBC today: the source's rows become JDBC
objects and then Doris columns, one value at a time, across JNI. The source is also
read by a single backend, because JDBC has no notion of how a result is partitioned.
This PR adds a new catalog type,
adbc, that reads an external source through anArrow Database Connectivity driver. Data arrives as
Arrow record batches and is handed to BE's scanner without a per-value conversion, and
a scan is split into the driver's own result partitions so that N backends read N
partitions in parallel. Neither is reachable through JDBC at all.
Phase one targets Arrow Flight SQL sources, which makes this the intended replacement
for the
remote_doriscatalog. Other sources need only their driver's.soand, wheretheir SQL differs from ANSI, a dialect implementation -- no architectural change.
Supported features
What an ADBC catalog gives you from the outside -- what you can do with it and what it does
for you without being asked. The boundaries are in Known limits further down.
CREATE CATALOG ... PROPERTIES("type" = "adbc", ...), then query the source's tables like any other external table. Phase one targets Arrow Flight SQL sources, including another Doris cluster.uri,user,passwordpartitioned_read=auto/disabled/required(auto),max_partitions(1024)EXPLAINprints that same statement.=,!=,<,<=,>,>=,IS [NOT] NULL,[NOT] INandAND/OR/NOTover them become the remoteWHERE. Anything else -- functions, arithmetic,LIKE,BETWEEN-- stays in Doris. Doris re-applies every predicate regardless, so pushdown changes speed only, never the rows you get.LIMITpushdownWHEREwas pushed, so the source never truncates ahead of a filter Doris still has to apply.COUNT(*)without column valuesSHOW DATABASES,SHOW TABLES,DESC,SHOW CREATE TABLEandinformation_schemaover the source's own databases, tables and columns.ARRAY/MAP/STRUCT,DECIMAL, date, and timestamp with and without a zone.REFRESHREFRESH CATALOG/DATABASE/TABLEdrops what is remembered.meta.cache.adbc.metadata.enable/.ttl-second(600) /.capacity(1000)ORDER BY,UNION, subqueries,SELECT ... INTO OUTFILE, and an MTMV built on an ADBC table.sql_dialect(auto-detected,ansifallback;dorisprovided)"adbc.adbc.snowflake.sql.db" = "...".adbc.*propertiesCREATE CATALOGrather than as a puzzling query failure later.driver_url,driver_checksum,driver_entrypointDesign decisions worth knowing before reviewing
.so. FE goes through the ADBC JNI bridge, whichwraps the same C driver manager BE links statically, and BE dlopens the identical
file. This is a measured constraint, not a preference: partition descriptors are
driver-private bytes, and two official implementations of the same protocol already
serialize incompatible messages and mis-parse each other silently rather than
erroring.
adbc_drivers_diron FE andbe/plugins/adbc_driverson every BE.driver_urltherefore accepts local references only (bare name,
file://, or an absolute path);remote schemes are rejected because a per-node download cannot promise the nodes
agree. "Driver file not found" is written as a first-class error for that reason, and
driver_checksumcan pin the build.dialect interface a source can claim by vendor name or by the
sql_dialectproperty.Predicates are pushed all-or-nothing per conjunct from a whitelist; BE re-applies
every predicate regardless, so pushdown is pure acceleration.
statement executes it. That makes this the first connector for which
EXPLAINhad tobe told not to plan for real -- see the SPI addition below.
Changes outside the connector
ConnectorScanRequest.isExplainOnly()(SPI) +PluginDrivenScanNodefills itEXPLAINreachesplanScanfor real (its explain level isNORMAL, soNereidsPlanner.distribute()does not return early). For this connector that would execute the very query it was asked only to describe.PluginDrivenScanNodere-asks for the display statement with the current columnsinit(), before Nereids prunes the scan tuple, soEXPLAIN'sQUERY:line named more columns than the statement that actually runs. Affects any connector that renders remote SQL from the column list (adbc, jdbc). What goes to BE is unchanged.PluginDrivenScanNode.mapFileFormatType()learns"arrow"TTableFormatFileDesc.adbc_paramsbe/src/vec/exec/format/adbc/*+file_scanner_v2gateFlightSqlSchemaHelper(FE, Arrow Flight SQL server)GetTablesdescribed aDATEV2column as date64 while BE writes date32, and described array/map/struct with placeholder children. A client that types its columns from that schema fails on the first batch. Visible to every Arrow Flight SQL client, not only this connector.DataTypeTimeStampTzSerDe::read_column_from_arrow(BE)thirdparty:arrow-adbcdoris_be; the JNI bridge is built for FE (upstream's prebuilt one needs GLIBC 2.34). The SQLite and Flight SQL drivers are fetched for tests only and are not shipped.Config.adbc_drivers_dir/adbc_driver_secure_path,conf/fe.confJVM options,build.shBehaviour to be aware of
filter ADBC sends, so the filter is applied where the answer is read.
partitioned_readisautoby default: split the scan when the driver can, read itas one statement when it cannot.
requiredforbids that downgrade, which is whatkeeps a test from going green while quietly exercising the fallback;
disabledis theescape hatch.
(
meta.cache.adbc.metadata.*), and everyREFRESHstatement drops it. A newlycreated remote table is reachable without any refresh.
INSERT,CREATE/DROP TABLEand the other write statements against anADBC catalog are rejected. An MTMV over an ADBC table does build and refresh.
Test coverage
fe-connector-adbc: 186 unit tests, no skips. About 25 of them drive the real SQLiteADBC driver through the same Java -> JNI -> C driver manager -> driver
.sopath FEtakes in production;
fe-connector-apicovers the new SPI field.the scanner gate and the TIMESTAMPTZ serde.
regression-test/suites/external_table_p0/adbc/): 20 suites against a livecatalog -- type mapping and semantics, complex and binary types, scan edges, predicate
pushdown, column pruning, query shapes, source table models, metadata operations,
nested catalogs, cross-source joins, large data, outfile, MTMV, negative cases, and
partitioned vs. single-statement reads compared row for row. Each suite returns early
when the driver is absent, so a cluster without one does not fail.
Known limits
them over several backends has only been reasoned from the code, not observed -- the
test environment has one backend, and
test_adbc_multi_backendreturns early there.IPV4from a Doris source arrives as a bareINT(both sides encode it as int32), anda source
DATETIMEarrives asTIMESTAMPTZ. Both are asserted, not worked around.driver_checksumverifies FE's copy only.Release note
Support ADBC catalog, which reads an external source through an Arrow Database
Connectivity driver: data is transferred as Arrow record batches and a scan is split
across backends by the driver's own result partitions. Phase one targets Arrow Flight
SQL sources. The driver library is supplied by the operator; Doris ships none.
Also fixes two defects on the Arrow Flight SQL path that are visible to existing
clients:
GetTablesreported the wrong Arrow type forDATEV2and placeholder childrenfor
ARRAY/MAP/STRUCT, and aTIMESTAMPTZcolumn read from Arrow was decoded bycopying its bits instead of converting the epoch.
Check List (For Author)
Test
Behavior changed:
adbc; no existing catalog type changes.DATEV2and for nested types (previously a client that trustedGetTablesfailed on the first batch).
EXPLAINon a plugin-driven catalog prints the projection the scan really asksfor; the statement sent to BE is unchanged.
Does this need documentation?
Check List (For Reviewer who merge this PR)