Compare commits

...

4 Commits

Author SHA1 Message Date
RiskyMH
ee04a6392a more 2025-02-20 16:04:59 +00:00
RiskyMH
7463df18bb better tests? 2025-02-20 15:33:12 +00:00
RiskyMH
72ed13fdae tests 2025-02-20 14:46:38 +00:00
RiskyMH
76fd9b2be6 Introduce bun ./file.sql 2025-02-20 14:19:45 +00:00
10 changed files with 353 additions and 14 deletions

View 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;
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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 => {},
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
View 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;

View File

@@ -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",

View File

@@ -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);

View File

@@ -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);
});
});
}