mirror of
https://github.com/swc-project/swc.git
synced 2024-12-21 12:41:54 +03:00
782 lines
21 KiB
TypeScript
782 lines
21 KiB
TypeScript
|
// Loaded from https://raw.githubusercontent.com/deno-postgres/deno-postgres/master/connection/connection.ts
|
||
|
|
||
|
|
||
|
/*!
|
||
|
* Substantial parts adapted from https://github.com/brianc/node-postgres
|
||
|
* which is licensed as follows:
|
||
|
*
|
||
|
* The MIT License (MIT)
|
||
|
*
|
||
|
* Copyright (c) 2010 - 2019 Brian Carlson
|
||
|
*
|
||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||
|
* a copy of this software and associated documentation files (the
|
||
|
* 'Software'), to deal in the Software without restriction, including
|
||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||
|
* the following conditions:
|
||
|
*
|
||
|
* The above copyright notice and this permission notice shall be
|
||
|
* included in all copies or substantial portions of the Software.
|
||
|
*
|
||
|
* THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
|
*/
|
||
|
|
||
|
import { bold, BufReader, BufWriter, yellow } from "../deps.ts";
|
||
|
import { DeferredStack } from "./deferred.ts";
|
||
|
import { hashMd5Password, readUInt32BE } from "../utils.ts";
|
||
|
import { PacketReader } from "./packet_reader.ts";
|
||
|
import { PacketWriter } from "./packet_writer.ts";
|
||
|
import { parseError, parseNotice } from "./warning.ts";
|
||
|
import {
|
||
|
Query,
|
||
|
QueryArrayResult,
|
||
|
QueryObjectResult,
|
||
|
QueryResult,
|
||
|
} from "../query/query.ts";
|
||
|
import type { ConnectionParams } from "./connection_params.ts";
|
||
|
|
||
|
export enum ResultType {
|
||
|
ARRAY,
|
||
|
OBJECT,
|
||
|
}
|
||
|
|
||
|
export enum Format {
|
||
|
TEXT = 0,
|
||
|
BINARY = 1,
|
||
|
}
|
||
|
|
||
|
enum TransactionStatus {
|
||
|
Idle = "I",
|
||
|
IdleInTransaction = "T",
|
||
|
InFailedTransaction = "E",
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This asserts the argument bind response is succesful
|
||
|
*/
|
||
|
function assertArgumentsResponse(msg: Message) {
|
||
|
switch (msg.type) {
|
||
|
// bind completed
|
||
|
case "2":
|
||
|
// no-op
|
||
|
break;
|
||
|
// error response
|
||
|
case "E":
|
||
|
throw parseError(msg);
|
||
|
default:
|
||
|
throw new Error(`Unexpected frame: ${msg.type}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function assertSuccessfulStartup(msg: Message) {
|
||
|
switch (msg.type) {
|
||
|
case "E":
|
||
|
throw parseError(msg);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// deno-lint-ignore camelcase
|
||
|
function assertSuccessfulAuthentication(auth_message: Message) {
|
||
|
if (auth_message.type === "E") {
|
||
|
throw parseError(auth_message);
|
||
|
} else if (auth_message.type !== "R") {
|
||
|
throw new Error(`Unexpected auth response: ${auth_message.type}.`);
|
||
|
}
|
||
|
|
||
|
const responseCode = auth_message.reader.readInt32();
|
||
|
if (responseCode !== 0) {
|
||
|
throw new Error(`Unexpected auth response code: ${responseCode}.`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This asserts the query parse response is succesful
|
||
|
*/
|
||
|
function assertQueryResponse(msg: Message) {
|
||
|
switch (msg.type) {
|
||
|
// parse completed
|
||
|
case "1":
|
||
|
// TODO: add to already parsed queries if
|
||
|
// query has name, so it's not parsed again
|
||
|
break;
|
||
|
// error response
|
||
|
case "E":
|
||
|
throw parseError(msg);
|
||
|
default:
|
||
|
throw new Error(`Unexpected frame: ${msg.type}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class Message {
|
||
|
public reader: PacketReader;
|
||
|
|
||
|
constructor(
|
||
|
public type: string,
|
||
|
public byteCount: number,
|
||
|
public body: Uint8Array,
|
||
|
) {
|
||
|
this.reader = new PacketReader(body);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class Column {
|
||
|
constructor(
|
||
|
public name: string,
|
||
|
public tableOid: number,
|
||
|
public index: number,
|
||
|
public typeOid: number,
|
||
|
public columnLength: number,
|
||
|
public typeModifier: number,
|
||
|
public format: Format,
|
||
|
) {}
|
||
|
}
|
||
|
|
||
|
export class RowDescription {
|
||
|
constructor(public columnCount: number, public columns: Column[]) {}
|
||
|
}
|
||
|
|
||
|
const decoder = new TextDecoder();
|
||
|
const encoder = new TextEncoder();
|
||
|
|
||
|
//TODO
|
||
|
//Refactor properties to not be lazily initialized
|
||
|
//or to handle their undefined value
|
||
|
export class Connection {
|
||
|
#bufReader!: BufReader;
|
||
|
#bufWriter!: BufWriter;
|
||
|
#conn!: Deno.Conn;
|
||
|
connected = false;
|
||
|
#packetWriter = new PacketWriter();
|
||
|
// TODO
|
||
|
// Find out what parameters are for
|
||
|
#parameters: { [key: string]: string } = {};
|
||
|
// TODO
|
||
|
// Find out what the pid is for
|
||
|
#pid?: number;
|
||
|
#queryLock: DeferredStack<undefined> = new DeferredStack(
|
||
|
1,
|
||
|
[undefined],
|
||
|
);
|
||
|
// TODO
|
||
|
// Find out what the secret key is for
|
||
|
#secretKey?: number;
|
||
|
// TODO
|
||
|
// Find out what the transaction status is used for
|
||
|
#transactionStatus?: TransactionStatus;
|
||
|
|
||
|
constructor(private connParams: ConnectionParams) {}
|
||
|
|
||
|
/** Read single message sent by backend */
|
||
|
private async readMessage(): Promise<Message> {
|
||
|
// TODO: reuse buffer instead of allocating new ones each for each read
|
||
|
const header = new Uint8Array(5);
|
||
|
await this.#bufReader.readFull(header);
|
||
|
const msgType = decoder.decode(header.slice(0, 1));
|
||
|
const msgLength = readUInt32BE(header, 1) - 4;
|
||
|
const msgBody = new Uint8Array(msgLength);
|
||
|
await this.#bufReader.readFull(msgBody);
|
||
|
|
||
|
return new Message(msgType, msgLength, msgBody);
|
||
|
}
|
||
|
|
||
|
private async serverAcceptsTLS(): Promise<boolean> {
|
||
|
const writer = this.#packetWriter;
|
||
|
writer.clear();
|
||
|
writer
|
||
|
.addInt32(8)
|
||
|
.addInt32(80877103)
|
||
|
.join();
|
||
|
|
||
|
await this.#bufWriter.write(writer.flush());
|
||
|
await this.#bufWriter.flush();
|
||
|
|
||
|
const response = new Uint8Array(1);
|
||
|
await this.#conn.read(response);
|
||
|
|
||
|
switch (String.fromCharCode(response[0])) {
|
||
|
case "S":
|
||
|
return true;
|
||
|
case "N":
|
||
|
return false;
|
||
|
default:
|
||
|
throw new Error(
|
||
|
`Could not check if server accepts SSL connections, server responded with: ${response}`,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private async sendStartupMessage(): Promise<Message> {
|
||
|
const writer = this.#packetWriter;
|
||
|
writer.clear();
|
||
|
// protocol version - 3.0, written as
|
||
|
writer.addInt16(3).addInt16(0);
|
||
|
const connParams = this.connParams;
|
||
|
// TODO: recognize other parameters
|
||
|
writer.addCString("user").addCString(connParams.user);
|
||
|
writer.addCString("database").addCString(connParams.database);
|
||
|
writer.addCString("application_name").addCString(
|
||
|
connParams.applicationName,
|
||
|
);
|
||
|
|
||
|
// eplicitly set utf-8 encoding
|
||
|
writer.addCString("client_encoding").addCString("'utf-8'");
|
||
|
// terminator after all parameters were writter
|
||
|
writer.addCString("");
|
||
|
|
||
|
const bodyBuffer = writer.flush();
|
||
|
const bodyLength = bodyBuffer.length + 4;
|
||
|
|
||
|
writer.clear();
|
||
|
|
||
|
const finalBuffer = writer
|
||
|
.addInt32(bodyLength)
|
||
|
.add(bodyBuffer)
|
||
|
.join();
|
||
|
|
||
|
await this.#bufWriter.write(finalBuffer);
|
||
|
await this.#bufWriter.flush();
|
||
|
|
||
|
return await this.readMessage();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* https://www.postgresql.org/docs/13/protocol-flow.html#id-1.10.5.7.3
|
||
|
* */
|
||
|
async startup() {
|
||
|
const {
|
||
|
hostname,
|
||
|
port,
|
||
|
tls: {
|
||
|
enforce: enforceTLS,
|
||
|
},
|
||
|
} = this.connParams;
|
||
|
|
||
|
this.#conn = await Deno.connect({ port, hostname });
|
||
|
this.#bufWriter = new BufWriter(this.#conn);
|
||
|
|
||
|
/**
|
||
|
* https://www.postgresql.org/docs/13/protocol-flow.html#id-1.10.5.7.11
|
||
|
* */
|
||
|
if (await this.serverAcceptsTLS()) {
|
||
|
try {
|
||
|
//@ts-ignore TS2339
|
||
|
if (typeof Deno.startTls === "undefined") {
|
||
|
throw new Error(
|
||
|
"You need to execute Deno with the `--unstable` argument in order to stablish a TLS connection",
|
||
|
);
|
||
|
}
|
||
|
//@ts-ignore TS2339
|
||
|
this.#conn = await Deno.startTls(this.#conn, { hostname });
|
||
|
} catch (e) {
|
||
|
if (!enforceTLS) {
|
||
|
console.error(
|
||
|
bold(yellow("TLS connection failed with message: ")) +
|
||
|
e.message +
|
||
|
"\n" +
|
||
|
bold("Defaulting to non-encrypted connection"),
|
||
|
);
|
||
|
this.#conn = await Deno.connect({ port, hostname });
|
||
|
} else {
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
this.#bufWriter = new BufWriter(this.#conn);
|
||
|
} else if (enforceTLS) {
|
||
|
throw new Error(
|
||
|
"The server isn't accepting TLS connections. Change the client configuration so TLS configuration isn't required to connect",
|
||
|
);
|
||
|
}
|
||
|
|
||
|
this.#bufReader = new BufReader(this.#conn);
|
||
|
|
||
|
try {
|
||
|
// deno-lint-ignore camelcase
|
||
|
const startup_response = await this.sendStartupMessage();
|
||
|
assertSuccessfulStartup(startup_response);
|
||
|
await this.authenticate(startup_response);
|
||
|
|
||
|
// Handle connection status
|
||
|
// (connected but not ready)
|
||
|
let msg;
|
||
|
connection_status:
|
||
|
while (true) {
|
||
|
msg = await this.readMessage();
|
||
|
switch (msg.type) {
|
||
|
// Connection error (wrong database or user)
|
||
|
case "E":
|
||
|
await this.processError(msg, false);
|
||
|
break;
|
||
|
// backend key data
|
||
|
case "K":
|
||
|
this._processBackendKeyData(msg);
|
||
|
break;
|
||
|
// parameter status
|
||
|
case "S":
|
||
|
this._processParameterStatus(msg);
|
||
|
break;
|
||
|
// ready for query
|
||
|
case "Z": {
|
||
|
this._processReadyForQuery(msg);
|
||
|
break connection_status;
|
||
|
}
|
||
|
default:
|
||
|
throw new Error(`Unknown response for startup: ${msg.type}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.connected = true;
|
||
|
} catch (e) {
|
||
|
this.#conn.close();
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TODO
|
||
|
// Why is this handling the startup message response?
|
||
|
/**
|
||
|
* Will attempt to authenticate with the database using the provided
|
||
|
* password credentials
|
||
|
*/
|
||
|
private async authenticate(msg: Message) {
|
||
|
const code = msg.reader.readInt32();
|
||
|
switch (code) {
|
||
|
// pass
|
||
|
case 0:
|
||
|
break;
|
||
|
// cleartext password
|
||
|
case 3:
|
||
|
await assertSuccessfulAuthentication(
|
||
|
await this.authenticateWithClearPassword(),
|
||
|
);
|
||
|
break;
|
||
|
// md5 password
|
||
|
case 5: {
|
||
|
const salt = msg.reader.readBytes(4);
|
||
|
await assertSuccessfulAuthentication(
|
||
|
await this.authenticateWithMd5(salt),
|
||
|
);
|
||
|
break;
|
||
|
}
|
||
|
case 7: {
|
||
|
throw new Error(
|
||
|
"Database server expected gss authentication, which is not supported at the moment",
|
||
|
);
|
||
|
}
|
||
|
// scram-sha-256 password
|
||
|
case 10: {
|
||
|
throw new Error(
|
||
|
"Database server expected scram-sha-256 authentication, which is not supported at the moment",
|
||
|
);
|
||
|
}
|
||
|
default:
|
||
|
throw new Error(`Unknown auth message code ${code}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private async authenticateWithClearPassword(): Promise<Message> {
|
||
|
this.#packetWriter.clear();
|
||
|
const password = this.connParams.password || "";
|
||
|
const buffer = this.#packetWriter.addCString(password).flush(0x70);
|
||
|
|
||
|
await this.#bufWriter.write(buffer);
|
||
|
await this.#bufWriter.flush();
|
||
|
|
||
|
return this.readMessage();
|
||
|
}
|
||
|
|
||
|
private async authenticateWithMd5(salt: Uint8Array): Promise<Message> {
|
||
|
this.#packetWriter.clear();
|
||
|
|
||
|
if (!this.connParams.password) {
|
||
|
throw new Error("Auth Error: attempting MD5 auth with password unset");
|
||
|
}
|
||
|
|
||
|
const password = hashMd5Password(
|
||
|
this.connParams.password,
|
||
|
this.connParams.user,
|
||
|
salt,
|
||
|
);
|
||
|
const buffer = this.#packetWriter.addCString(password).flush(0x70);
|
||
|
|
||
|
await this.#bufWriter.write(buffer);
|
||
|
await this.#bufWriter.flush();
|
||
|
|
||
|
return this.readMessage();
|
||
|
}
|
||
|
|
||
|
private _processBackendKeyData(msg: Message) {
|
||
|
this.#pid = msg.reader.readInt32();
|
||
|
this.#secretKey = msg.reader.readInt32();
|
||
|
}
|
||
|
|
||
|
private _processParameterStatus(msg: Message) {
|
||
|
// TODO: should we save all parameters?
|
||
|
const key = msg.reader.readCString();
|
||
|
const value = msg.reader.readCString();
|
||
|
this.#parameters[key] = value;
|
||
|
}
|
||
|
|
||
|
private _processReadyForQuery(msg: Message) {
|
||
|
const txStatus = msg.reader.readByte();
|
||
|
this.#transactionStatus = String.fromCharCode(
|
||
|
txStatus,
|
||
|
) as TransactionStatus;
|
||
|
}
|
||
|
|
||
|
private async _readReadyForQuery() {
|
||
|
const msg = await this.readMessage();
|
||
|
|
||
|
if (msg.type !== "Z") {
|
||
|
throw new Error(
|
||
|
`Unexpected message type: ${msg.type}, expected "Z" (ReadyForQuery)`,
|
||
|
);
|
||
|
}
|
||
|
|
||
|
this._processReadyForQuery(msg);
|
||
|
}
|
||
|
|
||
|
private async _simpleQuery(
|
||
|
query: Query,
|
||
|
type: ResultType,
|
||
|
): Promise<QueryResult> {
|
||
|
this.#packetWriter.clear();
|
||
|
|
||
|
const buffer = this.#packetWriter.addCString(query.text).flush(0x51);
|
||
|
|
||
|
await this.#bufWriter.write(buffer);
|
||
|
await this.#bufWriter.flush();
|
||
|
|
||
|
let result;
|
||
|
if (type === ResultType.ARRAY) {
|
||
|
result = new QueryArrayResult(query);
|
||
|
} else {
|
||
|
result = new QueryObjectResult(query);
|
||
|
}
|
||
|
|
||
|
let msg: Message;
|
||
|
|
||
|
msg = await this.readMessage();
|
||
|
|
||
|
// Query startup message, executed only once
|
||
|
switch (msg.type) {
|
||
|
// row description
|
||
|
case "T":
|
||
|
result.loadColumnDescriptions(this.parseRowDescription(msg));
|
||
|
break;
|
||
|
// no data
|
||
|
case "n":
|
||
|
break;
|
||
|
// error response
|
||
|
case "E":
|
||
|
await this.processError(msg);
|
||
|
break;
|
||
|
// notice response
|
||
|
case "N":
|
||
|
result.warnings.push(await this.processNotice(msg));
|
||
|
break;
|
||
|
// command complete
|
||
|
// TODO: this is duplicated in next loop
|
||
|
case "C": {
|
||
|
const commandTag = this.getCommandTag(msg);
|
||
|
result.handleCommandComplete(commandTag);
|
||
|
result.done();
|
||
|
break;
|
||
|
}
|
||
|
default:
|
||
|
throw new Error(`Unexpected frame: ${msg.type}`);
|
||
|
}
|
||
|
|
||
|
// Handle each row returned by the query
|
||
|
while (true) {
|
||
|
msg = await this.readMessage();
|
||
|
switch (msg.type) {
|
||
|
// data row
|
||
|
case "D": {
|
||
|
// this is actually packet read
|
||
|
result.insertRow(this.parseRowData(msg));
|
||
|
break;
|
||
|
}
|
||
|
// command complete
|
||
|
case "C": {
|
||
|
const commandTag = this.getCommandTag(msg);
|
||
|
result.handleCommandComplete(commandTag);
|
||
|
result.done();
|
||
|
break;
|
||
|
}
|
||
|
// ready for query
|
||
|
case "Z":
|
||
|
this._processReadyForQuery(msg);
|
||
|
return result;
|
||
|
// error response
|
||
|
case "E":
|
||
|
await this.processError(msg);
|
||
|
break;
|
||
|
// notice response
|
||
|
case "N":
|
||
|
result.warnings.push(await this.processNotice(msg));
|
||
|
break;
|
||
|
case "T":
|
||
|
result.loadColumnDescriptions(this.parseRowDescription(msg));
|
||
|
break;
|
||
|
default:
|
||
|
throw new Error(`Unexpected frame: ${msg.type}`);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private async appendQueryToMessage(query: Query) {
|
||
|
this.#packetWriter.clear();
|
||
|
|
||
|
const buffer = this.#packetWriter
|
||
|
.addCString("") // TODO: handle named queries (config.name)
|
||
|
.addCString(query.text)
|
||
|
.addInt16(0)
|
||
|
.flush(0x50);
|
||
|
await this.#bufWriter.write(buffer);
|
||
|
}
|
||
|
|
||
|
private async appendArgumentsToMessage(query: Query) {
|
||
|
this.#packetWriter.clear();
|
||
|
|
||
|
const hasBinaryArgs = query.args.some((arg) => arg instanceof Uint8Array);
|
||
|
|
||
|
// bind statement
|
||
|
this.#packetWriter.clear();
|
||
|
this.#packetWriter
|
||
|
.addCString("") // TODO: unnamed portal
|
||
|
.addCString(""); // TODO: unnamed prepared statement
|
||
|
|
||
|
if (hasBinaryArgs) {
|
||
|
this.#packetWriter.addInt16(query.args.length);
|
||
|
|
||
|
query.args.forEach((arg) => {
|
||
|
this.#packetWriter.addInt16(arg instanceof Uint8Array ? 1 : 0);
|
||
|
});
|
||
|
} else {
|
||
|
this.#packetWriter.addInt16(0);
|
||
|
}
|
||
|
|
||
|
this.#packetWriter.addInt16(query.args.length);
|
||
|
|
||
|
query.args.forEach((arg) => {
|
||
|
if (arg === null || typeof arg === "undefined") {
|
||
|
this.#packetWriter.addInt32(-1);
|
||
|
} else if (arg instanceof Uint8Array) {
|
||
|
this.#packetWriter.addInt32(arg.length);
|
||
|
this.#packetWriter.add(arg);
|
||
|
} else {
|
||
|
const byteLength = encoder.encode(arg).length;
|
||
|
this.#packetWriter.addInt32(byteLength);
|
||
|
this.#packetWriter.addString(arg);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
this.#packetWriter.addInt16(0);
|
||
|
const buffer = this.#packetWriter.flush(0x42);
|
||
|
await this.#bufWriter.write(buffer);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* This function appends the query type (in this case prepared statement)
|
||
|
* to the message
|
||
|
*/
|
||
|
private async appendQueryTypeToMessage() {
|
||
|
this.#packetWriter.clear();
|
||
|
|
||
|
const buffer = this.#packetWriter.addCString("P").flush(0x44);
|
||
|
await this.#bufWriter.write(buffer);
|
||
|
}
|
||
|
|
||
|
private async appendExecuteToMessage() {
|
||
|
this.#packetWriter.clear();
|
||
|
|
||
|
const buffer = this.#packetWriter
|
||
|
.addCString("") // unnamed portal
|
||
|
.addInt32(0)
|
||
|
.flush(0x45);
|
||
|
await this.#bufWriter.write(buffer);
|
||
|
}
|
||
|
|
||
|
private async appendSyncToMessage() {
|
||
|
this.#packetWriter.clear();
|
||
|
|
||
|
const buffer = this.#packetWriter.flush(0x53);
|
||
|
await this.#bufWriter.write(buffer);
|
||
|
}
|
||
|
|
||
|
private async processError(msg: Message, recoverable = true) {
|
||
|
const error = parseError(msg);
|
||
|
if (recoverable) {
|
||
|
await this._readReadyForQuery();
|
||
|
}
|
||
|
throw error;
|
||
|
}
|
||
|
|
||
|
private processNotice(msg: Message) {
|
||
|
const warning = parseNotice(msg);
|
||
|
console.error(`${bold(yellow(warning.severity))}: ${warning.message}`);
|
||
|
return warning;
|
||
|
}
|
||
|
|
||
|
// TODO: I believe error handling here is not correct, shouldn't 'sync' message be
|
||
|
// sent after error response is received in prepared statements?
|
||
|
/**
|
||
|
* https://www.postgresql.org/docs/13/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY
|
||
|
*/
|
||
|
private async _preparedQuery(
|
||
|
query: Query,
|
||
|
type: ResultType,
|
||
|
): Promise<QueryResult> {
|
||
|
await this.appendQueryToMessage(query);
|
||
|
await this.appendArgumentsToMessage(query);
|
||
|
await this.appendQueryTypeToMessage();
|
||
|
await this.appendExecuteToMessage();
|
||
|
await this.appendSyncToMessage();
|
||
|
// send all messages to backend
|
||
|
await this.#bufWriter.flush();
|
||
|
|
||
|
await assertQueryResponse(await this.readMessage());
|
||
|
await assertArgumentsResponse(await this.readMessage());
|
||
|
|
||
|
let result;
|
||
|
if (type === ResultType.ARRAY) {
|
||
|
result = new QueryArrayResult(query);
|
||
|
} else {
|
||
|
result = new QueryObjectResult(query);
|
||
|
}
|
||
|
let msg: Message;
|
||
|
msg = await this.readMessage();
|
||
|
|
||
|
switch (msg.type) {
|
||
|
// row description
|
||
|
case "T": {
|
||
|
const rowDescription = this.parseRowDescription(msg);
|
||
|
result.loadColumnDescriptions(rowDescription);
|
||
|
break;
|
||
|
}
|
||
|
// no data
|
||
|
case "n":
|
||
|
break;
|
||
|
// error
|
||
|
case "E":
|
||
|
await this.processError(msg);
|
||
|
break;
|
||
|
default:
|
||
|
throw new Error(`Unexpected frame: ${msg.type}`);
|
||
|
}
|
||
|
|
||
|
outerLoop:
|
||
|
while (true) {
|
||
|
msg = await this.readMessage();
|
||
|
switch (msg.type) {
|
||
|
// data row
|
||
|
case "D": {
|
||
|
// this is actually packet read
|
||
|
const rawDataRow = this.parseRowData(msg);
|
||
|
result.insertRow(rawDataRow);
|
||
|
break;
|
||
|
}
|
||
|
// command complete
|
||
|
case "C": {
|
||
|
const commandTag = this.getCommandTag(msg);
|
||
|
result.handleCommandComplete(commandTag);
|
||
|
result.done();
|
||
|
break outerLoop;
|
||
|
}
|
||
|
// error response
|
||
|
case "E":
|
||
|
await this.processError(msg);
|
||
|
break;
|
||
|
default:
|
||
|
throw new Error(`Unexpected frame: ${msg.type}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
await this._readReadyForQuery();
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
async query(query: Query, type: ResultType): Promise<QueryResult> {
|
||
|
if (!this.connected) {
|
||
|
throw new Error("The connection hasn't been initialized");
|
||
|
}
|
||
|
await this.#queryLock.pop();
|
||
|
try {
|
||
|
if (query.args.length === 0) {
|
||
|
return await this._simpleQuery(query, type);
|
||
|
} else {
|
||
|
return await this._preparedQuery(query, type);
|
||
|
}
|
||
|
} finally {
|
||
|
this.#queryLock.push(undefined);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private parseRowDescription(msg: Message): RowDescription {
|
||
|
const columnCount = msg.reader.readInt16();
|
||
|
const columns = [];
|
||
|
|
||
|
for (let i = 0; i < columnCount; i++) {
|
||
|
// TODO: if one of columns has 'format' == 'binary',
|
||
|
// all of them will be in same format?
|
||
|
const column = new Column(
|
||
|
msg.reader.readCString(), // name
|
||
|
msg.reader.readInt32(), // tableOid
|
||
|
msg.reader.readInt16(), // index
|
||
|
msg.reader.readInt32(), // dataTypeOid
|
||
|
msg.reader.readInt16(), // column
|
||
|
msg.reader.readInt32(), // typeModifier
|
||
|
msg.reader.readInt16(), // format
|
||
|
);
|
||
|
columns.push(column);
|
||
|
}
|
||
|
|
||
|
return new RowDescription(columnCount, columns);
|
||
|
}
|
||
|
|
||
|
//TODO
|
||
|
//Research corner cases where parseRowData can return null values
|
||
|
// deno-lint-ignore no-explicit-any
|
||
|
private parseRowData(msg: Message): any[] {
|
||
|
const fieldCount = msg.reader.readInt16();
|
||
|
const row = [];
|
||
|
|
||
|
for (let i = 0; i < fieldCount; i++) {
|
||
|
const colLength = msg.reader.readInt32();
|
||
|
|
||
|
if (colLength == -1) {
|
||
|
row.push(null);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// reading raw bytes here, they will be properly parsed later
|
||
|
row.push(msg.reader.readBytes(colLength));
|
||
|
}
|
||
|
|
||
|
return row;
|
||
|
}
|
||
|
|
||
|
private getCommandTag(msg: Message) {
|
||
|
return msg.reader.readString(msg.byteCount);
|
||
|
}
|
||
|
|
||
|
async end(): Promise<void> {
|
||
|
if (this.connected) {
|
||
|
const terminationMessage = new Uint8Array([0x58, 0x00, 0x00, 0x00, 0x04]);
|
||
|
await this.#bufWriter.write(terminationMessage);
|
||
|
await this.#bufWriter.flush();
|
||
|
this.#conn.close();
|
||
|
this.connected = false;
|
||
|
}
|
||
|
}
|
||
|
}
|