Skip to content

Commit 2e08299

Browse files
rsslldnphyclaude
andauthored
Fix NULL being appended to SQL statements with no interpolations (#341)
The zip function from es-toolkit pads arrays of different lengths with undefined. Since template literals always have one more fragment than values, when there were no interpolated values, zip would pair the single fragment with undefined, which then got converted to "NULL". Extracted a processTemplateParts helper that properly handles the template literal structure by pairing each value with its preceding fragment and appending the final fragment separately. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b45a180 commit 2e08299

2 files changed

Lines changed: 32 additions & 11 deletions

File tree

packages/sql/src/sql.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ describe("sql", () => {
2424
expect(statement.values).toEqual([]);
2525
});
2626

27+
test("interpolating undefined adds it as null rather than as a parameter", () => {
28+
const statement = sql`SELECT ${undefined} FROM dual`;
29+
expect(statement.text).toEqual("SELECT NULL FROM dual");
30+
expect(statement.values).toEqual([]);
31+
});
32+
2733
test("interpolating true and false adds them literally rather than as a parameter", () => {
2834
const statement = sql`SELECT ${true}, ${false} FROM dual`;
2935
expect(statement.text).toEqual("SELECT TRUE, FALSE FROM dual");

packages/sql/src/sql.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1818
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1919
*/
20-
import { zip } from "es-toolkit";
2120
import pg from "pg";
2221
import { format } from "sql-formatter";
2322
import { ZodType, z } from "zod";
@@ -42,25 +41,43 @@ const joinFragments = (
4241
};
4342
};
4443

44+
/**
45+
* Process template literal fragments and values into an ExpandedFragment.
46+
* Template literals always have one more fragment than values, so we pair
47+
* each value with its preceding fragment, then append the final fragment.
48+
*/
49+
const processTemplateParts = (
50+
fragments: readonly string[],
51+
values: readonly unknown[],
52+
): ExpandedFragment => {
53+
const fragmentsCopy = [...fragments];
54+
const lastFragment = fragmentsCopy.pop()!;
55+
const pairs = fragmentsCopy.map(
56+
(frag, i) => [frag, values[i]] as [string, unknown],
57+
);
58+
59+
const result = pairs
60+
.map(expandFragment)
61+
.reduce(joinFragments, { text: [""], values: [] });
62+
63+
result.text[result.text.length - 1]! += lastFragment;
64+
return result;
65+
};
66+
4567
const expandFragment = ([fragment, value]: [
4668
string,
4769
unknown,
4870
]): ExpandedFragment => {
4971
if (value === undefined) {
50-
return { text: [fragment], values: [] };
72+
return { text: [fragment + "NULL"], values: [] };
5173
} else if (value === null) {
5274
return { text: [fragment + "NULL"], values: [] };
5375
} else if (value === true) {
5476
return { text: [fragment + "TRUE"], values: [] };
5577
} else if (value === false) {
5678
return { text: [fragment + "FALSE"], values: [] };
5779
} else if (value instanceof SQLStatement) {
58-
const expanded = zip(value._text, value._values)
59-
.map(expandFragment)
60-
.reduce(joinFragments, {
61-
text: [""],
62-
values: [],
63-
});
80+
const expanded = processTemplateParts(value._text, value._values);
6481
return {
6582
text: [fragment + expanded.text[0]!, ...expanded.text.slice(1)],
6683
values: [...expanded.values],
@@ -126,10 +143,8 @@ function sql<ResultType extends pg.QueryResultRow = pg.QueryResultRow>(
126143
return (fragments, ...values) =>
127144
sql<ResultType>(fragments, ...values).withSchema(fragmentsOrSchema);
128145
}
129-
const result = zip(fragmentsOrSchema, values)
130-
.map(expandFragment)
131-
.reduce(joinFragments, { text: [""], values: [] });
132146

147+
const result = processTemplateParts(fragmentsOrSchema, values);
133148
return new SQLStatement<ResultType>(result.text, result.values);
134149
}
135150

0 commit comments

Comments
 (0)