-
-
Notifications
You must be signed in to change notification settings - Fork 90
Prepared Statement API #15
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
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
fd96e49
Added an API for prepared queries
tbartelmess 470d23a
Updated testPreparedQuery to close the connection
tbartelmess 38f78d7
Added a DEALLOCATE api
tbartelmess 7e33d61
Added a closure based API
tbartelmess 997e80e
Implemented log(to:)
tbartelmess b307f8b
Made prepared query a strcut
tbartelmess 14d2ca2
Spacing fixes
tbartelmess f6844d7
Implement 'Close' command for prepared statements
tbartelmess 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
34 changes: 34 additions & 0 deletions
34
Sources/PostgresNIO/Connection/PostgresDatabase+Close.swift
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,34 @@ | ||
| import NIO | ||
|
|
||
|
|
||
| /// PostgreSQL request to close a prepared statement or portal. | ||
| final class CloseRequest: PostgresRequest { | ||
|
|
||
| /// Name of the prepared statement or portal to close. | ||
| let name: String | ||
|
|
||
| /// Close | ||
| let target: PostgresMessage.Close.Target | ||
|
|
||
| init(name: String, closeType: PostgresMessage.Close.Target) { | ||
| self.name = name | ||
| self.target = closeType | ||
| } | ||
|
|
||
| func respond(to message: PostgresMessage) throws -> [PostgresMessage]? { | ||
| if message.identifier != .closeComplete { | ||
| fatalError("Unexpected PostgreSQL message \(message)") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func start() throws -> [PostgresMessage] { | ||
| let close = try PostgresMessage.Close(target: target, name: name).message() | ||
| let sync = try PostgresMessage.Sync().message() | ||
| return [close, sync] | ||
| } | ||
|
|
||
| func log(to logger: Logger) { | ||
| logger.debug("Requesting Close of \(name)") | ||
| } | ||
| } |
164 changes: 164 additions & 0 deletions
164
Sources/PostgresNIO/Connection/PostgresDatabase+PreparedQuery.swift
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,164 @@ | ||
| import Foundation | ||
|
|
||
| extension PostgresDatabase { | ||
| public func prepare(query: String) -> EventLoopFuture<PreparedQuery> { | ||
| let name = "nio-postgres-\(UUID().uuidString)" | ||
| let prepare = PrepareQueryRequest(query, as: name) | ||
| return self.send(prepare, logger: self.logger).map { () -> (PreparedQuery) in | ||
| let prepared = PreparedQuery(database: self, name: name, rowDescription: prepare.rowLookupTable!) | ||
| return prepared | ||
| } | ||
| } | ||
|
|
||
| public func prepare(query: String, handler: @escaping (PreparedQuery) -> EventLoopFuture<[[PostgresRow]]>) -> EventLoopFuture<[[PostgresRow]]> { | ||
| prepare(query: query) | ||
| .flatMap { preparedQuery in | ||
| handler(preparedQuery) | ||
| .flatMap { results in | ||
| preparedQuery.deallocate().map { results } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| public struct PreparedQuery { | ||
| let database: PostgresDatabase | ||
| let name: String | ||
| let rowLookupTable: PostgresRow.LookupTable | ||
|
|
||
| init(database: PostgresDatabase, name: String, rowDescription: PostgresRow.LookupTable) { | ||
| self.database = database | ||
| self.name = name | ||
| self.rowLookupTable = rowDescription | ||
| } | ||
|
|
||
| public func execute(_ binds: [PostgresData] = []) -> EventLoopFuture<[PostgresRow]> { | ||
| var rows: [PostgresRow] = [] | ||
| return self.execute(binds) { rows.append($0) }.map { rows } | ||
| } | ||
|
|
||
| public func execute(_ binds: [PostgresData] = [], _ onRow: @escaping (PostgresRow) throws -> ()) -> EventLoopFuture<Void> { | ||
| let handler = ExecutePreparedQuery(query: self, binds: binds, onRow: onRow) | ||
| return database.send(handler, logger: database.logger) | ||
| } | ||
|
|
||
| public func deallocate() -> EventLoopFuture<Void> { | ||
| database.send(CloseRequest(name: self.name, | ||
| closeType: .preparedStatement), | ||
| logger:database.logger) | ||
|
|
||
| } | ||
| } | ||
|
|
||
|
|
||
| private final class PrepareQueryRequest: PostgresRequest { | ||
| let query: String | ||
| let name: String | ||
| var rowLookupTable: PostgresRow.LookupTable? | ||
| var resultFormatCodes: [PostgresFormatCode] | ||
| var logger: Logger? | ||
|
|
||
| init(_ query: String, as name: String) { | ||
| self.query = query | ||
| self.name = name | ||
| self.resultFormatCodes = [.binary] | ||
| } | ||
|
|
||
| func respond(to message: PostgresMessage) throws -> [PostgresMessage]? { | ||
| switch message.identifier { | ||
| case .rowDescription: | ||
| let row = try PostgresMessage.RowDescription(message: message) | ||
| self.rowLookupTable = PostgresRow.LookupTable( | ||
| rowDescription: row, | ||
| resultFormat: self.resultFormatCodes | ||
| ) | ||
| return [] | ||
| case .parseComplete, .parameterDescription: | ||
| return [] | ||
| case .readyForQuery: | ||
| return nil | ||
| default: | ||
| fatalError("Unexpected message: \(message)") | ||
| } | ||
|
|
||
| } | ||
|
|
||
| func start() throws -> [PostgresMessage] { | ||
| let parse = PostgresMessage.Parse( | ||
| statementName: self.name, | ||
| query: self.query, | ||
| parameterTypes: [] | ||
| ) | ||
| let describe = PostgresMessage.Describe( | ||
| command: .statement, | ||
| name: self.name | ||
| ) | ||
| return try [parse.message(), describe.message(), PostgresMessage.Sync().message()] | ||
| } | ||
|
|
||
|
|
||
| func log(to logger: Logger) { | ||
| self.logger = logger | ||
| logger.debug("\(self.query) prepared as \(self.name)") | ||
| } | ||
| } | ||
|
|
||
|
|
||
| private final class ExecutePreparedQuery: PostgresRequest { | ||
| let query: PreparedQuery | ||
| let binds: [PostgresData] | ||
| var onRow: (PostgresRow) throws -> () | ||
| var resultFormatCodes: [PostgresFormatCode] | ||
| var logger: Logger? | ||
|
|
||
| init(query: PreparedQuery, binds: [PostgresData], onRow: @escaping (PostgresRow) throws -> ()) { | ||
| self.query = query | ||
| self.binds = binds | ||
| self.onRow = onRow | ||
| self.resultFormatCodes = [.binary] | ||
| } | ||
|
|
||
| func respond(to message: PostgresMessage) throws -> [PostgresMessage]? { | ||
| switch message.identifier { | ||
| case .bindComplete: | ||
| return [] | ||
| case .dataRow: | ||
| let data = try PostgresMessage.DataRow(message: message) | ||
| let row = PostgresRow(dataRow: data, lookupTable: query.rowLookupTable) | ||
| try onRow(row) | ||
| return [] | ||
| case .noData: | ||
| return [] | ||
| case .commandComplete: | ||
| return [] | ||
| case .readyForQuery: | ||
| return nil | ||
| default: throw PostgresError.protocol("Unexpected message during query: \(message)") | ||
| } | ||
| } | ||
|
|
||
| func start() throws -> [PostgresMessage] { | ||
|
|
||
| let bind = PostgresMessage.Bind( | ||
| portalName: "", | ||
| statementName: query.name, | ||
| parameterFormatCodes: self.binds.map { $0.formatCode }, | ||
| parameters: self.binds.map { .init(value: $0.value) }, | ||
| resultFormatCodes: self.resultFormatCodes | ||
| ) | ||
| let execute = PostgresMessage.Execute( | ||
| portalName: "", | ||
| maxRows: 0 | ||
| ) | ||
|
|
||
| let sync = PostgresMessage.Sync() | ||
| return try [bind.message(), execute.message(), sync.message()] | ||
| } | ||
|
|
||
| func log(to logger: Logger) { | ||
| self.logger = logger | ||
| logger.debug("Execute Prepared Query: \(query.name)") | ||
| } | ||
|
|
||
| } |
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,39 @@ | ||
| import NIO | ||
|
|
||
| extension PostgresMessage { | ||
| /// Identifies the message as a Close Command | ||
| public struct Close: PostgresMessageType { | ||
| public static var identifier: PostgresMessage.Identifier { | ||
| return .close | ||
| } | ||
|
|
||
| /// Close Target. Determines if the Close command should close a prepared statement | ||
| /// or portal. | ||
| public enum Target: Int8 { | ||
| case preparedStatement = 0x53 // 'S' - prepared statement | ||
| case portal = 0x50 // 'P' - portal | ||
| } | ||
|
|
||
| /// Determines if the `name` identifes a portal or a prepared statement | ||
| public var target: Target | ||
|
|
||
| /// The name of the prepared statement or portal to describe | ||
| /// (an empty string selects the unnamed prepared statement or portal). | ||
| public var name: String | ||
|
|
||
|
|
||
| /// See `CustomStringConvertible`. | ||
| public var description: String { | ||
| switch target { | ||
| case .preparedStatement: return "Statement(\(name))" | ||
| case .portal: return "Portal(\(name))" | ||
| } | ||
| } | ||
|
|
||
| /// Serializes this message into a byte buffer. | ||
| public func serialize(into buffer: inout ByteBuffer) throws { | ||
| buffer.writeInteger(target.rawValue) | ||
| buffer.write(nullTerminated: name) | ||
| } | ||
| } | ||
| } |
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
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this missing a deallocate for the prepared query? Maybe we should add an assertion to make sure it gets deallocated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A query doesn't need to get deallocated, because the session is closed by the end of this test.
Maybe deallocate is a bad name here because in most cases it refers to memory.