diff --git a/docs/howto/named_parameters.md b/docs/howto/named_parameters.md index f2af4352e9..98d27df283 100644 --- a/docs/howto/named_parameters.md +++ b/docs/howto/named_parameters.md @@ -56,3 +56,34 @@ SET END RETURNING *; ``` + +## Nullable parameters + +sqlc infers the nullability of any specified parameters, and often does exactly +what you want. If you want finer control over the nullability of your +parameters, you may use `sql.narg()` (**n**ullable arg) to override the default +behavior. Using `sql.narg` tells sqlc to ignore whatever nullability it has +inferred and generate a nullable parameter instead. There is no nullable +equivalent of the `@` syntax. + +Here is an example that uses a single query to allow updating an author's +name, bio or both. + +```sql +-- name: UpdateAuthor :one +UPDATE author +SET + name = coalesce(sqlc.narg('name'), name), + bio = coalesce(sqlc.narg('bio'), bio) +WHERE id = sqlc.arg('id'); +``` + +The following code is generated: + +```go +type UpdateAuthorParams struct { + Name sql.NullString + Bio sql.NullString + ID int64 +} +``` diff --git a/examples/authors/mysql/query.sql b/examples/authors/mysql/query.sql index c3b5866149..88067c8ecd 100644 --- a/examples/authors/mysql/query.sql +++ b/examples/authors/mysql/query.sql @@ -10,9 +10,16 @@ ORDER BY name; INSERT INTO authors ( name, bio ) VALUES ( - ?, ? + ?, ? ); /* name: DeleteAuthor :exec */ DELETE FROM authors WHERE id = ?; + +/* name: UpdateAuthor :exec */ +UPDATE authors +SET + name = coalesce(sqlc.narg('name'), name), + bio = coalesce(sqlc.narg('bio'), bio) +WHERE id = sqlc.arg('id'); diff --git a/examples/authors/mysql/query.sql.go b/examples/authors/mysql/query.sql.go index 7776498157..fd9970b166 100644 --- a/examples/authors/mysql/query.sql.go +++ b/examples/authors/mysql/query.sql.go @@ -14,7 +14,7 @@ const createAuthor = `-- name: CreateAuthor :execresult INSERT INTO authors ( name, bio ) VALUES ( - ?, ? + ?, ? ) ` @@ -76,3 +76,22 @@ func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { } return items, nil } + +const updateAuthor = `-- name: UpdateAuthor :exec +UPDATE authors +SET + name = coalesce(?, name), + bio = coalesce(?, bio) +WHERE id = ? +` + +type UpdateAuthorParams struct { + Name sql.NullString + Bio sql.NullString + ID int64 +} + +func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) error { + _, err := q.db.ExecContext(ctx, updateAuthor, arg.Name, arg.Bio, arg.ID) + return err +} diff --git a/examples/authors/postgresql/query.sql b/examples/authors/postgresql/query.sql index 75e38b2caf..5a91f7932a 100644 --- a/examples/authors/postgresql/query.sql +++ b/examples/authors/postgresql/query.sql @@ -17,3 +17,10 @@ RETURNING *; -- name: DeleteAuthor :exec DELETE FROM authors WHERE id = $1; + +-- name: UpdateAuthor :exec +UPDATE authors +SET + name = coalesce(sqlc.narg('name'), name), + bio = coalesce(sqlc.narg('bio'), bio) +WHERE id = sqlc.arg('id'); diff --git a/examples/authors/postgresql/query.sql.go b/examples/authors/postgresql/query.sql.go index 059af25236..a8556a2742 100644 --- a/examples/authors/postgresql/query.sql.go +++ b/examples/authors/postgresql/query.sql.go @@ -80,3 +80,22 @@ func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) { } return items, nil } + +const updateAuthor = `-- name: UpdateAuthor :exec +UPDATE authors +SET + name = coalesce($1, name), + bio = coalesce($2, bio) +WHERE id = $3 +` + +type UpdateAuthorParams struct { + Name sql.NullString + Bio sql.NullString + ID int64 +} + +func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) error { + _, err := q.db.ExecContext(ctx, updateAuthor, arg.Name, arg.Bio, arg.ID) + return err +} diff --git a/examples/kotlin/src/main/kotlin/com/example/authors/mysql/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/authors/mysql/Queries.kt index d9362b0aca..53f6c39f32 100644 --- a/examples/kotlin/src/main/kotlin/com/example/authors/mysql/Queries.kt +++ b/examples/kotlin/src/main/kotlin/com/example/authors/mysql/Queries.kt @@ -21,5 +21,11 @@ interface Queries { @Throws(SQLException::class) fun listAuthors(): List + @Throws(SQLException::class) + fun updateAuthor( + name: String?, + bio: String?, + id: Long) + } diff --git a/examples/kotlin/src/main/kotlin/com/example/authors/mysql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/authors/mysql/QueriesImpl.kt index cd479e73a8..d28c228a8f 100644 --- a/examples/kotlin/src/main/kotlin/com/example/authors/mysql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/authors/mysql/QueriesImpl.kt @@ -12,7 +12,7 @@ const val createAuthor = """-- name: createAuthor :execresult INSERT INTO authors ( name, bio ) VALUES ( - ?, ? + ?, ? ) """ @@ -31,6 +31,14 @@ SELECT id, name, bio FROM authors ORDER BY name """ +const val updateAuthor = """-- name: updateAuthor :exec +UPDATE authors +SET + name = coalesce(?, name), + bio = coalesce(?, bio) +WHERE id = ? +""" + class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) @@ -96,5 +104,19 @@ class QueriesImpl(private val conn: Connection) : Queries { } } + @Throws(SQLException::class) + override fun updateAuthor( + name: String?, + bio: String?, + id: Long) { + conn.prepareStatement(updateAuthor).use { stmt -> + stmt.setString(1, name) + stmt.setString(2, bio) + stmt.setLong(3, id) + + stmt.execute() + } + } + } diff --git a/examples/kotlin/src/main/kotlin/com/example/authors/postgresql/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/authors/postgresql/Queries.kt index f364aa29ca..473dc391cd 100644 --- a/examples/kotlin/src/main/kotlin/com/example/authors/postgresql/Queries.kt +++ b/examples/kotlin/src/main/kotlin/com/example/authors/postgresql/Queries.kt @@ -21,5 +21,11 @@ interface Queries { @Throws(SQLException::class) fun listAuthors(): List + @Throws(SQLException::class) + fun updateAuthor( + name: String?, + bio: String?, + id: Long) + } diff --git a/examples/kotlin/src/main/kotlin/com/example/authors/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/authors/postgresql/QueriesImpl.kt index b02857aa68..432d94999a 100644 --- a/examples/kotlin/src/main/kotlin/com/example/authors/postgresql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/authors/postgresql/QueriesImpl.kt @@ -32,6 +32,14 @@ SELECT id, name, bio FROM authors ORDER BY name """ +const val updateAuthor = """-- name: updateAuthor :exec +UPDATE authors +SET + name = coalesce(?, name), + bio = coalesce(?, bio) +WHERE id = ? +""" + class QueriesImpl(private val conn: Connection) : Queries { @Throws(SQLException::class) @@ -103,5 +111,19 @@ class QueriesImpl(private val conn: Connection) : Queries { } } + @Throws(SQLException::class) + override fun updateAuthor( + name: String?, + bio: String?, + id: Long) { + conn.prepareStatement(updateAuthor).use { stmt -> + stmt.setString(1, name) + stmt.setString(2, bio) + stmt.setLong(3, id) + + stmt.execute() + } + } + } diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Queries.kt index c0dda48898..6e2a15bd45 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Queries.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/Queries.kt @@ -49,8 +49,8 @@ interface Queries { fun updateBookISBN( title: String, tags: List, - isbn: String, - bookId: Int) + bookId: Int, + isbn: String) } diff --git a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt index 2b326102a0..2e116dd67c 100644 --- a/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/booktest/postgresql/QueriesImpl.kt @@ -103,7 +103,7 @@ class QueriesImpl(private val conn: Connection) : Queries { results.getString(2), results.getString(3), results.getString(4), - (results.getArray(5).array as Array).toList() + (results.getArray(5).array as? Array<*>)?.filterIsInstance()!!.toList() )) } ret @@ -127,7 +127,7 @@ class QueriesImpl(private val conn: Connection) : Queries { results.getString(5), results.getInt(6), results.getObject(7, OffsetDateTime::class.java), - (results.getArray(8).array as Array).toList() + (results.getArray(8).array as? Array<*>)?.filterIsInstance()!!.toList() )) } ret @@ -184,7 +184,7 @@ class QueriesImpl(private val conn: Connection) : Queries { results.getString(5), results.getInt(6), results.getObject(7, OffsetDateTime::class.java), - (results.getArray(8).array as Array).toList() + (results.getArray(8).array as? Array<*>)?.filterIsInstance()!!.toList() ) if (results.next()) { throw SQLException("expected one row in result set, but got many") @@ -239,7 +239,7 @@ class QueriesImpl(private val conn: Connection) : Queries { results.getString(5), results.getInt(6), results.getObject(7, OffsetDateTime::class.java), - (results.getArray(8).array as Array).toList() + (results.getArray(8).array as? Array<*>)?.filterIsInstance()!!.toList() ) if (results.next()) { throw SQLException("expected one row in result set, but got many") @@ -266,13 +266,13 @@ class QueriesImpl(private val conn: Connection) : Queries { override fun updateBookISBN( title: String, tags: List, - isbn: String, - bookId: Int) { + bookId: Int, + isbn: String) { conn.prepareStatement(updateBookISBN).use { stmt -> stmt.setString(1, title) stmt.setArray(2, conn.createArrayOf("pg_catalog.varchar", tags.toTypedArray())) - stmt.setString(3, isbn) - stmt.setInt(4, bookId) + stmt.setInt(3, bookId) + stmt.setString(4, isbn) stmt.execute() } diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/postgresql/Queries.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/postgresql/Queries.kt index 1af14cc4d8..507ac683b5 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/postgresql/Queries.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/postgresql/Queries.kt @@ -40,10 +40,10 @@ interface Queries { fun listVenues(city: String): List @Throws(SQLException::class) - fun updateCityName(name: String, slug: String) + fun updateCityName(slug: String, name: String) @Throws(SQLException::class) - fun updateVenueName(name: String, slug: String): Int? + fun updateVenueName(slug: String, name: String): Int? @Throws(SQLException::class) fun venueCountByCity(): List diff --git a/examples/kotlin/src/main/kotlin/com/example/ondeck/postgresql/QueriesImpl.kt b/examples/kotlin/src/main/kotlin/com/example/ondeck/postgresql/QueriesImpl.kt index 244c568229..a417d3d007 100644 --- a/examples/kotlin/src/main/kotlin/com/example/ondeck/postgresql/QueriesImpl.kt +++ b/examples/kotlin/src/main/kotlin/com/example/ondeck/postgresql/QueriesImpl.kt @@ -160,7 +160,6 @@ class QueriesImpl(private val conn: Connection) : Queries { override fun deleteVenue(slug: String) { conn.prepareStatement(deleteVenue).use { stmt -> stmt.setString(1, slug) - stmt.setString(2, slug) stmt.execute() } @@ -199,13 +198,13 @@ class QueriesImpl(private val conn: Connection) : Queries { val ret = Venue( results.getInt(1), Status.lookup(results.getString(2))!!, - (results.getArray(3).array as Array).map { v -> Status.lookup(v)!! }.toList(), + (results.getArray(3).array as? Array<*>)?.filterIsInstance()!!.map { v -> Status.lookup(v)!! }.toList(), results.getString(4), results.getString(5), results.getString(6), results.getString(7), results.getString(8), - (results.getArray(9).array as Array).toList(), + (results.getArray(9).array as? Array<*>)?.filterIsInstance()!!.toList(), results.getObject(10, LocalDateTime::class.java) ) if (results.next()) { @@ -242,13 +241,13 @@ class QueriesImpl(private val conn: Connection) : Queries { ret.add(Venue( results.getInt(1), Status.lookup(results.getString(2))!!, - (results.getArray(3).array as Array).map { v -> Status.lookup(v)!! }.toList(), + (results.getArray(3).array as? Array<*>)?.filterIsInstance()!!.map { v -> Status.lookup(v)!! }.toList(), results.getString(4), results.getString(5), results.getString(6), results.getString(7), results.getString(8), - (results.getArray(9).array as Array).toList(), + (results.getArray(9).array as? Array<*>)?.filterIsInstance()!!.toList(), results.getObject(10, LocalDateTime::class.java) )) } @@ -257,20 +256,20 @@ class QueriesImpl(private val conn: Connection) : Queries { } @Throws(SQLException::class) - override fun updateCityName(name: String, slug: String) { + override fun updateCityName(slug: String, name: String) { conn.prepareStatement(updateCityName).use { stmt -> - stmt.setString(1, name) - stmt.setString(2, slug) + stmt.setString(1, slug) + stmt.setString(2, name) stmt.execute() } } @Throws(SQLException::class) - override fun updateVenueName(name: String, slug: String): Int? { + override fun updateVenueName(slug: String, name: String): Int? { return conn.prepareStatement(updateVenueName).use { stmt -> - stmt.setString(1, name) - stmt.setString(2, slug) + stmt.setString(1, slug) + stmt.setString(2, name) val results = stmt.executeQuery() if (!results.next()) { diff --git a/examples/python/src/authors/query.py b/examples/python/src/authors/query.py index db99918720..28fcb20ae4 100644 --- a/examples/python/src/authors/query.py +++ b/examples/python/src/authors/query.py @@ -38,6 +38,15 @@ """ +UPDATE_AUTHOR = """-- name: update_author \\:exec +UPDATE authors +SET + name = coalesce(:p1, name), + bio = coalesce(:p2, bio) +WHERE id = :p3 +""" + + class Querier: def __init__(self, conn: sqlalchemy.engine.Connection): self._conn = conn @@ -74,6 +83,9 @@ def list_authors(self) -> Iterator[models.Author]: bio=row[2], ) + def update_author(self, *, name: Optional[str], bio: Optional[str], id: int) -> None: + self._conn.execute(sqlalchemy.text(UPDATE_AUTHOR), {"p1": name, "p2": bio, "p3": id}) + class AsyncQuerier: def __init__(self, conn: sqlalchemy.ext.asyncio.AsyncConnection): @@ -110,3 +122,6 @@ async def list_authors(self) -> AsyncIterator[models.Author]: name=row[1], bio=row[2], ) + + async def update_author(self, *, name: Optional[str], bio: Optional[str], id: int) -> None: + await self._conn.execute(sqlalchemy.text(UPDATE_AUTHOR), {"p1": name, "p2": bio, "p3": id}) diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index cea706d5d9..b332759b06 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -171,9 +171,6 @@ func Generate(ctx context.Context, e Env, dir, filename string, stderr io.Writer name = combo.Go.Package lang = "golang" } else if sql.Gen.Kotlin != nil { - if sql.Engine == config.EnginePostgreSQL { - parseOpts.UsePositionalParameters = true - } lang = "kotlin" name = combo.Kotlin.Package } else if sql.Gen.Python != nil { diff --git a/internal/codegen/kotlin/gen.go b/internal/codegen/kotlin/gen.go index 275b76c2b2..2507940d89 100644 --- a/internal/codegen/kotlin/gen.go +++ b/internal/codegen/kotlin/gen.go @@ -135,13 +135,13 @@ func (v Params) Bindings() string { func jdbcGet(t ktType, idx int) string { if t.IsEnum && t.IsArray { - return fmt.Sprintf(`(results.getArray(%d).array as Array).map { v -> %s.lookup(v)!! }.toList()`, idx, t.Name) + return fmt.Sprintf(`(results.getArray(%d).array as? Array<*>)?.filterIsInstance()!!.map { v -> %s.lookup(v)!! }.toList()`, idx, t.Name) } if t.IsEnum { return fmt.Sprintf("%s.lookup(results.getString(%d))!!", t.Name, idx) } if t.IsArray { - return fmt.Sprintf(`(results.getArray(%d).array as Array<%s>).toList()`, idx, t.Name) + return fmt.Sprintf(`(results.getArray(%d).array as? Array<*>)?.filterIsInstance<%s>()!!.toList()`, idx, t.Name) } if t.IsTime() { return fmt.Sprintf(`results.getObject(%d, %s::class.java)`, idx, t.Name) diff --git a/internal/compiler/parse.go b/internal/compiler/parse.go index cc54036d1d..f7ca7dc870 100644 --- a/internal/compiler/parse.go +++ b/internal/compiler/parse.go @@ -19,18 +19,6 @@ import ( var ErrUnsupportedStatementType = errors.New("parseQuery: unsupported statement type") -func rewriteNumberedParameters(refs []paramRef, raw *ast.RawStmt, sql string) ([]source.Edit, error) { - edits := make([]source.Edit, len(refs)) - for i, ref := range refs { - edits[i] = source.Edit{ - Location: ref.ref.Location - raw.StmtLocation, - Old: fmt.Sprintf("$%d", ref.ref.Number), - New: "?", - } - } - return edits, nil -} - func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, error) { if o.Debug.DumpAST { debug.Dump(stmt) @@ -90,19 +78,14 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query, if err != nil { return nil, err } - if o.UsePositionalParameters { - edits, err = rewriteNumberedParameters(refs, raw, rawSQL) - if err != nil { - return nil, err - } + + refs = uniqueParamRefs(refs, dollar) + if c.conf.Engine == config.EngineMySQL || !dollar { + sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Location < refs[j].ref.Location }) } else { - refs = uniqueParamRefs(refs, dollar) - if c.conf.Engine == config.EngineMySQL || !dollar { - sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Location < refs[j].ref.Location }) - } else { - sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Number < refs[j].ref.Number }) - } + sort.Slice(refs, func(i, j int) bool { return refs[i].ref.Number < refs[j].ref.Number }) } + qc, err := buildQueryCatalog(c.catalog, raw.Stmt) if err != nil { return nil, err diff --git a/internal/opts/parser.go b/internal/opts/parser.go index 7ce464be2c..d6fb399552 100644 --- a/internal/opts/parser.go +++ b/internal/opts/parser.go @@ -1,6 +1,5 @@ package opts type Parser struct { - UsePositionalParameters bool - Debug Debug + Debug Debug }