- Notifications
You must be signed in to change notification settings - Fork7
A Modern Finagle-Postgresql Client
License
finagle/roc
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
roc is a modernFinaglePostgresqlClient. What's modern? A Client relying on a6.x + version of Finagle.
Roc is published toMaven Central, so for the latest stable version add the following to your build:
libraryDependencies++=Seq("com.github.finagle"%%"roc-core"%"0.0.4","com.github.finagle"%%"roc-types"%"0.0.4")
Roc is under heavy development, so to stay up to with the latestSNAPSHOT
version add the following to your build instead:
resolvers+=Resolver.sonatypeRepo("snapshots")libraryDependencies++=Seq("com.github.finagle"%%"roc-core"%"0.0.5-SNAPSHOT" changing())
Opensbt console
and insert the following
scala>:pasteimportcom.twitter.util.Awaitimportroc.Postgresqlimportroc.postgresql.{Request,Row}valclient=Postgresql.client .withUserAndPasswd("username","password") .withDatabase("database") .newRichClient("inet!localhost:5432")valreq=newRequest("SELECT * FROM STATES;")valresult=Await.result(client.query(req))result: roc.postgresql.Result=Result(Text('id,23,1)Text('name,25,Alabama)Text('abbrv,1043,AL),Text('id,23,2)...)
Let's turn aResult
into all 50State(s)
.
importjava.time.ZonedDateTimeimportroc.types.decoders._caseclassState(id:Int,name:String,abbrv:String,insertedAt:ZonedDateTime)valrow2State: (Row)=> (State)= (row:Row)=> {valid= row.get('id).as[Int]valname= row.get('name).as[String]valabbrv= row.get('abbrv).as[String]valinsertedAt= row.get('inserted_at).as[TimestampWithTZ]State(id, name, abbrv, insertedAt)}valstates= result.map(row2State).toListstates:List[State]=List(State(1,Alabama,AL,2016-05-10T11:59:13.879709-05:00),State(2,Alaska,AK,2016-05-10T11:59:20.974995-05:00))
If you're into Scaladocs ( I am ), they can be foundhere.
The most important type in Roc isResult, the type returned after a Postgresql query is executed. Result implementsIterable so that it can be viewed as a collection ofRows.The two additional members ofResult
are:
valresult= client.query(newRequest("SELECT * FROM FOO;"))result.columns// all column information returned from the Requestresult.completedCommand// a String representation of what happend
- For an INSERT command, the tag is INSERT oid rows, where rows is the number of rows inserted. oid is the object ID of the inserted row if rows is 1 and the target table has OIDs; otherwise oid is 0.
- For a DELETE command, the tag is DELETE rows where rows is the number of rows deleted.
- For an UPDATE command, the tag is UPDATE rows where rows is the number of rows updated.
- For a SELECT or CREATE TABLE AS command, the tag is SELECT rows where rows is the number of rows retrieved.
- For a MOVE command, the tag is MOVE rows where rows is the number of rows the cursor's position has been changed by.
- For a FETCH command, the tag is FETCH rows where rows is the number of rows that have been retrieved from the cursor.
For anUPDATE
orINSERT
command, aResult
will have a length of0
and no column information, but will always return acompletedCommand
.From Postgresql's perspective, the fact that the query returns without giving an error is evidence that the command completed successfully, andRoc
will adhere to their style, not theJDBC
style.
ARow
holds a non-zero number ofElements.AnElement
is the actual value returned at[row][column]
.For example, if we were to execute the following:
scala>valreq=newRequest("SELECT COUNT(*) FROM STATES;")scala>valresult=Await.result(client.query(req))result: roc.postgresql.Result=Result(Text('count,20,50))
we are given aResult
with oneColumn
(with the name of'count
, a FormatCode of Text, and OID of 20), and oneRow
.To retrieve a value out of thatRow
, we do the following:
scala>valhead= result.head// let's just get the first rowscala>valcount= head.get('count)count: roc.postgresql.Element=Text('count,20,50)
Wait, what exactly is anElement
?
roc-core
has an extremely minimal design philosophy. That includes the decoding of actual data. In other words,we'll decode the bytes into the correct format, we'll tell you what that format is, but it's up to you (or another roc module)to go any further.Postgreql returns data in 3 possible formats:
- UTF-8 Text
- Binary Format (typically Big Endian)
- No data is returned for that column ( mean it is a NULL value )
roc-core
will decode this data into the given format, but goes no further in the process - it is up to core clients to decide how to procede.Going back to the example above, we see:
scala>valcount= head.get('count)count: roc.postgresql.Element=Text('count,20,50)
This means that Postgresql has returned a column name'count
, in a String format, with a Postgresql Type ofLong
.String encodings are typically preferred, and almost universally the case unless the column returned is binary data,or if aFETCH
command is used to return aCURSOR
.AnElement
has 3 sub-types
- Text
- Binary
- NULL
Yes, we've deliberately introduced a specificNULL
type into the system. This allows clients to handleNULL
cases in whatever way they see fit.
To get a value out of anElement
, you have several options:
scala>valcount= head.get('count)count: roc.postgresql.Element=Text('count,20,50)scala> count.asStringres4:String=50scala> count.asBytesroc.postgresql.failures$UnsupportedDecodingFailure:AttemptedBinary decoding ofString column.
If you attempt to get the String of a Binary element, you'll get anotherUnsupportedDecodingFailure
.The two attempts above are short cuts to getting values. The preferred method involves a fold:
deffold[A](fa:String=>A,fb:Array[Byte]=>A,fc: ()=>A):A=thismatch {caseText(_, _, value)=> fa(value)caseBinary(_, _, value)=> fb(value)caseNull(_, _)=> fc()}
This allows you to handleNULL
values in any way you see fit, and makes the decode process typesafe.BothasString
andasBytes
callfold
under the covers.
Finally, there is the ubiquitous parsing / decoding methodas[A]
:
defas[A](implicitf:ElementDecoder[A]):A= fold(f.textDecoder, f.binaryDecoder, f.nullDecoder)
AnElementDecoder
is a TypeClass to allow custom decoding in a more syntax friendly way. See theScaladocsor gitter for more information.
Theroc-types
project defines type aliases fromPostgresql => Scala
, and includesElementDecoder
instances for those types. The current types include
smallint => Short
int => Int
bigint => Long
real => Float
double precision => Double
char => Char (Note this is a C-Style understanding of a Char, not a UTF Rune)
text/CHARACTER VARYING => String
bool => Boolean
JSON/JSONB => Json
(via Jawn)Date => Date = java.time.LocalDate
Time => Time = java.time.LocalTime
TIME WITH TIME ZONE => TimestampWithTZ = java.time.ZonedDateTime
NULL => Option
To decode a column that may be NULL, clients should simply use anOption[A]
decoder, whereA = COLUMN TYPE
.Let's add apopulation
column to ourstates
table, of typeint
.
caseclassState(id:Int,name:String,abbrv:String,population:Option[Int],insertedAt:ZonedDateTime)valrow2State: (Row)=> (State)= (row:Row)=> {valid= row.get('id).as[Int]valname= row.get('name).as[String]valabbrv= row.get('abbrv).as[String]valpopulation= row.get('population).as[Int]valinsertedAt= row.get('inserted_at).as[TimestampWithTZ]State(id, name, abbrv, population, insertedAt)}valstate= result.map(row2State).toList.headstates:State=State(1,Alabama,AL,None,2016-05-10T12:46:59.998788-05:00)
As the type of column should be known at compile time,roc-types
throws anNullDecodedFailure(TYPE)
with a helpful error message if you attempt to decode aNULL
type:
scala>valrow= result.headrow: roc.postgresql.Row=Text('inserted_at,1184,2016-05-1012:46:59.998788-05)Null('population,23)Text('abbrv,1043,AL)Text('name,25,Alabama)Text('id,23,1)scala>valpopulation= row.get('population).as[Int]roc.types.failures$NullDecodedFailure:ANULL value was decodedfortypeINT.Hint: use theOption[INT] decoder, or ensure thatPostgres cannotreturnNULLfor the requested value.
The desire ofcore
is to be as minimal as possible. In practice, that means mapping a Finagle Service over Postgresql with as little bedazzling as possible.The Postgresql Client will be very minimalistic ( currently just one method,def query(Request): Future[Result]
), and the aim is forResult
to do as little as possible.Additional future modules may provide additional functionality.
The currentfinagle-postgres was developed preFinagle 6.x and does not use modern finagle abstractions. These are core enough to require what amounts to a complete rewrite of the driver.
roc (Rokh orRukh) is named after the Persian mythological bird of preyRoc,which was based in part on the now extinctelephant bird. Yes, that means we've hit the rare 4 point play with the name
Goal | Status |
---|---|
short name | ✔️ |
obligatory mythological reference | ✔️ |
bird reference for Twitter | ✔️ |
elephant reference for Postgresql | ✔️ |
roc is currently maintained byJeffrey Davis.
The roc project supports theTypelevelcode of conduct and wantsall of its channels (GitHub, gitter, etc.) to be welcoming environments for everyone.
Licensed under theBSD-3-Clause(Revised BSD Liscense); you may not use this software except in compliance with the License.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
About
A Modern Finagle-Postgresql Client