Skip to content

Add polyfills to make apache-arrow work in Node@14 #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ jobs:
strategy:
matrix:
# only LTS versions starting from the lowest we support
# TODO: Include Nodejs@14
node-version: ['16', '18', '20']
node-version: ['14', '16', '18', '20']
env:
cache-name: cache-node-modules
NYC_REPORT_DIR: coverage_unit_node${{ matrix.node-version }}
Expand Down
3 changes: 3 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Don't move this import - it should be placed before any other
import './polyfills';

import { Thrift } from 'thrift';
import TCLIService from '../thrift/TCLIService';
import TCLIService_types from '../thrift/TCLIService_types';
Expand Down
50 changes: 50 additions & 0 deletions lib/polyfills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* eslint-disable import/prefer-default-export */

// `Array.at` / `TypedArray.at` is supported only since Nodejs@16.6.0
// These methods are massively used by `apache-arrow@13`, but we have
// to use this version because older ones contain some other nasty bugs

// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tointegerorinfinity
function toIntegerOrInfinity(value: unknown): number {
const result = Number(value);

// Return `0` for NaN; return `+Infinity` / `-Infinity` as is
if (!Number.isFinite(result)) {
return Number.isNaN(result) ? 0 : result;
}

return Math.trunc(result);
}

// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-tolength
function toLength(value: unknown): number {
const result = toIntegerOrInfinity(value);
return result > 0 ? Math.min(result, Number.MAX_SAFE_INTEGER) : 0;
}

// https://tc39.es/ecma262/multipage/indexed-collections.html#sec-array.prototype.at
export function at<T>(this: Array<T>, index: number): T | undefined {
const length = toLength(this.length);
const relativeIndex = toIntegerOrInfinity(index);
const absoluteIndex = relativeIndex >= 0 ? relativeIndex : length + relativeIndex;
return absoluteIndex >= 0 && absoluteIndex < length ? this[absoluteIndex] : undefined;
}

const ArrayConstructors = [
global.Array,
global.Int8Array,
global.Uint8Array,
global.Uint8ClampedArray,
global.Int16Array,
global.Uint16Array,
global.Int32Array,
global.Uint32Array,
global.Float32Array,
global.Float64Array,
global.BigInt64Array,
global.BigUint64Array,
];

ArrayConstructors.forEach((ArrayConstructor) => {
ArrayConstructor.prototype.at = ArrayConstructor.prototype.at ?? at;
});
4 changes: 2 additions & 2 deletions tests/e2e/arrow.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ async function deleteTable(session, tableName) {
async function initializeTable(session, tableName) {
await deleteTable(session, tableName);

const createTable = fixtures.createTableSql.replaceAll('${table_name}', tableName);
const createTable = fixtures.createTableSql.replace(/\$\{table_name\}/g, tableName);
await execute(session, createTable);

const insertData = fixtures.insertDataSql.replaceAll('${table_name}', tableName);
const insertData = fixtures.insertDataSql.replace(/\$\{table_name\}/g, tableName);
await execute(session, insertData);
}

Expand Down
76 changes: 76 additions & 0 deletions tests/unit/polyfills.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const { expect } = require('chai');
const { at } = require('../../dist/polyfills');

const defaultArrayMock = {
0: 'a',
1: 'b',
2: 'c',
3: 'd',
length: 4,
at,
};

describe('Array.at', () => {
it('should handle zero index', () => {
const obj = { ...defaultArrayMock };
expect(obj.at(0)).to.eq('a');
expect(obj.at(Number('+0'))).to.eq('a');
expect(obj.at(Number('-0'))).to.eq('a');
});

it('should handle positive index', () => {
const obj = { ...defaultArrayMock };
expect(obj.at(2)).to.eq('c');
expect(obj.at(2.2)).to.eq('c');
expect(obj.at(2.8)).to.eq('c');
});

it('should handle negative index', () => {
const obj = { ...defaultArrayMock };
expect(obj.at(-2)).to.eq('c');
expect(obj.at(-2.2)).to.eq('c');
expect(obj.at(-2.8)).to.eq('c');
});

it('should handle positive infinity index', () => {
const obj = { ...defaultArrayMock };
expect(obj.at(Number.POSITIVE_INFINITY)).to.be.undefined;
});

it('should handle negative infinity index', () => {
const obj = { ...defaultArrayMock };
expect(obj.at(Number.NEGATIVE_INFINITY)).to.be.undefined;
});

it('should handle non-numeric index', () => {
const obj = { ...defaultArrayMock };
expect(obj.at('2')).to.eq('c');
});

it('should handle NaN index', () => {
const obj = { ...defaultArrayMock };
expect(obj.at(Number.NaN)).to.eq('a');
expect(obj.at('invalid')).to.eq('a');
});

it('should handle index out of bounds', () => {
const obj = { ...defaultArrayMock };
expect(obj.at(10)).to.be.undefined;
expect(obj.at(-10)).to.be.undefined;
});

it('should handle zero length', () => {
const obj = { ...defaultArrayMock, length: 0 };
expect(obj.at(2)).to.be.undefined;
});

it('should handle negative length', () => {
const obj = { ...defaultArrayMock, length: -4 };
expect(obj.at(2)).to.be.undefined;
});

it('should handle non-numeric length', () => {
const obj = { ...defaultArrayMock, length: 'invalid' };
expect(obj.at(2)).to.be.undefined;
});
});