Documentation
¶
Overview ¶
Package sqlstring provides MySQL-style value and identifier escaping and query formatting. It is a Go port of the npm "sqlstring" library (the escaping engine used by the popular mysqljs/mysql driver), reimplemented with only the Go standard library. Its purpose is to let callers build MySQL statements from dynamic data without opening the door to SQL injection.
The package is used whenever an application composes SQL text that embeds user-supplied or otherwise untrusted values and cannot (or does not wish to) rely solely on the database driver's own parameter binding. Rather than concatenating raw values into a query, callers pass those values through Escape (for data) or EscapeID (for identifiers), or let Format do both at once while filling placeholders. The escaped output is safe to splice into a query string because every value is turned into a self-contained, correctly quoted SQL literal.
Escape converts a Go value into a SQL literal. NULL is produced for nil (and nil pointers); booleans render as true/false; every integer and floating-point type renders as its numeric text; strings are single-quoted with dangerous characters escaped; []byte becomes an X'..' hex blob literal; time.Time becomes a quoted 'YYYY-MM-DD HH:MM:SS' timestamp; and slices or arrays are rendered as a parenthesized, comma-joined list of their escaped elements (ideal for IN clauses). Pointers are dereferenced and any other type is formatted with fmt and single-quoted. The critical step is quoteString, which wraps a string in single quotes and backslash-escapes the characters MySQL treats specially inside a string literal: NUL, backspace, tab, newline, carriage return, Ctrl-Z, the double quote, the single quote, and the backslash itself. Because an attacker's quote characters are neutralized this way, injected text can never break out of the literal and be interpreted as SQL syntax.
EscapeID makes an identifier (a table or column name) safe by wrapping it in backticks and doubling any embedded backtick, so a malicious name cannot close the quoting early. A dotted identifier such as "table.col" is split on the dot and each segment is quoted independently, yielding "`table`.`col`" so that qualified names keep working. This is the identifier counterpart to Escape's value quoting and is what the "??" placeholder uses.
Format performs placeholder substitution left to right. A "?" placeholder is replaced with the Escape of the next argument, and a "??" placeholder is replaced with the EscapeID of the next argument (coercing non-strings via fmt first). Arguments are consumed in order; if there are more placeholders than arguments the surplus placeholders are left untouched, and if there are more arguments than placeholders the surplus arguments are ignored. Because values and identifiers are always escaped as they are substituted, a query built with Format is injection-safe by construction: untrusted input becomes data or a quoted identifier, never executable SQL. The behavior tracks the npm original; the API is adapted to Go by taking a []any of arguments and returning a string instead of accepting a variadic JavaScript array.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Escape ¶
Escape returns a safely-quoted SQL literal for val suitable for MySQL. It handles nil (NULL), booleans, numeric types, strings, []byte, time.Time and slices/arrays (rendered as a parenthesized, comma-joined list). Unknown types are formatted with fmt and single-quoted.
Example ¶
ExampleEscape turns individual Go values into safely quoted SQL literals. Numbers render as bare numeric text, booleans as true/false, and nil becomes NULL, while strings are wrapped in single quotes with any dangerous characters backslash-escaped. The final case is the important one for injection safety: a string containing a single quote cannot terminate the literal early because the quote is escaped, so the malicious "OR '1'='1" fragment stays inert data rather than becoming SQL syntax. Each printed line shows the literal that would be spliced into a query.
package main
import (
"fmt"
"github.com/malcolmston/express/sqlstring"
)
func main() {
fmt.Println(sqlstring.Escape(42))
fmt.Println(sqlstring.Escape(true))
fmt.Println(sqlstring.Escape(nil))
fmt.Println(sqlstring.Escape("bob"))
fmt.Println(sqlstring.Escape("x' OR '1'='1"))
}
Output: 42 true NULL 'bob' 'x\' OR \'1\'=\'1'
Example (Types) ¶
ExampleEscape_types shows the richer values Escape understands beyond simple scalars. A []byte is rendered as an X'..' hexadecimal blob literal, a time.Time becomes a quoted 'YYYY-MM-DD HH:MM:SS' timestamp in its own location, and a slice or array is rendered as a parenthesized, comma-joined list of its escaped elements. The slice form is exactly what an IN clause needs, and because every element is escaped recursively, mixing numbers, strings, and NULL in one list stays safe. This example prints one value of each kind.
package main
import (
"fmt"
"time"
"github.com/malcolmston/express/sqlstring"
)
func main() {
fmt.Println(sqlstring.Escape([]byte{0xde, 0xad, 0xbe, 0xef}))
fmt.Println(sqlstring.Escape(time.Date(2026, 7, 4, 13, 5, 9, 0, time.UTC)))
fmt.Println(sqlstring.Escape([]any{1, "two", nil}))
}
Output: X'deadbeef' '2026-07-04 13:05:09' (1, 'two', NULL)
func EscapeID ¶
EscapeID wraps an identifier in backticks, doubling any embedded backticks so the result is a single safe MySQL identifier. A dotted identifier such as "table.col" is quoted per-segment: "`table`.`col`".
Example ¶
ExampleEscapeID makes a table or column name safe to embed in a query. The identifier is wrapped in backticks, and any backtick already inside the name is doubled so a crafted name cannot close the quoting early and inject SQL. A dotted identifier is treated as a qualified name: it is split on the dot and each segment is quoted independently, so "table.col" becomes "`table`.`col`". This example shows a plain name, a name containing a backtick, and a qualified name. Identifier escaping is the counterpart to value escaping and is what the "??" placeholder uses.
package main
import (
"fmt"
"github.com/malcolmston/express/sqlstring"
)
func main() {
fmt.Println(sqlstring.EscapeID("users"))
fmt.Println(sqlstring.EscapeID("weird`name"))
fmt.Println(sqlstring.EscapeID("db.users"))
}
Output: `users` `weird``name` `db`.`users`
func Format ¶
Format substitutes placeholders in sql left-to-right with values from args. A "?" placeholder is replaced with the escaped value (via Escape) and a "??" placeholder is replaced with the escaped identifier (via EscapeID). Leftover args are ignored; leftover placeholders are left in place.
Example ¶
ExampleFormat fills placeholders in a query template from left to right. A "?" placeholder is replaced with the Escape of the next argument and a "??" placeholder is replaced with the EscapeID of the next argument, so the table name is backtick-quoted while the values are single-quoted or rendered as numbers. Because substitution always escapes as it goes, the untrusted name "bob" is embedded as data and could not inject SQL even if it contained quote characters. The result is a complete, injection-safe statement ready to send to MySQL. This example builds a simple SELECT with one identifier and two values.
package main
import (
"fmt"
"github.com/malcolmston/express/sqlstring"
)
func main() {
q := sqlstring.Format(
"SELECT * FROM ?? WHERE id = ? AND name = ?",
[]any{"users", 5, "bob"},
)
fmt.Println(q)
}
Output: SELECT * FROM `users` WHERE id = 5 AND name = 'bob'
Example (Leftovers) ¶
ExampleFormat_leftovers documents how Format handles a mismatch between the number of placeholders and the number of arguments. When there are fewer arguments than placeholders, the surplus placeholders are left in the output untouched rather than causing a panic, as the "b=?" fragment shows. When the argument list is empty the template is returned verbatim. This forgiving behavior mirrors the npm original and makes partial formatting predictable. The example prints both cases so the contract is visible.
package main
import (
"fmt"
"github.com/malcolmston/express/sqlstring"
)
func main() {
fmt.Println(sqlstring.Format("a=? b=?", []any{1}))
fmt.Println(sqlstring.Format("no placeholders", nil))
}
Output: a=1 b=? no placeholders
Types ¶
This section is empty.