diff --git a/packages/sql/src/sql.test.ts b/packages/sql/src/sql.test.ts index 3926e90..8474c39 100644 --- a/packages/sql/src/sql.test.ts +++ b/packages/sql/src/sql.test.ts @@ -24,6 +24,12 @@ describe("sql", () => { expect(statement.values).toEqual([]); }); + test("interpolating undefined adds it as null rather than as a parameter", () => { + const statement = sql`SELECT ${undefined} FROM dual`; + expect(statement.text).toEqual("SELECT NULL FROM dual"); + expect(statement.values).toEqual([]); + }); + test("interpolating true and false adds them literally rather than as a parameter", () => { const statement = sql`SELECT ${true}, ${false} FROM dual`; expect(statement.text).toEqual("SELECT TRUE, FALSE FROM dual"); diff --git a/packages/sql/src/sql.ts b/packages/sql/src/sql.ts index c202827..daa649a 100644 --- a/packages/sql/src/sql.ts +++ b/packages/sql/src/sql.ts @@ -17,7 +17,6 @@ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -import { zip } from "es-toolkit"; import pg from "pg"; import { format } from "sql-formatter"; import { ZodType, z } from "zod"; @@ -42,12 +41,35 @@ const joinFragments = ( }; }; +/** + * Process template literal fragments and values into an ExpandedFragment. + * Template literals always have one more fragment than values, so we pair + * each value with its preceding fragment, then append the final fragment. + */ +const processTemplateParts = ( + fragments: readonly string[], + values: readonly unknown[], +): ExpandedFragment => { + const fragmentsCopy = [...fragments]; + const lastFragment = fragmentsCopy.pop()!; + const pairs = fragmentsCopy.map( + (frag, i) => [frag, values[i]] as [string, unknown], + ); + + const result = pairs + .map(expandFragment) + .reduce(joinFragments, { text: [""], values: [] }); + + result.text[result.text.length - 1]! += lastFragment; + return result; +}; + const expandFragment = ([fragment, value]: [ string, unknown, ]): ExpandedFragment => { if (value === undefined) { - return { text: [fragment], values: [] }; + return { text: [fragment + "NULL"], values: [] }; } else if (value === null) { return { text: [fragment + "NULL"], values: [] }; } else if (value === true) { @@ -55,12 +77,7 @@ const expandFragment = ([fragment, value]: [ } else if (value === false) { return { text: [fragment + "FALSE"], values: [] }; } else if (value instanceof SQLStatement) { - const expanded = zip(value._text, value._values) - .map(expandFragment) - .reduce(joinFragments, { - text: [""], - values: [], - }); + const expanded = processTemplateParts(value._text, value._values); return { text: [fragment + expanded.text[0]!, ...expanded.text.slice(1)], values: [...expanded.values], @@ -126,10 +143,8 @@ function sql( return (fragments, ...values) => sql(fragments, ...values).withSchema(fragmentsOrSchema); } - const result = zip(fragmentsOrSchema, values) - .map(expandFragment) - .reduce(joinFragments, { text: [""], values: [] }); + const result = processTemplateParts(fragmentsOrSchema, values); return new SQLStatement(result.text, result.values); }