mirror of
https://github.com/oven-sh/bun
synced 2026-02-11 11:29:02 +00:00
Support jsonb, idle_timeout, connection_timeout, max_lifetime timeouts in bun:sql. Add onopen and onclose callbacks. Fix missing "code" property appearing in errors. Add error codes for postgres. (#16045)
This commit is contained in:
@@ -731,6 +731,8 @@ pub const EventLoopTimer = struct {
|
||||
StatWatcherScheduler,
|
||||
UpgradedDuplex,
|
||||
WindowsNamedPipe,
|
||||
PostgresSQLConnectionTimeout,
|
||||
PostgresSQLConnectionMaxLifetime,
|
||||
|
||||
pub fn Type(comptime T: Tag) type {
|
||||
return switch (T) {
|
||||
@@ -740,6 +742,8 @@ pub const EventLoopTimer = struct {
|
||||
.StatWatcherScheduler => StatWatcherScheduler,
|
||||
.UpgradedDuplex => uws.UpgradedDuplex,
|
||||
.WindowsNamedPipe => uws.WindowsNamedPipe,
|
||||
.PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection,
|
||||
.PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection,
|
||||
};
|
||||
}
|
||||
} else enum {
|
||||
@@ -748,6 +752,8 @@ pub const EventLoopTimer = struct {
|
||||
TestRunner,
|
||||
StatWatcherScheduler,
|
||||
UpgradedDuplex,
|
||||
PostgresSQLConnectionTimeout,
|
||||
PostgresSQLConnectionMaxLifetime,
|
||||
|
||||
pub fn Type(comptime T: Tag) type {
|
||||
return switch (T) {
|
||||
@@ -756,6 +762,8 @@ pub const EventLoopTimer = struct {
|
||||
.TestRunner => JSC.Jest.TestRunner,
|
||||
.StatWatcherScheduler => StatWatcherScheduler,
|
||||
.UpgradedDuplex => uws.UpgradedDuplex,
|
||||
.PostgresSQLConnectionTimeout => JSC.Postgres.PostgresSQLConnection,
|
||||
.PostgresSQLConnectionMaxLifetime => JSC.Postgres.PostgresSQLConnection,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -808,11 +816,14 @@ pub const EventLoopTimer = struct {
|
||||
|
||||
pub fn fire(this: *EventLoopTimer, now: *const timespec, vm: *VirtualMachine) Arm {
|
||||
switch (this.tag) {
|
||||
.PostgresSQLConnectionTimeout => return @as(*JSC.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("timer", this))).onConnectionTimeout(),
|
||||
.PostgresSQLConnectionMaxLifetime => return @as(*JSC.Postgres.PostgresSQLConnection, @alignCast(@fieldParentPtr("max_lifetime_timer", this))).onMaxLifetimeTimeout(),
|
||||
inline else => |t| {
|
||||
var container: *t.Type() = @alignCast(@fieldParentPtr("event_loop_timer", this));
|
||||
if (comptime t.Type() == TimerObject) {
|
||||
return container.fire(now, vm);
|
||||
}
|
||||
|
||||
if (comptime t.Type() == StatWatcherScheduler) {
|
||||
return container.timerCallback();
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ export default [
|
||||
name: "PostgresSQLConnection",
|
||||
construct: true,
|
||||
finalize: true,
|
||||
hasPendingActivity: true,
|
||||
configurable: false,
|
||||
hasPendingActivity: true,
|
||||
klass: {
|
||||
// escapeString: {
|
||||
// fn: "escapeString",
|
||||
@@ -20,9 +20,6 @@ export default [
|
||||
close: {
|
||||
fn: "doClose",
|
||||
},
|
||||
flush: {
|
||||
fn: "doFlush",
|
||||
},
|
||||
connected: {
|
||||
getter: "getConnected",
|
||||
},
|
||||
@@ -32,17 +29,30 @@ export default [
|
||||
unref: {
|
||||
fn: "doUnref",
|
||||
},
|
||||
query: {
|
||||
fn: "createQuery",
|
||||
|
||||
queries: {
|
||||
getter: "getQueries",
|
||||
this: true,
|
||||
},
|
||||
onconnect: {
|
||||
getter: "getOnConnect",
|
||||
setter: "setOnConnect",
|
||||
this: true,
|
||||
},
|
||||
onclose: {
|
||||
getter: "getOnClose",
|
||||
setter: "setOnClose",
|
||||
this: true,
|
||||
},
|
||||
},
|
||||
values: ["onconnect", "onclose", "queries"],
|
||||
}),
|
||||
define({
|
||||
name: "PostgresSQLQuery",
|
||||
construct: true,
|
||||
finalize: true,
|
||||
configurable: false,
|
||||
hasPendingActivity: true,
|
||||
|
||||
JSType: "0b11101110",
|
||||
klass: {},
|
||||
proto: {
|
||||
@@ -59,7 +69,7 @@ export default [
|
||||
length: 0,
|
||||
},
|
||||
},
|
||||
values: ["pendingValue", "columns", "binding"],
|
||||
values: ["pendingValue", "target", "columns", "binding"],
|
||||
estimatedSize: true,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -50,6 +50,9 @@ static JSC::JSObject* createErrorPrototype(JSC::VM& vm, JSC::JSGlobalObject* glo
|
||||
case JSC::ErrorType::URIError:
|
||||
prototype = JSC::constructEmptyObject(globalObject, globalObject->m_URIErrorStructure.prototype(globalObject));
|
||||
break;
|
||||
case JSC::ErrorType::SyntaxError:
|
||||
prototype = JSC::constructEmptyObject(globalObject, globalObject->m_syntaxErrorStructure.prototype(globalObject));
|
||||
break;
|
||||
default: {
|
||||
RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("TODO: Add support for more error types");
|
||||
break;
|
||||
|
||||
@@ -98,7 +98,37 @@ export default [
|
||||
["ERR_ASYNC_CALLBACK", TypeError],
|
||||
|
||||
// Postgres
|
||||
["ERR_POSTGRES_ERROR_RESPONSE", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_AUTHENTICATION_FAILED_PBKDF2", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_SERVER_ERROR", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_SYNTAX_ERROR", SyntaxError, "PostgresError"],
|
||||
["ERR_POSTGRES_CONNECTION_CLOSED", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_EXPECTED_REQUEST", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_EXPECTED_STATEMENT", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_INVALID_BACKEND_KEY_DATA", TypeError, "PostgresError"],
|
||||
["ERR_POSTGRES_INVALID_BINARY_DATA", TypeError, "PostgresError"],
|
||||
["ERR_POSTGRES_INVALID_BYTE_SEQUENCE_FOR_ENCODING", TypeError, "PostgresError"],
|
||||
["ERR_POSTGRES_INVALID_BYTE_SEQUENCE", TypeError, "PostgresError"],
|
||||
["ERR_POSTGRES_INVALID_CHARACTER", TypeError, "PostgresError"],
|
||||
["ERR_POSTGRES_INVALID_MESSAGE_LENGTH", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_INVALID_MESSAGE", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_INVALID_QUERY_BINDING", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_INVALID_SERVER_KEY", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_INVALID_SERVER_SIGNATURE", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YET", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_OVERFLOW", TypeError, "PostgresError"],
|
||||
["ERR_POSTGRES_SASL_SIGNATURE_INVALID_BASE64", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_SASL_SIGNATURE_MISMATCH", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_TLS_NOT_AVAILABLE", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_TLS_UPGRADE_FAILED", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_UNEXPECTED_MESSAGE", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_UNKNOWN_AUTHENTICATION_METHOD", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_UNSUPPORTED_BYTEA_FORMAT", TypeError, "PostgresError"],
|
||||
["ERR_POSTGRES_UNSUPPORTED_INTEGER_SIZE", TypeError, "PostgresError"],
|
||||
["ERR_POSTGRES_IDLE_TIMEOUT", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_CONNECTION_TIMEOUT", Error, "PostgresError"],
|
||||
["ERR_POSTGRES_LIFETIME_TIMEOUT", Error, "PostgresError"],
|
||||
|
||||
// AWS
|
||||
["ERR_AWS_MISSING_CREDENTIALS", Error],
|
||||
|
||||
@@ -90,6 +90,36 @@ extern "C" JSPropertyIterator* Bun__JSPropertyIterator__create(JSC::JSGlobalObje
|
||||
return JSPropertyIterator::create(vm, array.releaseData());
|
||||
}
|
||||
|
||||
// The only non-own property that we sometimes want to get is the code property.
|
||||
extern "C" EncodedJSValue Bun__JSPropertyIterator__getCodeProperty(JSPropertyIterator* iter, JSC::JSGlobalObject* globalObject, JSC::JSObject* object)
|
||||
{
|
||||
if (UNLIKELY(!iter)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto& vm = iter->vm;
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
if (UNLIKELY(object->type() == JSC::ProxyObjectType)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto& builtinNames = WebCore::builtinNames(vm);
|
||||
|
||||
PropertySlot slot(object, PropertySlot::InternalMethodType::VMInquiry, vm.ptr());
|
||||
if (!object->getNonIndexPropertySlot(globalObject, builtinNames.codePublicName(), slot)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (slot.isAccessor() || slot.isCustom()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
return JSValue::encode(slot.getPureResult());
|
||||
}
|
||||
|
||||
extern "C" size_t Bun__JSPropertyIterator__getLongestPropertyName(JSPropertyIterator* iter, JSC::JSGlobalObject* globalObject, JSC::JSObject* object)
|
||||
{
|
||||
size_t longest = 0;
|
||||
|
||||
@@ -8,7 +8,7 @@ extern "C" fn Bun__JSPropertyIterator__getNameAndValueNonObservable(iter: ?*anyo
|
||||
extern "C" fn Bun__JSPropertyIterator__getName(iter: ?*anyopaque, propertyName: *bun.String, i: usize) void;
|
||||
extern "C" fn Bun__JSPropertyIterator__deinit(iter: ?*anyopaque) void;
|
||||
extern "C" fn Bun__JSPropertyIterator__getLongestPropertyName(iter: ?*anyopaque, globalObject: *JSC.JSGlobalObject, object: *anyopaque) usize;
|
||||
|
||||
extern "C" fn Bun__JSPropertyIterator__getCodeProperty(iter: ?*anyopaque, globalObject: *JSC.JSGlobalObject, object: *anyopaque) JSC.JSValue;
|
||||
pub const JSPropertyIteratorOptions = struct {
|
||||
skip_empty_name: bool,
|
||||
include_value: bool,
|
||||
@@ -27,6 +27,7 @@ pub fn JSPropertyIterator(comptime options: JSPropertyIteratorOptions) type {
|
||||
globalObject: *JSC.JSGlobalObject,
|
||||
object: *JSC.JSCell = undefined,
|
||||
value: JSC.JSValue = .zero,
|
||||
tried_code_property: bool = false,
|
||||
|
||||
pub fn getLongestPropertyName(this: *@This()) usize {
|
||||
if (this.impl == null) return 0;
|
||||
@@ -53,6 +54,7 @@ pub fn JSPropertyIterator(comptime options: JSPropertyIteratorOptions) type {
|
||||
pub fn reset(this: *@This()) void {
|
||||
this.iter_i = 0;
|
||||
this.i = 0;
|
||||
this.tried_code_property = false;
|
||||
}
|
||||
|
||||
/// The bun.String returned has not incremented it's reference count.
|
||||
@@ -90,5 +92,27 @@ pub fn JSPropertyIterator(comptime options: JSPropertyIteratorOptions) type {
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/// "code" is not always an own property, and we want to get it without risking exceptions.
|
||||
pub fn getCodeProperty(this: *@This()) ?bun.String {
|
||||
if (comptime !options.include_value) {
|
||||
@compileError("TODO");
|
||||
}
|
||||
|
||||
if (this.tried_code_property) {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.tried_code_property = true;
|
||||
|
||||
const current = Bun__JSPropertyIterator__getCodeProperty(this.impl, this.globalObject, this.object);
|
||||
if (current == .zero) {
|
||||
return null;
|
||||
}
|
||||
current.ensureStillAlive();
|
||||
this.value = current;
|
||||
|
||||
return bun.String.static("code");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6739,10 +6739,10 @@ pub const CallFrame = opaque {
|
||||
/// arguments(n).mut() -> `var args = argumentsAsArray(n); &args`
|
||||
pub fn arguments_old(self: *const CallFrame, comptime max: usize) Arguments(max) {
|
||||
const slice = self.arguments();
|
||||
comptime bun.assert(max <= 10);
|
||||
comptime bun.assert(max <= 13);
|
||||
return switch (@as(u4, @min(slice.len, max))) {
|
||||
0 => .{ .ptr = undefined, .len = 0 },
|
||||
inline 1...10 => |count| Arguments(max).init(comptime @min(count, max), slice.ptr),
|
||||
inline 1...13 => |count| Arguments(max).init(comptime @min(count, max), slice.ptr),
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3956,7 +3956,7 @@ pub const VirtualMachine = struct {
|
||||
defer iterator.deinit();
|
||||
const longest_name = @min(iterator.getLongestPropertyName(), 10);
|
||||
var is_first_property = true;
|
||||
while (iterator.next()) |field| {
|
||||
while (iterator.next() orelse iterator.getCodeProperty()) |field| {
|
||||
const value = iterator.value;
|
||||
if (field.eqlComptime("message") or field.eqlComptime("name") or field.eqlComptime("stack")) {
|
||||
continue;
|
||||
@@ -3966,6 +3966,7 @@ pub const VirtualMachine = struct {
|
||||
if (field.eqlComptime("code")) {
|
||||
if (value.isString()) {
|
||||
const str = value.toBunString(this.global);
|
||||
defer str.deref();
|
||||
if (!str.isEmpty()) {
|
||||
if (str.eql(name)) {
|
||||
continue;
|
||||
|
||||
@@ -75,6 +75,15 @@ class Query extends PublicPromise {
|
||||
[_handler];
|
||||
[_queryStatus] = 0;
|
||||
|
||||
[Symbol.for("nodejs.util.inspect.custom")]() {
|
||||
const status = this[_queryStatus];
|
||||
const active = (status & QueryStatus.active) != 0;
|
||||
const cancelled = (status & QueryStatus.cancelled) != 0;
|
||||
const executed = (status & QueryStatus.executed) != 0;
|
||||
const error = (status & QueryStatus.error) != 0;
|
||||
return `PostgresQuery { ${active ? "active" : ""} ${cancelled ? "cancelled" : ""} ${executed ? "executed" : ""} ${error ? "error" : ""} }`;
|
||||
}
|
||||
|
||||
constructor(handle, handler) {
|
||||
var resolve_, reject_;
|
||||
super((resolve, reject) => {
|
||||
@@ -182,7 +191,7 @@ class Query extends PublicPromise {
|
||||
Object.defineProperty(Query, Symbol.species, { value: PublicPromise });
|
||||
Object.defineProperty(Query, Symbol.toStringTag, { value: "Query" });
|
||||
init(
|
||||
function (query, result, commandTag, count) {
|
||||
function onResolvePostgresQuery(query, result, commandTag, count, queries) {
|
||||
$assert(result instanceof SQLResultArray, "Invalid result array");
|
||||
if (typeof commandTag === "string") {
|
||||
if (commandTag.length > 0) {
|
||||
@@ -194,18 +203,48 @@ init(
|
||||
|
||||
result.count = count || 0;
|
||||
|
||||
if (queries) {
|
||||
const queriesIndex = queries.indexOf(query);
|
||||
if (queriesIndex !== -1) {
|
||||
queries.splice(queriesIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
query.resolve(result);
|
||||
} catch (e) {}
|
||||
},
|
||||
function (query, reject) {
|
||||
function onRejectPostgresQuery(query, reject, queries) {
|
||||
if (queries) {
|
||||
const queriesIndex = queries.indexOf(query);
|
||||
if (queriesIndex !== -1) {
|
||||
queries.splice(queriesIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
query.reject(reject);
|
||||
} catch (e) {}
|
||||
},
|
||||
);
|
||||
|
||||
function createConnection({ hostname, port, username, password, tls, query, database, sslMode }, onConnected, onClose) {
|
||||
function createConnection(
|
||||
{
|
||||
hostname,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
tls,
|
||||
query,
|
||||
database,
|
||||
sslMode,
|
||||
idleTimeout = 0,
|
||||
connectionTimeout = 30 * 1000,
|
||||
maxLifetime = 0,
|
||||
},
|
||||
onConnected,
|
||||
onClose,
|
||||
) {
|
||||
return _createConnection(
|
||||
hostname,
|
||||
Number(port),
|
||||
@@ -221,6 +260,9 @@ function createConnection({ hostname, port, username, password, tls, query, data
|
||||
query || "",
|
||||
onConnected,
|
||||
onClose,
|
||||
idleTimeout,
|
||||
connectionTimeout,
|
||||
maxLifetime,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -312,7 +354,20 @@ class SQLArrayParameter {
|
||||
}
|
||||
|
||||
function loadOptions(o) {
|
||||
var hostname, port, username, password, database, tls, url, query, adapter;
|
||||
var hostname,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
database,
|
||||
tls,
|
||||
url,
|
||||
query,
|
||||
adapter,
|
||||
idleTimeout,
|
||||
connectionTimeout,
|
||||
maxLifetime,
|
||||
onconnect,
|
||||
onclose;
|
||||
const env = Bun.env;
|
||||
var sslMode: SSLMode = SSLMode.disable;
|
||||
|
||||
@@ -375,6 +430,48 @@ function loadOptions(o) {
|
||||
tls ||= o.tls || o.ssl;
|
||||
adapter ||= o.adapter || "postgres";
|
||||
|
||||
idleTimeout ??= o.idleTimeout;
|
||||
idleTimeout ??= o.idle_timeout;
|
||||
connectionTimeout ??= o.connectionTimeout;
|
||||
connectionTimeout ??= o.connection_timeout;
|
||||
maxLifetime ??= o.maxLifetime;
|
||||
maxLifetime ??= o.max_lifetime;
|
||||
|
||||
onconnect ??= o.onconnect;
|
||||
onclose ??= o.onclose;
|
||||
if (onconnect !== undefined) {
|
||||
if (!$isCallable(onconnect)) {
|
||||
throw $ERR_INVALID_ARG_TYPE("onconnect", "function", onconnect);
|
||||
}
|
||||
}
|
||||
|
||||
if (onclose !== undefined) {
|
||||
if (!$isCallable(onclose)) {
|
||||
throw $ERR_INVALID_ARG_TYPE("onclose", "function", onclose);
|
||||
}
|
||||
}
|
||||
|
||||
if (idleTimeout != null) {
|
||||
idleTimeout = Number(idleTimeout);
|
||||
if (idleTimeout > 2 ** 31 || idleTimeout < 0 || idleTimeout !== idleTimeout) {
|
||||
throw $ERR_INVALID_ARG_VALUE("idle_timeout must be a non-negative integer less than 2^31");
|
||||
}
|
||||
}
|
||||
|
||||
if (connectionTimeout != null) {
|
||||
connectionTimeout = Number(connectionTimeout);
|
||||
if (connectionTimeout > 2 ** 31 || connectionTimeout < 0 || connectionTimeout !== connectionTimeout) {
|
||||
throw $ERR_INVALID_ARG_VALUE("connection_timeout must be a non-negative integer less than 2^31");
|
||||
}
|
||||
}
|
||||
|
||||
if (maxLifetime != null) {
|
||||
maxLifetime = Number(maxLifetime);
|
||||
if (maxLifetime > 2 ** 31 || maxLifetime < 0 || maxLifetime !== maxLifetime) {
|
||||
throw $ERR_INVALID_ARG_VALUE("max_lifetime must be a non-negative integer less than 2^31");
|
||||
}
|
||||
}
|
||||
|
||||
if (sslMode !== SSLMode.disable && !tls?.serverName) {
|
||||
if (hostname) {
|
||||
tls = {
|
||||
@@ -398,7 +495,23 @@ function loadOptions(o) {
|
||||
throw new Error(`Unsupported adapter: ${adapter}. Only \"postgres\" is supported for now`);
|
||||
}
|
||||
|
||||
return { hostname, port, username, password, database, tls, query, sslMode };
|
||||
const ret: any = { hostname, port, username, password, database, tls, query, sslMode };
|
||||
if (idleTimeout != null) {
|
||||
ret.idleTimeout = idleTimeout;
|
||||
}
|
||||
if (connectionTimeout != null) {
|
||||
ret.connectionTimeout = connectionTimeout;
|
||||
}
|
||||
if (maxLifetime != null) {
|
||||
ret.maxLifetime = maxLifetime;
|
||||
}
|
||||
if (onconnect !== undefined) {
|
||||
ret.onconnect = onconnect;
|
||||
}
|
||||
if (onclose !== undefined) {
|
||||
ret.onclose = onclose;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function SQL(o) {
|
||||
@@ -407,6 +520,7 @@ function SQL(o) {
|
||||
connecting = false,
|
||||
closed = false,
|
||||
onConnect: any[] = [],
|
||||
storedErrorForClosedConnection,
|
||||
connectionInfo = loadOptions(o);
|
||||
|
||||
function connectedHandler(query, handle, err) {
|
||||
@@ -415,7 +529,7 @@ function SQL(o) {
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
return query.reject(new Error("Not connected"));
|
||||
return query.reject(storedErrorForClosedConnection || new Error("Not connected"));
|
||||
}
|
||||
|
||||
if (query.cancelled) {
|
||||
@@ -423,6 +537,10 @@ function SQL(o) {
|
||||
}
|
||||
|
||||
handle.run(connection, query);
|
||||
|
||||
// if the above throws, we don't want it to be in the array.
|
||||
// This array exists mostly to keep the in-flight queries alive.
|
||||
connection.queries.push(query);
|
||||
}
|
||||
|
||||
function pendingConnectionHandler(query, handle) {
|
||||
@@ -434,7 +552,7 @@ function SQL(o) {
|
||||
}
|
||||
|
||||
function closedConnectionHandler(query, handle) {
|
||||
query.reject(new Error("Connection closed"));
|
||||
query.reject(storedErrorForClosedConnection || new Error("Connection closed"));
|
||||
}
|
||||
|
||||
function onConnected(err, result) {
|
||||
@@ -443,11 +561,31 @@ function SQL(o) {
|
||||
handler(err);
|
||||
}
|
||||
onConnect = [];
|
||||
|
||||
if (connected && connectionInfo?.onconnect) {
|
||||
connectionInfo.onconnect(err);
|
||||
}
|
||||
}
|
||||
|
||||
function onClose(err) {
|
||||
function onClose(err, queries) {
|
||||
closed = true;
|
||||
storedErrorForClosedConnection = err;
|
||||
if (sql === lazyDefaultSQL) {
|
||||
resetDefaultSQL(initialDefaultSQL);
|
||||
}
|
||||
|
||||
onConnected(err, undefined);
|
||||
if (queries) {
|
||||
const queriesCopy = queries.slice();
|
||||
queries.length = 0;
|
||||
for (const handler of queriesCopy) {
|
||||
handler.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (connectionInfo?.onclose) {
|
||||
connectionInfo.onclose(err);
|
||||
}
|
||||
}
|
||||
|
||||
function doCreateQuery(strings, values) {
|
||||
@@ -568,18 +706,23 @@ function SQL(o) {
|
||||
}
|
||||
|
||||
var lazyDefaultSQL;
|
||||
var defaultSQLObject = function sql(strings, ...values) {
|
||||
|
||||
function resetDefaultSQL(sql) {
|
||||
lazyDefaultSQL = sql;
|
||||
Object.assign(defaultSQLObject, lazyDefaultSQL);
|
||||
exportsObject.default = exportsObject.sql = lazyDefaultSQL;
|
||||
}
|
||||
|
||||
var initialDefaultSQL;
|
||||
var defaultSQLObject = (initialDefaultSQL = function sql(strings, ...values) {
|
||||
if (new.target) {
|
||||
return SQL(strings);
|
||||
}
|
||||
|
||||
if (!lazyDefaultSQL) {
|
||||
lazyDefaultSQL = SQL(undefined);
|
||||
Object.assign(defaultSQLObject, lazyDefaultSQL);
|
||||
exportsObject.default = exportsObject.sql = lazyDefaultSQL;
|
||||
resetDefaultSQL(SQL(undefined));
|
||||
}
|
||||
return lazyDefaultSQL(strings, ...values);
|
||||
};
|
||||
});
|
||||
|
||||
var exportsObject = {
|
||||
sql: defaultSQLObject,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,7 @@ const int4 = postgres.int4;
|
||||
const int8 = postgres.int8;
|
||||
const PostgresInt64 = postgres.PostgresInt64;
|
||||
const types = postgres.types;
|
||||
|
||||
const AnyPostgresError = postgres.AnyPostgresError;
|
||||
pub const ArrayList = struct {
|
||||
array: *std.ArrayList(u8),
|
||||
|
||||
@@ -23,11 +23,11 @@ pub const ArrayList = struct {
|
||||
return this.array.items.len;
|
||||
}
|
||||
|
||||
pub fn write(this: @This(), bytes: []const u8) anyerror!void {
|
||||
pub fn write(this: @This(), bytes: []const u8) AnyPostgresError!void {
|
||||
try this.array.appendSlice(bytes);
|
||||
}
|
||||
|
||||
pub fn pwrite(this: @This(), bytes: []const u8, i: usize) anyerror!void {
|
||||
pub fn pwrite(this: @This(), bytes: []const u8, i: usize) AnyPostgresError!void {
|
||||
@memcpy(this.array.items[i..][0..bytes.len], bytes);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ pub const StackReader = struct {
|
||||
pub fn ensureCapacity(this: StackReader, count: usize) bool {
|
||||
return this.buffer.len >= (this.offset.* + count);
|
||||
}
|
||||
pub fn read(this: StackReader, count: usize) anyerror!Data {
|
||||
pub fn read(this: StackReader, count: usize) AnyPostgresError!Data {
|
||||
const offset = this.offset.*;
|
||||
if (!this.ensureCapacity(count)) {
|
||||
return error.ShortRead;
|
||||
@@ -82,7 +82,7 @@ pub const StackReader = struct {
|
||||
.temporary = this.buffer[offset..this.offset.*],
|
||||
};
|
||||
}
|
||||
pub fn readZ(this: StackReader) anyerror!Data {
|
||||
pub fn readZ(this: StackReader) AnyPostgresError!Data {
|
||||
const remaining = this.peek();
|
||||
if (bun.strings.indexOfChar(remaining, 0)) |zero| {
|
||||
this.skip(zero + 1);
|
||||
@@ -98,8 +98,8 @@ pub const StackReader = struct {
|
||||
pub fn NewWriterWrap(
|
||||
comptime Context: type,
|
||||
comptime offsetFn_: (fn (ctx: Context) usize),
|
||||
comptime writeFunction_: (fn (ctx: Context, bytes: []const u8) anyerror!void),
|
||||
comptime pwriteFunction_: (fn (ctx: Context, bytes: []const u8, offset: usize) anyerror!void),
|
||||
comptime writeFunction_: (fn (ctx: Context, bytes: []const u8) AnyPostgresError!void),
|
||||
comptime pwriteFunction_: (fn (ctx: Context, bytes: []const u8, offset: usize) AnyPostgresError!void),
|
||||
) type {
|
||||
return struct {
|
||||
wrapped: Context,
|
||||
@@ -111,7 +111,7 @@ pub fn NewWriterWrap(
|
||||
|
||||
pub const WrappedWriter = @This();
|
||||
|
||||
pub inline fn write(this: @This(), data: []const u8) anyerror!void {
|
||||
pub inline fn write(this: @This(), data: []const u8) AnyPostgresError!void {
|
||||
try writeFn(this.wrapped, data);
|
||||
}
|
||||
|
||||
@@ -119,16 +119,16 @@ pub fn NewWriterWrap(
|
||||
index: usize,
|
||||
context: WrappedWriter,
|
||||
|
||||
pub fn write(this: LengthWriter) anyerror!void {
|
||||
pub fn write(this: LengthWriter) AnyPostgresError!void {
|
||||
try this.context.pwrite(&Int32(this.context.offset() - this.index), this.index);
|
||||
}
|
||||
|
||||
pub fn writeExcludingSelf(this: LengthWriter) anyerror!void {
|
||||
pub fn writeExcludingSelf(this: LengthWriter) AnyPostgresError!void {
|
||||
try this.context.pwrite(&Int32(this.context.offset() -| (this.index + 4)), this.index);
|
||||
}
|
||||
};
|
||||
|
||||
pub inline fn length(this: @This()) anyerror!LengthWriter {
|
||||
pub inline fn length(this: @This()) AnyPostgresError!LengthWriter {
|
||||
const i = this.offset();
|
||||
try this.int4(0);
|
||||
return LengthWriter{
|
||||
@@ -141,7 +141,7 @@ pub fn NewWriterWrap(
|
||||
return offsetFn(this.wrapped);
|
||||
}
|
||||
|
||||
pub inline fn pwrite(this: @This(), data: []const u8, i: usize) anyerror!void {
|
||||
pub inline fn pwrite(this: @This(), data: []const u8, i: usize) AnyPostgresError!void {
|
||||
try pwriteFn(this.wrapped, data, i);
|
||||
}
|
||||
|
||||
@@ -208,81 +208,81 @@ pub fn NewWriterWrap(
|
||||
|
||||
pub const FieldType = enum(u8) {
|
||||
/// Severity: the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message), or a localized translation of one of these. Always present.
|
||||
S = 'S',
|
||||
severity = 'S',
|
||||
|
||||
/// Severity: the field contents are ERROR, FATAL, or PANIC (in an error message), or WARNING, NOTICE, DEBUG, INFO, or LOG (in a notice message). This is identical to the S field except that the contents are never localized. This is present only in messages generated by PostgreSQL versions 9.6 and later.
|
||||
V = 'V',
|
||||
localized_severity = 'V',
|
||||
|
||||
/// Code: the SQLSTATE code for the error (see Appendix A). Not localizable. Always present.
|
||||
C = 'C',
|
||||
code = 'C',
|
||||
|
||||
/// Message: the primary human-readable error message. This should be accurate but terse (typically one line). Always present.
|
||||
M = 'M',
|
||||
message = 'M',
|
||||
|
||||
/// Detail: an optional secondary error message carrying more detail about the problem. Might run to multiple lines.
|
||||
D = 'D',
|
||||
detail = 'D',
|
||||
|
||||
/// Hint: an optional suggestion what to do about the problem. This is intended to differ from Detail in that it offers advice (potentially inappropriate) rather than hard facts. Might run to multiple lines.
|
||||
H = 'H',
|
||||
hint = 'H',
|
||||
|
||||
/// Position: the field value is a decimal ASCII integer, indicating an error cursor position as an index into the original query string. The first character has index 1, and positions are measured in characters not bytes.
|
||||
P = 'P',
|
||||
position = 'P',
|
||||
|
||||
/// Internal position: this is defined the same as the P field, but it is used when the cursor position refers to an internally generated command rather than the one submitted by the client. The q field will always appear when this field appears.
|
||||
p = 'p',
|
||||
internal_position = 'p',
|
||||
|
||||
/// Internal query: the text of a failed internally-generated command. This could be, for example, an SQL query issued by a PL/pgSQL function.
|
||||
q = 'q',
|
||||
internal = 'q',
|
||||
|
||||
/// Where: an indication of the context in which the error occurred. Presently this includes a call stack traceback of active procedural language functions and internally-generated queries. The trace is one entry per line, most recent first.
|
||||
W = 'W',
|
||||
where = 'W',
|
||||
|
||||
/// Schema name: if the error was associated with a specific database object, the name of the schema containing that object, if any.
|
||||
s = 's',
|
||||
schema = 's',
|
||||
|
||||
/// Table name: if the error was associated with a specific table, the name of the table. (Refer to the schema name field for the name of the table's schema.)
|
||||
t = 't',
|
||||
table = 't',
|
||||
|
||||
/// Column name: if the error was associated with a specific table column, the name of the column. (Refer to the schema and table name fields to identify the table.)
|
||||
c = 'c',
|
||||
column = 'c',
|
||||
|
||||
/// Data type name: if the error was associated with a specific data type, the name of the data type. (Refer to the schema name field for the name of the data type's schema.)
|
||||
d = 'd',
|
||||
datatype = 'd',
|
||||
|
||||
/// Constraint name: if the error was associated with a specific constraint, the name of the constraint. Refer to fields listed above for the associated table or domain. (For this purpose, indexes are treated as constraints, even if they weren't created with constraint syntax.)
|
||||
n = 'n',
|
||||
constraint = 'n',
|
||||
|
||||
/// File: the file name of the source-code location where the error was reported.
|
||||
F = 'F',
|
||||
file = 'F',
|
||||
|
||||
/// Line: the line number of the source-code location where the error was reported.
|
||||
L = 'L',
|
||||
line = 'L',
|
||||
|
||||
/// Routine: the name of the source-code routine reporting the error.
|
||||
R = 'R',
|
||||
routine = 'R',
|
||||
|
||||
_,
|
||||
};
|
||||
|
||||
pub const FieldMessage = union(FieldType) {
|
||||
S: String,
|
||||
V: String,
|
||||
C: String,
|
||||
M: String,
|
||||
D: String,
|
||||
H: String,
|
||||
P: String,
|
||||
p: String,
|
||||
q: String,
|
||||
W: String,
|
||||
s: String,
|
||||
t: String,
|
||||
c: String,
|
||||
d: String,
|
||||
n: String,
|
||||
F: String,
|
||||
L: String,
|
||||
R: String,
|
||||
severity: String,
|
||||
localized_severity: String,
|
||||
code: String,
|
||||
message: String,
|
||||
detail: String,
|
||||
hint: String,
|
||||
position: String,
|
||||
internal_position: String,
|
||||
internal: String,
|
||||
where: String,
|
||||
schema: String,
|
||||
table: String,
|
||||
column: String,
|
||||
datatype: String,
|
||||
constraint: String,
|
||||
file: String,
|
||||
line: String,
|
||||
routine: String,
|
||||
|
||||
pub fn format(this: FieldMessage, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
switch (this) {
|
||||
@@ -319,24 +319,25 @@ pub const FieldMessage = union(FieldType) {
|
||||
|
||||
pub fn init(tag: FieldType, message: []const u8) !FieldMessage {
|
||||
return switch (tag) {
|
||||
.S => FieldMessage{ .S = String.createUTF8(message) },
|
||||
.V => FieldMessage{ .V = String.createUTF8(message) },
|
||||
.C => FieldMessage{ .C = String.createUTF8(message) },
|
||||
.M => FieldMessage{ .M = String.createUTF8(message) },
|
||||
.D => FieldMessage{ .D = String.createUTF8(message) },
|
||||
.H => FieldMessage{ .H = String.createUTF8(message) },
|
||||
.P => FieldMessage{ .P = String.createUTF8(message) },
|
||||
.p => FieldMessage{ .p = String.createUTF8(message) },
|
||||
.q => FieldMessage{ .q = String.createUTF8(message) },
|
||||
.W => FieldMessage{ .W = String.createUTF8(message) },
|
||||
.s => FieldMessage{ .s = String.createUTF8(message) },
|
||||
.t => FieldMessage{ .t = String.createUTF8(message) },
|
||||
.c => FieldMessage{ .c = String.createUTF8(message) },
|
||||
.d => FieldMessage{ .d = String.createUTF8(message) },
|
||||
.n => FieldMessage{ .n = String.createUTF8(message) },
|
||||
.F => FieldMessage{ .F = String.createUTF8(message) },
|
||||
.L => FieldMessage{ .L = String.createUTF8(message) },
|
||||
.R => FieldMessage{ .R = String.createUTF8(message) },
|
||||
.severity => FieldMessage{ .severity = String.createUTF8(message) },
|
||||
// Ignore this one for now.
|
||||
// .localized_severity => FieldMessage{ .localized_severity = String.createUTF8(message) },
|
||||
.code => FieldMessage{ .code = String.createUTF8(message) },
|
||||
.message => FieldMessage{ .message = String.createUTF8(message) },
|
||||
.detail => FieldMessage{ .detail = String.createUTF8(message) },
|
||||
.hint => FieldMessage{ .hint = String.createUTF8(message) },
|
||||
.position => FieldMessage{ .position = String.createUTF8(message) },
|
||||
.internal_position => FieldMessage{ .internal_position = String.createUTF8(message) },
|
||||
.internal => FieldMessage{ .internal = String.createUTF8(message) },
|
||||
.where => FieldMessage{ .where = String.createUTF8(message) },
|
||||
.schema => FieldMessage{ .schema = String.createUTF8(message) },
|
||||
.table => FieldMessage{ .table = String.createUTF8(message) },
|
||||
.column => FieldMessage{ .column = String.createUTF8(message) },
|
||||
.datatype => FieldMessage{ .datatype = String.createUTF8(message) },
|
||||
.constraint => FieldMessage{ .constraint = String.createUTF8(message) },
|
||||
.file => FieldMessage{ .file = String.createUTF8(message) },
|
||||
.line => FieldMessage{ .line = String.createUTF8(message) },
|
||||
.routine => FieldMessage{ .routine = String.createUTF8(message) },
|
||||
else => error.UnknownFieldType,
|
||||
};
|
||||
}
|
||||
@@ -348,8 +349,8 @@ pub fn NewReaderWrap(
|
||||
comptime peekFn_: (fn (ctx: Context) []const u8),
|
||||
comptime skipFn_: (fn (ctx: Context, count: usize) void),
|
||||
comptime ensureCapacityFn_: (fn (ctx: Context, count: usize) bool),
|
||||
comptime readFunction_: (fn (ctx: Context, count: usize) anyerror!Data),
|
||||
comptime readZ_: (fn (ctx: Context) anyerror!Data),
|
||||
comptime readFunction_: (fn (ctx: Context, count: usize) AnyPostgresError!Data),
|
||||
comptime readZ_: (fn (ctx: Context) AnyPostgresError!Data),
|
||||
) type {
|
||||
return struct {
|
||||
wrapped: Context,
|
||||
@@ -366,11 +367,11 @@ pub fn NewReaderWrap(
|
||||
markMessageStartFn(this.wrapped);
|
||||
}
|
||||
|
||||
pub inline fn read(this: @This(), count: usize) anyerror!Data {
|
||||
pub inline fn read(this: @This(), count: usize) AnyPostgresError!Data {
|
||||
return try readFn(this.wrapped, count);
|
||||
}
|
||||
|
||||
pub inline fn eatMessage(this: @This(), comptime msg_: anytype) anyerror!void {
|
||||
pub inline fn eatMessage(this: @This(), comptime msg_: anytype) AnyPostgresError!void {
|
||||
const msg = msg_[1..];
|
||||
try this.ensureCapacity(msg.len);
|
||||
|
||||
@@ -380,7 +381,7 @@ pub fn NewReaderWrap(
|
||||
return error.InvalidMessage;
|
||||
}
|
||||
|
||||
pub fn skip(this: @This(), count: usize) anyerror!void {
|
||||
pub fn skip(this: @This(), count: usize) AnyPostgresError!void {
|
||||
skipFn(this.wrapped, count);
|
||||
}
|
||||
|
||||
@@ -388,11 +389,11 @@ pub fn NewReaderWrap(
|
||||
return peekFn(this.wrapped);
|
||||
}
|
||||
|
||||
pub inline fn readZ(this: @This()) anyerror!Data {
|
||||
pub inline fn readZ(this: @This()) AnyPostgresError!Data {
|
||||
return try readZFn(this.wrapped);
|
||||
}
|
||||
|
||||
pub inline fn ensureCapacity(this: @This(), count: usize) anyerror!void {
|
||||
pub inline fn ensureCapacity(this: @This(), count: usize) AnyPostgresError!void {
|
||||
if (!ensureCapacityFn(this.wrapped, count)) {
|
||||
return error.ShortRead;
|
||||
}
|
||||
@@ -457,7 +458,7 @@ pub fn NewWriter(comptime Context: type) type {
|
||||
|
||||
fn decoderWrap(comptime Container: type, comptime decodeFn: anytype) type {
|
||||
return struct {
|
||||
pub fn decode(this: *Container, context: anytype) anyerror!void {
|
||||
pub fn decode(this: *Container, context: anytype) AnyPostgresError!void {
|
||||
const Context = @TypeOf(context);
|
||||
try decodeFn(this, Context, NewReader(Context){ .wrapped = context });
|
||||
}
|
||||
@@ -466,7 +467,7 @@ fn decoderWrap(comptime Container: type, comptime decodeFn: anytype) type {
|
||||
|
||||
fn writeWrap(comptime Container: type, comptime writeFn: anytype) type {
|
||||
return struct {
|
||||
pub fn write(this: *Container, context: anytype) anyerror!void {
|
||||
pub fn write(this: *Container, context: anytype) AnyPostgresError!void {
|
||||
const Context = @TypeOf(context);
|
||||
try writeFn(this, Context, NewWriter(Context){ .wrapped = context });
|
||||
}
|
||||
@@ -538,9 +539,6 @@ pub const Authentication = union(enum) {
|
||||
},
|
||||
5 => {
|
||||
if (message_length != 12) return error.InvalidMessageLength;
|
||||
if (!try reader.expectInt(u32, 5)) {
|
||||
return error.InvalidMessage;
|
||||
}
|
||||
var salt_data = try reader.bytes(4);
|
||||
defer salt_data.deinit();
|
||||
this.* = .{
|
||||
@@ -722,23 +720,117 @@ pub const ErrorResponse = struct {
|
||||
var b = bun.StringBuilder{};
|
||||
defer b.deinit(bun.default_allocator);
|
||||
|
||||
for (this.messages.items) |msg| {
|
||||
b.cap += switch (msg) {
|
||||
// Pre-calculate capacity to avoid reallocations
|
||||
for (this.messages.items) |*msg| {
|
||||
b.cap += switch (msg.*) {
|
||||
inline else => |m| m.utf8ByteLength(),
|
||||
} + 1;
|
||||
}
|
||||
b.allocate(bun.default_allocator) catch {};
|
||||
|
||||
for (this.messages.items) |msg| {
|
||||
var str = switch (msg) {
|
||||
inline else => |m| m.toUTF8(bun.default_allocator),
|
||||
};
|
||||
defer str.deinit();
|
||||
_ = b.append(str.slice());
|
||||
_ = b.append("\n");
|
||||
// Build a more structured error message
|
||||
var severity: String = String.dead;
|
||||
var code: String = String.dead;
|
||||
var message: String = String.dead;
|
||||
var detail: String = String.dead;
|
||||
var hint: String = String.dead;
|
||||
var position: String = String.dead;
|
||||
var where: String = String.dead;
|
||||
var schema: String = String.dead;
|
||||
var table: String = String.dead;
|
||||
var column: String = String.dead;
|
||||
var datatype: String = String.dead;
|
||||
var constraint: String = String.dead;
|
||||
var file: String = String.dead;
|
||||
var line: String = String.dead;
|
||||
var routine: String = String.dead;
|
||||
|
||||
for (this.messages.items) |*msg| {
|
||||
switch (msg.*) {
|
||||
.severity => |str| severity = str,
|
||||
.code => |str| code = str,
|
||||
.message => |str| message = str,
|
||||
.detail => |str| detail = str,
|
||||
.hint => |str| hint = str,
|
||||
.position => |str| position = str,
|
||||
.where => |str| where = str,
|
||||
.schema => |str| schema = str,
|
||||
.table => |str| table = str,
|
||||
.column => |str| column = str,
|
||||
.datatype => |str| datatype = str,
|
||||
.constraint => |str| constraint = str,
|
||||
.file => |str| file = str,
|
||||
.line => |str| line = str,
|
||||
.routine => |str| routine = str,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
return globalObject.createSyntaxErrorInstance("Postgres error occurred\n{s}", .{b.allocatedSlice()[0..b.len]});
|
||||
var needs_newline = false;
|
||||
construct_message: {
|
||||
if (!message.isEmpty()) {
|
||||
_ = b.appendStr(message);
|
||||
needs_newline = true;
|
||||
break :construct_message;
|
||||
}
|
||||
if (!detail.isEmpty()) {
|
||||
if (needs_newline) {
|
||||
_ = b.append("\n");
|
||||
} else {
|
||||
_ = b.append(" ");
|
||||
}
|
||||
needs_newline = true;
|
||||
_ = b.appendStr(detail);
|
||||
}
|
||||
if (!hint.isEmpty()) {
|
||||
if (needs_newline) {
|
||||
_ = b.append("\n");
|
||||
} else {
|
||||
_ = b.append(" ");
|
||||
}
|
||||
needs_newline = true;
|
||||
_ = b.appendStr(hint);
|
||||
}
|
||||
}
|
||||
|
||||
const possible_fields = .{
|
||||
.{ "detail", detail, void },
|
||||
.{ "hint", hint, void },
|
||||
.{ "column", column, void },
|
||||
.{ "constraint", constraint, void },
|
||||
.{ "datatype", datatype, void },
|
||||
.{ "errno", code, i32 },
|
||||
.{ "position", position, i32 },
|
||||
.{ "schema", schema, void },
|
||||
.{ "table", table, void },
|
||||
.{ "where", where, void },
|
||||
};
|
||||
|
||||
const error_code: JSC.Error =
|
||||
// https://www.postgresql.org/docs/8.1/errcodes-appendix.html
|
||||
if (code.toInt32() orelse 0 == 42601)
|
||||
JSC.Error.ERR_POSTGRES_SYNTAX_ERROR
|
||||
else
|
||||
JSC.Error.ERR_POSTGRES_SERVER_ERROR;
|
||||
const err = error_code.fmt(globalObject, "{s}", .{b.allocatedSlice()[0..b.len]});
|
||||
|
||||
inline for (possible_fields) |field| {
|
||||
if (!field.@"1".isEmpty()) {
|
||||
const value = brk: {
|
||||
if (field.@"2" == i32) {
|
||||
if (field.@"1".toInt32()) |val| {
|
||||
break :brk JSC.JSValue.jsNumberFromInt32(val);
|
||||
}
|
||||
}
|
||||
|
||||
break :brk field.@"1".toJS(globalObject);
|
||||
};
|
||||
|
||||
err.put(globalObject, JSC.ZigString.static(field.@"0"), value);
|
||||
}
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -847,7 +939,7 @@ pub const FormatCode = enum {
|
||||
pub const null_int4 = 4294967295;
|
||||
|
||||
pub const DataRow = struct {
|
||||
pub fn decode(context: anytype, comptime ContextType: type, reader: NewReader(ContextType), comptime forEach: fn (@TypeOf(context), index: u32, bytes: ?*Data) anyerror!bool) anyerror!void {
|
||||
pub fn decode(context: anytype, comptime ContextType: type, reader: NewReader(ContextType), comptime forEach: fn (@TypeOf(context), index: u32, bytes: ?*Data) AnyPostgresError!bool) AnyPostgresError!void {
|
||||
var remaining_bytes = try reader.length();
|
||||
remaining_bytes -|= 4;
|
||||
|
||||
@@ -885,7 +977,7 @@ pub const FieldDescription = struct {
|
||||
this.name.deinit();
|
||||
}
|
||||
|
||||
pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) !void {
|
||||
pub fn decodeInternal(this: *@This(), comptime Container: type, reader: NewReader(Container)) AnyPostgresError!void {
|
||||
var name = try reader.readZ();
|
||||
errdefer {
|
||||
name.deinit();
|
||||
@@ -1355,6 +1447,29 @@ pub const NoticeResponse = struct {
|
||||
}
|
||||
}
|
||||
pub const decode = decoderWrap(NoticeResponse, decodeInternal).decode;
|
||||
|
||||
pub fn toJS(this: NoticeResponse, globalObject: *JSC.JSGlobalObject) JSValue {
|
||||
var b = bun.StringBuilder{};
|
||||
defer b.deinit(bun.default_allocator);
|
||||
|
||||
for (this.messages.items) |msg| {
|
||||
b.cap += switch (msg) {
|
||||
inline else => |m| m.utf8ByteLength(),
|
||||
} + 1;
|
||||
}
|
||||
b.allocate(bun.default_allocator) catch {};
|
||||
|
||||
for (this.messages.items) |msg| {
|
||||
var str = switch (msg) {
|
||||
inline else => |m| m.toUTF8(bun.default_allocator),
|
||||
};
|
||||
defer str.deinit();
|
||||
_ = b.append(str.slice());
|
||||
_ = b.append("\n");
|
||||
}
|
||||
|
||||
return JSC.ZigString.init(b.allocatedSlice()[0..b.len]).toJS(globalObject);
|
||||
}
|
||||
};
|
||||
|
||||
pub const CopyFail = struct {
|
||||
|
||||
@@ -12,6 +12,7 @@ const JSValue = JSC.JSValue;
|
||||
const JSC = bun.JSC;
|
||||
const short = postgres.short;
|
||||
const int4 = postgres.int4;
|
||||
const AnyPostgresError = postgres.AnyPostgresError;
|
||||
|
||||
// select b.typname, b.oid, b.typarray
|
||||
// from pg_catalog.pg_type a
|
||||
@@ -169,8 +170,17 @@ pub const Tag = enum(short) {
|
||||
bit_array = 1561,
|
||||
varbit_array = 1563,
|
||||
numeric_array = 1231,
|
||||
jsonb = 3802,
|
||||
jsonb_array = 3807,
|
||||
// Not really sure what this is.
|
||||
jsonpath = 4072,
|
||||
jsonpath_array = 4073,
|
||||
_,
|
||||
|
||||
pub fn name(this: Tag) ?[]const u8 {
|
||||
return std.enums.tagName(Tag, this);
|
||||
}
|
||||
|
||||
pub fn isBinaryFormatSupported(this: Tag) bool {
|
||||
return switch (this) {
|
||||
// TODO: .int2_array, .float8_array,
|
||||
@@ -282,7 +292,7 @@ pub const Tag = enum(short) {
|
||||
globalObject: *JSC.JSGlobalObject,
|
||||
comptime Type: type,
|
||||
value: Type,
|
||||
) anyerror!JSValue {
|
||||
) AnyPostgresError!JSValue {
|
||||
switch (tag) {
|
||||
.numeric => {
|
||||
return numeric.toJS(globalObject, value);
|
||||
@@ -292,7 +302,7 @@ pub const Tag = enum(short) {
|
||||
return numeric.toJS(globalObject, value);
|
||||
},
|
||||
|
||||
.json => {
|
||||
.json, .jsonb => {
|
||||
return json.toJS(globalObject, value);
|
||||
},
|
||||
|
||||
@@ -326,7 +336,7 @@ pub const Tag = enum(short) {
|
||||
tag: Tag,
|
||||
globalObject: *JSC.JSGlobalObject,
|
||||
value: anytype,
|
||||
) anyerror!JSValue {
|
||||
) AnyPostgresError!JSValue {
|
||||
return toJSWithType(tag, globalObject, @TypeOf(value), value);
|
||||
}
|
||||
|
||||
@@ -363,16 +373,16 @@ pub const Tag = enum(short) {
|
||||
|
||||
// Ban these types:
|
||||
if (tag == .NumberObject) {
|
||||
return error.JSError;
|
||||
return globalObject.ERR_INVALID_ARG_TYPE("Number object is ambiguous and cannot be used as a PostgreSQL type", .{}).throw();
|
||||
}
|
||||
|
||||
if (tag == .BooleanObject) {
|
||||
return error.JSError;
|
||||
return globalObject.ERR_INVALID_ARG_TYPE("Boolean object is ambiguous and cannot be used as a PostgreSQL type", .{}).throw();
|
||||
}
|
||||
|
||||
// It's something internal
|
||||
if (!tag.isIndexable()) {
|
||||
return error.JSError;
|
||||
return globalObject.ERR_INVALID_ARG_TYPE("Unknown object is not a valid PostgreSQL type", .{}).throw();
|
||||
}
|
||||
|
||||
// We will JSON.stringify anything else.
|
||||
@@ -414,7 +424,7 @@ pub const string = struct {
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
comptime Type: type,
|
||||
value: Type,
|
||||
) anyerror!JSValue {
|
||||
) AnyPostgresError!JSValue {
|
||||
switch (comptime Type) {
|
||||
[:0]u8, []u8, []const u8, [:0]const u8 => {
|
||||
var str = String.fromUTF8(value);
|
||||
@@ -456,7 +466,7 @@ pub const numeric = struct {
|
||||
pub fn toJS(
|
||||
_: *JSC.JSGlobalObject,
|
||||
value: anytype,
|
||||
) anyerror!JSValue {
|
||||
) AnyPostgresError!JSValue {
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
};
|
||||
@@ -468,12 +478,12 @@ pub const json = struct {
|
||||
pub fn toJS(
|
||||
globalObject: *JSC.JSGlobalObject,
|
||||
value: *Data,
|
||||
) anyerror!JSValue {
|
||||
) AnyPostgresError!JSValue {
|
||||
defer value.deinit();
|
||||
var str = bun.String.fromUTF8(value.slice());
|
||||
defer str.deref();
|
||||
const parse_result = JSValue.parse(str.toJS(globalObject), globalObject);
|
||||
if (parse_result.isAnyError()) {
|
||||
if (parse_result.AnyPostgresError()) {
|
||||
return globalObject.throwValue(parse_result);
|
||||
}
|
||||
|
||||
@@ -488,7 +498,7 @@ pub const @"bool" = struct {
|
||||
pub fn toJS(
|
||||
_: *JSC.JSGlobalObject,
|
||||
value: bool,
|
||||
) anyerror!JSValue {
|
||||
) AnyPostgresError!JSValue {
|
||||
return JSValue.jsBoolean(value);
|
||||
}
|
||||
};
|
||||
@@ -548,7 +558,7 @@ pub const bytea = struct {
|
||||
pub fn toJS(
|
||||
globalObject: *JSC.JSGlobalObject,
|
||||
value: *Data,
|
||||
) anyerror!JSValue {
|
||||
) AnyPostgresError!JSValue {
|
||||
defer value.deinit();
|
||||
|
||||
// var slice = value.slice()[@min(1, value.len)..];
|
||||
|
||||
@@ -323,6 +323,12 @@ pub const String = extern struct {
|
||||
extern fn BunString__fromUTF16ToLatin1(bytes: [*]const u16, len: usize) String;
|
||||
extern fn BunString__fromLatin1Unitialized(len: usize) String;
|
||||
extern fn BunString__fromUTF16Unitialized(len: usize) String;
|
||||
extern fn BunString__toInt32(this: String) i64;
|
||||
pub fn toInt32(this: String) ?i32 {
|
||||
const val = BunString__toInt32(this);
|
||||
if (val > std.math.maxInt(i32)) return null;
|
||||
return @intCast(val);
|
||||
}
|
||||
|
||||
pub fn ascii(bytes: []const u8) String {
|
||||
return String{ .tag = .ZigString, .value = .{ .ZigString = ZigString.init(bytes) } };
|
||||
|
||||
@@ -89,6 +89,12 @@ pub fn appendZ(this: *StringBuilder, slice: string) [:0]const u8 {
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn appendStr(this: *StringBuilder, str: bun.String) string {
|
||||
const slice = str.toUTF8(bun.default_allocator);
|
||||
defer slice.deinit();
|
||||
return this.append(slice.slice());
|
||||
}
|
||||
|
||||
pub fn append(this: *StringBuilder, slice: string) string {
|
||||
if (comptime Environment.allow_assert) {
|
||||
assert(this.len <= this.cap); // didn't count everything
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { postgres, sql } from "bun:sql";
|
||||
import { expect, test } from "bun:test";
|
||||
import { expect, test, mock } from "bun:test";
|
||||
import { $ } from "bun";
|
||||
import { bunExe, isCI, withoutAggressiveGC } from "harness";
|
||||
import path from "path";
|
||||
@@ -13,18 +13,20 @@ if (!isCI) {
|
||||
// local all postgres trust
|
||||
// local all bun_sql_test_scram scram-sha-256
|
||||
// local all bun_sql_test trust
|
||||
//
|
||||
// local all bun_sql_test_md5 md5
|
||||
|
||||
// # IPv4 local connections:
|
||||
// host all ${USERNAME} 127.0.0.1/32 trust
|
||||
// host all postgres 127.0.0.1/32 trust
|
||||
// host all bun_sql_test_scram 127.0.0.1/32 scram-sha-256
|
||||
// host all bun_sql_test 127.0.0.1/32 trust
|
||||
// host all bun_sql_test_md5 127.0.0.1/32 md5
|
||||
// # IPv6 local connections:
|
||||
// host all ${USERNAME} ::1/128 trust
|
||||
// host all postgres ::1/128 trust
|
||||
// host all bun_sql_test ::1/128 trust
|
||||
// host all bun_sql_test_scram ::1/128 scram-sha-256
|
||||
//
|
||||
// host all bun_sql_test_md5 ::1/128 md5
|
||||
// # Allow replication connections from localhost, by a user with the
|
||||
// # replication privilege.
|
||||
// local replication all trust
|
||||
@@ -33,9 +35,6 @@ if (!isCI) {
|
||||
// --- Expected pg_hba.conf ---
|
||||
process.env.DATABASE_URL = "postgres://bun_sql_test@localhost:5432/bun_sql_test";
|
||||
|
||||
const delay = ms => Bun.sleep(ms);
|
||||
const rel = x => new URL(x, import.meta.url);
|
||||
|
||||
const login = {
|
||||
username: "bun_sql_test",
|
||||
};
|
||||
@@ -54,8 +53,8 @@ if (!isCI) {
|
||||
db: "bun_sql_test",
|
||||
username: login.username,
|
||||
password: login.password,
|
||||
idle_timeout: 1,
|
||||
connect_timeout: 1,
|
||||
idle_timeout: 0,
|
||||
connect_timeout: 0,
|
||||
max: 1,
|
||||
};
|
||||
|
||||
@@ -67,6 +66,97 @@ if (!isCI) {
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
test("Connection timeout works", async () => {
|
||||
const onclose = mock();
|
||||
const onconnect = mock();
|
||||
await using sql = postgres({
|
||||
...options,
|
||||
hostname: "unreachable_host",
|
||||
connection_timeout: 1,
|
||||
onconnect,
|
||||
onclose,
|
||||
});
|
||||
let error: any;
|
||||
try {
|
||||
await sql`select pg_sleep(2)`;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error.code).toBe(`ERR_POSTGRES_CONNECTION_TIMEOUT`);
|
||||
expect(error.message).toContain("Connection timeout after 1ms");
|
||||
expect(onconnect).not.toHaveBeenCalled();
|
||||
expect(onclose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("Idle timeout works at start", async () => {
|
||||
const onclose = mock();
|
||||
const onconnect = mock();
|
||||
await using sql = postgres({
|
||||
...options,
|
||||
idle_timeout: 1,
|
||||
onconnect,
|
||||
onclose,
|
||||
});
|
||||
let error: any;
|
||||
try {
|
||||
await sql`select pg_sleep(2)`;
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
expect(error.code).toBe(`ERR_POSTGRES_IDLE_TIMEOUT`);
|
||||
expect(onconnect).toHaveBeenCalled();
|
||||
expect(onclose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("Idle timeout is reset when a query is run", async () => {
|
||||
const onClosePromise = Promise.withResolvers();
|
||||
const onclose = mock(err => {
|
||||
onClosePromise.resolve(err);
|
||||
});
|
||||
const onconnect = mock();
|
||||
await using sql = postgres({
|
||||
...options,
|
||||
idle_timeout: 100,
|
||||
onconnect,
|
||||
onclose,
|
||||
});
|
||||
expect(await sql`select 123 as x`).toEqual([{ x: 123 }]);
|
||||
expect(onconnect).toHaveBeenCalledTimes(1);
|
||||
expect(onclose).not.toHaveBeenCalled();
|
||||
const err = await onClosePromise.promise;
|
||||
expect(err.code).toBe(`ERR_POSTGRES_IDLE_TIMEOUT`);
|
||||
});
|
||||
|
||||
test("Max lifetime works", async () => {
|
||||
const onClosePromise = Promise.withResolvers();
|
||||
const onclose = mock(err => {
|
||||
onClosePromise.resolve(err);
|
||||
});
|
||||
const onconnect = mock();
|
||||
const sql = postgres({
|
||||
...options,
|
||||
max_lifetime: 64,
|
||||
onconnect,
|
||||
onclose,
|
||||
});
|
||||
let error: any;
|
||||
expect(await sql`select 1 as x`).toEqual([{ x: 1 }]);
|
||||
expect(onconnect).toHaveBeenCalledTimes(1);
|
||||
try {
|
||||
while (true) {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await sql`select pg_sleep(1)`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(onclose).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(error.code).toBe(`ERR_POSTGRES_LIFETIME_TIMEOUT`);
|
||||
});
|
||||
|
||||
test("Uses default database without slash", async () => {
|
||||
const sql = postgres("postgres://localhost");
|
||||
expect(sql.options.username).toBe(sql.options.database);
|
||||
@@ -145,10 +235,9 @@ if (!isCI) {
|
||||
expect(x).toEqual({ a: "hello", b: 42 });
|
||||
});
|
||||
|
||||
// It's treating as a string.
|
||||
test.todo("implicit jsonb", async () => {
|
||||
test("implicit jsonb", async () => {
|
||||
const x = (await sql`select ${{ a: "hello", b: 42 }}::jsonb as x`)[0].x;
|
||||
expect([x.a, x.b].join(",")).toBe("hello,42");
|
||||
expect(x).toEqual({ a: "hello", b: 42 });
|
||||
});
|
||||
|
||||
test("bulk insert nested sql()", async () => {
|
||||
@@ -428,9 +517,11 @@ if (!isCI) {
|
||||
test("Null sets to null", async () => expect((await sql`select ${null} as x`)[0].x).toBeNull());
|
||||
|
||||
// Add code property.
|
||||
test.todo("Throw syntax error", async () => {
|
||||
const code = await sql`wat 1`.catch(x => x);
|
||||
console.log({ code });
|
||||
test("Throw syntax error", async () => {
|
||||
const err = await sql`wat 1`.catch(x => x);
|
||||
expect(err.code).toBe("ERR_POSTGRES_SYNTAX_ERROR");
|
||||
expect(err.errno).toBe(42601);
|
||||
expect(err).toBeInstanceOf(SyntaxError);
|
||||
});
|
||||
|
||||
// t('Connect using uri', async() =>
|
||||
@@ -502,13 +593,26 @@ if (!isCI) {
|
||||
// return [1, (await sql`select 1 as x`)[0].x]
|
||||
// })
|
||||
|
||||
// t('Login without password', async() => {
|
||||
// return [true, (await postgres({ ...options, ...login })`select true as x`)[0].x]
|
||||
// })
|
||||
test("Login without password", async () => {
|
||||
await using sql = postgres({ ...options, ...login });
|
||||
expect((await sql`select true as x`)[0].x).toBe(true);
|
||||
});
|
||||
|
||||
// t('Login using MD5', async() => {
|
||||
// return [true, (await postgres({ ...options, ...login_md5 })`select true as x`)[0].x]
|
||||
// })
|
||||
test("Login using MD5", async () => {
|
||||
await using sql = postgres({ ...options, ...login_md5 });
|
||||
expect(await sql`select true as x`).toEqual([{ x: true }]);
|
||||
});
|
||||
|
||||
test("Login with bad credentials propagates error from server", async () => {
|
||||
const sql = postgres({ ...options, ...login_md5, username: "bad_user", password: "bad_password" });
|
||||
let err;
|
||||
try {
|
||||
await sql`select true as x`;
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
expect(err.code).toBe("ERR_POSTGRES_SERVER_ERROR");
|
||||
});
|
||||
|
||||
test("Login using scram-sha-256", async () => {
|
||||
await using sql = postgres({ ...options, ...login_scram });
|
||||
@@ -1159,9 +1263,10 @@ if (!isCI) {
|
||||
// ]
|
||||
// })
|
||||
|
||||
// t('dynamic column name', async() => {
|
||||
// return ['!not_valid', Object.keys((await sql`select 1 as ${ sql('!not_valid') }`)[0])[0]]
|
||||
// })
|
||||
test.todo("dynamic column name", async () => {
|
||||
const result = await sql`select 1 as ${"\\!not_valid"}`;
|
||||
expect(Object.keys(result[0])[0]).toBe("!not_valid");
|
||||
});
|
||||
|
||||
// t('dynamic select as', async() => {
|
||||
// return ['2', (await sql`select ${ sql({ a: 1, b: 2 }) }`)[0].b]
|
||||
@@ -1178,12 +1283,12 @@ if (!isCI) {
|
||||
// return ['the answer', (await sql`insert into test ${ sql(x) } returning *`)[0].b, await sql`drop table test`]
|
||||
// })
|
||||
|
||||
// t('dynamic insert pluck', async() => {
|
||||
// await sql`create table test (a int, b text)`
|
||||
// const x = { a: 42, b: 'the answer' }
|
||||
|
||||
// return [null, (await sql`insert into test ${ sql(x, 'a') } returning *`)[0].b, await sql`drop table test`]
|
||||
// })
|
||||
// test.todo("dynamic insert pluck", async () => {
|
||||
// await sql`create table test (a int, b text)`;
|
||||
// const x = { a: 42, b: "the answer" };
|
||||
// const [{ b }] = await sql`insert into test ${sql(x, "a")} returning *`;
|
||||
// expect(b).toBe("the answer");
|
||||
// });
|
||||
|
||||
// t('dynamic in with empty array', async() => {
|
||||
// await sql`create table test (a int)`
|
||||
|
||||
Reference in New Issue
Block a user