Repository navigation
sql: serialize JS arrays in sql(object) as PostgreSQL array literals #29552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
d12010a
sql: serialize JS arrays in sql(object) as PostgreSQL array literals
robobun 05a6b6a
[autofix.ci] apply automated fixes
autofix-ci[bot] 3c3e09e
address review feedback
robobun 096f29f
sql: add uuid_array (OID 2951) and emit SQL NULL for non-finite Date
robobun 07582a2
sql: serialize jsonb[] elements as JSON, not nested PG array dimensions
robobun 15f2da7
[autofix.ci] apply automated fixes
autofix-ci[bot] 70670c2
sql: decode int[] / float4[] binary results that contain NULL elements
robobun 519fa97
sql: collapse arrayElementTag and trim array_serializer/test boilerplate
robobun 09185a3
test(29551): fall back to local postgres when docker is unavailable
robobun 05d42cb
sql: mark the binary-null-array cell as owning its backing store
robobun 5f84a4f
[autofix.ci] apply automated fixes
autofix-ci[bot] e14be3b
empty: retrigger CI after stale aggregate buildkite/bun status
robobun cdf0424
sql: mark parseArray's ArrayList as owned by the cell
robobun 8d3b35c
empty: retrigger past debian-asan bun-install / cron / loader flakes
robobun bfb2b0b
Revert "sql: mark parseArray's ArrayList as owned by the cell"
robobun 3cc42b1
sql.test: update UUID-array assertion to match new parsed output + gu…
robobun 9970e18
empty: retrigger past expired darwin test shards + bun-install asan f…
robobun 383b5fb
empty: retrigger past debian-asan bun-install shard flake
robobun f70fbf5
Merge remote-tracking branch 'origin/main' into farm/83c18c6c/fix-sql…
robobun 0dc6a43
test(resolve): give load-file-loader-a-lot 30s under ASAN/debug
robobun 52f86e5
sql: collapse Tag.isArray via comptime, trim narrative comments, drop…
robobun 4d4339b
DataCell: drop stray comment; NodeFSStatBinding: scope Windows S_IF* …
robobun 5f22222
test(29551): restore docker-less Postgres fallback; scale loader test…
robobun 9317fde
Merge remote-tracking branch 'origin/main' into farm/83c18c6c/fix-sql…
robobun 5c5dfa2
Merge main; drop redundant NodeFSStatBinding S_IF* scoping
robobun 0b0fe9d
empty: retrigger past test-worker-nested-uncaught + win-aarch64 socke…
robobun c399e90
test(29551): add service-free wire-protocol assertions via mock PG ba…
robobun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| //! Emits a PG text-format array literal (e.g. `{"a","b"}`) for a JS array | ||
| //! bound to an array-typed parameter — used by `sql(object)` expansion. | ||
| //! Mirrors `arrayValueSerializer` in `src/js/internal/sql/postgres.ts`. | ||
| pub const Error = AnyPostgresError || bun.JSError; | ||
|
|
||
| pub fn writeTo( | ||
| globalObject: *jsc.JSGlobalObject, | ||
| value: JSValue, | ||
| element_tag: Tag, | ||
| comptime Context: type, | ||
| writer: protocol.NewWriter(Context), | ||
| ) Error!void { | ||
| const l = try writer.length(); | ||
| try writeArrayLiteral(globalObject, value, element_tag, Context, writer); | ||
| try l.writeExcludingSelf(); | ||
| } | ||
|
|
||
| fn writeArrayLiteral( | ||
| globalObject: *jsc.JSGlobalObject, | ||
| value: JSValue, | ||
| element_tag: Tag, | ||
| comptime Context: type, | ||
| writer: protocol.NewWriter(Context), | ||
| ) Error!void { | ||
| // Both entry points gate on `value.isArray()` before dispatching here — | ||
| // `writeTo` via `writeBind` in `PostgresRequest.zig`, and the recursive | ||
| // call in `writeElement` below. A non-array would be a caller bug. | ||
| bun.assert(value.isArray()); | ||
|
|
||
|
robobun marked this conversation as resolved.
|
||
| var iter = try jsc.JSArrayIterator.init(value, globalObject); | ||
| try writer.write("{"); | ||
| // `box[]` is the sole PG array type whose element delimiter is `;`. | ||
| const delimiter: []const u8 = if (element_tag == .box) ";" else ","; | ||
| var first = true; | ||
| while (try iter.next()) |element| { | ||
| if (!first) try writer.write(delimiter); | ||
| first = false; | ||
| try writeElement(globalObject, element, element_tag, Context, writer); | ||
| } | ||
|
robobun marked this conversation as resolved.
|
||
| try writer.write("}"); | ||
| } | ||
|
|
||
| fn writeElement( | ||
| globalObject: *jsc.JSGlobalObject, | ||
| value: JSValue, | ||
| element_tag: Tag, | ||
| comptime Context: type, | ||
| writer: protocol.NewWriter(Context), | ||
| ) Error!void { | ||
| if (value.isEmptyOrUndefinedOrNull()) { | ||
| try writer.write("null"); | ||
| return; | ||
| } | ||
|
claude[bot] marked this conversation as resolved.
|
||
|
|
||
| // JSON / JSONB elements are JSON values — arrays/objects/primitives all | ||
| // get stringified and embedded as a quoted, escaped string. Must precede | ||
| // the `isArray` branch so e.g. a `jsonb[]` value `[[1,2],[3,4]]` | ||
| // becomes the 1-D literal `{"[1,2]","[3,4]"}` (two jsonb values), not | ||
| // a 2-D PG array `{{"1","2"},{"3","4"}}`. | ||
| if (element_tag == .json or element_tag == .jsonb) { | ||
| var str = bun.String.empty; | ||
| defer str.deref(); | ||
| try value.jsonStringifyFast(globalObject, &str); | ||
| const slice = str.toUTF8WithoutRef(bun.default_allocator); | ||
| defer slice.deinit(); | ||
| try writer.write("\""); | ||
| try writeEscaped(Context, writer, slice.slice()); | ||
| try writer.write("\""); | ||
| return; | ||
| } | ||
|
|
||
| if (value.isArray()) { | ||
| try writeArrayLiteral(globalObject, value, element_tag, Context, writer); | ||
| return; | ||
| } | ||
|
|
||
| // bytea[] elements are Buffers — encode as `"\x<hex>"`. Typed arrays | ||
| // are NOT matched here; they fall through to the array/default path so | ||
| // they serialize as their own array of numeric elements. This mirrors | ||
| // `arrayValueSerializer` in `src/js/internal/sql/postgres.ts` which | ||
| // gates on `Buffer.isBuffer(value)`, not on `ArrayBuffer.isView`. | ||
| if (element_tag == .bytea and value.isBuffer(globalObject)) { | ||
| if (value.asArrayBuffer(globalObject)) |buf| { | ||
| try writer.write("\"\\\\x"); | ||
| try writeHex(Context, writer, buf.byteSlice()); | ||
| try writer.write("\""); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| if (element_tag == .bool and value.isBoolean()) { | ||
| try writer.write(if (value.toBoolean()) "t" else "f"); | ||
| return; | ||
| } | ||
|
|
||
| // Date -> ISO 8601 in UTC, quoted. `toISOString` returns an empty slice | ||
| // for non-finite dates (NaN, ±Infinity); emit NULL rather than falling | ||
| // through to `String.fromJS`, which would yield `"Invalid Date"` — PG | ||
| // rejects that as invalid input syntax for any date/timestamp column. | ||
| if (value.isDate()) { | ||
| var iso_buf: [29]u8 = undefined; | ||
| const iso = value.toISOString(globalObject, &iso_buf); | ||
| if (iso.len == 0) { | ||
| try writer.write("null"); | ||
| return; | ||
| } | ||
| try writer.write("\""); | ||
| try writer.write(iso); | ||
| try writer.write("\""); | ||
| return; | ||
| } | ||
|
|
||
| var str = try bun.String.fromJS(value, globalObject); | ||
| if (str.tag == .Dead) return error.OutOfMemory; | ||
| defer str.deref(); | ||
| const slice = str.toUTF8WithoutRef(bun.default_allocator); | ||
| defer slice.deinit(); | ||
|
|
||
| try writer.write("\""); | ||
| try writeEscaped(Context, writer, slice.slice()); | ||
| try writer.write("\""); | ||
| } | ||
|
|
||
| /// PG array-literal element escape: backslash and double-quote are prefixed | ||
| /// with a backslash. Mirrors `arrayEscape` in | ||
| /// `src/js/internal/sql/postgres.ts`. | ||
| fn writeEscaped( | ||
| comptime Context: type, | ||
| writer: protocol.NewWriter(Context), | ||
| bytes: []const u8, | ||
| ) AnyPostgresError!void { | ||
| var start: usize = 0; | ||
| for (bytes, 0..) |c, i| { | ||
| switch (c) { | ||
| '\\', '"' => { | ||
| if (i > start) try writer.write(bytes[start..i]); | ||
| const escape_pair: [2]u8 = .{ '\\', c }; | ||
| try writer.write(&escape_pair); | ||
| start = i + 1; | ||
| }, | ||
| else => {}, | ||
| } | ||
| } | ||
| if (start < bytes.len) try writer.write(bytes[start..]); | ||
| } | ||
|
|
||
| const hex_digits = "0123456789abcdef"; | ||
|
|
||
| fn writeHex( | ||
| comptime Context: type, | ||
| writer: protocol.NewWriter(Context), | ||
| bytes: []const u8, | ||
| ) AnyPostgresError!void { | ||
| var buf: [2]u8 = undefined; | ||
| for (bytes) |b| { | ||
| buf[0] = hex_digits[b >> 4]; | ||
| buf[1] = hex_digits[b & 0xf]; | ||
| try writer.write(&buf); | ||
| } | ||
| } | ||
|
|
||
| const bun = @import("bun"); | ||
| const protocol = @import("../PostgresProtocol.zig"); | ||
| const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError; | ||
| const Tag = @import("./Tag.zig").Tag; | ||
|
|
||
| const jsc = bun.jsc; | ||
| const JSValue = jsc.JSValue; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.