UNPKG

ts-postgres

Version:
921 lines (920 loc) 36.5 kB
import { Buffer } from 'node:buffer'; import { ElasticBuffer } from './buffer.js'; import { sign } from './sasl.js'; import { sum } from './utils.js'; import { arrayDataTypeMapping, isPoint, DataFormat, DataType, } from './types.js'; const arrayMask = 1 << 31; const readerMask = 1 << 29; const infinity = Number('Infinity'); const timeshift = 946684800000; const isUndefined = Object.is.bind(null, undefined); export var Command; (function (Command) { Command[Command["Bind"] = 66] = "Bind"; Command[Command["Close"] = 67] = "Close"; Command[Command["Describe"] = 68] = "Describe"; Command[Command["End"] = 88] = "End"; Command[Command["Execute"] = 69] = "Execute"; Command[Command["Flush"] = 72] = "Flush"; Command[Command["Parse"] = 80] = "Parse"; Command[Command["Password"] = 112] = "Password"; Command[Command["Query"] = 81] = "Query"; Command[Command["Sync"] = 83] = "Sync"; })(Command || (Command = {})); export var SASL; (function (SASL) { SASL[SASL["SASLResponse"] = 112] = "SASLResponse"; })(SASL || (SASL = {})); export var ErrorLevel; (function (ErrorLevel) { ErrorLevel["Debug1"] = "DEBUG1"; ErrorLevel["Debug2"] = "DEBUG2"; ErrorLevel["Debug3"] = "DEBUG3"; ErrorLevel["Debug4"] = "DEBUG4"; ErrorLevel["Debug5"] = "DEBUG5"; ErrorLevel["Error"] = "ERROR"; ErrorLevel["Fatal"] = "FATAL"; ErrorLevel["Log"] = "LOG"; ErrorLevel["Notice"] = "NOTICE"; ErrorLevel["Panic"] = "PANIC"; })(ErrorLevel || (ErrorLevel = {})); export var Message; (function (Message) { Message[Message["Authentication"] = 82] = "Authentication"; Message[Message["BackendKeyData"] = 75] = "BackendKeyData"; Message[Message["BindComplete"] = 50] = "BindComplete"; Message[Message["CloseComplete"] = 51] = "CloseComplete"; Message[Message["CommandComplete"] = 67] = "CommandComplete"; Message[Message["EmptyQueryResponse"] = 73] = "EmptyQueryResponse"; Message[Message["ErrorResponse"] = 69] = "ErrorResponse"; Message[Message["NoData"] = 110] = "NoData"; Message[Message["Notice"] = 78] = "Notice"; Message[Message["NotificationResponse"] = 65] = "NotificationResponse"; Message[Message["ParseComplete"] = 49] = "ParseComplete"; Message[Message["ParameterDescription"] = 116] = "ParameterDescription"; Message[Message["ParameterStatus"] = 83] = "ParameterStatus"; Message[Message["ReadyForQuery"] = 90] = "ReadyForQuery"; Message[Message["RowData"] = 68] = "RowData"; Message[Message["RowDescription"] = 84] = "RowDescription"; })(Message || (Message = {})); export var SSLResponseCode; (function (SSLResponseCode) { SSLResponseCode[SSLResponseCode["Supported"] = 83] = "Supported"; SSLResponseCode[SSLResponseCode["NotSupported"] = 78] = "NotSupported"; })(SSLResponseCode || (SSLResponseCode = {})); export var TransactionStatus; (function (TransactionStatus) { TransactionStatus[TransactionStatus["Idle"] = 73] = "Idle"; TransactionStatus[TransactionStatus["InTransaction"] = 84] = "InTransaction"; TransactionStatus[TransactionStatus["InError"] = 69] = "InError"; })(TransactionStatus || (TransactionStatus = {})); export var SegmentType; (function (SegmentType) { SegmentType[SegmentType["Buffer"] = 0] = "Buffer"; SegmentType[SegmentType["Float4"] = 1] = "Float4"; SegmentType[SegmentType["Float8"] = 2] = "Float8"; SegmentType[SegmentType["Int8"] = 3] = "Int8"; SegmentType[SegmentType["Int16BE"] = 4] = "Int16BE"; SegmentType[SegmentType["Int32BE"] = 5] = "Int32BE"; SegmentType[SegmentType["Int64BE"] = 6] = "Int64BE"; SegmentType[SegmentType["UInt32BE"] = 7] = "UInt32BE"; })(SegmentType || (SegmentType = {})); export class DatabaseError extends Error { constructor(level, code, message, detail, hint, file, line, routine, position) { super(message); this.level = level; this.code = code; this.message = message; this.detail = detail; this.hint = hint; this.file = file; this.line = line; this.routine = routine; this.position = position; const actualProto = new.target.prototype; if (Object.setPrototypeOf) { Object.setPrototypeOf(this, actualProto); } else { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ this.__proto__ = actualProto; } } } const nullBuffer = Buffer.from('null'); function dateToStringUTC(date, includeTime) { const pad = (n, length) => n.toString().padStart(length, '0'); const year = date.getUTCFullYear(); const isBC = year < 0; let result = pad(isBC ? 1 - year : year, 4) + '-' + pad(date.getUTCMonth() + 1, 2) + '-' + pad(date.getUTCDate(), 2); if (includeTime) { result += 'T' + pad(date.getUTCHours(), 2) + ':' + pad(date.getUTCMinutes(), 2) + ':' + pad(date.getUTCSeconds(), 2) + '.' + pad(date.getUTCMilliseconds(), 3) + '+00:00'; } if (isBC) { result += ' BC'; } return result; } function formatUuid(bytes) { const slice = (start, end) => { return bytes.subarray(start, end).toString('hex'); }; return [ slice(0, 4), slice(4, 6), slice(6, 8), slice(8, 10), slice(10, 16), ].join('-'); } function parseUuid(uuid) { return Buffer.from(uuid.replace(/-/g, ''), 'hex'); } function makeBuffer(s, encoding, nullTerminate = false) { return Buffer.from(nullTerminate ? s + '\0' : s, encoding); } function makeBufferSegment(s, encoding, nullTerminate = false) { return [SegmentType.Buffer, makeBuffer(s, encoding, nullTerminate)]; } function getSegmentSize(segment, value) { switch (segment) { case SegmentType.Buffer: { if (value instanceof Buffer) { return value.length; } else { break; } } case SegmentType.Int64BE: case SegmentType.Float8: { return 8; } case SegmentType.Int8: { return 1; } case SegmentType.Int16BE: { return 2; } case SegmentType.Float4: case SegmentType.Int32BE: case SegmentType.UInt32BE: { return 4; } } return -1; } function getMessageSize(code, segments) { // Messages are composed of a one byte message code plus a // 32-bit message length. let size = 4 + (code ? 1 : 0); // Precompute total message size. const length = segments.length; for (let i = 0; i < length; i++) { const [segment, value] = segments[i]; size += Math.max(getSegmentSize(segment, value), 0); } return size; } export function readRowDescription(buffer, start, types) { let offset = start; const length = buffer.readInt16BE(offset); const columns = new Uint32Array(length); const names = new Array(length); offset += 2; let i = 0; while (i < length) { const j = buffer.indexOf('\0', offset); const name = buffer.subarray(offset, j).toString(); const dataType = buffer.readInt32BE(j + 7); const innerDataType = arrayDataTypeMapping.get(dataType); const isArray = typeof innerDataType !== 'undefined'; const typeReader = types ? types.get(dataType) : undefined; columns[i] = (innerDataType || dataType) | (isArray ? arrayMask : 0) | (typeReader ? readerMask : 0); names[i] = name; i++; offset = j + 19; } return { columns: columns, names: names, }; } export function readRowData(buffer, row, columnSpecification, encoding, bigints, types, streams) { const columns = row.length; const bufferLength = buffer.length; // Find the row index (i.e., column) that's undefined which is // where we start reading row data. let i = row.findIndex(isUndefined); let offset = 0; while (i < columns) { // Must have enough data available to read column size. const start = offset + 4; if (bufferLength < start) break; const j = i; i++; const length = buffer.readInt32BE(offset); // If the length is reported as -1, this means a NULL value. const dataLength = length >= 0 ? length : 0; const end = start + dataLength; const remaining = end - bufferLength; const partial = remaining > 0; let value = null; if (start < end) { const spec = columnSpecification[j]; let skip = false; if (streams && spec === DataType.Bytea) { const stream = streams[j]; if (stream) { const slice = buffer.subarray(start, end); const alloc = Buffer.allocUnsafe(slice.length); slice.copy(alloc, 0, 0, slice.length); stream.write(alloc); buffer.writeInt32BE(length - alloc.length, bufferLength - 4); if (partial) { return bufferLength - 4; } skip = true; } } if (partial) { break; } if (!skip) { const dataType = spec & ~arrayMask & ~readerMask; const isArray = (spec & arrayMask) !== 0; const isReader = (spec & readerMask) !== 0; if (isReader) { const reader = types?.get(dataType); if (reader) { value = reader(buffer, start, end, DataFormat.Binary, encoding); } } else { const read = (t, start, end) => { if (start === end) return null; /* Cutoff for system object OIDs; see comments in src/include/access/transam.h We do not support user object OIDs. */ if (t >= DataType.MinUserOid) return null; switch (t) { case DataType.Bool: return buffer[start] !== 0; case DataType.Date: { const n = buffer.readInt32BE(start); if (n === 0x7fffffff) return infinity; if (n === -0x80000000) return -infinity; // Shift from 2000 to 1970 and fix units. return new Date(n * 1000 * 86400 + timeshift); } case DataType.Timestamp: case DataType.Timestamptz: { const lo = buffer.readUInt32BE(start + 4); const hi = buffer.readInt32BE(start); if (lo === 0xffffffff && hi === 0x7fffffff) return infinity; if (lo === 0x00000000 && hi === -0x80000000) return -infinity; return new Date((lo + hi * 4294967296) / 1000 + timeshift); } case DataType.Int2: return buffer.readInt16BE(start); case DataType.Int4: case DataType.Oid: return buffer.readInt32BE(start); case DataType.Int8: { const value = buffer.readBigInt64BE(start); if (bigints) return value; if (value > Number.MAX_SAFE_INTEGER) { throw new Error("INT8 value too big for 'number' type"); } return Number(value); } case DataType.Float4: return buffer.readFloatBE(start); case DataType.Float8: return buffer.readDoubleBE(start); case DataType.Bpchar: case DataType.Char: case DataType.Name: case DataType.Text: case DataType.Varchar: return buffer.toString(encoding, start, end); case DataType.Bytea: const new_buffer = Buffer.allocUnsafe(end - start); buffer.copy(new_buffer, 0, start, end); return new_buffer; case DataType.Jsonb: if (buffer[start] === 1) { const jsonb = buffer.toString(encoding, start + 1, end); if (jsonb) { return JSON.parse(jsonb); } } break; case DataType.Json: const json = buffer.toString(encoding, start, end); if (json) { return JSON.parse(json); } break; case DataType.Point: return { x: buffer.readDoubleBE(start), y: buffer.readDoubleBE(start + 8), }; case DataType.Uuid: return formatUuid(buffer.subarray(start, end)); } return null; }; if (isArray) { let offset = start; const readArray = (size) => { const array = new Array(size); for (let j = 0; j < size; j++) { const length = buffer.readInt32BE(offset); offset += 4; let value = null; if (length >= 0) { const elementStart = offset; offset = elementStart + length; value = read(elementType, elementStart, offset); } array[j] = value; } return array; }; const dimCount = buffer.readInt32BE(offset) - 1; const elementType = buffer.readInt32BE((offset += 8)); offset += 4; if (dimCount === 0) { const size = buffer.readInt32BE(offset); offset += 8; value = readArray(size); } else { const arrays = new Array(dimCount); const dims = new Uint32Array(dimCount); for (let j = 0; j < dimCount; j++) { const size = buffer.readInt32BE(offset); dims[j] = size; offset += 8; } const size = buffer.readInt32BE(offset); const counts = Uint32Array.from(dims); const total = dims.reduce((a, b) => a * b); offset += 8; for (let l = 0; l < total; l++) { let next = readArray(size); for (let j = dimCount - 1; j >= 0; j--) { const count = counts[j]; const dim = dims[j]; const k = dim - count; const m = count - 1; if (k === 0) { arrays[j] = new Array(dim); } const array = arrays[j]; array[k] = next; counts[j] = m || dims[j]; if (m !== 0) break; next = array; } } value = arrays[0]; } } else { value = read(dataType, start, end); } } } } row[j] = value; offset = end; } return offset; } export function writeMessage(code, segments) { const size = getMessageSize(code, segments); const buffer = Buffer.allocUnsafe(size); writeMessageInto(code, segments, buffer); return buffer; } function writeMessageInto(code, segments, buffer) { let offset = 0; if (code) buffer[offset++] = code; buffer.writeInt32BE(buffer.length - (code ? 1 : 0), offset); offset += 4; const length = segments.length; for (let i = 0; i < length; i++) { const [segment, value] = segments[i]; switch (segment) { case SegmentType.Buffer: { if (value instanceof Buffer) { value.copy(buffer, offset); offset += value.length; } break; } case SegmentType.Float4: { const n = Number(value); buffer.writeFloatBE(n, offset); offset += 4; break; } case SegmentType.Float8: { const n = Number(value); buffer.writeDoubleBE(n, offset); offset += 8; break; } case SegmentType.Int8: { const n = Number(value); buffer.writeInt8(n, offset); offset += 1; break; } case SegmentType.Int16BE: { const n = Number(value); buffer.writeInt16BE(n, offset); offset += 2; break; } case SegmentType.Int32BE: { const n = Number(value); buffer.writeInt32BE(n, offset); offset += 4; break; } case SegmentType.Int64BE: { const n = value instanceof Buffer ? value.readBigInt64BE(0) : typeof value === 'bigint' ? value : BigInt(Number(value)); buffer.writeBigInt64BE(n, offset); offset += 8; break; } case SegmentType.UInt32BE: { const n = Number(value); buffer.writeUInt32BE(n, offset); offset += 4; break; } } } } export class Reader { constructor(buffer, start, end) { this.buffer = buffer; this.start = start; this.end = end; } readInt32BE() { const n = this.buffer.readInt32BE(this.start); this.start += 4; return n; } readCString(encoding) { const start = this.start; const i = this.buffer.indexOf(0, start); const s = this.buffer.toString(encoding, start, i); this.start = i + 1; return s; } readRowData(row, columnSpecification, encoding, bigints, types, streams) { return readRowData(this.buffer.subarray(this.start, this.end), row, columnSpecification, encoding, bigints, types, streams); } readRowDescription(types) { return readRowDescription(this.buffer, this.start, types); } } export class Writer { constructor(encoding) { this.encoding = encoding; this.outgoing = new ElasticBuffer(); } bind(name, portal, format = DataFormat.Binary, values = [], types = []) { // We silently ignore any mismatch here, assuming that the // query will fail and make the error evident. const length = Math.min(types.length, values.length); const segments = [ makeBufferSegment(portal, this.encoding, true), makeBufferSegment(name, this.encoding, true), [SegmentType.Int16BE, length], ]; const getFormat = typeof format === 'number' ? () => format : (i) => format[i]; for (let i = 0; i < length; i++) { segments.push([SegmentType.Int16BE, getFormat(i)]); } segments.push([SegmentType.Int16BE, length]); const add = (message, value) => { segments.push([message, value]); return getSegmentSize(message, value); }; const reserve = (message) => { const segment = [message, null]; segments.push(segment); return (value) => { segment[1] = value; }; }; const addBinaryValue = (value, dataType) => { let size = -1; const setSize = reserve(SegmentType.Int32BE); if (value === null) { setSize(-1); return 0; } switch (dataType) { case DataType.Bool: { size = add(SegmentType.Int8, value ? 1 : 0); break; } case DataType.Date: { if (value === infinity) { size = add(SegmentType.Int32BE, 0x7fffffff); } else if (value === -infinity) { size = add(SegmentType.Int32BE, -0x80000000); } else if (value instanceof Date) { size = add(SegmentType.Int32BE, (value.getTime() - timeshift) / (1000 * 86400)); } break; } case DataType.Timestamp: case DataType.Timestamptz: { if (value === infinity) { size = sum(add(SegmentType.UInt32BE, 0x7fffffff), add(SegmentType.UInt32BE, 0xffffffff)); } else if (value === -infinity) { size = sum(add(SegmentType.UInt32BE, 0x80000000), add(SegmentType.UInt32BE, 0x00000000)); } else if (value instanceof Date) { const n = (value.getTime() - timeshift) * 1000; const f = Math.floor(n / 4294967296); const r = n - f * 4294967296; size = sum(add(SegmentType.Int32BE, f), add(SegmentType.UInt32BE, r)); } break; } case DataType.Bpchar: case DataType.Bytea: case DataType.Char: case DataType.Name: case DataType.Text: case DataType.Varchar: { if (value instanceof Buffer) { size = add(SegmentType.Buffer, value); } else { const s = String(value); size = add(SegmentType.Buffer, makeBuffer(s, this.encoding)); } break; } case DataType.Float4: { size = add(SegmentType.Float4, Number(value)); break; } case DataType.Float8: { size = add(SegmentType.Float8, Number(value)); break; } case DataType.Int2: { size = add(SegmentType.Int16BE, Number(value)); break; } case DataType.Int4: case DataType.Oid: { size = add(SegmentType.Int32BE, Number(value)); break; } case DataType.Int8: { size = add(SegmentType.Int64BE, value instanceof Buffer ? value.readBigInt64BE(0) : typeof value === 'bigint' ? value : Number(value)); break; } case DataType.Point: { if (isPoint(value)) { size = sum(add(SegmentType.Float8, value.x), add(SegmentType.Float8, value.y)); } break; } case DataType.Jsonb: const body = JSON.stringify(value); add(SegmentType.Int8, 0x01); size = 1 + add(SegmentType.Buffer, makeBuffer(body, this.encoding)); break; case DataType.Json: { const body = JSON.stringify(value); size = add(SegmentType.Buffer, makeBuffer(body, this.encoding)); break; } case DataType.Uuid: { try { if (typeof value === 'string') { const buffer = parseUuid(value); size = add(SegmentType.Buffer, buffer); } } catch (error) { throw new Error(`Invalid UUID: ${value} (${error})`); } break; } default: { const innerDataType = arrayDataTypeMapping.get(dataType); if (innerDataType && value instanceof Array) { size = addBinaryArray(value, innerDataType); } else { throw new Error(`Unsupported data type: ${dataType}`); } } } setSize(size); return size; }; const addBinaryArray = (value, dataType) => { const setDimCount = reserve(SegmentType.Int32BE); add(SegmentType.Int32BE, 1); add(SegmentType.Int32BE, dataType); let bytes = 12; let dimCount = 0; const go = (level, value) => { const length = value.length; if (length === 0) return; if (level === dimCount) { bytes += sum(add(SegmentType.Int32BE, length), add(SegmentType.Int32BE, 1)); dimCount++; } for (let i = 0; i < length; i++) { const v = value[i]; if (v instanceof Array) { go(level + 1, v); } else { bytes += addBinaryValue(v, dataType) + 4; } } }; go(0, value); setDimCount(dimCount); return bytes; }; const getTextFromValue = (value, dataType) => { if (value === null) return null; switch (dataType) { case DataType.Bool: return value ? 't' : 'f'; case DataType.Int2: case DataType.Int4: case DataType.Int8: case DataType.Oid: case DataType.Float4: case DataType.Float8: if (typeof value === 'number') { return value.toString(); } break; case DataType.Bpchar: case DataType.Bytea: case DataType.Char: case DataType.Name: case DataType.Text: case DataType.Varchar: return (typeof value === 'string' ? value : value instanceof Buffer ? value.toString(this.encoding) : value.toString()); case DataType.Date: return value instanceof Date ? dateToStringUTC(value, false) : value.toString(); case DataType.Timestamp: case DataType.Timestamptz: return value instanceof Date ? dateToStringUTC(value, true) : value.toString(); case DataType.Jsonb: case DataType.Json: return JSON.stringify(value); default: { const innerDataType = arrayDataTypeMapping.get(dataType); if (innerDataType) { if (value instanceof Array) { return getTextFromArray(value, innerDataType); } } throw new Error(`Unsupported data type: ${dataType}`); } } return null; }; const getTextFromArray = (value, dataType) => { const strings = []; strings.push('{'); const escape = (s) => { return s .replace(/\\/gu, '\\\\') .replace(/"/gu, '\\"') .replace(/,/gu, '\\,'); }; for (let i = 0; i < value.length; i++) { if (i > 0) strings.push(','); const child = value[i]; const result = child instanceof Array ? getTextFromArray(child, dataType) : getTextFromValue(child, dataType); if (result instanceof Array) { strings.push(...result); } else { strings.push(result === null ? 'null' : escape(result)); } } strings.push('}'); return strings; }; for (let i = 0; i < length; i++) { const value = values[i]; const dataType = types[i]; const format = getFormat(i); if (format === DataFormat.Binary) { addBinaryValue(value, dataType); } else { const result = getTextFromValue(value, dataType); const setSize = reserve(SegmentType.Int32BE); const size = result instanceof Array ? sum(...result.map((s) => add(SegmentType.Buffer, makeBuffer(s, this.encoding)))) : add(SegmentType.Buffer, result === null ? nullBuffer : (makeBuffer(result, this.encoding))); setSize(size); } } add(SegmentType.Int16BE, 1); add(SegmentType.Int16BE, 1); this.enqueue(Command.Bind, segments); } close(name, kind) { this.enqueue(Command.Close, [ makeBufferSegment(kind + name, this.encoding, true), ]); } describe(name, kind) { this.enqueue(Command.Describe, [ makeBufferSegment(kind + name, this.encoding, true), ]); } execute(portal, limit = 0) { this.enqueue(Command.Execute, [ makeBufferSegment(portal, this.encoding, true), [SegmentType.Int32BE, limit], ]); } end() { this.enqueue(Command.End, []); } flush() { this.enqueue(Command.Flush, []); } parse(name, text, types = []) { const length = types.length; const segments = [ makeBufferSegment(name, this.encoding, true), makeBufferSegment(text, this.encoding, true), [SegmentType.Int16BE, length], ]; for (let i = 0; i < length; i++) { segments.push([SegmentType.Int32BE, types[i]]); } this.enqueue(Command.Parse, segments); } password(text) { this.enqueue(Command.Password, [ makeBufferSegment(text, this.encoding, true), ]); } saslInitialResponse(mechanism, clientNonce) { if (mechanism !== 'SCRAM-SHA-256') return false; const response = Buffer.from('n,,n=*,r=' + clientNonce); this.enqueue(SASL.SASLResponse, [ makeBufferSegment(mechanism, this.encoding, true), [SegmentType.Int32BE, response.length], [SegmentType.Buffer, response], ]); return true; } saslResponse(data, password, clientNonce) { const [response, signature] = sign(data, password, clientNonce); this.enqueue(SASL.SASLResponse, [ makeBufferSegment(response, this.encoding, false), ]); return signature; } saslFinal(data, serverSignature) { if (!data.split(',').find((attr) => { if (attr[0] === 'v') { return attr.substr(2) === serverSignature; } return false; })) throw new Error('SASL server signature does not match'); } send(socket) { if (this.outgoing.empty) return; const buffer = this.outgoing.consume(); if (buffer) { return socket.write(buffer, () => this.outgoing.offer(buffer)); } return true; } startup(settings) { const data = []; const options = { user: settings.user, database: settings.database, client_encoding: this.encoding, client_min_messages: settings.clientMinMessages, default_table_access_method: settings.defaultTableAccessMethod, default_tablespace: settings.defaultTablespace, default_transaction_isolation: settings.defaultTransactionIsolation, extra_float_digits: settings.extraFloatDigits, idle_in_transaction_session_timeout: settings.idleInTransactionSessionTimeout, idle_session_timeout: settings.idleSessionTimeout, lock_timeout: settings.lockTimeout, search_path: settings.searchPath, statement_timeout: settings.statementTimeout, }; for (const [k, v] of Object.entries(options)) { if (v !== undefined && v !== '') { data.push(k); data.push(String(v)); } } data.push(''); const segments = [ [SegmentType.Int16BE, 3], [SegmentType.Int16BE, 0], ]; for (const s of data) { segments.push(makeBufferSegment(s, this.encoding, true)); } this.enqueue(null, segments); } startupSSL() { const segments = [ [SegmentType.Int16BE, 0x04d2], [SegmentType.Int16BE, 0x162f], ]; this.enqueue(null, segments); } sync() { this.enqueue(Command.Sync, []); } enqueue(code, segments) { const size = getMessageSize(code, segments); // Allocate space and write segments. const buffer = this.outgoing.getBuffer(size); writeMessageInto(code, segments, buffer); } } //# sourceMappingURL=protocol.js.map