Skip to content
Open
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Add the default-enabled `enableGeoSpatialSupport` option for kernel-backed GEOMETRY / GEOGRAPHY results. Enabled mode returns the canonical `{ srid, wkb }` Arrow value; disabled mode returns WKT / EWKT.
Comment thread
cathleeny marked this conversation as resolved.
- Preserve DECIMAL precision and scale qualifiers in kernel-backed result metadata, matching the Thrift path.
- Pin transitive `brace-expansion` 1.x / 2.x and `ip-address` dependencies for repository and CI installs to patched releases for GHSA-6j4f-fj2g-mc7p, GHSA-qhr7-859c-m2p7, GHSA-j6r3-76f7-8jcv, and GHSA-h3mg-xc3c-68pw.
- Add `escapeMetadataCatalogName` (default `false`) to escape catalog underscores in Thrift and kernel metadata requests.
- Fix Thrift `getFunctions()` to report the requested `FUNCTION_CAT`, matching JDBC and kernel.

Expand Down
11 changes: 6 additions & 5 deletions CONNECTION_PARAMETERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,12 @@ disabling verification.

## Results and type rendering

| Option | Type | Thrift | Kernel | Default Value | Note |
| ----------------------------- | --------- | :----: | :----: | ------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `preserveBigNumericPrecision` | `boolean` | ✅ | ✅ | `false` | Returns DECIMAL as an exact string and BIGINT as `bigint` on both. |
| `disableRowMaterialization` | `boolean` | ✅ | ✅ | `false` | Fetches and parses Arrow batches but returns `null` row placeholders instead of converting cells. Intended for fetch-throughput tests. |
| `enableMetricViewMetadata` | `boolean` | ✅ | ⚠️ | `false` | Injected into session configuration on both paths. Kernel may drop its non-allowlisted configuration key. |
| Option | Type | Thrift | Kernel | Default Value | Note |
| ----------------------------- | --------- | :----: | :----: | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `preserveBigNumericPrecision` | `boolean` | ✅ | ✅ | `false` | Returns DECIMAL as an exact string and BIGINT as `bigint` on both. |
| `disableRowMaterialization` | `boolean` | ✅ | ✅ | `false` | Fetches and parses Arrow batches but returns `null` row placeholders instead of converting cells. Intended for fetch-throughput tests. |
| `enableGeoSpatialSupport` | `boolean` | ❌ | ✅ | `true` | Returns GEOMETRY / GEOGRAPHY as canonical `{ srid: number, wkb: Buffer }` values when `true`, or WKT / EWKT strings when `false`. The choice is local and is not forwarded to SEA. |
| `enableMetricViewMetadata` | `boolean` | ✅ | ⚠️ | `false` | Injected into session configuration on both paths. Kernel may drop its non-allowlisted configuration key. |

## Metadata catalog filters

Expand Down
2 changes: 1 addition & 1 deletion KERNEL_REV
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2c958bfba476a0b0f165c829988959a32bf12f3e
b7e9310b27be16a6c42490e58c4b5c4a525320bc
11 changes: 11 additions & 0 deletions lib/contracts/IDBSQLClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,17 @@ export type ConnectionOptions = {
*/
disableRowMaterialization?: boolean;

/**
* Enable native kernel-backed `GEOMETRY` and `GEOGRAPHY` results. `true`
* (the default) returns the canonical Arrow value as
* `{ srid: number, wkb: Buffer }`; `false` returns WKT / EWKT strings.
* This is a client-side conversion choice and is never forwarded to the SQL
* Execution API.
*
* Only the kernel backend uses this option.
*/
enableGeoSpatialSupport?: boolean;

/**
* Extra HTTP headers attached to driver-owned out-of-band requests
* (telemetry POSTs and feature-flag GETs). Not applied to the primary
Expand Down
16 changes: 15 additions & 1 deletion lib/kernel/KernelArrowIpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.

import { RecordBatchReader, MessageReader, MessageHeader, Schema, Field, DataType, TypeMap } from 'apache-arrow';
import { TTableSchema, TTypeId, TPrimitiveTypeEntry } from '../../thrift/TCLIService_types';
import { TTableSchema, TTypeId, TPrimitiveTypeEntry, TTypeQualifiers } from '../../thrift/TCLIService_types';
import { rewriteDurationToInt64, DURATION_UNIT_METADATA_KEY } from './KernelArrowIpcDurationFix';
import HiveDriverError from '../errors/HiveDriverError';

Expand Down Expand Up @@ -264,6 +264,20 @@ export function arrowSchemaToThriftSchema(arrowSchema: Schema<TypeMap>): TTableS
const primitiveEntry: TPrimitiveTypeEntry = {
type: arrowTypeToTTypeId(field),
};
if (primitiveEntry.type === TTypeId.DECIMAL_TYPE) {
Comment thread
cathleeny marked this conversation as resolved.
const qualifiers: TTypeQualifiers['qualifiers'] = {};
const decimal = DataType.isDecimal(field.type) ? field.type : undefined;
for (const key of ['precision', 'scale'] as const) {
// Prefer manifest qualifiers, including for text-encoded decimals.
// Native Arrow decimals also carry these when metadata is absent.
const text = field.metadata.get(`databricks.type_${key}`);
const parsed = text !== undefined && /^-?\d+$/.test(text) ? Number(text) : NaN;
const minimum = key === 'precision' ? 1 : -0x80000000;
const value = Number.isInteger(parsed) && parsed >= minimum && parsed <= 0x7fffffff ? parsed : decimal?.[key];
if (value !== undefined) qualifiers[key] = { i32Value: value };
}
if (Object.keys(qualifiers).length > 0) primitiveEntry.typeQualifiers = { qualifiers };
}
return {
columnName: field.name,
typeDesc: {
Expand Down
19 changes: 19 additions & 0 deletions lib/kernel/KernelAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ export interface KernelSessionDefaults {
* remove one.
*/
complexTypesAsJson?: boolean;
/**
* Enable native GEOMETRY / GEOGRAPHY values. `true` (the default) returns
* canonical `struct<srid:int32,wkb:binary>` values; `false` returns WKT /
* EWKT strings. Applied locally by the kernel and never sent to SEA.
*/
enableGeoSpatialSupport?: boolean;
/**
* Per-session kernel connection-pool size
* (kernel `ConnectionOptions.max_connections`). Validated as a positive
Expand Down Expand Up @@ -819,6 +825,7 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel
hostName: string;
httpPath: string;
intervalsAsString: boolean;
enableGeoSpatialSupport: boolean;
maxConnections?: number;
} & KernelTlsOptions &
KernelHttpOptions &
Expand All @@ -833,6 +840,9 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel
// (native Arrow) — they already decode identically to Thrift via the
// shared Arrow converter, so `complexTypesAsJson` is not forced on.
intervalsAsString: true,
// Match the public driver contract and the kernel default: geospatial
Comment thread
cathleeny marked this conversation as resolved.
// support is enabled unless the caller explicitly requests WKT / EWKT.
enableGeoSpatialSupport: true,
// TLS knobs (server-cert verification toggle + custom CA + mTLS client
// identity). Validated and normalised (string PEM → Buffer) here so the
// napi shape only sees a Buffer.
Expand All @@ -843,6 +853,15 @@ export function buildKernelConnectionOptions(options: ConnectionOptions): Kernel
...buildKernelProxyOptions(options),
};

if (options.enableGeoSpatialSupport !== undefined) {
if (typeof options.enableGeoSpatialSupport !== 'boolean') {
throw new HiveDriverError(
`kernel backend: \`enableGeoSpatialSupport\` must be a boolean; got ${typeof options.enableGeoSpatialSupport}.`,
);
}
base.enableGeoSpatialSupport = options.enableGeoSpatialSupport;
}

// kernel-only pool sizing; read via cast to match how this function reads the
// other kernel-specific options (TLS) — they live on the internal options
// surface, not the published public `ConnectionOptions` `.d.ts`.
Expand Down
8 changes: 8 additions & 0 deletions native/kernel/index.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,11 @@
"basic-ftp": "^6.2.1",
"@75lb/deep-merge": "^1.1.2",
"ws": "^8.18.0",
"ip-address": "^10.1.1",
"ip-address": "^10.7.1",
"form-data": "^4.0.4",
"uuid": "^11.1.1",
"serialize-javascript": "^7.0.5"
"serialize-javascript": "^7.0.5",
"brace-expansion@1": "1.1.21",
"brace-expansion@2": "2.1.7"
}
}
64 changes: 62 additions & 2 deletions tests/unit/kernel/KernelArrowIpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@
// limitations under the License.

import { expect } from 'chai';
import { tableFromArrays, RecordBatchStreamWriter } from 'apache-arrow';
import { countRowsInIpc, decodeIpcSchema, patchIpcBytes } from '../../../lib/kernel/KernelArrowIpc';
import { tableFromArrays, RecordBatchStreamWriter, Schema, Field, Decimal, Utf8, Int32 } from 'apache-arrow';
import {
arrowSchemaToThriftSchema,
countRowsInIpc,
decodeIpcSchema,
patchIpcBytes,
} from '../../../lib/kernel/KernelArrowIpc';
import { TTypeId } from '../../../thrift/TCLIService_types';
import { rewriteDurationToInt64 } from '../../../lib/kernel/KernelArrowIpcDurationFix';
import HiveDriverError from '../../../lib/errors/HiveDriverError';

Expand Down Expand Up @@ -100,3 +106,57 @@ describe('KernelArrowIpc.decodeIpcSchema', () => {
expect(() => decodeIpcSchema(Buffer.alloc(0))).to.throw(HiveDriverError);
});
});

describe('KernelArrowIpc.arrowSchemaToThriftSchema decimal qualifiers', () => {
for (const [precision, scale] of [
[10, 2],
[38, 0],
[38, 38],
[10, -2],
]) {
it(`preserves Arrow DECIMAL(${precision},${scale}) precision and scale`, () => {
const schema = new Schema([new Field('amount', new Decimal(scale, precision, 128), true)]);
expect(arrowSchemaToThriftSchema(schema).columns[0].typeDesc.types[0].primitiveEntry).to.deep.equal({
type: TTypeId.DECIMAL_TYPE,
typeQualifiers: { qualifiers: { precision: { i32Value: precision }, scale: { i32Value: scale } } },
});
});
}

for (const type of [new Utf8(), new Decimal(2, 10, 128)]) {
it(`prefers logical decimal qualifiers over the ${type} physical type`, () => {
const metadata = new Map([
['databricks.type_name', 'DECIMAL'],
['databricks.type_precision', '20'],
['databricks.type_scale', '0'],
]);
const schema = new Schema([new Field('amount', type, true, metadata)]);
expect(arrowSchemaToThriftSchema(schema).columns[0].typeDesc.types[0].primitiveEntry).to.deep.equal({
type: TTypeId.DECIMAL_TYPE,
typeQualifiers: { qualifiers: { precision: { i32Value: 20 }, scale: { i32Value: 0 } } },
});
});
}

for (const invalid of ['', 'invalid', '10.5', '2147483648']) {
it(`falls back to Arrow for invalid decimal qualifiers ${JSON.stringify(invalid)}`, () => {
const metadata = new Map([
['databricks.type_precision', invalid],
['databricks.type_scale', invalid],
]);
const schema = new Schema([new Field('amount', new Decimal(2, 10, 128), true, metadata)]);
expect(
arrowSchemaToThriftSchema(schema).columns[0].typeDesc.types[0].primitiveEntry?.typeQualifiers,
).to.deep.equal({ qualifiers: { precision: { i32Value: 10 }, scale: { i32Value: 2 } } });
});
}

it('does not invent missing qualifiers or attach decimal qualifiers to other types', () => {
const schema = new Schema([
new Field('unknown_decimal', new Utf8(), true, new Map([['databricks.type_name', 'DECIMAL']])),
new Field('integer', new Int32(), true, new Map([['databricks.type_precision', '10']])),
]);
const entries = arrowSchemaToThriftSchema(schema).columns.map((column) => column.typeDesc.types[0].primitiveEntry);
expect(entries).to.deep.equal([{ type: TTypeId.DECIMAL_TYPE }, { type: TTypeId.INT_TYPE }]);
});
});
Loading
Loading