From 4e4e85bb05b9d3dc0989fa5715bbb797a03d0cdb Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 11 Nov 2026 23:00:38 +0000 Subject: [PATCH 1/4] feat: report hasResultSet in result metadata DDL such as CREATE TABLE and an empty SELECT looked identical: zero rows and, on Thrift, the same schema, because the server reports a placeholder `Result STRING` column for statements without a result set and sets hasResultSet on every handle. The Arrow schema the server sends is truthful (no fields for DDL), and the kernel returns a zero-column schema. Add an optional `hasResultSet` to `getResultMetadata()` derived from those schemas. Catalog operations report true; Thrift without Arrow results reports undefined. No extra requests; the fetch path is unchanged. Signed-off-by: Vu Anh Phung Co-authored-by: Isaac --- CHANGELOG.md | 1 + lib/contracts/ResultMetadata.ts | 13 +++ lib/kernel/KernelOperationBackend.ts | 2 + lib/thrift-backend/EmptyOperationBackend.ts | 7 +- lib/thrift-backend/ThriftOperationBackend.ts | 34 +++++- tests/e2e/result_set_presence.test.ts | 98 ++++++++++++++++ tests/unit/DBSQLOperation.test.ts | 109 ++++++++++++++++++ .../kernel/KernelOperationBackend.test.ts | 19 +++ .../ThriftPrimaryKeysMetadata.test.ts | 1 + 9 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/result_set_presence.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 77d44b38..f8407790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Add `hasResultSet` to `getResultMetadata()`. It is `false` for statements that return no result set, such as `CREATE TABLE`, and `true` for queries even when they return no rows; on the Thrift backend it is `undefined` when Arrow results are disabled. (#309) - CloudFetch retry-exhaustion errors now identify the download and the last failure: the `RetryError` message reads `CloudFetch result download failed: (last error: )`. `` is `HTTP : ` (or `HTTP ` when the status text is empty) for HTTP failures, and `network error `, `network error ()` (e.g. `aborted`, when there is no `code`) or a bare `network error` for network failures (the raw network-error message, which can contain the presigned URL, is not copied). If no cause can be determined, the ` (last error: …)` suffix is omitted. The `RetryError` instance, `errorCode` and `payload` (including the node-fetch `Request` / `Response`) are unchanged. (#471) - Report untyped NULL columns as STRING in kernel result metadata, matching Thrift. (#470) - Reject non-scalar query parameter values at bind time with a `ParameterError` on both the Thrift and kernel backends, instead of stringifying them (which could leave a Thrift operation stuck before FINISHED). Newly rejected on the Thrift backend: arrays, plain objects, boxed primitives (e.g. `new String('x')`, `new Number(1)`), functions, symbols, and a `DBSQLParameter` that wraps another `DBSQLParameter`. Primitives, `Date`, `Int64`, and `DBSQLParameter` are still accepted. diff --git a/lib/contracts/ResultMetadata.ts b/lib/contracts/ResultMetadata.ts index e332cb33..942f9611 100644 --- a/lib/contracts/ResultMetadata.ts +++ b/lib/contracts/ResultMetadata.ts @@ -36,4 +36,17 @@ export interface ResultMetadata { /** True iff the operation is a staging (PUT/GET/REMOVE) operation. */ isStagingOperation: boolean; + + /** + * Whether the statement produced a result set. `false` for statements that + * return no columns, such as DDL (`CREATE TABLE`, `DROP TABLE`) or `USE`; + * `true` for queries, even when they return zero rows. DML returns a result + * set with affected-row counts. Use this rather than `schema`, which on the + * Thrift backend reports a placeholder `Result` column for statements without + * a result set. + * + * `undefined` when the backend cannot tell: on the Thrift backend this + * requires Arrow results (`arrowEnabled`, the default). + */ + hasResultSet?: boolean; } diff --git a/lib/kernel/KernelOperationBackend.ts b/lib/kernel/KernelOperationBackend.ts index 8173a0fe..da5fa043 100644 --- a/lib/kernel/KernelOperationBackend.ts +++ b/lib/kernel/KernelOperationBackend.ts @@ -364,6 +364,8 @@ export default class KernelOperationBackend implements IOperationBackend { // "Duration" (18)`. Matches what the per-batch fetch path already does. arrowSchema: patchIpcBytes(arrowSchemaIpc.ipcBytes), isStagingOperation: false, + // SEA reports a zero-column schema for statements without a result set (e.g. DDL). + hasResultSet: arrowSchema.fields.length > 0, }; this.metadata = meta; return meta; diff --git a/lib/thrift-backend/EmptyOperationBackend.ts b/lib/thrift-backend/EmptyOperationBackend.ts index daa9c13c..80697df5 100644 --- a/lib/thrift-backend/EmptyOperationBackend.ts +++ b/lib/thrift-backend/EmptyOperationBackend.ts @@ -31,7 +31,12 @@ export default class EmptyOperationBackend implements IOperationBackend { } public async getResultMetadata(): Promise { - return { schema: this.schema, resultFormat: ResultFormat.ColumnBased, isStagingOperation: false }; + return { + schema: this.schema, + resultFormat: ResultFormat.ColumnBased, + isStagingOperation: false, + hasResultSet: true, + }; } public async cancel(): Promise { diff --git a/lib/thrift-backend/ThriftOperationBackend.ts b/lib/thrift-backend/ThriftOperationBackend.ts index dd5ac327..e6edbcef 100644 --- a/lib/thrift-backend/ThriftOperationBackend.ts +++ b/lib/thrift-backend/ThriftOperationBackend.ts @@ -1,7 +1,9 @@ import { stringify, NIL } from 'uuid'; +import { MessageReader } from 'apache-arrow'; import { TGetOperationStatusResp, TOperationHandle, + TOperationType, TSparkDirectResults, TGetResultSetMetadataResp, TSparkRowSetType, @@ -101,6 +103,8 @@ export default class ThriftOperationBackend implements IOperationBackend { private resultHandler?: ResultSlicer; + private arrowSchemaHasFields?: { arrowSchema: Buffer; value?: boolean }; + constructor({ handle, directResults, @@ -453,7 +457,6 @@ export default class ThriftOperationBackend implements IOperationBackend { }; } - // eslint-disable-next-line class-methods-use-this private adaptResultMetadata(response: TGetResultSetMetadataResp): ResultMetadata { return { schema: response.schema, @@ -461,6 +464,35 @@ export default class ThriftOperationBackend implements IOperationBackend { lz4Compressed: response.lz4Compressed, arrowSchema: response.arrowSchema, isStagingOperation: Boolean(response.isStagingOperation), + hasResultSet: this.resultSetPresence(response.arrowSchema), }; } + + // The server sets the handle's `hasResultSet` even for DDL, and gives it a + // placeholder `Result` column in `schema`; only the Arrow schema has no fields. + // Catalog operations always return a result set. + private resultSetPresence(arrowSchema?: Buffer): boolean | undefined { + const { operationType } = this; + if ( + operationType !== undefined && + operationType !== TOperationType.EXECUTE_STATEMENT && + operationType !== TOperationType.UNKNOWN + ) { + return true; + } + if (!arrowSchema) { + return undefined; + } + if (this.arrowSchemaHasFields?.arrowSchema !== arrowSchema) { + let value: boolean | undefined; + try { + const schema = new MessageReader(arrowSchema).readSchema(); + value = schema ? schema.fields.length > 0 : undefined; + } catch { + value = undefined; + } + this.arrowSchemaHasFields = { arrowSchema, value }; + } + return this.arrowSchemaHasFields.value; + } } diff --git a/tests/e2e/result_set_presence.test.ts b/tests/e2e/result_set_presence.test.ts new file mode 100644 index 00000000..c4830849 --- /dev/null +++ b/tests/e2e/result_set_presence.test.ts @@ -0,0 +1,98 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { DBSQLClient } from '../../lib'; +import { ConnectionOptions } from '../../lib/contracts/IDBSQLClient'; +import { ClientConfig } from '../../lib/contracts/IClientContext'; +import { InternalConnectionOptions } from '../../lib/contracts/InternalConnectionOptions'; +import IDBSQLSession from '../../lib/contracts/IDBSQLSession'; +import { tryGetKernelNative } from '../../lib/kernel/KernelNativeLoader'; +import config from './utils/config'; + +async function openSession(useKernel: boolean, customConfig: Partial = {}) { + const client = new DBSQLClient(); + if (Object.keys(customConfig).length > 0) { + const clientConfig = client.getConfig(); + sinon.stub(client, 'getConfig').returns({ ...clientConfig, ...customConfig }); + } + await client.connect({ + host: config.host, + path: config.path, + token: config.token, + useKernel, + } as ConnectionOptions & InternalConnectionOptions); + const session = await client.openSession({ initialCatalog: config.catalog, initialSchema: config.schema }); + return { client, session }; +} + +async function hasResultSet(session: IDBSQLSession, statement: string) { + const operation = await session.executeStatement(statement); + try { + const { hasResultSet: value } = await operation.getResultMetadata(); + await operation.fetchAll(); + return value; + } finally { + await operation.close(); + } +} + +const backends = [ + { name: 'Thrift', useKernel: false }, + { name: 'kernel', useKernel: true, skip: tryGetKernelNative() === undefined }, +]; + +backends.forEach(({ name, useKernel, skip }) => { + describe(`getResultMetadata().hasResultSet (${name})`, () => { + const table = `${config.catalog}.${config.schema}.result_set_presence_${config.tableSuffix}_${name}`; + let client: DBSQLClient; + let session: IDBSQLSession; + + before(async function setup() { + if (skip) { + // eslint-disable-next-line no-invalid-this + this.skip(); + } + ({ client, session } = await openSession(useKernel)); + }); + + after(async () => { + try { + if (session) { + await hasResultSet(session, `DROP TABLE IF EXISTS ${table}`); + await session.close(); + } + } finally { + await client?.close(); + } + }); + + it('is false for DDL and true for an empty SELECT', async () => { + expect(await hasResultSet(session, `CREATE TABLE IF NOT EXISTS ${table} (col1 INT)`)).to.be.false; + expect(await hasResultSet(session, `SELECT 'sample' AS Result WHERE 1 = 0`)).to.be.true; + expect(await hasResultSet(session, 'SELECT 1 AS one')).to.be.true; + expect(await hasResultSet(session, `INSERT INTO ${table} VALUES (1)`)).to.be.true; + expect(await hasResultSet(session, `USE SCHEMA ${config.schema}`)).to.be.false; + }); + + it('is true for catalog operations', async () => { + const operation = await session.getTables({ catalogName: config.catalog, schemaName: config.schema }); + try { + expect((await operation.getResultMetadata()).hasResultSet).to.be.true; + } finally { + await operation.close(); + } + }); + + if (!useKernel) { + it('is undefined when Arrow results are disabled', async () => { + const columnar = await openSession(false, { arrowEnabled: false }); + try { + expect(await hasResultSet(columnar.session, `SELECT 'sample' AS Result WHERE 1 = 0`)).to.be.undefined; + } finally { + await columnar.session.close(); + await columnar.client.close(); + sinon.restore(); + } + }); + } + }); +}); diff --git a/tests/unit/DBSQLOperation.test.ts b/tests/unit/DBSQLOperation.test.ts index f9195105..b44ba6c1 100644 --- a/tests/unit/DBSQLOperation.test.ts +++ b/tests/unit/DBSQLOperation.test.ts @@ -790,6 +790,115 @@ describe('DBSQLOperation', () => { }); }); + describe('getResultMetadata hasResultSet', () => { + // Arrow schemas captured from a live SQL warehouse. Both statements get the + // same placeholder Thrift schema (one `Result STRING` column); only the Arrow + // schema shows that the DDL produced no columns. + const createTableArrowSchema = Buffer.from( + 'ffffffff400000001000000000000a000e0006000d0008000a000000000004001000000000010a000c000000080004000a00000008' + + '00000008000000000000000000000000000000', + 'hex', + ); + const emptySelectArrowSchema = Buffer.from( + 'ffffffff080100001000000000000a000e0006000d0008000a000000000004001000000000010a000c000000080004000a00000008' + + '000000080000000000000001000000180000000000120018001400000013000c00000008000400120000001400000090000000940000' + + '000000000590000000020000004800000004000000c8ffffff08000000140000000800000022737472696e67220000000017000000' + + '537061726b3a44617461547970653a4a736f6e547970650008000c000800040008000000080000001000000006000000535452494e47' + + '000016000000537061726b3a44617461547970653a53716c4e616d65000000000000040004000400000006000000526573756c7400' + + '0000000000', + 'hex', + ); + const placeholderSchema = { + columns: [ + { + columnName: 'Result', + position: 1, + comment: '', + typeDesc: { types: [{ primitiveEntry: { type: TTypeId.STRING_TYPE } }] }, + }, + ], + }; + + function directResults(arrowSchema?: Buffer): TSparkDirectResults { + return { + operationStatus: { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + operationState: TOperationState.FINISHED_STATE, + }, + resultSetMetadata: { + status: { statusCode: TStatusCode.SUCCESS_STATUS }, + schema: placeholderSchema, + resultFormat: arrowSchema ? TSparkRowSetType.ARROW_BASED_SET : TSparkRowSetType.COLUMN_BASED_SET, + arrowSchema, + }, + }; + } + + it('is false for DDL whose Arrow schema has no fields, without extra requests', async () => { + const context = new ClientContextStub(); + const driver = sinon.spy(context.driver); + const operation = createOperationForTest({ + handle: operationHandleStub({ hasResultSet: true }), + context, + directResults: directResults(createTableArrowSchema), + }); + + const metadata = await operation.getResultMetadata(); + + expect(metadata.hasResultSet).to.be.false; + expect(await operation.getSchema()).to.deep.equal(placeholderSchema); + expect(driver.getOperationStatus.called).to.be.false; + expect(driver.getResultSetMetadata.called).to.be.false; + }); + + it('is true for an empty SELECT that has the same Thrift schema as DDL', async () => { + const context = new ClientContextStub(); + const operation = createOperationForTest({ + handle: operationHandleStub({ hasResultSet: true }), + context, + directResults: directResults(emptySelectArrowSchema), + }); + + const metadata = await operation.getResultMetadata(); + + expect(metadata.hasResultSet).to.be.true; + expect(await operation.getSchema()).to.deep.equal(placeholderSchema); + }); + + it('is undefined for a statement without an Arrow schema', async () => { + const context = new ClientContextStub(); + const operation = createOperationForTest({ + handle: operationHandleStub({ hasResultSet: true }), + context, + directResults: directResults(), + }); + + expect((await operation.getResultMetadata()).hasResultSet).to.be.undefined; + }); + + it('is undefined when the Arrow schema cannot be decoded', async () => { + const context = new ClientContextStub(); + const operation = createOperationForTest({ + handle: operationHandleStub({ hasResultSet: true }), + context, + directResults: directResults(Buffer.from('not an arrow schema')), + }); + + expect((await operation.getResultMetadata()).hasResultSet).to.be.undefined; + }); + + it('is true for catalog operations, which never carry an Arrow schema', async () => { + const context = new ClientContextStub(); + const operation = createOperationForTest({ + handle: operationHandleStub({ hasResultSet: true, operationType: TOperationType.GET_TABLES }), + context, + directResults: directResults(), + }); + + expect((await operation.getResultMetadata()).hasResultSet).to.be.true; + }); + }); + describe('fetchChunk', () => { it('should return immediately if operation has no results', async () => { const context = new ClientContextStub(); diff --git a/tests/unit/kernel/KernelOperationBackend.test.ts b/tests/unit/kernel/KernelOperationBackend.test.ts index 972de4fc..43e400dd 100644 --- a/tests/unit/kernel/KernelOperationBackend.test.ts +++ b/tests/unit/kernel/KernelOperationBackend.test.ts @@ -259,6 +259,25 @@ describe('KernelOperationBackend — M0 datatype round-trip via napi → ArrowRe }); } + it('reports hasResultSet=false for a zero-column (DDL) schema and true for an empty SELECT', async () => { + const ddl = new KernelOperationBackend({ + statement: new StatementStub(ipcSchemaOnly(new Schema([])), []), + context: new ClientContextStub(), + }); + expect((await ddl.getResultMetadata()).hasResultSet).to.equal(false); + expect(await ddl.fetchChunk({ limit: 100 })).to.deep.equal([]); + + const emptySelect = new KernelOperationBackend({ + statement: new StatementStub( + ipcSchemaOnly(new Schema([withTypeName(new Field('Result', new Utf8()), 'STRING')])), + [], + ), + context: new ClientContextStub(), + }); + expect((await emptySelect.getResultMetadata()).hasResultSet).to.equal(true); + expect(await emptySelect.fetchChunk({ limit: 100 })).to.deep.equal([]); + }); + it('preserves decimal qualifiers for a schema-only result with zero scale', async () => { const schema = new Schema([new Field('amount', new Decimal(0, 38, 128), true)]); const backend = new KernelOperationBackend({ diff --git a/tests/unit/thrift-backend/ThriftPrimaryKeysMetadata.test.ts b/tests/unit/thrift-backend/ThriftPrimaryKeysMetadata.test.ts index 9d239399..46636054 100644 --- a/tests/unit/thrift-backend/ThriftPrimaryKeysMetadata.test.ts +++ b/tests/unit/thrift-backend/ThriftPrimaryKeysMetadata.test.ts @@ -64,6 +64,7 @@ async function expectEmptyPrimaryKeys(operation: IOperation): Promise { const metadata = await (operation as DBSQLOperation).getMetadata(); expect(metadata.status.statusCode).to.equal(TStatusCode.SUCCESS_STATUS); expect(metadata.resultFormat).to.equal(TSparkRowSetType.COLUMN_BASED_SET); + expect((await operation.getResultMetadata()).hasResultSet).to.be.true; const status = await operation.status(); expect(status.operationState).to.equal(TOperationState.FINISHED_STATE); expect(status.status.statusCode).to.equal(TStatusCode.SUCCESS_STATUS); From f171c1c1c0afd2002b10ffb2b46613688eb5924e Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 11 Nov 2026 23:11:25 +0000 Subject: [PATCH 2/4] test: always restore the getConfig stub in the result-set e2e spec Signed-off-by: Vu Anh Phung Co-authored-by: Isaac --- tests/e2e/result_set_presence.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/result_set_presence.test.ts b/tests/e2e/result_set_presence.test.ts index c4830849..c373fb9b 100644 --- a/tests/e2e/result_set_presence.test.ts +++ b/tests/e2e/result_set_presence.test.ts @@ -65,6 +65,8 @@ backends.forEach(({ name, useKernel, skip }) => { } }); + afterEach(() => sinon.restore()); + it('is false for DDL and true for an empty SELECT', async () => { expect(await hasResultSet(session, `CREATE TABLE IF NOT EXISTS ${table} (col1 INT)`)).to.be.false; expect(await hasResultSet(session, `SELECT 'sample' AS Result WHERE 1 = 0`)).to.be.true; @@ -90,7 +92,6 @@ backends.forEach(({ name, useKernel, skip }) => { } finally { await columnar.session.close(); await columnar.client.close(); - sinon.restore(); } }); } From 76a3bcf623fb8e7a3daaec013006e3cf3de5c4f6 Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 11 Nov 2026 23:20:08 +0000 Subject: [PATCH 3/4] fix: honor an explicit hasResultSet=false from the Thrift server Signed-off-by: Vu Anh Phung Co-authored-by: Isaac --- lib/thrift-backend/ThriftOperationBackend.ts | 5 ++++- tests/unit/DBSQLOperation.test.ts | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/thrift-backend/ThriftOperationBackend.ts b/lib/thrift-backend/ThriftOperationBackend.ts index e6edbcef..3c2b6d16 100644 --- a/lib/thrift-backend/ThriftOperationBackend.ts +++ b/lib/thrift-backend/ThriftOperationBackend.ts @@ -468,7 +468,7 @@ export default class ThriftOperationBackend implements IOperationBackend { }; } - // The server sets the handle's `hasResultSet` even for DDL, and gives it a + // Warehouses set the handle's `hasResultSet` even for DDL and give it a // placeholder `Result` column in `schema`; only the Arrow schema has no fields. // Catalog operations always return a result set. private resultSetPresence(arrowSchema?: Buffer): boolean | undefined { @@ -480,6 +480,9 @@ export default class ThriftOperationBackend implements IOperationBackend { ) { return true; } + if (this.operationHandle.hasResultSet === false) { + return false; + } if (!arrowSchema) { return undefined; } diff --git a/tests/unit/DBSQLOperation.test.ts b/tests/unit/DBSQLOperation.test.ts index b44ba6c1..e9afcec9 100644 --- a/tests/unit/DBSQLOperation.test.ts +++ b/tests/unit/DBSQLOperation.test.ts @@ -876,6 +876,17 @@ describe('DBSQLOperation', () => { expect((await operation.getResultMetadata()).hasResultSet).to.be.undefined; }); + it('is false when the server reports no result set, even without an Arrow schema', async () => { + const context = new ClientContextStub(); + const operation = createOperationForTest({ + handle: operationHandleStub({ hasResultSet: false }), + context, + directResults: directResults(), + }); + + expect((await operation.getResultMetadata()).hasResultSet).to.be.false; + }); + it('is undefined when the Arrow schema cannot be decoded', async () => { const context = new ClientContextStub(); const operation = createOperationForTest({ From 8381b441ebc79ee63c2a1551e57e595a859a4295 Mon Sep 17 00:00:00 2001 From: Vu Anh Phung Date: Wed, 11 Nov 2026 23:26:33 +0000 Subject: [PATCH 4/4] chore: log Arrow schema decode failures at debug level Signed-off-by: Vu Anh Phung Co-authored-by: Isaac --- lib/thrift-backend/ThriftOperationBackend.ts | 7 +++++-- tests/unit/DBSQLOperation.test.ts | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/thrift-backend/ThriftOperationBackend.ts b/lib/thrift-backend/ThriftOperationBackend.ts index 3c2b6d16..f44dec93 100644 --- a/lib/thrift-backend/ThriftOperationBackend.ts +++ b/lib/thrift-backend/ThriftOperationBackend.ts @@ -491,8 +491,11 @@ export default class ThriftOperationBackend implements IOperationBackend { try { const schema = new MessageReader(arrowSchema).readSchema(); value = schema ? schema.fields.length > 0 : undefined; - } catch { - value = undefined; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.context + .getLogger() + .log(LogLevel.debug, `Cannot decode the Arrow schema of operation ${this.id}: ${message}`); } this.arrowSchemaHasFields = { arrowSchema, value }; } diff --git a/tests/unit/DBSQLOperation.test.ts b/tests/unit/DBSQLOperation.test.ts index e9afcec9..485751ce 100644 --- a/tests/unit/DBSQLOperation.test.ts +++ b/tests/unit/DBSQLOperation.test.ts @@ -18,6 +18,7 @@ import ArrowResultConverter from '../../lib/result/ArrowResultConverter'; import ArrowResultHandler from '../../lib/result/ArrowResultHandler'; import CloudFetchResultHandler from '../../lib/result/CloudFetchResultHandler'; import ResultSlicer from '../../lib/result/ResultSlicer'; +import { LogLevel } from '../../lib/contracts/IDBSQLLogger'; import ClientContextStub from './.stubs/ClientContextStub'; import { createOperationForTest } from './.stubs/createOperationForTest'; @@ -889,6 +890,7 @@ describe('DBSQLOperation', () => { it('is undefined when the Arrow schema cannot be decoded', async () => { const context = new ClientContextStub(); + const log = sinon.spy(context.logger, 'log'); const operation = createOperationForTest({ handle: operationHandleStub({ hasResultSet: true }), context, @@ -896,6 +898,7 @@ describe('DBSQLOperation', () => { }); expect((await operation.getResultMetadata()).hasResultSet).to.be.undefined; + expect(log.calledWithMatch(LogLevel.debug, 'Cannot decode the Arrow schema')).to.be.true; }); it('is true for catalog operations, which never carry an Arrow schema', async () => {