Skip to content
Closed
1,089 changes: 59 additions & 1,030 deletions src/js/internal/sql/mysql.ts

Large diffs are not rendered by default.

1,097 changes: 61 additions & 1,036 deletions src/js/internal/sql/postgres.ts

Large diffs are not rendered by default.

1,144 changes: 1,144 additions & 0 deletions src/js/internal/sql/shared.ts

Large diffs are not rendered by default.

235 changes: 36 additions & 199 deletions src/js/internal/sql/sqlite.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import type * as BunSQLiteModule from "bun:sqlite";
import type { BaseQueryHandle, Query, SQLQueryResultMode } from "./query";
import type { ArrayType, DatabaseAdapter, OnConnected, SQLArrayParameter, SQLHelper, SQLResultArray } from "./shared";

const { SQLHelper, SQLResultArray, buildDefinedColumnsAndQuery } = require("internal/sql/shared");
const {
Query,
SQLQueryResultMode,
symbols: { _strings, _values },
} = require("internal/sql/query");
import type {
ArrayType,
DatabaseAdapter,
OnConnected,
SQLCommand as SharedSQLCommand,
SQLArrayParameter,
SQLResultArray,
} from "./shared";

const { SQLResultArray, normalizeQuery, pushBindParam } = require("internal/sql/shared");
const { SQLQueryResultMode } = require("internal/sql/query");
const { SQLiteError } = require("internal/sql/errors");

let lazySQLiteModule: typeof BunSQLiteModule;
Expand Down Expand Up @@ -377,204 +380,38 @@ class SQLiteAdapter implements DatabaseAdapter<BunSQLiteModule.Database, BunSQLi
});
}
normalizeQuery(strings: string | TemplateStringsArray, values: unknown[], binding_idx = 1): [string, unknown[]] {
if (typeof strings === "string") {
// identifier or unsafe query
return [strings, values || []];
}

if (!$isArray(strings)) {
// we should not hit this path
throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused");
}

const str_len = strings.length;
if (str_len === 0) {
return ["", []];
}
return normalizeQuery(this, strings, values, binding_idx);
}

let binding_values: any[] = [];
let query = "";
// SQLite uses ? for placeholders, not $1, $2, etc.
placeholder(_index: number): string {
return "?";
}

for (let i = 0; i < str_len; i++) {
const string = strings[i];
bindParam(value: unknown, binding_values: unknown[], index: number): string {
return pushBindParam(this, value, binding_values, index);
}

if (typeof string === "string") {
query += string;
getHelperCommand(query: string): SharedSQLCommand {
// when partial is true we stop on the first command we find
const { command } = parseSQLQuery(query, true);

if (values.length > i) {
const value = values[i];
// only selectIn, insert, update, updateSet are allowed
if (command === SQLCommand.none || command === SQLCommand.where) {
throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and WHERE IN commands");
}
// the local SQLCommand enum is numerically identical to the shared one
return command as unknown as SharedSQLCommand;
}

if (value instanceof Query) {
const q = value as Query<any, any>;
const [sub_query, sub_values] = this.normalizeQuery(q[_strings], q[_values], binding_idx);
isUpsertUpdate(_query: string): boolean {
return false;
}

query += sub_query;
for (let j = 0; j < sub_values.length; j++) {
binding_values.push(sub_values[j]);
}
binding_idx += sub_values.length;
} else if (value instanceof SQLHelper) {
// when partial is true we stop on the first command we find
const { command } = parseSQLQuery(query, true);

// only selectIn, insert, update, updateSet are allowed
if (command === SQLCommand.none || command === SQLCommand.where) {
throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and WHERE IN commands");
}
const { columns, value: items } = value as SQLHelper;
const columnCount = columns.length;
if (columnCount === 0 && command !== SQLCommand.in) {
throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`);
}
const lastColumnIndex = columns.length - 1;

if (command === SQLCommand.insert) {
//
// insert into users ${sql(users)} or insert into users ${sql(user)}
//

// Build column list while determining which columns have at least one defined value
const { definedColumns, columnsSql } = buildDefinedColumnsAndQuery(
columns,
items,
this.escapeIdentifier.bind(this),
);

const definedColumnCount = definedColumns.length;
if (definedColumnCount === 0) {
throw new SyntaxError("Insert needs to have at least one column with a defined value");
}
const lastDefinedColumnIndex = definedColumnCount - 1;

query += columnsSql;
if ($isArray(items)) {
const itemsCount = items.length;
const lastItemIndex = itemsCount - 1;
for (let j = 0; j < itemsCount; j++) {
query += "(";
const item = items[j];
for (let k = 0; k < definedColumnCount; k++) {
const column = definedColumns[k];
const columnValue = item[column];
// SQLite uses ? for placeholders, not $1, $2, etc.
query += `?${k < lastDefinedColumnIndex ? ", " : ""}`;
// If this item has undefined for a column that other items defined, use null
binding_values.push(typeof columnValue === "undefined" ? null : columnValue);
}
if (j < lastItemIndex) {
query += "),";
} else {
query += ") "; // the user can add RETURNING * or RETURNING id
}
}
} else {
query += "(";
const item = items;
for (let j = 0; j < definedColumnCount; j++) {
const column = definedColumns[j];
const columnValue = item[column];
// SQLite uses ? for placeholders
query += `?${j < lastDefinedColumnIndex ? ", " : ""}`;
binding_values.push(columnValue);
}
query += ") "; // the user can add RETURNING * or RETURNING id
}
} else if (command === SQLCommand.in) {
// SELECT * FROM users WHERE id IN (${sql([1, 2, 3])})
if (!$isArray(items)) {
throw new SyntaxError("An array of values is required for WHERE IN helper");
}
const itemsCount = items.length;
const lastItemIndex = itemsCount - 1;
query += "(";
for (let j = 0; j < itemsCount; j++) {
// SQLite uses ? for placeholders
query += `?${j < lastItemIndex ? ", " : ""}`;
if (columnCount > 0) {
// we must use a key from a object
if (columnCount > 1) {
// we should not pass multiple columns here
throw new SyntaxError("Cannot use WHERE IN helper with multiple columns");
}
// SELECT * FROM users WHERE id IN (${sql(users, "id")})
const value = items[j];
if (typeof value === "undefined") {
binding_values.push(null);
} else {
const value_from_key = value[columns[0]];

if (typeof value_from_key === "undefined") {
binding_values.push(null);
} else {
binding_values.push(value_from_key);
}
}
} else {
const value = items[j];
if (typeof value === "undefined") {
binding_values.push(null);
} else {
binding_values.push(value);
}
}
}
query += ") "; // more conditions can be added after this
} else {
// UPDATE users SET ${sql({ name: "John", age: 31 })} WHERE id = 1
let item;
if ($isArray(items)) {
if (items.length > 1) {
throw new SyntaxError("Cannot use array of objects for UPDATE");
}
item = items[0];
} else {
item = items;
}
// no need to include if is updateSet
if (command === SQLCommand.update) {
query += " SET ";
}
for (let i = 0; i < columnCount; i++) {
const column = columns[i];
const columnValue = item[column];
if (typeof columnValue === "undefined") {
// skip undefined values, this is the expected behavior in JS
continue;
}
// SQLite uses ? for placeholders
query += `${this.escapeIdentifier(column)} = ?${i < lastColumnIndex ? ", " : ""}`;
if (typeof columnValue === "undefined") {
binding_values.push(null);
} else {
binding_values.push(columnValue);
}
}
if (query.endsWith(", ")) {
// we got an undefined value at the end, lets remove the last comma
query = query.substring(0, query.length - 2);
}
if (query.endsWith("SET ")) {
throw new SyntaxError("Update needs to have at least one column");
}
// the user can add where clause after this
query += " ";
}
} else {
// SQLite uses ? for placeholders
query += `? `;
if (typeof value === "undefined") {
binding_values.push(null);
} else {
binding_values.push(value);
}
}
}
} else {
throw new SyntaxError("Invalid query: SQL Fragment cannot be executed or was misused");
}
throwIfUpdateEmpty(query: string, _hasValues: boolean): void {
if (query.endsWith("SET ")) {
throw new SyntaxError("Update needs to have at least one column");
}

return [query, binding_values];
}

connect(onConnected: OnConnected<BunSQLiteModule.Database>, reserved?: boolean) {
Expand Down
14 changes: 10 additions & 4 deletions src/sql/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ pub mod shared {
pub mod connection_flags;
#[path = "Data.rs"]
pub mod data;
#[path = "QueryStatus.rs"]
pub mod query_status;
#[path = "SQLQueryResultMode.rs"]
pub mod sql_query_result_mode;
#[path = "StackReader.rs"]
pub mod stack_reader;
#[path = "StatementStatus.rs"]
pub mod statement_status;

pub use column_identifier::ColumnIdentifier;
pub use connection_flags::ConnectionFlags;
pub use data::Data;
pub use sql_query_result_mode::SQLQueryResultMode;
pub use stack_reader::StackReader;
}

pub mod mysql {
Expand All @@ -31,8 +38,6 @@ pub mod mysql {
pub mod mysql_request;
#[path = "MySQLTypes.rs"]
pub mod mysql_types;
#[path = "QueryStatus.rs"]
pub mod query_status;
#[path = "SSLMode.rs"]
pub mod ssl_mode;
#[path = "StatusFlags.rs"]
Expand Down Expand Up @@ -105,7 +110,7 @@ pub mod mysql {
pub use handshake_response41::HandshakeResponse41;
pub use handshake_v10::HandshakeV10;
pub use local_infile_request::LocalInfileRequest;
pub use new_reader::{Decode, NewReader, NewReaderOf, ReadableInt, ReaderContext};
pub use new_reader::{Decode, NewReader, ReadableInt, ReaderContext};
pub use new_writer::{NewWriter, NewWriterWrap, Packet, WriterContext, write_wrap};
pub use ok_packet::OKPacket;
pub use packet_header::PacketHeader;
Expand All @@ -118,11 +123,12 @@ pub mod mysql {
pub use crate::mysql::mysql_types::FieldType;
}

pub use crate::shared::query_status;
pub use crate::shared::query_status::Status as QueryStatus;
pub use auth_method::AuthMethod;
pub use capabilities::Capabilities;
pub use connection_state::ConnectionState;
pub use mysql_query_result::MySQLQueryResult;
pub use query_status::Status as QueryStatus;
pub use ssl_mode::SSLMode;
pub use status_flags::{StatusFlag, StatusFlags};
pub use tls_status::TLSStatus;
Expand Down
Loading
Loading