mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 23:18:47 +00:00
Compare commits
4 Commits
claude/fix
...
riskymh/sq
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee04a6392a | ||
|
|
7463df18bb | ||
|
|
72ed13fdae | ||
|
|
76fd9b2be6 |
41
src/bun.js/bindings/SQLEntryPoint.cpp
Normal file
41
src/bun.js/bindings/SQLEntryPoint.cpp
Normal file
@@ -0,0 +1,41 @@
|
||||
#include "root.h"
|
||||
|
||||
#include "JavaScriptCore/CallData.h"
|
||||
#include <JavaScriptCore/ObjectConstructor.h>
|
||||
#include "InternalModuleRegistry.h"
|
||||
#include "ModuleLoader.h"
|
||||
#include "ZigGlobalObject.h"
|
||||
#include <JavaScriptCore/JSInternalPromise.h>
|
||||
|
||||
namespace Bun {
|
||||
using namespace JSC;
|
||||
extern "C" JSInternalPromise* Bun__loadSQLEntryPoint(Zig::GlobalObject* globalObject)
|
||||
{
|
||||
auto& vm = globalObject->vm();
|
||||
auto scope = DECLARE_THROW_SCOPE(vm);
|
||||
JSInternalPromise* promise = JSInternalPromise::create(vm, globalObject->internalPromiseStructure());
|
||||
|
||||
JSValue sqlModule = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::InternalSql);
|
||||
if (UNLIKELY(scope.exception())) {
|
||||
return promise->rejectWithCaughtException(globalObject, scope);
|
||||
}
|
||||
|
||||
JSObject* sqlModuleObject = sqlModule.getObject();
|
||||
if (UNLIKELY(!sqlModuleObject)) {
|
||||
BUN_PANIC("Failed to load SQL entry point");
|
||||
}
|
||||
|
||||
MarkedArgumentBuffer args;
|
||||
JSValue result = JSC::call(globalObject, sqlModuleObject, args, "Failed to load SQL entry point"_s);
|
||||
if (UNLIKELY(scope.exception())) {
|
||||
return promise->rejectWithCaughtException(globalObject, scope);
|
||||
}
|
||||
|
||||
promise = jsDynamicCast<JSInternalPromise*>(result);
|
||||
if (UNLIKELY(!promise)) {
|
||||
BUN_PANIC("Failed to load SQL entry point");
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -769,6 +769,7 @@ pub const VirtualMachine = struct {
|
||||
log: *logger.Log,
|
||||
main: string = "",
|
||||
main_is_html_entrypoint: bool = false,
|
||||
main_is_sql_entrypoint: bool = false,
|
||||
main_resolved_path: bun.String = bun.String.empty,
|
||||
main_hash: u32 = 0,
|
||||
process: bun.JSC.C.JSObjectRef = null,
|
||||
@@ -3100,6 +3101,7 @@ pub const VirtualMachine = struct {
|
||||
}
|
||||
|
||||
extern fn Bun__loadHTMLEntryPoint(global: *JSGlobalObject) *JSInternalPromise;
|
||||
extern fn Bun__loadSQLEntryPoint(global: *JSGlobalObject) *JSInternalPromise;
|
||||
|
||||
pub fn reloadEntryPoint(this: *VirtualMachine, entry_path: []const u8) !*JSInternalPromise {
|
||||
this.has_loaded = false;
|
||||
@@ -3108,7 +3110,7 @@ pub const VirtualMachine = struct {
|
||||
|
||||
try this.ensureDebugger(true);
|
||||
|
||||
if (!this.main_is_html_entrypoint) {
|
||||
if (!this.main_is_html_entrypoint and !this.main_is_sql_entrypoint) {
|
||||
try this.entry_point.generate(
|
||||
this.allocator,
|
||||
this.bun_watcher != .none,
|
||||
@@ -3125,10 +3127,12 @@ pub const VirtualMachine = struct {
|
||||
return promise;
|
||||
}
|
||||
|
||||
const promise = if (!this.main_is_html_entrypoint)
|
||||
const promise = if (!this.main_is_html_entrypoint and !this.main_is_sql_entrypoint)
|
||||
JSModuleLoader.loadAndEvaluateModule(this.global, &String.init(main_file_name)) orelse return error.JSError
|
||||
else if (this.main_is_html_entrypoint)
|
||||
Bun__loadHTMLEntryPoint(this.global)
|
||||
else
|
||||
Bun__loadHTMLEntryPoint(this.global);
|
||||
Bun__loadSQLEntryPoint(this.global);
|
||||
|
||||
this.pending_internal_promise = promise;
|
||||
JSValue.fromCell(promise).ensureStillAlive();
|
||||
|
||||
@@ -280,6 +280,7 @@ pub const Run = struct {
|
||||
doPreconnect(ctx.runtime_options.preconnect);
|
||||
|
||||
vm.main_is_html_entrypoint = (loader orelse vm.transpiler.options.loader(std.fs.path.extension(entry_path))) == .html;
|
||||
vm.main_is_sql_entrypoint = (loader orelse vm.transpiler.options.loader(std.fs.path.extension(entry_path))) == .sql;
|
||||
|
||||
const callback = OpaqueWrap(Run, Run.start);
|
||||
vm.global.vm().holdAPILock(&run, callback);
|
||||
|
||||
@@ -4174,7 +4174,7 @@ pub const ParseTask = struct {
|
||||
return ast;
|
||||
},
|
||||
// TODO:
|
||||
.dataurl, .base64, .bunsh => {
|
||||
.dataurl, .base64, .bunsh, .sql => {
|
||||
return try getEmptyAST(log, transpiler, opts, allocator, source, E.String);
|
||||
},
|
||||
.file, .wasm => {
|
||||
@@ -8572,7 +8572,7 @@ pub const LinkerContext = struct {
|
||||
.{@tagName(loader)},
|
||||
) catch bun.outOfMemory();
|
||||
},
|
||||
.css, .file, .toml, .wasm, .base64, .dataurl, .text, .bunsh => {},
|
||||
.css, .file, .toml, .wasm, .base64, .dataurl, .text, .bunsh, .sql => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1528,7 +1528,7 @@ pub const RunCommand = struct {
|
||||
var resolved_mutable = resolved;
|
||||
const path = resolved_mutable.path().?;
|
||||
const loader: bun.options.Loader = this_transpiler.options.loaders.get(path.name.ext) orelse .tsx;
|
||||
if (loader.canBeRunByBun() or loader == .html) {
|
||||
if (loader.canBeRunByBun() or loader == .html or loader == .sql) {
|
||||
log("Resolved to: `{s}`", .{path.text});
|
||||
return _bootAndHandleError(ctx, path.text, loader);
|
||||
} else {
|
||||
@@ -1540,6 +1540,10 @@ pub const RunCommand = struct {
|
||||
if (strings.containsChar(target_name, '*')) {
|
||||
return _bootAndHandleError(ctx, target_name, .html);
|
||||
}
|
||||
} else if (strings.hasSuffixComptime(target_name, ".sql")) {
|
||||
if (strings.containsChar(target_name, '*')) {
|
||||
return _bootAndHandleError(ctx, target_name, .sql);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// It imports the entry points and initializes a server.
|
||||
import type { HTMLBundle, Server } from "bun";
|
||||
const initial = performance.now();
|
||||
const argv = process.argv;
|
||||
const argv = [...process.execArgv, ...process.argv.slice(1)];
|
||||
|
||||
// `import` cannot be used in this file and only Bun builtin modules can be used.
|
||||
const path = require("node:path");
|
||||
@@ -17,7 +17,7 @@ async function start() {
|
||||
let port: number | undefined = undefined;
|
||||
|
||||
// Step 1. Resolve all HTML entry points
|
||||
for (let i = 1, argvLength = argv.length; i < argvLength; i++) {
|
||||
for (let i = 0, argvLength = argv.length; i < argvLength; i++) {
|
||||
const arg = argv[i];
|
||||
|
||||
if (!arg.endsWith(".html")) {
|
||||
|
||||
118
src/js/internal/sql.ts
Normal file
118
src/js/internal/sql.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// This is the file that loads when you pass a '.sql' entry point to Bun.
|
||||
// It imports the entry points and executes them.
|
||||
|
||||
// `import` cannot be used in this file and only Bun builtin modules can be used.
|
||||
const path = require("node:path");
|
||||
|
||||
const initial = performance.now();
|
||||
|
||||
async function start() {
|
||||
const cwd = process.cwd();
|
||||
const args = process.argv.slice(1);
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
console.error("DATABASE_URL environment variable is not set!");
|
||||
console.error("Please set it in your .env file or as an environment variable");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find SQL files to execute
|
||||
let sqlFiles: string[] = [];
|
||||
for (const arg of args) {
|
||||
if (!arg.endsWith(".sql")) {
|
||||
if (arg === "--help") {
|
||||
console.log(`
|
||||
Bun v${Bun.version} (sql)
|
||||
|
||||
Usage:
|
||||
bun [...sql-files]
|
||||
|
||||
Examples:
|
||||
bun query.sql
|
||||
bun ./queries/*.sql
|
||||
|
||||
This is a small wrapper around Bun.sql\`\` that automatically executes SQL files.
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.includes("*") || arg.includes("**") || arg.includes("{")) {
|
||||
const glob = new Bun.Glob(arg);
|
||||
|
||||
for (const file of glob.scanSync(cwd)) {
|
||||
let resolved = path.resolve(cwd, file);
|
||||
if (resolved.includes(path.sep + "node_modules" + path.sep)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
resolved = Bun.resolveSync(resolved, cwd);
|
||||
} catch {
|
||||
resolved = Bun.resolveSync("./" + resolved, cwd);
|
||||
}
|
||||
|
||||
if (resolved.includes(path.sep + "node_modules" + path.sep)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sqlFiles.push(resolved);
|
||||
}
|
||||
} else {
|
||||
let resolved = arg;
|
||||
try {
|
||||
resolved = Bun.resolveSync(arg, cwd);
|
||||
} catch {
|
||||
resolved = Bun.resolveSync("./" + arg, cwd);
|
||||
}
|
||||
|
||||
if (resolved.includes(path.sep + "node_modules" + path.sep)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sqlFiles.push(resolved);
|
||||
}
|
||||
|
||||
if (args.length > 1) {
|
||||
sqlFiles = [...new Set(sqlFiles)];
|
||||
}
|
||||
}
|
||||
|
||||
if (sqlFiles.length === 0) {
|
||||
console.error("No SQL files found matching " + JSON.stringify(Bun.main));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Execute each SQL file
|
||||
await Bun.sql.transaction(async tx => {
|
||||
for (const file of sqlFiles) {
|
||||
const { default: sqlContent } = await import(file, { with: { type: "text" } });
|
||||
|
||||
if (sqlFiles.length > 1) {
|
||||
if (file.startsWith(cwd)) {
|
||||
console.log(`${file.slice(cwd.length + 1)}:`);
|
||||
} else {
|
||||
console.log(`${file}:`);
|
||||
}
|
||||
}
|
||||
|
||||
const results = await tx.unsafe(sqlContent);
|
||||
|
||||
if (results.length === 0) {
|
||||
if (sqlFiles.length > 1) console.log(results);
|
||||
} else {
|
||||
console.table(results);
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = (performance.now() - initial).toFixed(2);
|
||||
if (sqlFiles.length > 1) {
|
||||
console.log(`Executed ${sqlFiles.length} SQL ${sqlFiles.length === 1 ? "file" : "files"} in ${elapsed}ms`);
|
||||
} else {
|
||||
console.log(`Executed SQL in ${elapsed}ms`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default start;
|
||||
@@ -645,6 +645,7 @@ pub const Loader = enum(u8) {
|
||||
sqlite,
|
||||
sqlite_embedded,
|
||||
html,
|
||||
sql,
|
||||
|
||||
pub fn disableHTML(this: Loader) Loader {
|
||||
return switch (this) {
|
||||
@@ -765,6 +766,7 @@ pub const Loader = enum(u8) {
|
||||
.{ "sqlite", .sqlite },
|
||||
.{ "sqlite_embedded", .sqlite_embedded },
|
||||
.{ "html", .html },
|
||||
.{ "sql", .sql },
|
||||
});
|
||||
|
||||
pub const api_names = bun.ComptimeStringMap(Api.Loader, .{
|
||||
@@ -816,7 +818,7 @@ pub const Loader = enum(u8) {
|
||||
.tsx => .tsx,
|
||||
.css => .css,
|
||||
.html => .html,
|
||||
.file, .bunsh => .file,
|
||||
.file, .bunsh, .sql => .file,
|
||||
.json => .json,
|
||||
.toml => .toml,
|
||||
.wasm => .wasm,
|
||||
@@ -904,7 +906,9 @@ const default_loaders_posix = .{
|
||||
.{ ".node", .napi },
|
||||
.{ ".txt", .text },
|
||||
.{ ".text", .text },
|
||||
|
||||
.{ ".html", .html },
|
||||
.{ ".sql", .sql },
|
||||
.{ ".jsonc", .json },
|
||||
};
|
||||
const default_loaders_win32 = default_loaders_posix ++ .{
|
||||
@@ -1303,7 +1307,7 @@ pub fn definesFromTransformOptions(
|
||||
);
|
||||
}
|
||||
|
||||
const default_loader_ext_bun = [_]string{ ".node", ".html" };
|
||||
const default_loader_ext_bun = [_]string{ ".node", ".html", ".sql" };
|
||||
const default_loader_ext = [_]string{
|
||||
".jsx", ".json",
|
||||
".js", ".mjs",
|
||||
|
||||
@@ -773,7 +773,7 @@ pub const Transpiler = struct {
|
||||
output_file.value = .{ .buffer = .{ .allocator = alloc, .bytes = result.code } };
|
||||
},
|
||||
|
||||
.html, .bunsh, .sqlite_embedded, .sqlite, .wasm, .file, .napi => {
|
||||
.html, .bunsh, .sqlite_embedded, .sqlite, .wasm, .file, .napi, .sql => {
|
||||
const hashed_name = try transpiler.linker.getHashedFilename(file_path, null);
|
||||
var pathname = try transpiler.allocator.alloc(u8, hashed_name.len + file_path.name.ext.len);
|
||||
bun.copy(u8, pathname, hashed_name);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { sql, SQL, randomUUIDv7 } from "bun";
|
||||
import { sql, SQL, randomUUIDv7, write } from "bun";
|
||||
const postgres = (...args) => new sql(...args);
|
||||
import { expect, test, mock, beforeAll, afterAll, describe } from "bun:test";
|
||||
import { $ } from "bun";
|
||||
import { bunExe, isCI, withoutAggressiveGC, isLinux } from "harness";
|
||||
import path from "path";
|
||||
import { bunExe, isCI, withoutAggressiveGC, isLinux, bunEnv, tmpdirSync } from "harness";
|
||||
import path, { join } from "path";
|
||||
|
||||
import { exec, execSync } from "child_process";
|
||||
import { promisify } from "util";
|
||||
@@ -10854,4 +10854,171 @@ if (isDockerEnabled()) {
|
||||
expect(results[1].price).toBe("0.0123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("bun run ./file.sql", () => {
|
||||
// Helper to create temp test files
|
||||
async function createTempSQLFiles() {
|
||||
const dir = tmpdirSync();
|
||||
|
||||
await Promise.all([
|
||||
write(join(dir, "single.sql"), "SELECT 1 as num;"),
|
||||
write(join(dir, "query1.sql"), "SELECT 2 as num;"),
|
||||
write(join(dir, "query2.sql"), "SELECT 3 as num;"),
|
||||
]);
|
||||
|
||||
return dir;
|
||||
}
|
||||
|
||||
test("executes single SQL file", async () => {
|
||||
const dir = await createTempSQLFiles();
|
||||
const { stdout, stderr, exitCode } = Bun.spawnSync({
|
||||
cmd: [bunExe(), join(dir, "single.sql")],
|
||||
env: { ...bunEnv, DATABASE_URL: process.env.DATABASE_URL },
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
const error = stderr.toString();
|
||||
|
||||
expect(error).toBe("");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
expect(output).toContain("│ num │");
|
||||
expect(output).toContain("│ 1 │");
|
||||
expect(output).toContain("Executed SQL in");
|
||||
});
|
||||
|
||||
test("executes multiple SQL files", async () => {
|
||||
const dir = await createTempSQLFiles();
|
||||
const { stdout, stderr, exitCode } = Bun.spawnSync({
|
||||
cmd: [bunExe(), join(dir, "query1.sql"), join(dir, "query2.sql")],
|
||||
env: { ...bunEnv, DATABASE_URL: process.env.DATABASE_URL },
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
const error = stderr.toString();
|
||||
|
||||
expect(error).toBe("");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
expect(output).toContain("query1.sql:");
|
||||
expect(output).toContain("│ 2 │");
|
||||
expect(output).toContain("query2.sql:");
|
||||
expect(output).toContain("│ 3 │");
|
||||
expect(output).toContain("Executed 2 SQL files in");
|
||||
});
|
||||
|
||||
test("executes SQL files using glob pattern", async () => {
|
||||
const dir = await createTempSQLFiles();
|
||||
const { stdout, stderr, exitCode } = Bun.spawnSync({
|
||||
cmd: [bunExe(), join(dir, "query*.sql")],
|
||||
env: { ...bunEnv, DATABASE_URL: process.env.DATABASE_URL },
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
const error = stderr.toString();
|
||||
|
||||
expect(error).toBe("");
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
expect(output).toContain("query1.sql:");
|
||||
expect(output).toContain("│ 2 │");
|
||||
expect(output).toContain("query2.sql:");
|
||||
expect(output).toContain("│ 3 │");
|
||||
expect(output).toContain("Executed 2 SQL files in");
|
||||
});
|
||||
|
||||
test("shows help message with --help flag", async () => {
|
||||
const dir = await createTempSQLFiles();
|
||||
const { stdout, stderr, exitCode } = Bun.spawnSync({
|
||||
cmd: [bunExe(), "single.sql", "--help"],
|
||||
cwd: dir,
|
||||
env: { ...bunEnv, DATABASE_URL: process.env.DATABASE_URL },
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
const error = stderr.toString();
|
||||
|
||||
expect(output).toContain("Bun v");
|
||||
expect(output).toContain("Usage:");
|
||||
expect(output).toContain("bun [...sql-files]");
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(error).toBe("");
|
||||
});
|
||||
|
||||
test("throws error when no SQL files found", async () => {
|
||||
const dir = await createTempSQLFiles();
|
||||
const { stdout, stderr, exitCode } = Bun.spawnSync({
|
||||
cmd: [bunExe(), join(dir, "nonexistent.sql")],
|
||||
env: { ...bunEnv, DATABASE_URL: process.env.DATABASE_URL },
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
const error = stderr.toString();
|
||||
|
||||
// expect(error).toBe("");
|
||||
// expect(error).toContain("No SQL files found");
|
||||
expect(exitCode).not.toBe(0);
|
||||
});
|
||||
|
||||
test("executes many statements", async () => {
|
||||
const dir = await createTempSQLFiles();
|
||||
|
||||
await write(
|
||||
join(dir, "sql.sql"),
|
||||
`
|
||||
CREATE TEMPORARY TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL
|
||||
);
|
||||
INSERT INTO users (id, name, email) VALUES (1, 'John Doe', 'john@example.com');
|
||||
INSERT INTO users (id, name, email) VALUES (2, 'Jane Smith', 'jane@example.com');
|
||||
|
||||
SELECT * FROM users;
|
||||
`,
|
||||
);
|
||||
|
||||
const { stdout, stderr, exitCode } = Bun.spawnSync({
|
||||
cmd: [bunExe(), "sql.sql"],
|
||||
cwd: dir,
|
||||
env: { ...bunEnv, DATABASE_URL: process.env.DATABASE_URL },
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
stdin: "ignore",
|
||||
});
|
||||
|
||||
const output = stdout.toString();
|
||||
const error = stderr.toString();
|
||||
|
||||
expect(error).toBe("");
|
||||
expect(output.slice(0, output.lastIndexOf("\n\n"))).toMatchInlineSnapshot(`
|
||||
"┌───┬──────────────┬───────┬────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────┐
|
||||
│ │ command │ count │ 0 │ 1 │
|
||||
├───┼──────────────┼───────┼────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ 0 │ CREATE TABLE │ 0 │ │ │
|
||||
│ 1 │ INSERT │ 1 │ │ │
|
||||
│ 2 │ INSERT │ 1 │ │ │
|
||||
│ 3 │ SELECT │ 2 │ { id: 1, name: "John Doe", email: "john@example.com" } │ { id: 2, name: "Jane Smith", email: "jane@example.com" } │
|
||||
└───┴──────────────┴───────┴────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────┘"
|
||||
`);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user