diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index ad1155258..2d7d613c5 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -12,6 +12,7 @@ const { ScriptWriterEval, SqlGenerator, playJsonScriptWriter, + serializeJsTypesForJsonStringify, } = require('dbgate-tools'); const requireEngineDriver = require('../utility/requireEngineDriver'); const { connectUtility } = require('../utility/connectUtility'); @@ -232,7 +233,7 @@ async function handleQueryData({ msgid, sql, range }, skipReadonlyCheck = false) try { if (!skipReadonlyCheck) ensureExecuteCustomScript(driver); const res = await driver.query(dbhan, sql, { range }); - process.send({ msgtype: 'response', msgid, ...res }); + process.send({ msgtype: 'response', msgid, ...serializeJsTypesForJsonStringify(res) }); } catch (err) { process.send({ msgtype: 'response', diff --git a/packages/api/src/utility/handleQueryStream.js b/packages/api/src/utility/handleQueryStream.js index e0f64ba73..76e573a57 100644 --- a/packages/api/src/utility/handleQueryStream.js +++ b/packages/api/src/utility/handleQueryStream.js @@ -4,6 +4,7 @@ const fs = require('fs'); const _ = require('lodash'); const { jsldir } = require('../utility/directories'); +const { serializeJsTypesReplacer } = require('dbgate-tools'); class QueryStreamTableWriter { constructor(sesid = undefined) { @@ -38,7 +39,7 @@ class QueryStreamTableWriter { row(row) { // console.log('ACCEPT ROW', row); - this.currentStream.write(JSON.stringify(row) + '\n'); + this.currentStream.write(JSON.stringify(row, serializeJsTypesReplacer) + '\n'); this.currentRowCount += 1; if (!this.plannedStats) { diff --git a/packages/filterparser/src/filterTool.ts b/packages/filterparser/src/filterTool.ts index 2e502db96..864e16021 100644 --- a/packages/filterparser/src/filterTool.ts +++ b/packages/filterparser/src/filterTool.ts @@ -20,6 +20,7 @@ export function getFilterValueExpression(value, dataType?) { if (value === true) return 'TRUE'; if (value === false) return 'FALSE'; if (value.$oid) return `ObjectId("${value.$oid}")`; + if (value.$bigint) return value.$bigint; if (value.type == 'Buffer' && Array.isArray(value.data)) { return '0x' + arrayToHexString(value.data); } diff --git a/packages/filterparser/src/parseFilter.ts b/packages/filterparser/src/parseFilter.ts index fbc224280..144b40f28 100644 --- a/packages/filterparser/src/parseFilter.ts +++ b/packages/filterparser/src/parseFilter.ts @@ -2,14 +2,18 @@ import P from 'parsimmon'; import moment from 'moment'; import { Condition } from 'dbgate-sqltree'; import { interpretEscapes, token, word, whitespace } from './common'; -import { hexStringToArray } from 'dbgate-tools'; +import { hexStringToArray, parseNumberSafe } from 'dbgate-tools'; import { FilterBehaviour, TransformType } from 'dbgate-types'; const binaryCondition = (operator, numberDualTesting = false) => value => { - const numValue = parseFloat(value); - if (numberDualTesting && !isNaN(numValue)) { + const numValue = parseNumberSafe(value); + if ( + numberDualTesting && + // @ts-ignore + !isNaN(numValue) + ) { return { conditionType: 'or', conditions: [ @@ -345,17 +349,17 @@ const createParser = (filterBehaviour: FilterBehaviour) => { string1Num: () => token(P.regexp(/"-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?"/, 1)) - .map(Number) + .map(parseNumberSafe) .desc('numer quoted'), string2Num: () => token(P.regexp(/'-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?'/, 1)) - .map(Number) + .map(parseNumberSafe) .desc('numer quoted'), number: () => token(P.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)) - .map(Number) + .map(parseNumberSafe) .desc('number'), objectid: () => token(P.regexp(/ObjectId\(['"]?[0-9a-f]{24}['"]?\)/)).desc('ObjectId'), diff --git a/packages/sqltree/src/evaluateCondition.ts b/packages/sqltree/src/evaluateCondition.ts index 795fe4965..d52d1365e 100644 --- a/packages/sqltree/src/evaluateCondition.ts +++ b/packages/sqltree/src/evaluateCondition.ts @@ -16,11 +16,17 @@ function isLike(value, test) { return res; } +function extractRawValue(value) { + if (value?.$bigint) return value.$bigint; + if (value?.$oid) return value.$oid; + return value; +} + export function evaluateCondition(condition: Condition, values) { switch (condition.conditionType) { case 'binary': - const left = evaluateExpression(condition.left, values); - const right = evaluateExpression(condition.right, values); + const left = extractRawValue(evaluateExpression(condition.left, values)); + const right = extractRawValue(evaluateExpression(condition.right, values)); switch (condition.operator) { case '=': return left == right; @@ -50,10 +56,15 @@ export function evaluateCondition(condition: Condition, values) { case 'or': return condition.conditions.some(cond => evaluateCondition(cond, values)); case 'like': - return isLike(evaluateExpression(condition.left, values), evaluateExpression(condition.right, values)); - break; + return isLike( + extractRawValue(evaluateExpression(condition.left, values)), + extractRawValue(evaluateExpression(condition.right, values)) + ); case 'notLike': - return !isLike(evaluateExpression(condition.left, values), evaluateExpression(condition.right, values)); + return !isLike( + extractRawValue(evaluateExpression(condition.left, values)), + extractRawValue(evaluateExpression(condition.right, values)) + ); case 'not': return !evaluateCondition(condition.condition, values); case 'anyColumnPass': diff --git a/packages/tools/src/SqlDumper.ts b/packages/tools/src/SqlDumper.ts index 371fb75fe..d5bc11ec6 100644 --- a/packages/tools/src/SqlDumper.ts +++ b/packages/tools/src/SqlDumper.ts @@ -78,6 +78,7 @@ export class SqlDumper implements AlterProcessor { else if (_isNumber(value)) this.putRaw(value.toString()); else if (_isDate(value)) this.putStringValue(new Date(value).toISOString()); else if (value?.type == 'Buffer' && _isArray(value?.data)) this.putByteArrayValue(value?.data); + else if (value?.$bigint) this.putRaw(value?.$bigint); else if (_isPlainObject(value) || _isArray(value)) this.putStringValue(JSON.stringify(value)); else this.put('^null'); } diff --git a/packages/tools/src/stringTools.ts b/packages/tools/src/stringTools.ts index 1bd65e653..adf6aec3c 100644 --- a/packages/tools/src/stringTools.ts +++ b/packages/tools/src/stringTools.ts @@ -4,6 +4,7 @@ import _isDate from 'lodash/isDate'; import _isNumber from 'lodash/isNumber'; import _isPlainObject from 'lodash/isPlainObject'; import _pad from 'lodash/pad'; +import _cloneDeepWith from 'lodash/cloneDeepWith'; import { DataEditorTypesBehaviour } from 'dbgate-types'; export type EditorDataType = @@ -208,6 +209,12 @@ export function stringifyCellValue( } } } + if (value?.$bigint) { + return { + value: value.$bigint, + gridStyle: 'valueCellStyle', + }; + } if (editorTypes?.parseDateAsDollar) { if (value?.$date) { @@ -343,6 +350,9 @@ export function shouldOpenMultilineDialog(value) { if (value?.$date) { return false; } + if (value?.$bigint) { + return false; + } if (_isPlainObject(value) || _isArray(value)) { return true; } @@ -573,3 +583,44 @@ export function jsonLinesParse(jsonLines: string): any[] { }) .filter(x => x); } + +export function serializeJsTypesForJsonStringify(obj) { + return _cloneDeepWith(obj, value => { + if (typeof value === 'bigint') { + return { $bigint: value.toString() }; + } + }); +} + +export function deserializeJsTypesFromJsonParse(obj) { + return _cloneDeepWith(obj, value => { + if (value?.$bigint) { + return BigInt(value.$bigint); + } + }); +} + +export function serializeJsTypesReplacer(key, value) { + if (typeof value === 'bigint') { + return { $bigint: value.toString() }; + } + return value; +} + +export function deserializeJsTypesReviver(key, value) { + if (value?.$bigint) { + return BigInt(value.$bigint); + } + return value; +} + +export function parseNumberSafe(value) { + if (/^-?[0-9]+$/.test(value)) { + const parsed = parseInt(value); + if (Number.isSafeInteger(parsed)) { + return parsed; + } + return BigInt(value); + } + return parseFloat(value); +} diff --git a/packages/web/src/datagrid/DataGridCell.svelte b/packages/web/src/datagrid/DataGridCell.svelte index ba618460c..f7596cb69 100644 --- a/packages/web/src/datagrid/DataGridCell.svelte +++ b/packages/web/src/datagrid/DataGridCell.svelte @@ -54,7 +54,8 @@ $: style = computeStyle(maxWidth, col); - $: isJson = _.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid; + $: isJson = + _.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid && !value.$bigint; // don't parse JSON for explicit data types $: jsonParsedValue = !editorTypes?.explicitDataType && isJsonLikeLongString(value) ? safeJsonParse(value) : null; diff --git a/packages/web/src/datagrid/gridutil.ts b/packages/web/src/datagrid/gridutil.ts index a631cd512..0d155d87b 100644 --- a/packages/web/src/datagrid/gridutil.ts +++ b/packages/web/src/datagrid/gridutil.ts @@ -72,6 +72,7 @@ export function countColumnSizes(grider: Grider, columns, containerWidth, displa let text = value; if (_.isArray(value)) text = `[${value.length} items]`; else if (value?.$oid) text = `ObjectId("${value.$oid}")`; + else if (value?.$bigint) text = value.$bigint; else if (isJsonLikeLongString(value) && safeJsonParse(value)) text = '(JSON)'; const width = context.measureText(text).width + 8; // console.log('colName', colName, text, width); diff --git a/packages/web/src/utility/api.ts b/packages/web/src/utility/api.ts index bcb72a31f..bfe827cfe 100644 --- a/packages/web/src/utility/api.ts +++ b/packages/web/src/utility/api.ts @@ -13,6 +13,7 @@ import { callServerPing } from './connectionsPinger'; import { batchDispatchCacheTriggers, dispatchCacheChange } from './cache'; import { isAdminPage, isOneOfPage } from './pageDefs'; import { openWebLink } from './simpleTools'; +import { serializeJsTypesReplacer } from 'dbgate-tools'; export const strmid = uuidv1(); @@ -177,7 +178,7 @@ export async function apiCall( 'Content-Type': 'application/json', ...resolveApiHeaders(), }, - body: JSON.stringify(args), + body: JSON.stringify(args, serializeJsTypesReplacer), }); if (resp.status == 401 && !apiDisabled) { diff --git a/plugins/dbgate-plugin-postgres/src/backend/drivers.js b/plugins/dbgate-plugin-postgres/src/backend/drivers.js index 54dcd146e..3413915b6 100644 --- a/plugins/dbgate-plugin-postgres/src/backend/drivers.js +++ b/plugins/dbgate-plugin-postgres/src/backend/drivers.js @@ -21,6 +21,11 @@ const logger = getLogger('postreDriver'); pg.types.setTypeParser(1082, 'text', val => val); // date pg.types.setTypeParser(1114, 'text', val => val); // timestamp without timezone pg.types.setTypeParser(1184, 'text', val => val); // timestamp +pg.types.setTypeParser(20, 'text', val => { + const parsed = parseInt(val); + if (Number.isSafeInteger(parsed)) return parsed; + return BigInt(val); +}); // timestamp function extractGeographyDate(value) { try {