Compare commits

...

27 Commits

Author SHA1 Message Date
Jarred Sumner
7f569d450c Update bun.zig 2024-12-02 07:42:01 -08:00
Jarred Sumner
220c23d85c Merge branch 'main' into jarred/mysql 2024-12-02 07:40:34 -08:00
Jarred Sumner
ba1d471eaf Silence debugger connection error 2024-11-25 19:02:32 -08:00
Jarred Sumner
9639615a78 Update .gitignore 2024-11-25 19:02:09 -08:00
Jarred Sumner
a35a61c082 Implement connect attributes 2024-11-25 12:57:57 -08:00
Jarred Sumner
739c25320a Update mysql.zig 2024-11-25 12:07:51 -08:00
Jarred Sumner
7ed2266622 Various tweaks 2024-11-25 12:04:48 -08:00
Jarred Sumner
22c6241aaf more 2024-11-25 08:10:19 -08:00
Jarred Sumner
0e239f4497 more 2024-11-25 07:48:25 -08:00
Jarred Sumner
8f52c02ae6 hmm 2024-11-25 07:21:04 -08:00
Jarred Sumner
1cb2a9601d more 2024-11-25 07:02:44 -08:00
Jarred Sumner
bc559e69c3 wip 2024-11-25 06:36:14 -08:00
Jarred Sumner
181d349825 Update sql.ts 2024-11-25 06:01:17 -08:00
Jarred Sumner
99ed017f48 it compiled 2024-11-25 05:49:35 -08:00
Jarred Sumner
62e451caea Merge branch 'main' into jarred/mysql 2024-11-25 04:59:36 -08:00
Jarred Sumner
31d091557e Merge branch 'main' into jarred/mysql 2024-11-25 04:44:20 -08:00
Jarred Sumner
bdd0fe44e7 more 2024-11-25 04:38:33 -08:00
Jarred Sumner
4b82bf43eb more 2024-11-25 04:28:35 -08:00
Jarred Sumner
1debc84195 more 2024-11-25 04:04:28 -08:00
Jarred Sumner
7bbaa9f3df more 2024-11-25 03:23:27 -08:00
Jarred Sumner
42a63a128a more 2024-11-25 02:55:52 -08:00
Jarred Sumner
d1f3ceebdb so much code 2024-11-25 02:12:57 -08:00
Jarred Sumner
028e98e18d lots of code 2024-11-25 02:12:44 -08:00
Jarred Sumner
b1f8cd623f Merge branch 'main' into jarred/mysql 2024-11-25 00:26:38 -08:00
Jarred Sumner
28a7b8da76 wip 2024-11-20 01:42:32 -08:00
Jarred Sumner
d1159c858d more 2024-11-19 23:55:30 -08:00
Jarred Sumner
f27c2942d7 intiial 2024-11-19 22:07:44 -08:00
16 changed files with 5371 additions and 369 deletions

3
.gitignore vendored
View File

@@ -176,4 +176,5 @@ test/js/third_party/prisma/prisma/sqlite/dev.db-journal
.buildkite/ci.yml
*.sock
scratch*.{js,ts,tsx,cjs,mjs}
scratch*
scratch*.{js,ts,tsx,cjs,mjs}

View File

@@ -0,0 +1,65 @@
import { define } from "../../codegen/class-definitions";
export default [
define({
name: "MySQLConnection",
construct: true,
finalize: true,
hasPendingActivity: true,
configurable: false,
klass: {
// escapeString: {
// fn: "escapeString",
// },
// escapeIdentifier: {
// fn: "escapeIdentifier",
// },
},
JSType: "0b11101110",
proto: {
close: {
fn: "doClose",
},
flush: {
fn: "doFlush",
},
connected: {
getter: "getConnected",
},
ref: {
fn: "doRef",
},
unref: {
fn: "doUnref",
},
query: {
fn: "createQuery",
},
},
}),
define({
name: "MySQLQuery",
construct: true,
finalize: true,
configurable: false,
hasPendingActivity: true,
JSType: "0b11101110",
klass: {},
proto: {
run: {
fn: "doRun",
length: 2,
},
cancel: {
fn: "doCancel",
length: 0,
},
done: {
fn: "doDone",
length: 0,
},
},
values: ["pendingValue", "columns", "binding"],
estimatedSize: true,
}),
];

View File

@@ -246,6 +246,45 @@ static JSC::JSValue toJS(JSC::Structure* structure, DataCell* cells, unsigned co
return object;
}
extern "C" EncodedJSValue MySQL__ValueToJS(JSC::JSGlobalObject* globalObject, void* value);
static JSC::JSValue toJS(JSC::VM& vm, JSC::JSGlobalObject* globalObject, void** values, JSC::Structure* structure, unsigned count)
{
auto* object = JSC::constructEmptyObject(vm, structure);
auto scope = DECLARE_THROW_SCOPE(vm);
for (unsigned i = 0; i < count; i++) {
auto* value = values[i];
EncodedJSValue result = MySQL__ValueToJS(globalObject, value);
RETURN_IF_EXCEPTION(scope, {});
object->putDirectOffset(vm, i, JSValue::decode(result));
}
return object;
}
extern "C" EncodedJSValue MySQL__toJSFromRow(JSC::JSGlobalObject* globalObject, EncodedJSValue encodedStructureValue, EncodedJSValue arrayValue, void** rows, size_t cellCount)
{
JSC::VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);
auto* array = arrayValue ? jsDynamicCast<JSC::JSArray*>(JSValue::decode(arrayValue)) : nullptr;
auto* structure = jsDynamicCast<JSC::Structure*>(JSValue::decode(encodedStructureValue));
JSValue result = toJS(vm, globalObject, rows, structure, cellCount);
RETURN_IF_EXCEPTION(scope, {});
if (array) {
array->push(globalObject, result);
return JSValue::encode(array);
}
auto* newArray = JSC::constructEmptyArray(globalObject, static_cast<ArrayAllocationProfile*>(nullptr), cellCount);
if (!newArray)
return {};
newArray->putDirectIndex(globalObject, 0, result);
return JSValue::encode(newArray);
}
static JSC::JSValue toJS(JSC::JSArray* array, JSC::Structure* structure, DataCell* cells, unsigned count, JSC::JSGlobalObject* globalObject)
{
JSValue value = toJS(structure, cells, count, globalObject);

View File

@@ -4086,6 +4086,18 @@ pub const JSValue = enum(i64) {
return cppFn("coerceToDouble", .{ this, globalObject });
}
pub fn coerceToDoubleCheckingErrors(
this: JSValue,
globalObject: *JSC.JSGlobalObject,
) JSError!f64 {
const num = this.coerceToDouble(globalObject);
if (globalObject.hasException()) {
return error.JSError;
}
return num;
}
pub fn coerce(this: JSValue, comptime T: type, globalThis: *JSC.JSGlobalObject) T {
return switch (T) {
ZigString => this.getZigString(globalThis),

View File

@@ -73,6 +73,8 @@ pub const Classes = struct {
pub const BytesInternalReadableStreamSource = JSC.WebCore.ByteStream.Source;
pub const PostgresSQLConnection = JSC.Postgres.PostgresSQLConnection;
pub const PostgresSQLQuery = JSC.Postgres.PostgresSQLQuery;
pub const MySQLConnection = bun.sql.mysql.MySQLConnection;
pub const MySQLQuery = bun.sql.mysql.MySQLQuery;
pub const TextEncoderStreamEncoder = JSC.WebCore.TextEncoderStreamEncoder;
pub const NativeZlib = JSC.API.NativeZlib;
pub const NativeBrotli = JSC.API.NativeBrotli;

View File

@@ -24,6 +24,7 @@ stdin_store: ?*Blob.Store = null,
stdout_store: ?*Blob.Store = null,
postgresql_context: JSC.Postgres.PostgresSQLContext = .{},
mysql_context: JSC.MySQL.MySQLContext = .{},
entropy_cache: ?*EntropyCache = null,

View File

@@ -4118,3 +4118,8 @@ pub inline fn isComptimeKnown(x: anytype) bool {
pub inline fn itemOrNull(comptime T: type, slice: []const T, index: usize) ?T {
return if (index < slice.len) slice[index] else null;
}
/// Code used by the classes generator
pub const gen_classes_lib = @import("gen_classes_lib.zig");
pub const sql = @import("./sql/shared_sql.zig");

View File

@@ -340,19 +340,23 @@ pub const Run = struct {
Output.prettyErrorln("Error occurred loading entry point: {s}", .{@errorName(err)});
Output.flush();
}
// TODO: Do a event loop tick when we figure out how to watch the file that wasn't found
// under hot reload mode
vm.exit_handler.exit_code = 1;
vm.onExit();
if (run.any_unhandled) {
bun.JSC.SavedSourceMap.MissingSourceMapNoteInfo.print();
Output.prettyErrorln(
"<r>\n<d>{s}<r>",
.{Global.unhandled_error_bun_version_string},
);
if (vm.hot_reload != .none) {
vm.eventLoop().tick();
vm.eventLoop().tickPossiblyForever();
} else {
vm.exit_handler.exit_code = 1;
vm.onExit();
if (run.any_unhandled) {
bun.JSC.SavedSourceMap.MissingSourceMapNoteInfo.print();
Output.prettyErrorln(
"<r>\n<d>{s}<r>",
.{Global.unhandled_error_bun_version_string},
);
}
vm.globalExit();
}
vm.globalExit();
}
// don't run the GC if we don't actually need to

View File

@@ -34,13 +34,6 @@ const _queryStatus = Symbol("status");
const _handler = Symbol("handler");
const PublicPromise = Promise;
const {
createConnection: _createConnection,
createQuery,
PostgresSQLConnection,
init,
} = $zig("postgres.zig", "createBinding");
function normalizeSSLMode(value: string): SSLMode {
if (!value) {
return SSLMode.disable;
@@ -181,101 +174,256 @@ class Query extends PublicPromise {
}
Object.defineProperty(Query, Symbol.species, { value: PublicPromise });
Object.defineProperty(Query, Symbol.toStringTag, { value: "Query" });
init(
function (query, result, commandTag, count) {
$assert(result instanceof SQLResultArray, "Invalid result array");
if (typeof commandTag === "string") {
if (commandTag.length > 0) {
result.command = commandTag;
}
} else {
result.command = cmds[commandTag];
}
result.count = count || 0;
try {
query.resolve(result);
} catch (e) {}
},
function (query, reject) {
try {
query.reject(reject);
} catch (e) {}
},
);
function createConnection({ hostname, port, username, password, tls, query, database, sslMode }, onConnected, onClose) {
return _createConnection(
hostname,
Number(port),
username || "",
password || "",
database || "",
// > The default value for sslmode is prefer. As is shown in the table, this
// makes no sense from a security point of view, and it only promises
// performance overhead if possible. It is only provided as the default for
// backward compatibility, and is not recommended in secure deployments.
sslMode || SSLMode.disable,
tls || null,
query || "",
onConnected,
onClose,
);
}
var hasSQLArrayParameter = false;
function normalizeStrings(strings, values) {
hasSQLArrayParameter = false;
if ($isJSArray(strings)) {
const count = strings.length;
if (count === 0) {
return "";
}
var out = strings[0];
function createPostgresAdapter() {
const {
createConnection: _createConnection,
createQuery,
PostgresSQLConnection,
init,
} = $zig("postgres.zig", "createBinding");
// For now, only support insert queries with array parameters
//
// insert into users ${sql(users)}
//
if (values.length > 0 && typeof values[0] === "object" && values[0] && values[0] instanceof SQLArrayParameter) {
if (values.length > 1) {
throw new Error("Cannot mix array parameters with other values");
function normalizeStrings(strings, values) {
hasSQLArrayParameter = false;
if ($isJSArray(strings)) {
const count = strings.length;
if (count === 0) {
return "";
}
hasSQLArrayParameter = true;
const { columns, value } = values[0];
const groupCount = value.length;
out += `values `;
let columnIndex = 1;
let columnCount = columns.length;
let lastColumnIndex = columnCount - 1;
var out = strings[0];
for (var i = 0; i < groupCount; i++) {
out += i > 0 ? `, (` : `(`;
// For now, only support insert queries with array parameters
//
// insert into users ${sql(users)}
//
if (values.length > 0 && typeof values[0] === "object" && values[0] && values[0] instanceof SQLArrayParameter) {
if (values.length > 1) {
throw new Error("Cannot mix array parameters with other values");
}
hasSQLArrayParameter = true;
const { columns, value } = values[0];
const groupCount = value.length;
out += `values `;
for (var j = 0; j < lastColumnIndex; j++) {
out += `$${columnIndex++}, `;
let columnIndex = 1;
let columnCount = columns.length;
let lastColumnIndex = columnCount - 1;
for (var i = 0; i < groupCount; i++) {
out += i > 0 ? `, (` : `(`;
for (var j = 0; j < lastColumnIndex; j++) {
out += `$${columnIndex++}, `;
}
out += `$${columnIndex++})`;
}
out += `$${columnIndex++})`;
for (var i = 1; i < count; i++) {
out += strings[i];
}
return out;
}
for (var i = 1; i < count; i++) {
out += strings[i];
out += `$${i}${strings[i]}`;
}
return out;
}
for (var i = 1; i < count; i++) {
out += `$${i}${strings[i]}`;
}
return out;
return strings + "";
}
return strings + "";
function createConnection(
{ hostname, port, username, password, tls, query, database, sslMode },
onConnected,
onClose,
) {
return _createConnection(
hostname,
Number(port),
username || "",
password || "",
database || "",
// > The default value for sslmode is prefer. As is shown in the table, this
// makes no sense from a security point of view, and it only promises
// performance overhead if possible. It is only provided as the default for
// backward compatibility, and is not recommended in secure deployments.
sslMode || SSLMode.disable,
tls || null,
query || "",
onConnected,
onClose,
);
}
function doCreateQuery(strings, values) {
const sqlString = normalizeStrings(strings, values);
let columns;
if (hasSQLArrayParameter) {
hasSQLArrayParameter = false;
const v = values[0];
columns = v.columns;
values = v.value;
}
return createQuery(sqlString, values, new SQLResultArray(), columns);
}
init(
function (query, result, commandTag, count) {
$assert(result instanceof SQLResultArray, "Invalid result array");
if (typeof commandTag === "string") {
if (commandTag.length > 0) {
result.command = commandTag;
}
} else {
result.command = cmds[commandTag];
}
result.count = count || 0;
try {
query.resolve(result);
} catch (e) {}
},
function (query, reject) {
try {
query.reject(reject);
} catch (e) {}
},
);
return { createConnection, doCreateQuery, normalizeStrings, PostgresSQLConnection };
}
let postgres: ReturnType<typeof createPostgresAdapter>;
let mysql: ReturnType<typeof createMySQLAdapter>;
function createMySQLAdapter() {
function normalizeStrings(strings, values) {
hasSQLArrayParameter = false;
if ($isJSArray(strings)) {
const count = strings.length;
if (count === 0) {
return "";
}
var out = strings[0];
// For now, only support insert queries with array parameters
//
// insert into users ${sql(users)}
//
if (values.length > 0 && typeof values[0] === "object" && values[0] && values[0] instanceof SQLArrayParameter) {
if (values.length > 1) {
throw new Error("Cannot mix array parameters with other values");
}
hasSQLArrayParameter = true;
const { columns, value } = values[0];
const groupCount = value.length;
out += `values `;
let columnCount = columns.length;
let lastColumnIndex = columnCount - 1;
for (var i = 0; i < groupCount; i++) {
out += i > 0 ? `, (` : `(`;
for (var j = 0; j < lastColumnIndex; j++) {
out += `?, `;
}
out += `?)`;
}
for (var i = 1; i < count; i++) {
out += strings[i];
}
return out;
}
for (var i = 1; i < count; i++) {
out += `?`;
}
return out;
}
return strings + "";
}
const {
createConnection: _createConnection,
createQuery,
MySQLConnection,
init,
} = $zig("mysql.zig", "createBinding");
function createConnection(
{ hostname, port, username, password, tls, query, database, sslMode },
onConnected,
onClose,
) {
return _createConnection(
hostname,
Number(port),
username || "",
password || "",
database || "",
// > The default value for sslmode is prefer. As is shown in the table, this
// makes no sense from a security point of view, and it only promises
// performance overhead if possible. It is only provided as the default for
// backward compatibility, and is not recommended in secure deployments.
sslMode || SSLMode.disable,
tls || null,
query || "",
onConnected,
onClose,
);
}
function doCreateQuery(strings, values) {
const sqlString = normalizeStrings(strings, values);
let columns;
if (hasSQLArrayParameter) {
hasSQLArrayParameter = false;
const v = values[0];
columns = v.columns;
values = v.value;
}
return createQuery(sqlString, values, new SQLResultArray(), columns);
}
init(
function (query, result, commandTag, count) {
$assert(result instanceof SQLResultArray, "Invalid result array");
if (typeof commandTag === "string") {
if (commandTag.length > 0) {
result.command = commandTag;
}
} else {
result.command = cmds[commandTag];
}
result.count = count || 0;
try {
query.resolve(result);
} catch (e) {}
},
function (query, reject) {
try {
query.reject(reject);
} catch (e) {}
},
);
return { createConnection, doCreateQuery, normalizeStrings, MySQLConnection };
}
class SQLArrayParameter {
@@ -311,20 +459,68 @@ class SQLArrayParameter {
}
}
const enum DatabaseAdapter {
postgres = 0,
mysql = 1,
}
const databaseAdapterPort: Record<DatabaseAdapter, number> = {
[DatabaseAdapter.postgres]: 5432,
[DatabaseAdapter.mysql]: 3306,
};
function getAdapter(adapter: DatabaseAdapter) {
if (adapter === DatabaseAdapter.postgres) {
if (!postgres) {
postgres = createPostgresAdapter();
}
return postgres;
}
if (adapter === DatabaseAdapter.mysql) {
if (!mysql) {
mysql = createMySQLAdapter();
}
return mysql;
}
throw new Error(`Unsupported adapter: ${adapter}`);
}
function loadOptions(o) {
var hostname, port, username, password, database, tls, url, query, adapter;
const env = Bun.env;
var sslMode: SSLMode = SSLMode.disable;
if (o === undefined || (typeof o === "string" && o.length === 0)) {
let urlString = env.POSTGRES_URL || env.DATABASE_URL || env.PGURL || env.PG_URL;
if (!urlString) {
urlString = env.TLS_POSTGRES_DATABASE_URL || env.TLS_DATABASE_URL;
let urlString = env.TLS_POSTGRES_DATABASE_URL;
if (urlString) {
adapter = DatabaseAdapter.postgres;
} else {
urlString = env.TLS_MYSQL_DATABASE_URL || env.TLS_MARIADB_DATABASE_URL;
if (urlString) {
sslMode = SSLMode.require;
adapter = DatabaseAdapter.mysql;
}
}
if (urlString) {
sslMode = SSLMode.require;
} else {
urlString ||= env.POSTGRES_URL || env.PGURL || env.PG_URL;
if (urlString) {
adapter = DatabaseAdapter.postgres;
} else {
urlString ||= env.MYSQL_URL || env.MARIADB_URL;
if (urlString) {
adapter = DatabaseAdapter.mysql;
}
}
}
if (!urlString) {
urlString = env.DATABASE_URL;
}
if (urlString) {
url = new URL(urlString);
o = {};
@@ -349,10 +545,11 @@ function loadOptions(o) {
url = new URL(o);
}
let protocol;
if (url) {
({ hostname, port, username, password, protocol: adapter } = o = url);
if (adapter[adapter.length - 1] === ":") {
adapter = adapter.slice(0, -1);
({ hostname, port, username, password, protocol } = o = url);
if (protocol[protocol.length - 1] === ":") {
protocol = protocol.slice(0, -1);
}
const queryObject = url.searchParams.toJSON();
@@ -367,13 +564,36 @@ function loadOptions(o) {
query = query.trim();
}
protocol ||= o.protocol || "postgres";
if (protocol === "postgresql") {
protocol = "postgres";
} else if (protocol === "mariadb") {
protocol = "mysql";
}
if (protocol === "mysql") {
adapter = DatabaseAdapter.mysql;
} else if (protocol === "postgres") {
adapter = DatabaseAdapter.postgres;
} else if (protocol && !(protocol === "postgres" || protocol === "mysql")) {
throw new TypeError(`Unsupported protocol: ${protocol}. Only "postgres" and "mysql" are supported`);
}
hostname ||= o.hostname || o.host || env.PGHOST || "localhost";
port ||= Number(o.port || env.PGPORT || 5432);
username ||= o.username || o.user || env.PGUSERNAME || env.PGUSER || env.USER || env.USERNAME || "postgres";
port ||= Number(
o.port || (adapter === DatabaseAdapter.postgres ? env.PGPORT : env.MYSQL_PORT) || databaseAdapterPort[adapter],
);
username ||=
o.username ||
o.user ||
env.PGUSERNAME ||
env.PGUSER ||
env.USER ||
env.USERNAME ||
(protocol === "postgres" ? "postgres" : "root");
database ||= o.database || o.db || (url?.pathname ?? "").slice(1) || env.PGDATABASE || username;
password ||= o.password || o.pass || env.PGPASSWORD || "";
tls ||= o.tls || o.ssl;
adapter ||= o.adapter || "postgres";
if (sslMode !== SSLMode.disable && !tls?.serverName) {
if (hostname) {
@@ -394,11 +614,7 @@ function loadOptions(o) {
throw new Error(`Invalid port: ${port}`);
}
if (adapter && !(adapter === "postgres" || adapter === "postgresql")) {
throw new Error(`Unsupported adapter: ${adapter}. Only \"postgres\" is supported for now`);
}
return { hostname, port, username, password, database, tls, query, sslMode };
return { hostname, port, username, password, database, tls, query, sslMode, adapter };
}
function SQL(o) {
@@ -409,6 +625,8 @@ function SQL(o) {
onConnect: any[] = [],
connectionInfo = loadOptions(o);
var { createConnection, doCreateQuery } = getAdapter(connectionInfo.adapter);
function connectedHandler(query, handle, err) {
if (err) {
return query.reject(err);
@@ -450,19 +668,6 @@ function SQL(o) {
onConnected(err, undefined);
}
function doCreateQuery(strings, values) {
const sqlString = normalizeStrings(strings, values);
let columns;
if (hasSQLArrayParameter) {
hasSQLArrayParameter = false;
const v = values[0];
columns = v.columns;
values = v.value;
}
return createQuery(sqlString, values, new SQLResultArray(), columns);
}
function connectedSQL(strings, values) {
return new Query(doCreateQuery(strings, values), connectedHandler);
}

View File

@@ -55,6 +55,7 @@ pub const API = struct {
pub const NativeBrotli = @import("./bun.js/node/node_zlib_binding.zig").SNativeBrotli;
};
pub const Postgres = @import("./sql/postgres.zig");
pub const MySQL = @import("./sql/mysql.zig");
pub const DNS = @import("./bun.js/api/bun/dns_resolver.zig");
pub const FFI = @import("./bun.js/api/ffi.zig").FFI;
pub const Node = struct {

62
src/sql/SocketMonitor.zig Normal file
View File

@@ -0,0 +1,62 @@
const std = @import("std");
const debug = @import("./postgres.zig").debug;
const DebugSocketMonitorWriter = struct {
var file: std.fs.File = undefined;
var enabled = false;
var check = std.once(load);
pub fn write(data: []const u8) void {
file.writeAll(data) catch {};
}
fn load() void {
if (bun.getenvZAnyCase("BUN_POSTGRES_SOCKET_MONITOR")) |monitor| {
enabled = true;
file = std.fs.cwd().createFile(monitor, .{ .truncate = true }) catch {
enabled = false;
return;
};
debug("writing to {s}", .{monitor});
}
}
};
const DebugSocketMonitorReader = struct {
var file: std.fs.File = undefined;
var enabled = false;
var check = std.once(load);
fn load() void {
if (bun.getenvZAnyCase("BUN_POSTGRES_SOCKET_MONITOR_READER")) |monitor| {
enabled = true;
file = std.fs.cwd().createFile(monitor, .{ .truncate = true }) catch {
enabled = false;
return;
};
debug("duplicating reads to {s}", .{monitor});
}
}
pub fn write(data: []const u8) void {
file.writeAll(data) catch {};
}
};
pub fn write(data: []const u8) void {
if (comptime bun.Environment.isDebug) {
DebugSocketMonitorWriter.check.call();
if (DebugSocketMonitorWriter.enabled) {
DebugSocketMonitorWriter.write(data);
}
}
}
pub fn read(data: []const u8) void {
if (comptime bun.Environment.isDebug) {
DebugSocketMonitorReader.check.call();
if (DebugSocketMonitorReader.enabled) {
DebugSocketMonitorReader.write(data);
}
}
}
const bun = @import("root").bun;

2040
src/sql/mysql.zig Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,967 @@
const std = @import("std");
const bun = @import("root").bun;
const mysql = bun.JSC.MySQL;
const Data = bun.sql.Data;
const protocol = @This();
const MySQLInt32 = mysql.MySQLInt32;
const MySQLInt16 = mysql.MySQLInt16;
const String = bun.String;
const debug = mysql.debug;
const JSValue = bun.JSC.JSValue;
const JSC = bun.JSC;
pub const CharacterSet = enum(u8) {
big5_chinese_ci = 1,
latin2_czech_cs = 2,
dec8_swedish_ci = 3,
cp850_general_ci = 4,
latin1_german1_ci = 5,
hp8_english_ci = 6,
koi8r_general_ci = 7,
latin1_swedish_ci = 8,
latin2_general_ci = 9,
swe7_swedish_ci = 10,
ascii_general_ci = 11,
ujis_japanese_ci = 12,
sjis_japanese_ci = 13,
cp1251_bulgarian_ci = 14,
latin1_danish_ci = 15,
hebrew_general_ci = 16,
tis620_thai_ci = 18,
euckr_korean_ci = 19,
latin7_estonian_cs = 20,
latin2_hungarian_ci = 21,
koi8u_general_ci = 22,
cp1251_ukrainian_ci = 23,
gb2312_chinese_ci = 24,
greek_general_ci = 25,
cp1250_general_ci = 26,
latin2_croatian_ci = 27,
gbk_chinese_ci = 28,
cp1257_lithuanian_ci = 29,
latin5_turkish_ci = 30,
latin1_german2_ci = 31,
armscii8_general_ci = 32,
utf8mb3_general_ci = 33,
cp1250_czech_cs = 34,
ucs2_general_ci = 35,
cp866_general_ci = 36,
keybcs2_general_ci = 37,
macce_general_ci = 38,
macroman_general_ci = 39,
cp852_general_ci = 40,
latin7_general_ci = 41,
latin7_general_cs = 42,
macce_bin = 43,
cp1250_croatian_ci = 44,
utf8mb4_general_ci = 45,
utf8mb4_bin = 46,
latin1_bin = 47,
latin1_general_ci = 48,
latin1_general_cs = 49,
cp1251_bin = 50,
cp1251_general_ci = 51,
cp1251_general_cs = 52,
macroman_bin = 53,
utf16_general_ci = 54,
utf16_bin = 55,
utf16le_general_ci = 56,
cp1256_general_ci = 57,
cp1257_bin = 58,
cp1257_general_ci = 59,
utf32_general_ci = 60,
utf32_bin = 61,
utf16le_bin = 62,
binary = 63,
armscii8_bin = 64,
ascii_bin = 65,
cp1250_bin = 66,
cp1256_bin = 67,
cp866_bin = 68,
dec8_bin = 69,
greek_bin = 70,
hebrew_bin = 71,
hp8_bin = 72,
keybcs2_bin = 73,
koi8r_bin = 74,
koi8u_bin = 75,
utf8mb3_tolower_ci = 76,
latin2_bin = 77,
latin5_bin = 78,
latin7_bin = 79,
cp850_bin = 80,
cp852_bin = 81,
swe7_bin = 82,
utf8mb3_bin = 83,
big5_bin = 84,
euckr_bin = 85,
gb2312_bin = 86,
gbk_bin = 87,
sjis_bin = 88,
tis620_bin = 89,
ucs2_bin = 90,
ujis_bin = 91,
geostd8_general_ci = 92,
geostd8_bin = 93,
latin1_spanish_ci = 94,
cp932_japanese_ci = 95,
cp932_bin = 96,
eucjpms_japanese_ci = 97,
eucjpms_bin = 98,
cp1250_polish_ci = 99,
utf16_unicode_ci = 101,
utf16_icelandic_ci = 102,
utf16_latvian_ci = 103,
utf16_romanian_ci = 104,
utf16_slovenian_ci = 105,
utf16_polish_ci = 106,
utf16_estonian_ci = 107,
utf16_spanish_ci = 108,
utf16_swedish_ci = 109,
utf16_turkish_ci = 110,
utf16_czech_ci = 111,
utf16_danish_ci = 112,
utf16_lithuanian_ci = 113,
utf16_slovak_ci = 114,
utf16_spanish2_ci = 115,
utf16_roman_ci = 116,
utf16_persian_ci = 117,
utf16_esperanto_ci = 118,
utf16_hungarian_ci = 119,
utf16_sinhala_ci = 120,
utf16_german2_ci = 121,
utf16_croatian_ci = 122,
utf16_unicode_520_ci = 123,
utf16_vietnamese_ci = 124,
ucs2_unicode_ci = 128,
ucs2_icelandic_ci = 129,
ucs2_latvian_ci = 130,
ucs2_romanian_ci = 131,
ucs2_slovenian_ci = 132,
ucs2_polish_ci = 133,
ucs2_estonian_ci = 134,
ucs2_spanish_ci = 135,
ucs2_swedish_ci = 136,
ucs2_turkish_ci = 137,
ucs2_czech_ci = 138,
ucs2_danish_ci = 139,
ucs2_lithuanian_ci = 140,
ucs2_slovak_ci = 141,
ucs2_spanish2_ci = 142,
ucs2_roman_ci = 143,
ucs2_persian_ci = 144,
ucs2_esperanto_ci = 145,
ucs2_hungarian_ci = 146,
ucs2_sinhala_ci = 147,
ucs2_german2_ci = 148,
ucs2_croatian_ci = 149,
ucs2_unicode_520_ci = 150,
ucs2_vietnamese_ci = 151,
ucs2_general_mysql500_ci = 159,
utf32_unicode_ci = 160,
utf32_icelandic_ci = 161,
utf32_latvian_ci = 162,
utf32_romanian_ci = 163,
utf32_slovenian_ci = 164,
utf32_polish_ci = 165,
utf32_estonian_ci = 166,
utf32_spanish_ci = 167,
utf32_swedish_ci = 168,
utf32_turkish_ci = 169,
utf32_czech_ci = 170,
utf32_danish_ci = 171,
utf32_lithuanian_ci = 172,
utf32_slovak_ci = 173,
utf32_spanish2_ci = 174,
utf32_roman_ci = 175,
utf32_persian_ci = 176,
utf32_esperanto_ci = 177,
utf32_hungarian_ci = 178,
utf32_sinhala_ci = 179,
utf32_german2_ci = 180,
utf32_croatian_ci = 181,
utf32_unicode_520_ci = 182,
utf32_vietnamese_ci = 183,
utf8mb3_unicode_ci = 192,
utf8mb3_icelandic_ci = 193,
utf8mb3_latvian_ci = 194,
utf8mb3_romanian_ci = 195,
utf8mb3_slovenian_ci = 196,
utf8mb3_polish_ci = 197,
utf8mb3_estonian_ci = 198,
utf8mb3_spanish_ci = 199,
utf8mb3_swedish_ci = 200,
utf8mb3_turkish_ci = 201,
utf8mb3_czech_ci = 202,
utf8mb3_danish_ci = 203,
utf8mb3_lithuanian_ci = 204,
utf8mb3_slovak_ci = 205,
utf8mb3_spanish2_ci = 206,
utf8mb3_roman_ci = 207,
utf8mb3_persian_ci = 208,
utf8mb3_esperanto_ci = 209,
utf8mb3_hungarian_ci = 210,
utf8mb3_sinhala_ci = 211,
utf8mb3_german2_ci = 212,
utf8mb3_croatian_ci = 213,
utf8mb3_unicode_520_ci = 214,
utf8mb3_vietnamese_ci = 215,
utf8mb3_general_mysql500_ci = 223,
utf8mb4_unicode_ci = 224,
utf8mb4_icelandic_ci = 225,
utf8mb4_latvian_ci = 226,
utf8mb4_romanian_ci = 227,
utf8mb4_slovenian_ci = 228,
utf8mb4_polish_ci = 229,
utf8mb4_estonian_ci = 230,
utf8mb4_spanish_ci = 231,
utf8mb4_swedish_ci = 232,
utf8mb4_turkish_ci = 233,
utf8mb4_czech_ci = 234,
utf8mb4_danish_ci = 235,
utf8mb4_lithuanian_ci = 236,
utf8mb4_slovak_ci = 237,
utf8mb4_spanish2_ci = 238,
utf8mb4_roman_ci = 239,
utf8mb4_persian_ci = 240,
utf8mb4_esperanto_ci = 241,
utf8mb4_hungarian_ci = 242,
utf8mb4_sinhala_ci = 243,
utf8mb4_german2_ci = 244,
utf8mb4_croatian_ci = 245,
utf8mb4_unicode_520_ci = 246,
utf8mb4_vietnamese_ci = 247,
gb18030_chinese_ci = 248,
gb18030_bin = 249,
gb18030_unicode_520_ci = 250,
_,
pub const default = CharacterSet.utf8mb4_general_ci;
pub fn label(this: CharacterSet) []const u8 {
if (@intFromEnum(this) < 100 and @intFromEnum(this) > 0) {
return @tagName(this);
}
return "(unknown)";
}
};
// MySQL field types
// https://dev.mysql.com/doc/dev/mysql-server/latest/binary__log__types_8h.html#a8935f33b06a3a88ba403c63acd806920
pub const FieldType = enum(u8) {
MYSQL_TYPE_DECIMAL = 0x00,
MYSQL_TYPE_TINY = 0x01,
MYSQL_TYPE_SHORT = 0x02,
MYSQL_TYPE_LONG = 0x03,
MYSQL_TYPE_FLOAT = 0x04,
MYSQL_TYPE_DOUBLE = 0x05,
MYSQL_TYPE_NULL = 0x06,
MYSQL_TYPE_TIMESTAMP = 0x07,
MYSQL_TYPE_LONGLONG = 0x08,
MYSQL_TYPE_INT24 = 0x09,
MYSQL_TYPE_DATE = 0x0a,
MYSQL_TYPE_TIME = 0x0b,
MYSQL_TYPE_DATETIME = 0x0c,
MYSQL_TYPE_YEAR = 0x0d,
MYSQL_TYPE_NEWDATE = 0x0e,
MYSQL_TYPE_VARCHAR = 0x0f,
MYSQL_TYPE_BIT = 0x10,
MYSQL_TYPE_TIMESTAMP2 = 0x11,
MYSQL_TYPE_DATETIME2 = 0x12,
MYSQL_TYPE_TIME2 = 0x13,
MYSQL_TYPE_JSON = 0xf5,
MYSQL_TYPE_NEWDECIMAL = 0xf6,
MYSQL_TYPE_ENUM = 0xf7,
MYSQL_TYPE_SET = 0xf8,
MYSQL_TYPE_TINY_BLOB = 0xf9,
MYSQL_TYPE_MEDIUM_BLOB = 0xfa,
MYSQL_TYPE_LONG_BLOB = 0xfb,
MYSQL_TYPE_BLOB = 0xfc,
MYSQL_TYPE_VAR_STRING = 0xfd,
MYSQL_TYPE_STRING = 0xfe,
MYSQL_TYPE_GEOMETRY = 0xff,
_,
pub fn fromJS(globalObject: *JSC.JSGlobalObject, value: JSValue) bun.JSError!FieldType {
if (value.isEmptyOrUndefinedOrNull()) {
return .MYSQL_TYPE_NULL;
}
if (value.isCell()) {
const tag = value.jsType();
if (tag.isStringLike()) {
return .MYSQL_TYPE_VARCHAR;
}
if (tag == .JSDate) {
return .MYSQL_TYPE_DATETIME;
}
if (tag.isTypedArray()) {
return .MYSQL_TYPE_BLOB;
}
if (tag == .HeapBigInt) {
return .MYSQL_TYPE_LONGLONG;
}
if (tag.isArrayLike() and value.getLength(globalObject) > 0) {
return FieldType.fromJS(globalObject, value.getIndex(globalObject, 0));
}
if (globalObject.hasException()) return error.JSError;
// Ban these types:
if (tag == .NumberObject) {
return error.JSError;
}
if (tag == .BooleanObject) {
return error.JSError;
}
// It's something internal
if (!tag.isIndexable()) {
return error.JSError;
}
// We will JSON.stringify anything else.
if (tag.isObject()) {
return .MYSQL_TYPE_JSON;
}
}
if (value.isAnyInt()) {
const int = value.toInt64();
if (int >= std.math.minInt(i32) and int <= std.math.maxInt(i32)) {
return .MYSQL_TYPE_LONG;
}
return .MYSQL_TYPE_LONGLONG;
}
if (value.isNumber()) {
return .MYSQL_TYPE_DOUBLE;
}
if (value.isBoolean()) {
return .MYSQL_TYPE_TINY;
}
return .MYSQL_TYPE_VARCHAR;
}
pub fn isBinaryFormatSupported(this: FieldType) bool {
return switch (this) {
.MYSQL_TYPE_TINY,
.MYSQL_TYPE_SHORT,
.MYSQL_TYPE_LONG,
.MYSQL_TYPE_FLOAT,
.MYSQL_TYPE_DOUBLE,
.MYSQL_TYPE_LONGLONG,
.MYSQL_TYPE_TIME,
.MYSQL_TYPE_DATE,
.MYSQL_TYPE_DATETIME,
.MYSQL_TYPE_TIMESTAMP,
.MYSQL_TYPE_TINY_BLOB,
.MYSQL_TYPE_MEDIUM_BLOB,
.MYSQL_TYPE_LONG_BLOB,
.MYSQL_TYPE_BLOB,
.MYSQL_TYPE_STRING,
.MYSQL_TYPE_VARCHAR,
.MYSQL_TYPE_VAR_STRING,
.MYSQL_TYPE_JSON,
=> true,
else => false,
};
}
pub fn toJSType(this: FieldType) JSValue.JSType {
return switch (this) {
.MYSQL_TYPE_TINY,
.MYSQL_TYPE_SHORT,
.MYSQL_TYPE_LONG,
.MYSQL_TYPE_INT24,
.MYSQL_TYPE_YEAR,
=> .NumberObject,
.MYSQL_TYPE_LONGLONG => .BigInt64Array,
.MYSQL_TYPE_FLOAT,
.MYSQL_TYPE_DOUBLE,
.MYSQL_TYPE_DECIMAL,
.MYSQL_TYPE_NEWDECIMAL,
=> .Float64Array,
.MYSQL_TYPE_NULL => .Null,
.MYSQL_TYPE_JSON => .Object,
.MYSQL_TYPE_TIMESTAMP,
.MYSQL_TYPE_DATETIME,
.MYSQL_TYPE_DATE,
.MYSQL_TYPE_TIME,
=> .JSDate,
else => .String,
};
}
};
// Add this near the top of the file
const BoundedArray = std.BoundedArray;
pub const Value = union(enum) {
null,
bool: bool,
short: i16,
ushort: u16,
int: i32,
uint: u32,
long: i64,
ulong: u64,
float: f32,
double: f64,
string: JSC.ZigString.Slice,
string_data: Data,
bytes: JSC.ZigString.Slice,
bytes_data: Data,
date: DateTime,
timestamp: Timestamp,
time: Time,
decimal: Decimal,
pub fn deinit(this: *Value, allocator: std.mem.Allocator) void {
switch (this.*) {
inline .string, .bytes => |*slice| slice.deinit(),
inline .string_data, .bytes_data => |*data| data.deinit(),
.decimal => |*decimal| decimal.deinit(allocator),
else => {},
}
}
pub fn toData(
this: *const Value,
field_type: FieldType,
) !Data {
var buffer: [15]u8 = undefined; // Large enough for all fixed-size types
var stream = std.io.fixedBufferStream(&buffer);
var writer = stream.writer();
switch (this.*) {
.null => return Data{ .empty = {} },
.bool => |b| try writer.writeByte(if (b) 1 else 0),
.short => |s| try writer.writeInt(i16, s, .little),
.ushort => |s| try writer.writeInt(u16, s, .little),
.int => |i| try writer.writeInt(i32, i, .little),
.uint => |i| try writer.writeInt(u32, i, .little),
.long => |l| try writer.writeInt(i64, l, .little),
.ulong => |l| try writer.writeInt(u64, l, .little),
.float => |f| try writer.writeInt(u32, @bitCast(f), .little),
.double => |d| try writer.writeInt(u64, @bitCast(d), .little),
inline .date, .timestamp, .time => |d| {
stream.pos = d.toBinary(field_type, &buffer);
},
.decimal => |dec| return try dec.toBinary(field_type),
.string_data, .bytes_data => |data| return data,
.string, .bytes => |slice| return if (slice.len > 0)
Data{ .temporary = slice.slice() }
else
Data{ .empty = {} },
}
return try Data.create(buffer[0..stream.pos], bun.default_allocator);
}
pub fn fromJS(value: JSC.JSValue, globalObject: *JSC.JSGlobalObject, field_type: FieldType, unsigned: bool) !Value {
// TODO: Handle unsigned
_ = unsigned; // autofix
return switch (field_type) {
.MYSQL_TYPE_TINY => Value{ .bool = value.toBoolean() },
.MYSQL_TYPE_SHORT => Value{ .short = globalObject.validateIntegerRange(value, i16, 0, .{ .min = std.math.minInt(i16), .max = std.math.maxInt(i16) }) orelse return error.JSError },
.MYSQL_TYPE_LONG => Value{ .int = globalObject.validateIntegerRange(value, i32, 0, .{ .min = std.math.minInt(i32), .max = std.math.maxInt(i32) }) orelse return error.JSError },
.MYSQL_TYPE_LONGLONG => Value{ .long = globalObject.validateIntegerRange(value, i64, 0, .{ .min = std.math.minInt(i64), .max = std.math.maxInt(i64) }) orelse return error.JSError },
.MYSQL_TYPE_FLOAT => Value{ .float = @floatCast(try value.coerceToDoubleCheckingErrors(globalObject)) },
.MYSQL_TYPE_DOUBLE => Value{ .double = try value.coerceToDoubleCheckingErrors(globalObject) },
.MYSQL_TYPE_TIME => Value{ .time = try Time.fromJS(value, globalObject) },
.MYSQL_TYPE_DATE => Value{ .date = try DateTime.fromJS(value, globalObject) },
.MYSQL_TYPE_DATETIME => Value{ .date = try DateTime.fromJS(value, globalObject) },
.MYSQL_TYPE_TIMESTAMP => Value{ .timestamp = try Timestamp.fromJS(value, globalObject) },
.MYSQL_TYPE_TINY_BLOB, .MYSQL_TYPE_MEDIUM_BLOB, .MYSQL_TYPE_LONG_BLOB, .MYSQL_TYPE_BLOB => {
if (value.asArrayBuffer(globalObject)) |array_buffer| {
return Value{ .bytes = JSC.ZigString.Slice.fromUTF8NeverFree(array_buffer.slice()) };
}
if (value.as(JSC.WebCore.Blob)) |blob| {
if (blob.needsToReadFile()) {
return globalObject.throwInvalidArguments("File blobs are not supported", .{});
}
return Value{ .bytes = JSC.ZigString.Slice.fromUTF8NeverFree(blob.sharedView()) };
}
if (value.isString()) {
const str = bun.String.tryFromJS(value, globalObject) orelse return error.JSError;
defer str.deref();
return Value{ .string = str.toUTF8(bun.default_allocator) };
}
return globalObject.throwInvalidArguments("Expected a string, blob, or array buffer", .{});
},
.MYSQL_TYPE_JSON => {
var str: bun.String = bun.String.empty;
value.jsonStringify(globalObject, 0, &str);
if (globalObject.hasException()) return error.JSError;
defer str.deref();
return Value{ .string = str.toUTF8(bun.default_allocator) };
},
// .MYSQL_TYPE_VARCHAR, .MYSQL_TYPE_VAR_STRING, .MYSQL_TYPE_STRING => {
else => {
const str = bun.String.tryFromJS(value, globalObject) orelse return error.JSError;
defer str.deref();
return Value{ .string = str.toUTF8(bun.default_allocator) };
},
};
}
pub const Timestamp = struct {
seconds: u32,
microseconds: u24,
pub fn fromData(data: *const Data) !Timestamp {
return fromBinary(data.slice());
}
pub fn fromBinary(val: []const u8) Timestamp {
return .{
// Bytes 0-3: [seconds] (32-bit little-endian unsigned integer)
// Number of seconds since Unix epoch
.seconds = std.mem.readInt(u32, val[0..4], .little),
// Bytes 4-6: [microseconds] (24-bit little-endian unsigned integer)
.microseconds = if (val.len == 7) std.mem.readInt(u24, val[4..7], .little) else 0,
};
}
pub fn fromUnixTimestamp(timestamp: i64) Timestamp {
const timestamp_u64: u64 = @intCast(@max(timestamp, 0));
return .{
.seconds = @truncate(timestamp_u64),
.microseconds = @truncate(@mod(timestamp_u64, 1_000_000)),
};
}
pub fn fromJS(value: JSValue, globalObject: *JSC.JSGlobalObject) !Timestamp {
if (value.isDate()) {
const ts = @divFloor(@as(i64, @intFromFloat(value.getUnixTimestamp())), 1000);
return Timestamp.fromUnixTimestamp(ts);
}
if (value.isNumber()) {
const double = value.asNumber();
return Timestamp.fromUnixTimestamp(@intFromFloat(double));
}
return globalObject.throwInvalidArguments("Expected a date or number", .{});
}
pub fn toUnixTimestamp(this: Timestamp) f64 {
return @as(f64, @floatFromInt(this.seconds)) + @as(f64, @floatFromInt(this.microseconds)) / 1_000_000;
}
pub fn toJS(this: Timestamp, globalObject: *JSC.JSGlobalObject) JSValue {
const timestamp: f64 = @floatCast(this.toUnixTimestamp());
return JSValue.fromDateNumber(globalObject, timestamp * 1000);
}
pub fn toBinary(this: *const Timestamp, field_type: FieldType, buffer: []u8) u8 {
std.mem.writeInt(u32, buffer[0..4], this.seconds, .little);
std.mem.writeInt(u24, buffer[4..7], this.microseconds, .little);
return switch (field_type) {
// [4 bytes] - unix timestamp as uint32_t LE
.MYSQL_TYPE_TIMESTAMP => 4,
// [7 bytes] - unix timestamp as uint32_t LE + microseconds as uint24_t LE
.MYSQL_TYPE_TIMESTAMP2 => 7,
else => unreachable,
};
}
};
pub const DateTime = struct {
year: u16 = 0,
month: u8 = 0,
day: u8 = 0,
hour: u8 = 0,
minute: u8 = 0,
second: u8 = 0,
microsecond: u32 = 0,
pub fn fromData(data: *const Data) !DateTime {
return fromBinary(data.slice());
}
pub fn fromBinaryDate(val: []const u8) DateTime {
return .{
.year = std.mem.readInt(u16, val[0..2], .little),
.month = val[2],
.day = val[3],
};
}
pub fn fromBinary(val: []const u8) DateTime {
switch (val.len) {
4 => {
// Byte 1: [year LSB] (8 bits of year)
// Byte 2: [year MSB] (8 bits of year)
// Byte 3: [month] (8-bit unsigned integer, 1-12)
// Byte 4: [day] (8-bit unsigned integer, 1-31)
return .{
.year = std.mem.readInt(u16, val[0..2], .little),
.month = val[2],
.day = val[3],
};
},
7 => {
// Byte 1: [year LSB] (8 bits of year)
// Byte 2: [year MSB] (8 bits of year)
// Byte 3: [month] (8-bit unsigned integer, 1-12)
// Byte 4: [day] (8-bit unsigned integer, 1-31)
// Byte 5: [hour] (8-bit unsigned integer, 0-23)
// Byte 6: [minute] (8-bit unsigned integer, 0-59)
// Byte 7: [second] (8-bit unsigned integer, 0-59)
return .{
.year = std.mem.readInt(u16, val[0..2], .little),
.month = val[3],
.day = val[4],
.hour = val[5],
.minute = val[6],
.second = val[7],
};
},
11 => {
// Byte 1: [year LSB] (8 bits of year)
// Byte 2: [year MSB] (8 bits of year)
// Byte 3: [month] (8-bit unsigned integer, 1-12)
// Byte 4: [day] (8-bit unsigned integer, 1-31)
// Byte 5: [hour] (8-bit unsigned integer, 0-23)
// Byte 6: [minute] (8-bit unsigned integer, 0-59)
// Byte 7: [second] (8-bit unsigned integer, 0-59)
// Byte 8-11: [microseconds] (32-bit little-endian unsigned integer
return .{
.year = std.mem.readInt(u16, val[0..2], .little),
.month = val[3],
.day = val[4],
.hour = val[5],
.minute = val[6],
.second = val[7],
.microsecond = std.mem.readInt(u32, val[8..12], .little),
};
},
else => bun.Output.panic("Invalid datetime length: {d}", .{val.len}),
}
}
pub fn toBinary(this: *const DateTime, field_type: FieldType, buffer: []u8) u8 {
switch (field_type) {
.MYSQL_TYPE_YEAR => {
std.mem.writeInt(u16, buffer[0..2], this.year, .little);
return 2;
},
.MYSQL_TYPE_DATE => {
std.mem.writeInt(u16, buffer[0..2], this.year, .little);
buffer[2] = this.month;
buffer[3] = this.day;
return 4;
},
.MYSQL_TYPE_DATETIME => {
std.mem.writeInt(u16, buffer[0..2], this.year, .little);
buffer[2] = this.month;
buffer[3] = this.day;
buffer[4] = this.hour;
buffer[5] = this.minute;
buffer[6] = this.second;
if (this.microsecond == 0) {
return 7;
} else {
std.mem.writeInt(u32, buffer[7..11], this.microsecond, .little);
return 11;
}
},
else => unreachable,
}
}
pub fn toUnixTimestamp(this: *const DateTime) i64 {
// Convert to Unix timestamp (seconds since 1970-01-01)
var ts: i64 = 0;
const days = gregorianDays(this.year, this.month, this.day);
ts += days * 86400;
ts += @as(i64, this.hour) * 3600;
ts += @as(i64, this.minute) * 60;
ts += this.second;
return ts;
}
pub fn fromUnixTimestamp(timestamp: i64) DateTime {
var ts = timestamp;
const days = @divFloor(ts, 86400);
ts = @mod(ts, 86400);
const hour = @divFloor(ts, 3600);
ts = @mod(ts, 3600);
const minute = @divFloor(ts, 60);
const second = @mod(ts, 60);
const date = gregorianDate(@intCast(days));
return .{
.year = date.year,
.month = date.month,
.day = date.day,
.hour = @intCast(hour),
.minute = @intCast(minute),
.second = @intCast(second),
};
}
pub fn toJS(this: DateTime, globalObject: *JSC.JSGlobalObject) JSValue {
const ts = this.toUnixTimestamp();
return JSValue.fromDateNumber(globalObject, @floatFromInt(ts * 1000));
}
pub fn fromJS(value: JSValue, globalObject: *JSC.JSGlobalObject) !DateTime {
if (value.isDate()) {
const ts = @divFloor(@as(i64, @intFromFloat(value.getUnixTimestamp())), 1000);
return DateTime.fromUnixTimestamp(ts);
}
if (value.isNumber()) {
const double = value.asNumber();
return DateTime.fromUnixTimestamp(@intFromFloat(double));
}
return globalObject.throwInvalidArguments("Expected a date or number", .{});
}
};
pub const Time = struct {
negative: bool = false,
days: u32 = 0,
hours: u8 = 0,
minutes: u8 = 0,
seconds: u8 = 0,
microseconds: u32 = 0,
pub fn fromJS(value: JSValue, globalObject: *JSC.JSGlobalObject) !Time {
if (value.isDate()) {
const ts = @divFloor(@as(i64, @intFromFloat(value.getUnixTimestamp())), 1000);
return Time.fromUnixTimestamp(ts);
} else if (value.isAnyInt()) {
const int = value.toInt64();
return Time.fromUnixTimestamp(int);
} else {
return globalObject.throwInvalidArguments("Expected a date or number", .{});
}
}
pub fn fromUnixTimestamp(timestamp: i64) Time {
return .{
.negative = timestamp < 0,
.days = @intCast(@divFloor(timestamp, 86400)),
.hours = @intCast(@divFloor(@mod(timestamp, 86400), 3600)),
.minutes = @intCast(@divFloor(@mod(timestamp, 3600), 60)),
.seconds = @intCast(@mod(timestamp, 60)),
};
}
pub fn toUnixTimestamp(this: *const Time) i64 {
var total_ms: i64 = 0;
total_ms +|= @as(i64, this.days) *| 86400000;
total_ms +|= @as(i64, this.hours) *| 3600000;
total_ms +|= @as(i64, this.minutes) *| 60000;
total_ms +|= @as(i64, this.seconds) *| 1000;
total_ms +|= @divFloor(this.microseconds, 1000);
return total_ms;
}
pub fn fromData(data: *const Data) !Time {
return fromBinary(data.slice());
}
pub fn fromBinary(val: []const u8) Time {
if (val.len == 0) {
return Time{};
}
var time = Time{};
const length = val[0];
if (length >= 8) {
time.negative = val[1] != 0;
time.days = std.mem.readInt(u32, val[2..6], .little);
time.hours = val[6];
time.minutes = val[7];
time.seconds = val[8];
}
if (length > 8) {
time.microseconds = std.mem.readInt(u32, val[9..13], .little);
}
return time;
}
pub fn toJS(this: Time, globalObject: *JSC.JSGlobalObject) JSValue {
_ = globalObject; // autofix
var total_ms: i64 = 0;
total_ms +|= @as(i64, this.days) * 86400000;
total_ms +|= @as(i64, this.hours) * 3600000;
total_ms +|= @as(i64, this.minutes) * 60000;
total_ms +|= @as(i64, this.seconds) * 1000;
total_ms +|= @divFloor(this.microseconds, 1000);
if (this.negative) {
total_ms = -total_ms;
}
return JSValue.jsDoubleNumber(@floatFromInt(total_ms));
}
pub fn toBinary(this: *const Time, field_type: FieldType, buffer: []u8) u8 {
switch (field_type) {
.MYSQL_TYPE_TIME, .MYSQL_TYPE_TIME2 => {
buffer[1] = if (this.negative) 1 else 0;
std.mem.writeInt(u32, buffer[2..6], this.days, .little);
buffer[6] = this.hours;
buffer[7] = this.minutes;
buffer[8] = this.seconds;
if (this.microseconds == 0) {
buffer[0] = 8; // length
return 9;
} else {
buffer[0] = 12; // length
std.mem.writeInt(u32, buffer[9..][0..4], this.microseconds, .little);
return 12;
}
},
else => unreachable,
}
}
};
pub const Decimal = struct {
// MySQL DECIMAL is stored as a sequence of base-10 digits
digits: []const u8,
scale: u8,
negative: bool,
pub fn deinit(this: *Decimal, allocator: std.mem.Allocator) void {
allocator.free(this.digits);
}
pub fn toJS(this: Decimal, globalObject: *JSC.JSGlobalObject) JSValue {
var stack = std.heap.stackFallback(64, bun.default_allocator);
var str = std.ArrayList(u8).init(stack.get());
defer str.deinit();
if (this.negative) {
str.append('-') catch return JSValue.jsNumber(0);
}
const decimal_pos = this.digits.len - this.scale;
for (this.digits, 0..) |digit, i| {
if (i == decimal_pos and this.scale > 0) {
str.append('.') catch return JSValue.jsNumber(0);
}
str.append(digit + '0') catch return JSValue.jsNumber(0);
}
var js_str = bun.String.createUTF8(str.items);
return js_str.transferToJS(globalObject);
}
pub fn toBinary(this: Decimal, field_type: FieldType) !Data {
_ = this; // autofix
_ = field_type; // autofix
bun.todoPanic(@src(), "Decimal.toBinary not implemented", .{});
}
pub fn fromData(data: *const Data) !Decimal {
return fromBinary(data.slice());
}
pub fn fromBinary(val: []const u8) Decimal {
_ = val; // autofix
// TODO: handle overflow
bun.todoPanic(@src(), "Decimal.fromBinary not implemented", .{});
}
};
pub fn toJS(this: *const Value, globalObject: *JSC.JSGlobalObject) JSValue {
return switch (this.*) {
.null => JSValue.jsNull(),
.bool => |b| JSValue.jsBoolean(b),
inline .string, .string_data => |*str| {
var out = bun.String.createUTF8(str.slice());
return out.transferToJS(globalObject);
},
inline .bytes, .bytes_data => |*data| JSC.ArrayBuffer.createBuffer(globalObject, data.slice()),
inline .long, .int, .float, .double, .short, .ushort, .uint, .ulong => |t| JSValue.jsNumber(t),
inline .timestamp, .date, .time, .decimal => |*d| d.toJS(globalObject),
};
}
export fn MySQL__ValueToJS(globalObject: *JSC.JSGlobalObject, value: *Value) JSValue {
return value.toJS(globalObject);
}
};
// Helper functions for date calculations
fn isLeapYear(year: u16) bool {
return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0;
}
fn daysInMonth(year: u16, month: u8) u8 {
const days = [_]u8{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
if (month == 2 and isLeapYear(year)) {
return 29;
}
return days[month - 1];
}
fn gregorianDays(year: u16, month: u8, day: u8) i32 {
// Calculate days since 1970-01-01
const y = @as(i32, year) - 1970;
var days: i32 = y * 365 + @divFloor(y, 4) - @divFloor(y, 100) + @divFloor(y, 400);
var m = month;
while (m > 1) : (m -= 1) {
days += daysInMonth(year, m - 1);
}
return days + day - 1;
}
const Date = struct {
year: u16,
month: u8,
day: u8,
};
fn gregorianDate(days: i32) Date {
// Convert days since 1970-01-01 to year/month/day
var d = days;
var y: u16 = 1970;
while (d >= 365 + @as(u16, @intFromBool(isLeapYear(y)))) : (y += 1) {
d -= 365 + @as(u16, @intFromBool(isLeapYear(y)));
}
var m: u8 = 1;
while (d >= daysInMonth(y, m)) : (m += 1) {
d -= daysInMonth(y, m);
}
return .{
.year = y,
.month = m,
.day = @intCast(d + 1),
};
}

View File

@@ -21,134 +21,14 @@ pub const SSLMode = enum(u8) {
verify_ca = 3,
verify_full = 4,
};
pub const Data = union(enum) {
owned: bun.ByteList,
temporary: []const u8,
empty: void,
pub fn toOwned(this: @This()) !bun.ByteList {
return switch (this) {
.owned => this.owned,
.temporary => bun.ByteList.init(try bun.default_allocator.dupe(u8, this.temporary)),
.empty => bun.ByteList.init(&.{}),
};
}
pub fn deinit(this: *@This()) void {
switch (this.*) {
.owned => this.owned.deinitWithAllocator(bun.default_allocator),
.temporary => {},
.empty => {},
}
}
/// Zero bytes before deinit
/// Generally, for security reasons.
pub fn zdeinit(this: *@This()) void {
switch (this.*) {
.owned => {
// Zero bytes before deinit
@memset(this.owned.slice(), 0);
this.owned.deinitWithAllocator(bun.default_allocator);
},
.temporary => {},
.empty => {},
}
}
pub fn slice(this: @This()) []const u8 {
return switch (this) {
.owned => this.owned.slice(),
.temporary => this.temporary,
.empty => "",
};
}
pub fn substring(this: @This(), start_index: usize, end_index: usize) Data {
return switch (this) {
.owned => .{ .temporary = this.owned.slice()[start_index..end_index] },
.temporary => .{ .temporary = this.temporary[start_index..end_index] },
.empty => .{ .empty = {} },
};
}
pub fn sliceZ(this: @This()) [:0]const u8 {
return switch (this) {
.owned => this.owned.slice()[0..this.owned.len :0],
.temporary => this.temporary[0..this.temporary.len :0],
.empty => "",
};
}
};
pub const Data = sql.Data;
pub const protocol = @import("./postgres/postgres_protocol.zig");
pub const types = @import("./postgres/postgres_types.zig");
const sql = @import("./shared_sql.zig");
const Socket = uws.AnySocket;
const PreparedStatementsMap = std.HashMapUnmanaged(u64, *PostgresSQLStatement, bun.IdentityContext(u64), 80);
const SocketMonitor = struct {
const DebugSocketMonitorWriter = struct {
var file: std.fs.File = undefined;
var enabled = false;
var check = std.once(load);
pub fn write(data: []const u8) void {
file.writeAll(data) catch {};
}
fn load() void {
if (bun.getenvZAnyCase("BUN_POSTGRES_SOCKET_MONITOR")) |monitor| {
enabled = true;
file = std.fs.cwd().createFile(monitor, .{ .truncate = true }) catch {
enabled = false;
return;
};
debug("writing to {s}", .{monitor});
}
}
};
const DebugSocketMonitorReader = struct {
var file: std.fs.File = undefined;
var enabled = false;
var check = std.once(load);
fn load() void {
if (bun.getenvZAnyCase("BUN_POSTGRES_SOCKET_MONITOR_READER")) |monitor| {
enabled = true;
file = std.fs.cwd().createFile(monitor, .{ .truncate = true }) catch {
enabled = false;
return;
};
debug("duplicating reads to {s}", .{monitor});
}
}
pub fn write(data: []const u8) void {
file.writeAll(data) catch {};
}
};
pub fn write(data: []const u8) void {
if (comptime bun.Environment.isDebug) {
DebugSocketMonitorWriter.check.call();
if (DebugSocketMonitorWriter.enabled) {
DebugSocketMonitorWriter.write(data);
}
}
}
pub fn read(data: []const u8) void {
if (comptime bun.Environment.isDebug) {
DebugSocketMonitorReader.check.call();
if (DebugSocketMonitorReader.enabled) {
DebugSocketMonitorReader.write(data);
}
}
}
};
const SocketMonitor = @import("./SocketMonitor.zig");
const QueryBindingIterator = sql.QueryBindingIterator;
pub const PostgresSQLContext = struct {
tcp: ?*uws.SocketContext = null,
@@ -415,7 +295,8 @@ pub const PostgresSQLQuery = struct {
pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*PostgresSQLQuery {
_ = callframe;
return globalThis.throw2("PostgresSQLQuery cannot be constructed directly", .{});
globalThis.ERR_ILLEGAL_CONSTRUCTOR("PostgresSQLQuery cannot be constructed directly", .{}).throw();
return error.JSError;
}
pub fn estimatedSize(this: *PostgresSQLQuery) usize {
@@ -1479,7 +1360,11 @@ pub const PostgresSQLConnection = struct {
.password = password,
.options = options,
.options_buf = options_buf,
.socket = undefined,
.socket = .{
.SocketTCP = .{
.socket = .{ .detached = {} },
},
},
.requests = PostgresRequest.Queue.init(bun.default_allocator),
.statements = PreparedStatementsMap{},
.tls_config = tls_config,
@@ -1737,7 +1622,7 @@ pub const PostgresSQLConnection = struct {
pub const Value = extern union {
null: u8,
string: bun.WTF.StringImpl,
string: ?bun.WTF.StringImpl,
float8: f64,
int4: i32,
int8: i64,
@@ -1782,7 +1667,9 @@ pub const PostgresSQLConnection = struct {
switch (this.tag) {
.string => {
this.value.string.deref();
if (this.value.string != null) {
this.value.string.?.deref();
}
},
.json => {
this.value.json.deref();
@@ -2563,124 +2450,6 @@ pub const PostgresSQLStatement = struct {
}
};
const QueryBindingIterator = union(enum) {
array: JSC.JSArrayIterator,
objects: ObjectIterator,
pub fn init(array: JSValue, columns: JSValue, globalObject: *JSC.JSGlobalObject) QueryBindingIterator {
if (columns.isEmptyOrUndefinedOrNull()) {
return .{ .array = JSC.JSArrayIterator.init(array, globalObject) };
}
return .{
.objects = .{
.array = array,
.columns = columns,
.globalObject = globalObject,
.columns_count = columns.getLength(globalObject),
.array_length = array.getLength(globalObject),
},
};
}
pub const ObjectIterator = struct {
array: JSValue,
columns: JSValue = .zero,
globalObject: *JSC.JSGlobalObject,
cell_i: usize = 0,
row_i: usize = 0,
current_row: JSC.JSValue = .zero,
columns_count: usize = 0,
array_length: usize = 0,
any_failed: bool = false,
pub fn next(this: *ObjectIterator) ?JSC.JSValue {
if (this.row_i >= this.array_length) {
return null;
}
const cell_i = this.cell_i;
this.cell_i += 1;
const row_i = this.row_i;
const globalObject = this.globalObject;
if (this.current_row == .zero) {
this.current_row = JSC.JSObject.getIndex(this.array, globalObject, @intCast(row_i));
if (this.current_row.isEmptyOrUndefinedOrNull()) {
if (!globalObject.hasException())
globalObject.throw("Expected a row to be returned at index {d}", .{row_i});
this.any_failed = true;
return null;
}
}
defer {
if (this.cell_i >= this.columns_count) {
this.cell_i = 0;
this.current_row = .zero;
this.row_i += 1;
}
}
const property = JSC.JSObject.getIndex(this.columns, globalObject, @intCast(cell_i));
if (property == .zero or property == .undefined) {
if (!globalObject.hasException())
globalObject.throw("Expected a column at index {d} in row {d}", .{ cell_i, row_i });
this.any_failed = true;
return null;
}
const value = this.current_row.getOwnByValue(globalObject, property);
if (value == .zero or value == .undefined) {
if (!globalObject.hasException())
globalObject.throw("Expected a value at index {d} in row {d}", .{ cell_i, row_i });
this.any_failed = true;
return null;
}
return value;
}
};
pub fn next(this: *QueryBindingIterator) ?JSC.JSValue {
return switch (this.*) {
.array => |*iter| iter.next(),
.objects => |*iter| iter.next(),
};
}
pub fn anyFailed(this: *const QueryBindingIterator) bool {
return switch (this.*) {
.array => false,
.objects => |*iter| iter.any_failed,
};
}
pub fn to(this: *QueryBindingIterator, index: u32) void {
switch (this.*) {
.array => |*iter| iter.i = index,
.objects => |*iter| {
iter.cell_i = index % iter.columns_count;
iter.row_i = index / iter.columns_count;
iter.current_row = .zero;
},
}
}
pub fn reset(this: *QueryBindingIterator) void {
switch (this.*) {
.array => |*iter| {
iter.i = 0;
},
.objects => |*iter| {
iter.cell_i = 0;
iter.row_i = 0;
iter.current_row = .zero;
},
}
}
};
const Signature = struct {
fields: []const int4,
name: []const u8,

209
src/sql/shared_sql.zig Normal file
View File

@@ -0,0 +1,209 @@
const JSC = bun.JSC;
const bun = @import("root").bun;
const JSValue = JSC.JSValue;
const std = @import("std");
pub const QueryBindingIterator = union(enum) {
array: JSC.JSArrayIterator,
objects: ObjectIterator,
pub fn init(array: JSValue, columns: JSValue, globalObject: *JSC.JSGlobalObject) QueryBindingIterator {
if (columns.isEmptyOrUndefinedOrNull()) {
return .{ .array = JSC.JSArrayIterator.init(array, globalObject) };
}
return .{
.objects = .{
.array = array,
.columns = columns,
.globalObject = globalObject,
.columns_count = columns.getLength(globalObject),
.array_length = array.getLength(globalObject),
},
};
}
pub const ObjectIterator = struct {
array: JSValue,
columns: JSValue = .zero,
globalObject: *JSC.JSGlobalObject,
cell_i: usize = 0,
row_i: usize = 0,
current_row: JSC.JSValue = .zero,
columns_count: usize = 0,
array_length: usize = 0,
any_failed: bool = false,
pub fn next(this: *ObjectIterator) ?JSC.JSValue {
if (this.row_i >= this.array_length) {
return null;
}
const cell_i = this.cell_i;
this.cell_i += 1;
const row_i = this.row_i;
const globalObject = this.globalObject;
if (this.current_row == .zero) {
this.current_row = JSC.JSObject.getIndex(this.array, globalObject, @intCast(row_i));
if (this.current_row.isEmptyOrUndefinedOrNull()) {
if (!globalObject.hasException())
globalObject.throw("Expected a row to be returned at index {d}", .{row_i});
this.any_failed = true;
return null;
}
}
defer {
if (this.cell_i >= this.columns_count) {
this.cell_i = 0;
this.current_row = .zero;
this.row_i += 1;
}
}
const property = JSC.JSObject.getIndex(this.columns, globalObject, @intCast(cell_i));
if (property == .zero or property == .undefined) {
if (!globalObject.hasException())
globalObject.throw("Expected a column at index {d} in row {d}", .{ cell_i, row_i });
this.any_failed = true;
return null;
}
const value = this.current_row.getOwnByValue(globalObject, property);
if (value == .zero or value == .undefined) {
if (!globalObject.hasException())
globalObject.throw("Expected a value at index {d} in row {d}", .{ cell_i, row_i });
this.any_failed = true;
return null;
}
return value;
}
};
pub fn next(this: *QueryBindingIterator) ?JSC.JSValue {
return switch (this.*) {
.array => |*iter| iter.next(),
.objects => |*iter| iter.next(),
};
}
pub fn anyFailed(this: *const QueryBindingIterator) bool {
return switch (this.*) {
.array => false,
.objects => |*iter| iter.any_failed,
};
}
pub fn to(this: *QueryBindingIterator, index: u32) void {
switch (this.*) {
.array => |*iter| iter.i = index,
.objects => |*iter| {
iter.cell_i = index % iter.columns_count;
iter.row_i = index / iter.columns_count;
iter.current_row = .zero;
},
}
}
pub fn reset(this: *QueryBindingIterator) void {
switch (this.*) {
.array => |*iter| {
iter.i = 0;
},
.objects => |*iter| {
iter.cell_i = 0;
iter.row_i = 0;
iter.current_row = .zero;
},
}
}
};
// Represents data that can be either owned or temporary
pub const Data = union(enum) {
owned: bun.ByteList,
temporary: []const u8,
inline_storage: std.BoundedArray(u8, 15),
empty: void,
pub fn create(possibly_inline_bytes: []const u8, allocator: std.mem.Allocator) !Data {
if (possibly_inline_bytes.len == 0) {
return .{ .empty = {} };
}
if (possibly_inline_bytes.len <= 15) {
var inline_storage = std.BoundedArray(u8, 15){};
@memcpy(inline_storage.buffer[0..possibly_inline_bytes.len], possibly_inline_bytes);
inline_storage.len = @truncate(possibly_inline_bytes.len);
return .{ .inline_storage = inline_storage };
}
return .{ .owned = bun.ByteList.init(try allocator.dupe(u8, possibly_inline_bytes)) };
}
pub fn toOwned(this: @This()) !bun.ByteList {
return switch (this) {
.owned => this.owned,
.temporary => bun.ByteList.init(try bun.default_allocator.dupe(u8, this.temporary)),
.empty => bun.ByteList.init(&.{}),
.inline_storage => bun.ByteList.init(try bun.default_allocator.dupe(u8, this.inline_storage.slice())),
};
}
pub fn deinit(this: *@This()) void {
switch (this.*) {
.owned => this.owned.deinitWithAllocator(bun.default_allocator),
.temporary => {},
.empty => {},
.inline_storage => {},
}
}
/// Zero bytes before deinit
/// Generally, for security reasons.
pub fn zdeinit(this: *@This()) void {
switch (this.*) {
.owned => {
// Zero bytes before deinit
@memset(this.owned.slice(), 0);
this.owned.deinitWithAllocator(bun.default_allocator);
},
.temporary => {},
.empty => {},
.inline_storage => {},
}
}
pub fn slice(this: *const @This()) []const u8 {
return switch (this.*) {
.owned => this.owned.slice(),
.temporary => this.temporary,
.empty => "",
.inline_storage => this.inline_storage.slice(),
};
}
pub fn substring(this: *const @This(), start_index: usize, end_index: usize) Data {
return switch (this.*) {
.owned => .{ .temporary = this.owned.slice()[start_index..end_index] },
.temporary => .{ .temporary = this.temporary[start_index..end_index] },
.empty => .{ .empty = {} },
.inline_storage => .{ .temporary = this.inline_storage.slice()[start_index..end_index] },
};
}
pub fn sliceZ(this: *const @This()) [:0]const u8 {
return switch (this.*) {
.owned => this.owned.slice()[0..this.owned.len :0],
.temporary => this.temporary[0..this.temporary.len :0],
.empty => "",
.inline_storage => this.inline_storage.slice()[0..this.inline_storage.len :0],
};
}
};
pub const postgres = @import("./postgres.zig");
pub const mysql = @import("./mysql.zig");