diff --git a/.gitignore b/.gitignore index 3fd294c..69980fc 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,4 @@ dist .tern-port .idea +out \ No newline at end of file diff --git a/integration-test/type.test.ts b/integration-test/type.test.ts index e862217..a5e8689 100644 --- a/integration-test/type.test.ts +++ b/integration-test/type.test.ts @@ -1,6 +1,7 @@ import { connect, Row, FullResult } from '../dist/index' import { fetch } from 'undici' import * as dotenv from 'dotenv' +import { uint8ArrayToHex } from '../src/format' dotenv.config() const databaseURL = process.env.DATABASE_URL @@ -24,7 +25,7 @@ const multiDataTable = ` t_decimal DECIMAL(38, 19), t_char CHAR, t_varchar VARCHAR(10), - c_binary binary(16), + c_binary binary(3), c_varbinary varbinary(16), t_tinytext TINYTEXT, t_text TEXT, @@ -84,6 +85,9 @@ const nullResult = { t_json: null } +// binary: x'1520c5' is the hex of 'FSDF' decoded from base64 (1520c5 has 3 bytes) +// blob : assume tidb serverless decode them with utf8 +// bit: b'01010101' convert to hex is 55 (85 in 10 base) const insertSQL = ` INSERT INTO ${database}.${table}( t_tinyint, t_tinyint_unsigned, t_smallint, t_smallint_unsigned, t_mediumint , t_mediumint_unsigned, t_int, t_int_unsigned, t_bigint, t_bigint_unsigned @@ -94,7 +98,7 @@ INSERT INTO ${database}.${table}( t_tinyint, t_tinyint_unsigned, t_smallint, t_s , t_enum,t_bit, t_set, t_json) VALUES ( -128, 255, -32768, 65535, -8388608, 16777215, -2147483648, 1, -9223372036854775808, 18446744073709551615 , true, 123.456, 123.123, 123456789012.123456789012 - , '测', '测试', x'89504E470D0A1A0A', x'89504E470D0A1A0A', '测试tinytext', '0', '测试mediumtext', '测试longtext' + , '测', '测试', x'1520c5', x'1520c5', '测试tinytext', '0', '测试mediumtext', '测试longtext' , 'tinyblob', 'blob', 'mediumblob', 'longblob' , '1977-01-01', '9999-12-31 23:59:59', '19731230153000', '23:59:59', '2154' , 'enum2',b'01010101', 'a,b','{"a":1,"b":"2"}') @@ -117,8 +121,8 @@ const fullTypeResult = { t_decimal: '123456789012.1234567890120000000', t_char: '测', t_varchar: '测试', - c_binary: '�PNG\r\n\x1A\n\x00\x00\x00\x00\x00\x00\x00\x00', - c_varbinary: '�PNG\r\n\x1A\n', + c_binary: 'FSDF', + c_varbinary: 'FSDF', t_tinytext: '测试tinytext', t_text: '0', t_mediumtext: '测试mediumtext', @@ -134,7 +138,7 @@ const fullTypeResult = { t_year: 2154, t_enum: 'enum2', t_set: 'a,b', - t_bit: '\x00\x00\x00\x00\x00\x00\x00U', + t_bit: '0x0000000000000055', t_json: { a: 1, b: '2' } } @@ -156,11 +160,46 @@ describe('types', () => { }) test('test all types', async () => { - const con = connect({ url: databaseURL, database: database, fetch, debug: true }) + const con = connect({ url: databaseURL, database: database, fetch }) await con.execute(`delete from ${table}`) await con.execute(insertSQL) const rows = (await con.execute('select * from multi_data_type')) as Row[] expect(rows.length).toEqual(1) + // binary type returns Uint8Array, encode with base64 + rows[0]['c_binary'] = Buffer.from(rows[0]['c_binary']).toString('base64') + rows[0]['c_varbinary'] = Buffer.from(rows[0]['c_varbinary']).toString('base64') + // blob type returns Uint8Array, encode with utf8 + rows[0]['t_tinyblob'] = Buffer.from(rows[0]['t_tinyblob']).toString() + rows[0]['t_blob'] = Buffer.from(rows[0]['t_blob']).toString() + rows[0]['t_mediumblob'] = Buffer.from(rows[0]['t_mediumblob']).toString() + rows[0]['t_longblob'] = Buffer.from(rows[0]['t_longblob']).toString() + // bit type returns Uint8Array, get it with hex + rows[0]['t_bit'] = uint8ArrayToHex(rows[0]['t_bit']) + expect(JSON.stringify(rows[0])).toEqual(JSON.stringify(fullTypeResult)) }) + + test('test raw bytes as input', async () => { + const con = connect({ url: databaseURL, database: database, fetch }) + const tableName = 'raw_bytes' + const tableDDL = ` + create table ${tableName} ( + bytes blob + )` + await con.execute(`DROP table IF EXISTS ${tableName}`) + await con.execute(tableDDL) + + const input = 'FSDF' + const inputAsBuffer = Buffer.from(input, 'base64') + await con.execute(`insert into ${tableName} values (?)`, [inputAsBuffer]) + const rows = (await con.execute(`select * from ${tableName}`)) as Row[] + + console.log(rows) + expect(rows.length).toEqual(1) + const outputRaw = rows[0]['bytes'] + const outputAsBuffer = Buffer.from(outputRaw) + const output = outputAsBuffer.toString('base64') + + expect(input).toEqual(output) + }) }) diff --git a/src/decode.ts b/src/decode.ts index 950bf2a..2073503 100644 --- a/src/decode.ts +++ b/src/decode.ts @@ -32,25 +32,34 @@ export function cast(field: Field, value: string | null, decoder: Decoders): any case 'DECIMAL': case 'CHAR': case 'VARCHAR': - case 'BINARY': - case 'VARBINARY': - case 'TINYTEXT': case 'TEXT': case 'MEDIUMTEXT': case 'LONGTEXT': - case 'TINYBLOB': - case 'BLOB': - case 'MEDIUMBLOB': - case 'LONGBLOB': + case 'TINYTEXT': case 'DATE': case 'TIME': case 'DATETIME': case 'TIMESTAMP': - case 'BIT': return value + case 'BLOB': + case 'TINYBLOB': + case 'MEDIUMBLOB': + case 'LONGBLOB': + case 'BINARY': + case 'VARBINARY': + case 'BIT': + return hexToUint8Array(value) case 'JSON': return JSON.parse(value) default: return value } } + +function hexToUint8Array(hexString: string): Uint8Array { + const uint8Array = new Uint8Array(hexString.length / 2) + for (let i = 0; i < hexString.length; i += 2) { + uint8Array[i / 2] = parseInt(hexString.substring(i, i + 2), 16) + } + return uint8Array +} diff --git a/src/format.ts b/src/format.ts index 39ad691..39ac5c2 100644 --- a/src/format.ts +++ b/src/format.ts @@ -35,6 +35,10 @@ function sanitize(value: Value): string { return value ? 'true' : 'false' } + if (value instanceof Uint8Array) { + return uint8ArrayToHex(value) + } + if (typeof value === 'string') { return quote(value) } @@ -84,3 +88,8 @@ function replacement(text: string): string { return '' } } + +export function uint8ArrayToHex(uint8: Uint8Array): string { + const digits = Array.from(uint8).map((i) => i.toString(16).padStart(2, '0')) + return `0x${digits.join('')}` +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..e69de29