You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Doris can silently plan an Iceberg table locally even when the REST catalog requires server-side scan planning. The existing rejectServerSideScanPlanning() guard does not detect the table after Doris creates its snapshot read view.
The SDK correctly returns a RESTTable, whose newScan() returns RESTTableScan and whose allowDistributedPlanning() returns false. However, both of these methods reconstruct any BaseTable subclass as a plain BaseTable:
IcebergStatementScope.snapshotReadTable(Table)
IcebergTableCache.TableOwner.snapshotReadTable()
Since RESTTable extends BaseTable, the reconstruction discards its scan implementation and the SupportsDistributedScanPlanning marker. IcebergScanPlanProvider.resolveTable() checks the guard after obtaining this wrapped table:
SDK RESTTable
newScan() = RESTTableScan
allowDistributedPlanning() = false
↓ snapshot read wrapper
plain BaseTable
newScan() = DataTableScan
no SupportsDistributedScanPlanning marker
↓ rejectServerSideScanPlanning()
guard does not match; local metadata reads proceed
This is observable even with a server-side table override. With the Doris catalog configured as client and the load-table response configured as server, the FE logs:
Scan planning mode mismatch for table plan_db.t1: client config=client, server config=server. Server config will take precedence.
Nevertheless, the query performs local planning and does not call /plan.
Relevant code at the tested commit
The snapshot wrapper preserves the operations and metadata but constructs a plain table:
+ "; configure the REST catalog to use client-side scan planning");
}
}
Observed consequences
Rows outside a simulated server-side filter are returned. A test REST proxy adds id <= 2 to each task's residual-filter. The official Iceberg generic reader returns rows 1 and 2, while Doris returns rows 1, 2, 3 and 4. Doris never requests the plan carrying that filter.
The intended unsupported-mode error is lost. Calling the existing guard with a real, unwrapped RESTTable throws the expected error. Calling the same guard after either snapshot wrapper does not throw.
With restricted storage permissions, queries fail with an unrelated S3 403. When the client can read data files but is explicitly denied access to the table's metadata files, Doris attempts to read the manifest list locally and fails. The same credentials work for SDK server-side planning and data-file access.
The row-filter case uses a controlled test proxy and client credentials that can read the full table. It demonstrates a result difference under that simulated policy, not a finding against a particular production catalog's authorization implementation.
What You Expected?
Until Doris supports server-side scan planning, a table requiring it should be rejected before local manifest access with the existing error:
Iceberg server-side scan planning is not supported for table plan_db.t1; configure the REST catalog to use client-side scan planning
Snapshot pinning and table caching must not silently turn a server-planned table into a locally planned table. The same rule should apply whether server comes from client catalog configuration or the server's table-level override.
Full server-side planning support can be a separate change. That implementation would need to consume the server's tasks, apply residuals and use any scan-scoped credentials; simply removing the guard would not resolve these issues.
How to Reproduce?
Common setup
Run the REST fixture at http://127.0.0.1:18181 with a MinIO warehouse at s3://warehouse/wh/. MinIO is accessible to FE/BE at http://127.0.0.1:19000; the fixture uses its Docker-network endpoint and has access to both metadata and data files.
Create a client-planned catalog and a small table (the credentials below are local test credentials):
This produces two Parquet files containing four rows. The tests below use this same table; no delete files are involved.
A. End-to-end row-filter comparison
Place a test proxy at http://127.0.0.1:18182, forwarding to the fixture with these two changes for plan_db.t1:
In the load-table response, set config["scan-planning-mode"] = "server".
In the synchronous /plan response, AND the following expression with each task's original residual-filter:
{"type": "lt-eq", "term": "id", "value": 2}
Keep the original file paths and record counts unchanged, and record incoming requests. The proxy used here only handles this synchronous test case; it is not a general policy service.
Create another Doris catalog with the same storage credentials, using the proxy URI and explicitly setting 'scan-planning-mode' = 'client'. The server's table-level override should take precedence:
CREATE CATALOG rest_policy_probe PROPERTIES (
'type'='iceberg',
'iceberg.catalog.type'='rest',
'uri'='http://127.0.0.1:18182',
's3.access_key'='admin',
's3.secret_key'='password',
's3.endpoint'='http://127.0.0.1:19000',
's3.region'='us-east-1',
'use_path_style'='true',
'scan-planning-mode'='client'
);
SELECT*FROMrest_policy_probe.plan_db.t1 ORDER BY id;
Compare against the official Iceberg 1.11.0 generic reader using the same REST and storage settings:
Tabletable = catalog.loadTable(TableIdentifier.of("plan_db", "t1"));
try (CloseableIterable<Record> rows = IcebergGenerics.read(table).build()) {
for (Recordrow : rows) {
// Collect the actual records read from Parquet; sort by id for comparison.
}
}
No client-side filter is supplied. Iceberg's GenericReader reads the Parquet files and applies each task's residual.
Execution
Actual rows
Observed POST /plan through proxy
Official reader, direct fixture without injected policy
(1,a), (2,b), (3,c), (4,d)
Not applicable: proxy not used
Official reader, through policy proxy
(1,a), (2,b)
1
Doris SQL, through the same proxy
(1,a), (2,b), (3,c), (4,d)
0
The no-policy baseline confirms that the underlying data is unchanged. The filtered reference result comes from actual Parquet reads, not from manually filtering the Doris result.
B. End-to-end restricted-storage comparison
Use the original fixture without the policy proxy. Give a new client identity:
permission to read warehouse/wh/plan_db/t1/data/*;
an explicit deny on s3:GetObject for warehouse/wh/plan_db/t1/metadata/* (this also denies S3 HEAD access).
Keep the fixture's own storage credentials unrestricted. The test bucket already had a public policy, so an explicit identity-policy deny was used and its effect verified.
Create a fresh Doris catalog using the restricted credentials and 'scan-planning-mode' = 'server', then run a full-row SELECT.
Observed results:
Catalog creation and SHOW TABLES succeed.
The SDK's RESTTableScan.planFiles() succeeds and returns two file tasks. Reading a byte from each data file with the same credentials succeeds; directly accessing the manifest list returns 403.
Doris SELECT fails with ERROR 1105 (HY000), errCode = 2, and Forbidden (Service: S3, Status Code: 403, ...), instead of the unsupported-mode error.
A separate Java probe loads a real SDK RESTTable and invokes the running plugin's existing private guard and snapshot methods reflectively, without modifying their implementation:
Guard input
Outcome
Raw SDK RESTTable
Throws the expected DorisConnectorException
Result of IcebergStatementScope.snapshotReadTable
Plain BaseTable; guard returns normally
Result of IcebergTableCache.TableOwner.snapshotReadTable
Plain BaseTable; guard returns normally
This is a method-level control, not an end-to-end SQL test of a fix. The cache probe isolates the snapshot conversion; it does not exercise the entire cache lifecycle.
Reproduction sources and recorded results
The sources below are from the local reproduction. Save the proxy and reference reader as server.py and ReferenceReader.java. Start the Compose stack, create the common table above, then start the proxy with python3 server.py and run the policy catalog SQL above. The proxy logs its requests to requests.jsonl beside its source.
For the reference reader, use Java 17 and the tested Doris FE distribution's lib/* and plugins/connector/iceberg/lib/*, plus Iceberg data/parquet/orc 1.11.0, Parquet hadoop-bundle/avro 1.17.0, ORC core 1.8.4 and Hive storage-api 2.8.1 jars. Put the Parquet 1.17.0 jars before Doris FE jars on the reference classpath to avoid old Parquet classes from FE. Run java -cp "$REFERENCE_CP" ReferenceReader.java http://127.0.0.1:18182; use port 18181 for the unfiltered baseline. This is a reference-reader classpath only, not a change to the running Doris installation.
The bucket and credentials are for this isolated local test. The Compose initializer enables public bucket access; the restricted-storage control uses an explicit identity-policy Deny to override it.
Docker Compose (compose.yaml)
services:
minio:
image: minio/minio:RELEASE.2025-01-20T14-49-07Zcontainer_name: lakespace-miniocommand: server /data --console-address ":9001"environment:
- MINIO_ROOT_USER=admin
- MINIO_ROOT_PASSWORD=password
- MINIO_DOMAIN=minioports:
- "19000:9000"
- "19001:9001"healthcheck:
test: ["CMD", "mc", "ready", "local"]interval: 3stimeout: 5sretries: 30mc:
image: minio/mc:RELEASE.2025-01-17T23-25-50Zcontainer_name: lakespace-mcdepends_on:
minio:
condition: service_healthyentrypoint: > /bin/sh -c " mc alias set local http://minio:9000 admin password && mc mb -p local/warehouse && mc anonymous set public local/warehouse && echo bucket-ready"rest:
image: apache/iceberg-rest-fixture@sha256:db8de90b5b7693d4ac334c336f91d9bbe320d7b19f4f514d26de84cdfbcbfe8dcontainer_name: lakespace-iceberg-restdepends_on:
mc:
condition: service_completed_successfullyports:
- "18181:8181"environment:
- AWS_ACCESS_KEY_ID=admin
- AWS_SECRET_ACCESS_KEY=password
- AWS_REGION=us-east-1
- CATALOG_WAREHOUSE=s3://warehouse/wh/
- CATALOG_IO__IMPL=org.apache.iceberg.aws.s3.S3FileIO
- CATALOG_S3_ENDPOINT=http://minio:9000
- CATALOG_S3_PATH__STYLE__ACCESS=truehealthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8181/v1/config"]interval: 3stimeout: 5sretries: 30
Attach the policy to a separate MinIO test user using mc admin policy create and mc admin policy attach --user. Configure only the restricted Doris catalog with that user's credentials; keep the REST fixture's original admin credentials.
The raw scan-planning-mode property already reaches the SDK through IcebergCatalogFactory's property forwarding. Adding a new property alias alone would not fix the behavior above.
The current IcebergScanPlanProviderTest.serverPlannedTable helper creates a proxy implementing Table and SupportsDistributedScanPlanning. It is not a BaseTable, so it does not exercise the conversion that affects a real RESTTable.
Regression coverage should include real REST-loaded tables through both snapshot paths, client-configured and server-forced planning modes, rejection before local manifest access, and unchanged client-planned queries.
No claim is made here about asynchronous planning, credential vending, delete files, or a specific production catalog's policy implementation; these were not part of this reproduction.
Proposed fix for PR 1 (subject to implementation validation)
Check the original table at the boundary where it enters a snapshot read view, before rebuilding RESTTable as BaseTable and before operations that depend on local manifests.
Extract a shared mode check. Reuse the existing SupportsDistributedScanPlanning / !allowDistributedPlanning() condition and unsupported-mode error across read paths.
Cover both wrapping paths. Check the original SDK table on statement-scoped direct loads and cache load/borrow paths. Audit cache metadata sizing and serialization for manifest access as well; checking only after TableOwner.snapshotReadTable() returns is too late. Keep the check inside the existing resource-management scope so rejection releases load guards and borrowed references correctly.
Retain scan-entry checks. Keep the existing scan-side guard, but do not rely on it to identify a plain BaseTable after capabilities have been lost. Audit metadata-table base-table resolution for the same wrapping issue.
Preserve normal snapshot semantics. Client-planned tables should retain their current snapshot, cache and ownership behavior. Do not fix this by unconditionally returning the original mutable REST table, which could change metadata consistency within a query.
Illustrative ordering only, not a final API or patch:
// Within the existing load/lease resource scope:Tableraw = loadOrBorrowTable();
rejectUnsupportedPlanning(raw); // Inspect capabilities before wrapping/local manifest work.TablereadView = createSnapshotReadView(raw);
Validation should cover real RESTTable instances, cache enabled/disabled, client-requested and server-forced modes, empty and populated tables, metadata-table base resolution and cleanup on rejection. Both existing end-to-end reproductions should change from extra rows or S3 403 to an explicit unsupported-mode error, while client-planned queries and snapshot-isolation tests continue to pass.
This restores rejection without enabling server-side planning. Final check placement requires auditing callers and resource lifecycles; complete support remains in the feature's subsequent PR.
Proposed follow-up plan
PR 1: restore the guard. Fix the snapshot-wrapper paths while retaining metadata isolation and resource ownership. Add real RESTTable tests; verify that the row-filter and restricted-storage reproductions both fail explicitly before local manifest reads, while client-planned queries remain correct. This PR closes this bug issue.
PR 2: implement server-side planning. Track complete support in [Feature] Support Iceberg REST server-side scan planning #68036. Before enabling supported reads, implement REST task planning and execution together with residual enforcement, scan-scoped credentials, lifecycle handling and regression tests. In that version, the policy test must return only rows 1 and 2, and the restricted-storage query must succeed.
The feature remains open after PR 1. Preparatory changes for PR 2 may be split if needed, but must not enable partially supported scans.
Search before asking
Version
28577df9f6d(2026-09-15), with a local build-packaging fix for macOS. No scan-planning code changes were applied.apache/iceberg-rest-fixture@sha256:db8de90b5b7693d4ac334c336f91d9bbe320d7b19f4f514d26de84cdfbcbfe8d.minio/minio:RELEASE.2025-01-20T14-49-07Z.What's Wrong?
Doris can silently plan an Iceberg table locally even when the REST catalog requires server-side scan planning. The existing
rejectServerSideScanPlanning()guard does not detect the table after Doris creates its snapshot read view.The SDK correctly returns a
RESTTable, whosenewScan()returnsRESTTableScanand whoseallowDistributedPlanning()returnsfalse. However, both of these methods reconstruct anyBaseTablesubclass as a plainBaseTable:IcebergStatementScope.snapshotReadTable(Table)IcebergTableCache.TableOwner.snapshotReadTable()Since
RESTTableextendsBaseTable, the reconstruction discards its scan implementation and theSupportsDistributedScanPlanningmarker.IcebergScanPlanProvider.resolveTable()checks the guard after obtaining this wrapped table:This is observable even with a server-side table override. With the Doris catalog configured as
clientand the load-table response configured asserver, the FE logs:Nevertheless, the query performs local planning and does not call
/plan.Relevant code at the tested commit
The snapshot wrapper preserves the operations and metadata but constructs a plain table:
Statement snapshot conversion:
doris/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergStatementScope.java
Lines 257 to 267 in 28577df
The same conversion in the table-cache snapshot path:
doris/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergTableCache.java
Lines 284 to 295 in 28577df
The guard runs after obtaining the snapshot read table and depends on its runtime interface:
doris/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java
Lines 3105 to 3123 in 28577df
Observed consequences
id <= 2to each task'sresidual-filter. The official Iceberg generic reader returns rows 1 and 2, while Doris returns rows 1, 2, 3 and 4. Doris never requests the plan carrying that filter.RESTTablethrows the expected error. Calling the same guard after either snapshot wrapper does not throw.The row-filter case uses a controlled test proxy and client credentials that can read the full table. It demonstrates a result difference under that simulated policy, not a finding against a particular production catalog's authorization implementation.
What You Expected?
Until Doris supports server-side scan planning, a table requiring it should be rejected before local manifest access with the existing error:
Snapshot pinning and table caching must not silently turn a server-planned table into a locally planned table. The same rule should apply whether
servercomes from client catalog configuration or the server's table-level override.Full server-side planning support can be a separate change. That implementation would need to consume the server's tasks, apply residuals and use any scan-scoped credentials; simply removing the guard would not resolve these issues.
How to Reproduce?
Common setup
Run the REST fixture at
http://127.0.0.1:18181with a MinIO warehouse ats3://warehouse/wh/. MinIO is accessible to FE/BE athttp://127.0.0.1:19000; the fixture uses its Docker-network endpoint and has access to both metadata and data files.Create a client-planned catalog and a small table (the credentials below are local test credentials):
CREATE CATALOG rest_client PROPERTIES ( 'type' = 'iceberg', 'iceberg.catalog.type' = 'rest', 'uri' = 'http://127.0.0.1:18181', 's3.access_key' = 'admin', 's3.secret_key' = 'password', 's3.endpoint' = 'http://127.0.0.1:19000', 's3.region' = 'us-east-1', 'use_path_style' = 'true' ); SWITCH rest_client; CREATE DATABASE IF NOT EXISTS plan_db; CREATE TABLE plan_db.t1 (id INT, v STRING) PROPERTIES ('write-format' = 'parquet'); INSERT INTO plan_db.t1 VALUES (1, 'a'), (2, 'b'), (3, 'c'); INSERT INTO plan_db.t1 VALUES (4, 'd');This produces two Parquet files containing four rows. The tests below use this same table; no delete files are involved.
A. End-to-end row-filter comparison
Place a test proxy at
http://127.0.0.1:18182, forwarding to the fixture with these two changes forplan_db.t1:config["scan-planning-mode"] = "server"./planresponse, AND the following expression with each task's originalresidual-filter:{"type": "lt-eq", "term": "id", "value": 2}Keep the original file paths and record counts unchanged, and record incoming requests. The proxy used here only handles this synchronous test case; it is not a general policy service.
Create another Doris catalog with the same storage credentials, using the proxy URI and explicitly setting
'scan-planning-mode' = 'client'. The server's table-level override should take precedence:CREATE CATALOG rest_policy_probe PROPERTIES ( 'type' = 'iceberg', 'iceberg.catalog.type' = 'rest', 'uri' = 'http://127.0.0.1:18182', 's3.access_key' = 'admin', 's3.secret_key' = 'password', 's3.endpoint' = 'http://127.0.0.1:19000', 's3.region' = 'us-east-1', 'use_path_style' = 'true', 'scan-planning-mode' = 'client' ); SELECT * FROM rest_policy_probe.plan_db.t1 ORDER BY id;Compare against the official Iceberg 1.11.0 generic reader using the same REST and storage settings:
No client-side filter is supplied. Iceberg's
GenericReaderreads the Parquet files and applies each task's residual./planthrough proxy(1,a), (2,b), (3,c), (4,d)(1,a), (2,b)(1,a), (2,b), (3,c), (4,d)The no-policy baseline confirms that the underlying data is unchanged. The filtered reference result comes from actual Parquet reads, not from manually filtering the Doris result.
B. End-to-end restricted-storage comparison
Use the original fixture without the policy proxy. Give a new client identity:
warehouse/wh/plan_db/t1/data/*;s3:GetObjectforwarehouse/wh/plan_db/t1/metadata/*(this also denies S3 HEAD access).Keep the fixture's own storage credentials unrestricted. The test bucket already had a public policy, so an explicit identity-policy deny was used and its effect verified.
Create a fresh Doris catalog using the restricted credentials and
'scan-planning-mode' = 'server', then run a full-rowSELECT.Observed results:
SHOW TABLESsucceed.RESTTableScan.planFiles()succeeds and returns two file tasks. Reading a byte from each data file with the same credentials succeeds; directly accessing the manifest list returns 403.SELECTfails withERROR 1105 (HY000),errCode = 2, andForbidden (Service: S3, Status Code: 403, ...), instead of the unsupported-mode error.The FE stack identifies the local access:
C. Guard control using a real RESTTable
A separate Java probe loads a real SDK
RESTTableand invokes the running plugin's existing private guard and snapshot methods reflectively, without modifying their implementation:RESTTableDorisConnectorExceptionIcebergStatementScope.snapshotReadTableBaseTable; guard returns normallyIcebergTableCache.TableOwner.snapshotReadTableBaseTable; guard returns normallyThis is a method-level control, not an end-to-end SQL test of a fix. The cache probe isolates the snapshot conversion; it does not exercise the entire cache lifecycle.
Reproduction sources and recorded results
The sources below are from the local reproduction. Save the proxy and reference reader as
server.pyandReferenceReader.java. Start the Compose stack, create the common table above, then start the proxy withpython3 server.pyand run the policy catalog SQL above. The proxy logs its requests torequests.jsonlbeside its source.For the reference reader, use Java 17 and the tested Doris FE distribution's
lib/*andplugins/connector/iceberg/lib/*, plus Iceberg data/parquet/orc 1.11.0, Parquet hadoop-bundle/avro 1.17.0, ORC core 1.8.4 and Hive storage-api 2.8.1 jars. Put the Parquet 1.17.0 jars before Doris FE jars on the reference classpath to avoid old Parquet classes from FE. Runjava -cp "$REFERENCE_CP" ReferenceReader.java http://127.0.0.1:18182; use port 18181 for the unfiltered baseline. This is a reference-reader classpath only, not a change to the running Doris installation.The bucket and credentials are for this isolated local test. The Compose initializer enables public bucket access; the restricted-storage control uses an explicit identity-policy Deny to override it.
Docker Compose (compose.yaml)
Policy proxy (server.py)
Official generic-reader client (ReferenceReader.java)
Restricted client identity policy (policy.json)
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetBucketLocation" ], "Resource": [ "arn:aws:s3:::warehouse" ] }, { "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::warehouse/wh/plan_db/t1/data/*" ] }, { "Effect": "Deny", "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::warehouse/wh/plan_db/t1/metadata/*" ] } ] }Attach the policy to a separate MinIO test user using
mc admin policy createandmc admin policy attach --user. Configure only the restricted Doris catalog with that user's credentials; keep the REST fixture's original admin credentials.Recorded full-read comparison
{ "reference-baseline": { "rows": [ "1\ta", "2\tb", "3\tc", "4\td" ], "plan_requests": 0 }, "reference-policy": { "rows": [ "1\ta", "2\tb" ], "plan_requests": 1 }, "doris-policy": { "rows": [ "1\ta", "2\tb", "3\tc", "4\td" ], "plan_requests": 0 } }Reference policy-reader request trace
{"method": "GET", "path": "/v1/config", "status": 200, "modified": null} {"method": "GET", "path": "/v1/namespaces/plan_db/tables/t1?snapshots=all", "status": 200, "modified": "force-server"} {"method": "POST", "path": "/v1/namespaces/plan_db/tables/t1/plan", "status": 200, "modified": "append-id-le-2"} {"method": "DELETE", "path": "/v1/namespaces/plan_db/tables/t1/plan/sync-02915d45-5652-466a-9354-edc4dc6edbf9", "status": 204, "modified": null}Method-level guard control (PolicyGuardProbe.java)
The guard probe requires the running FE connector jars on its classpath and uses the same local endpoints. It does not modify those jars.
Anything Else?
Feature tracking: [Feature] Support Iceberg REST server-side scan planning #68036. This bug tracks restoring rejection; the feature tracks complete server-side planning support.
Related: [fix](iceberg) Fix historical scans after schema evolution #67687 upgraded Iceberg to 1.11.0 and added the unsupported-mode guard.
The raw
scan-planning-modeproperty already reaches the SDK throughIcebergCatalogFactory's property forwarding. Adding a new property alias alone would not fix the behavior above.The current
IcebergScanPlanProviderTest.serverPlannedTablehelper creates a proxy implementingTableandSupportsDistributedScanPlanning. It is not aBaseTable, so it does not exercise the conversion that affects a realRESTTable.Regression coverage should include real REST-loaded tables through both snapshot paths, client-configured and server-forced planning modes, rejection before local manifest access, and unchanged client-planned queries.
No claim is made here about asynchronous planning, credential vending, delete files, or a specific production catalog's policy implementation; these were not part of this reproduction.
Proposed fix for PR 1 (subject to implementation validation)
Check the original table at the boundary where it enters a snapshot read view, before rebuilding
RESTTableasBaseTableand before operations that depend on local manifests.SupportsDistributedScanPlanning/!allowDistributedPlanning()condition and unsupported-mode error across read paths.TableOwner.snapshotReadTable()returns is too late. Keep the check inside the existing resource-management scope so rejection releases load guards and borrowed references correctly.BaseTableafter capabilities have been lost. Audit metadata-table base-table resolution for the same wrapping issue.Illustrative ordering only, not a final API or patch:
Validation should cover real RESTTable instances, cache enabled/disabled, client-requested and server-forced modes, empty and populated tables, metadata-table base resolution and cleanup on rejection. Both existing end-to-end reproductions should change from extra rows or S3 403 to an explicit unsupported-mode error, while client-planned queries and snapshot-isolation tests continue to pass.
This restores rejection without enabling server-side planning. Final check placement requires auditing callers and resource lifecycles; complete support remains in the feature's subsequent PR.
Proposed follow-up plan
The feature remains open after PR 1. Preparatory changes for PR 2 may be split if needed, but must not enable partially supported scans.
Are you willing to submit PR?
Code of Conduct