Skip to content
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

feat: respect iox::column_type::field metadata when mapping query #491

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
## 1.0.0 [unreleased]

### Features

1. [#491](https://github.com/InfluxCommunity/influxdb3-js/pull/491): Respect iox::column_type::field metadata when
mapping query results into values.
- iox::column_type::field::integer: => number
- iox::column_type::field::uinteger: => number
- iox::column_type::field::float: => number
- iox::column_type::field::string: => string
- iox::column_type::field::boolean: => boolean

## 0.13.0 [unreleased]

## 0.12.0 [2024-10-22]
Expand Down
2 changes: 1 addition & 1 deletion packages/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@influxdata/influxdb3-client",
"version": "0.12.0",
"version": "1.0.0",
"description": "The Client that provides a simple and convenient way to interact with InfluxDB 3.",
"scripts": {
"apidoc:extract": "api-extractor run",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export * from './util/logger'
export * from './util/escape'
export * from './util/time'
export * from './util/generics'
export {collectAll} from './util/common'
export {collectAll, isNumber} from './util/common'
export * from './Point'
export * from './PointValues'
export {default as InfluxDBClient} from './InfluxDBClient'
Expand Down
65 changes: 65 additions & 0 deletions packages/client/src/util/TypeCasting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {Field} from 'apache-arrow'
import {isNumber, isUnsignedNumber} from './common'
import {Type as ArrowType} from 'apache-arrow/enum'

/**
* Function to cast value return base on metadata from InfluxDB.
*
* @param field the Field object from Arrow
* @param value the value to cast
* @return the value with the correct type
*/
export function getMappedValue(field: Field, value: any): any {
if (value === null || value === undefined) {
return null
}

const metaType = field.metadata.get('iox::column::type')

if (!metaType || (!metaType && field.typeId === ArrowType.Timestamp)) {
NguyenHoangSon96 marked this conversation as resolved.
Show resolved Hide resolved
return value
}

const [, , valueType, _fieldType] = metaType.split('::')

if (valueType === 'timestamp') {
return value
}

if (valueType === 'field') {
switch (_fieldType) {
case 'integer':
if (isNumber(value)) {
return parseInt(value)
}
console.warn(`Value ${value} is not an integer`)
return value
case 'uinteger':
if (isUnsignedNumber(value)) {
return parseInt(value)
}
console.warn(`Value ${value} is not an unsigned integer`)
return value
case 'float':
if (isNumber(value)) {
return parseFloat(value)
}
console.warn(`Value ${value} is not a float`)
return value
case 'boolean':
if (typeof value === 'boolean') {
return value
}
console.warn(`Value ${value} is not a boolean`)
return value
case 'string':
if (typeof value === 'string') {
return String(value)
}
console.warn(`Value ${value} is not a string`)
return value
default:
return value
}
}
}
39 changes: 39 additions & 0 deletions packages/client/src/util/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,42 @@ export const collectAll = async <T>(
}
return results
}

/**
* Check if an input value is a valid number.
*
* @param value - The value to check
* @returns Returns true if the value is a valid number else false
*/
export const isNumber = (value?: number | string | null): boolean => {
if (value === null || undefined) {
return false
}

if (
typeof value === 'string' &&
(value === '' || value.indexOf(' ') !== -1)
) {
return false
}

return value !== '' && !isNaN(Number(value?.toString()))
}

/**
* Check if an input value is a valid unsigned number.
*
* @param value - The value to check
* @returns Returns true if the value is a valid unsigned number else false
*/
export const isUnsignedNumber = (value?: number | string | null): boolean => {
if (!isNumber(value)) {
return false
}

if (typeof value === 'string') {
return Number(value) >= 0
}

return typeof value === 'number' && value >= 0
}
2 changes: 1 addition & 1 deletion packages/client/test/integration/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ describe('e2e test', () => {
expect(row['director']).to.equal('J_Ford')
}
expect(count).to.be.greaterThan(0)
}).timeout(5_000)
}).timeout(10_000)

it('queries to points with parameters', async () => {
const {database, token, url} = getEnvVariables()
Expand Down
42 changes: 42 additions & 0 deletions packages/client/test/unit/util/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {isNumber} from '../../../src'
import {expect} from 'chai'
import {isUnsignedNumber} from '../../../src/util/common'

describe('Test functions in common', () => {
NguyenHoangSon96 marked this conversation as resolved.
Show resolved Hide resolved
it('should check if value is a valid number', () => {
expect(isNumber(1)).equal(true)
expect(isNumber(-1)).equal(true)
expect(isNumber(-1.2)).equal(true)
expect(isNumber('-1.2')).equal(true)
expect(isNumber('2')).equal(true)

expect(isNumber('a')).equal(false)
expect(isNumber('true')).equal(false)
expect(isNumber('')).equal(false)
expect(isNumber(' ')).equal(false)
expect(isNumber('32a')).equal(false)
expect(isNumber('32 ')).equal(false)
expect(isNumber(null)).equal(false)
expect(isNumber(undefined)).equal(false)
expect(isNumber(NaN)).equal(false)
})

it('should check if value is a valid unsigned number', () => {
expect(isUnsignedNumber(1)).equal(true)
expect(isUnsignedNumber(1.2)).equal(true)
expect(isUnsignedNumber('1.2')).equal(true)
expect(isUnsignedNumber('2')).equal(true)

expect(isUnsignedNumber(-2.3)).equal(false)
expect(isUnsignedNumber('-2.3')).equal(false)
expect(isUnsignedNumber('a')).equal(false)
expect(isUnsignedNumber('true')).equal(false)
expect(isUnsignedNumber('')).equal(false)
expect(isUnsignedNumber(' ')).equal(false)
expect(isUnsignedNumber('32a')).equal(false)
expect(isUnsignedNumber('32 ')).equal(false)
expect(isUnsignedNumber(null)).equal(false)
expect(isUnsignedNumber(undefined)).equal(false)
expect(isUnsignedNumber(NaN)).equal(false)
})
})
100 changes: 100 additions & 0 deletions packages/client/test/unit/util/typeCasting.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
Bool,
Field,
Float64,
Int64,
Timestamp,
TimeUnit,
Uint64,
Utf8,
} from 'apache-arrow'
import {getMappedValue} from '../../../src/util/TypeCasting'
import {expect} from 'chai'

describe('Type casting test', () => {
NguyenHoangSon96 marked this conversation as resolved.
Show resolved Hide resolved
it('getMappedValue test', () => {
// If pass the correct value type to getMappedValue() it will return the value with a correct type
// If pass the incorrect value type to getMappedValue() it will NOT throws any error but return the passed value

const fieldName = 'test'
let field: Field

field = generateIntField(fieldName)
expect(getMappedValue(field, 1)).to.equal(1)
expect(getMappedValue(field, 'a')).to.equal('a')

field = generateUnsignedIntField(fieldName)
expect(getMappedValue(field, 1)).to.equal(1)
expect(getMappedValue(field, -1)).to.equal(-1)
expect(getMappedValue(field, 'a')).to.equal('a')

field = generateFloatField(fieldName)
expect(getMappedValue(field, 1.1)).to.equal(1.1)
expect(getMappedValue(field, 'a')).to.equal('a')

field = generateBooleanField(fieldName)
expect(getMappedValue(field, true)).to.equal(true)
expect(getMappedValue(field, 'a')).to.equal('a')

field = generateStringField(fieldName)
expect(getMappedValue(field, 'a')).to.equal('a')
expect(getMappedValue(field, true)).to.equal(true)

field = generateTimeStamp(fieldName)
const nowNanoSecond = Date.now() * 1_000_000
expect(getMappedValue(field, nowNanoSecond)).to.equal(nowNanoSecond)

field = generateIntFieldTestTypeMeta(fieldName)
expect(getMappedValue(field, 1)).to.equal(1)

// If metadata is null return the value
field = new Field(fieldName, new Int64(), true, null)
expect(getMappedValue(field, 1)).to.equal(1)

// If value is null return null
field = new Field(fieldName, new Int64(), true, null)
expect(getMappedValue(field, null)).to.equal(null)
})
})

function generateIntField(name: string): Field {
const map = new Map<string, string>()
map.set('iox::column::type', 'iox::column_type::field::integer')
return new Field(name, new Int64(), true, map)
}

function generateUnsignedIntField(name: string): Field {
const map = new Map<string, string>()
map.set('iox::column::type', 'iox::column_type::field::uinteger')
return new Field(name, new Uint64(), true, map)
}

function generateFloatField(name: string): Field {
const map = new Map<string, string>()
map.set('iox::column::type', 'iox::column_type::field::float')
return new Field(name, new Float64(), true, map)
}

function generateStringField(name: string): Field {
const map = new Map<string, string>()
map.set('iox::column::type', 'iox::column_type::field::string')
return new Field(name, new Utf8(), true, map)
}

function generateBooleanField(name: string): Field {
const map = new Map<string, string>()
map.set('iox::column::type', 'iox::column_type::field::boolean')
return new Field(name, new Bool(), true, map)
}

function generateIntFieldTestTypeMeta(name: string): Field {
const map = new Map<string, string>()
map.set('iox::column::type', 'iox::column_type::field::test')
return new Field(name, new Int64(), true, map)
}

function generateTimeStamp(name: string): Field {
const map = new Map<string, string>()
map.set('iox::column::type', 'iox::column_type::timestamp')
return new Field(name, new Timestamp(TimeUnit.NANOSECOND), true, map)
}
Loading