diff --git a/src/js/internal/sql/mysql.ts b/src/js/internal/sql/mysql.ts index e338ddadfd..766512d026 100644 --- a/src/js/internal/sql/mysql.ts +++ b/src/js/internal/sql/mysql.ts @@ -134,7 +134,7 @@ const enum SQLCommand { update = 1, updateSet = 2, where = 3, - whereIn = 4, + in = 4, none = -1, } export type { SQLCommand }; @@ -146,7 +146,7 @@ function commandToString(command: SQLCommand): string { case SQLCommand.updateSet: case SQLCommand.update: return "UPDATE"; - case SQLCommand.whereIn: + case SQLCommand.in: case SQLCommand.where: return "WHERE"; default: @@ -161,7 +161,8 @@ function detectCommand(query: string): SQLCommand { let token = ""; let command = SQLCommand.none; let quoted = false; - for (let i = 0; i < text_len; i++) { + // we need to reverse search so we find the closest command to the parameter + for (let i = text_len - 1; i >= 0; i--) { const char = text[i]; switch (char) { case " ": // Space @@ -172,37 +173,19 @@ function detectCommand(query: string): SQLCommand { case "\v": { switch (token) { case "insert": { - if (command === SQLCommand.none) { - return SQLCommand.insert; - } - return command; + return SQLCommand.insert; } case "update": { - if (command === SQLCommand.none) { - command = SQLCommand.update; - token = ""; - continue; // try to find SET - } - return command; + return SQLCommand.update; } case "where": { - command = SQLCommand.where; - token = ""; - continue; // try to find IN + return SQLCommand.where; } case "set": { - if (command === SQLCommand.update) { - command = SQLCommand.updateSet; - token = ""; - continue; // try to find WHERE - } - return command; + return SQLCommand.updateSet; } case "in": { - if (command === SQLCommand.where) { - return SQLCommand.whereIn; - } - return command; + return SQLCommand.in; } default: { token = ""; @@ -217,43 +200,31 @@ function detectCommand(query: string): SQLCommand { continue; } if (!quoted) { - token += char; + token = char + token; } } } } if (token) { - switch (command) { - case SQLCommand.none: { - switch (token) { - case "insert": - return SQLCommand.insert; - case "update": - return SQLCommand.update; - case "where": - return SQLCommand.where; - default: - return SQLCommand.none; - } - } - case SQLCommand.update: { - if (token === "set") { - return SQLCommand.updateSet; - } + switch (token) { + case "insert": + return SQLCommand.insert; + case "update": return SQLCommand.update; - } - case SQLCommand.where: { - if (token === "in") { - return SQLCommand.whereIn; - } + case "where": return SQLCommand.where; - } + case "set": + return SQLCommand.updateSet; + case "in": + case "any": + case "all": + return SQLCommand.in; + default: + return SQLCommand.none; } } - return command; } - const enum PooledConnectionState { pending = 0, connected = 1, @@ -1034,11 +1005,11 @@ class MySQLAdapter const command = detectCommand(query); // 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"); + throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and IN commands"); } const { columns, value: items } = value as SQLHelper; const columnCount = columns.length; - if (columnCount === 0 && command !== SQLCommand.whereIn) { + if (columnCount === 0 && command !== SQLCommand.in) { throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`); } const lastColumnIndex = columns.length - 1; @@ -1093,7 +1064,7 @@ class MySQLAdapter } query += ") "; // the user can add RETURNING * or RETURNING id } - } else if (command === SQLCommand.whereIn) { + } 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"); @@ -1143,19 +1114,29 @@ class MySQLAdapter } else { item = items; } - // no need to include if is updateSet - if (command === SQLCommand.update) { + // no need to include if is updateSet or upsert + const isUpsert = query.trimEnd().endsWith("ON DUPLICATE KEY UPDATE"); + if (command === SQLCommand.update && !isUpsert) { query += " SET "; } + let hasValues = false; for (let i = 0; i < columnCount; i++) { const column = columns[i]; const columnValue = item[column]; - query += `${this.escapeIdentifier(column)} = ?${i < lastColumnIndex ? ", " : ""}`; if (typeof columnValue === "undefined") { - binding_values.push(null); - } else { - binding_values.push(columnValue); + // skip undefined values, this is the expected behavior in JS + continue; } + hasValues = true; + query += `${this.escapeIdentifier(column)} = ?${i < lastColumnIndex ? ", " : ""}`; + 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 (!hasValues) { + throw new SyntaxError("Update needs to have at least one column"); } query += " "; // the user can add where clause after this } diff --git a/src/js/internal/sql/postgres.ts b/src/js/internal/sql/postgres.ts index a86c78ca5c..d4d1828719 100644 --- a/src/js/internal/sql/postgres.ts +++ b/src/js/internal/sql/postgres.ts @@ -354,7 +354,7 @@ const enum SQLCommand { update = 1, updateSet = 2, where = 3, - whereIn = 4, + in = 4, none = -1, } export type { SQLCommand }; @@ -366,7 +366,7 @@ function commandToString(command: SQLCommand): string { case SQLCommand.updateSet: case SQLCommand.update: return "UPDATE"; - case SQLCommand.whereIn: + case SQLCommand.in: case SQLCommand.where: return "WHERE"; default: @@ -381,7 +381,8 @@ function detectCommand(query: string): SQLCommand { let token = ""; let command = SQLCommand.none; let quoted = false; - for (let i = 0; i < text_len; i++) { + // we need to reverse search so we find the closest command to the parameter + for (let i = text_len - 1; i >= 0; i--) { const char = text[i]; switch (char) { case " ": // Space @@ -392,37 +393,19 @@ function detectCommand(query: string): SQLCommand { case "\v": { switch (token) { case "insert": { - if (command === SQLCommand.none) { - return SQLCommand.insert; - } - return command; + return SQLCommand.insert; } case "update": { - if (command === SQLCommand.none) { - command = SQLCommand.update; - token = ""; - continue; // try to find SET - } - return command; + return SQLCommand.update; } case "where": { - command = SQLCommand.where; - token = ""; - continue; // try to find IN + return SQLCommand.where; } case "set": { - if (command === SQLCommand.update) { - command = SQLCommand.updateSet; - token = ""; - continue; // try to find WHERE - } - return command; + return SQLCommand.updateSet; } case "in": { - if (command === SQLCommand.where) { - return SQLCommand.whereIn; - } - return command; + return SQLCommand.in; } default: { token = ""; @@ -437,40 +420,27 @@ function detectCommand(query: string): SQLCommand { continue; } if (!quoted) { - token += char; + token = char + token; } } } } if (token) { - switch (command) { - case SQLCommand.none: { - switch (token) { - case "insert": - return SQLCommand.insert; - case "update": - return SQLCommand.update; - case "where": - return SQLCommand.where; - default: - return SQLCommand.none; - } - } - case SQLCommand.update: { - if (token === "set") { - return SQLCommand.updateSet; - } + switch (token) { + case "insert": + return SQLCommand.insert; + case "update": return SQLCommand.update; - } - case SQLCommand.where: { - if (token === "in") { - return SQLCommand.whereIn; - } + case "where": return SQLCommand.where; - } + case "set": + return SQLCommand.updateSet; + case "in": + return SQLCommand.in; + default: + return SQLCommand.none; } } - return command; } @@ -1268,11 +1238,11 @@ class PostgresAdapter const command = detectCommand(query); // 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"); + throw new SyntaxError("Helpers are only allowed for INSERT, UPDATE and IN commands"); } const { columns, value: items } = value as SQLHelper; const columnCount = columns.length; - if (columnCount === 0 && command !== SQLCommand.whereIn) { + if (columnCount === 0 && command !== SQLCommand.in) { throw new SyntaxError(`Cannot ${commandToString(command)} with no columns`); } const lastColumnIndex = columns.length - 1; @@ -1302,8 +1272,6 @@ class PostgresAdapter query += `$${binding_idx++}${k < lastColumnIndex ? ", " : ""}`; if (typeof columnValue === "undefined") { binding_values.push(null); - } else if ($isArray(columnValue)) { - binding_values.push(serializeArray(columnValue, "JSON")); } else { binding_values.push(columnValue); } @@ -1323,19 +1291,13 @@ class PostgresAdapter query += `$${binding_idx++}${j < lastColumnIndex ? ", " : ""}`; if (typeof columnValue === "undefined") { binding_values.push(null); - } else if ($isArray(columnValue)) { - // Handle array values in single fields: - // - JSON/JSONB fields can be an array - // - For dedicated SQL array field types (e.g., INTEGER[], TEXT[]), - // users should use the sql.array() helper instead - binding_values.push(serializeArray(columnValue, "JSON")); } else { binding_values.push(columnValue); } } query += ") "; // the user can add RETURNING * or RETURNING id } - } else if (command === SQLCommand.whereIn) { + } 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"); @@ -1360,8 +1322,6 @@ class PostgresAdapter if (typeof value_from_key === "undefined") { binding_values.push(null); - } else if ($isArray(value_from_key)) { - binding_values.push(serializeArray(value_from_key, "JSON")); } else { binding_values.push(value_from_key); } @@ -1370,8 +1330,6 @@ class PostgresAdapter const value = items[j]; if (typeof value === "undefined") { binding_values.push(null); - } else if ($isArray(value)) { - binding_values.push(serializeArray(value, "JSON")); } else { binding_values.push(value); } @@ -1393,21 +1351,27 @@ class PostgresAdapter if (command === SQLCommand.update) { query += " SET "; } + let hasValues = false; for (let i = 0; i < columnCount; i++) { const column = columns[i]; const columnValue = item[column]; - query += `${this.escapeIdentifier(column)} = $${binding_idx++}${i < lastColumnIndex ? ", " : ""}`; if (typeof columnValue === "undefined") { - binding_values.push(null); - } else { - if ($isArray(columnValue)) { - binding_values.push(serializeArray(columnValue, "JSON")); - } else { - binding_values.push(columnValue); - } + // skip undefined values, this is the expected behavior in JS + continue; } + hasValues = true; + query += `${this.escapeIdentifier(column)} = $${binding_idx++}${i < lastColumnIndex ? ", " : ""}`; + binding_values.push(columnValue); } - query += " "; // the user can add where clause after this + if (query.endsWith(", ")) { + // we got an undefined value at the end, lets remove the last comma + query = query.substring(0, query.length - 2); + } + if (!hasValues) { + throw new SyntaxError("Update needs to have at least one column"); + } + // the user can add where clause after this + query += " "; } } else if (value instanceof SQLArrayParameter) { query += `$${binding_idx++}::${value.arrayType}[] `; @@ -1417,11 +1381,7 @@ class PostgresAdapter if (typeof value === "undefined") { binding_values.push(null); } else { - if ($isArray(value)) { - binding_values.push(serializeArray(value, "JSON")); - } else { - binding_values.push(value); - } + binding_values.push(value); } } } diff --git a/src/js/internal/sql/sqlite.ts b/src/js/internal/sql/sqlite.ts index a123380091..0a150cb79a 100644 --- a/src/js/internal/sql/sqlite.ts +++ b/src/js/internal/sql/sqlite.ts @@ -23,7 +23,7 @@ const enum SQLCommand { update = 1, updateSet = 2, where = 3, - whereIn = 4, + in = 4, none = -1, } @@ -40,7 +40,7 @@ function commandToString(command: SQLCommand): string { case SQLCommand.updateSet: case SQLCommand.update: return "UPDATE"; - case SQLCommand.whereIn: + case SQLCommand.in: case SQLCommand.where: return "WHERE"; default: @@ -132,6 +132,9 @@ function parseSQLQuery(query: string): SQLParsedInfo { firstKeyword = "EXPLAIN"; } else if (matchAsciiIgnoreCase(query, tokenStart, i, "with")) { firstKeyword = "WITH"; + } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { + firstKeyword = "IN"; + command = SQLCommand.in; } } else { // After we have the first keyword, look for other keywords @@ -142,9 +145,7 @@ function parseSQLQuery(query: string): SQLParsedInfo { command = SQLCommand.updateSet; } } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - if (command === SQLCommand.where) { - command = SQLCommand.whereIn; - } + command = SQLCommand.in; } else if (matchAsciiIgnoreCase(query, tokenStart, i, "returning")) { hasReturning = true; } @@ -210,6 +211,9 @@ function parseSQLQuery(query: string): SQLParsedInfo { firstKeyword = "EXPLAIN"; } else if (matchAsciiIgnoreCase(query, tokenStart, i, "with")) { firstKeyword = "WITH"; + } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { + firstKeyword = "IN"; + command = SQLCommand.in; } } else { // After we have the first keyword, look for other keywords @@ -220,9 +224,7 @@ function parseSQLQuery(query: string): SQLParsedInfo { command = SQLCommand.updateSet; } } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - if (command === SQLCommand.where) { - command = SQLCommand.whereIn; - } + command = SQLCommand.in; } else if (matchAsciiIgnoreCase(query, tokenStart, i, "returning")) { hasReturning = true; } @@ -271,6 +273,9 @@ function parseSQLQuery(query: string): SQLParsedInfo { firstKeyword = "EXPLAIN"; } else if (matchAsciiIgnoreCase(query, tokenStart, i, "with")) { firstKeyword = "WITH"; + } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { + firstKeyword = "IN"; + command = SQLCommand.in; } } else { // After we have the first keyword, look for other keywords @@ -281,9 +286,7 @@ function parseSQLQuery(query: string): SQLParsedInfo { command = SQLCommand.updateSet; } } else if (matchAsciiIgnoreCase(query, tokenStart, i, "in")) { - if (command === SQLCommand.where) { - command = SQLCommand.whereIn; - } + command = SQLCommand.in; } else if (matchAsciiIgnoreCase(query, tokenStart, i, "returning")) { hasReturning = true; } @@ -489,7 +492,6 @@ class SQLiteAdapter implements DatabaseAdapter { - return numeric.toJS(globalObject, value); + return JSValue.jsNumber(value); }, .float4, .float8 => { - return numeric.toJS(globalObject, value); + return JSValue.jsNumber(value); }, .json, .jsonb => { @@ -299,7 +299,7 @@ pub const Tag = enum(short) { }, .int4 => { - return numeric.toJS(globalObject, value); + return JSValue.jsNumber(value); }, else => { @@ -342,8 +342,9 @@ pub const Tag = enum(short) { return .int8; } - if (tag.isArrayLike() and try value.getLength(globalObject) > 0) { - return Tag.fromJS(globalObject, try value.getIndex(globalObject, 0)); + if (tag.isArrayLike()) { + // We will JSON.stringify anything else. + return .json; } // Ban these types: @@ -397,7 +398,6 @@ const bun = @import("bun"); const bytea = @import("./bytea.zig"); const date = @import("./date.zig"); const json = @import("./json.zig"); -const numeric = @import("./numeric.zig"); const std = @import("std"); const string = @import("./PostgresString.zig"); const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; diff --git a/src/sql/postgres/types/numeric.zig b/src/sql/postgres/types/numeric.zig deleted file mode 100644 index b9717df4e3..0000000000 --- a/src/sql/postgres/types/numeric.zig +++ /dev/null @@ -1,18 +0,0 @@ -pub const to = 0; -pub const from = [_]short{ 21, 23, 26, 700, 701 }; - -pub fn toJS( - _: *jsc.JSGlobalObject, - value: anytype, -) AnyPostgresError!JSValue { - return JSValue.jsNumber(value); -} - -const bun = @import("bun"); -const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; - -const int_types = @import("./int_types.zig"); -const short = int_types.short; - -const jsc = bun.jsc; -const JSValue = jsc.JSValue; diff --git a/test/js/sql/sql-mysql.helpers.test.ts b/test/js/sql/sql-mysql.helpers.test.ts index dc5d91e3cc..be599c02a0 100644 --- a/test/js/sql/sql-mysql.helpers.test.ts +++ b/test/js/sql/sql-mysql.helpers.test.ts @@ -32,6 +32,40 @@ describeWithContainer( expect(result[0].name).toBe("John"); expect(result[0].age).toBe(30); }); + + test("insert into with select helper with IN", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + { + await sql`INSERT INTO ${sql(random_name)} ${sql({ id: 1, name: "John", age: 30 })}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(30); + } + await sql`CREATE TEMPORARY TABLE ${sql(random_name + "2")} (id int, name text, age int)`; + { + await sql`INSERT INTO ${sql(random_name + "2")} (id, name, age) SELECT id, name, age FROM ${sql(random_name)} WHERE id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name + "2")}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(30); + } + }); + + test("select helper with IN using fragment", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + await sql`INSERT INTO ${sql(random_name)} ${sql({ id: 1, name: "John", age: 30 })}`; + const fragment = sql`id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name)} WHERE ${fragment}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(30); + }); + test("update helper", async () => { await using sql = new SQL({ ...getOptions(), max: 1 }); const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); @@ -64,6 +98,133 @@ describeWithContainer( expect(result[1].age).toBe(18); }); + test("update helper with AND IN", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: "Mary", age: 18 })} WHERE 1=1 AND id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("Mary"); + expect(result[0].age).toBe(18); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Mary"); + expect(result[1].age).toBe(18); + }); + + test("update helper with undefined values", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: "Mary", age: undefined })} WHERE id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("Mary"); + expect(result[0].age).toBe(30); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Mary"); + expect(result[1].age).toBe(25); + }); + test("update helper that starts with undefined values", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: undefined, age: 19 })} WHERE id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(19); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Jane"); + expect(result[1].age).toBe(19); + }); + + test("update helper with undefined values and no columns", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + try { + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: undefined, age: undefined })} WHERE id IN ${sql([1, 2])}`; + expect.unreachable(); + } catch (e) { + expect(e).toBeInstanceOf(SyntaxError); + expect(e.message).toBe("Update needs to have at least one column"); + } + }); + + test("upsert helper", async () => { + await using sql = new SQL({ ...getOptions(), max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql` + CREATE TABLE IF NOT EXISTS ${sql(random_name)} ( + id int PRIMARY KEY, + foo text NOT NULL, + email VARCHAR(255) NOT NULL UNIQUE + ) + `; + + const data = { id: 1, foo: "hello", email: "bunny@bun.com" }; + await sql` + INSERT INTO ${sql(random_name)} ${sql(data)} + ON DUPLICATE KEY UPDATE ${sql(data)} + `; + let id = 0; + { + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBeDefined(); + expect(result[0].foo).toBe("hello"); + expect(result[0].email).toBe("bunny@bun.com"); + id = result.lastInsertRowid; + } + + { + const data = { foo: "hello2", email: "bunny2@bun.com" }; + await sql` + INSERT INTO ${sql(random_name)} ${sql({ id, ...data })} + ON DUPLICATE KEY UPDATE ${sql(data)} + `; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBeDefined(); + expect(result[0].foo).toBe("hello2"); + expect(result[0].email).toBe("bunny2@bun.com"); + } + + { + const data = { foo: "hello3", email: "bunny2@bun.com" }; + await sql` + INSERT INTO ${sql(random_name)} ${sql({ id, ...data })} + ON DUPLICATE KEY UPDATE ${sql(data)} + `; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBeDefined(); + expect(result[0].foo).toBe("hello3"); + expect(result[0].email).toBe("bunny2@bun.com"); + } + }); test("update helper with IN and column name", async () => { await using sql = new SQL({ ...getOptions(), max: 1 }); const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); diff --git a/test/js/sql/sql.test.ts b/test/js/sql/sql.test.ts index 56af346d03..40b49f7ec6 100644 --- a/test/js/sql/sql.test.ts +++ b/test/js/sql/sql.test.ts @@ -117,7 +117,6 @@ if (isDockerEnabled()) { socketProxy.stop(); } }); - test("should handle numeric values with many digits", async () => { await using sql = postgres(options); // handle numbers big than 10,4 with zeros at the end and start, starting with 0. or not @@ -140,7 +139,7 @@ if (isDockerEnabled()) { }); describe("Array helpers", () => { - test("SQL heper should support sql.array", async () => { + test("SQL helper should support sql.array", async () => { await using sql = postgres(options); const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); await sql`CREATE TEMPORARY TABLE ${sql(random_name)} ( @@ -196,9 +195,7 @@ if (isDockerEnabled()) { const [{ id, json }] = await sql`select * from ${sql(random_name)}`; expect(id).toBe(1); - // TODO: we should properly parse the jsonb values here but we are returning the string as is - // internally we are probably trying to JSON.parse the string but it fails because of the array format is different - expect(json).toEqual('{"\\"a\\"","\\"b\\""}'); + expect(json).toEqual(["a", "b"]); }); test("should be able to insert array in fields", async () => { await using sql = postgres(options); @@ -211,9 +208,7 @@ if (isDockerEnabled()) { await sql`insert into ${sql(random_name)} (json) values (${["a", "b"]})`; const [{ id, json }] = await sql`select * from ${sql(random_name)}`; expect(id).toBe(1); - // TODO: we should properly parse the jsonb values here - // internally we are probably trying to JSON.parse the string but it fails because of the array format is different - expect(json).toEqual('{"\\"a\\"","\\"b\\""}'); + expect(json).toEqual(["a", "b"]); }); test("sql.array should support TEXT arrays", async () => { @@ -1027,10 +1022,10 @@ if (isDockerEnabled()) { // expect(result[0].x[2].getTime()).toBe(now.getTime()); // }); - test.todo("Array of Box", async () => { + test("Array of Box", async () => { const result = await sql`select ${"{(1,2),(3,4);(4,5),(6,7)}"}::box[] as x`; - console.log(result); - expect(result[0].x.join(";")).toBe("(1,2);(3,4);(4,5);(6,7)"); + // box type will reorder the values and this is correct + expect(result[0].x).toEqual(["(3,4),(1,2)", "(6,7),(4,5)"]); }); // t('Nested array n2', async() => @@ -11553,6 +11548,38 @@ CREATE TABLE ${table_name} ( expect(result[0].name).toBe("John"); expect(result[0].age).toBe(30); }); + + test("insert into with select helper using where IN", async () => { + await using sql = postgres({ ...options, max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + { + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const result = + await sql`INSERT INTO ${sql(random_name)} ${sql({ id: 1, name: "John", age: 30 })} RETURNING *`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(30); + } + { + const result = + await sql`INSERT INTO ${sql(random_name)} (id, name, age) SELECT id, name, age FROM ${sql(random_name)} WHERE id IN ${sql([1, 2])} RETURNING *`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(30); + } + }); + + test("select helper with IN using fragment", async () => { + await using sql = postgres({ ...options, max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + await sql`INSERT INTO ${sql(random_name)} ${sql({ id: 1, name: "John", age: 30 })}`; + const fragment = sql`id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name)} WHERE ${fragment}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(30); + }); test("update helper", async () => { await using sql = postgres({ ...options, max: 1 }); const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); @@ -11585,6 +11612,143 @@ CREATE TABLE ${table_name} ( expect(result[1].age).toBe(18); }); + test("update helper with undefined values", async () => { + await using sql = postgres({ ...options, max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + const result = + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: "Mary", age: undefined })} WHERE id IN ${sql([1, 2])} RETURNING *`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("Mary"); + expect(result[0].age).toBe(30); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Mary"); + expect(result[1].age).toBe(25); + }); + test("update helper that starts with undefined values", async () => { + await using sql = postgres({ ...options, max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + const result = + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: undefined, age: 19 })} WHERE id IN ${sql([1, 2])} RETURNING *`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(19); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Jane"); + expect(result[1].age).toBe(19); + }); + + test("update helper with undefined values and no columns", async () => { + await using sql = postgres({ ...options, max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + try { + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: undefined, age: undefined })} WHERE id IN ${sql([1, 2])} RETURNING *`; + expect.unreachable(); + } catch (e) { + expect(e).toBeInstanceOf(SyntaxError); + expect(e.message).toBe("Update needs to have at least one column"); + } + }); + + test("upsert helper", async () => { + await using sql = postgres({ ...options, max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql` + CREATE TABLE IF NOT EXISTS ${sql(random_name)} ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + foo text NOT NULL DEFAULT '', + email text NOT NULL UNIQUE + ) + `; + { + const { email, ...data } = { email: "bunny@bun.com", foo: "hello" }; + await sql` + INSERT INTO ${sql(random_name)} + ${sql({ ...data, email })} + ON CONFLICT (email) DO UPDATE + SET ${sql(data)} + `; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBeDefined(); + expect(result[0].foo).toBe("hello"); + expect(result[0].email).toBe("bunny@bun.com"); + } + + { + const { email, ...data } = { email: "bunny@bun.com", foo: "hello2" }; + await sql` + INSERT INTO ${sql(random_name)} + ${sql({ ...data, email })} + ON CONFLICT (email) DO UPDATE + SET ${sql(data)} + `; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBeDefined(); + expect(result[0].foo).toBe("hello2"); + expect(result[0].email).toBe("bunny@bun.com"); + } + }); + + test("update helper with AND IN", async () => { + await using sql = postgres({ ...options, max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + const result = + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: "Mary", age: 18 })} WHERE 1=1 AND id IN ${sql([1, 2])} RETURNING *`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("Mary"); + expect(result[0].age).toBe(18); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Mary"); + expect(result[1].age).toBe(18); + }); + + test("update helper with ANY", async () => { + await using sql = postgres({ ...options, max: 1 }); + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + const result = + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: "Mary", age: 18 })} WHERE id = ANY (${sql.array([1, 2], "int")}) RETURNING *`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("Mary"); + expect(result[0].age).toBe(18); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Mary"); + expect(result[1].age).toBe(18); + }); + test("update helper with IN for strings", async () => { await using sql = postgres({ ...options, max: 1 }); const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); diff --git a/test/js/sql/sqlite-sql.test.ts b/test/js/sql/sqlite-sql.test.ts index 12d9189dc6..dbcdb739a2 100644 --- a/test/js/sql/sqlite-sql.test.ts +++ b/test/js/sql/sqlite-sql.test.ts @@ -1,4 +1,4 @@ -import { SQL } from "bun"; +import { randomUUIDv7, SQL } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"; import { tempDirWithFiles } from "harness"; import { existsSync } from "node:fs"; @@ -1311,6 +1311,130 @@ describe("SQL helpers", () => { expect(results[0].value).toBe("test"); }); + test("insert into with select helper using where IN", async () => { + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + { + await sql`INSERT INTO ${sql(random_name)} ${sql({ id: 1, name: "John", age: 30 })}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(30); + } + await sql`CREATE TEMPORARY TABLE ${sql(random_name + "2")} (id int, name text, age int)`; + { + await sql`INSERT INTO ${sql(random_name + "2")} (id, name, age) SELECT id, name, age FROM ${sql(random_name)} WHERE id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name + "2")}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(30); + } + }); + + test("update helper with undefined values", async () => { + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: "Mary", age: undefined })} WHERE id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("Mary"); + expect(result[0].age).toBe(30); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Mary"); + expect(result[1].age).toBe(25); + }); + test("update helper that starts with undefined values", async () => { + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: undefined, age: 19 })} WHERE id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(19); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Jane"); + expect(result[1].age).toBe(19); + }); + + test("update helper with undefined values and no columns", async () => { + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + try { + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: undefined, age: undefined })} WHERE id IN ${sql([1, 2])}`; + expect.unreachable(); + } catch (e) { + expect(e).toBeInstanceOf(SyntaxError); + expect(e.message).toBe("Update needs to have at least one column"); + } + }); + + test("update helper with IN and column name", async () => { + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: "Mary", age: 18 })} WHERE id IN ${sql(users, "id")}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("Mary"); + expect(result[0].age).toBe(18); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Mary"); + expect(result[1].age).toBe(18); + }); + + test("select helper with IN using fragment", async () => { + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + await sql`INSERT INTO ${sql(random_name)} ${sql({ id: 1, name: "John", age: 30 })}`; + const fragment = sql`id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name)} WHERE ${fragment}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("John"); + expect(result[0].age).toBe(30); + }); + + test("update helper with AND IN", async () => { + const random_name = "test_" + randomUUIDv7("hex").replaceAll("-", ""); + await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (id int, name text, age int)`; + const users = [ + { id: 1, name: "John", age: 30 }, + { id: 2, name: "Jane", age: 25 }, + ]; + await sql`INSERT INTO ${sql(random_name)} ${sql(users)}`; + + await sql`UPDATE ${sql(random_name)} SET ${sql({ name: "Mary", age: 18 })} WHERE 1=1 AND id IN ${sql([1, 2])}`; + const result = await sql`SELECT * FROM ${sql(random_name)}`; + expect(result[0].id).toBe(1); + expect(result[0].name).toBe("Mary"); + expect(result[0].age).toBe(18); + expect(result[1].id).toBe(2); + expect(result[1].name).toBe("Mary"); + expect(result[1].age).toBe(18); + }); + test("file execution", async () => { const dir = tempDirWithFiles("sql-files", { "schema.sql": `