diff --git a/.gitignore b/.gitignore
index e8c9575d..c83ec207 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,23 @@
+databases/*
+out/*
+generate_bundles.rb
.cache
target/*
bin/*
.idea*
.classpath
-.project
-.settings/*
+.project/*
+**/.settings/*
project/target/*
-project/project/*
\ No newline at end of file
+project/project/*
+postgresql-async/target/*
+db-async-common/target/*
+mysql-async/target/*
+.rvmrc
+.ruby-version
+.ruby-gemset
+*.jar
+*.iml
+.project
+.vagrant/*
+vendor/*
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..3e334f1a
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,24 @@
+language: scala
+scala:
+ - 2.10.4
+ - 2.11.7
+ - 2.12.1
+
+jdk:
+ - oraclejdk8
+
+services:
+ - postgresql
+ - mysql
+cache:
+ directories:
+ - vendor/bundle
+ - $HOME/.m2
+ - $HOME/.ivy2
+ - $HOME/.sbt
+before_script:
+ - ./script/prepare_build.sh
+
+notifications:
+ email:
+ - linhares.mauricio@gmail.com
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 00000000..ce4b61ae
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,164 @@
+
+
+
+- [Changelog](#changelog)
+ - [0.2.19 - 2016-03-17](#0219---2016-03-17)
+ - [0.2.18 - 2015-08-08](#0218---2015-08-08)
+ - [0.2.17 - 2015-07-13](#0217---2015-07-13)
+ - [0.2.16 - 2015-01-04](#0216---2015-01-04)
+ - [0.2.15 - 2014-09-12](#0215---2014-09-12)
+ - [0.2.14 - 2014-08-30](#0214---2014-08-30)
+ - [0.2.13 - 2014-04-07](#0213---2014-04-07)
+ - [0.2.12 - 2014-01-11](#0212---2014-01-11)
+ - [0.2.11 - 2014-01-11](#0211---2014-01-11)
+ - [0.2.10 - 2013-12-18](#0210---2013-12-18)
+ - [0.2.9 - 2013-12-01](#029---2013-12-01)
+ - [0.2.8 - 2013-09-24](#028---2013-09-24)
+ - [0.2.7 - 2013-09-09](#027---2013-09-09)
+ - [0.2.5](#025)
+ - [0.2.4 - 2013-07-06](#024---2013-07-06)
+ - [0.2.3 - 2013-05-21](#023---2013-05-21)
+ - [0.2.2 - 2013-05-18](#022---2013-05-18)
+ - [0.1.1 - 2013-04-30](#011---2013-04-30)
+ - [0.1.0 - 2013-04-29](#010---2013-04-29)
+
+
+
+# Changelog
+
+## 0.2.20 - 2017-09-17
+
+* Building for Scala 2.12;
+* Fix SFL4J deprecation warning - #201 - @golem131;
+
+## 0.2.19 - 2016-03-17
+
+* Always use `NUMERIC` when handling numbers in prepared statements in PostgreSQL;
+* SSL support for PostgreSQL - @alexdupre - #85;
+
+## 0.2.18 - 2015-08-08
+
+* Timeouts implemented queries for MySQL and PostgreSQL - @lifey - #147
+
+## 0.2.17 - 2015-07-13
+
+* Fixed pool leak issue - @haski
+* Fixed date time formatting issue - #142
+
+## 0.2.16 - 2015-01-04
+
+* Add support to byte arrays for PostgreSQL 8 and older - @SattaiLanfear - #21;
+* Make sure connections are returned to the pool before the result is returned to the user - @haski - #119;
+* Support to `SEND_LONG_DATA` to MySQL - @mst-appear - #115;
+* Support for `ByteBuffer` and `ByteBuf` for binary data - @mst-appear - #113 #112;
+* Fixed encoding backslashes in PostgreSQL arrays - @dylex - #110;
+* Included `escape` encoding method for bytes in PostgreSQL - @SattaiLanfear - #107;
+
+## 0.2.15 - 2014-09-12
+
+* Fixes issue where PostgreSQL decoders fail to produce a NULL value if the null is wrapped by a `Some` instance - #99;
+* Fixes issue where the 253 case of length encoded fields on MySQL produce a wrong value;
+
+## 0.2.14 - 2014-08-30
+
+* Remove failed prepared statement from cache - @dboissin - #95
+* Added support to zeroed dates on MySQL - #93
+* Cross compilation to Scala 2.11 is functional - @lpiepiora
+* Connect to older MySQL versions where auth protocol isn't provided - #37
+* Eclipse project support - @fwbrasil - #89
+* Make timeouts configurable - @fwbrasil - #90
+
+## 0.2.13 - 2014-04-07
+
+* Accepts MySQL old and unsafe auth methods - #37
+* Do not name every single logger as they all leak - @njeuk #86
+
+## 0.2.12 - 2014-01-11
+
+* Do not check for handshake requests after a real handshake has happened already - MySQL - #80;
+
+## 0.2.11 - 2014-01-11
+
+* LISTEN/NOTIFY support for PostgreSQL
+* Driver logs prepared statement data for PostgreSQL calls when logging is set to debug - #77;
+* MySQL and PostgreSQL drivers log network bytes read/written when logging is set to trace;
+* PostgreSQL now correctly sends JSON to JSON fields without requiring a cast - #75;
+* LocalDateTime is not printed (driver fails silently) - #74;
+* Support for ENUM types on PostgreSQL - #75;
+* Allow configuring the execution context used by PostgreSQL connections - #72
+* Naming executors so we can see the threads created - #71
+
+## 0.2.10 - 2013-12-18
+
+* Removed application_name from PostgreSQL default connection values - #70
+
+## 0.2.9 - 2013-12-01
+
+* PostgreSQL driver cannot parse value set by current_timestamp with timezone - #51
+* Add AsyncObjectPool.use to combine take and giveBack - #53
+* Add support for postgres interval type as Period - #56
+* Connection mutex improvements for issue - #59
+* Improve URL parser to allow missing hostname/dbname - #64
+* Decode OIDs as Long - #62
+* Improve placeholders and prepared statement handling - #65
+* ResultSet.columnNames order does not match ResultSet order - #61
+* Add Connection.inTransaction to wrap queries in a transaction block - #54
+* Add support for MySQL BINARY/VARBINARY types - #55
+
+## 0.2.8 - 2013-09-24
+
+* Validate the number of parameters for prepared statements - fixes #47
+* Adding support for MySQL BIT type - fixes #48
+* Use the rows count as the affected rows for MySQL also - fixes #46
+
+## 0.2.7 - 2013-09-09
+
+* Upgrading Netty to 4.0.9
+* Removing direct dependency on `logback` and making it depend on SFL4J only, upgrading JodaTime - by @kxbmap
+* MySQL doesn't set columnNames in QueryResult - #42
+* Timestamps with microseconds fail to be parsed on PostgreSQL - #41
+
+## 0.2.5
+
+* Allow the ClientSocketChannelFactory and ExecutionContext to be given at the connections instead of
+ always using the driver provided ones - #38
+* Upgraded to Netty 4 - @normanmaurer
+
+## 0.2.4 - 2013-07-06
+
+* Mysql driver fails for null TIMESTAMP
+* Mysql driver fails for big strings (TEXT)
+* Support (auto convert) Option in prepared stmt parameters - @magro - #30
+* Allow database connections strings without port, username and password - @magro - #32
+* Support 'postgres' protocol in heroku like db urls - @magro - #33
+* Fix jdbc:postgresql url format to use 'user' instead 'username' - @magro - #35
+
+
+## 0.2.3 - 2013-05-21
+
+* Upgraded Netty to 3.6.6.Final
+* Added support for `timestamp(n)` values on MySQL
+* Updated docs to reference MySQL's 5.6 support for microseconds - @theon
+* MySQL driver returns DateTime instead of LocalDateTime when in text protocol - #24
+
+## 0.2.2 - 2013-05-18
+
+* Implement MySQL support, should be able to execute common statements, prepared statements and login with password (#9)
+* Concurrency problem for multiple queries - @fwbrasil - #18
+* Support prepared statement with more than 64 characters on PostgreSQL - @fwbrasil - #16
+* Do not accept returned connections to pool that aren't ready for query - @fwbrasil - #15
+* Multiple executions of a prepared statement that doesn't return rows fail - @fwbrasil - #13
+* Optimize match/cases to `@switch` (#10)
+* Reimplement the PostgreSQLMD5Digest.java in Scala - (#8)
+
+## 0.1.1 - 2013-04-30
+
+* Query promises fulfilled before cleaning up the query promise cause the futures to either hang forever or fail with a
+ "query already running" message (#2)
+* Optimize MessageEncoder to use a match instead of a map (#3)
+* MessageDecoder should validate sizes and correctly handle negative or too large messages (#4)
+* Move generic pool classes to the com.github.mauricio.async.db package (#9)
+
+## 0.1.0 - 2013-04-29
+
+* First public release
diff --git a/LICENCE.txt b/LICENCE.txt
deleted file mode 100644
index 24c32684..00000000
--- a/LICENCE.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-Copyright (C) 2012 Maurício Linhares - mauricio (dot) linhares (at) gmail
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 00000000..fc389d02
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [2013] [Maurício Linhares]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/Procfile b/Procfile
index 4f4624de..13e2e8fd 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1,2 @@
-postgresql: /usr/local/Cellar/postgresql/9.1.2/bin/postgres -D /Users/mauricio/databases/postgresql
+postgresql: postgres -D vendor/postgresql
+mysql: mysqld --log-warnings --console
\ No newline at end of file
diff --git a/README.markdown b/README.markdown
index 8f72305d..79f4b057 100644
--- a/README.markdown
+++ b/README.markdown
@@ -1,26 +1,141 @@
-# postgresql-netty - an async Netty/NIO based PostgreSQL driver written in Scala
+
+
-The main goal of this project is to implement a performant and fully functional async PostgreSQL driver. This project
-has no interest in JDBC, it's supposed to be a clean room implementation for people interested in talking directly
-to PostgreSQL.
+- This project is not being maintained anymore, feel free to fork and work on it
+ - [Abstractions and integrations](#abstractions-and-integrations)
+ - [Include them as dependencies](#include-them-as-dependencies)
+ - [Database connections and encodings](#database-connections-and-encodings)
+ - [Prepared statements gotcha](#prepared-statements-gotcha)
+ - [What are the design goals?](#what-are-the-design-goals)
+ - [What is missing?](#what-is-missing)
+ - [How can you help?](#how-can-you-help)
+ - [Main public interface](#main-public-interface)
+ - [Connection](#connection)
+ - [QueryResult](#queryresult)
+ - [ResultSet](#resultset)
+ - [Prepared statements](#prepared-statements)
+ - [Transactions](#transactions)
+ - [Example usage (for PostgreSQL, but it looks almost the same on MySQL)](#example-usage-for-postgresql-but-it-looks-almost-the-same-on-mysql)
+ - [LISTEN/NOTIFY support (PostgreSQL only)](#listennotify-support-postgresql-only)
+ - [Contributing](#contributing)
+ - [Licence](#licence)
-[PostgreSQL protocol information and definition can be found here](http://www.postgresql.org/docs/devel/static/protocol.html)
+
-## What can it do now?
+# [](https://travis-ci.org/mauricio/postgresql-async) This project is not being maintained anymore, feel free to fork and work on it
-- connect to a database without authentication (it only connects if it gets an AuthenticationOk message)
-- receive parameters
-- receive database notices
-- execute direct queries (without portals/prepared statements)
-- parses all basic PostgreSQL types, other types are parsed as string
-- date, time and timestamp types are handled as JodaTime objects and **not** as **java.util.Date** objects
+The main goal for this project is to implement simple, async, performant and reliable database drivers for
+PostgreSQL and MySQL in Scala. This is not supposed to be a JDBC replacement, these drivers aim to cover the common
+process of _send a statement, get a response_ that you usually see in applications out there. So it's unlikely
+there will be support for updating result sets live or stuff like that.
-## What is missing?
+This project always returns [JodaTime](http://joda-time.sourceforge.net/) when dealing with date types and not the
+`java.util.Date` class.
+
+If you want information specific to the drivers, check the [PostgreSQL README](postgresql-async/README.md) and the
+[MySQL README](mysql-async/README.md).
+
+You can view the project's [CHANGELOG here](CHANGELOG.md).
+
+## Abstractions and integrations
+
+* [Activate Framework](http://activate-framework.org/) - full ORM solution for persisting objects using a software
+ transactional memory (STM) layer;
+* [ScalikeJDBC-Async](https://github.com/scalikejdbc/scalikejdbc-async) - provides an abstraction layer on top of the
+ driver allowing you to write less SQL and make use of a nice high level database access API;
+* [mod-mysql-postgresql](https://github.com/vert-x/mod-mysql-postgresql) - [vert.x](http://vertx.io/) module that integrates
+ the driver into a vert.x application;
+* [dbmapper](https://github.com/njeuk/dbmapper) - enables SQL queries with automatic mapping from the database table to the Scala
+ class and a mechanism to create a Table Date Gateway model with very little boiler plate code;
+* [Quill](http://getquill.io) - A compile-time language integrated query library for Scala.
+
+## Include them as dependencies
+
+And if you're in a hurry, you can include them in your build like this, if you're using PostgreSQL:
+
+```scala
+"com.github.mauricio" %% "postgresql-async" % "0.2.21"
+```
+
+Or Maven:
+
+```xml
+
+ com.github.mauricio
+ postgresql-async_2.11
+ 0.2.21
+
+```
+
+respectively for Scala 2.12:
+```xml
+
+ com.github.mauricio
+ postgresql-async_2.12
+ 0.2.21
+
+```
+
+And if you're into MySQL:
+
+```scala
+"com.github.mauricio" %% "mysql-async" % "0.2.21"
+```
+
+Or Maven:
+
+```xml
+
+ com.github.mauricio
+ mysql-async_2.11
+ 0.2.21
+
+```
+respectively for Scala 2.12:
+```xml
+
+ com.github.mauricio
+ mysql-async_2.12
+ 0.2.21
+
+```
+
+## Database connections and encodings
+
+**READ THIS NOW**
+
+Both clients will let you set the database encoding for something else. Unless you are **1000% sure** of what you are doing,
+**DO NOT** change the default encoding (currently, UTF-8). Some people assume the connection encoding is the **database**
+or **columns** encoding but **IT IS NOT**, this is just the connection encoding that is used between client and servers
+doing communication.
+
+When you change the encoding of the **connection** you are not affecting your database's encoding and your columns
+**WILL NOT** be stored with the connection encoding. If the connection and database/column encoding is different, your
+database will automatically translate from the connection encoding to the correct encoding and all your data will be
+safely stored at your database/column encoding.
-- portals/prepared statements
-- stored procedures
-- authentication mechanisms
-- benchmarks and more testing
+This is as long as you are using the correct string types, BLOB columns will not be translated since they're supposed
+to hold a stream of bytes.
+
+So, just don't touch it, create your tables and columns with the correct encoding and be happy.
+
+## Prepared statements gotcha
+
+If you have used JDBC before, you might have heard that prepared statements are the best thing on earth when talking
+to databases. This isn't exactly true all the time (as you can see on [this presentation](http://www.youtube.com/watch?v=kWOAHIpmLAI)
+by [@tenderlove](http://github.com/tenderlove)) and there is a memory cost in keeping prepared statements.
+
+Prepared statements are tied to a connection, they are not database-wide, so, if you generate your queries dynamically
+all the time you might eventually blow up your connection memory and your database memory.
+
+Why?
+
+Because when you create a prepared statement, locally, the connection keeps the prepared statement description in memory.
+This can be the returned columns information, input parameters information, query text, query identifier that will be
+used to execute the query and other flags. This also causes a data structure to be created at your server **for every
+connection**.
+
+So, prepared statements are awesome, but are not free. Use them judiciously.
## What are the design goals?
@@ -28,14 +143,177 @@ to PostgreSQL.
- small memory footprint
- avoid copying data as much as possible (we're always trying to use wrap and slice on buffers)
- easy to use, call a method, get a future or a callback and be happy
-- never, ever, block (the only real blocking right now is at the connection pool)
+- never, ever, block
- all features covered by tests
+- next to zero dependencies (it currently depends on Netty, JodaTime and SFL4J only)
+
+## What is missing?
+
+- more authentication mechanisms
+- benchmarks
+- more tests (run the `jacoco:cover` sbt task and see where you can improve)
+- timeout handler for initial handshare and queries
-## How can I help?
+## How can you help?
- checkout the source code
- find bugs, find places where performance can be improved
- check the **What is missing** piece
+- check the [issues page](https://github.com/mauricio/postgresql-async/issues) for bugs or new features
- send a pull request with specs
-This project is freely available under the MIT licence, use it at your own risk.
\ No newline at end of file
+## Main public interface
+
+### Connection
+
+Represents a connection to the database. This is the **root** object you will be using in your application. You will
+find three classes that implement this trait, `PostgreSQLConnection`, `MySQLConnection` and `ConnectionPool`.
+The difference between them is that `ConnectionPool` is, as the name implies, a pool of connections and you
+need to give it an connection factory so it can create connections and manage them.
+
+To create both you will need a `Configuration` object with your database details. You can create one manually or
+create one from a JDBC or Heroku database URL using the `URLParser` object.
+
+### QueryResult
+
+It's the output of running a statement against the database (either using `sendQuery` or `sendPreparedStatement`).
+This object will contain the amount of rows, status message and the possible `ResultSet` (Option\[ResultSet]) if the
+query returns any rows.
+
+### ResultSet
+
+It's an IndexedSeq\[Array\[Any]], every item is a row returned by the database. The database types are returned as Scala
+objects that fit the original type, so `smallint` becomes a `Short`, `numeric` becomes `BigDecimal`, `varchar` becomes
+`String` and so on. You can find the whole default transformation list at the project specific documentation.
+
+### Prepared statements
+
+Databases support prepared or precompiled statements. These statements allow the database to precompile the query
+on the first execution and reuse this compiled representation on future executions, this makes it faster and also allows
+for safer data escaping when dealing with user provided data.
+
+To execute a prepared statement you grab a connection and:
+
+```scala
+val connection : Connection = ...
+val future = connection.sendPreparedStatement( "SELECT * FROM products WHERE products.name = ?", Array( "Dominion" ) )
+```
+
+The `?` (question mark) in the query is a parameter placeholder, it allows you to set a value in that place in the
+query without having to escape stuff yourself. The driver itself will make sure this parameter is delivered to the
+database in a safe way so you don't have to worry about SQL injection attacks.
+
+The basic numbers, Joda Time date, time, timestamp objects, strings and arrays of these objects are all valid values
+as prepared statement parameters and they will be encoded to their respective database types. Remember that not all databases
+are created equal, so not every type will work or might work in unexpected ways. For instance, MySQL doesn't have array
+types, so, if you send an array or collection to MySQL it won't work.
+
+Remember that parameters are positional the order they show up at query should be the same as the one in the array or
+sequence given to the method call.
+
+## Transactions
+
+Both drivers support transactions at the database level, the isolation level is the default for your database/connection,
+to change the isolation level just call your database's command to set the isolation level for what you want.
+
+Here's an example of how transactions work:
+
+```scala
+ val future = connection.inTransaction {
+ c =>
+ c.sendPreparedStatement(this.insert)
+ .flatMap( r => c.sendPreparedStatement(this.insert))
+ }
+```
+
+The `inTransaction` method allows you to execute a collection of statements in a single transactions, just use the
+connection object you will receive in your block and send your statements to it. Given each statement causes a new
+future to be returned, you need to `flatMap` the calls to be able to get a `Future[T]` instead of `Future[Future[...]]`
+ back.
+
+If all futures succeed, the transaction is committed normally, if any of them fail, a `rollback` is issued to the
+database. You should not reuse a database connection that has rolled back a transaction, just close it and create a
+new connection to continue using it.
+
+## Example usage (for PostgreSQL, but it looks almost the same on MySQL)
+
+You can find a small Play 2 app using it [here](https://github.com/mauricio/postgresql-async-app) and a blog post about
+it [here](http://mauricio.github.io/2013/04/29/async-database-access-with-postgresql-play-scala-and-heroku.html).
+
+In short, what you would usually do is:
+```scala
+import com.github.mauricio.async.db.postgresql.PostgreSQLConnection
+import com.github.mauricio.async.db.postgresql.util.URLParser
+import com.github.mauricio.async.db.util.ExecutorServiceUtils.CachedExecutionContext
+import com.github.mauricio.async.db.{RowData, QueryResult, Connection}
+import scala.concurrent.duration._
+import scala.concurrent.{Await, Future}
+
+object BasicExample {
+
+ def main(args: Array[String]) {
+
+ val configuration = URLParser.parse("jdbc:postgresql://localhost:5233/my_database?user=postgres&password=somepassword")
+ val connection: Connection = new PostgreSQLConnection(configuration)
+
+ Await.result(connection.connect, 5 seconds)
+
+ val future: Future[QueryResult] = connection.sendQuery("SELECT 0")
+
+ val mapResult: Future[Any] = future.map(queryResult => queryResult.rows match {
+ case Some(resultSet) => {
+ val row : RowData = resultSet.head
+ row(0)
+ }
+ case None => -1
+ }
+ )
+
+ val result = Await.result( mapResult, 5 seconds )
+
+ println(result)
+
+ connection.disconnect
+
+ }
+
+}
+```
+
+First, create a `PostgreSQLConnection`, connect it to the database, execute queries (this object is not thread
+safe, so you must execute only one query at a time) and work with the futures it returns. Once you are done, call
+disconnect and the connection is closed.
+
+You can also use the `ConnectionPool` provided by the driver to simplify working with database connections in your app.
+Check the blog post above for more details and the project's ScalaDocs.
+
+## LISTEN/NOTIFY support (PostgreSQL only)
+
+LISTEN/NOTIFY is a PostgreSQL-specific feature for database-wide publish-subscribe scenarios. You can listen to database
+notifications as such:
+
+```scala
+ val connection: Connection = ...
+
+ connection.sendQuery("LISTEN my_channel")
+ connection.registerNotifyListener {
+ message =>
+ println(s"channel: ${message.channel}, payload: ${message.payload}")
+ }
+```
+
+## Contributing
+
+Contributing to the project is simple, fork it on Github, hack on what you're insterested in seeing done or at the
+bug you want to fix and send a pull request back. If you thing the change is too big or requires architectural changes
+please create an issue **before** you start working on it so we can discuss what you're trying to do.
+
+You should be easily able to build this project in your favorite IDE since it's built by [SBT](http://www.scala-sbt.org/)
+using a plugin that generates your IDE's project files. You can use [sbt-idea](https://github.com/mpeltonen/sbt-idea)
+for IntelliJ Idea and [sbteclipse](https://github.com/typesafehub/sbteclipse) for Eclipse integration.
+
+[Check our list of contributors!](https://github.com/mauricio/postgresql-async/graphs/contributors)
+
+## Licence
+
+This project is freely available under the Apache 2 licence, fork, fix and send back :)
diff --git a/bootstrap.sh b/bootstrap.sh
new file mode 100644
index 00000000..451d77cf
--- /dev/null
+++ b/bootstrap.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+yum -y install mysql-server
+service mysqld start
+mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO root;"
+mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'mysql_vagrant' IDENTIFIED BY 'generic_password' WITH GRANT OPTION";
\ No newline at end of file
diff --git a/db-async-common/src/main/java/com/github/mauricio/async/db/util/BufferDumper.java b/db-async-common/src/main/java/com/github/mauricio/async/db/util/BufferDumper.java
new file mode 100644
index 00000000..cd9ef72f
--- /dev/null
+++ b/db-async-common/src/main/java/com/github/mauricio/async/db/util/BufferDumper.java
@@ -0,0 +1,90 @@
+package com.github.mauricio.async.db.util;
+
+import io.netty.buffer.ByteBuf;
+
+public class BufferDumper {
+
+ public static final String dumpAsHex(ByteBuf buffer) {
+ int length = buffer.readableBytes();
+ byte[] byteBuffer = new byte[length];
+
+ buffer.markReaderIndex();
+ buffer.readBytes(byteBuffer);
+ buffer.resetReaderIndex();
+
+ StringBuffer outputBuf = new StringBuffer(length * 4);
+
+ int p = 0;
+ int rows = length / 8;
+
+ for (int i = 0; (i < rows) && (p < length); i++) {
+ int ptemp = p;
+
+ outputBuf.append(i + ": ");
+
+ for (int j = 0; j < 8; j++) {
+ String hexVal = Integer.toHexString(byteBuffer[ptemp] & 0xff);
+
+ if (hexVal.length() == 1) {
+ hexVal = "0" + hexVal; //$NON-NLS-1$
+ }
+
+ outputBuf.append(hexVal + " "); //$NON-NLS-1$
+ ptemp++;
+ }
+
+ outputBuf.append(" "); //$NON-NLS-1$
+
+ for (int j = 0; j < 8; j++) {
+ int b = 0xff & byteBuffer[p];
+
+ if (b > 32 && b < 127) {
+ outputBuf.append((char) b + " "); //$NON-NLS-1$
+ } else {
+ outputBuf.append(". "); //$NON-NLS-1$
+ }
+
+ p++;
+ }
+
+ outputBuf.append("\n"); //$NON-NLS-1$
+ }
+
+ outputBuf.append(rows + ": ");
+
+ int n = 0;
+
+ for (int i = p; i < length; i++) {
+ String hexVal = Integer.toHexString(byteBuffer[i] & 0xff);
+
+ if (hexVal.length() == 1) {
+ hexVal = "0" + hexVal; //$NON-NLS-1$
+ }
+
+ outputBuf.append(hexVal + " "); //$NON-NLS-1$
+ n++;
+ }
+
+ for (int i = n; i < 8; i++) {
+ outputBuf.append(" "); //$NON-NLS-1$
+ }
+
+ outputBuf.append(" "); //$NON-NLS-1$
+
+ for (int i = p; i < length; i++) {
+ int b = 0xff & byteBuffer[i];
+
+ if (b > 32 && b < 127) {
+ outputBuf.append((char) b + " "); //$NON-NLS-1$
+ } else {
+ outputBuf.append(". "); //$NON-NLS-1$
+ }
+ }
+
+ outputBuf.append("\n"); //$NON-NLS-1$
+ outputBuf.append("Total " + byteBuffer.length + " bytes read\n");
+
+ return outputBuf.toString();
+ }
+
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala
new file mode 100644
index 00000000..cde267cf
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/Configuration.scala
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db
+
+import java.nio.charset.Charset
+
+import io.netty.buffer.{ByteBufAllocator, PooledByteBufAllocator}
+import io.netty.util.CharsetUtil
+
+import scala.concurrent.duration._
+
+object Configuration {
+ val DefaultCharset = CharsetUtil.UTF_8
+
+ @deprecated("Use com.github.mauricio.async.db.postgresql.util.URLParser.DEFAULT or com.github.mauricio.async.db.mysql.util.URLParser.DEFAULT.", since = "0.2.20")
+ val Default = new Configuration("postgres")
+}
+
+/**
+ *
+ * Contains the configuration necessary to connect to a database.
+ *
+ * @param username database username
+ * @param host database host, defaults to "localhost"
+ * @param port database port, defaults to 5432
+ * @param password password, defaults to no password
+ * @param database database name, defaults to no database
+ * @param ssl ssl configuration
+ * @param charset charset for the connection, defaults to UTF-8, make sure you know what you are doing if you
+ * change this
+ * @param maximumMessageSize the maximum size a message from the server could possibly have, this limits possible
+ * OOM or eternal loop attacks the client could have, defaults to 16 MB. You can set this
+ * to any value you would like but again, make sure you know what you are doing if you do
+ * change it.
+ * @param allocator the netty buffer allocator to be used
+ * @param connectTimeout the timeout for connecting to servers
+ * @param testTimeout the timeout for connection tests performed by pools
+ * @param queryTimeout the optional query timeout
+ *
+ */
+
+case class Configuration(username: String,
+ host: String = "localhost",
+ port: Int = 5432,
+ password: Option[String] = None,
+ database: Option[String] = None,
+ ssl: SSLConfiguration = SSLConfiguration(),
+ charset: Charset = Configuration.DefaultCharset,
+ maximumMessageSize: Int = 16777216,
+ allocator: ByteBufAllocator = PooledByteBufAllocator.DEFAULT,
+ connectTimeout: Duration = 5.seconds,
+ testTimeout: Duration = 5.seconds,
+ queryTimeout: Option[Duration] = None)
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/Connection.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/Connection.scala
new file mode 100644
index 00000000..1b58bdcc
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/Connection.scala
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db
+
+import concurrent.Future
+
+/**
+ *
+ * Base interface for all objects that behave like a connection. This trait will usually be implemented by the
+ * objects that connect to a database, either over the filesystem or sockets. {@link Connection} are not supposed
+ * to be thread-safe and clients should assume implementations **are not** thread safe and shouldn't try to perform
+ * more than one statement (either common or prepared) at the same time. They should wait for the previous statement
+ * to be executed to then be able to pick the next one.
+ *
+ * You can, for instance, compose on top of the futures returned by this class to execute many statements
+ * at the same time:
+ *
+ * {{{
+ * val handler: Connection = ...
+ * val result: Future[QueryResult] = handler.connect
+ * .map(parameters => handler)
+ * .flatMap(connection => connection.sendQuery("BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ"))
+ * .flatMap(query => handler.sendQuery("SELECT 0"))
+ * .flatMap(query => handler.sendQuery("COMMIT").map(value => query))
+ *
+ * val queryResult: QueryResult = Await.result(result, Duration(5, SECONDS))
+ * }}}
+ *
+ */
+
+trait Connection {
+
+ /**
+ *
+ * Disconnects this object. You should discard this object after calling this method. No more queries
+ * will be accepted.
+ *
+ * @return
+ */
+
+ def disconnect: Future[Connection]
+
+ /**
+ *
+ * Connects this object to the database. Connection objects are not necessarily created with a connection to the
+ * database so you might have to call this method to be able to run queries against it.
+ *
+ * @return
+ */
+
+ def connect: Future[Connection]
+
+ /**
+ *
+ * Checks whether we are still connected to the database.
+ *
+ * @return
+ */
+
+ def isConnected: Boolean
+
+ /**
+ *
+ * Sends a statement to the database. The statement can be anything your database can execute. Not all statements
+ * will return a collection of rows, so check the returned object if there are rows available.
+ *
+ * @param query
+ * @return
+ */
+
+ def sendQuery(query: String): Future[QueryResult]
+
+ /**
+ *
+ * Sends a prepared statement to the database. Prepared statements are special statements that are pre-compiled
+ * by the database to run faster, they also allow you to avoid SQL injection attacks by not having to concatenate
+ * strings from possibly unsafe sources (like users) and sending them directy to the database.
+ *
+ * When sending a prepared statement, you can insert ? signs in your statement and then provide values at the method
+ * call 'values' parameter, as in:
+ *
+ * {{{
+ * connection.sendPreparedStatement( "SELECT * FROM users WHERE users.login = ?", Array( "john-doe" ) )
+ * }}}
+ *
+ * As you are using the ? as the placeholder for the value, you don't have to perform any kind of manipulation
+ * to the value, just provide it as is and the database will clean it up. You must provide as many parameters
+ * as you have provided placeholders, so, if your query is as "INSERT INTO users (login,email) VALUES (?,?)" you
+ * have to provide an array with at least two values, as in:
+ *
+ * {{{
+ * Array("john-doe", "doe@mail.com")
+ * }}}
+ *
+ * You can still use this method if your statement doesn't take any parameters, the default is an empty collection.
+ *
+ * @param query
+ * @param values
+ * @return
+ */
+
+ def sendPreparedStatement(query: String, values: Seq[Any] = List()): Future[QueryResult]
+
+ /**
+ *
+ * Executes an (asynchronous) function within a transaction block.
+ * If the function completes successfully, the transaction is committed, otherwise it is aborted.
+ *
+ * @param f operation to execute on this connection
+ * @return result of f, conditional on transaction operations succeeding
+ */
+
+ def inTransaction[A](f : Connection => Future[A])(implicit executionContext : scala.concurrent.ExecutionContext) : Future[A] = {
+ this.sendQuery("BEGIN").flatMap { _ =>
+ val p = scala.concurrent.Promise[A]()
+ f(this).onComplete { r =>
+ this.sendQuery(if (r.isFailure) "ROLLBACK" else "COMMIT").onComplete {
+ case scala.util.Failure(e) if r.isSuccess => p.failure(e)
+ case _ => p.complete(r)
+ }
+ }
+ p.future
+ }
+ }
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/KindedMessage.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/KindedMessage.scala
new file mode 100644
index 00000000..2074bbd6
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/KindedMessage.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db
+
+trait KindedMessage extends Serializable {
+
+ def kind : Int
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/QueryResult.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/QueryResult.scala
new file mode 100644
index 00000000..ed84f56d
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/QueryResult.scala
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db
+
+/**
+ *
+ * This is the result of the execution of a statement, contains basic information as the number or rows
+ * affected by the statement and the rows returned if there were any.
+ *
+ * @param rowsAffected
+ * @param statusMessage
+ * @param rows
+ */
+
+class QueryResult(val rowsAffected: Long, val statusMessage: String, val rows: Option[ResultSet] = None) {
+
+ override def toString: String = {
+ "QueryResult{rows -> %s,status -> %s}".format(this.rowsAffected, this.statusMessage)
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/ResultSet.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/ResultSet.scala
new file mode 100644
index 00000000..b3e8b7f9
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/ResultSet.scala
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db
+
+/**
+ *
+ * Represents the collection of rows that is returned from a statement inside a {@link QueryResult}. It's basically
+ * a collection of Array[Any]. Mutating fields in this array will not affect the database in any way
+ *
+ */
+
+trait ResultSet extends IndexedSeq[RowData] {
+
+ /**
+ *
+ * The names of the columns returned by the statement.
+ *
+ * @return
+ */
+
+ def columnNames : IndexedSeq[String]
+
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/RowData.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/RowData.scala
new file mode 100644
index 00000000..2a5d0845
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/RowData.scala
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db
+
+/**
+ *
+ * Represents a row from a database, allows clients to access rows by column number or column name.
+ *
+ */
+
+trait RowData extends IndexedSeq[Any] {
+
+ /**
+ *
+ * Returns a column value by it's position in the originating query.
+ *
+ * @param columnNumber
+ * @return
+ */
+
+ def apply( columnNumber : Int ) : Any
+
+ /**
+ *
+ * Returns a column value by it's name in the originating query.
+ *
+ * @param columnName
+ * @return
+ */
+
+ def apply( columnName : String ) : Any
+
+ /**
+ *
+ * Number of this row in the query results. Counts start at 0.
+ *
+ * @return
+ */
+
+ def rowNumber : Int
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/SSLConfiguration.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/SSLConfiguration.scala
new file mode 100644
index 00000000..9ae657fe
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/SSLConfiguration.scala
@@ -0,0 +1,31 @@
+package com.github.mauricio.async.db
+
+import java.io.File
+
+import SSLConfiguration.Mode
+
+/**
+ *
+ * Contains the SSL configuration necessary to connect to a database.
+ *
+ * @param mode whether and with what priority a SSL connection will be negotiated, default disabled
+ * @param rootCert path to PEM encoded trusted root certificates, None to use internal JDK cacerts, defaults to None
+ *
+ */
+case class SSLConfiguration(mode: Mode.Value = Mode.Disable, rootCert: Option[java.io.File] = None)
+
+object SSLConfiguration {
+
+ object Mode extends Enumeration {
+ val Disable = Value("disable") // only try a non-SSL connection
+ val Prefer = Value("prefer") // first try an SSL connection; if that fails, try a non-SSL connection
+ val Require = Value("require") // only try an SSL connection, but don't verify Certificate Authority
+ val VerifyCA = Value("verify-ca") // only try an SSL connection, and verify that the server certificate is issued by a trusted certificate authority (CA)
+ val VerifyFull = Value("verify-full") // only try an SSL connection, verify that the server certificate is issued by a trusted CA and that the server host name matches that in the certificate
+ }
+
+ def apply(properties: Map[String, String]): SSLConfiguration = SSLConfiguration(
+ mode = Mode.withName(properties.get("sslmode").getOrElse("disable")),
+ rootCert = properties.get("sslrootcert").map(new File(_))
+ )
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/BigDecimalEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/BigDecimalEncoderDecoder.scala
new file mode 100644
index 00000000..110a2ba7
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/BigDecimalEncoderDecoder.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+object BigDecimalEncoderDecoder extends ColumnEncoderDecoder {
+
+ override def decode(value: String): Any = BigDecimal(value)
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/BigIntegerEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/BigIntegerEncoderDecoder.scala
new file mode 100644
index 00000000..41a347cf
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/BigIntegerEncoderDecoder.scala
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+object BigIntegerEncoderDecoder extends ColumnEncoderDecoder {
+ override def decode(value: String): Any = BigInt(value)
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ByteDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ByteDecoder.scala
new file mode 100644
index 00000000..ea5b3f05
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ByteDecoder.scala
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+object ByteDecoder extends ColumnDecoder {
+ override def decode(value: String): Any = value.toByte
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnDecoder.scala
new file mode 100644
index 00000000..a7ea19f2
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnDecoder.scala
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import java.nio.charset.Charset
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.general.ColumnData
+
+trait ColumnDecoder {
+
+ def decode( kind : ColumnData, value : ByteBuf, charset : Charset ) : Any = {
+ val bytes = new Array[Byte](value.readableBytes())
+ value.readBytes(bytes)
+ decode(new String(bytes, charset))
+ }
+
+ def decode( value : String ) : Any
+
+ def supportsStringDecoding : Boolean = true
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnDecoderRegistry.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnDecoderRegistry.scala
new file mode 100644
index 00000000..5f3af8ab
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnDecoderRegistry.scala
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import java.nio.charset.Charset
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.general.ColumnData
+
+trait ColumnDecoderRegistry {
+
+ def decode(kind: ColumnData, value: ByteBuf, charset : Charset) : Any
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnEncoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnEncoder.scala
new file mode 100644
index 00000000..1f2e8bf8
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnEncoder.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+trait ColumnEncoder {
+
+ def encode(value: Any): String = value.toString
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnEncoderDecoder.scala
new file mode 100644
index 00000000..520c931c
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnEncoderDecoder.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+trait ColumnEncoderDecoder extends ColumnEncoder with ColumnDecoder
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnEncoderRegistry.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnEncoderRegistry.scala
new file mode 100644
index 00000000..3b540e80
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ColumnEncoderRegistry.scala
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+trait ColumnEncoderRegistry {
+
+ def encode( value : Any ) : String
+
+ def kindOf( value : Any ) : Int
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/DateEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/DateEncoderDecoder.scala
new file mode 100644
index 00000000..53c2f2dd
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/DateEncoderDecoder.scala
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import org.joda.time.format.DateTimeFormat
+import org.joda.time.{ReadablePartial, LocalDate}
+import com.github.mauricio.async.db.exceptions.DateEncoderNotAvailableException
+
+object DateEncoderDecoder extends ColumnEncoderDecoder {
+
+ private val ZeroedDate = "0000-00-00"
+
+ private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd")
+
+ override def decode(value: String): LocalDate =
+ if ( ZeroedDate == value ) {
+ null
+ } else {
+ this.formatter.parseLocalDate(value)
+ }
+
+ override def encode(value: Any): String = {
+ value match {
+ case d: java.sql.Date => this.formatter.print(new LocalDate(d))
+ case d: ReadablePartial => this.formatter.print(d)
+ case _ => throw new DateEncoderNotAvailableException(value)
+ }
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/DoubleEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/DoubleEncoderDecoder.scala
new file mode 100644
index 00000000..70836d42
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/DoubleEncoderDecoder.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+
+object DoubleEncoderDecoder extends ColumnEncoderDecoder {
+ override def decode(value: String): Double = value.toDouble
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/FloatEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/FloatEncoderDecoder.scala
new file mode 100644
index 00000000..bcae2049
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/FloatEncoderDecoder.scala
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+object FloatEncoderDecoder extends ColumnEncoderDecoder {
+ override def decode(value: String): Float = value.toFloat
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/InetAddressEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/InetAddressEncoderDecoder.scala
new file mode 100644
index 00000000..ecac853d
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/InetAddressEncoderDecoder.scala
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import java.net.InetAddress
+import sun.net.util.IPAddressUtil.{textToNumericFormatV4,textToNumericFormatV6}
+
+object InetAddressEncoderDecoder extends ColumnEncoderDecoder {
+
+ override def decode(value: String): Any = {
+ if (value contains ':') {
+ InetAddress.getByAddress(textToNumericFormatV6(value))
+ } else {
+ InetAddress.getByAddress(textToNumericFormatV4(value))
+ }
+ }
+
+ override def encode(value: Any): String = {
+ value.asInstanceOf[InetAddress].getHostAddress
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/IntegerEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/IntegerEncoderDecoder.scala
new file mode 100644
index 00000000..ed8b4fa5
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/IntegerEncoderDecoder.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+object IntegerEncoderDecoder extends ColumnEncoderDecoder {
+
+ override def decode(value: String): Int = value.toInt
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/LocalDateTimeEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/LocalDateTimeEncoderDecoder.scala
new file mode 100644
index 00000000..6cb67ad9
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/LocalDateTimeEncoderDecoder.scala
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import org.joda.time.format.DateTimeFormatterBuilder
+import org.joda.time.LocalDateTime
+
+object LocalDateTimeEncoderDecoder extends ColumnEncoderDecoder {
+
+ private val ZeroedTimestamp = "0000-00-00 00:00:00"
+
+ private val optional = new DateTimeFormatterBuilder()
+ .appendPattern(".SSSSSS").toParser
+
+ private val format = new DateTimeFormatterBuilder()
+ .appendPattern("yyyy-MM-dd HH:mm:ss")
+ .appendOptional(optional)
+ .toFormatter
+
+ override def encode(value: Any): String =
+ format.print(value.asInstanceOf[LocalDateTime])
+
+ override def decode(value: String): LocalDateTime =
+ if (ZeroedTimestamp == value) {
+ null
+ } else {
+ format.parseLocalDateTime(value)
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/LongEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/LongEncoderDecoder.scala
new file mode 100644
index 00000000..3f9bdef2
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/LongEncoderDecoder.scala
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+object LongEncoderDecoder extends ColumnEncoderDecoder {
+ override def decode(value: String): Long = value.toLong
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/SQLTimeEncoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/SQLTimeEncoder.scala
new file mode 100644
index 00000000..b075754d
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/SQLTimeEncoder.scala
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import org.joda.time.format.DateTimeFormatterBuilder
+import org.joda.time.LocalTime
+
+object SQLTimeEncoder extends ColumnEncoder {
+
+ final private val format = new DateTimeFormatterBuilder()
+ .appendPattern("HH:mm:ss")
+ .toFormatter
+
+ override def encode(value: Any): String = {
+ val time = value.asInstanceOf[java.sql.Time]
+
+ format.print( new LocalTime(time.getTime) )
+ }
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ShortEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ShortEncoderDecoder.scala
new file mode 100644
index 00000000..77071746
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/ShortEncoderDecoder.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+object ShortEncoderDecoder extends ColumnEncoderDecoder {
+
+ override def decode(value: String): Any = value.toShort
+
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/StringEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/StringEncoderDecoder.scala
new file mode 100644
index 00000000..18c2adb5
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/StringEncoderDecoder.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+
+object StringEncoderDecoder extends ColumnEncoderDecoder {
+ override def decode(value: String): String = value
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimeEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimeEncoderDecoder.scala
new file mode 100644
index 00000000..a7d0c879
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimeEncoderDecoder.scala
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import org.joda.time.LocalTime
+import org.joda.time.format.DateTimeFormatterBuilder
+
+object TimeEncoderDecoder {
+ val Instance = new TimeEncoderDecoder()
+}
+
+class TimeEncoderDecoder extends ColumnEncoderDecoder {
+
+ final private val optional = new DateTimeFormatterBuilder()
+ .appendPattern(".SSSSSS").toParser
+
+ final private val format = new DateTimeFormatterBuilder()
+ .appendPattern("HH:mm:ss")
+ .appendOptional(optional)
+ .toFormatter
+
+ final private val printer = new DateTimeFormatterBuilder()
+ .appendPattern("HH:mm:ss.SSSSSS")
+ .toFormatter
+
+ def formatter = format
+
+ override def decode(value: String): LocalTime =
+ format.parseLocalTime(value)
+
+ override def encode(value: Any): String =
+ this.printer.print(value.asInstanceOf[LocalTime])
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimeWithTimezoneEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimeWithTimezoneEncoderDecoder.scala
new file mode 100644
index 00000000..f4e9a18c
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimeWithTimezoneEncoderDecoder.scala
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import org.joda.time.format.DateTimeFormat
+
+object TimeWithTimezoneEncoderDecoder extends TimeEncoderDecoder {
+
+ private val format = DateTimeFormat.forPattern("HH:mm:ss.SSSSSSZ")
+
+ override def formatter = format
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimestampEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimestampEncoderDecoder.scala
new file mode 100644
index 00000000..c3e32ac2
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimestampEncoderDecoder.scala
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import com.github.mauricio.async.db.exceptions.DateEncoderNotAvailableException
+import java.sql.Timestamp
+import java.util.{Calendar, Date}
+import org.joda.time._
+import org.joda.time.format.DateTimeFormatterBuilder
+
+object TimestampEncoderDecoder {
+ val BaseFormat = "yyyy-MM-dd HH:mm:ss"
+ val MillisFormat = ".SSSSSS"
+ val Instance = new TimestampEncoderDecoder()
+}
+
+class TimestampEncoderDecoder extends ColumnEncoderDecoder {
+
+ import TimestampEncoderDecoder._
+
+ private val optional = new DateTimeFormatterBuilder()
+ .appendPattern(MillisFormat).toParser
+ private val optionalTimeZone = new DateTimeFormatterBuilder()
+ .appendPattern("Z").toParser
+
+ private val builder = new DateTimeFormatterBuilder()
+ .appendPattern(BaseFormat)
+ .appendOptional(optional)
+ .appendOptional(optionalTimeZone)
+
+ private val timezonedPrinter = new DateTimeFormatterBuilder()
+ .appendPattern(s"${BaseFormat}${MillisFormat}Z").toFormatter
+
+ private val nonTimezonedPrinter = new DateTimeFormatterBuilder()
+ .appendPattern(s"${BaseFormat}${MillisFormat}").toFormatter
+
+ private val format = builder.toFormatter
+
+ def formatter = format
+
+ override def decode(value: String): Any = {
+ formatter.parseLocalDateTime(value)
+ }
+
+ override def encode(value: Any): String = {
+ value match {
+ case t: Timestamp => this.timezonedPrinter.print(new DateTime(t))
+ case t: Date => this.timezonedPrinter.print(new DateTime(t))
+ case t: Calendar => this.timezonedPrinter.print(new DateTime(t))
+ case t: LocalDateTime => this.nonTimezonedPrinter.print(t)
+ case t: ReadableDateTime => this.timezonedPrinter.print(t)
+ case _ => throw new DateEncoderNotAvailableException(value)
+ }
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimestampWithTimezoneEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimestampWithTimezoneEncoderDecoder.scala
new file mode 100644
index 00000000..258f23ee
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/TimestampWithTimezoneEncoderDecoder.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import org.joda.time.DateTime
+import org.joda.time.format.DateTimeFormat
+
+object TimestampWithTimezoneEncoderDecoder extends TimestampEncoderDecoder {
+
+ private val format = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSSSSZ")
+
+ override def formatter = format
+
+ override def decode(value: String): Any = {
+ formatter.parseDateTime(value)
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/column/UUIDEncoderDecoder.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/UUIDEncoderDecoder.scala
new file mode 100644
index 00000000..11987835
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/column/UUIDEncoderDecoder.scala
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import java.util.UUID
+
+object UUIDEncoderDecoder extends ColumnEncoderDecoder {
+
+ override def decode(value: String): UUID = UUID.fromString(value)
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/BufferNotFullyConsumedException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/BufferNotFullyConsumedException.scala
new file mode 100644
index 00000000..52e7481f
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/BufferNotFullyConsumedException.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+import io.netty.buffer.ByteBuf
+
+class BufferNotFullyConsumedException ( buffer : ByteBuf )
+ extends DatabaseException( "Buffer was not fully consumed by decoder, %s bytes to read".format(buffer.readableBytes()) )
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/CanceledChannelFutureException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/CanceledChannelFutureException.scala
new file mode 100644
index 00000000..db1168e1
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/CanceledChannelFutureException.scala
@@ -0,0 +1,22 @@
+/*
+* Copyright 2013 Maurício Linhares
+*
+* Maurício Linhares licenses this file to you under the Apache License,
+* version 2.0 (the "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at:
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations
+* under the License.
+*/
+
+package com.github.mauricio.async.db.exceptions
+
+import io.netty.channel.ChannelFuture
+
+class CanceledChannelFutureException( val channelFuture : ChannelFuture )
+ extends IllegalStateException ( "This channel future was canceled -> %s".format(channelFuture) )
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionNotConnectedException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionNotConnectedException.scala
new file mode 100644
index 00000000..31ef5f45
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionNotConnectedException.scala
@@ -0,0 +1,6 @@
+package com.github.mauricio.async.db.exceptions
+
+import com.github.mauricio.async.db.Connection
+
+class ConnectionNotConnectedException( val connection : Connection )
+ extends DatabaseException( "The connection %s is not connected to the database".format(connection) )
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionStillRunningQueryException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionStillRunningQueryException.scala
new file mode 100644
index 00000000..a6302410
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionStillRunningQueryException.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+class ConnectionStillRunningQueryException( connectionCount : Long, caughtRace : Boolean)
+ extends DatabaseException ( "[%s] - There is a query still being run here - race -> %s".format(
+ connectionCount,
+ caughtRace
+ ))
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionTimeoutedException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionTimeoutedException.scala
new file mode 100644
index 00000000..7e02c17c
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ConnectionTimeoutedException.scala
@@ -0,0 +1,6 @@
+package com.github.mauricio.async.db.exceptions
+
+import com.github.mauricio.async.db.Connection
+
+class ConnectionTimeoutedException( val connection : Connection )
+ extends DatabaseException( "The connection %s has a timeouted query and is being closed".format(connection) )
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/DatabaseException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/DatabaseException.scala
new file mode 100644
index 00000000..1975edcb
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/DatabaseException.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+class DatabaseException(message: String, cause : Throwable) extends RuntimeException(message) {
+
+ def this( message : String ) = this(message, null)
+
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/DateEncoderNotAvailableException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/DateEncoderNotAvailableException.scala
new file mode 100644
index 00000000..a30559b7
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/DateEncoderNotAvailableException.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+class DateEncoderNotAvailableException(value: Any)
+ extends DatabaseException("There is no encoder for value [%s] of type %s".format(value, value.getClass.getCanonicalName))
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/EncoderNotAvailableException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/EncoderNotAvailableException.scala
new file mode 100644
index 00000000..09ff916a
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/EncoderNotAvailableException.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+import com.github.mauricio.async.db.KindedMessage
+
+class EncoderNotAvailableException(message: KindedMessage)
+ extends DatabaseException("Encoder not available for name %s".format(message.kind))
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/InsufficientParametersException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/InsufficientParametersException.scala
new file mode 100644
index 00000000..7a9054ee
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/InsufficientParametersException.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+/**
+ *
+ * Raised when the user gives more or less parameters than the query takes. Each parameter is a ?
+ * (question mark) in the query string. The count of ? should be the same as the count of items in the provided
+ * sequence of parameters.
+ *
+ * @param expected the expected count of parameters
+ * @param given the collection given
+ */
+class InsufficientParametersException( expected : Int, given : Seq[Any] )
+ extends DatabaseException(
+ "The query contains %s parameters but you gave it %s (%s)".format(expected, given.length, given.mkString(",")
+ )
+ )
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/NegativeMessageSizeException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/NegativeMessageSizeException.scala
new file mode 100644
index 00000000..35d09a1f
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/NegativeMessageSizeException.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+class NegativeMessageSizeException( code : Byte, size : Int )
+ extends DatabaseException( "Message of type %d had negative size %s".format(code, size) )
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ParserNotAvailableException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ParserNotAvailableException.scala
new file mode 100644
index 00000000..06b3b549
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/ParserNotAvailableException.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+class ParserNotAvailableException(t: Byte)
+ extends DatabaseException("There is no parser available for message type '%s' (%s)".format(t, Integer.toHexString(t)))
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnableToParseURLException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnableToParseURLException.scala
new file mode 100644
index 00000000..0d2799df
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnableToParseURLException.scala
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2016 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+/**
+ * Thrown to indicate that a URL Parser could not understand the provided URL.
+ */
+class UnableToParseURLException(message: String, base: Throwable) extends RuntimeException(message, base) {
+ def this(message: String) = this(message, null)
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnknownLengthException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnknownLengthException.scala
new file mode 100644
index 00000000..6cae6f68
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnknownLengthException.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+class UnknownLengthException ( length : Int )
+ extends DatabaseException( "Can't handle the length %d".format(length) )
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnsupportedAuthenticationMethodException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnsupportedAuthenticationMethodException.scala
new file mode 100644
index 00000000..c28e8c1e
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/exceptions/UnsupportedAuthenticationMethodException.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.exceptions
+
+class UnsupportedAuthenticationMethodException(val authenticationType: String)
+ extends DatabaseException("Unknown authentication method -> '%s'".format(authenticationType)) {
+
+ def this( authType : Int ) {
+ this(authType.toString)
+ }
+
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/general/ArrayRowData.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/general/ArrayRowData.scala
new file mode 100644
index 00000000..fe582481
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/general/ArrayRowData.scala
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.general
+
+import com.github.mauricio.async.db.RowData
+
+class ArrayRowData(row : Int, val mapping : Map[String, Int], val columns : Array[Any]) extends RowData
+{
+
+ /**
+ *
+ * Returns a column value by it's position in the originating query.
+ *
+ * @param columnNumber
+ * @return
+ */
+ def apply(columnNumber: Int): Any = columns(columnNumber)
+
+ /**
+ *
+ * Returns a column value by it's name in the originating query.
+ *
+ * @param columnName
+ * @return
+ */
+ def apply(columnName: String): Any = columns( mapping(columnName) )
+
+ /**
+ *
+ * Number of this row in the query results. Counts start at 0.
+ *
+ * @return
+ */
+ def rowNumber: Int = row
+
+ def length: Int = columns.length
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/general/ColumnData.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/general/ColumnData.scala
new file mode 100644
index 00000000..4799c8da
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/general/ColumnData.scala
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.general
+
+trait ColumnData {
+
+ def name : String
+ def dataType : Int
+ def dataTypeSize : Long
+
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/general/MutableResultSet.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/general/MutableResultSet.scala
new file mode 100644
index 00000000..00cc712b
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/general/MutableResultSet.scala
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.general
+
+import collection.mutable.ArrayBuffer
+import com.github.mauricio.async.db.{RowData, ResultSet}
+import com.github.mauricio.async.db.util.Log
+
+object MutableResultSet {
+ val log = Log.get[MutableResultSet[Nothing]]
+}
+
+class MutableResultSet[T <: ColumnData](
+ val columnTypes: IndexedSeq[T]) extends ResultSet {
+
+ private val rows = new ArrayBuffer[RowData]()
+ private val columnMapping: Map[String, Int] = this.columnTypes.indices.map(
+ index =>
+ ( this.columnTypes(index).name, index ) ).toMap
+
+
+ val columnNames : IndexedSeq[String] = this.columnTypes.map(c => c.name)
+
+ val types : IndexedSeq[Int] = this.columnTypes.map(c => c.dataType)
+
+ override def length: Int = this.rows.length
+
+ override def apply(idx: Int): RowData = this.rows(idx)
+
+ def addRow(row : Array[Any] ) {
+ this.rows += new ArrayRowData(this.rows.size, this.columnMapping, row)
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/AsyncObjectPool.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/AsyncObjectPool.scala
new file mode 100644
index 00000000..3e4345a8
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/AsyncObjectPool.scala
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.pool
+
+import scala.concurrent.{ExecutionContext, Future, Promise}
+
+/**
+ *
+ * Defines the common interface for async object pools. These are pools that do not block clients trying to acquire
+ * a resource from it. Different than the usual synchronous pool, you **must** return objects back to it manually
+ * since it's impossible for the pool to know when the object is ready to be given back.
+ *
+ * @tparam T
+ */
+
+trait AsyncObjectPool[T] {
+
+ /**
+ *
+ * Returns an object from the pool to the callee with the returned future. If the pool can not create or enqueue
+ * requests it will fill the returned [[scala.concurrent.Future]] with an
+ * [[com.github.mauricio.async.db.pool.PoolExhaustedException]].
+ *
+ * @return future that will eventually return a usable pool object.
+ */
+
+ def take: Future[T]
+
+ /**
+ *
+ * Returns an object taken from the pool back to it. This object will become available for another client to use.
+ * If the object is invalid or can not be reused for some reason the [[scala.concurrent.Future]] returned will contain
+ * the error that prevented this object of being added back to the pool. The object is then discarded from the pool.
+ *
+ * @param item
+ * @return
+ */
+
+ def giveBack( item : T ) : Future[AsyncObjectPool[T]]
+
+ /**
+ *
+ * Closes this pool and future calls to **take** will cause the [[scala.concurrent.Future]] to raise an
+ * [[com.github.mauricio.async.db.pool.PoolAlreadyTerminatedException]].
+ *
+ * @return
+ */
+
+ def close : Future[AsyncObjectPool[T]]
+
+ /**
+ *
+ * Retrieve and use an object from the pool for a single computation, returning it when the operation completes.
+ *
+ * @param f function that uses the object
+ * @return f wrapped with take and giveBack
+ */
+
+ def use[A](f: (T) => Future[A])(implicit executionContext: ExecutionContext): Future[A] =
+ take.flatMap { item =>
+ val p = Promise[A]()
+ try {
+ f(item).onComplete { r =>
+ giveBack(item).onComplete { _ =>
+ p.complete(r)
+ }
+ }
+ } catch {
+ // calling f might throw exception.
+ // in that case the item will be removed from the pool if identified as invalid by the factory.
+ // the error returned to the user is the original error thrown by f.
+ case error: Throwable =>
+ giveBack(item).onComplete { _ =>
+ p.failure(error)
+ }
+ }
+
+ p.future
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/ConnectionPool.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/ConnectionPool.scala
new file mode 100644
index 00000000..2a3add4b
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/ConnectionPool.scala
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.pool
+
+import com.github.mauricio.async.db.util.ExecutorServiceUtils
+import com.github.mauricio.async.db.{QueryResult, Connection}
+import scala.concurrent.{ExecutionContext, Future}
+
+/**
+ *
+ * Pool specialized in database connections that also simplifies connection handling by
+ * implementing the [[com.github.mauricio.async.db.Connection]] trait and saving clients from having to implement
+ * the "give back" part of pool management. This lets you do your job without having to worry
+ * about managing and giving back connection objects to the pool.
+ *
+ * The downside of this is that you should not start transactions or any kind of long running process
+ * in this object as the object will be sent back to the pool right after executing a query. If you
+ * need to start transactions you will have to take an object from the pool, do it and then give it
+ * back manually.
+ *
+ * @param factory
+ * @param configuration
+ */
+
+class ConnectionPool[T <: Connection](
+ factory: ObjectFactory[T],
+ configuration: PoolConfiguration,
+ executionContext: ExecutionContext = ExecutorServiceUtils.CachedExecutionContext
+ )
+ extends SingleThreadedAsyncObjectPool[T](factory, configuration)
+ with Connection {
+
+ /**
+ *
+ * Closes the pool, you should discard the object.
+ *
+ * @return
+ */
+
+ def disconnect: Future[Connection] = if ( this.isConnected ) {
+ this.close.map(item => this)(executionContext)
+ } else {
+ Future.successful(this)
+ }
+
+ /**
+ *
+ * Always returns an empty map.
+ *
+ * @return
+ */
+
+ def connect: Future[Connection] = Future.successful(this)
+
+ def isConnected: Boolean = !this.isClosed
+
+ /**
+ *
+ * Picks one connection and runs this query against it. The query should be stateless, it should not
+ * start transactions and should not leave anything to be cleaned up in the future. The behavior of this
+ * object is undefined if you start a transaction from this method.
+ *
+ * @param query
+ * @return
+ */
+
+ def sendQuery(query: String): Future[QueryResult] =
+ this.use(_.sendQuery(query))(executionContext)
+
+ /**
+ *
+ * Picks one connection and runs this query against it. The query should be stateless, it should not
+ * start transactions and should not leave anything to be cleaned up in the future. The behavior of this
+ * object is undefined if you start a transaction from this method.
+ *
+ * @param query
+ * @param values
+ * @return
+ */
+
+ def sendPreparedStatement(query: String, values: Seq[Any] = List()): Future[QueryResult] =
+ this.use(_.sendPreparedStatement(query, values))(executionContext)
+
+ /**
+ *
+ * Picks one connection and executes an (asynchronous) function on it within a transaction block.
+ * If the function completes successfully, the transaction is committed, otherwise it is aborted.
+ * Either way, the connection is returned to the pool on completion.
+ *
+ * @param f operation to execute on a connection
+ * @return result of f, conditional on transaction operations succeeding
+ */
+
+ override def inTransaction[A](f : Connection => Future[A])(implicit context : ExecutionContext = executionContext) : Future[A] =
+ this.use(_.inTransaction[A](f)(context))(executionContext)
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/ObjectFactory.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/ObjectFactory.scala
new file mode 100644
index 00000000..bf6c610e
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/ObjectFactory.scala
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.pool
+
+import scala.util.Try
+
+/**
+ *
+ * Definition for objects that can be used as a factory for [[com.github.mauricio.async.db.pool.AsyncObjectPool]]
+ * objects.
+ *
+ * @tparam T the kind of object this factory produces.
+ */
+
+trait ObjectFactory[T] {
+
+ /**
+ *
+ * Creates a valid object to be used in the pool. This method can block if necessary to make sure a correctly built
+ * is created.
+ *
+ * @return
+ */
+
+ def create : T
+
+ /**
+ *
+ * This method should "close" and release all resources acquired by the pooled object. This object will not be used
+ * anymore so any cleanup necessary to remove it from memory should be made in this method. Implementors should not
+ * raise an exception under any circumstances, the factory should log and clean up the exception itself.
+ *
+ * @param item
+ */
+
+ def destroy( item : T )
+
+ /**
+ *
+ * Validates that an object can still be used for it's purpose. This method should test the object to make sure
+ * it's still valid for clients to use. If you have a database connection, test if you are still connected, if you're
+ * accessing a file system, make sure you can still see and change the file.
+ *
+ * You decide how fast this method should return and what it will test, you should usually do something that's fast
+ * enough not to slow down the pool usage, since this call will be made whenever an object returns to the pool.
+ *
+ * If this object is not valid anymore, a [[scala.util.Failure]] should be returned, otherwise [[scala.util.Success]]
+ * should be the result of this call.
+ *
+ * @param item an object produced by this pool
+ * @return
+ */
+
+ def validate( item : T ) : Try[T]
+
+ /**
+ *
+ * Does a full test on the given object making sure it's still valid. Different than validate, that's called whenever
+ * an object is given back to the pool and should usually be fast, this method will be called when objects are
+ * idle to make sure they don't "timeout" or become stale in anyway.
+ *
+ * For convenience, this method defaults to call **validate** but you can implement it in a different way if you
+ * would like to.
+ *
+ * @param item an object produced by this pool
+ * @return
+ */
+
+ def test( item : T ) : Try[T] = validate(item)
+
+
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PartitionedAsyncObjectPool.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PartitionedAsyncObjectPool.scala
new file mode 100644
index 00000000..8561ed54
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PartitionedAsyncObjectPool.scala
@@ -0,0 +1,65 @@
+package com.github.mauricio.async.db.pool
+
+import scala.concurrent.Future
+import com.github.mauricio.async.db.util.ExecutorServiceUtils
+import scala.concurrent.Promise
+import java.util.concurrent.ConcurrentHashMap
+import scala.util.Success
+import scala.util.Failure
+
+class PartitionedAsyncObjectPool[T](
+ factory: ObjectFactory[T],
+ configuration: PoolConfiguration,
+ numberOfPartitions: Int)
+ extends AsyncObjectPool[T] {
+
+ import ExecutorServiceUtils.CachedExecutionContext
+
+ private val pools =
+ (0 until numberOfPartitions)
+ .map(_ -> new SingleThreadedAsyncObjectPool(factory, partitionConfig))
+ .toMap
+
+ private val checkouts = new ConcurrentHashMap[T, SingleThreadedAsyncObjectPool[T]]
+
+ def take: Future[T] = {
+ val pool = currentPool
+ pool.take.andThen {
+ case Success(conn) =>
+ checkouts.put(conn, pool)
+ case Failure(_) =>
+ }
+ }
+
+ def giveBack(item: T) =
+ checkouts
+ .remove(item)
+ .giveBack(item)
+ .map(_ => this)
+
+ def close =
+ Future.sequence(pools.values.map(_.close)).map {
+ _ => this
+ }
+
+ def availables: Traversable[T] = pools.values.map(_.availables).flatten
+
+ def inUse: Traversable[T] = pools.values.map(_.inUse).flatten
+
+ def queued: Traversable[Promise[T]] = pools.values.map(_.queued).flatten
+
+ protected def isClosed =
+ pools.values.forall(_.isClosed)
+
+ private def currentPool =
+ pools(currentThreadAffinity)
+
+ private def currentThreadAffinity =
+ (Thread.currentThread.getId % numberOfPartitions).toInt
+
+ private def partitionConfig =
+ configuration.copy(
+ maxObjects = configuration.maxObjects / numberOfPartitions,
+ maxQueueSize = configuration.maxQueueSize / numberOfPartitions
+ )
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PartitionedConnectionPool.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PartitionedConnectionPool.scala
new file mode 100644
index 00000000..698534f5
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PartitionedConnectionPool.scala
@@ -0,0 +1,33 @@
+package com.github.mauricio.async.db.pool;
+
+import com.github.mauricio.async.db.util.ExecutorServiceUtils
+import com.github.mauricio.async.db.{ QueryResult, Connection }
+import scala.concurrent.{ ExecutionContext, Future }
+
+class PartitionedConnectionPool[T <: Connection](
+ factory: ObjectFactory[T],
+ configuration: PoolConfiguration,
+ numberOfPartitions: Int,
+ executionContext: ExecutionContext = ExecutorServiceUtils.CachedExecutionContext)
+ extends PartitionedAsyncObjectPool[T](factory, configuration, numberOfPartitions)
+ with Connection {
+
+ def disconnect: Future[Connection] = if (this.isConnected) {
+ this.close.map(item => this)(executionContext)
+ } else {
+ Future.successful(this)
+ }
+
+ def connect: Future[Connection] = Future.successful(this)
+
+ def isConnected: Boolean = !this.isClosed
+
+ def sendQuery(query: String): Future[QueryResult] =
+ this.use(_.sendQuery(query))(executionContext)
+
+ def sendPreparedStatement(query: String, values: Seq[Any] = List()): Future[QueryResult] =
+ this.use(_.sendPreparedStatement(query, values))(executionContext)
+
+ override def inTransaction[A](f: Connection => Future[A])(implicit context: ExecutionContext = executionContext): Future[A] =
+ this.use(_.inTransaction[A](f)(context))(executionContext)
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolAlreadyTerminatedException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolAlreadyTerminatedException.scala
new file mode 100644
index 00000000..8626eb39
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolAlreadyTerminatedException.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+package com.github.mauricio.async.db.pool
+
+/**
+ *
+ * Thrown when the pool has already been closed.
+ *
+ */
+
+class PoolAlreadyTerminatedException extends IllegalStateException( "This pool has already been terminated" )
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolConfiguration.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolConfiguration.scala
new file mode 100644
index 00000000..0ac567f2
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolConfiguration.scala
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.pool
+
+object PoolConfiguration {
+ val Default = new PoolConfiguration(10, 4, 10)
+}
+
+/**
+ *
+ * Defines specific pieces of a pool's behavior.
+ *
+ * @param maxObjects how many objects this pool will hold
+ * @param maxIdle number of milliseconds for which the objects are going to be kept as idle (not in use by clients of the pool)
+ * @param maxQueueSize when there are no more objects, the pool can queue up requests to serve later then there
+ * are objects available, this is the maximum number of enqueued requests
+ * @param validationInterval pools will use this value as the timer period to validate idle objects.
+ */
+
+case class PoolConfiguration(
+ maxObjects: Int,
+ maxIdle: Long,
+ maxQueueSize: Int,
+ validationInterval: Long = 5000
+ )
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolExhaustedException.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolExhaustedException.scala
new file mode 100644
index 00000000..15bab085
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/PoolExhaustedException.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.pool
+
+/**
+ *
+ * Raised when a pool has reached it's limit of available objects.
+ *
+ * @param message
+ */
+
+class PoolExhaustedException( message : String ) extends IllegalStateException( message )
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala
new file mode 100644
index 00000000..b4f25ae2
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/SingleThreadedAsyncObjectPool.scala
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.pool
+
+import java.util.concurrent.RejectedExecutionException
+
+import com.github.mauricio.async.db.util.{Log, Worker}
+import java.util.concurrent.atomic.AtomicLong
+import java.util.{Timer, TimerTask}
+
+import scala.collection.mutable.{ArrayBuffer, Queue}
+import scala.concurrent.{Future, Promise}
+import scala.util.{Failure, Success}
+
+object SingleThreadedAsyncObjectPool {
+ val Counter = new AtomicLong()
+ val log = Log.get[SingleThreadedAsyncObjectPool[Nothing]]
+}
+
+/**
+ *
+ * Implements an [[com.github.mauricio.async.db.pool.AsyncObjectPool]] using a single thread from a
+ * fixed executor service as an event loop to cause all calls to be sequential.
+ *
+ * Once you are done with this object remember to call it's close method to clean up the thread pool and
+ * it's objects as this might prevent your application from ending.
+ *
+ * @param factory
+ * @param configuration
+ * @tparam T type of the object this pool holds
+ */
+
+class SingleThreadedAsyncObjectPool[T](
+ factory: ObjectFactory[T],
+ configuration: PoolConfiguration
+ ) extends AsyncObjectPool[T] {
+
+ import SingleThreadedAsyncObjectPool.{Counter, log}
+
+ private val mainPool = Worker()
+ private var poolables = List.empty[PoolableHolder[T]]
+ private val checkouts = new ArrayBuffer[T](configuration.maxObjects)
+ private val waitQueue = new Queue[Promise[T]]()
+ private val timer = new Timer("async-object-pool-timer-" + Counter.incrementAndGet(), true)
+ timer.scheduleAtFixedRate(new TimerTask {
+ def run() {
+ mainPool.action {
+ testObjects
+ }
+ }
+ }, configuration.validationInterval, configuration.validationInterval)
+
+ private var closed = false
+
+ /**
+ *
+ * Asks for an object from the pool, this object should be returned to the pool when not in use anymore.
+ *
+ * @return
+ */
+
+ def take: Future[T] = {
+
+ if (this.closed) {
+ return Promise.failed(new PoolAlreadyTerminatedException).future
+ }
+
+ val promise = Promise[T]()
+ this.checkout(promise)
+ promise.future
+ }
+
+ /**
+ *
+ * Returns an object to the pool. The object is validated before being added to the collection
+ * of available objects to make sure we have a usable object. If the object isn't valid it's discarded.
+ *
+ * @param item
+ * @return
+ */
+
+ def giveBack(item: T): Future[AsyncObjectPool[T]] = {
+ val promise = Promise[AsyncObjectPool[T]]()
+ this.mainPool.action {
+ // Ensure it came from this pool
+ val idx = this.checkouts.indexOf(item)
+ if(idx >= 0) {
+ this.checkouts.remove(idx)
+ this.factory.validate(item) match {
+ case Success(item) => {
+ this.addBack(item, promise)
+ }
+ case Failure(e) => {
+ this.factory.destroy(item)
+ promise.failure(e)
+ }
+ }
+ } else {
+ // It's already a failure but lets doublecheck why
+ val isFromOurPool = (item match {
+ case x: AnyRef => this.poolables.find(holder => x eq holder.item.asInstanceOf[AnyRef])
+ case _ => this.poolables.find(holder => item == holder.item)
+ }).isDefined
+
+ if(isFromOurPool) {
+ promise.failure(new IllegalStateException("This item has already been returned"))
+ } else {
+ promise.failure(new IllegalArgumentException("The returned item did not come from this pool."))
+ }
+ }
+ }
+
+ promise.future
+ }
+
+ def isFull: Boolean = this.poolables.isEmpty && this.checkouts.size == configuration.maxObjects
+
+ def close: Future[AsyncObjectPool[T]] = {
+ try {
+ val promise = Promise[AsyncObjectPool[T]]()
+ this.mainPool.action {
+ if (!this.closed) {
+ try {
+ this.timer.cancel()
+ this.mainPool.shutdown
+ this.closed = true
+ (this.poolables.map(i => i.item) ++ this.checkouts).foreach(item => factory.destroy(item))
+ promise.success(this)
+ } catch {
+ case e: Exception => promise.failure(e)
+ }
+ } else {
+ promise.success(this)
+ }
+ }
+ promise.future
+ } catch {
+ case e: RejectedExecutionException if this.closed =>
+ Future.successful(this)
+ }
+ }
+
+ def availables: Traversable[T] = this.poolables.map(item => item.item)
+
+ def inUse: Traversable[T] = this.checkouts
+
+ def queued: Traversable[Promise[T]] = this.waitQueue
+
+ def isClosed: Boolean = this.closed
+
+ /**
+ *
+ * Adds back an object that was in use to the list of poolable objects.
+ *
+ * @param item
+ * @param promise
+ */
+
+ private def addBack(item: T, promise: Promise[AsyncObjectPool[T]]) {
+ this.poolables ::= new PoolableHolder[T](item)
+
+ if (this.waitQueue.nonEmpty) {
+ this.checkout(this.waitQueue.dequeue())
+ }
+
+ promise.success(this)
+ }
+
+ /**
+ *
+ * Enqueues a promise to be fulfilled in the future when objects are sent back to the pool. If
+ * we have already reached the limit of enqueued objects, fail the promise.
+ *
+ * @param promise
+ */
+
+ private def enqueuePromise(promise: Promise[T]) {
+ if (this.waitQueue.size >= configuration.maxQueueSize) {
+ val exception = new PoolExhaustedException("There are no objects available and the waitQueue is full")
+ exception.fillInStackTrace()
+ promise.failure(exception)
+ } else {
+ this.waitQueue += promise
+ }
+ }
+
+ private def checkout(promise: Promise[T]) {
+ this.mainPool.action {
+ if (this.isFull) {
+ this.enqueuePromise(promise)
+ } else {
+ this.createOrReturnItem(promise)
+ }
+ }
+ }
+
+ /**
+ *
+ * Checks if there is a poolable object available and returns it to the promise.
+ * If there are no objects available, create a new one using the factory and return it.
+ *
+ * @param promise
+ */
+
+ private def createOrReturnItem(promise: Promise[T]) {
+ if (this.poolables.isEmpty) {
+ try {
+ val item = this.factory.create
+ this.checkouts += item
+ promise.success(item)
+ } catch {
+ case e: Exception => promise.failure(e)
+ }
+ } else {
+ val h :: t = this.poolables
+ this.poolables = t
+ val item = h.item
+ this.checkouts += item
+ promise.success(item)
+ }
+ }
+
+ override def finalize() {
+ this.close
+ }
+
+ /**
+ *
+ * Validates pooled objects not in use to make sure they are all usable, great if
+ * you're holding onto network connections since you can "ping" the destination
+ * to keep the connection alive.
+ *
+ */
+
+ private def testObjects {
+ val removals = new ArrayBuffer[PoolableHolder[T]]()
+ this.poolables.foreach {
+ poolable =>
+ this.factory.test(poolable.item) match {
+ case Success(item) => {
+ if (poolable.timeElapsed > configuration.maxIdle) {
+ log.debug("Connection was idle for {}, maxIdle is {}, removing it", poolable.timeElapsed, configuration.maxIdle)
+ removals += poolable
+ factory.destroy(poolable.item)
+ }
+ }
+ case Failure(e) => {
+ log.error("Failed to validate object", e)
+ removals += poolable
+ factory.destroy(poolable.item)
+ }
+ }
+ }
+ this.poolables = this.poolables.diff(removals)
+ }
+
+ private class PoolableHolder[T](val item: T) {
+ val time = System.currentTimeMillis()
+
+ def timeElapsed = System.currentTimeMillis() - time
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/TimeoutScheduler.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/TimeoutScheduler.scala
new file mode 100644
index 00000000..d97a9ca1
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/pool/TimeoutScheduler.scala
@@ -0,0 +1,63 @@
+package com.github.mauricio.async.db.pool
+
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.{TimeUnit, TimeoutException, ScheduledFuture}
+import io.netty.channel.EventLoopGroup
+import scala.concurrent.{ExecutionContext, Promise}
+import scala.concurrent.duration.Duration
+
+trait TimeoutScheduler {
+
+ private var isTimeoutedBool = new AtomicBoolean(false)
+
+ /**
+ *
+ * The event loop group to be used for scheduling.
+ *
+ * @return
+ */
+
+ def eventLoopGroup : EventLoopGroup
+
+ /**
+ * Implementors should decide here what they want to do when a timeout occur
+ */
+
+ def onTimeout // implementors should decide here what they want to do when a timeout occur
+
+ /**
+ *
+ * We need this property as isClosed takes time to complete and
+ * we don't want the connection to be used again.
+ *
+ * @return
+ */
+
+ def isTimeouted : Boolean =
+ isTimeoutedBool.get
+
+ def addTimeout[A](
+ promise: Promise[A],
+ durationOption: Option[Duration])
+ (implicit executionContext : ExecutionContext) : Option[ScheduledFuture[_]] = {
+ durationOption.map {
+ duration =>
+ val scheduledFuture = schedule(
+ {
+ if (promise.tryFailure(new TimeoutException(s"Operation is timeouted after it took too long to return (${duration})"))) {
+ isTimeoutedBool.set(true)
+ onTimeout
+ }
+ },
+ duration)
+ promise.future.onComplete(x => scheduledFuture.cancel(false))
+
+ scheduledFuture
+ }
+ }
+
+ def schedule(block: => Unit, duration: Duration) : ScheduledFuture[_] =
+ eventLoopGroup.schedule(new Runnable {
+ override def run(): Unit = block
+ }, duration.toMillis, TimeUnit.MILLISECONDS)
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/AbstractURIParser.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/AbstractURIParser.scala
new file mode 100644
index 00000000..e18de6e1
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/AbstractURIParser.scala
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2016 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package com.github.mauricio.async.db.util
+
+import java.net.{URI, URISyntaxException, URLDecoder}
+import java.nio.charset.Charset
+
+import com.github.mauricio.async.db.exceptions.UnableToParseURLException
+import com.github.mauricio.async.db.{Configuration, SSLConfiguration}
+import org.slf4j.LoggerFactory
+
+import scala.util.matching.Regex
+
+/**
+ * Common parser assisting methods for PG and MySQL URI parsers.
+ */
+abstract class AbstractURIParser {
+ import AbstractURIParser._
+
+ protected val logger = LoggerFactory.getLogger(getClass)
+
+ /**
+ * Parses out userInfo into a tuple of optional username and password
+ *
+ * @param userInfo the optional user info string
+ * @return a tuple of optional username and password
+ */
+ final protected def parseUserInfo(userInfo: Option[String]): (Option[String], Option[String]) = userInfo.map(_.split(":", 2).toList) match {
+ case Some(user :: pass :: Nil) ⇒ (Some(user), Some(pass))
+ case Some(user :: Nil) ⇒ (Some(user), None)
+ case _ ⇒ (None, None)
+ }
+
+ /**
+ * A Regex that will match the base name of the driver scheme, minus jdbc:.
+ * Eg: postgres(?:ul)?
+ */
+ protected val SCHEME: Regex
+
+ /**
+ * The default for this particular URLParser, ie: appropriate and specific to PG or MySQL accordingly
+ */
+ val DEFAULT: Configuration
+
+
+ /**
+ * Parses the provided url and returns a Configuration based upon it. On an error,
+ * @param url the URL to parse.
+ * @param charset the charset to use.
+ * @return a Configuration.
+ */
+ @throws[UnableToParseURLException]("if the URL does not match the expected type, or cannot be parsed for any reason")
+ def parseOrDie(url: String,
+ charset: Charset = DEFAULT.charset): Configuration = {
+ try {
+ val properties = parse(new URI(url).parseServerAuthority)
+
+ assembleConfiguration(properties, charset)
+ } catch {
+ case e: URISyntaxException =>
+ throw new UnableToParseURLException(s"Failed to parse URL: $url", e)
+ }
+ }
+
+
+ /**
+ * Parses the provided url and returns a Configuration based upon it. On an error,
+ * a default configuration is returned.
+ * @param url the URL to parse.
+ * @param charset the charset to use.
+ * @return a Configuration.
+ */
+ def parse(url: String,
+ charset: Charset = DEFAULT.charset
+ ): Configuration = {
+ try {
+ parseOrDie(url, charset)
+ } catch {
+ case e: Exception =>
+ logger.warn(s"Connection url '$url' could not be parsed.", e)
+ // Fallback to default to maintain current behavior
+ DEFAULT
+ }
+ }
+
+ /**
+ * Assembles a configuration out of the provided property map. This is the generic form, subclasses may override to
+ * handle additional properties.
+ * @param properties the extracted properties from the URL.
+ * @param charset the charset passed in to parse or parseOrDie.
+ * @return
+ */
+ protected def assembleConfiguration(properties: Map[String, String], charset: Charset): Configuration = {
+ DEFAULT.copy(
+ username = properties.getOrElse(USERNAME, DEFAULT.username),
+ password = properties.get(PASSWORD),
+ database = properties.get(DBNAME),
+ host = properties.getOrElse(HOST, DEFAULT.host),
+ port = properties.get(PORT).map(_.toInt).getOrElse(DEFAULT.port),
+ ssl = SSLConfiguration(properties),
+ charset = charset
+ )
+ }
+
+
+ protected def parse(uri: URI): Map[String, String] = {
+ uri.getScheme match {
+ case SCHEME() =>
+ val userInfo = parseUserInfo(Option(uri.getUserInfo))
+
+ val port = Some(uri.getPort).filter(_ > 0)
+ val db = Option(uri.getPath).map(_.stripPrefix("/")).filterNot(_.isEmpty)
+ val host = Option(uri.getHost)
+
+ val builder = Map.newBuilder[String, String]
+ builder ++= userInfo._1.map(USERNAME -> _)
+ builder ++= userInfo._2.map(PASSWORD -> _)
+ builder ++= port.map(PORT -> _.toString)
+ builder ++= db.map(DBNAME -> _)
+ builder ++= host.map(HOST -> unwrapIpv6address(_))
+
+ // Parse query string parameters and just append them, overriding anything previously set
+ builder ++= (for {
+ qs <- Option(uri.getQuery).toSeq
+ parameter <- qs.split('&')
+ Array(name, value) = parameter.split('=')
+ if name.nonEmpty && value.nonEmpty
+ } yield URLDecoder.decode(name, "UTF-8") -> URLDecoder.decode(value, "UTF-8"))
+
+
+ builder.result
+ case "jdbc" =>
+ handleJDBC(uri)
+ case _ =>
+ throw new UnableToParseURLException("Unrecognized URI scheme")
+ }
+ }
+
+ /**
+ * This method breaks out handling of the jdbc: prefixed uri's, allowing them to be handled differently
+ * without reimplementing all of parse.
+ */
+ protected def handleJDBC(uri: URI): Map[String, String] = parse(new URI(uri.getSchemeSpecificPart))
+
+
+ final protected def unwrapIpv6address(server: String): String = {
+ if (server.startsWith("[")) {
+ server.substring(1, server.length() - 1)
+ } else server
+ }
+
+}
+
+object AbstractURIParser {
+ // Constants and value names
+ val PORT = "port"
+ val DBNAME = "database"
+ val HOST = "host"
+ val USERNAME = "user"
+ val PASSWORD = "password"
+}
+
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ByteBufferUtils.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ByteBufferUtils.scala
new file mode 100644
index 00000000..1cd73dce
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ByteBufferUtils.scala
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import java.nio.charset.Charset
+import java.nio.ByteOrder
+import io.netty.buffer.{Unpooled, ByteBuf}
+
+object ByteBufferUtils {
+
+ def writeLength(buffer: ByteBuf) {
+
+ val length = buffer.writerIndex() - 1
+ buffer.markWriterIndex()
+ buffer.writerIndex(1)
+ buffer.writeInt(length)
+
+ buffer.resetWriterIndex()
+
+ }
+
+ def writeCString(content: String, b: ByteBuf, charset: Charset): Unit = {
+ b.writeBytes(content.getBytes(charset))
+ b.writeByte(0)
+ }
+
+ def writeSizedString( content : String, b : ByteBuf, charset : Charset ) {
+ val bytes = content.getBytes(charset)
+ b.writeByte(bytes.length)
+ b.writeBytes(bytes)
+ }
+
+ def readCString(b: ByteBuf, charset: Charset): String = {
+ b.markReaderIndex()
+
+ var byte: Byte = 0
+ var count = 0
+
+ do {
+ byte = b.readByte()
+ count += 1
+ } while (byte != 0)
+
+ b.resetReaderIndex()
+
+ val result = b.toString(b.readerIndex(), count - 1, charset)
+
+ b.readerIndex(b.readerIndex() + count)
+
+ result
+ }
+
+ def readUntilEOF( b : ByteBuf, charset : Charset ) : String = {
+ if ( b.readableBytes() == 0 ) {
+ return ""
+ }
+
+ b.markReaderIndex()
+
+ var byte: Byte = -1
+ var count = 0
+ var offset = 1
+
+ while (byte != 0) {
+ if ( b.readableBytes() > 0 ) {
+ byte = b.readByte()
+ count += 1
+ } else {
+ byte = 0
+ offset = 0
+ }
+ }
+
+ b.resetReaderIndex()
+
+ val result = b.toString(b.readerIndex(), count - offset, charset)
+
+ b.readerIndex(b.readerIndex() + count)
+
+ result
+ }
+
+ def read3BytesInt( b : ByteBuf ) : Int = {
+ (b.readByte() & 0xff) | ((b.readByte() & 0xff) << 8) | ((b.readByte() & 0xff) << 16)
+ }
+
+ def write3BytesInt( b : ByteBuf, value : Int ) {
+ b.writeByte( value & 0xff )
+ b.writeByte( value >>> 8 )
+ b.writeByte( value >>> 16 )
+ }
+
+ def writePacketLength(buffer: ByteBuf, sequence : Int = 1) {
+ val length = buffer.writerIndex() - 4
+ buffer.markWriterIndex()
+ buffer.writerIndex(0)
+
+ write3BytesInt( buffer, length )
+ buffer.writeByte(sequence)
+
+ buffer.resetWriterIndex()
+ }
+
+ def packetBuffer( estimate : Int = 1024 ) : ByteBuf = {
+ val buffer = mysqlBuffer(estimate)
+
+ buffer.writeInt(0)
+
+ buffer
+ }
+
+ def mysqlBuffer( estimate : Int = 1024 ) : ByteBuf = {
+ Unpooled.buffer(estimate).order(ByteOrder.LITTLE_ENDIAN)
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ChannelFutureTransformer.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ChannelFutureTransformer.scala
new file mode 100644
index 00000000..32280db2
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ChannelFutureTransformer.scala
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import io.netty.channel.{ChannelFutureListener, ChannelFuture}
+import scala.concurrent.{Promise, Future}
+import com.github.mauricio.async.db.exceptions.CanceledChannelFutureException
+import scala.language.implicitConversions
+
+object ChannelFutureTransformer {
+
+ implicit def toFuture(channelFuture: ChannelFuture): Future[ChannelFuture] = {
+ val promise = Promise[ChannelFuture]
+
+ channelFuture.addListener(new ChannelFutureListener {
+ def operationComplete(future: ChannelFuture) {
+ if ( future.isSuccess ) {
+ promise.success(future)
+ } else {
+ val exception = if ( future.cause == null ) {
+ new CanceledChannelFutureException(future)
+ .fillInStackTrace()
+ } else {
+ future.cause
+ }
+
+ promise.failure(exception)
+
+ }
+ }
+ })
+
+ promise.future
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ChannelWrapper.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ChannelWrapper.scala
new file mode 100644
index 00000000..94bca43e
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ChannelWrapper.scala
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import com.github.mauricio.async.db.exceptions.UnknownLengthException
+import java.nio.charset.Charset
+import scala.language.implicitConversions
+import io.netty.buffer.ByteBuf
+
+object ChannelWrapper {
+ implicit def bufferToWrapper( buffer : ByteBuf ) = new ChannelWrapper(buffer)
+
+ final val MySQL_NULL = 0xfb
+ final val log = Log.get[ChannelWrapper]
+
+}
+
+class ChannelWrapper( val buffer : ByteBuf ) extends AnyVal {
+
+ import ChannelWrapper._
+
+ def readFixedString( length : Int, charset : Charset ) : String = {
+ val bytes = new Array[Byte](length)
+ buffer.readBytes( bytes )
+ new String( bytes, charset )
+ }
+
+ def readCString( charset : Charset ) = ByteBufferUtils.readCString(buffer, charset)
+
+ def readUntilEOF( charset: Charset ) = ByteBufferUtils.readUntilEOF(buffer, charset)
+
+ def readLengthEncodedString( charset : Charset ) : String = {
+ val length = readBinaryLength
+ readFixedString(length.asInstanceOf[Int], charset)
+ }
+
+ def readBinaryLength : Long = {
+ val firstByte = buffer.readUnsignedByte()
+
+ if ( firstByte <= 250 ) {
+ firstByte
+ } else {
+ firstByte match {
+ case MySQL_NULL => -1
+ case 252 => buffer.readUnsignedShort()
+ case 253 => readLongInt
+ case 254 => buffer.readLong()
+ case _ => throw new UnknownLengthException(firstByte)
+ }
+ }
+
+ }
+
+ def readLongInt : Int = {
+ val first = buffer.readByte()
+ val second = buffer.readByte()
+ val third = buffer.readByte()
+
+ ( first & 0xff ) | (( second & 0xff ) << 8) | ((third & 0xff) << 16)
+ }
+
+ def writeLength( length : Long ) {
+ if (length < 251) {
+ buffer.writeByte( length.asInstanceOf[Byte])
+ } else if (length < 65536L) {
+ buffer.writeByte(252)
+ buffer.writeShort(length.asInstanceOf[Int])
+ } else if (length < 16777216L) {
+ buffer.writeByte(253)
+ writeLongInt(length.asInstanceOf[Int])
+ } else {
+ buffer.writeByte(254)
+ buffer.writeLong(length)
+ }
+ }
+
+ def writeLongInt(i : Int) {
+ buffer.writeByte( i & 0xff )
+ buffer.writeByte( i >>> 8 )
+ buffer.writeByte( i >>> 16 )
+ }
+
+ def writeLenghtEncodedString( value : String, charset : Charset ) {
+ val bytes = value.getBytes(charset)
+ writeLength(bytes.length)
+ buffer.writeBytes(bytes)
+ }
+
+ def writePacketLength( sequence : Int = 0 ) {
+ ByteBufferUtils.writePacketLength(buffer, sequence )
+ }
+
+ def mysqlReadInt() : Int = {
+ val first = buffer.readByte()
+ val last = buffer.readByte()
+
+ (first & 0xff) | ((last & 0xff) << 8)
+ }
+
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/DaemonThreadsFactory.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/DaemonThreadsFactory.scala
new file mode 100644
index 00000000..2839564a
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/DaemonThreadsFactory.scala
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import java.util.concurrent.{ Executors, ThreadFactory }
+import java.util.concurrent.atomic.AtomicInteger
+
+case class DaemonThreadsFactory(name: String) extends ThreadFactory {
+
+ private val threadNumber = new AtomicInteger(1)
+
+ def newThread(r: Runnable): Thread = {
+ val thread = Executors.defaultThreadFactory().newThread(r)
+ thread.setDaemon(true)
+ val threadName = name + "-thread-" + threadNumber.getAndIncrement
+ thread.setName(threadName)
+ thread
+ }
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ExecutorServiceUtils.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ExecutorServiceUtils.scala
new file mode 100644
index 00000000..6246dae7
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/ExecutorServiceUtils.scala
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import java.util.concurrent.{ExecutorService, Executors}
+import scala.concurrent.ExecutionContext
+
+object ExecutorServiceUtils {
+ implicit val CachedThreadPool = Executors.newCachedThreadPool(DaemonThreadsFactory("db-async-default"))
+ implicit val CachedExecutionContext = ExecutionContext.fromExecutor( CachedThreadPool )
+
+ def newFixedPool( count : Int, name: String ) : ExecutorService = {
+ Executors.newFixedThreadPool( count, DaemonThreadsFactory(name) )
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/FutureUtils.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/FutureUtils.scala
new file mode 100644
index 00000000..ccff6609
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/FutureUtils.scala
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import scala.concurrent.{Await, Future}
+import scala.concurrent.duration._
+import scala.language.postfixOps
+
+object FutureUtils {
+
+ def awaitFuture[T]( future : Future[T] ) : T = {
+ Await.result(future, 5 seconds )
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/HexCodec.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/HexCodec.scala
new file mode 100644
index 00000000..5b74d27f
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/HexCodec.scala
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+/**
+ * The code from this class was copied from the Hex class at commons-codec
+ */
+
+object HexCodec {
+
+ private final val Digits = Array[Char]('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
+
+ private def toDigit( ch : Char, index : Int ) : Int = {
+ val digit = Character.digit( ch, 16 )
+
+ if ( digit == -1 ) {
+ throw new IllegalArgumentException("Illegal hexadecimal character " + ch + " at index " + index);
+ }
+
+ return digit
+ }
+
+ /**
+ *
+ * Turns a HEX based char sequence into a Byte array
+ *
+ * @param value
+ * @param start
+ * @return
+ */
+
+ def decode( value : CharSequence, start : Int = 0 ) : Array[Byte] = {
+
+ val length = value.length - start
+ val end = value.length()
+
+ if ( (length & 0x01) != 0 ) {
+ throw new IllegalArgumentException("Odd number of characters. A hex encoded byte array has to be even.")
+ }
+
+ val out = new Array[Byte](length >> 1)
+
+ var i = 0
+ var j = start
+
+ while ( j < end ) {
+ var f = toDigit(value.charAt(j), j) << 4
+ j += 1
+ f = f | toDigit(value.charAt(j), j)
+ j += 1
+ out(i) = (f & 0xff).asInstanceOf[Byte]
+ i += 1
+ }
+
+ out
+ }
+
+ /**
+ *
+ * Encodes a byte array into a String encoded with Hex values.
+ *
+ * @param bytes
+ * @param prefix
+ * @return
+ */
+
+ def encode( bytes : Array[Byte], prefix : Array[Char] = Array.empty ) : String = {
+ val length = (bytes.length * 2) + prefix.length
+ val chars = new Array[Char](length)
+
+ if ( prefix.length != 0 ) {
+ var x = 0
+ while ( x < prefix.length ) {
+ chars(x) = prefix(x)
+ x += 1
+ }
+ }
+
+ val dataLength = bytes.length
+ var j = prefix.length
+ var i = 0
+
+ while ( i < dataLength ) {
+ chars(j) = Digits(( 0xF0 & bytes(i)) >>> 4 )
+ j += 1
+ chars(j) = Digits( 0x0F & bytes(i) )
+ j += 1
+ i += 1
+ }
+
+ new String(chars)
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/Log.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/Log.scala
new file mode 100644
index 00000000..df7df4cd
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/Log.scala
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import org.slf4j.LoggerFactory
+
+object Log {
+
+ def get[T](implicit tag: reflect.ClassTag[T]) = {
+ LoggerFactory.getLogger(tag.runtimeClass.getName)
+ }
+
+ def getByName(name: String) = {
+ LoggerFactory.getLogger(name)
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/NettyUtils.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/NettyUtils.scala
new file mode 100644
index 00000000..c9e09f1a
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/NettyUtils.scala
@@ -0,0 +1,26 @@
+package com.github.mauricio.async.db.util
+
+import io.netty.channel.nio.NioEventLoopGroup
+import io.netty.util.internal.logging.{InternalLoggerFactory, Slf4JLoggerFactory}
+
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+object NettyUtils {
+
+ InternalLoggerFactory.setDefaultFactory(Slf4JLoggerFactory.INSTANCE)
+ lazy val DefaultEventLoopGroup = new NioEventLoopGroup(0, DaemonThreadsFactory("db-async-netty"))
+
+}
\ No newline at end of file
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/PrintUtils.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/PrintUtils.scala
new file mode 100644
index 00000000..db0ea002
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/PrintUtils.scala
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import io.netty.buffer.ByteBuf
+
+object PrintUtils {
+
+ private val log = Log.getByName(this.getClass.getName)
+
+ def printArray( name : String, buffer : ByteBuf ) {
+ buffer.markReaderIndex()
+ val bytes = new Array[Byte](buffer.readableBytes())
+ buffer.readBytes(bytes)
+ buffer.resetReaderIndex()
+ log.debug( s"$name Array[Byte](${bytes.mkString(", ")})" )
+ }
+
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/Version.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/Version.scala
new file mode 100644
index 00000000..1d991324
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/Version.scala
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import scala.util._
+import scala.util.Success
+
+object Version {
+
+ private def tryParse( index : Int, pieces : Array[String] ) : Int = {
+
+ Try {
+ pieces(index).toInt
+ } match {
+ case Success(value) => value
+ case Failure(e) => 0
+ }
+
+ }
+
+ def apply( version : String ) : Version = {
+ val pieces = version.split('.')
+ new Version( tryParse(0, pieces), tryParse(1, pieces), tryParse(2, pieces) )
+ }
+
+}
+
+case class Version( major : Int, minor : Int, maintenance : Int ) extends Ordered[Version] {
+ override def compare( y: Version): Int = {
+
+ if ( this == y ) {
+ return 0
+ }
+
+ if ( this.major != y.major ) {
+ return this.major.compare(y.major)
+ }
+
+ if ( this.minor != y.minor ) {
+ return this.minor.compare(y.minor)
+ }
+
+ this.maintenance.compare(y.maintenance)
+ }
+}
diff --git a/db-async-common/src/main/scala/com/github/mauricio/async/db/util/Worker.scala b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/Worker.scala
new file mode 100644
index 00000000..0988b052
--- /dev/null
+++ b/db-async-common/src/main/scala/com/github/mauricio/async/db/util/Worker.scala
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import java.util.concurrent.ExecutorService
+import scala.concurrent.{ExecutionContextExecutorService, ExecutionContext}
+
+object Worker {
+ val log = Log.get[Worker]
+
+ def apply() : Worker = apply(ExecutorServiceUtils.newFixedPool(1, "db-async-worker"))
+
+ def apply( executorService : ExecutorService ) : Worker = {
+ new Worker(ExecutionContext.fromExecutorService( executorService ))
+ }
+
+}
+
+class Worker( val executionContext : ExecutionContextExecutorService ) {
+
+ import Worker.log
+
+ def action(f: => Unit) {
+ this.executionContext.execute(new Runnable {
+ def run() {
+ try {
+ f
+ } catch {
+ case e : Exception => {
+ log.error("Failed to execute task %s".format(f), e)
+ }
+ }
+ }
+ })
+ }
+
+ def shutdown {
+ this.executionContext.shutdown()
+ }
+
+}
diff --git a/db-async-common/src/test/resources/logback.xml b/db-async-common/src/test/resources/logback.xml
new file mode 100644
index 00000000..3c9d6579
--- /dev/null
+++ b/db-async-common/src/test/resources/logback.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ [%level][%thread][%d][%c{5}] %msg%ex%n
+
+
+
+
+ target/common-async-tests.log
+
+ [%level][%thread][%d][%c{5}] %msg%ex%n
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/column/TimestampEncoderDecoderSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/column/TimestampEncoderDecoderSpec.scala
new file mode 100644
index 00000000..4bf33453
--- /dev/null
+++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/column/TimestampEncoderDecoderSpec.scala
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.column
+
+import org.specs2.mutable.Specification
+import org.joda.time.DateTime
+import java.sql.Timestamp
+import org.joda.time.format.DateTimeFormatterBuilder
+import java.util.Calendar
+
+class TimestampEncoderDecoderSpec extends Specification {
+
+ val encoder = TimestampEncoderDecoder.Instance
+ val dateTime = new DateTime()
+ .withDate(2013, 12, 27)
+ .withTime(8, 40, 50, 800)
+
+ val result = "2013-12-27 08:40:50.800000"
+ val formatter = new DateTimeFormatterBuilder().appendPattern("Z").toFormatter
+ val resultWithTimezone = s"2013-12-27 08:40:50.800000${formatter.print(dateTime)}"
+
+ "decoder" should {
+
+ "should print a timestamp" in {
+ val timestamp = new Timestamp(dateTime.toDate.getTime)
+ encoder.encode(timestamp) === resultWithTimezone
+ }
+
+ "should print a LocalDateTime" in {
+ encoder.encode(dateTime.toLocalDateTime) === result
+ }
+
+ "should print a date" in {
+ encoder.encode(dateTime.toDate) === resultWithTimezone
+ }
+
+ "should print a calendar" in {
+ val calendar = Calendar.getInstance()
+ calendar.setTime(dateTime.toDate)
+ encoder.encode(calendar) === resultWithTimezone
+ }
+
+ "should print a datetime" in {
+ encoder.encode(dateTime) === resultWithTimezone
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala
new file mode 100644
index 00000000..7c8bfdc4
--- /dev/null
+++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/AbstractAsyncObjectPoolSpec.scala
@@ -0,0 +1,229 @@
+package com.github.mauricio.async.db.pool
+
+import com.github.mauricio.async.db.pool.AbstractAsyncObjectPoolSpec.Widget
+import org.mockito.Mockito.reset
+import org.specs2.mock.Mockito
+import org.specs2.mutable.Specification
+
+import scala.concurrent.{Await, Future}
+import scala.util.Failure
+
+import scala.reflect.runtime.universe.TypeTag
+import scala.util.Try
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+
+/**
+ * This spec is designed abstract to allow testing of any implementation of AsyncObjectPool, against the common
+ * requirements the interface expects.
+ *
+ * @tparam T the AsyncObjectPool being tested.
+ */
+abstract class AbstractAsyncObjectPoolSpec[T <: AsyncObjectPool[Widget]](implicit tag: TypeTag[T])
+ extends Specification
+ with Mockito {
+
+ import AbstractAsyncObjectPoolSpec._
+
+ protected def pool(factory: ObjectFactory[Widget] = new TestWidgetFactory, conf: PoolConfiguration = PoolConfiguration.Default): T
+
+ // Evaluates to the type of AsyncObjectPool
+ s"the ${tag.tpe.erasure} variant of AsyncObjectPool" should {
+
+ "successfully retrieve and return a Widget" in {
+ val p = pool()
+ val widget = Await.result(p.take, Duration.Inf)
+
+ widget must not beNull
+
+ val thePool = Await.result(p.giveBack(widget), Duration.Inf)
+ thePool must be(p)
+ }
+
+ "reject Widgets that did not come from it" in {
+ val p = pool()
+
+ Await.result(p.giveBack(Widget(null)), Duration.Inf) must throwAn[IllegalArgumentException]
+ }
+
+ "scale contents" >> {
+ sequential
+
+ val factory = spy(new TestWidgetFactory)
+
+ val p = pool(
+ factory = factory,
+ conf = PoolConfiguration(
+ maxObjects = 5,
+ maxIdle = 2,
+ maxQueueSize = 5,
+ validationInterval = 2000
+ ))
+
+
+
+ var taken = Seq.empty[Widget]
+ "can take up to maxObjects" in {
+ taken = Await.result(Future.sequence(for (i <- 1 to 5) yield p.take), Duration.Inf)
+
+ taken must have size 5
+ taken.head must not beNull;
+ taken(1) must not beNull;
+ taken(2) must not beNull;
+ taken(3) must not beNull;
+ taken(4) must not beNull
+ }
+
+ "does not attempt to expire taken items" in {
+ // Wait 3 seconds to ensure idle check has run at least once
+ there was after(3.seconds).no(factory).destroy(any[Widget])
+ }
+
+ reset(factory) // Considered bad form, but necessary as we depend on previous state in these tests
+ "takes maxObjects back" in {
+ val returns = Await.result(Future.sequence(for (widget <- taken) yield p.giveBack(widget)), Duration.Inf)
+
+ returns must have size 5
+
+ returns.head must be(p)
+ returns(1) must be(p)
+ returns(2) must be(p)
+ returns(3) must be(p)
+ returns(4) must be(p)
+ }
+
+ "protest returning an item that was already returned" in {
+ val resultFuture = p.giveBack(taken.head)
+
+ Await.result(resultFuture, Duration.Inf) must throwAn[IllegalStateException]
+ }
+
+ "destroy down to maxIdle widgets" in {
+ Thread.sleep(3000)
+ there were 5.times(factory).destroy(any[Widget])
+ }
+ }
+
+ "queue requests after running out" in {
+ val p = pool(conf = PoolConfiguration.Default.copy(maxObjects = 2, maxQueueSize = 1))
+
+ val widgets = Await.result(Future.sequence(for (i <- 1 to 2) yield p.take), Duration.Inf)
+
+ val future = p.take
+
+ // Wait five seconds
+ Thread.sleep(5000)
+
+ val failedFuture = p.take
+
+ // Cannot be done, would exceed maxObjects
+ future.isCompleted must beFalse
+
+ Await.result(failedFuture, Duration.Inf) must throwA[PoolExhaustedException]
+
+ Await.result(p.giveBack(widgets.head), Duration.Inf) must be(p)
+
+ Await.result(future, Duration(5, SECONDS)) must be(widgets.head)
+ }
+
+ "refuse to allow take after being closed" in {
+ val p = pool()
+
+ Await.result(p.close, Duration.Inf) must be(p)
+
+ Await.result(p.take, Duration.Inf) must throwA[PoolAlreadyTerminatedException]
+ }
+
+ "allow being closed more than once" in {
+ val p = pool()
+
+ Await.result(p.close, Duration.Inf) must be(p)
+
+ Await.result(p.close, Duration.Inf) must be(p)
+ }
+
+
+ "destroy a failed widget" in {
+ val factory = spy(new TestWidgetFactory)
+ val p = pool(factory = factory)
+
+ val widget = Await.result(p.take, Duration.Inf)
+
+ widget must not beNull
+
+ factory.validate(widget) returns Failure(new RuntimeException("This is a bad widget!"))
+
+ Await.result(p.giveBack(widget), Duration.Inf) must throwA[RuntimeException](message = "This is a bad widget!")
+
+ there was atLeastOne(factory).destroy(widget)
+ }
+
+ "clean up widgets that die in the pool" in {
+ val factory = spy(new TestWidgetFactory)
+ // Deliberately make it impossible to expire (nearly)
+ val p = pool(factory = factory, conf = PoolConfiguration.Default.copy(maxIdle = Long.MaxValue, validationInterval = 2000))
+
+ val widget = Await.result(p.take, Duration.Inf)
+
+ widget must not beNull
+
+ Await.result(p.giveBack(widget), Duration.Inf) must be(p)
+
+ there was atLeastOne(factory).validate(widget)
+ there were no(factory).destroy(widget)
+
+ there was after(3.seconds).atLeastTwo(factory).validate(widget)
+
+ factory.validate(widget) returns Failure(new RuntimeException("Test Exception, Not an Error"))
+
+ there was after(3.seconds).one(factory).destroy(widget)
+
+ Await.ready(p.take, Duration.Inf)
+
+ there was two(factory).create
+ }
+
+ }
+
+}
+
+object AbstractAsyncObjectPoolSpec {
+
+ case class Widget(factory: TestWidgetFactory)
+
+ class TestWidgetFactory extends ObjectFactory[Widget] {
+
+ override def create: Widget = Widget(this)
+
+ override def destroy(item: Widget) = {}
+
+ override def validate(item: Widget): Try[Widget] = Try {
+ if (item.factory eq this)
+ item
+ else
+ throw new IllegalArgumentException("Not our item")
+ }
+ }
+
+}
+
+
+class SingleThreadedAsyncObjectPoolSpec extends AbstractAsyncObjectPoolSpec[SingleThreadedAsyncObjectPool[Widget]] {
+
+ import AbstractAsyncObjectPoolSpec._
+
+ override protected def pool(factory: ObjectFactory[Widget], conf: PoolConfiguration) =
+ new SingleThreadedAsyncObjectPool(factory, conf)
+
+ "SingleThreadedAsyncObjectPool" should {
+ "successfully record a closed state" in {
+ val p = pool()
+
+ Await.result(p.close, Duration.Inf) must be(p)
+
+ p.isClosed must beTrue
+ }
+
+ }
+
+}
diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/DummyTimeoutScheduler.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/DummyTimeoutScheduler.scala
new file mode 100644
index 00000000..6935259e
--- /dev/null
+++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/DummyTimeoutScheduler.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.pool
+
+import java.util.concurrent.atomic.AtomicInteger
+import com.github.mauricio.async.db.util.{NettyUtils, ExecutorServiceUtils}
+import io.netty.channel.EventLoopGroup
+
+/**
+ * Implementation of TimeoutScheduler used for testing
+ */
+class DummyTimeoutScheduler extends TimeoutScheduler {
+ implicit val internalPool = ExecutorServiceUtils.CachedExecutionContext
+ private val timeOuts = new AtomicInteger
+ override def onTimeout = timeOuts.incrementAndGet
+ def timeoutCount = timeOuts.get()
+ def eventLoopGroup : EventLoopGroup = NettyUtils.DefaultEventLoopGroup
+}
diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/PartitionedAsyncObjectPoolSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/PartitionedAsyncObjectPoolSpec.scala
new file mode 100644
index 00000000..51d58fb0
--- /dev/null
+++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/PartitionedAsyncObjectPoolSpec.scala
@@ -0,0 +1,289 @@
+package com.github.mauricio.async.db.pool
+
+import java.util.concurrent.atomic.AtomicInteger
+
+import org.specs2.mutable.Specification
+import scala.util.Try
+import scala.concurrent.Await
+import scala.concurrent.duration._
+import scala.concurrent.Future
+import org.specs2.mutable.SpecificationWithJUnit
+import language.reflectiveCalls
+import com.github.mauricio.async.db.util.ExecutorServiceUtils
+import scala.concurrent.ExecutionContext
+import java.util.concurrent.Executors
+
+class PartitionedAsyncObjectPoolSpec extends SpecificationWithJUnit {
+ isolated
+ sequential
+
+ val config =
+ PoolConfiguration(100, Long.MaxValue, 100, Int.MaxValue)
+ private var current = new AtomicInteger
+ val factory = new ObjectFactory[Int] {
+ var reject = Set[Int]()
+ var failCreate = false
+
+ def create =
+ if (failCreate)
+ throw new IllegalStateException
+ else {
+ current.incrementAndGet()
+ }
+ def destroy(item: Int) = {}
+ def validate(item: Int) =
+ Try {
+ if (reject.contains(item))
+ throw new IllegalStateException
+ else item
+ }
+ }
+
+ val pool = new PartitionedAsyncObjectPool(factory, config, 2)
+ def maxObjects = config.maxObjects / 2
+ def maxIdle = config.maxIdle / 2
+ def maxQueueSize = config.maxQueueSize / 2
+
+ "pool contents" >> {
+
+ "before exceed maxObjects" >> {
+
+ "take one element" in {
+ takeAndWait(1)
+
+ pool.inUse.size mustEqual 1
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 0
+ }
+
+ "take one element and return it invalid" in {
+ takeAndWait(1)
+ factory.reject += 1
+
+ await(pool.giveBack(1)) must throwA[IllegalStateException]
+
+ pool.inUse.size mustEqual 0
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 0
+ }
+
+ "take one failed element" in {
+ factory.failCreate = true
+ takeAndWait(1) must throwA[IllegalStateException]
+
+ pool.inUse.size mustEqual 0
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 0
+ }
+
+ "take maxObjects" in {
+ takeAndWait(maxObjects)
+
+ pool.inUse.size mustEqual maxObjects
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 0
+ }
+
+ "take maxObjects - 1 and take one failed" in {
+ takeAndWait(maxObjects - 1)
+
+ factory.failCreate = true
+ takeAndWait(1) must throwA[IllegalStateException]
+
+ pool.inUse.size mustEqual maxObjects - 1
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 0
+ }
+
+ "take maxObjects and receive one back" in {
+ takeAndWait(maxObjects)
+ await(pool.giveBack(1))
+
+ pool.inUse.size mustEqual maxObjects - 1
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 1
+ }
+
+ "take maxObjects and receive one invalid back" in {
+ takeAndWait(maxObjects)
+ factory.reject += 1
+ await(pool.giveBack(1)) must throwA[IllegalStateException]
+
+ pool.inUse.size mustEqual maxObjects - 1
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 0
+ }
+ }
+
+ "after exceed maxObjects" >> {
+
+ takeAndWait(maxObjects)
+
+ "before exceed maxQueueSize" >> {
+
+ "one take queued" in {
+ pool.take
+
+ pool.inUse.size mustEqual maxObjects
+ pool.queued.size mustEqual 1
+ pool.availables.size mustEqual 0
+ }
+
+ "one take queued and receive one item back" in {
+ val taking = pool.take
+
+ await(pool.giveBack(1))
+
+ await(taking) mustEqual 1
+ pool.inUse.size mustEqual maxObjects
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 0
+ }
+
+ "one take queued and receive one invalid item back" in {
+ val taking = pool.take
+ factory.reject += 1
+ await(pool.giveBack(1)) must throwA[IllegalStateException]
+
+ pool.inUse.size mustEqual maxObjects - 1
+ pool.queued.size mustEqual 1
+ pool.availables.size mustEqual 0
+ }
+
+ "maxQueueSize takes queued" in {
+ for (_ <- 0 until maxQueueSize)
+ pool.take
+
+ pool.inUse.size mustEqual maxObjects
+ pool.queued.size mustEqual maxQueueSize
+ pool.availables.size mustEqual 0
+ }
+
+ "maxQueueSize takes queued and receive one back" in {
+ val taking = pool.take
+ for (_ <- 0 until maxQueueSize - 1)
+ pool.take
+
+ await(pool.giveBack(10))
+
+ await(taking) mustEqual 10
+ pool.inUse.size mustEqual maxObjects
+ pool.queued.size mustEqual maxQueueSize - 1
+ pool.availables.size mustEqual 0
+ }
+
+ "maxQueueSize takes queued and receive one invalid back" in {
+ for (_ <- 0 until maxQueueSize)
+ pool.take
+
+ factory.reject += 11
+ await(pool.giveBack(11)) must throwA[IllegalStateException]
+
+ pool.inUse.size mustEqual maxObjects - 1
+ pool.queued.size mustEqual maxQueueSize
+ pool.availables.size mustEqual 0
+ }
+ }
+
+ "after exceed maxQueueSize" >> {
+
+ for (_ <- 0 until maxQueueSize)
+ pool.take
+
+ "start to reject takes" in {
+ await(pool.take) must throwA[PoolExhaustedException]
+
+ pool.inUse.size mustEqual maxObjects
+ pool.queued.size mustEqual maxQueueSize
+ pool.availables.size mustEqual 0
+ }
+
+ "receive an object back" in {
+ await(pool.giveBack(1))
+
+ pool.inUse.size mustEqual maxObjects
+ pool.queued.size mustEqual maxQueueSize - 1
+ pool.availables.size mustEqual 0
+ }
+
+ "receive an invalid object back" in {
+ factory.reject += 1
+ await(pool.giveBack(1)) must throwA[IllegalStateException]
+
+ pool.inUse.size mustEqual maxObjects - 1
+ pool.queued.size mustEqual maxQueueSize
+ pool.availables.size mustEqual 0
+ }
+
+ "receive maxQueueSize objects back" in {
+ for (i <- 1 to maxQueueSize)
+ await(pool.giveBack(i))
+
+ pool.inUse.size mustEqual maxObjects
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 0
+ }
+
+ "receive maxQueueSize invalid objects back" in {
+ for (i <- 1 to maxQueueSize) {
+ factory.reject += i
+ await(pool.giveBack(i)) must throwA[IllegalStateException]
+ }
+
+ pool.inUse.size mustEqual maxObjects - maxQueueSize
+ pool.queued.size mustEqual maxQueueSize
+ pool.availables.size mustEqual 0
+ }
+
+ "receive maxQueueSize + 1 object back" in {
+ for (i <- 1 to maxQueueSize)
+ await(pool.giveBack(i))
+
+ await(pool.giveBack(1))
+ pool.inUse.size mustEqual maxObjects - 1
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 1
+ }
+
+ "receive maxQueueSize + 1 invalid object back" in {
+ for (i <- 1 to maxQueueSize)
+ await(pool.giveBack(i))
+
+ factory.reject += 1
+ await(pool.giveBack(1)) must throwA[IllegalStateException]
+ pool.inUse.size mustEqual maxObjects - 1
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 0
+ }
+ }
+ }
+ }
+
+ "gives back the connection to the original pool" in {
+ val executor = Executors.newFixedThreadPool(20)
+ implicit val context = ExecutionContext.fromExecutor(executor)
+
+ val takes =
+ for (_ <- 0 until 30) yield {
+ Future().flatMap(_ => pool.take)
+ }
+ val takesAndReturns =
+ Future.sequence(takes).flatMap { items =>
+ Future.sequence(items.map(pool.giveBack))
+ }
+
+ await(takesAndReturns)
+
+ executor.shutdown
+ pool.inUse.size mustEqual 0
+ pool.queued.size mustEqual 0
+ pool.availables.size mustEqual 30
+ }
+
+ private def takeAndWait(objects: Int) =
+ for (_ <- 0 until objects)
+ await(pool.take)
+
+ private def await[T](future: Future[T]) =
+ Await.result(future, Duration.Inf)
+}
diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala
new file mode 100644
index 00000000..0c6d85b4
--- /dev/null
+++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/pool/TimeoutSchedulerSpec.scala
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package com.github.mauricio.async.db.pool
+
+import java.util.concurrent.{ScheduledFuture, TimeoutException}
+import com.github.mauricio.async.db.util.{ByteBufferUtils, ExecutorServiceUtils}
+import org.specs2.mutable.SpecificationWithJUnit
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.concurrent.duration._
+import scala.concurrent.{Future, Promise}
+
+/**
+ * Tests for TimeoutScheduler
+ */
+class TimeoutSchedulerSpec extends SpecificationWithJUnit {
+
+ val TIMEOUT_DID_NOT_PASS = "timeout did not pass"
+
+ "test timeout did not pass" in {
+ val timeoutScheduler = new DummyTimeoutScheduler()
+ val promise = Promise[String]()
+ val scheduledFuture = timeoutScheduler.addTimeout(promise,Some(Duration(1000, MILLISECONDS)))
+ Thread.sleep(100);
+ promise.isCompleted === false
+ promise.success(TIMEOUT_DID_NOT_PASS)
+ Thread.sleep(1500)
+ promise.future.value.get.get === TIMEOUT_DID_NOT_PASS
+ scheduledFuture.get.isCancelled === true
+ timeoutScheduler.timeoutCount === 0
+ }
+
+ "test timeout passed" in {
+ val timeoutMillis = 100
+ val promise = Promise[String]()
+ val timeoutScheduler = new DummyTimeoutScheduler()
+ val scheduledFuture = timeoutScheduler.addTimeout(promise,Some(Duration(timeoutMillis, MILLISECONDS)))
+ Thread.sleep(1000)
+ promise.isCompleted === true
+ scheduledFuture.get.isCancelled === false
+ promise.trySuccess(TIMEOUT_DID_NOT_PASS)
+ timeoutScheduler.timeoutCount === 1
+ promise.future.value.get.get must throwA[TimeoutException](message = s"Operation is timeouted after it took too long to return \\(${timeoutMillis} milliseconds\\)")
+ }
+
+ "test no timeout" in {
+ val timeoutScheduler = new DummyTimeoutScheduler()
+ val promise = Promise[String]()
+ val scheduledFuture = timeoutScheduler.addTimeout(promise,None)
+ Thread.sleep(1000)
+ scheduledFuture === None
+ promise.isCompleted === false
+ promise.success(TIMEOUT_DID_NOT_PASS)
+ promise.future.value.get.get === TIMEOUT_DID_NOT_PASS
+ timeoutScheduler.timeoutCount === 0
+ }
+}
+
diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/util/ChannelUtilsSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/util/ChannelUtilsSpec.scala
new file mode 100644
index 00000000..e6238d38
--- /dev/null
+++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/util/ChannelUtilsSpec.scala
@@ -0,0 +1,63 @@
+package com.github.mauricio.async.db.util
+
+import org.specs2.mutable.Specification
+import io.netty.util.CharsetUtil
+import io.netty.buffer.Unpooled
+
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+class ChannelUtilsSpec extends Specification {
+
+ val charset = CharsetUtil.UTF_8
+
+ "utils" should {
+
+ "correctly write and read a string" in {
+ val content = "some text"
+ val buffer = Unpooled.buffer()
+
+ ByteBufferUtils.writeCString(content, buffer, charset)
+
+ ByteBufferUtils.readCString(buffer, charset) === content
+ buffer.readableBytes() === 0
+ }
+
+ "correctly read the buggy MySQL EOF string when there is an EOF" in {
+ val content = "some text"
+ val buffer = Unpooled.buffer()
+
+ ByteBufferUtils.writeCString(content, buffer, charset)
+
+ ByteBufferUtils.readUntilEOF(buffer, charset) === content
+ buffer.readableBytes() === 0
+ }
+
+ "correctly read the buggy MySQL EOF string when there is no EOF" in {
+
+ val content = "some text"
+ val buffer = Unpooled.buffer()
+
+ buffer.writeBytes(content.getBytes(charset))
+
+ ByteBufferUtils.readUntilEOF(buffer, charset) === content
+ buffer.readableBytes() === 0
+
+ }
+
+ }
+
+}
diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/util/HexCodecSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/util/HexCodecSpec.scala
new file mode 100644
index 00000000..27aa9777
--- /dev/null
+++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/util/HexCodecSpec.scala
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import org.specs2.mutable.Specification
+
+object HexCodecSpec {
+
+ final val sampleArray = Array[Byte](83, 97, 121, 32, 72, 101, 108, 108, 111, 32, 116, 111, 32, 77, 121, 32, 76, 105, 116, 116, 108, 101, 32, 70, 114, 105, 101, 110, 100)
+ final val sampleHex = "5361792048656c6c6f20746f204d79204c6974746c6520467269656e64".toUpperCase
+ final val HexStart = "\\x"
+ final val HexStartChars = HexStart.toCharArray
+
+}
+
+class HexCodecSpec extends Specification {
+
+ import HexCodecSpec._
+
+ "codec" should {
+
+ "correctly generate an array of bytes" in {
+
+ val bytes = HexCodec.decode( "5361792048656c6c6f20746f204d79204c6974746c6520467269656e64" )
+ bytes === sampleArray
+
+ }
+
+ "correctly generate a string from an array of bytes" in {
+ HexCodec.encode(sampleArray) === sampleHex
+ }
+
+ "correctly generate a byte array from the PG output" in {
+
+ val input = "\\x53617920"
+ val bytes = Array[Byte](83, 97, 121, 32)
+ HexCodec.decode(input, 2) === bytes
+
+ }
+
+ "correctly encode to hex using the PostgreSQL format" in {
+ HexCodec.encode(sampleArray, HexStartChars) === (HexStart + sampleHex)
+ }
+
+ }
+
+}
diff --git a/db-async-common/src/test/scala/com/github/mauricio/async/db/util/VersionSpec.scala b/db-async-common/src/test/scala/com/github/mauricio/async/db/util/VersionSpec.scala
new file mode 100644
index 00000000..0c4e7972
--- /dev/null
+++ b/db-async-common/src/test/scala/com/github/mauricio/async/db/util/VersionSpec.scala
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.util
+
+import org.specs2.mutable.Specification
+
+class VersionSpec extends Specification {
+
+ "version" should {
+
+ "correctly parse versions" in {
+ val version = Version("9.1.4")
+ version.major === 9
+ version.minor === 1
+ version.maintenance === 4
+ }
+
+ "correctly parse with missing fields" in {
+ val version = Version("8.7")
+ version.major === 8
+ version.minor === 7
+ version.maintenance === 0
+ }
+
+ "correctly compare between major different versions" in {
+
+ val version1 = Version("8.2.0")
+ val version2 = Version("9.2.0")
+
+ version2 must beGreaterThan(version1)
+
+ }
+
+ "correctly compare between major different versions" in {
+
+ val version1 = Version("8.2.0")
+ val version2 = Version("8.2.0")
+
+ version2 === version1
+
+ }
+
+
+ "correctly compare between major different versions" in {
+
+ val version1 = Version("8.2.8")
+ val version2 = Version("8.2.87")
+
+ version2 must beGreaterThan( version1 )
+
+ }
+
+ "correctly compare two different versions" in {
+
+ val version1 = Version("9.1.2")
+ val version2 = Version("9.2.0")
+
+ version2 must beGreaterThan(version1)
+
+ }
+
+ }
+
+}
diff --git a/mysql-async/README.md b/mysql-async/README.md
new file mode 100644
index 00000000..3a152286
--- /dev/null
+++ b/mysql-async/README.md
@@ -0,0 +1,98 @@
+
+
+**Table of Contents**
+
+- [mysql-async - an async, Netty based, MySQL driver written in Scala 2.10 and 2.11](#mysql-async---an-async-netty-based-mysql-driver-written-in-scala-210)
+ - [What can it do now?](#what-can-it-do-now)
+ - [Gotchas](#gotchas)
+ - [Supported types](#supported-types)
+
+
+
+# mysql-async - an async, Netty based, MySQL driver written in Scala 2.10 and 2.11
+
+This is the MySQL part of the async driver collection. As the PostgreSQL version, it is not supposed to be a JDBC replacement, but a simpler solution for those that need something that queries and then returns rows.
+
+You can find more information about the MySQL network protocol [here](http://dev.mysql.com/doc/internals/en/client-server-protocol.html).
+
+## What can it do now?
+
+* connect do databases with the **mysql_native_password** method (that's the usual way)
+* execute common statements
+* execute prepared statements
+* supports MySQL servers from 4.1 and above (should also work the same way when using MariaDB or other MySQL derived projects)
+* supports most available database types
+
+## Gotchas
+
+* `unsigned` types are not supported, their behaviour when using this driver is undefined.
+* Prior to version [5.6.4](http://dev.mysql.com/doc/refman/5.6/en/fractional-seconds.html) MySQL truncates millis in `datetime`, `timestamp` and `time` fields. If your date has millis,
+ they will be gone ([docs here](http://dev.mysql.com/doc/refman/5.0/en/fractional-seconds.html))
+* If using `5.6` support for microseconds on `timestamp` fields (using the `timestamp(3)` syntax) you can't
+ go longer than 3 in precision since `JodaTime` and `Date` objects in Java only go as far as millis and not micro.
+ For `time` fields, since `Duration` is used, you get full microsecond precision.
+* Timezone support is rather complicated ([see here](http://dev.mysql.com/doc/refman/5.5/en/time-zone-support.html)),
+ avoid using timezones in MySQL. This driver just stores the dates as they are and won't perform any computation
+ or calculation. I'd recommend using only `datetime` fields and avoid `timestamp` fields as much as possible.
+* `time` in MySQL is not exactly a time in hours, minutes, seconds. It's a period/duration and it can be expressed in
+ days too (you could, for instance, say that a time is __-120d 19:27:30.000 001__). As much as this does not make much
+ sense, that is how it was implemented at the database and as a driver we need to stay true to it, so, while you
+ **can** send `java.sql.Time` and `LocalTime` objects to the database, when reading these values you will always
+ receive a `scala.concurrent.Duration` object since it is the closest thing we have to what a `time` value in MySQL means.
+* MySQL can store dates with values like `0000-00-00` or `0000-00-00 00:00:00` but it's not possible to represent dates like this in Java (nor there would actually be a date with a zero day or month, this is just MySQL being lenient on invalid dates) so the driver just returns `null` for any case like that.
+
+## Supported types
+
+When you are receiving data from a `ResultSet`:
+
+MySQL type | Scala/Java type
+--- | --- | ---
+date | LocalDate
+datetime | LocalDateTime
+new_date | LocalDate
+timestamp | LocalDateTime
+tinyint | Byte
+smallint | Short
+year | Short
+float | Float
+double | Double
+int24 | Int
+mediumint | Int
+bigint | Long
+numeric | BigDecimal
+new_decimal | BigDecimal
+decimal | BigDecimal
+string | String
+var_string | String
+varchar | String
+time | scala.concurrent.Duration
+text | String
+enum | String
+blob | Array[Byte]
+
+Now when you're setting parameters for a prepared statement:
+
+Scala/Java type | MySQL type
+--- | --- | ---
+Byte | tinyint
+Short | smallint
+Int | mediumint
+Float | float
+Double | double
+BigDecimal | decimal
+LocalDate | date
+DateTime | timestamp
+scala.concurrent.Duration | time
+java.sql.Date | date
+java.util.Date | timestamp
+java.sql.Timestamp | timestamp
+java.sql.Time | time
+String | string
+Array[Byte] | blob
+java.nio.ByteBuffer | blob
+io.netty.buffer.ByteBuf | blob
+
+The maximum size of a blob is 2^24-9 bytes (almost 16 MiB).
+
+You don't have to match exact values when sending parameters for your prepared statements, MySQL is usually smart
+enough to understand that if you have sent an Int to `smallint` column it has to truncate the 4 bytes into 2.
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/MySQLConnection.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/MySQLConnection.scala
new file mode 100644
index 00000000..cb4a85b0
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/MySQLConnection.scala
@@ -0,0 +1,268 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import java.util.concurrent.atomic.{AtomicLong, AtomicReference}
+
+import com.github.mauricio.async.db._
+import com.github.mauricio.async.db.exceptions._
+import com.github.mauricio.async.db.mysql.codec.{MySQLConnectionHandler, MySQLHandlerDelegate}
+import com.github.mauricio.async.db.mysql.exceptions.MySQLException
+import com.github.mauricio.async.db.mysql.message.client._
+import com.github.mauricio.async.db.mysql.message.server._
+import com.github.mauricio.async.db.mysql.util.CharsetMapper
+import com.github.mauricio.async.db.pool.TimeoutScheduler
+import com.github.mauricio.async.db.util.ChannelFutureTransformer.toFuture
+import com.github.mauricio.async.db.util._
+import io.netty.channel.{ChannelHandlerContext, EventLoopGroup}
+
+import scala.concurrent.{ExecutionContext, Future, Promise}
+import scala.util.{Failure, Success}
+
+object MySQLConnection {
+ final val Counter = new AtomicLong()
+ final val MicrosecondsVersion = Version(5,6,0)
+ final val log = Log.get[MySQLConnection]
+}
+
+class MySQLConnection(
+ configuration: Configuration,
+ charsetMapper: CharsetMapper = CharsetMapper.Instance,
+ group : EventLoopGroup = NettyUtils.DefaultEventLoopGroup,
+ implicit val executionContext : ExecutionContext = ExecutorServiceUtils.CachedExecutionContext
+ )
+ extends MySQLHandlerDelegate
+ with Connection
+ with TimeoutScheduler
+{
+
+ import MySQLConnection.log
+
+ // validate that this charset is supported
+ charsetMapper.toInt(configuration.charset)
+
+ private final val connectionCount = MySQLConnection.Counter.incrementAndGet()
+ private final val connectionId = s"[mysql-connection-$connectionCount]"
+
+ private final val connectionHandler = new MySQLConnectionHandler(
+ configuration,
+ charsetMapper,
+ this,
+ group,
+ executionContext,
+ connectionId)
+
+ private final val connectionPromise = Promise[Connection]()
+ private final val disconnectionPromise = Promise[Connection]()
+
+ private val queryPromiseReference = new AtomicReference[Option[Promise[QueryResult]]](None)
+ private var connected = false
+ private var _lastException : Throwable = null
+ private var serverVersion : Version = null
+
+ def version = this.serverVersion
+ def lastException : Throwable = this._lastException
+ def count : Long = this.connectionCount
+
+ override def eventLoopGroup : EventLoopGroup = group
+
+ def connect: Future[Connection] = {
+ this.connectionHandler.connect.onFailure {
+ case e => this.connectionPromise.tryFailure(e)
+ }
+
+ this.connectionPromise.future
+ }
+
+ def close: Future[Connection] = {
+ if ( this.isConnected ) {
+ if (!this.disconnectionPromise.isCompleted) {
+ val exception = new DatabaseException("Connection is being closed")
+ exception.fillInStackTrace()
+ this.failQueryPromise(exception)
+ this.connectionHandler.clearQueryState
+ this.connectionHandler.write(QuitMessage.Instance).onComplete {
+ case Success(channelFuture) => {
+ this.connectionHandler.disconnect.onComplete {
+ case Success(closeFuture) => this.disconnectionPromise.trySuccess(this)
+ case Failure(e) => this.disconnectionPromise.tryFailure(e)
+ }
+ }
+ case Failure(exception) => this.disconnectionPromise.tryFailure(exception)
+ }
+ }
+ }
+
+ this.disconnectionPromise.future
+ }
+
+ override def connected(ctx: ChannelHandlerContext) {
+ log.debug("Connected to {}", ctx.channel.remoteAddress)
+ this.connected = true
+ }
+
+ override def exceptionCaught(throwable: Throwable) {
+ log.error("Transport failure ", throwable)
+ setException(throwable)
+ }
+
+ override def onError(message: ErrorMessage) {
+ log.error("Received an error message -> {}", message)
+ val exception = new MySQLException(message)
+ exception.fillInStackTrace()
+ this.setException(exception)
+ }
+
+ private def setException( t : Throwable ) {
+ this._lastException = t
+ this.connectionPromise.tryFailure(t)
+ this.failQueryPromise(t)
+ }
+
+ override def onOk(message: OkMessage) {
+ if ( !this.connectionPromise.isCompleted ) {
+ log.debug("Connected to database")
+ this.connectionPromise.success(this)
+ } else {
+ if (this.isQuerying) {
+ this.succeedQueryPromise(
+ new MySQLQueryResult(
+ message.affectedRows,
+ message.message,
+ message.lastInsertId,
+ message.statusFlags,
+ message.warnings
+ )
+ )
+ } else {
+ log.warn("Received OK when not querying or connecting, not sure what this is")
+ }
+ }
+ }
+
+ def onEOF(message: EOFMessage) {
+ if (this.isQuerying) {
+ this.succeedQueryPromise(
+ new MySQLQueryResult(
+ 0,
+ null,
+ -1,
+ message.flags,
+ message.warningCount
+ )
+ )
+ }
+ }
+
+ override def onHandshake(message: HandshakeMessage) {
+ this.serverVersion = Version(message.serverVersion)
+
+ this.connectionHandler.write(new HandshakeResponseMessage(
+ configuration.username,
+ configuration.charset,
+ message.seed,
+ message.authenticationMethod,
+ database = configuration.database,
+ password = configuration.password
+ ))
+ }
+
+ override def switchAuthentication( message : AuthenticationSwitchRequest ) {
+ this.connectionHandler.write(new AuthenticationSwitchResponse( configuration.password, message ))
+ }
+
+ def sendQuery(query: String): Future[QueryResult] = {
+ this.validateIsReadyForQuery()
+ val promise = Promise[QueryResult]()
+ this.setQueryPromise(promise)
+ this.connectionHandler.write(new QueryMessage(query))
+ addTimeout(promise, configuration.queryTimeout)
+ promise.future
+ }
+
+ private def failQueryPromise(t: Throwable) {
+ this.clearQueryPromise.foreach {
+ _.tryFailure(t)
+ }
+ }
+
+ private def succeedQueryPromise(queryResult: QueryResult) {
+
+ this.clearQueryPromise.foreach {
+ _.success(queryResult)
+ }
+
+ }
+
+ def isQuerying: Boolean = this.queryPromise.isDefined
+
+ def onResultSet(resultSet: ResultSet, message: EOFMessage) {
+ if (this.isQuerying) {
+ this.succeedQueryPromise(
+ new MySQLQueryResult(
+ resultSet.size,
+ null,
+ -1,
+ message.flags,
+ message.warningCount,
+ Some(resultSet)
+ )
+ )
+ }
+ }
+
+ def disconnect: Future[Connection] = this.close
+ override def onTimeout = disconnect
+
+ def isConnected: Boolean = this.connectionHandler.isConnected
+
+ def sendPreparedStatement(query: String, values: Seq[Any]): Future[QueryResult] = {
+ this.validateIsReadyForQuery()
+ val totalParameters = query.count( _ == '?')
+ if ( values.length != totalParameters ) {
+ throw new InsufficientParametersException(totalParameters, values)
+ }
+ val promise = Promise[QueryResult]()
+ this.setQueryPromise(promise)
+ this.connectionHandler.sendPreparedStatement(query, values)
+ addTimeout(promise,configuration.queryTimeout)
+ promise.future
+ }
+
+
+ override def toString: String = {
+ "%s(%s,%d)".format(this.getClass.getName, this.connectionId, this.connectionCount)
+ }
+
+ private def validateIsReadyForQuery() {
+ if ( isQuerying ) {
+ throw new ConnectionStillRunningQueryException(this.connectionCount, false)
+ }
+ }
+
+ private def queryPromise: Option[Promise[QueryResult]] = queryPromiseReference.get()
+
+ private def setQueryPromise(promise: Promise[QueryResult]) {
+ if (!this.queryPromiseReference.compareAndSet(None, Some(promise)))
+ throw new ConnectionStillRunningQueryException(this.connectionCount, true)
+ }
+
+ private def clearQueryPromise : Option[Promise[QueryResult]] = {
+ this.queryPromiseReference.getAndSet(None)
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/MySQLQueryResult.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/MySQLQueryResult.scala
new file mode 100644
index 00000000..e7619685
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/MySQLQueryResult.scala
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import com.github.mauricio.async.db.{ResultSet, QueryResult}
+
+class MySQLQueryResult(
+ rowsAffected: Long,
+ message: String,
+ val lastInsertId: Long,
+ val statusFlags: Int,
+ val warnings: Int,
+ rows: Option[ResultSet] = None) extends QueryResult(rowsAffected, message, rows)
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowDecoder.scala
new file mode 100644
index 00000000..22c6cee5
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowDecoder.scala
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary
+
+import _root_.io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.exceptions.BufferNotFullyConsumedException
+import com.github.mauricio.async.db.mysql.message.server.ColumnDefinitionMessage
+import com.github.mauricio.async.db.util._
+import scala.collection.mutable.ArrayBuffer
+
+object BinaryRowDecoder {
+ final val log = Log.get[BinaryRowDecoder]
+ final val BitMapOffset = 9
+}
+
+class BinaryRowDecoder {
+
+ //import BinaryRowDecoder._
+
+ def decode(buffer: ByteBuf, columns: Seq[ColumnDefinitionMessage]): Array[Any] = {
+
+ //log.debug("columns are {} - {}", buffer.readableBytes(), columns)
+ //log.debug( "decoding row\n{}", MySQLHelper.dumpAsHex(buffer))
+ //PrintUtils.printArray("bitmap", buffer)
+
+ val nullCount = (columns.size + 9) / 8
+
+ val nullBitMask = new Array[Byte](nullCount)
+ buffer.readBytes(nullBitMask)
+
+ var nullMaskPos = 0
+ var bit = 4
+
+ val row = new ArrayBuffer[Any](columns.size)
+
+ var index = 0
+
+ while (index < columns.size) {
+
+ if ((nullBitMask(nullMaskPos) & bit) != 0) {
+ row += null
+ } else {
+
+ val column = columns(index)
+
+ //log.debug(s"${decoder.getClass.getSimpleName} - ${buffer.readableBytes()}")
+ //log.debug("Column value [{}] - {}", value, column.name)
+
+ row += column.binaryDecoder.decode(buffer)
+ }
+
+ bit <<= 1
+
+ if (( bit & 255) == 0) {
+ bit = 1
+ nullMaskPos += 1
+ }
+
+ index += 1
+ }
+
+ //log.debug("values are {}", row)
+
+ if (buffer.readableBytes() != 0) {
+ throw new BufferNotFullyConsumedException(buffer)
+ }
+
+ row.toArray
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowEncoder.scala
new file mode 100644
index 00000000..aff0b36f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowEncoder.scala
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary
+
+import java.nio.ByteBuffer
+import java.nio.charset.Charset
+
+import com.github.mauricio.async.db.mysql.binary.encoder._
+import com.github.mauricio.async.db.util._
+import io.netty.buffer.ByteBuf
+import org.joda.time._
+
+object BinaryRowEncoder {
+ final val log = Log.get[BinaryRowEncoder]
+}
+
+class BinaryRowEncoder( charset : Charset ) {
+
+ private final val stringEncoder = new StringEncoder(charset)
+ private final val encoders = Map[Class[_],BinaryEncoder](
+ classOf[String] -> this.stringEncoder,
+ classOf[BigInt] -> this.stringEncoder,
+ classOf[BigDecimal] -> this.stringEncoder,
+ classOf[java.math.BigDecimal] -> this.stringEncoder,
+ classOf[java.math.BigInteger] -> this.stringEncoder,
+ classOf[Byte] -> ByteEncoder,
+ classOf[java.lang.Byte] -> ByteEncoder,
+ classOf[Short] -> ShortEncoder,
+ classOf[java.lang.Short] -> ShortEncoder,
+ classOf[Int] -> IntegerEncoder,
+ classOf[java.lang.Integer] -> IntegerEncoder,
+ classOf[Long] -> LongEncoder,
+ classOf[java.lang.Long] -> LongEncoder,
+ classOf[Float] -> FloatEncoder,
+ classOf[java.lang.Float] -> FloatEncoder,
+ classOf[Double] -> DoubleEncoder,
+ classOf[java.lang.Double] -> DoubleEncoder,
+ classOf[LocalDateTime] -> LocalDateTimeEncoder,
+ classOf[DateTime] -> DateTimeEncoder,
+ classOf[LocalDate] -> LocalDateEncoder,
+ classOf[java.util.Date] -> JavaDateEncoder,
+ classOf[java.sql.Timestamp] -> SQLTimestampEncoder,
+ classOf[java.sql.Date] -> SQLDateEncoder,
+ classOf[java.sql.Time] -> SQLTimeEncoder,
+ classOf[scala.concurrent.duration.FiniteDuration] -> DurationEncoder,
+ classOf[Array[Byte]] -> ByteArrayEncoder,
+ classOf[Boolean] -> BooleanEncoder,
+ classOf[java.lang.Boolean] -> BooleanEncoder
+ )
+
+ def encoderFor( v : Any ) : BinaryEncoder = {
+
+ this.encoders.get(v.getClass) match {
+ case Some(encoder) => encoder
+ case None => {
+ v match {
+ case v : CharSequence => this.stringEncoder
+ case v : BigInt => this.stringEncoder
+ case v : java.math.BigInteger => this.stringEncoder
+ case v : BigDecimal => this.stringEncoder
+ case v : java.math.BigDecimal => this.stringEncoder
+ case v : ReadableDateTime => DateTimeEncoder
+ case v : ReadableInstant => ReadableInstantEncoder
+ case v : LocalDateTime => LocalDateTimeEncoder
+ case v : java.sql.Timestamp => SQLTimestampEncoder
+ case v : java.sql.Date => SQLDateEncoder
+ case v : java.util.Calendar => CalendarEncoder
+ case v : LocalDate => LocalDateEncoder
+ case v : LocalTime => LocalTimeEncoder
+ case v : java.sql.Time => SQLTimeEncoder
+ case v : scala.concurrent.duration.Duration => DurationEncoder
+ case v : java.util.Date => JavaDateEncoder
+ case v : ByteBuffer => ByteBufferEncoder
+ case v : ByteBuf => ByteBufEncoder
+ }
+ }
+ }
+
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/BigDecimalDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/BigDecimalDecoder.scala
new file mode 100644
index 00000000..812612e5
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/BigDecimalDecoder.scala
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import java.nio.charset.Charset
+import io.netty.buffer.ByteBuf
+
+class BigDecimalDecoder( charset : Charset ) extends BinaryDecoder {
+ def decode(buffer: ByteBuf): Any = {
+ BigDecimal( buffer.readLengthEncodedString(charset) )
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/BinaryDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/BinaryDecoder.scala
new file mode 100644
index 00000000..3883c199
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/BinaryDecoder.scala
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+
+trait BinaryDecoder {
+
+ def decode( buffer : ByteBuf ) : Any
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/ByteArrayDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/ByteArrayDecoder.scala
new file mode 100644
index 00000000..7aa9a4d3
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/ByteArrayDecoder.scala
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+
+object ByteArrayDecoder extends BinaryDecoder {
+ def decode(buffer: ByteBuf): Any = {
+ val length = buffer.readBinaryLength
+ val bytes = new Array[Byte](length.toInt)
+ buffer.readBytes(bytes)
+
+ bytes
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/ByteDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/ByteDecoder.scala
new file mode 100644
index 00000000..50940cf6
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/ByteDecoder.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+
+object ByteDecoder extends BinaryDecoder {
+ def decode(buffer: ByteBuf): Any = buffer.readByte()
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/DateDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/DateDecoder.scala
new file mode 100644
index 00000000..2d66e792
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/DateDecoder.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+import org.joda.time.LocalDate
+
+object DateDecoder extends BinaryDecoder {
+ override def decode(buffer: ByteBuf): LocalDate = {
+ val result = TimestampDecoder.decode(buffer)
+
+ if ( result != null ) {
+ result.toLocalDate
+ } else {
+ null
+ }
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/DoubleDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/DoubleDecoder.scala
new file mode 100644
index 00000000..e5fccb91
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/DoubleDecoder.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+
+object DoubleDecoder extends BinaryDecoder {
+ def decode(buffer: ByteBuf): Any = buffer.readDouble()
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/FloatDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/FloatDecoder.scala
new file mode 100644
index 00000000..25d00296
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/FloatDecoder.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+
+object FloatDecoder extends BinaryDecoder {
+ def decode(buffer: ByteBuf): Any = buffer.readFloat()
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/IntegerDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/IntegerDecoder.scala
new file mode 100644
index 00000000..3a515a03
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/IntegerDecoder.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+
+object IntegerDecoder extends BinaryDecoder {
+ def decode(buffer: ByteBuf): Any = buffer.readInt()
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/LongDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/LongDecoder.scala
new file mode 100644
index 00000000..b5800252
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/LongDecoder.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+
+object LongDecoder extends BinaryDecoder {
+ def decode(buffer: ByteBuf): Any = buffer.readLong()
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/NullDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/NullDecoder.scala
new file mode 100644
index 00000000..3b2181e7
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/NullDecoder.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+
+object NullDecoder extends BinaryDecoder {
+ def decode(buffer: ByteBuf): Any = null
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/ShortDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/ShortDecoder.scala
new file mode 100644
index 00000000..f5a04b74
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/ShortDecoder.scala
@@ -0,0 +1,23 @@
+/*
+* Copyright 2013 Maurício Linhares
+*
+* Maurício Linhares licenses this file to you under the Apache License,
+* version 2.0 (the "License"); you may not use this file except in compliance
+* with the License. You may obtain a copy of the License at:
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+* License for the specific language governing permissions and limitations
+* under the License.
+*/
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+
+object ShortDecoder extends BinaryDecoder {
+ def decode(buffer: ByteBuf): Any = buffer.readShort()
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/StringDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/StringDecoder.scala
new file mode 100644
index 00000000..56c05a15
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/StringDecoder.scala
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import com.github.mauricio.async.db.util.Log
+import java.nio.charset.Charset
+
+object StringDecoder {
+ final val log = Log.get[StringDecoder]
+}
+
+class StringDecoder( charset : Charset ) extends BinaryDecoder {
+
+ def decode(buffer: ByteBuf): Any = {
+ buffer.readLengthEncodedString(charset)
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/TimeDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/TimeDecoder.scala
new file mode 100644
index 00000000..8b7f1ac6
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/TimeDecoder.scala
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+import scala.concurrent.duration._
+
+object TimeDecoder extends BinaryDecoder {
+ def decode(buffer: ByteBuf): Duration = {
+
+ buffer.readUnsignedByte() match {
+ case 0 => 0.seconds
+ case 8 => {
+
+ val isNegative = buffer.readUnsignedByte() == 1
+
+ val duration = buffer.readUnsignedInt().days +
+ buffer.readUnsignedByte().hours +
+ buffer.readUnsignedByte().minutes +
+ buffer.readUnsignedByte().seconds
+
+ if ( isNegative ) {
+ duration.neg()
+ } else {
+ duration
+ }
+
+ }
+ case 12 => {
+
+ val isNegative = buffer.readUnsignedByte() == 1
+
+ val duration = buffer.readUnsignedInt().days +
+ buffer.readUnsignedByte().hours +
+ buffer.readUnsignedByte().minutes +
+ buffer.readUnsignedByte().seconds +
+ buffer.readUnsignedInt().micros
+
+ if ( isNegative ) {
+ duration.neg()
+ } else {
+ duration
+ }
+
+ }
+ }
+
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/TimestampDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/TimestampDecoder.scala
new file mode 100644
index 00000000..b7476a7a
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/decoder/TimestampDecoder.scala
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.decoder
+
+import io.netty.buffer.ByteBuf
+import org.joda.time.LocalDateTime
+
+object TimestampDecoder extends BinaryDecoder {
+ def decode(buffer: ByteBuf): LocalDateTime = {
+ val size = buffer.readUnsignedByte()
+
+ size match {
+ case 0 => null
+ case 4 => new LocalDateTime()
+ .withDate(buffer.readUnsignedShort(), buffer.readUnsignedByte(), buffer.readUnsignedByte())
+ .withTime(0, 0, 0, 0)
+ case 7 => new LocalDateTime()
+ .withDate(buffer.readUnsignedShort(), buffer.readUnsignedByte(), buffer.readUnsignedByte())
+ .withTime(buffer.readUnsignedByte(), buffer.readUnsignedByte(), buffer.readUnsignedByte(), 0)
+ case 11 => new LocalDateTime()
+ .withDate(buffer.readUnsignedShort(), buffer.readUnsignedByte(), buffer.readUnsignedByte())
+ .withTime(buffer.readUnsignedByte(), buffer.readUnsignedByte(), buffer.readUnsignedByte(), buffer.readUnsignedInt().toInt / 1000)
+ }
+ }
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/BinaryEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/BinaryEncoder.scala
new file mode 100644
index 00000000..bb504ce6
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/BinaryEncoder.scala
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+
+trait BinaryEncoder {
+
+ def encode( value : Any, buffer : ByteBuf )
+
+ def encodesTo : Int
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/BooleanEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/BooleanEncoder.scala
new file mode 100644
index 00000000..56700009
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/BooleanEncoder.scala
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object BooleanEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val boolean = value.asInstanceOf[Boolean]
+ if ( boolean ) {
+ buffer.writeByte(1)
+ } else {
+ buffer.writeByte(0)
+ }
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TINY
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteArrayEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteArrayEncoder.scala
new file mode 100644
index 00000000..260f22a4
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteArrayEncoder.scala
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object ByteArrayEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val bytes = value.asInstanceOf[Array[Byte]]
+
+ buffer.writeLength(bytes.length)
+ buffer.writeBytes(bytes)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_BLOB
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteBufEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteBufEncoder.scala
new file mode 100644
index 00000000..62b62560
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteBufEncoder.scala
@@ -0,0 +1,17 @@
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import io.netty.buffer.ByteBuf
+
+object ByteBufEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val bytes = value.asInstanceOf[ByteBuf]
+
+ buffer.writeLength(bytes.readableBytes())
+ buffer.writeBytes(bytes)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_BLOB
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteBufferEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteBufferEncoder.scala
new file mode 100644
index 00000000..329709ad
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteBufferEncoder.scala
@@ -0,0 +1,19 @@
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import java.nio.ByteBuffer
+
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import io.netty.buffer.ByteBuf
+
+object ByteBufferEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val bytes = value.asInstanceOf[ByteBuffer]
+
+ buffer.writeLength(bytes.remaining())
+ buffer.writeBytes(bytes)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_BLOB
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteEncoder.scala
new file mode 100644
index 00000000..33472753
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ByteEncoder.scala
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import com.github.mauricio.async.db.util.Log
+
+object ByteEncoder extends BinaryEncoder {
+
+ def encode(value: Any, buffer: ByteBuf) {
+ buffer.writeByte(value.asInstanceOf[Byte])
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TINY
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/CalendarEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/CalendarEncoder.scala
new file mode 100644
index 00000000..488af40a
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/CalendarEncoder.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import java.util.Calendar
+import org.joda.time.{LocalDateTime, DateTime}
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object CalendarEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val calendar = value.asInstanceOf[Calendar]
+ LocalDateTimeEncoder.encode(new LocalDateTime(calendar.getTimeInMillis), buffer)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TIMESTAMP
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/DateTimeEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/DateTimeEncoder.scala
new file mode 100644
index 00000000..9e7f4956
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/DateTimeEncoder.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import org.joda.time._
+
+object DateTimeEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val instant = value.asInstanceOf[ReadableDateTime]
+
+ LocalDateTimeEncoder.encode(new LocalDateTime(instant.getMillis), buffer)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TIMESTAMP
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/DoubleEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/DoubleEncoder.scala
new file mode 100644
index 00000000..9e792c8d
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/DoubleEncoder.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object DoubleEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ buffer.writeDouble(value.asInstanceOf[Double])
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_DOUBLE
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/DurationEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/DurationEncoder.scala
new file mode 100644
index 00000000..f3231e17
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/DurationEncoder.scala
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import scala.concurrent.duration._
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object DurationEncoder extends BinaryEncoder {
+
+ private final val Zero = 0.seconds
+
+ def encode(value: Any, buffer: ByteBuf) {
+ val duration = value.asInstanceOf[Duration]
+
+ val days = duration.toDays
+ val hoursDuration = duration - days.days
+ val hours = hoursDuration.toHours
+ val minutesDuration = hoursDuration - hours.hours
+ val minutes = minutesDuration.toMinutes
+ val secondsDuration = minutesDuration - minutes.minutes
+ val seconds = secondsDuration.toSeconds
+ val microsDuration = secondsDuration - seconds.seconds
+ val micros = microsDuration.toMicros
+
+ val hasMicros = micros != 0
+
+ if ( hasMicros ) {
+ buffer.writeByte(12)
+ } else {
+ buffer.writeByte(8)
+ }
+
+ if (duration > Zero) {
+ buffer.writeByte(0)
+ } else {
+ buffer.writeByte(1)
+ }
+
+ buffer.writeInt(days.asInstanceOf[Int])
+ buffer.writeByte(hours.asInstanceOf[Int])
+ buffer.writeByte(minutes.asInstanceOf[Int])
+ buffer.writeByte(seconds.asInstanceOf[Int])
+
+ if ( hasMicros ) {
+ buffer.writeInt(micros.asInstanceOf[Int])
+ }
+
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TIME
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/FloatEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/FloatEncoder.scala
new file mode 100644
index 00000000..f664ac35
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/FloatEncoder.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object FloatEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ buffer.writeFloat(value.asInstanceOf[Float])
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_FLOAT
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/IntegerEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/IntegerEncoder.scala
new file mode 100644
index 00000000..03c35fda
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/IntegerEncoder.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object IntegerEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ buffer.writeInt(value.asInstanceOf[Int])
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_LONG
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/JavaDateEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/JavaDateEncoder.scala
new file mode 100644
index 00000000..65710db5
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/JavaDateEncoder.scala
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import org.joda.time.{LocalDateTime, DateTime}
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object JavaDateEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val date = value.asInstanceOf[java.util.Date]
+ LocalDateTimeEncoder.encode(new LocalDateTime(date.getTime), buffer)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TIMESTAMP
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LocalDateEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LocalDateEncoder.scala
new file mode 100644
index 00000000..09bbec4f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LocalDateEncoder.scala
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import org.joda.time.LocalDate
+import com.github.mauricio.async.db.exceptions.DateEncoderNotAvailableException
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object LocalDateEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val date = value.asInstanceOf[LocalDate]
+
+ buffer.writeByte(4)
+ buffer.writeShort(date.getYear)
+ buffer.writeByte(date.getMonthOfYear)
+ buffer.writeByte(date.getDayOfMonth)
+
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_DATE
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LocalDateTimeEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LocalDateTimeEncoder.scala
new file mode 100644
index 00000000..48b1f6c6
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LocalDateTimeEncoder.scala
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import org.joda.time._
+
+object LocalDateTimeEncoder extends BinaryEncoder {
+
+ def encode(value: Any, buffer: ByteBuf) {
+ val instant = value.asInstanceOf[LocalDateTime]
+
+ val hasMillis = instant.getMillisOfSecond != 0
+
+ if ( hasMillis ) {
+ buffer.writeByte(11)
+ } else {
+ buffer.writeByte(7)
+ }
+
+ buffer.writeShort(instant.getYear)
+ buffer.writeByte(instant.getMonthOfYear)
+ buffer.writeByte(instant.getDayOfMonth)
+ buffer.writeByte(instant.getHourOfDay)
+ buffer.writeByte(instant.getMinuteOfHour)
+ buffer.writeByte(instant.getSecondOfMinute)
+
+ if ( hasMillis ) {
+ buffer.writeInt(instant.getMillisOfSecond * 1000)
+ }
+
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TIMESTAMP
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LocalTimeEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LocalTimeEncoder.scala
new file mode 100644
index 00000000..ccbf9dac
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LocalTimeEncoder.scala
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import org.joda.time.LocalTime
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object LocalTimeEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val time = value.asInstanceOf[LocalTime]
+
+ val hasMillis = time.getMillisOfSecond != 0
+
+ if ( hasMillis ) {
+ buffer.writeByte(12)
+ } else {
+ buffer.writeByte(8)
+ }
+
+ if ( time.getMillisOfDay > 0 ) {
+ buffer.writeByte(0)
+ } else {
+ buffer.writeByte(1)
+ }
+
+ buffer.writeInt(0)
+
+ buffer.writeByte(time.getHourOfDay)
+ buffer.writeByte(time.getMinuteOfHour)
+ buffer.writeByte(time.getSecondOfMinute)
+
+ if ( hasMillis ) {
+ buffer.writeInt(time.getMillisOfSecond * 1000)
+ }
+
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TIME
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LongEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LongEncoder.scala
new file mode 100644
index 00000000..91c86e92
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/LongEncoder.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object LongEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ buffer.writeLong(value.asInstanceOf[Long])
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_LONGLONG
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ReadableInstantEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ReadableInstantEncoder.scala
new file mode 100644
index 00000000..e5781d3a
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ReadableInstantEncoder.scala
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import org.joda.time._
+
+object ReadableInstantEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val date = value.asInstanceOf[ReadableInstant]
+ LocalDateTimeEncoder.encode(new LocalDateTime(date.getMillis), buffer)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TIMESTAMP
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/SQLDateEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/SQLDateEncoder.scala
new file mode 100644
index 00000000..6af7d7ea
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/SQLDateEncoder.scala
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import org.joda.time.LocalDate
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object SQLDateEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val date = value.asInstanceOf[java.sql.Date]
+
+ LocalDateEncoder.encode(new LocalDate(date), buffer)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_DATE
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/SQLTimeEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/SQLTimeEncoder.scala
new file mode 100644
index 00000000..199c0d1b
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/SQLTimeEncoder.scala
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import org.joda.time.LocalTime
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object SQLTimeEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val sqlTime = value.asInstanceOf[java.sql.Time].getTime
+ val time = new LocalTime( sqlTime )
+ LocalTimeEncoder.encode(time, buffer)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TIME
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/SQLTimestampEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/SQLTimestampEncoder.scala
new file mode 100644
index 00000000..f76b2ebc
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/SQLTimestampEncoder.scala
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import org.joda.time.{LocalDateTime, DateTime}
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object SQLTimestampEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ val date = value.asInstanceOf[java.sql.Timestamp]
+ LocalDateTimeEncoder.encode(new LocalDateTime(date.getTime), buffer)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_TIMESTAMP
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ShortEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ShortEncoder.scala
new file mode 100644
index 00000000..86cbbc66
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/ShortEncoder.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+
+object ShortEncoder extends BinaryEncoder {
+ def encode(value: Any, buffer: ByteBuf) {
+ buffer.writeShort(value.asInstanceOf[Short])
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_SHORT
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/StringEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/StringEncoder.scala
new file mode 100644
index 00000000..d7ac4614
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/binary/encoder/StringEncoder.scala
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import com.github.mauricio.async.db.util.Log
+import java.nio.charset.Charset
+
+object StringEncoder {
+ final val log = Log.get[StringEncoder]
+}
+
+class StringEncoder( charset : Charset ) extends BinaryEncoder {
+
+ def encode(value: Any, buffer: ByteBuf) {
+ buffer.writeLenghtEncodedString(value.toString, charset)
+ }
+
+ def encodesTo: Int = ColumnTypes.FIELD_TYPE_VARCHAR
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/DecoderRegistry.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/DecoderRegistry.scala
new file mode 100644
index 00000000..798f9231
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/DecoderRegistry.scala
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.codec
+
+import com.github.mauricio.async.db.mysql.binary.decoder._
+import scala.annotation.switch
+import com.github.mauricio.async.db.mysql.column._
+import com.github.mauricio.async.db.mysql.util.CharsetMapper
+import java.nio.charset.Charset
+import com.github.mauricio.async.db.column._
+import com.github.mauricio.async.db.column.{ByteDecoder => TextByteDecoder}
+import com.github.mauricio.async.db.mysql.binary.decoder.ByteDecoder
+import com.github.mauricio.async.db.mysql.binary.decoder.TimeDecoder
+import com.github.mauricio.async.db.mysql.column.{TimeDecoder => TextTimeDecoder}
+
+class DecoderRegistry(charset: Charset) {
+
+ private final val bigDecimalDecoder = new BigDecimalDecoder(charset)
+ private final val stringDecoder = new StringDecoder(charset)
+
+ def binaryDecoderFor(columnType: Int, charsetCode: Int): BinaryDecoder = {
+
+ (columnType: @switch) match {
+ case ColumnTypes.FIELD_TYPE_VARCHAR |
+ ColumnTypes.FIELD_TYPE_ENUM => this.stringDecoder
+ case ColumnTypes.FIELD_TYPE_BLOB |
+ ColumnTypes.FIELD_TYPE_LONG_BLOB |
+ ColumnTypes.FIELD_TYPE_MEDIUM_BLOB |
+ ColumnTypes.FIELD_TYPE_TINY_BLOB |
+ ColumnTypes.FIELD_TYPE_VAR_STRING |
+ ColumnTypes.FIELD_TYPE_STRING => {
+ if (charsetCode == CharsetMapper.Binary) {
+ ByteArrayDecoder
+ } else {
+ this.stringDecoder
+ }
+ }
+ case ColumnTypes.FIELD_TYPE_BIT => ByteArrayDecoder
+ case ColumnTypes.FIELD_TYPE_LONGLONG => LongDecoder
+ case ColumnTypes.FIELD_TYPE_LONG | ColumnTypes.FIELD_TYPE_INT24 => IntegerDecoder
+ case ColumnTypes.FIELD_TYPE_YEAR | ColumnTypes.FIELD_TYPE_SHORT => ShortDecoder
+ case ColumnTypes.FIELD_TYPE_TINY => ByteDecoder
+ case ColumnTypes.FIELD_TYPE_DOUBLE => DoubleDecoder
+ case ColumnTypes.FIELD_TYPE_FLOAT => FloatDecoder
+ case ColumnTypes.FIELD_TYPE_NUMERIC |
+ ColumnTypes.FIELD_TYPE_DECIMAL |
+ ColumnTypes.FIELD_TYPE_NEW_DECIMAL => this.bigDecimalDecoder
+ case ColumnTypes.FIELD_TYPE_DATETIME | ColumnTypes.FIELD_TYPE_TIMESTAMP => TimestampDecoder
+ case ColumnTypes.FIELD_TYPE_DATE => DateDecoder
+ case ColumnTypes.FIELD_TYPE_TIME => TimeDecoder
+ case ColumnTypes.FIELD_TYPE_NULL => NullDecoder
+ }
+ }
+
+ def textDecoderFor(columnType: Int, charsetCode: Int): ColumnDecoder = {
+ (columnType: @switch) match {
+ case ColumnTypes.FIELD_TYPE_DATE => DateEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_DATETIME |
+ ColumnTypes.FIELD_TYPE_TIMESTAMP => LocalDateTimeEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_DECIMAL |
+ ColumnTypes.FIELD_TYPE_NEW_DECIMAL |
+ ColumnTypes.FIELD_TYPE_NUMERIC => BigDecimalEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_DOUBLE => DoubleEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_FLOAT => FloatEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_INT24 => IntegerEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_LONG => IntegerEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_LONGLONG => LongEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_NEWDATE => DateEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_SHORT => ShortEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_TIME => TextTimeDecoder
+ case ColumnTypes.FIELD_TYPE_TINY => TextByteDecoder
+ case ColumnTypes.FIELD_TYPE_VARCHAR |
+ ColumnTypes.FIELD_TYPE_ENUM => StringEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_YEAR => ShortEncoderDecoder
+ case ColumnTypes.FIELD_TYPE_BIT => ByteArrayColumnDecoder
+ case ColumnTypes.FIELD_TYPE_BLOB |
+ ColumnTypes.FIELD_TYPE_VAR_STRING |
+ ColumnTypes.FIELD_TYPE_STRING => {
+ if (charsetCode == CharsetMapper.Binary) {
+ ByteArrayColumnDecoder
+ } else {
+ StringEncoderDecoder
+ }
+ }
+ case _ => StringEncoderDecoder
+ }
+
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/LittleEndianByteBufAllocator.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/LittleEndianByteBufAllocator.scala
new file mode 100644
index 00000000..0fdc790a
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/LittleEndianByteBufAllocator.scala
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2013 Norman Maurer
+ *
+ * Norman Maurer, licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package com.github.mauricio.async.db.mysql.codec
+
+import io.netty.buffer.{CompositeByteBuf, UnpooledByteBufAllocator, ByteBuf, ByteBufAllocator}
+import java.nio.ByteOrder
+
+object LittleEndianByteBufAllocator {
+ val INSTANCE = new LittleEndianByteBufAllocator
+}
+
+/**
+ * Allocates ByteBuf which have LITTLE_ENDIAN order.
+ */
+class LittleEndianByteBufAllocator extends ByteBufAllocator {
+ private val allocator = new UnpooledByteBufAllocator(false)
+
+ def isDirectBufferPooled: Boolean = false
+
+ def buffer() = littleEndian(allocator.buffer())
+
+ def buffer(initialCapacity: Int) = littleEndian(allocator.buffer(initialCapacity))
+
+ def buffer(initialCapacity: Int, maxCapacity: Int) = littleEndian(allocator.buffer(initialCapacity, maxCapacity))
+
+ def ioBuffer() = littleEndian(allocator.ioBuffer())
+
+ def ioBuffer(initialCapacity: Int) = littleEndian(allocator.ioBuffer(initialCapacity))
+
+ def ioBuffer(initialCapacity: Int, maxCapacity: Int) = littleEndian(allocator.ioBuffer(initialCapacity, maxCapacity))
+
+ def heapBuffer() = littleEndian(allocator.heapBuffer())
+
+ def heapBuffer(initialCapacity: Int) = littleEndian(allocator.heapBuffer(initialCapacity))
+
+ def heapBuffer(initialCapacity: Int, maxCapacity: Int) = littleEndian(allocator.heapBuffer(initialCapacity, maxCapacity))
+
+ def directBuffer() = littleEndian(allocator.directBuffer())
+
+ def directBuffer(initialCapacity: Int) = littleEndian(allocator.directBuffer(initialCapacity))
+
+ def directBuffer(initialCapacity: Int, maxCapacity: Int): ByteBuf = littleEndian(allocator.directBuffer(initialCapacity, maxCapacity))
+
+ def compositeBuffer() = allocator.compositeBuffer()
+
+ def compositeBuffer(maxNumComponents: Int) = allocator.compositeBuffer(maxNumComponents)
+
+ def compositeHeapBuffer() = allocator.compositeHeapBuffer()
+
+ def compositeHeapBuffer(maxNumComponents: Int) = allocator.compositeHeapBuffer(maxNumComponents)
+
+ def compositeDirectBuffer() = allocator.compositeDirectBuffer()
+
+ def compositeDirectBuffer(maxNumComponents: Int): CompositeByteBuf = allocator.compositeDirectBuffer(maxNumComponents)
+
+ def calculateNewCapacity(minNewCapacity: Int, maxCapacity: Int): Int = allocator.calculateNewCapacity(minNewCapacity, maxCapacity)
+
+ private def littleEndian(b: ByteBuf) = b.order(ByteOrder.LITTLE_ENDIAN)
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLConnectionHandler.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLConnectionHandler.scala
new file mode 100644
index 00000000..792aff77
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLConnectionHandler.scala
@@ -0,0 +1,363 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.codec
+
+import java.net.InetSocketAddress
+import java.nio.ByteBuffer
+import java.util.concurrent.TimeUnit
+
+import com.github.mauricio.async.db.Configuration
+import com.github.mauricio.async.db.exceptions.DatabaseException
+import com.github.mauricio.async.db.general.MutableResultSet
+import com.github.mauricio.async.db.mysql.binary.BinaryRowDecoder
+import com.github.mauricio.async.db.mysql.message.client._
+import com.github.mauricio.async.db.mysql.message.server._
+import com.github.mauricio.async.db.mysql.util.CharsetMapper
+import com.github.mauricio.async.db.util.ChannelFutureTransformer.toFuture
+import com.github.mauricio.async.db.util._
+import io.netty.bootstrap.Bootstrap
+import io.netty.buffer.{ByteBuf, ByteBufAllocator, Unpooled}
+import io.netty.channel._
+import io.netty.channel.socket.nio.NioSocketChannel
+import io.netty.handler.codec.CodecException
+
+import scala.annotation.switch
+import scala.collection.mutable.{ArrayBuffer, HashMap}
+import scala.concurrent._
+import scala.concurrent.duration.Duration
+
+class MySQLConnectionHandler(
+ configuration: Configuration,
+ charsetMapper: CharsetMapper,
+ handlerDelegate: MySQLHandlerDelegate,
+ group : EventLoopGroup,
+ executionContext : ExecutionContext,
+ connectionId : String
+ )
+ extends SimpleChannelInboundHandler[Object] {
+
+ private implicit val internalPool = executionContext
+ private final val log = Log.getByName(s"[connection-handler]${connectionId}")
+ private final val bootstrap = new Bootstrap().group(this.group)
+ private final val connectionPromise = Promise[MySQLConnectionHandler]
+ private final val decoder = new MySQLFrameDecoder(configuration.charset, connectionId)
+ private final val encoder = new MySQLOneToOneEncoder(configuration.charset, charsetMapper)
+ private final val sendLongDataEncoder = new SendLongDataEncoder()
+ private final val currentParameters = new ArrayBuffer[ColumnDefinitionMessage]()
+ private final val currentColumns = new ArrayBuffer[ColumnDefinitionMessage]()
+ private final val parsedStatements = new HashMap[String,PreparedStatementHolder]()
+ private final val binaryRowDecoder = new BinaryRowDecoder()
+
+ private var currentPreparedStatementHolder : PreparedStatementHolder = null
+ private var currentPreparedStatement : PreparedStatement = null
+ private var currentQuery : MutableResultSet[ColumnDefinitionMessage] = null
+ private var currentContext: ChannelHandlerContext = null
+
+ def connect: Future[MySQLConnectionHandler] = {
+ this.bootstrap.channel(classOf[NioSocketChannel])
+ this.bootstrap.handler(new ChannelInitializer[io.netty.channel.Channel]() {
+
+ override def initChannel(channel: io.netty.channel.Channel): Unit = {
+ channel.pipeline.addLast(
+ decoder,
+ encoder,
+ sendLongDataEncoder,
+ MySQLConnectionHandler.this)
+ }
+
+ })
+
+ this.bootstrap.option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true)
+ this.bootstrap.option[ByteBufAllocator](ChannelOption.ALLOCATOR, LittleEndianByteBufAllocator.INSTANCE)
+
+ this.bootstrap.connect(new InetSocketAddress(configuration.host, configuration.port)).onFailure {
+ case exception => this.connectionPromise.tryFailure(exception)
+ }
+
+ this.connectionPromise.future
+ }
+
+ override def channelRead0(ctx: ChannelHandlerContext, message: Object) {
+ message match {
+ case m: ServerMessage => {
+ (m.kind: @switch) match {
+ case ServerMessage.ServerProtocolVersion => {
+ handlerDelegate.onHandshake(m.asInstanceOf[HandshakeMessage])
+ }
+ case ServerMessage.Ok => {
+ this.clearQueryState
+ handlerDelegate.onOk(m.asInstanceOf[OkMessage])
+ }
+ case ServerMessage.Error => {
+ this.clearQueryState
+ handlerDelegate.onError(m.asInstanceOf[ErrorMessage])
+ }
+ case ServerMessage.EOF => {
+ this.handleEOF(m)
+ }
+ case ServerMessage.ColumnDefinition => {
+ val message = m.asInstanceOf[ColumnDefinitionMessage]
+
+ if ( currentPreparedStatementHolder != null && this.currentPreparedStatementHolder.needsAny ) {
+ this.currentPreparedStatementHolder.add(message)
+ }
+
+ this.currentColumns += message
+ }
+ case ServerMessage.ColumnDefinitionFinished => {
+ this.onColumnDefinitionFinished()
+ }
+ case ServerMessage.PreparedStatementPrepareResponse => {
+ this.onPreparedStatementPrepareResponse(m.asInstanceOf[PreparedStatementPrepareResponse])
+ }
+ case ServerMessage.Row => {
+ val message = m.asInstanceOf[ResultSetRowMessage]
+ val items = new Array[Any](message.size)
+
+ var x = 0
+ while ( x < message.size ) {
+ items(x) = if ( message(x) == null ) {
+ null
+ } else {
+ val columnDescription = this.currentQuery.columnTypes(x)
+ columnDescription.textDecoder.decode(columnDescription, message(x), configuration.charset)
+ }
+ x += 1
+ }
+
+ this.currentQuery.addRow(items)
+ }
+ case ServerMessage.BinaryRow => {
+ val message = m.asInstanceOf[BinaryRowMessage]
+ this.currentQuery.addRow( this.binaryRowDecoder.decode(message.buffer, this.currentColumns ))
+ }
+ case ServerMessage.ParamProcessingFinished => {
+ }
+ case ServerMessage.ParamAndColumnProcessingFinished => {
+ this.onColumnDefinitionFinished()
+ }
+ }
+ }
+ }
+
+ }
+
+ override def channelActive(ctx: ChannelHandlerContext): Unit = {
+ log.debug("Channel became active")
+ handlerDelegate.connected(ctx)
+ }
+
+
+ override def channelInactive(ctx: ChannelHandlerContext) {
+ log.debug("Channel became inactive")
+ }
+
+ override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
+ // unwrap CodecException if needed
+ cause match {
+ case t: CodecException => handleException(t.getCause)
+ case _ => handleException(cause)
+ }
+
+ }
+
+ private def handleException(cause: Throwable) {
+ if (!this.connectionPromise.isCompleted) {
+ this.connectionPromise.failure(cause)
+ }
+ handlerDelegate.exceptionCaught(cause)
+ }
+
+ override def handlerAdded(ctx: ChannelHandlerContext) {
+ this.currentContext = ctx
+ }
+
+ def write( message : QueryMessage ) : ChannelFuture = {
+ this.decoder.queryProcessStarted()
+ writeAndHandleError(message)
+ }
+
+ def sendPreparedStatement( query: String, values: Seq[Any] ): Future[ChannelFuture] = {
+ val preparedStatement = new PreparedStatement(query, values)
+
+ this.currentColumns.clear()
+ this.currentParameters.clear()
+
+ this.currentPreparedStatement = preparedStatement
+
+ this.parsedStatements.get(preparedStatement.statement) match {
+ case Some( item ) => {
+ this.executePreparedStatement(item.statementId, item.columns.size, preparedStatement.values, item.parameters)
+ }
+ case None => {
+ decoder.preparedStatementPrepareStarted()
+ writeAndHandleError( new PreparedStatementPrepareMessage(preparedStatement.statement) )
+ }
+ }
+ }
+
+ def write( message : HandshakeResponseMessage ) : ChannelFuture = {
+ decoder.hasDoneHandshake = true
+ writeAndHandleError(message)
+ }
+
+ def write( message : AuthenticationSwitchResponse ) : ChannelFuture = writeAndHandleError(message)
+
+ def write( message : QuitMessage ) : ChannelFuture = {
+ writeAndHandleError(message)
+ }
+
+ def disconnect: ChannelFuture = this.currentContext.close()
+
+ def clearQueryState {
+ this.currentColumns.clear()
+ this.currentParameters.clear()
+ this.currentQuery = null
+ }
+
+ def isConnected : Boolean = {
+ if ( this.currentContext != null && this.currentContext.channel() != null ) {
+ this.currentContext.channel.isActive
+ } else {
+ false
+ }
+ }
+
+ private def executePreparedStatement( statementId : Array[Byte], columnsCount : Int, values : Seq[Any], parameters : Seq[ColumnDefinitionMessage] ): Future[ChannelFuture] = {
+ decoder.preparedStatementExecuteStarted(columnsCount, parameters.size)
+ this.currentColumns.clear()
+ this.currentParameters.clear()
+
+ val (nonLongIndicesOpt, longValuesOpt) = values.zipWithIndex.map {
+ case (Some(value), index) if isLong(value) => (None, Some(index, value))
+ case (value, index) if isLong(value) => (None, Some(index, value))
+ case (_, index) => (Some(index), None)
+ }.unzip
+ val nonLongIndices: Seq[Int] = nonLongIndicesOpt.flatten
+ val longValues: Seq[(Int, Any)] = longValuesOpt.flatten
+
+ if (longValues.nonEmpty) {
+ val (firstIndex, firstValue) = longValues.head
+ var channelFuture: Future[ChannelFuture] = sendLongParameter(statementId, firstIndex, firstValue)
+ longValues.tail foreach { case (index, value) =>
+ channelFuture = channelFuture.flatMap { _ =>
+ sendLongParameter(statementId, index, value)
+ }
+ }
+ channelFuture flatMap { _ =>
+ writeAndHandleError(new PreparedStatementExecuteMessage(statementId, values, nonLongIndices.toSet, parameters))
+ }
+ } else {
+ writeAndHandleError(new PreparedStatementExecuteMessage(statementId, values, nonLongIndices.toSet, parameters))
+ }
+ }
+
+ private def isLong(value: Any): Boolean = {
+ value match {
+ case v : Array[Byte] => v.length > SendLongDataEncoder.LONG_THRESHOLD
+ case v : ByteBuffer => v.remaining() > SendLongDataEncoder.LONG_THRESHOLD
+ case v : ByteBuf => v.readableBytes() > SendLongDataEncoder.LONG_THRESHOLD
+
+ case _ => false
+ }
+ }
+
+ private def sendLongParameter(statementId: Array[Byte], index: Int, longValue: Any): Future[ChannelFuture] = {
+ longValue match {
+ case v : Array[Byte] =>
+ sendBuffer(Unpooled.wrappedBuffer(v), statementId, index)
+
+ case v : ByteBuffer =>
+ sendBuffer(Unpooled.wrappedBuffer(v), statementId, index)
+
+ case v : ByteBuf =>
+ sendBuffer(v, statementId, index)
+ }
+ }
+
+ private def sendBuffer(buffer: ByteBuf, statementId: Array[Byte], paramId: Int): ChannelFuture = {
+ writeAndHandleError(new SendLongDataMessage(statementId, buffer, paramId))
+ }
+
+ private def onPreparedStatementPrepareResponse( message : PreparedStatementPrepareResponse ) {
+ this.currentPreparedStatementHolder = new PreparedStatementHolder( this.currentPreparedStatement.statement, message)
+ }
+
+ def onColumnDefinitionFinished() {
+
+ val columns = if ( this.currentPreparedStatementHolder != null ) {
+ this.currentPreparedStatementHolder.columns
+ } else {
+ this.currentColumns
+ }
+
+ this.currentQuery = new MutableResultSet[ColumnDefinitionMessage](columns)
+
+ if ( this.currentPreparedStatementHolder != null ) {
+ this.parsedStatements.put( this.currentPreparedStatementHolder.statement, this.currentPreparedStatementHolder )
+ this.executePreparedStatement(
+ this.currentPreparedStatementHolder.statementId,
+ this.currentPreparedStatementHolder.columns.size,
+ this.currentPreparedStatement.values,
+ this.currentPreparedStatementHolder.parameters
+ )
+ this.currentPreparedStatementHolder = null
+ this.currentPreparedStatement = null
+ }
+ }
+
+ private def writeAndHandleError( message : Any ) : ChannelFuture = {
+ if ( this.currentContext.channel().isActive ) {
+ val res = this.currentContext.writeAndFlush(message)
+
+ res.onFailure {
+ case e : Throwable => handleException(e)
+ }
+
+ res
+ } else {
+ val error = new DatabaseException("This channel is not active and can't take messages")
+ handleException(error)
+ this.currentContext.channel().newFailedFuture(error)
+ }
+ }
+
+ private def handleEOF( m : ServerMessage ) {
+ m match {
+ case eof : EOFMessage => {
+ val resultSet = this.currentQuery
+ this.clearQueryState
+
+ if ( resultSet != null ) {
+ handlerDelegate.onResultSet( resultSet, eof )
+ } else {
+ handlerDelegate.onEOF(eof)
+ }
+ }
+ case authenticationSwitch : AuthenticationSwitchRequest => {
+ handlerDelegate.switchAuthentication(authenticationSwitch)
+ }
+ }
+ }
+
+ def schedule(block: => Unit, duration: Duration): Unit = {
+ this.currentContext.channel().eventLoop().schedule(new Runnable {
+ override def run(): Unit = block
+ }, duration.toMillis, TimeUnit.MILLISECONDS)
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLFrameDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLFrameDecoder.scala
new file mode 100644
index 00000000..bd55f4fd
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLFrameDecoder.scala
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.codec
+
+import com.github.mauricio.async.db.exceptions._
+import com.github.mauricio.async.db.mysql.decoder._
+import com.github.mauricio.async.db.mysql.message.server._
+import com.github.mauricio.async.db.util.ByteBufferUtils.read3BytesInt
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import com.github.mauricio.async.db.util.{BufferDumper, Log}
+import io.netty.buffer.ByteBuf
+import io.netty.channel.ChannelHandlerContext
+import io.netty.handler.codec.ByteToMessageDecoder
+import java.nio.ByteOrder
+import java.nio.charset.Charset
+import java.util.concurrent.atomic.AtomicInteger
+
+
+class MySQLFrameDecoder(charset: Charset, connectionId: String) extends ByteToMessageDecoder {
+
+ private final val log = Log.getByName(s"[frame-decoder]${connectionId}")
+ private final val messagesCount = new AtomicInteger()
+ private final val handshakeDecoder = new HandshakeV10Decoder(charset)
+ private final val errorDecoder = new ErrorDecoder(charset)
+ private final val okDecoder = new OkDecoder(charset)
+ private final val columnDecoder = new ColumnDefinitionDecoder(charset, new DecoderRegistry(charset))
+ private final val rowDecoder = new ResultSetRowDecoder(charset)
+ private final val preparedStatementPrepareDecoder = new PreparedStatementPrepareResponseDecoder()
+ private final val authenticationSwitchDecoder = new AuthenticationSwitchRequestDecoder(charset)
+
+ private[codec] var processingColumns = false
+ private[codec] var processingParams = false
+ private[codec] var isInQuery = false
+ private[codec] var isPreparedStatementPrepare = false
+ private[codec] var isPreparedStatementExecute = false
+ private[codec] var isPreparedStatementExecuteRows = false
+ private[codec] var hasDoneHandshake = false
+
+ private[codec] var totalParams = 0L
+ private[codec] var processedParams = 0L
+ private[codec] var totalColumns = 0L
+ private[codec] var processedColumns = 0L
+
+ private var hasReadColumnsCount = false
+
+ def decode(ctx: ChannelHandlerContext, buffer: ByteBuf, out: java.util.List[Object]): Unit = {
+ if (buffer.readableBytes() > 4) {
+
+ buffer.markReaderIndex()
+
+ val size = read3BytesInt(buffer)
+
+ val sequence = buffer.readUnsignedByte() // we have to read this
+
+ if (buffer.readableBytes() >= size) {
+
+ messagesCount.incrementAndGet()
+
+ val messageType = buffer.getByte(buffer.readerIndex())
+
+ if (size < 0) {
+ throw new NegativeMessageSizeException(messageType, size)
+ }
+
+ val slice = buffer.readSlice(size)
+
+ if (log.isTraceEnabled) {
+ log.trace(s"Reading message type $messageType - " +
+ s"(count=$messagesCount,hasDoneHandshake=$hasDoneHandshake,size=$size,isInQuery=$isInQuery,processingColumns=$processingColumns,processingParams=$processingParams,processedColumns=$processedColumns,processedParams=$processedParams)" +
+ s"\n${BufferDumper.dumpAsHex(slice)}}")
+ }
+
+ slice.readByte()
+
+ if (this.hasDoneHandshake) {
+ this.handleCommonFlow(messageType, slice, out)
+ } else {
+ val decoder = messageType match {
+ case ServerMessage.Error => {
+ this.clear
+ this.errorDecoder
+ }
+ case _ => this.handshakeDecoder
+ }
+ this.doDecoding(decoder, slice, out)
+ }
+ } else {
+ buffer.resetReaderIndex()
+ }
+
+ }
+ }
+
+ private def handleCommonFlow(messageType: Byte, slice: ByteBuf, out: java.util.List[Object]) {
+ val decoder = messageType match {
+ case ServerMessage.Error => {
+ this.clear
+ this.errorDecoder
+ }
+ case ServerMessage.EOF => {
+
+ if (this.processingParams && this.totalParams > 0) {
+ this.processingParams = false
+ if (this.totalColumns == 0) {
+ ParamAndColumnProcessingFinishedDecoder
+ } else {
+ ParamProcessingFinishedDecoder
+ }
+ } else {
+ if (this.processingColumns) {
+ this.processingColumns = false
+ ColumnProcessingFinishedDecoder
+ } else {
+ this.clear
+ EOFMessageDecoder
+ }
+ }
+
+ }
+ case ServerMessage.Ok => {
+ if (this.isPreparedStatementPrepare) {
+ this.preparedStatementPrepareDecoder
+ } else {
+ if (this.isPreparedStatementExecuteRows) {
+ null
+ } else {
+ this.clear
+ this.okDecoder
+ }
+ }
+ }
+ case _ => {
+
+ if (this.isInQuery) {
+ null
+ } else {
+ throw new ParserNotAvailableException(messageType)
+ }
+
+ }
+ }
+
+ doDecoding(decoder, slice, out)
+ }
+
+ private def doDecoding(decoder: MessageDecoder, slice: ByteBuf, out: java.util.List[Object]) {
+ if (decoder == null) {
+ slice.readerIndex(slice.readerIndex() - 1)
+ val result = decodeQueryResult(slice)
+
+ if (slice.readableBytes() != 0) {
+ throw new BufferNotFullyConsumedException(slice)
+ }
+ if (result != null) {
+ out.add(result)
+ }
+ } else {
+ val result = decoder.decode(slice)
+
+ result match {
+ case m: PreparedStatementPrepareResponse => {
+ this.hasReadColumnsCount = true
+ this.totalColumns = m.columnsCount
+ this.totalParams = m.paramsCount
+ }
+ case m: ParamAndColumnProcessingFinishedMessage => {
+ this.clear
+ }
+ case m: ColumnProcessingFinishedMessage if this.isPreparedStatementPrepare => {
+ this.clear
+ }
+ case m: ColumnProcessingFinishedMessage if this.isPreparedStatementExecute => {
+ this.isPreparedStatementExecuteRows = true
+ }
+ case _ =>
+ }
+
+ if (slice.readableBytes() != 0) {
+ throw new BufferNotFullyConsumedException(slice)
+ }
+
+ if (result != null) {
+ result match {
+ case m: PreparedStatementPrepareResponse => {
+ out.add(result)
+ if (m.columnsCount == 0 && m.paramsCount == 0) {
+ this.clear
+ out.add(new ParamAndColumnProcessingFinishedMessage(new EOFMessage(0, 0)))
+ }
+ }
+ case _ => out.add(result)
+ }
+ }
+ }
+ }
+
+ private def decodeQueryResult(slice: ByteBuf): AnyRef = {
+ if (!hasReadColumnsCount) {
+ this.hasReadColumnsCount = true
+ this.totalColumns = slice.readBinaryLength
+ return null
+ }
+
+ if (this.processingParams && this.totalParams != this.processedParams) {
+ this.processedParams += 1
+ return this.columnDecoder.decode(slice)
+ }
+
+
+ if (this.totalColumns == this.processedColumns) {
+ if (this.isPreparedStatementExecute) {
+ val row = slice.readBytes(slice.readableBytes())
+ row.readByte() // reads initial 00 at message
+ new BinaryRowMessage(row)
+ } else {
+ this.rowDecoder.decode(slice)
+ }
+ } else {
+ this.processedColumns += 1
+ this.columnDecoder.decode(slice)
+ }
+
+ }
+
+ def preparedStatementPrepareStarted() {
+ this.queryProcessStarted()
+ this.hasReadColumnsCount = true
+ this.processingParams = true
+ this.processingColumns = true
+ this.isPreparedStatementPrepare = true
+ }
+
+ def preparedStatementExecuteStarted(columnsCount: Int, paramsCount: Int) {
+ this.queryProcessStarted()
+ this.hasReadColumnsCount = false
+ this.totalColumns = columnsCount
+ this.totalParams = paramsCount
+ this.isPreparedStatementExecute = true
+ this.processingParams = false
+ }
+
+ def queryProcessStarted() {
+ this.isInQuery = true
+ this.processingColumns = true
+ this.hasReadColumnsCount = false
+ }
+
+ private def clear {
+ this.isPreparedStatementPrepare = false
+ this.isPreparedStatementExecute = false
+ this.isPreparedStatementExecuteRows = false
+ this.isInQuery = false
+ this.processingColumns = false
+ this.processingParams = false
+ this.totalColumns = 0
+ this.processedColumns = 0
+ this.totalParams = 0
+ this.processedParams = 0
+ this.hasReadColumnsCount = false
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLHandlerDelegate.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLHandlerDelegate.scala
new file mode 100644
index 00000000..10c25df7
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLHandlerDelegate.scala
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.codec
+
+import com.github.mauricio.async.db.ResultSet
+import com.github.mauricio.async.db.mysql.message.server._
+import io.netty.channel.ChannelHandlerContext
+
+trait MySQLHandlerDelegate {
+
+ def onHandshake( message : HandshakeMessage )
+ def onError( message : ErrorMessage )
+ def onOk( message : OkMessage )
+ def onEOF( message : EOFMessage )
+ def exceptionCaught( exception : Throwable )
+ def connected( ctx : ChannelHandlerContext )
+ def onResultSet( resultSet : ResultSet, message : EOFMessage )
+ def switchAuthentication( message : AuthenticationSwitchRequest )
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLOneToOneEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLOneToOneEncoder.scala
new file mode 100644
index 00000000..f666cbc8
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/MySQLOneToOneEncoder.scala
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.codec
+
+import com.github.mauricio.async.db.exceptions.EncoderNotAvailableException
+import com.github.mauricio.async.db.mysql.binary.BinaryRowEncoder
+import com.github.mauricio.async.db.mysql.encoder._
+import com.github.mauricio.async.db.mysql.message.client.ClientMessage
+import com.github.mauricio.async.db.mysql.util.CharsetMapper
+import com.github.mauricio.async.db.util.{BufferDumper, ByteBufferUtils, Log}
+import java.nio.charset.Charset
+import scala.annotation.switch
+import io.netty.channel.ChannelHandlerContext
+import io.netty.handler.codec.MessageToMessageEncoder
+import io.netty.buffer.ByteBuf
+
+object MySQLOneToOneEncoder {
+ val log = Log.get[MySQLOneToOneEncoder]
+}
+
+class MySQLOneToOneEncoder(charset: Charset, charsetMapper: CharsetMapper)
+ extends MessageToMessageEncoder[ClientMessage](classOf[ClientMessage]) {
+
+ import MySQLOneToOneEncoder.log
+
+ private final val handshakeResponseEncoder = new HandshakeResponseEncoder(charset, charsetMapper)
+ private final val queryEncoder = new QueryMessageEncoder(charset)
+ private final val rowEncoder = new BinaryRowEncoder(charset)
+ private final val prepareEncoder = new PreparedStatementPrepareEncoder(charset)
+ private final val executeEncoder = new PreparedStatementExecuteEncoder(rowEncoder)
+ private final val authenticationSwitchEncoder = new AuthenticationSwitchResponseEncoder(charset)
+
+ private var sequence = 1
+
+ def encode(ctx: ChannelHandlerContext, message: ClientMessage, out: java.util.List[Object]): Unit = {
+ val encoder = (message.kind: @switch) match {
+ case ClientMessage.ClientProtocolVersion => this.handshakeResponseEncoder
+ case ClientMessage.Quit => {
+ sequence = 0
+ QuitMessageEncoder
+ }
+ case ClientMessage.Query => {
+ sequence = 0
+ this.queryEncoder
+ }
+ case ClientMessage.PreparedStatementExecute => {
+ sequence = 0
+ this.executeEncoder
+ }
+ case ClientMessage.PreparedStatementPrepare => {
+ sequence = 0
+ this.prepareEncoder
+ }
+ case ClientMessage.AuthSwitchResponse => {
+ sequence += 1
+ this.authenticationSwitchEncoder
+ }
+ case _ => throw new EncoderNotAvailableException(message)
+ }
+
+ val result: ByteBuf = encoder.encode(message)
+
+ ByteBufferUtils.writePacketLength(result, sequence)
+
+ sequence += 1
+
+ if ( log.isTraceEnabled ) {
+ log.trace(s"Writing message ${message.getClass.getName} - \n${BufferDumper.dumpAsHex(result)}")
+ }
+
+ out.add(result)
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/PreparedStatement.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/PreparedStatement.scala
new file mode 100644
index 00000000..08fb0d9f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/PreparedStatement.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.codec
+
+case class PreparedStatement ( statement : String, values : Seq[Any])
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/PreparedStatementHolder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/PreparedStatementHolder.scala
new file mode 100644
index 00000000..4578a118
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/PreparedStatementHolder.scala
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.codec
+
+import com.github.mauricio.async.db.mysql.message.server.{ColumnDefinitionMessage, PreparedStatementPrepareResponse}
+import scala.collection.mutable.ArrayBuffer
+
+class PreparedStatementHolder( val statement : String, val message : PreparedStatementPrepareResponse ) {
+
+ val columns = new ArrayBuffer[ColumnDefinitionMessage]
+ val parameters = new ArrayBuffer[ColumnDefinitionMessage]
+
+ def statementId : Array[Byte] = message.statementId
+
+ def needsParameters : Boolean = message.paramsCount != this.parameters.length
+
+ def needsColumns : Boolean = message.columnsCount != this.columns.length
+
+ def needsAny : Boolean = this.needsParameters || this.needsColumns
+
+ def add( column : ColumnDefinitionMessage ) {
+ if ( this.needsParameters ) {
+ this.parameters += column
+ } else {
+ if ( this.needsColumns ) {
+ this.columns += column
+ }
+ }
+ }
+
+ override def toString: String = s"PreparedStatementHolder($statement)"
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/SendLongDataEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/SendLongDataEncoder.scala
new file mode 100644
index 00000000..ce51140f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/codec/SendLongDataEncoder.scala
@@ -0,0 +1,40 @@
+package com.github.mauricio.async.db.mysql.codec
+
+import com.github.mauricio.async.db.mysql.message.client.{ClientMessage, SendLongDataMessage}
+import com.github.mauricio.async.db.util.{ByteBufferUtils, Log}
+import io.netty.buffer.Unpooled
+import io.netty.channel.ChannelHandlerContext
+import io.netty.handler.codec.MessageToMessageEncoder
+
+object SendLongDataEncoder {
+ val log = Log.get[SendLongDataEncoder]
+
+ val LONG_THRESHOLD = 1023
+}
+
+class SendLongDataEncoder
+ extends MessageToMessageEncoder[SendLongDataMessage](classOf[SendLongDataMessage]) {
+
+ import com.github.mauricio.async.db.mysql.codec.SendLongDataEncoder.log
+
+ def encode(ctx: ChannelHandlerContext, message: SendLongDataMessage, out: java.util.List[Object]): Unit = {
+ if ( log.isTraceEnabled ) {
+ log.trace(s"Writing message ${message.toString}")
+ }
+
+ val sequence = 0
+
+ val headerBuffer = ByteBufferUtils.mysqlBuffer(3 + 1 + 1 + 4 + 2)
+ ByteBufferUtils.write3BytesInt(headerBuffer, 1 + 4 + 2 + message.value.readableBytes())
+ headerBuffer.writeByte(sequence)
+
+ headerBuffer.writeByte(ClientMessage.PreparedStatementSendLongData)
+ headerBuffer.writeBytes(message.statementId)
+ headerBuffer.writeShort(message.paramId)
+
+ val result = Unpooled.wrappedBuffer(headerBuffer, message.value)
+
+ out.add(result)
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/column/ByteArrayColumnDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/column/ByteArrayColumnDecoder.scala
new file mode 100644
index 00000000..b7956c6a
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/column/ByteArrayColumnDecoder.scala
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.column
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.column.ColumnDecoder
+import java.nio.charset.Charset
+import com.github.mauricio.async.db.general.ColumnData
+
+object ByteArrayColumnDecoder extends ColumnDecoder {
+
+ override def decode(kind: ColumnData , value: ByteBuf, charset: Charset): Any = {
+ val bytes = new Array[Byte](value.readableBytes())
+ value.readBytes(bytes)
+ bytes
+ }
+
+ def decode(value: String): Any = {
+ throw new UnsupportedOperationException("This method should never be called for byte arrays")
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/column/ColumnTypes.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/column/ColumnTypes.scala
new file mode 100644
index 00000000..b8bc89f7
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/column/ColumnTypes.scala
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.column
+
+object ColumnTypes {
+
+ final val FIELD_TYPE_BIT = 16
+
+ final val FIELD_TYPE_BLOB = 252
+
+ final val FIELD_TYPE_DATE = 10
+
+ final val FIELD_TYPE_DATETIME = 12
+
+ final val FIELD_TYPE_DECIMAL = 0
+
+ final val FIELD_TYPE_NUMERIC = -10
+
+ final val FIELD_TYPE_DOUBLE = 5
+
+ final val FIELD_TYPE_ENUM = 247
+
+ final val FIELD_TYPE_FLOAT = 4
+
+ final val FIELD_TYPE_GEOMETRY = 255
+
+ final val FIELD_TYPE_INT24 = 9
+
+ final val FIELD_TYPE_LONG = 3
+
+ final val FIELD_TYPE_LONG_BLOB = 251
+
+ final val FIELD_TYPE_LONGLONG = 8
+
+ final val FIELD_TYPE_MEDIUM_BLOB = 250
+
+ final val FIELD_TYPE_NEW_DECIMAL = 246
+
+ final val FIELD_TYPE_NEWDATE = 14
+
+ final val FIELD_TYPE_NULL = 6
+
+ final val FIELD_TYPE_SET = 248
+
+ final val FIELD_TYPE_SHORT = 2
+
+ final val FIELD_TYPE_STRING = 254
+
+ final val FIELD_TYPE_TIME = 11
+
+ final val FIELD_TYPE_TIMESTAMP = 7
+
+ final val FIELD_TYPE_TINY = 1
+
+ final val FIELD_TYPE_TINY_BLOB = 249
+
+ final val FIELD_TYPE_VAR_STRING = 253
+
+ final val FIELD_TYPE_VARCHAR = 15
+
+ final val FIELD_TYPE_YEAR = 13
+
+ final val Mapping = Map(
+ FIELD_TYPE_BIT -> "bit",
+ FIELD_TYPE_BLOB -> "blob",
+ FIELD_TYPE_DATE -> "date",
+ FIELD_TYPE_DATETIME -> "datetime",
+ FIELD_TYPE_DECIMAL -> "decimal",
+ FIELD_TYPE_DOUBLE -> "double",
+ FIELD_TYPE_ENUM -> "enum",
+ FIELD_TYPE_FLOAT -> "float",
+ FIELD_TYPE_GEOMETRY -> "geometry",
+ FIELD_TYPE_INT24 -> "int64",
+ FIELD_TYPE_LONG -> "integer",
+ FIELD_TYPE_LONGLONG -> "long",
+ FIELD_TYPE_LONG_BLOB -> "long_blob",
+ FIELD_TYPE_MEDIUM_BLOB -> "medium_blob",
+ FIELD_TYPE_NEW_DECIMAL -> "new_decimal",
+ FIELD_TYPE_NEWDATE -> "new_date",
+ FIELD_TYPE_NULL -> "null",
+ FIELD_TYPE_NUMERIC -> "numeric",
+ FIELD_TYPE_SET -> "set",
+ FIELD_TYPE_SHORT -> "short",
+ FIELD_TYPE_STRING -> "string",
+ FIELD_TYPE_TIME -> "time",
+ FIELD_TYPE_TIMESTAMP -> "timestamp",
+ FIELD_TYPE_TINY -> "tiny",
+ FIELD_TYPE_TINY_BLOB -> "tiny_blob",
+ FIELD_TYPE_VAR_STRING -> "var_string",
+ FIELD_TYPE_VARCHAR -> "varchar",
+ FIELD_TYPE_YEAR -> "year"
+ )
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/column/TimeDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/column/TimeDecoder.scala
new file mode 100644
index 00000000..56098c3c
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/column/TimeDecoder.scala
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.column
+
+import com.github.mauricio.async.db.column.ColumnDecoder
+import scala.concurrent.duration._
+
+object TimeDecoder extends ColumnDecoder {
+
+ final val Hour = 1.hour.toMillis
+
+ override def decode(value: String): Duration = {
+
+ val pieces = value.split(':')
+
+ val secondsAndMillis = pieces(2).split('.')
+
+ val parts = if ( secondsAndMillis.length == 2 ) {
+ (secondsAndMillis(0).toInt,secondsAndMillis(1).toInt)
+ } else {
+ (secondsAndMillis(0).toInt,0)
+ }
+
+ val hours = pieces(0).toInt
+ val minutes = pieces(1).toInt
+
+ hours.hours + minutes.minutes + parts._1.seconds + parts._2.millis
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/AuthenticationSwitchRequestDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/AuthenticationSwitchRequestDecoder.scala
new file mode 100644
index 00000000..d96abfd0
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/AuthenticationSwitchRequestDecoder.scala
@@ -0,0 +1,15 @@
+package com.github.mauricio.async.db.mysql.decoder
+
+import java.nio.charset.Charset
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.server.{AuthenticationSwitchRequest, ServerMessage}
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+
+class AuthenticationSwitchRequestDecoder( charset : Charset ) extends MessageDecoder {
+ def decode(buffer: ByteBuf): ServerMessage = {
+ new AuthenticationSwitchRequest(
+ buffer.readCString(charset),
+ buffer.readUntilEOF(charset)
+ )
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ColumnDefinitionDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ColumnDefinitionDecoder.scala
new file mode 100644
index 00000000..70fc3f01
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ColumnDefinitionDecoder.scala
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.server.ColumnDefinitionMessage
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import com.github.mauricio.async.db.util.Log
+import java.nio.charset.Charset
+import com.github.mauricio.async.db.mysql.codec.DecoderRegistry
+
+object ColumnDefinitionDecoder {
+ final val log = Log.get[ColumnDefinitionDecoder]
+}
+
+class ColumnDefinitionDecoder(charset: Charset, registry : DecoderRegistry) extends MessageDecoder {
+
+ override def decode(buffer: ByteBuf): ColumnDefinitionMessage = {
+
+ val catalog = buffer.readLengthEncodedString(charset)
+ val schema = buffer.readLengthEncodedString(charset)
+ val table = buffer.readLengthEncodedString(charset)
+ val originalTable = buffer.readLengthEncodedString(charset)
+ val name = buffer.readLengthEncodedString(charset)
+ val originalName = buffer.readLengthEncodedString(charset)
+
+ buffer.readBinaryLength
+
+ val characterSet = buffer.readUnsignedShort()
+ val columnLength = buffer.readUnsignedInt()
+ val columnType = buffer.readUnsignedByte()
+ val flags = buffer.readShort()
+ val decimals = buffer.readByte()
+
+ buffer.readShort()
+
+ new ColumnDefinitionMessage(
+ catalog,
+ schema,
+ table,
+ originalTable,
+ name,
+ originalName,
+ characterSet,
+ columnLength,
+ columnType,
+ flags,
+ decimals,
+ registry.binaryDecoderFor(columnType, characterSet),
+ registry.textDecoderFor(columnType,characterSet)
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ColumnProcessingFinishedDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ColumnProcessingFinishedDecoder.scala
new file mode 100644
index 00000000..1dfa615f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ColumnProcessingFinishedDecoder.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.server.{ColumnProcessingFinishedMessage, ServerMessage}
+
+object ColumnProcessingFinishedDecoder extends MessageDecoder {
+
+ def decode(buffer: ByteBuf): ServerMessage = {
+ new ColumnProcessingFinishedMessage( EOFMessageDecoder.decode(buffer) )
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/EOFMessageDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/EOFMessageDecoder.scala
new file mode 100644
index 00000000..318d71c1
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/EOFMessageDecoder.scala
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.server.EOFMessage
+
+object EOFMessageDecoder extends MessageDecoder {
+
+ def decode(buffer: ByteBuf): EOFMessage = {
+ new EOFMessage(
+ buffer.readUnsignedShort(),
+ buffer.readUnsignedShort() )
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ErrorDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ErrorDecoder.scala
new file mode 100644
index 00000000..4cc36c27
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ErrorDecoder.scala
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.server.{ErrorMessage, ServerMessage}
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import java.nio.charset.Charset
+import scala.language.implicitConversions
+
+class ErrorDecoder( charset : Charset ) extends MessageDecoder {
+
+ def decode(buffer: ByteBuf): ServerMessage = {
+
+ new ErrorMessage(
+ buffer.readShort(),
+ buffer.readFixedString( 6, charset ),
+ buffer.readUntilEOF(charset)
+ )
+
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/HandshakeV10Decoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/HandshakeV10Decoder.scala
new file mode 100644
index 00000000..05feca12
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/HandshakeV10Decoder.scala
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import java.nio.charset.Charset
+
+import com.github.mauricio.async.db.mysql.encoder.auth.AuthenticationMethod
+import com.github.mauricio.async.db.mysql.message.server.{HandshakeMessage, ServerMessage}
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import com.github.mauricio.async.db.util.Log
+import io.netty.buffer.ByteBuf
+import io.netty.util.CharsetUtil
+
+object HandshakeV10Decoder {
+ final val log = Log.get[HandshakeV10Decoder]
+ final val SeedSize = 8
+ final val SeedComplementSize = 12
+ final val Padding = 10
+ final val ASCII = CharsetUtil.US_ASCII
+}
+
+class HandshakeV10Decoder(charset: Charset) extends MessageDecoder {
+
+ import com.github.mauricio.async.db.mysql.decoder.HandshakeV10Decoder._
+ import com.github.mauricio.async.db.mysql.util.MySQLIO._
+
+ def decode(buffer: ByteBuf): ServerMessage = {
+
+ val serverVersion = buffer.readCString(ASCII)
+ val connectionId = buffer.readUnsignedInt()
+
+ var seed = new Array[Byte](SeedSize + SeedComplementSize)
+ buffer.readBytes(seed, 0, SeedSize)
+
+ buffer.readByte() // filler
+
+ // read capability flags (lower 2 bytes)
+ var serverCapabilityFlags = buffer.readUnsignedShort()
+
+ /* New protocol with 16 bytes to describe server characteristics */
+ // read character set (1 byte)
+ val characterSet = buffer.readByte() & 0xff
+ // read status flags (2 bytes)
+ val statusFlags = buffer.readUnsignedShort()
+
+ // read capability flags (upper 2 bytes)
+ serverCapabilityFlags |= buffer.readUnsignedShort() << 16
+
+ var authPluginDataLength = 0
+ var authenticationMethod = AuthenticationMethod.Native
+
+ if ((serverCapabilityFlags & CLIENT_PLUGIN_AUTH) != 0) {
+ // read length of auth-plugin-data (1 byte)
+ authPluginDataLength = buffer.readByte() & 0xff
+ } else {
+ // read filler ([00])
+ buffer.readByte()
+ }
+
+ // next 10 bytes are reserved (all [00])
+ buffer.readerIndex(buffer.readerIndex() + Padding)
+
+ log.debug(s"Auth plugin data length was ${authPluginDataLength}")
+
+ if ((serverCapabilityFlags & CLIENT_SECURE_CONNECTION) != 0) {
+ val complement = if ( authPluginDataLength > 0 ) {
+ authPluginDataLength - 1 - SeedSize
+ } else {
+ SeedComplementSize
+ }
+
+ buffer.readBytes(seed, SeedSize, complement)
+ buffer.readByte()
+ }
+
+ if ((serverCapabilityFlags & CLIENT_PLUGIN_AUTH) != 0) {
+ authenticationMethod = buffer.readUntilEOF(ASCII)
+ }
+
+ val message = new HandshakeMessage(
+ serverVersion,
+ connectionId,
+ seed,
+ serverCapabilityFlags,
+ characterSet = characterSet,
+ statusFlags = statusFlags,
+ authenticationMethod = authenticationMethod
+ )
+
+ log.debug(s"handshake message was ${message}")
+
+ message
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/MessageDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/MessageDecoder.scala
new file mode 100644
index 00000000..246b29ab
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/MessageDecoder.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.server.ServerMessage
+
+trait MessageDecoder {
+
+ def decode( buffer : ByteBuf ) : ServerMessage
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/OkDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/OkDecoder.scala
new file mode 100644
index 00000000..f2e56376
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/OkDecoder.scala
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.server.{OkMessage, ServerMessage}
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import java.nio.charset.Charset
+
+class OkDecoder( charset : Charset ) extends MessageDecoder {
+
+ def decode(buffer: ByteBuf): ServerMessage = {
+
+ new OkMessage(
+ buffer.readBinaryLength,
+ buffer.readBinaryLength,
+ buffer.readShort(),
+ buffer.readShort(),
+ buffer.readUntilEOF(charset)
+ )
+
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ParamAndColumnProcessingFinishedDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ParamAndColumnProcessingFinishedDecoder.scala
new file mode 100644
index 00000000..959ef40c
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ParamAndColumnProcessingFinishedDecoder.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.server.{ParamAndColumnProcessingFinishedMessage, ServerMessage}
+
+object ParamAndColumnProcessingFinishedDecoder extends MessageDecoder {
+ def decode(buffer: ByteBuf): ServerMessage = {
+ new ParamAndColumnProcessingFinishedMessage(EOFMessageDecoder.decode(buffer))
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ParamProcessingFinishedDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ParamProcessingFinishedDecoder.scala
new file mode 100644
index 00000000..faaec179
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ParamProcessingFinishedDecoder.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.server.{ParamProcessingFinishedMessage, ServerMessage}
+
+object ParamProcessingFinishedDecoder extends MessageDecoder {
+
+ def decode(buffer: ByteBuf): ServerMessage = {
+ new ParamProcessingFinishedMessage(EOFMessageDecoder.decode(buffer))
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/PreparedStatementPrepareResponseDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/PreparedStatementPrepareResponseDecoder.scala
new file mode 100644
index 00000000..dfb3a73d
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/PreparedStatementPrepareResponseDecoder.scala
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import com.github.mauricio.async.db.mysql.message.server.{PreparedStatementPrepareResponse, ServerMessage}
+import com.github.mauricio.async.db.util.{BufferDumper, Log}
+import io.netty.buffer.ByteBuf
+
+class PreparedStatementPrepareResponseDecoder extends MessageDecoder {
+
+ final val log = Log.get[PreparedStatementPrepareResponseDecoder]
+
+ def decode(buffer: ByteBuf): ServerMessage = {
+
+ //val dump = MySQLHelper.dumpAsHex(buffer)
+ //log.debug("prepared statement response dump is \n{}", dump)
+
+ val statementId = Array[Byte]( buffer.readByte(), buffer.readByte(), buffer.readByte(), buffer.readByte() )
+ val columnsCount = buffer.readUnsignedShort()
+ val paramsCount = buffer.readUnsignedShort()
+
+ // filler
+ buffer.readByte()
+
+ val warningCount = buffer.readShort()
+
+ new PreparedStatementPrepareResponse(
+ statementId = statementId,
+ warningCount = warningCount,
+ columnsCount = columnsCount,
+ paramsCount = paramsCount
+ )
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ResultSetRowDecoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ResultSetRowDecoder.scala
new file mode 100644
index 00000000..1a2a9fbd
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/decoder/ResultSetRowDecoder.scala
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.decoder
+
+import java.nio.charset.Charset
+
+import com.github.mauricio.async.db.mysql.message.server.{ResultSetRowMessage, ServerMessage}
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import io.netty.buffer.ByteBuf
+
+object ResultSetRowDecoder {
+
+ final val NULL = 0xfb
+
+}
+
+class ResultSetRowDecoder(charset: Charset) extends MessageDecoder {
+
+ import com.github.mauricio.async.db.mysql.decoder.ResultSetRowDecoder.NULL
+
+ def decode(buffer: ByteBuf): ServerMessage = {
+ val row = new ResultSetRowMessage()
+
+ while (buffer.isReadable()) {
+ if (buffer.getUnsignedByte(buffer.readerIndex()) == NULL) {
+ buffer.readByte()
+ row += null
+ } else {
+ val length = buffer.readBinaryLength.asInstanceOf[Int]
+ row += buffer.readBytes(length)
+ }
+ }
+
+ row
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/AuthenticationSwitchResponseEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/AuthenticationSwitchResponseEncoder.scala
new file mode 100644
index 00000000..6d7a0e7c
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/AuthenticationSwitchResponseEncoder.scala
@@ -0,0 +1,27 @@
+package com.github.mauricio.async.db.mysql.encoder
+
+import com.github.mauricio.async.db.mysql.message.client.{AuthenticationSwitchResponse, ClientMessage}
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.exceptions.UnsupportedAuthenticationMethodException
+import com.github.mauricio.async.db.mysql.encoder.auth.AuthenticationMethod
+import java.nio.charset.Charset
+import com.github.mauricio.async.db.util.ByteBufferUtils
+
+class AuthenticationSwitchResponseEncoder( charset : Charset ) extends MessageEncoder {
+
+ def encode(message: ClientMessage): ByteBuf = {
+ val switch = message.asInstanceOf[AuthenticationSwitchResponse]
+
+ val method = switch.request.method
+ val authenticator = AuthenticationMethod.Availables.getOrElse(
+ method, { throw new UnsupportedAuthenticationMethodException(method) })
+
+ val buffer = ByteBufferUtils.packetBuffer()
+
+ val bytes = authenticator.generateAuthentication(charset, switch.password, switch.request.seed.getBytes(charset) )
+ buffer.writeBytes(bytes)
+
+ buffer
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/HandshakeResponseEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/HandshakeResponseEncoder.scala
new file mode 100644
index 00000000..9865e55e
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/HandshakeResponseEncoder.scala
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.encoder
+
+import java.nio.charset.Charset
+
+import com.github.mauricio.async.db.exceptions.UnsupportedAuthenticationMethodException
+import com.github.mauricio.async.db.mysql.encoder.auth.AuthenticationMethod
+import com.github.mauricio.async.db.mysql.message.client.{ClientMessage, HandshakeResponseMessage}
+import com.github.mauricio.async.db.mysql.util.CharsetMapper
+import com.github.mauricio.async.db.util.{ByteBufferUtils, Log}
+import io.netty.buffer.ByteBuf
+
+object HandshakeResponseEncoder {
+
+ final val MAX_3_BYTES = 0x00ffffff
+ final val PADDING: Array[Byte] = List.fill(23) {
+ 0.toByte
+ }.toArray
+
+ final val log = Log.get[HandshakeResponseEncoder]
+
+}
+
+class HandshakeResponseEncoder(charset: Charset, charsetMapper: CharsetMapper) extends MessageEncoder {
+
+ import com.github.mauricio.async.db.mysql.encoder.HandshakeResponseEncoder._
+ import com.github.mauricio.async.db.mysql.util.MySQLIO._
+
+ private val authenticationMethods = AuthenticationMethod.Availables
+
+ def encode(message: ClientMessage): ByteBuf = {
+
+ val m = message.asInstanceOf[HandshakeResponseMessage]
+
+ var clientCapabilities = 0
+
+ clientCapabilities |=
+ CLIENT_PLUGIN_AUTH |
+ CLIENT_PROTOCOL_41 |
+ CLIENT_TRANSACTIONS |
+ CLIENT_MULTI_RESULTS |
+ CLIENT_SECURE_CONNECTION
+
+ if (m.database.isDefined) {
+ clientCapabilities |= CLIENT_CONNECT_WITH_DB
+ }
+
+ val buffer = ByteBufferUtils.packetBuffer()
+
+ buffer.writeInt(clientCapabilities)
+ buffer.writeInt(MAX_3_BYTES)
+ buffer.writeByte(charsetMapper.toInt(charset))
+ buffer.writeBytes(PADDING)
+ ByteBufferUtils.writeCString( m.username, buffer, charset )
+
+ if ( m.password.isDefined ) {
+ val method = m.authenticationMethod
+ val authenticator = this.authenticationMethods.getOrElse(
+ method, { throw new UnsupportedAuthenticationMethodException(method) })
+ val bytes = authenticator.generateAuthentication(charset, m.password, m.seed)
+ buffer.writeByte(bytes.length)
+ buffer.writeBytes(bytes)
+ } else {
+ buffer.writeByte(0)
+ }
+
+ if ( m.database.isDefined ) {
+ ByteBufferUtils.writeCString( m.database.get, buffer, charset )
+ }
+
+ ByteBufferUtils.writeCString( m.authenticationMethod, buffer, charset )
+
+ buffer
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/MessageEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/MessageEncoder.scala
new file mode 100644
index 00000000..e1704e4f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/MessageEncoder.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.client.ClientMessage
+
+trait MessageEncoder {
+
+ def encode( message : ClientMessage ) : ByteBuf
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/PreparedStatementExecuteEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/PreparedStatementExecuteEncoder.scala
new file mode 100644
index 00000000..c52658c9
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/PreparedStatementExecuteEncoder.scala
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.encoder
+
+import io.netty.buffer.{ByteBuf, Unpooled}
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import com.github.mauricio.async.db.mysql.binary.BinaryRowEncoder
+import com.github.mauricio.async.db.mysql.message.client.{PreparedStatementExecuteMessage, ClientMessage}
+import com.github.mauricio.async.db.util.ByteBufferUtils
+
+class PreparedStatementExecuteEncoder( rowEncoder : BinaryRowEncoder ) extends MessageEncoder {
+
+ def encode(message: ClientMessage): ByteBuf = {
+ val m = message.asInstanceOf[PreparedStatementExecuteMessage]
+
+ val buffer = ByteBufferUtils.packetBuffer()
+ buffer.writeByte( m.kind )
+ buffer.writeBytes(m.statementId)
+ buffer.writeByte(0x00) // no cursor
+ buffer.writeInt(1)
+
+ if ( m.parameters.isEmpty ) {
+ buffer
+ } else {
+ Unpooled.wrappedBuffer(buffer, encodeValues(m.values, m.valuesToInclude))
+ }
+
+ }
+
+ private[encoder] def encodeValues( values : Seq[Any], valuesToInclude: Set[Int] ) : ByteBuf = {
+ val nullBitsCount = (values.size + 7) / 8
+ val nullBits = new Array[Byte](nullBitsCount)
+ val bitMapBuffer = ByteBufferUtils.mysqlBuffer(1 + nullBitsCount)
+ val parameterTypesBuffer = ByteBufferUtils.mysqlBuffer(values.size * 2)
+ val parameterValuesBuffer = ByteBufferUtils.mysqlBuffer()
+
+ var index = 0
+
+ while ( index < values.length ) {
+ val value = values(index)
+ if ( value == null || value == None ) {
+ nullBits(index / 8) = (nullBits(index / 8) | (1 << (index & 7))).asInstanceOf[Byte]
+ parameterTypesBuffer.writeShort(ColumnTypes.FIELD_TYPE_NULL)
+ } else {
+ value match {
+ case Some(v) => encodeValue(parameterTypesBuffer, parameterValuesBuffer, v, valuesToInclude(index))
+ case _ => encodeValue(parameterTypesBuffer, parameterValuesBuffer, value, valuesToInclude(index))
+ }
+ }
+ index += 1
+ }
+
+ bitMapBuffer.writeBytes(nullBits)
+ if ( values.size > 0 ) {
+ bitMapBuffer.writeByte(1)
+ } else {
+ bitMapBuffer.writeByte(0)
+ }
+
+ Unpooled.wrappedBuffer( bitMapBuffer, parameterTypesBuffer, parameterValuesBuffer )
+ }
+
+ private def encodeValue(parameterTypesBuffer: ByteBuf, parameterValuesBuffer: ByteBuf, value: Any, includeValue: Boolean) : Unit = {
+ val encoder = rowEncoder.encoderFor(value)
+ parameterTypesBuffer.writeShort(encoder.encodesTo)
+ if (includeValue)
+ encoder.encode(value, parameterValuesBuffer)
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/PreparedStatementPrepareEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/PreparedStatementPrepareEncoder.scala
new file mode 100644
index 00000000..bfdb5812
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/PreparedStatementPrepareEncoder.scala
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.client.{PreparedStatementPrepareMessage, ClientMessage}
+import com.github.mauricio.async.db.util.ByteBufferUtils
+import java.nio.charset.Charset
+
+class PreparedStatementPrepareEncoder( charset : Charset ) extends MessageEncoder {
+
+ def encode(message: ClientMessage): ByteBuf = {
+ val m = message.asInstanceOf[PreparedStatementPrepareMessage]
+ val statement = m.statement.getBytes(charset)
+ val buffer = ByteBufferUtils.packetBuffer( 4 + 1 + statement.size)
+ buffer.writeByte( m.kind )
+ buffer.writeBytes( statement )
+
+ buffer
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/QueryMessageEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/QueryMessageEncoder.scala
new file mode 100644
index 00000000..a8e0ec1f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/QueryMessageEncoder.scala
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.client.{QueryMessage, ClientMessage}
+import com.github.mauricio.async.db.util.ByteBufferUtils
+import java.nio.charset.Charset
+
+class QueryMessageEncoder( charset : Charset ) extends MessageEncoder {
+
+ def encode(message: ClientMessage): ByteBuf = {
+
+ val m = message.asInstanceOf[QueryMessage]
+ val encodedQuery = m.query.getBytes( charset )
+ val buffer = ByteBufferUtils.packetBuffer(4 + 1 + encodedQuery.length )
+ buffer.writeByte( ClientMessage.Query )
+ buffer.writeBytes( encodedQuery )
+
+ buffer
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/QuitMessageEncoder.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/QuitMessageEncoder.scala
new file mode 100644
index 00000000..1b4add07
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/QuitMessageEncoder.scala
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.encoder
+
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.mysql.message.client.ClientMessage
+import com.github.mauricio.async.db.util.ByteBufferUtils
+
+object QuitMessageEncoder extends MessageEncoder {
+
+ def encode(message: ClientMessage): ByteBuf = {
+ val buffer = ByteBufferUtils.packetBuffer(5)
+ buffer.writeByte( ClientMessage.Quit )
+ buffer
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/auth/AuthenticationMethod.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/auth/AuthenticationMethod.scala
new file mode 100644
index 00000000..50cf1073
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/auth/AuthenticationMethod.scala
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.encoder.auth
+
+import java.nio.charset.Charset
+
+object AuthenticationMethod {
+
+ final val Native = "mysql_native_password"
+ final val Old = "mysql_old_password"
+
+ final val Availables = Map(
+ Native -> MySQLNativePasswordAuthentication,
+ Old -> OldPasswordAuthentication
+ )
+}
+
+trait AuthenticationMethod {
+
+ def generateAuthentication( charset : Charset, password : Option[String], seed : Array[Byte] ) : Array[Byte]
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/auth/MySQLNativePasswordAuthentication.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/auth/MySQLNativePasswordAuthentication.scala
new file mode 100644
index 00000000..21991684
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/auth/MySQLNativePasswordAuthentication.scala
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.encoder.auth
+
+import java.nio.charset.Charset
+import java.security.MessageDigest
+
+object MySQLNativePasswordAuthentication extends AuthenticationMethod {
+
+ final val EmptyArray = Array.empty[Byte]
+
+ def generateAuthentication(charset : Charset, password: Option[String], seed : Array[Byte]): Array[Byte] = {
+
+ if ( password.isDefined ) {
+ scramble411(charset, password.get, seed )
+ } else {
+ EmptyArray
+ }
+
+ }
+
+ private def scramble411(charset : Charset, password : String, seed : Array[Byte] ) : Array[Byte] = {
+
+ val messageDigest = MessageDigest.getInstance("SHA-1")
+ val initialDigest = messageDigest.digest(password.getBytes(charset))
+
+ messageDigest.reset()
+
+ val finalDigest = messageDigest.digest(initialDigest)
+
+ messageDigest.reset()
+
+ messageDigest.update(seed)
+ messageDigest.update(finalDigest)
+
+ val result = messageDigest.digest()
+ var counter = 0
+
+ while ( counter < result.length ) {
+ result(counter) = (result(counter) ^ initialDigest(counter)).asInstanceOf[Byte]
+ counter += 1
+ }
+
+ result
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/auth/OldPasswordAuthentication.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/auth/OldPasswordAuthentication.scala
new file mode 100644
index 00000000..0b5ca289
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/encoder/auth/OldPasswordAuthentication.scala
@@ -0,0 +1,76 @@
+package com.github.mauricio.async.db.mysql.encoder.auth
+
+import java.nio.charset.Charset
+import java.nio.CharBuffer
+
+object OldPasswordAuthentication extends AuthenticationMethod {
+
+ final val EmptyArray = Array.empty[Byte]
+
+ def generateAuthentication(charset: Charset, password: Option[String], seed: Array[Byte]): Array[Byte] = {
+ password match {
+ case Some(value) if !value.isEmpty => {
+ newCrypt(charset, value, new String(seed, charset) )
+ }
+ case _ => EmptyArray
+ }
+ }
+
+ def newCrypt( charset: Charset, password : String, seed : String) : Array[Byte] = {
+ var b : Byte = 0
+ var d : Double = 0
+
+ val pw = newHash(seed)
+ val msg = newHash(password)
+ val max = 0x3fffffffL
+ var seed1 = (pw._1 ^ msg._1) % max
+ var seed2 = (pw._2 ^ msg._2) % max
+ val chars = new Array[Char](seed.length)
+
+ var i = 0
+ while ( i < seed.length ) {
+ seed1 = ((seed1 * 3) + seed2) % max
+ seed2 = (seed1 + seed2 + 33) % max
+ d = seed1.toDouble / max.toDouble
+ b = java.lang.Math.floor((d * 31) + 64).toByte
+ chars(i) = b.toChar
+ i += 1
+ }
+
+ seed1 = ((seed1 * 3) + seed2) % max
+ seed2 = (seed1 + seed2 + 33) % max
+ d = seed1.toDouble / max.toDouble
+ b = java.lang.Math.floor(d * 31).toByte
+
+ var j = 0
+ while( j < seed.length ) {
+ chars(j) = (chars(j) ^ b).toChar
+ j += 1
+ }
+
+ val bytes = new String(chars).getBytes(charset)
+ val result = new Array[Byte](bytes.length + 1)
+ System.arraycopy(bytes, 0, result, 0, bytes.length )
+ result
+ }
+
+ private def newHash(password : String) : (Long,Long) = {
+ var nr = 1345345333L
+ var add = 7L
+ var nr2 = 0x12345671L
+ var tmp = 0L
+
+ password.foreach {
+ c =>
+ if (c != ' ' && c != '\t') {
+ tmp = (0xff & c)
+ nr ^= ((((nr & 63) + add) * tmp) + (nr << 8))
+ nr2 += ((nr2 << 8) ^ nr)
+ add += tmp
+ }
+ }
+
+ (nr & 0x7fffffffL, nr2 & 0x7fffffffL)
+ }
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/exceptions/CharsetMappingNotAvailableException.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/exceptions/CharsetMappingNotAvailableException.scala
new file mode 100644
index 00000000..b63b2c90
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/exceptions/CharsetMappingNotAvailableException.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.exceptions
+
+import com.github.mauricio.async.db.exceptions.DatabaseException
+import java.nio.charset.Charset
+
+class CharsetMappingNotAvailableException (charset : Charset)
+ extends DatabaseException( "There is no MySQL charset mapping name for the Java Charset %s".format(charset.name()) )
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/exceptions/MySQLException.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/exceptions/MySQLException.scala
new file mode 100644
index 00000000..c8209f17
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/exceptions/MySQLException.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.exceptions
+
+import com.github.mauricio.async.db.exceptions.DatabaseException
+import com.github.mauricio.async.db.mysql.message.server.ErrorMessage
+
+class MySQLException( val errorMessage : ErrorMessage )
+ extends DatabaseException("Error %d - %s - %s".format(errorMessage.errorCode, errorMessage.sqlState, errorMessage.errorMessage))
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/AuthenticationSwitchResponse.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/AuthenticationSwitchResponse.scala
new file mode 100644
index 00000000..5a0f7a4b
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/AuthenticationSwitchResponse.scala
@@ -0,0 +1,6 @@
+package com.github.mauricio.async.db.mysql.message.client
+
+import com.github.mauricio.async.db.mysql.message.server.AuthenticationSwitchRequest
+
+case class AuthenticationSwitchResponse( password : Option[String], request : AuthenticationSwitchRequest )
+ extends ClientMessage(ClientMessage.AuthSwitchResponse)
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/ClientMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/ClientMessage.scala
new file mode 100644
index 00000000..2a2a1b1f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/ClientMessage.scala
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.client
+
+import com.github.mauricio.async.db.KindedMessage
+
+object ClientMessage {
+
+ final val ClientProtocolVersion = 0x09 // COM_STATISTICS
+ final val Quit = 0x01 // COM_QUIT
+ final val Query = 0x03 // COM_QUERY
+ final val PreparedStatementPrepare = 0x16 // COM_STMT_PREPARE
+ final val PreparedStatementExecute = 0x17 // COM_STMT_EXECUTE
+ final val PreparedStatementSendLongData = 0x18 // COM_STMT_SEND_LONG_DATA
+ final val AuthSwitchResponse = 0xfe // AuthSwitchRequest
+
+}
+
+class ClientMessage ( val kind : Int ) extends KindedMessage
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/HandshakeResponseMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/HandshakeResponseMessage.scala
new file mode 100644
index 00000000..50b7e839
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/HandshakeResponseMessage.scala
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.client
+
+import java.nio.charset.Charset
+
+case class HandshakeResponseMessage(
+ username: String,
+ charset: Charset,
+ seed: Array[Byte],
+ authenticationMethod: String,
+ password: Option[String] = None,
+ database: Option[String] = None
+ )
+ extends ClientMessage(ClientMessage.ClientProtocolVersion)
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/PreparedStatementExecuteMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/PreparedStatementExecuteMessage.scala
new file mode 100644
index 00000000..f87ddede
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/PreparedStatementExecuteMessage.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.client
+
+import com.github.mauricio.async.db.mysql.message.server.ColumnDefinitionMessage
+
+case class PreparedStatementExecuteMessage (
+ statementId : Array[Byte],
+ values : Seq[Any],
+ valuesToInclude : Set[Int],
+ parameters : Seq[ColumnDefinitionMessage] )
+ extends ClientMessage( ClientMessage.PreparedStatementExecute )
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/PreparedStatementPrepareMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/PreparedStatementPrepareMessage.scala
new file mode 100644
index 00000000..385eb011
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/PreparedStatementPrepareMessage.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.client
+
+case class PreparedStatementPrepareMessage( statement : String )
+ extends ClientMessage( ClientMessage.PreparedStatementPrepare )
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/QueryMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/QueryMessage.scala
new file mode 100644
index 00000000..ce8d3768
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/QueryMessage.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.client
+
+case class QueryMessage( query : String ) extends ClientMessage( ClientMessage.Query )
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/QuitMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/QuitMessage.scala
new file mode 100644
index 00000000..a73aeadc
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/QuitMessage.scala
@@ -0,0 +1,7 @@
+package com.github.mauricio.async.db.mysql.message.client
+
+object QuitMessage {
+ val Instance = new QuitMessage();
+}
+
+class QuitMessage extends ClientMessage( ClientMessage.Quit )
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/SendLongDataMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/SendLongDataMessage.scala
new file mode 100644
index 00000000..db66db1f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/client/SendLongDataMessage.scala
@@ -0,0 +1,8 @@
+package com.github.mauricio.async.db.mysql.message.client
+
+import io.netty.buffer.ByteBuf
+
+case class SendLongDataMessage (
+ statementId : Array[Byte],
+ value : ByteBuf,
+ paramId : Int )
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/AuthenticationSwitchRequest.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/AuthenticationSwitchRequest.scala
new file mode 100644
index 00000000..58500c1f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/AuthenticationSwitchRequest.scala
@@ -0,0 +1,6 @@
+package com.github.mauricio.async.db.mysql.message.server
+
+case class AuthenticationSwitchRequest(
+ method : String,
+ seed : String )
+ extends ServerMessage(ServerMessage.EOF)
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/BinaryRowMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/BinaryRowMessage.scala
new file mode 100644
index 00000000..2f40154c
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/BinaryRowMessage.scala
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+import io.netty.buffer.ByteBuf
+
+case class BinaryRowMessage ( buffer : ByteBuf ) extends ServerMessage( ServerMessage.BinaryRow )
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ColumnDefinitionMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ColumnDefinitionMessage.scala
new file mode 100644
index 00000000..d88cd82f
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ColumnDefinitionMessage.scala
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import com.github.mauricio.async.db.mysql.util.CharsetMapper
+import com.github.mauricio.async.db.general.ColumnData
+import com.github.mauricio.async.db.mysql.binary.decoder.BinaryDecoder
+import com.github.mauricio.async.db.column.ColumnDecoder
+
+case class ColumnDefinitionMessage(
+ catalog: String,
+ schema: String,
+ table: String,
+ originalTable: String,
+ name: String,
+ originalName: String,
+ characterSet: Int,
+ columnLength: Long,
+ columnType: Int,
+ flags: Short,
+ decimals: Byte,
+ binaryDecoder: BinaryDecoder,
+ textDecoder: ColumnDecoder
+ )
+ extends ServerMessage(ServerMessage.ColumnDefinition)
+ with ColumnData {
+
+ def dataType: Int = this.columnType
+ def dataTypeSize : Long = this.columnLength
+
+ override def toString: String = {
+ val columnTypeName = ColumnTypes.Mapping.getOrElse(columnType, columnType)
+ val charsetName = CharsetMapper.DefaultCharsetsById.getOrElse(characterSet, characterSet)
+
+ s"${this.getClass.getSimpleName}(name=$name,columnType=${columnTypeName},table=$table,charset=$charsetName,decimals=$decimals})"
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ColumnProcessingFinishedMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ColumnProcessingFinishedMessage.scala
new file mode 100644
index 00000000..df3e63e5
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ColumnProcessingFinishedMessage.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+case class ColumnProcessingFinishedMessage( eofMessage : EOFMessage ) extends ServerMessage( ServerMessage.ColumnDefinitionFinished )
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/EOFMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/EOFMessage.scala
new file mode 100644
index 00000000..bd7d767c
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/EOFMessage.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+case class EOFMessage( warningCount : Int, flags : Int )
+ extends ServerMessage( ServerMessage.EOF )
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ErrorMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ErrorMessage.scala
new file mode 100644
index 00000000..68e68eeb
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ErrorMessage.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+case class ErrorMessage( errorCode : Int, sqlState : String, errorMessage : String )
+ extends ServerMessage( ServerMessage.Error )
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/HandshakeMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/HandshakeMessage.scala
new file mode 100644
index 00000000..dd16044a
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/HandshakeMessage.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+case class HandshakeMessage(
+ serverVersion: String,
+ connectionId: Long,
+ seed: Array[Byte],
+ serverCapabilities: Int,
+ characterSet: Int,
+ statusFlags: Int,
+ authenticationMethod : String
+ )
+ extends ServerMessage(ServerMessage.ServerProtocolVersion)
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/OkMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/OkMessage.scala
new file mode 100644
index 00000000..97dc2c9b
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/OkMessage.scala
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+case class OkMessage(
+ affectedRows : Long,
+ lastInsertId : Long,
+ statusFlags : Int,
+ warnings : Int,
+ message : String )
+ extends ServerMessage( ServerMessage.Ok )
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ParamAndColumnProcessingFinishedMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ParamAndColumnProcessingFinishedMessage.scala
new file mode 100644
index 00000000..92d41d47
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ParamAndColumnProcessingFinishedMessage.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+case class ParamAndColumnProcessingFinishedMessage ( eofMessage : EOFMessage )
+ extends ServerMessage( ServerMessage.ParamAndColumnProcessingFinished )
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ParamProcessingFinishedMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ParamProcessingFinishedMessage.scala
new file mode 100644
index 00000000..145c5422
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ParamProcessingFinishedMessage.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+case class ParamProcessingFinishedMessage( eofMessage : EOFMessage )
+ extends ServerMessage( ServerMessage.ParamProcessingFinished )
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/PreparedStatementPrepareResponse.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/PreparedStatementPrepareResponse.scala
new file mode 100644
index 00000000..fd3ff480
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/PreparedStatementPrepareResponse.scala
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+case class PreparedStatementPrepareResponse (
+ statementId : Array[Byte],
+ warningCount : Short,
+ paramsCount : Int,
+ columnsCount : Int )
+ extends ServerMessage( ServerMessage.PreparedStatementPrepareResponse )
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ResultSetRowMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ResultSetRowMessage.scala
new file mode 100644
index 00000000..da752575
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ResultSetRowMessage.scala
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+import scala.collection.mutable
+import scala.collection.mutable.ArrayBuffer
+import io.netty.buffer.ByteBuf
+
+class ResultSetRowMessage
+ extends ServerMessage( ServerMessage.Row )
+ with mutable.Buffer[ByteBuf]
+{
+
+ private val buffer = new ArrayBuffer[ByteBuf]()
+
+ def length: Int = buffer.length
+
+ def apply(idx: Int): ByteBuf = buffer(idx)
+
+ def update(n: Int, newelem: ByteBuf) {
+ buffer.update(n, newelem)
+ }
+
+ def +=(elem: ByteBuf): this.type = {
+ this.buffer += elem
+ this
+ }
+
+ def clear() {
+ this.buffer.clear()
+ }
+
+ def +=:(elem: ByteBuf): this.type = {
+ this.buffer.+=:(elem)
+ this
+ }
+
+ def insertAll(n: Int, elems: Traversable[ByteBuf]) {
+ this.buffer.insertAll(n, elems)
+ }
+
+ def remove(n: Int): ByteBuf = {
+ this.buffer.remove(n)
+ }
+
+ override def iterator: Iterator[ByteBuf] = this.buffer.iterator
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ServerMessage.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ServerMessage.scala
new file mode 100644
index 00000000..ad3e58fc
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/message/server/ServerMessage.scala
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+
+package com.github.mauricio.async.db.mysql.message.server
+
+import com.github.mauricio.async.db.KindedMessage
+
+object ServerMessage {
+
+ final val ServerProtocolVersion = 10
+ final val Error = -1
+ final val Ok = 0
+ final val EOF = -2
+
+ // these messages don't actually exist
+ // but we use them to simplify the switch statements
+ final val ColumnDefinition = 100
+ final val ColumnDefinitionFinished = 101
+ final val ParamProcessingFinished = 102
+ final val ParamAndColumnProcessingFinished = 103
+ final val Row = 104
+ final val BinaryRow = 105
+ final val PreparedStatementPrepareResponse = 106
+
+}
+
+class ServerMessage( val kind : Int ) extends KindedMessage
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/pool/MySQLConnectionFactory.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/pool/MySQLConnectionFactory.scala
new file mode 100644
index 00000000..273e76af
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/pool/MySQLConnectionFactory.scala
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.pool
+
+import com.github.mauricio.async.db.Configuration
+import com.github.mauricio.async.db.pool.ObjectFactory
+import com.github.mauricio.async.db.mysql.MySQLConnection
+import scala.util.Try
+import scala.concurrent.Await
+import com.github.mauricio.async.db.util.Log
+import com.github.mauricio.async.db.exceptions.{ConnectionTimeoutedException, ConnectionStillRunningQueryException, ConnectionNotConnectedException}
+
+object MySQLConnectionFactory {
+ final val log = Log.get[MySQLConnectionFactory]
+}
+
+/**
+ *
+ * Connection pool factory for [[com.github.mauricio.async.db.mysql.MySQLConnection]] objects.
+ *
+ * @param configuration a valid configuration to connect to a MySQL server.
+ *
+ */
+
+class MySQLConnectionFactory( configuration : Configuration ) extends ObjectFactory[MySQLConnection] {
+
+ import MySQLConnectionFactory.log
+
+ /**
+ *
+ * Creates a valid object to be used in the pool. This method can block if necessary to make sure a correctly built
+ * is created.
+ *
+ * @return
+ */
+ def create: MySQLConnection = {
+ val connection = new MySQLConnection(configuration)
+ Await.result(connection.connect, configuration.connectTimeout )
+
+ connection
+ }
+
+ /**
+ *
+ * This method should "close" and release all resources acquired by the pooled object. This object will not be used
+ * anymore so any cleanup necessary to remove it from memory should be made in this method. Implementors should not
+ * raise an exception under any circumstances, the factory should log and clean up the exception itself.
+ *
+ * @param item
+ */
+ def destroy(item: MySQLConnection) {
+ try {
+ item.disconnect
+ } catch {
+ case e : Exception => {
+ log.error("Failed to close the connection", e)
+ }
+ }
+ }
+
+ /**
+ *
+ * Validates that an object can still be used for it's purpose. This method should test the object to make sure
+ * it's still valid for clients to use. If you have a database connection, test if you are still connected, if you're
+ * accessing a file system, make sure you can still see and change the file.
+ *
+ * You decide how fast this method should return and what it will test, you should usually do something that's fast
+ * enough not to slow down the pool usage, since this call will be made whenever an object returns to the pool.
+ *
+ * If this object is not valid anymore, a [[scala.util.Failure]] should be returned, otherwise [[scala.util.Success]]
+ * should be the result of this call.
+ *
+ * @param item an object produced by this pool
+ * @return
+ */
+ def validate(item: MySQLConnection): Try[MySQLConnection] = {
+ Try{
+ if ( item.isTimeouted ) {
+ throw new ConnectionTimeoutedException(item)
+ }
+ if ( !item.isConnected ) {
+ throw new ConnectionNotConnectedException(item)
+ }
+
+ if (item.lastException != null) {
+ throw item.lastException
+ }
+
+ if ( item.isQuerying ) {
+ throw new ConnectionStillRunningQueryException(item.count, false)
+ }
+
+ item
+ }
+ }
+
+ /**
+ *
+ * Does a full test on the given object making sure it's still valid. Different than validate, that's called whenever
+ * an object is given back to the pool and should usually be fast, this method will be called when objects are
+ * idle to make sure they don't "timeout" or become stale in anyway.
+ *
+ * For convenience, this method defaults to call **validate** but you can implement it in a different way if you
+ * would like to.
+ *
+ * @param item an object produced by this pool
+ * @return
+ */
+ override def test(item: MySQLConnection): Try[MySQLConnection] = {
+ Try {
+ Await.result(item.sendQuery("SELECT 0"), configuration.testTimeout)
+ item
+ }
+ }
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/CharsetMapper.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/CharsetMapper.scala
new file mode 100644
index 00000000..4446e06d
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/CharsetMapper.scala
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.util
+
+import com.github.mauricio.async.db.mysql.exceptions.CharsetMappingNotAvailableException
+import java.nio.charset.Charset
+import io.netty.util.CharsetUtil
+
+object CharsetMapper {
+
+ final val Binary = 63
+
+ final val DefaultCharsetsByCharset = Map[Charset,Int](
+ CharsetUtil.UTF_8 -> 83,
+ CharsetUtil.US_ASCII -> 11,
+ CharsetUtil.US_ASCII -> 65,
+ CharsetUtil.ISO_8859_1 -> 3,
+ CharsetUtil.ISO_8859_1 -> 69
+ )
+
+ final val DefaultCharsetsById = DefaultCharsetsByCharset.map { pair => (pair._2, pair._1.name()) }
+
+ final val Instance = new CharsetMapper()
+}
+
+class CharsetMapper( charsetsToIntComplement : Map[Charset,Int] = Map.empty[Charset,Int] ) {
+
+ private var charsetsToInt = CharsetMapper.DefaultCharsetsByCharset ++ charsetsToIntComplement
+
+ def toInt( charset : Charset ) : Int = {
+ charsetsToInt.getOrElse(charset, {
+ throw new CharsetMappingNotAvailableException(charset)
+ })
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/MySQLIO.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/MySQLIO.scala
new file mode 100644
index 00000000..3b56ecc0
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/MySQLIO.scala
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2014 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.util
+
+object MySQLIO {
+
+ final val CLIENT_PROTOCOL_41 = 0x0200
+ final val CLIENT_CONNECT_WITH_DB = 0x0008
+ final val CLIENT_TRANSACTIONS = 0x2000
+ final val CLIENT_MULTI_RESULTS = 0x20000
+ final val CLIENT_LONG_FLAG = 0x0001
+ final val CLIENT_PLUGIN_AUTH = 0x00080000
+ final val CLIENT_SECURE_CONNECTION = 0x00008000
+
+}
diff --git a/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/URLParser.scala b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/URLParser.scala
new file mode 100644
index 00000000..ba9c0333
--- /dev/null
+++ b/mysql-async/src/main/scala/com/github/mauricio/async/db/mysql/util/URLParser.scala
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package com.github.mauricio.async.db.mysql.util
+
+import com.github.mauricio.async.db.util.AbstractURIParser
+import com.github.mauricio.async.db.Configuration
+
+/**
+ * The MySQL URL parser.
+ */
+object URLParser extends AbstractURIParser {
+
+ /**
+ * The default configuration for MySQL.
+ */
+ override val DEFAULT = Configuration(
+ username = "root",
+ host = "127.0.0.1", //Matched JDBC default
+ port = 3306,
+ password = None,
+ database = None
+ )
+
+ override protected val SCHEME = "^mysql$".r
+
+}
diff --git a/mysql-async/src/test/resources/logback.xml b/mysql-async/src/test/resources/logback.xml
new file mode 100644
index 00000000..e0084899
--- /dev/null
+++ b/mysql-async/src/test/resources/logback.xml
@@ -0,0 +1,20 @@
+
+
+
+
+ [%level][%thread][%d][%c{5}] %msg%ex%n
+
+
+
+
+ target/mysql-async-tests.log
+
+ [%level][%thread][%d][%c{5}] %msg%ex%n
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/BinaryColumnsSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/BinaryColumnsSpec.scala
new file mode 100644
index 00000000..6c7c1313
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/BinaryColumnsSpec.scala
@@ -0,0 +1,152 @@
+package com.github.mauricio.async.db.mysql
+
+import org.specs2.mutable.Specification
+import java.util.UUID
+import java.nio.ByteBuffer
+import io.netty.buffer.Unpooled
+import io.netty.util.CharsetUtil
+import com.github.mauricio.async.db.RowData
+
+class BinaryColumnsSpec extends Specification with ConnectionHelper {
+
+ "connection" should {
+
+ "correctly load fields as byte arrays" in {
+
+ val create = """CREATE TEMPORARY TABLE t (
+ | id BIGINT NOT NULL AUTO_INCREMENT,
+ | uuid BINARY(36) NOT NULL,
+ | address VARBINARY(16) NOT NULL,
+ | PRIMARY KEY (id),
+ | INDEX idx_t_uuid (uuid),
+ | INDEX idx_t_address (address)
+ |);""".stripMargin
+
+ val uuid = UUID.randomUUID().toString
+ val host = "127.0.0.1"
+
+ val preparedInsert = "INSERT INTO t (uuid, address) VALUES (?, ?)"
+ val insert = s"INSERT INTO t (uuid, address) VALUES ('${uuid}', '${host}')"
+ val select = "SELECT * FROM t"
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+ executeQuery(connection, insert)
+
+ val result = executeQuery(connection, select).rows.get
+
+ compareBytes(result(0), "uuid", uuid )
+ compareBytes(result(0), "address", host )
+
+ executePreparedStatement( connection, preparedInsert, uuid, host)
+
+ val otherResult = executePreparedStatement(connection, select).rows.get
+
+ compareBytes(otherResult(1), "uuid", uuid )
+ compareBytes(otherResult(1), "address", host )
+ }
+
+ }
+
+ "support BINARY type" in {
+
+ val create =
+ """CREATE TEMPORARY TABLE POSTS (
+ | id INT NOT NULL AUTO_INCREMENT,
+ | binary_column BINARY(20),
+ | primary key (id))
+ """.stripMargin
+
+ val insert = "INSERT INTO POSTS (binary_column) VALUES (?)"
+ val select = "SELECT * FROM POSTS"
+ val bytes = (1 to 10).map(_.toByte).toArray
+ val padding = Array.fill[Byte](10)(0)
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+ executePreparedStatement(connection, insert, bytes)
+ val row = executeQuery(connection, select).rows.get(0)
+ row("id") === 1
+ row("binary_column") === bytes ++ padding
+ }
+
+ }
+
+ "support VARBINARY type" in {
+
+ val create =
+ """CREATE TEMPORARY TABLE POSTS (
+ | id INT NOT NULL AUTO_INCREMENT,
+ | varbinary_column VARBINARY(20),
+ | primary key (id))
+ """.stripMargin
+
+ val insert = "INSERT INTO POSTS (varbinary_column) VALUES (?)"
+ val select = "SELECT * FROM POSTS"
+ val bytes = (1 to 10).map(_.toByte).toArray
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+ executePreparedStatement(connection, insert, bytes)
+ val row = executeQuery(connection, select).rows.get(0)
+ row("id") === 1
+ row("varbinary_column") === bytes
+ }
+
+ }
+
+ "support BLOB type" in {
+
+ val bytes = (1 to 10).map(_.toByte).toArray
+
+ testBlob(bytes)
+
+ }
+
+ "support BLOB type with large values" in {
+
+ val bytes = (1 to 2100).map(_.toByte).toArray
+
+ testBlob(bytes)
+
+ }
+
+ }
+
+ def testBlob(bytes: Array[Byte]) = {
+ val create =
+ """CREATE TEMPORARY TABLE POSTS (
+ | id INT NOT NULL,
+ | blob_column BLOB,
+ | primary key (id))
+ """.stripMargin
+
+ val insert = "INSERT INTO POSTS (id,blob_column) VALUES (?,?)"
+ val select = "SELECT id,blob_column FROM POSTS ORDER BY id"
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+ executePreparedStatement(connection, insert, 1, Some(bytes))
+ executePreparedStatement(connection, insert, 2, ByteBuffer.wrap(bytes))
+ executePreparedStatement(connection, insert, 3, Unpooled.wrappedBuffer(bytes))
+
+ val Some(rows) = executeQuery(connection, select).rows
+ rows(0)("id") === 1
+ rows(0)("blob_column") === bytes
+ rows(1)("id") === 2
+ rows(1)("blob_column") === bytes
+ rows(2)("id") === 3
+ rows(2)("blob_column") === bytes
+ rows.size === 3
+ }
+
+ }
+
+ def compareBytes( row : RowData, column : String, expected : String ) =
+ row(column) === expected.getBytes(CharsetUtil.UTF_8)
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/BitSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/BitSpec.scala
new file mode 100644
index 00000000..ade3e6ce
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/BitSpec.scala
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import org.specs2.mutable.Specification
+
+class BitSpec extends Specification with ConnectionHelper {
+
+ "when processing bit columns" should {
+
+ "result in binary data" in {
+
+ withConnection {
+ connection =>
+ val create = """CREATE TEMPORARY TABLE binary_test
+ (
+ id INT NOT NULL AUTO_INCREMENT,
+ some_bit BIT(1) NOT NULL,
+ PRIMARY KEY (id)
+ )"""
+
+ executeQuery(connection, create)
+ executePreparedStatement(connection,
+ "INSERT INTO binary_test (some_bit) VALUES (B'0'),(B'1')")
+
+ val rows = executePreparedStatement(connection, "select * from binary_test").rows.get
+
+ val bit0 = rows(0)("some_bit")
+ val bit1 = rows(1)("some_bit")
+
+ bit0 === Array(0)
+ bit1 === Array(1)
+ }
+
+ }
+
+ "result in binary data in BIT(2) column" in {
+
+ withConnection {
+ connection =>
+ val create = """CREATE TEMPORARY TABLE binary_test
+ (
+ id INT NOT NULL AUTO_INCREMENT,
+ some_bit BIT(2) NOT NULL,
+ CONSTRAINT bigserial_column_pkey PRIMARY KEY (id)
+ )"""
+
+ executeQuery(connection, create)
+ executePreparedStatement(connection,
+ "INSERT INTO binary_test (some_bit) VALUES (B'00'),(B'01'),(B'10'),(B'11')")
+
+ val rows = executePreparedStatement(connection, "select * from binary_test").rows.get
+
+ val bit0 = rows(0)("some_bit")
+ val bit1 = rows(1)("some_bit")
+ val bit2 = rows(2)("some_bit")
+ val bit3 = rows(3)("some_bit")
+
+ bit0 === Array(0)
+ bit1 === Array(1)
+ bit2 === Array(2)
+ bit3 === Array(3)
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ClientPluginAuthDisabledSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ClientPluginAuthDisabledSpec.scala
new file mode 100644
index 00000000..c4c8b6c7
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ClientPluginAuthDisabledSpec.scala
@@ -0,0 +1,61 @@
+package com.github.mauricio.async.db.mysql
+
+import com.github.mauricio.async.db.Configuration
+import org.specs2.mutable.Specification
+
+/**
+ *
+ * To run this spec you have to use the Vagrant file provided with the base project
+ * and you have to start MySQL there. The expected MySQL version is 5.1.73.
+ * Make sure the bootstrap.sh script is run, if it isn't, manually run it yourself.
+ *
+ */
+
+class ClientPluginAuthDisabledSpec extends Specification with ConnectionHelper {
+
+ "connection" should {
+
+ "connect and query the database without a password" in {
+
+ if (System.getenv("TRAVIS") == null) {
+ withConnection {
+ connection =>
+ executeQuery(connection, "select version()")
+ success("did work")
+ }
+ } else {
+ skipped("not to be run on travis")
+ }
+
+ }
+
+ "connect and query the database with a password" in {
+
+ if (System.getenv("TRAVIS") == null) {
+ withConfigurableConnection(vagrantConfiguration) {
+ connection =>
+ executeQuery(connection, "select version()")
+ success("did work")
+ }
+ } else {
+ skipped("not to be run on travis")
+ }
+
+ }
+
+ }
+
+ override def defaultConfiguration = new Configuration(
+ "root",
+ "localhost",
+ port = 3307
+ )
+
+ def vagrantConfiguration = new Configuration(
+ "mysql_vagrant",
+ "localhost",
+ port = 3307,
+ password = Some("generic_password")
+ )
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ConcurrentlyRunTest.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ConcurrentlyRunTest.scala
new file mode 100644
index 00000000..d67fb189
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ConcurrentlyRunTest.scala
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import com.github.mauricio.async.db.util.Log
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * Mainly a way to try to figure out why sometimes MySQL will fail with a bad prepared statement response message.
+ */
+
+object ConcurrentlyRunTest extends ConnectionHelper with Runnable {
+
+ private val log = Log.getByName(this.getClass.getName)
+ private val counter = new AtomicInteger()
+ private val failures = new AtomicInteger()
+
+ def run() {
+ 1.until(50).foreach(x => execute(counter.incrementAndGet()))
+ }
+
+ def main(args : Array[String]) {
+
+ log.info("Starting executing code")
+
+ val threads = 1.until(10).map(x => new Thread(this))
+
+ threads.foreach {t => t.start()}
+
+ while ( !threads.forall(t => t.isAlive) ) {
+ Thread.sleep(5000)
+ }
+
+ log.info(s"Finished executing code, failed execution ${failures.get()} times")
+
+ }
+
+
+ def execute(count : Int) {
+ try {
+ log.info(s"====> run $count")
+ val create = """CREATE TEMPORARY TABLE posts (
+ | id INT NOT NULL AUTO_INCREMENT,
+ | some_text TEXT not null,
+ | some_date DATE,
+ | primary key (id) )""".stripMargin
+
+ val insert = "insert into posts (some_text) values (?)"
+ val select = "select * from posts limit 100"
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+
+ executePreparedStatement(connection, insert, "this is some text here")
+
+ val row = executeQuery(connection, select).rows.get(0)
+ assert(row("id") == 1)
+ assert(row("some_text") == "this is some text here")
+ assert(row("some_date") == null)
+
+ val queryRow = executePreparedStatement(connection, select).rows.get(0)
+
+ assert(queryRow("id") == 1)
+ assert(queryRow("some_text") == "this is some text here")
+ assert(queryRow("some_date") == null)
+
+ }
+ } catch {
+ case e : Exception => {
+ failures.incrementAndGet()
+ log.error( s"Failed to execute on run $count - ${e.getMessage}", e)
+ }
+ }
+
+ }
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ConnectionHelper.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ConnectionHelper.scala
new file mode 100644
index 00000000..8ace95e7
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ConnectionHelper.scala
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import com.github.mauricio.async.db.util.FutureUtils.awaitFuture
+import com.github.mauricio.async.db._
+import com.github.mauricio.async.db.pool.{PoolConfiguration, ConnectionPool}
+import com.github.mauricio.async.db.mysql.pool.MySQLConnectionFactory
+import scala.Some
+
+trait ConnectionHelper {
+
+ val createTableNumericColumns =
+ """
+ |create temporary table numbers (
+ |id int auto_increment not null,
+ |number_tinyint tinyint not null,
+ |number_smallint smallint not null,
+ |number_mediumint mediumint not null,
+ |number_int int not null,
+ |number_bigint bigint not null,
+ |number_decimal decimal(9,6),
+ |number_float float,
+ |number_double double,
+ |primary key (id)
+ |)
+ """.stripMargin
+
+ val insertTableNumericColumns =
+ """
+ |insert into numbers (
+ |number_tinyint,
+ |number_smallint,
+ |number_mediumint,
+ |number_int,
+ |number_bigint,
+ |number_decimal,
+ |number_float,
+ |number_double
+ |) values
+ |(-100, 32766, 8388607, 2147483647, 9223372036854775807, 450.764491, 14.7, 87650.9876)
+ """.stripMargin
+
+ val preparedInsertTableNumericColumns =
+ """
+ |insert into numbers (
+ |number_tinyint,
+ |number_smallint,
+ |number_mediumint,
+ |number_int,
+ |number_bigint,
+ |number_decimal,
+ |number_float,
+ |number_double
+ |) values
+ |(?, ?, ?, ?, ?, ?, ?, ?)
+ """.stripMargin
+
+ val createTableTimeColumns =
+ """CREATE TEMPORARY TABLE posts (
+ id INT NOT NULL AUTO_INCREMENT,
+ created_at_date DATE not null,
+ created_at_datetime DATETIME not null,
+ created_at_timestamp TIMESTAMP not null,
+ created_at_time TIME not null,
+ created_at_year YEAR not null,
+ primary key (id)
+ )"""
+
+ val insertTableTimeColumns =
+ """
+ |insert into posts (created_at_date, created_at_datetime, created_at_timestamp, created_at_time, created_at_year)
+ |values ( '2038-01-19', '2013-01-19 03:14:07', '2020-01-19 03:14:07', '03:14:07', '1999' )
+ """.stripMargin
+
+ final val createTable = """CREATE TEMPORARY TABLE users (
+ id INT NOT NULL AUTO_INCREMENT ,
+ name VARCHAR(255) CHARACTER SET 'utf8' NOT NULL ,
+ PRIMARY KEY (id) );"""
+ final val insert = """INSERT INTO users (name) VALUES ('Maurício Aragão')"""
+ final val select = """SELECT * FROM users"""
+
+ def defaultConfiguration = new Configuration(
+ "mysql_async",
+ "localhost",
+ port = 3306,
+ password = Some("root"),
+ database = Some("mysql_async_tests")
+ )
+
+ def withPool[T]( fn : (ConnectionPool[MySQLConnection]) => T ) : T = {
+
+ val factory = new MySQLConnectionFactory(this.defaultConfiguration)
+ val pool = new ConnectionPool[MySQLConnection](factory, PoolConfiguration.Default)
+
+ try {
+ fn(pool)
+ } finally {
+ awaitFuture( pool.close )
+ }
+
+ }
+
+ def withConfigurablePool[T]( configuration : Configuration )( fn : (ConnectionPool[MySQLConnection]) => T ) : T = {
+
+ val factory = new MySQLConnectionFactory(configuration)
+ val pool = new ConnectionPool[MySQLConnection](factory, PoolConfiguration.Default)
+
+ try {
+ fn(pool)
+ } finally {
+ awaitFuture( pool.close )
+ }
+
+ }
+
+ def withConnection[T]( fn : (MySQLConnection) => T ) : T =
+ withConfigurableConnection(this.defaultConfiguration)(fn)
+
+ def withConfigurableConnection[T]( configuration : Configuration )(fn : (MySQLConnection) => T) : T = {
+ val connection = new MySQLConnection(configuration)
+
+ try {
+ awaitFuture( connection.connect )
+ fn(connection)
+ } finally {
+ awaitFuture( connection.close )
+ }
+
+ }
+
+ def executeQuery( connection : Connection, query : String ) : QueryResult = {
+ awaitFuture( connection.sendQuery(query) )
+ }
+
+ def executePreparedStatement( connection : Connection, query : String, values : Any * ) : QueryResult = {
+ awaitFuture( connection.sendPreparedStatement(query, values) )
+ }
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ExecuteManyQueriesSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ExecuteManyQueriesSpec.scala
new file mode 100644
index 00000000..1086af78
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ExecuteManyQueriesSpec.scala
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import org.specs2.mutable.Specification
+
+class ExecuteManyQueriesSpec extends Specification with ConnectionHelper {
+
+ "connection" should {
+
+ "execute many queries one after the other" in {
+
+ withConnection {
+ connection =>
+ 1.until(500).foreach {
+ index =>
+ val rows = executeQuery(connection, "SELECT 6578, 'this is some text'").rows.get
+
+ rows.size === 1
+
+ val row = rows(0)
+
+ row(0) === 6578
+ row(1) === "this is some text"
+ }
+
+ success
+ }
+
+ }
+
+ "execute many prepared statements one after the other" in {
+ withConnection {
+ connection =>
+ 1.until(500).foreach {
+ index =>
+ val rows = executePreparedStatement(connection, "SELECT 6578, 'this is some text'").rows.get
+
+ rows.size === 1
+
+ val row = rows(0)
+
+ row(0) === 6578
+ row(1) === "this is some text"
+ }
+
+ success
+ }
+ }
+
+ }
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/MySQLConnectionSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/MySQLConnectionSpec.scala
new file mode 100644
index 00000000..5e5500fa
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/MySQLConnectionSpec.scala
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import com.github.mauricio.async.db.Configuration
+import com.github.mauricio.async.db.util.FutureUtils.awaitFuture
+import org.specs2.mutable.Specification
+
+class MySQLConnectionSpec extends Specification {
+
+ val configuration = new Configuration(
+ "mysql_async",
+ "localhost",
+ port = 3306,
+ password = Some("root"),
+ database = Some("mysql_async_tests")
+ )
+
+ val configurationWithoutPassword = new Configuration(
+ "mysql_async_nopw",
+ "localhost",
+ port = 3306,
+ password = None,
+ database = Some("mysql_async_tests")
+ )
+
+ val configurationWithoutDatabase = new Configuration(
+ "mysql_async_nopw",
+ "localhost",
+ port = 3306,
+ password = None,
+ database = Some("mysql_async_tests")
+ )
+
+ val configurationWithPasswordWithoutDatabase = new Configuration(
+ "mysql_async",
+ "localhost",
+ port = 3306,
+ password = Some("root"),
+ database = None
+ )
+
+ "connection" should {
+
+ "connect to a MySQL instance with a password" in {
+
+ withNonConnectedConnection {
+ connection =>
+ awaitFuture(connection.connect) === connection
+ }(configuration)
+
+ }
+
+ "connect to a MySQL instance without password" in {
+ withNonConnectedConnection({
+ connection =>
+ awaitFuture(connection.connect) === connection
+ }) (configurationWithoutPassword)
+ }
+
+ "connect to a MySQL instance without a database" in {
+ withNonConnectedConnection({
+ connection =>
+ awaitFuture(connection.connect) === connection
+ }) (configurationWithoutDatabase)
+ }
+
+ "connect to a MySQL instance without database with password" in {
+ withNonConnectedConnection({
+ connection =>
+ awaitFuture(connection.connect) === connection
+ }) (configurationWithPasswordWithoutDatabase)
+ }
+
+ }
+
+ def withNonConnectedConnection[T](fn: (MySQLConnection) => T)(cfg: Configuration = configuration): T = {
+
+ val connection = new MySQLConnection(cfg)
+ try {
+ fn(connection)
+ } finally {
+ awaitFuture(connection.close)
+ }
+
+
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/PreparedStatementsSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/PreparedStatementsSpec.scala
new file mode 100644
index 00000000..2b6bfe8a
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/PreparedStatementsSpec.scala
@@ -0,0 +1,397 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import java.sql.Timestamp
+import java.util.concurrent.TimeUnit
+import org.joda.time._
+import org.specs2.mutable.Specification
+import scala.concurrent.duration.Duration
+import scala.Some
+
+class PreparedStatementsSpec extends Specification with ConnectionHelper {
+
+ "connection" should {
+
+ "be able to execute prepared statements" in {
+
+ withConnection {
+ connection =>
+ val result = executePreparedStatement(connection, "select 1 as id , 'joe' as name").rows.get
+
+ result(0)("name") === "joe"
+ result(0)("id") === 1
+ result.length === 1
+
+ val otherResult = executePreparedStatement(connection, "select 1 as id , 'joe' as name").rows.get
+
+ otherResult(0)("name") === "joe"
+ otherResult(0)("id") === 1
+ otherResult.length === 1
+ }
+
+ }
+
+ "be able to detect a null value in a prepared statement" in {
+
+ withConnection {
+ connection =>
+ val result = executePreparedStatement(connection, "select 1 as id , 'joe' as name, NULL as null_value").rows.get
+
+ result(0)("name") === "joe"
+ result(0)("id") === 1
+ result(0)("null_value") must beNull
+ result.length === 1
+
+ }
+
+ }
+
+ "be able to select numbers and process them" in {
+
+ withConnection {
+ connection =>
+ executeQuery(connection, createTableNumericColumns)
+ executeQuery(connection, insertTableNumericColumns)
+ val result = executePreparedStatement(connection, "SELECT * FROM numbers").rows.get(0)
+
+ result("number_tinyint").asInstanceOf[Byte] === -100
+ result("number_smallint").asInstanceOf[Short] === 32766
+ result("number_mediumint").asInstanceOf[Int] === 8388607
+ result("number_int").asInstanceOf[Int] === 2147483647
+ result("number_bigint").asInstanceOf[Long] === 9223372036854775807L
+ result("number_decimal") === BigDecimal(450.764491)
+ result("number_float") === 14.7F
+ result("number_double") === 87650.9876
+ }
+
+ }
+
+ "be able to select from a table with timestamps" in {
+
+ withConnection {
+ connection =>
+ executeQuery(connection, createTableTimeColumns)
+ executeQuery(connection, insertTableTimeColumns)
+ val result = executePreparedStatement(connection, "SELECT * FROM posts").rows.get(0)
+
+ val date = result("created_at_date").asInstanceOf[LocalDate]
+
+ date.getYear === 2038
+ date.getMonthOfYear === 1
+ date.getDayOfMonth === 19
+
+ val dateTime = result("created_at_datetime").asInstanceOf[LocalDateTime]
+ dateTime.getYear === 2013
+ dateTime.getMonthOfYear === 1
+ dateTime.getDayOfMonth === 19
+ dateTime.getHourOfDay === 3
+ dateTime.getMinuteOfHour === 14
+ dateTime.getSecondOfMinute === 7
+
+ val timestamp = result("created_at_timestamp").asInstanceOf[LocalDateTime]
+ timestamp.getYear === 2020
+ timestamp.getMonthOfYear === 1
+ timestamp.getDayOfMonth === 19
+ timestamp.getHourOfDay === 3
+ timestamp.getMinuteOfHour === 14
+ timestamp.getSecondOfMinute === 7
+
+ result("created_at_time") === Duration(3, TimeUnit.HOURS) + Duration(14, TimeUnit.MINUTES) + Duration(7, TimeUnit.SECONDS)
+
+ val year = result("created_at_year").asInstanceOf[Short]
+
+ year === 1999
+ }
+
+ }
+
+ "it should be able to bind statement values to the prepared statement" in {
+
+ withConnection {
+ connection =>
+ val insert =
+ """
+ |insert into numbers (
+ |number_tinyint,
+ |number_smallint,
+ |number_mediumint,
+ |number_int,
+ |number_bigint,
+ |number_decimal,
+ |number_float,
+ |number_double
+ |) values
+ |(
+ |?,
+ |?,
+ |?,
+ |?,
+ |?,
+ |?,
+ |?,
+ |?)
+ """.stripMargin
+
+
+ val byte: Byte = 10
+ val short: Short = 679
+ val mediumInt = 778
+ val int = 875468
+ val bigInt = BigInt(100007654)
+ val bigDecimal = BigDecimal("198.657809")
+ val double = 98.765
+ val float = 432.8F
+
+ executeQuery(connection, this.createTableNumericColumns)
+ executePreparedStatement(connection,
+ insert,
+ byte,
+ short,
+ mediumInt,
+ int,
+ bigInt,
+ bigDecimal,
+ float,
+ double)
+
+ val row = executePreparedStatement(connection, "SELECT * FROM numbers").rows.get(0)
+
+ row("number_tinyint") === byte
+ row("number_smallint") === short
+ row("number_mediumint") === mediumInt
+ row("number_int") === int
+ row("number_bigint") === bigInt
+ row("number_decimal") === bigDecimal
+ row("number_float") === float
+ row("number_double") === double
+
+ }
+
+ }
+
+ "bind parameters on a prepared statement" in {
+
+ val create = """CREATE TEMPORARY TABLE posts (
+ | id INT NOT NULL AUTO_INCREMENT,
+ | some_text TEXT not null,
+ | primary key (id) )""".stripMargin
+
+ val insert = "insert into posts (some_text) values (?)"
+ val select = "select * from posts"
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+ executePreparedStatement(connection, insert, "this is some text here")
+ val row = executePreparedStatement(connection, select).rows.get(0)
+
+ row("id") === 1
+ row("some_text") === "this is some text here"
+
+ val queryRow = executeQuery(connection, select).rows.get(0)
+
+ queryRow("id") === 1
+ queryRow("some_text") === "this is some text here"
+
+
+ }
+ }
+
+ "bind timestamp parameters to a table" in {
+
+ val insert =
+ """
+ |insert into posts (created_at_date, created_at_datetime, created_at_timestamp, created_at_time, created_at_year)
+ |values ( ?, ?, ?, ?, ? )
+ """.stripMargin
+
+ val date = new LocalDate(2011, 9, 8)
+ val dateTime = new LocalDateTime(2012, 5, 27, 15, 29, 55)
+ val timestamp = new Timestamp(dateTime.toDateTime.getMillis)
+ val time = Duration( 3, TimeUnit.HOURS ) + Duration( 5, TimeUnit.MINUTES ) + Duration(10, TimeUnit.SECONDS)
+ val year = 2012
+
+ withConnection {
+ connection =>
+ executeQuery(connection, this.createTableTimeColumns)
+ executePreparedStatement(connection, insert, date, dateTime, timestamp, time, year)
+ val rows = executePreparedStatement(connection, "select * from posts where created_at_year > ?", 2011).rows.get
+
+ rows.length === 1
+ val row = rows(0)
+
+ row("created_at_date") === date
+ row("created_at_timestamp") === new LocalDateTime( timestamp.getTime )
+ row("created_at_time") === time
+ row("created_at_year") === year
+ row("created_at_datetime") === dateTime
+
+ }
+ }
+
+ "read a timestamp with microseconds" in {
+
+ val create =
+ """CREATE TEMPORARY TABLE posts (
+ id INT NOT NULL AUTO_INCREMENT,
+ created_at_timestamp TIMESTAMP(3) not null,
+ created_at_time TIME(3) not null,
+ primary key (id)
+ )"""
+
+ val insert =
+ """INSERT INTO posts ( created_at_timestamp, created_at_time )
+ | VALUES ( '2013-01-19 03:14:07.019', '03:14:07.019' )""".stripMargin
+
+ val time = Duration(3, TimeUnit.HOURS ) +
+ Duration(14, TimeUnit.MINUTES) +
+ Duration(7, TimeUnit.SECONDS) +
+ Duration(19, TimeUnit.MILLISECONDS)
+
+ val timestamp = new LocalDateTime(2013, 1, 19, 3, 14, 7, 19)
+ val select = "SELECT * FROM posts"
+
+ withConnection {
+ connection =>
+
+ if ( connection.version < MySQLConnection.MicrosecondsVersion ) {
+ true === true // no op
+ } else {
+ executeQuery(connection, create)
+ executeQuery(connection, insert)
+ val rows = executePreparedStatement( connection, select).rows.get
+
+ val row = rows(0)
+
+ row("created_at_time") === time
+ row("created_at_timestamp") === timestamp
+
+ val otherRow = executeQuery( connection, select ).rows.get(0)
+
+ otherRow("created_at_time") === time
+ otherRow("created_at_timestamp") === timestamp
+ }
+
+ }
+
+ }
+
+ "support prepared statement with a big string" in {
+
+ val bigString = {
+ val builder = new StringBuilder()
+ for (i <- 0 until 400)
+ builder.append( "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789")
+ builder.toString()
+ }
+
+ withConnection {
+ connection =>
+ executeQuery(connection, "CREATE TEMPORARY TABLE BIGSTRING( id INT NOT NULL AUTO_INCREMENT, STRING LONGTEXT, primary key (id))")
+ executePreparedStatement(connection, "INSERT INTO BIGSTRING (STRING) VALUES (?)", bigString)
+ val row = executePreparedStatement(connection, "SELECT STRING, id FROM BIGSTRING").rows.get(0)
+ row("id") === 1
+ val result = row("STRING").asInstanceOf[String]
+ result === bigString
+ }
+ }
+
+ "support setting null to a column" in {
+ withConnection {
+ connection =>
+ executeQuery(connection, "CREATE TEMPORARY TABLE timestamps ( id INT NOT NULL, moment TIMESTAMP NULL, primary key (id))")
+ executePreparedStatement(connection, "INSERT INTO timestamps (moment, id) VALUES (?, ?)", null, 10)
+ val row = executePreparedStatement(connection, "SELECT moment, id FROM timestamps").rows.get(0)
+ row("id") === 10
+ row("moment") === null
+ }
+ }
+
+ "support setting None to a column" in {
+ withConnection {
+ connection =>
+ executeQuery(connection, "CREATE TEMPORARY TABLE timestamps ( id INT NOT NULL, moment TIMESTAMP NULL, primary key (id))")
+ executePreparedStatement(connection, "INSERT INTO timestamps (moment, id) VALUES (?, ?)", None, 10)
+ val row = executePreparedStatement(connection, "SELECT moment, id FROM timestamps").rows.get(0)
+ row("id") === 10
+ row("moment") === null
+ }
+ }
+
+ "support setting Some(value) to a column" in {
+ withConnection {
+ connection =>
+ executeQuery(connection, "CREATE TEMPORARY TABLE timestamps ( id INT NOT NULL, moment TIMESTAMP NULL, primary key (id))")
+ val moment = LocalDateTime.now().withMillisOfDay(0) // cut off millis to match timestamp
+ executePreparedStatement(connection, "INSERT INTO timestamps (moment, id) VALUES (?, ?)", Some(moment), 10)
+ val row = executePreparedStatement(connection, "SELECT moment, id FROM timestamps").rows.get(0)
+ row("id") === 10
+ row("moment") === moment
+ }
+ }
+
+ "bind parameters on a prepared statement with limit" in {
+
+ val create = """CREATE TEMPORARY TABLE posts (
+ | id INT NOT NULL AUTO_INCREMENT,
+ | some_text TEXT not null,
+ | some_date DATE,
+ | primary key (id) )""".stripMargin
+
+ val insert = "insert into posts (some_text) values (?)"
+ val select = "select * from posts limit 100"
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+
+ executePreparedStatement(connection, insert, "this is some text here")
+
+ val row = executeQuery(connection, select).rows.get(0)
+
+ row("id") === 1
+ row("some_text") === "this is some text here"
+ row("some_date") must beNull
+
+ val queryRow = executePreparedStatement(connection, select).rows.get(0)
+
+ queryRow("id") === 1
+ queryRow("some_text") === "this is some text here"
+ queryRow("some_date") must beNull
+
+ }
+ }
+
+ "insert with prepared statements and without columns" in {
+ withConnection {
+ connection =>
+ executeQuery(connection, this.createTable)
+
+ executePreparedStatement(connection, this.insert)
+
+ val result = executePreparedStatement(connection, this.select).rows.get
+ result.size === 1
+
+ result(0)("name") === "Maurício Aragão"
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/QuerySpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/QuerySpec.scala
new file mode 100644
index 00000000..4e249387
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/QuerySpec.scala
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import com.github.mauricio.async.db.mysql.exceptions.MySQLException
+import org.joda.time._
+import org.specs2.mutable.Specification
+import scala.concurrent.duration.Duration
+import java.util.concurrent.TimeUnit
+import io.netty.util.CharsetUtil
+import com.github.mauricio.async.db.exceptions.InsufficientParametersException
+import org.specs2.matcher.MatchResult
+import com.github.mauricio.async.db.{QueryResult, ResultSet}
+
+class QuerySpec extends Specification with ConnectionHelper {
+
+ "connection" should {
+
+ "be able to run a DML query" in {
+
+ withConnection {
+ connection =>
+ executeQuery(connection, this.createTable).rowsAffected === 0
+ }
+
+ }
+
+ "raise an exception upon a bad statement" in {
+ withConnection {
+ connection =>
+ executeQuery(connection, "this is not SQL") must throwA[MySQLException].like {
+ case e => e.asInstanceOf[MySQLException].errorMessage.sqlState === "#42000"
+ }
+ }
+ }
+
+ "be able to select from a table" in {
+
+ withConnection {
+ connection =>
+ executeQuery(connection, this.createTable).rowsAffected === 0
+ executeQuery(connection, this.insert).rowsAffected === 1
+ val result = executeQuery(connection, this.select).rows.get
+
+ result(0)("id") === 1
+ result(0)("name") === "Maurício Aragão"
+ }
+
+ }
+
+ "be able to select from a table with timestamps" in {
+
+ withConnection {
+ connection =>
+ executeQuery(connection, createTableTimeColumns)
+ executeQuery(connection, insertTableTimeColumns)
+ val result = executeQuery(connection, "SELECT * FROM posts").rows.get(0)
+
+ val date = result("created_at_date").asInstanceOf[LocalDate]
+
+ date.getYear === 2038
+ date.getMonthOfYear === 1
+ date.getDayOfMonth === 19
+
+ val dateTime = result("created_at_datetime").asInstanceOf[LocalDateTime]
+ dateTime.getYear === 2013
+ dateTime.getMonthOfYear === 1
+ dateTime.getDayOfMonth === 19
+ dateTime.getHourOfDay === 3
+ dateTime.getMinuteOfHour === 14
+ dateTime.getSecondOfMinute === 7
+
+ val timestamp = result("created_at_timestamp").asInstanceOf[LocalDateTime]
+ timestamp.getYear === 2020
+ timestamp.getMonthOfYear === 1
+ timestamp.getDayOfMonth === 19
+ timestamp.getHourOfDay === 3
+ timestamp.getMinuteOfHour === 14
+ timestamp.getSecondOfMinute === 7
+
+
+ result("created_at_time") === Duration(3, TimeUnit.HOURS) + Duration(14, TimeUnit.MINUTES) + Duration(7, TimeUnit.SECONDS)
+
+ val year = result("created_at_year").asInstanceOf[Short]
+
+ year === 1999
+ }
+
+ }
+
+ "be able to select from a table with the various numeric types" in {
+
+ withConnection {
+ connection =>
+ executeQuery(connection, createTableNumericColumns)
+ executeQuery(connection, insertTableNumericColumns)
+ val result = executeQuery(connection, "SELECT * FROM numbers").rows.get(0)
+
+ result("number_tinyint").asInstanceOf[Byte] === -100
+ result("number_smallint").asInstanceOf[Short] === 32766
+ result("number_mediumint").asInstanceOf[Int] === 8388607
+ result("number_int").asInstanceOf[Int] === 2147483647
+ result("number_bigint").asInstanceOf[Long] === 9223372036854775807L
+ result("number_decimal") === BigDecimal(450.764491)
+ result("number_float") === 14.7F
+ result("number_double") === 87650.9876
+ }
+
+ }
+
+ "be able to read from a BLOB column when in text protocol" in {
+ val create = """CREATE TEMPORARY TABLE posts (
+ | id INT NOT NULL AUTO_INCREMENT,
+ | some_bytes BLOB not null,
+ | primary key (id) )""".stripMargin
+
+ val insert = "insert into posts (some_bytes) values (?)"
+ val select = "select * from posts"
+ val bytes = "this is some text here".getBytes(CharsetUtil.UTF_8)
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+ executePreparedStatement(connection, insert, bytes)
+ val row = executeQuery(connection, select).rows.get(0)
+ row("id") === 1
+ row("some_bytes") === bytes
+ }
+ }
+
+ "have column names on result set" in {
+
+ val create = """CREATE TEMPORARY TABLE posts (
+ | id INT NOT NULL AUTO_INCREMENT,
+ | some_bytes BLOB not null,
+ | primary key (id) )""".stripMargin
+
+ val createIdeas = """CREATE TEMPORARY TABLE ideas (
+ | id INT NOT NULL AUTO_INCREMENT,
+ | some_idea VARCHAR(255) NOT NULL,
+ | primary key (id) )""".stripMargin
+
+ val select = "SELECT * FROM posts"
+ val selectIdeas = "SELECT * FROM ideas"
+
+ val matcher: QueryResult => List[MatchResult[IndexedSeq[String]]] = { result =>
+ val columns = result.rows.get.columnNames
+ List(columns must contain(allOf("id", "some_bytes")).inOrder, columns must have size (2))
+ }
+
+ val ideasMatcher: QueryResult => List[MatchResult[IndexedSeq[String]]] = { result =>
+ val columns = result.rows.get.columnNames
+ List(columns must contain(allOf("id", "some_idea")).inOrder, columns must have size (2))
+ }
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+ executeQuery(connection, createIdeas)
+
+ matcher(executePreparedStatement(connection, select))
+ ideasMatcher(executePreparedStatement(connection, selectIdeas))
+
+ matcher(executePreparedStatement(connection, select))
+ ideasMatcher(executePreparedStatement(connection, selectIdeas))
+
+ matcher(executeQuery(connection, select))
+ ideasMatcher(executeQuery(connection, selectIdeas))
+
+ success("completed")
+ }
+
+ }
+
+ "support BIT type" in {
+
+ val create =
+ """CREATE TEMPORARY TABLE POSTS (
+ | id INT NOT NULL AUTO_INCREMENT,
+ | bit_column BIT(20),
+ | primary key (id))
+ """.stripMargin
+
+ val insert = "INSERT INTO POSTS (bit_column) VALUES (b'10000000')"
+ val select = "SELECT * FROM POSTS"
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+ executeQuery(connection, insert)
+
+ val rows = executeQuery(connection, select).rows.get
+ rows(0)("bit_column") === Array(0, 0, -128)
+
+ val preparedRows = executePreparedStatement(connection, select).rows.get
+ preparedRows(0)("bit_column") === Array(0, 0, -128)
+ }
+
+ }
+
+ "fail if number of args required is different than the number of provided parameters" in {
+
+ withConnection {
+ connection =>
+ executePreparedStatement(
+ connection,
+ "select * from some_table where c = ? and b = ?",
+ "one", "two", "three") must throwAn[InsufficientParametersException]
+ }
+
+ }
+
+ "select from another empty table with many columns" in {
+ withConnection {
+ connection =>
+ val create = """create temporary table test_11 (
+ | id int primary key not null,
+ | c2 text not null, c3 text not null, c4 text not null,
+ | c5 text not null, c6 text not null, c7 text not null,
+ | c8 text not null, c9 text not null, c10 text not null,
+ | c11 text not null
+ |) ENGINE=InnoDB DEFAULT CHARSET=utf8;""".stripMargin
+
+ executeQuery(connection, create)
+
+ val result = executeQuery(connection, "select * from test_11")
+
+ result.rows.get.size === 0
+ }
+ }
+
+ "select from an empty table with many columns" in {
+
+ withConnection {
+ connection =>
+
+ val create = """create temporary table test_10 (
+ | id int primary key not null,
+ | c2 text not null, c3 text not null, c4 text not null,
+ | c5 text not null, c6 text not null, c7 text not null,
+ | c8 text not null, c9 text not null, c10 text not null
+ |) ENGINE=InnoDB DEFAULT CHARSET=utf8;""".stripMargin
+
+ executeQuery(connection, create)
+
+ val result = executeQuery(connection, "select * from test_10")
+
+ result.rows.get.size === 0
+ }
+
+ }
+
+ "select from a large text column" in {
+
+ val create = "create temporary table bombs (id char(4), bomb mediumtext character set ascii)"
+
+ val insert = """ insert bombs values
+ | ('bomb', repeat(' ',65536+16384+8192+4096+2048+1024+512+256+128)),
+ | ('good', repeat(' ',65536+16384+8192+4096+2048+1024+512+256+128-1))""".stripMargin
+
+
+ withConnection {
+ connection =>
+ executeQuery(connection, create)
+ executeQuery(connection, insert)
+ val result = executeQuery(connection, "select bomb from bombs").rows.get
+
+ result.size === 2
+
+ result(0)("bomb").asInstanceOf[String].length === 98176
+ result(1)("bomb").asInstanceOf[String].length === 98175
+ }
+
+ }
+
+
+ }
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/QueryTimeoutSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/QueryTimeoutSpec.scala
new file mode 100644
index 00000000..65827432
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/QueryTimeoutSpec.scala
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import java.util.concurrent.TimeoutException
+import com.github.mauricio.async.db.Configuration
+import org.specs2.execute.{AsResult, Success, ResultExecution}
+import org.specs2.mutable.Specification
+import scala.concurrent.Await
+import scala.concurrent.duration._
+
+class QueryTimeoutSpec extends Specification with ConnectionHelper {
+ implicit def unitAsResult: AsResult[Unit] = new AsResult[Unit] {
+ def asResult(r: =>Unit) =
+ ResultExecution.execute(r)(_ => Success())
+ }
+ "Simple query with 1 nanosec timeout" in {
+ withConfigurablePool(shortTimeoutConfiguration) {
+ pool => {
+ val connection = Await.result(pool.take, Duration(10,SECONDS))
+ connection.isTimeouted === false
+ connection.isConnected === true
+ val queryResultFuture = connection.sendQuery("select sleep(1)")
+ Await.result(queryResultFuture, Duration(10,SECONDS)) must throwA[TimeoutException]()
+ connection.isTimeouted === true
+ Await.ready(pool.giveBack(connection), Duration(10,SECONDS))
+ pool.availables.count(_ == connection) === 0 // connection removed from pool
+ // we do not know when the connection will be closed.
+ }
+ }
+ }
+
+ "Simple query with 5 sec timeout" in {
+ withConfigurablePool(longTimeoutConfiguration) {
+ pool => {
+ val connection = Await.result(pool.take, Duration(10,SECONDS))
+ connection.isTimeouted === false
+ connection.isConnected === true
+ val queryResultFuture = connection.sendQuery("select sleep(1)")
+ Await.result(queryResultFuture, Duration(10,SECONDS)).rows.get.size === 1
+ connection.isTimeouted === false
+ connection.isConnected === true
+ Await.ready(pool.giveBack(connection), Duration(10,SECONDS))
+ pool.availables.count(_ == connection) === 1 // connection returned to pool
+ }
+ }
+ }
+
+ def shortTimeoutConfiguration = new Configuration(
+ "mysql_async",
+ "localhost",
+ port = 3306,
+ password = Some("root"),
+ database = Some("mysql_async_tests"),
+ queryTimeout = Some(Duration(1,NANOSECONDS))
+ )
+
+ def longTimeoutConfiguration = new Configuration(
+ "mysql_async",
+ "localhost",
+ port = 3306,
+ password = Some("root"),
+ database = Some("mysql_async_tests"),
+ queryTimeout = Some(Duration(5,SECONDS))
+ )
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/StoredProceduresSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/StoredProceduresSpec.scala
new file mode 100644
index 00000000..d8ff2142
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/StoredProceduresSpec.scala
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql
+
+import com.github.mauricio.async.db.ResultSet
+import com.github.mauricio.async.db.util.FutureUtils._
+import org.specs2.mutable.Specification
+import scala.concurrent.ExecutionContext.Implicits.global
+
+class StoredProceduresSpec extends Specification with ConnectionHelper {
+
+ "connection" should {
+
+ "be able to execute create stored procedure" in {
+ withConnection {
+ connection =>
+ val future = for(
+ drop <- connection.sendQuery("DROP PROCEDURE IF exists helloWorld;");
+ create <- connection.sendQuery(
+ """
+ CREATE PROCEDURE helloWorld(OUT param1 VARCHAR(20))
+ BEGIN
+ SELECT 'hello' INTO param1;
+ END
+ """
+ )
+ ) yield create
+ awaitFuture(future).statusMessage === ""
+ }
+ }
+
+ "be able to call stored procedure" in {
+ withConnection {
+ connection =>
+ val future = for(
+ drop <- connection.sendQuery("DROP PROCEDURE IF exists constTest;");
+ create <- connection.sendQuery(
+ """
+ CREATE PROCEDURE constTest(OUT param INT)
+ BEGIN
+ SELECT 125 INTO param;
+ END
+ """
+ );
+ call <- connection.sendQuery("CALL constTest(@arg)");
+ arg <- connection.sendQuery("SELECT @arg")
+ ) yield arg
+ val result: Option[ResultSet] = awaitFuture(future).rows
+ result.isDefined === true
+ val rows = result.get
+ rows.size === 1
+ rows(0)(rows.columnNames.head) === 125
+ }
+ }
+
+ "be able to call stored procedure with input parameter" in {
+ withConnection {
+ connection =>
+ val future = for(
+ drop <- connection.sendQuery("DROP PROCEDURE IF exists addTest;");
+ create <- connection.sendQuery(
+ """
+ CREATE PROCEDURE addTest(IN a INT, IN b INT, OUT sum INT)
+ BEGIN
+ SELECT a+b INTO sum;
+ END
+ """
+ );
+ call <- connection.sendQuery("CALL addTest(132, 245, @sm)");
+ res <- connection.sendQuery("SELECT @sm")
+ ) yield res
+ val result: Option[ResultSet] = awaitFuture(future).rows
+ result.isDefined === true
+ val rows = result.get
+ rows.size === 1
+ rows(0)(rows.columnNames.head) === 377
+ }
+ }
+
+ "be able to remove stored procedure" in {
+ withConnection {
+ connection =>
+ val createResult: Option[ResultSet] = awaitFuture(
+ for(
+ drop <- connection.sendQuery("DROP PROCEDURE IF exists remTest;");
+ create <- connection.sendQuery(
+ """
+ CREATE PROCEDURE remTest(OUT cnst INT)
+ BEGIN
+ SELECT 987 INTO cnst;
+ END
+ """
+ );
+ routine <- connection.sendQuery(
+ """
+ SELECT routine_name FROM INFORMATION_SCHEMA.ROUTINES WHERE routine_name="remTest"
+ """
+ )
+ ) yield routine
+ ).rows
+ createResult.isDefined === true
+ createResult.get.size === 1
+ createResult.get(0)("routine_name") === "remTest"
+ val removeResult: Option[ResultSet] = awaitFuture(
+ for(
+ drop <- connection.sendQuery("DROP PROCEDURE remTest;");
+ routine <- connection.sendQuery(
+ """
+ SELECT routine_name FROM INFORMATION_SCHEMA.ROUTINES WHERE routine_name="remTest"
+ """
+ )
+ ) yield routine
+ ).rows
+ removeResult.isDefined === true
+ removeResult.get.isEmpty === true
+ }
+ }
+ }
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala
new file mode 100644
index 00000000..83548c9b
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/TransactionSpec.scala
@@ -0,0 +1,149 @@
+package com.github.mauricio.async.db.mysql
+
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+
+import org.specs2.mutable.Specification
+import com.github.mauricio.async.db.util.FutureUtils.awaitFuture
+import com.github.mauricio.async.db.mysql.exceptions.MySQLException
+import com.github.mauricio.async.db.Connection
+
+import scala.concurrent.duration.Duration
+import scala.concurrent.{Await, Future}
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.util.{Success, Failure}
+
+object TransactionSpec {
+
+ val BrokenInsert = """INSERT INTO users (id, name) VALUES (1, 'Maurício Aragão')"""
+ val InsertUser = """INSERT INTO users (name) VALUES (?)"""
+ val TransactionInsert = "insert into transaction_test (id) values (?)"
+
+}
+
+class TransactionSpec extends Specification with ConnectionHelper {
+
+ import TransactionSpec._
+
+ "connection in transaction" should {
+
+ "correctly store the values of the transaction" in {
+ withConnection {
+ connection =>
+ executeQuery(connection, this.createTable)
+
+ val future = connection.inTransaction {
+ c =>
+ c.sendPreparedStatement(this.insert)
+ .flatMap( r => connection.sendPreparedStatement(this.insert))
+ }
+
+ awaitFuture(future)
+
+ val result = executePreparedStatement(connection, this.select).rows.get
+ result.size === 2
+
+ result(0)("name") === "Maurício Aragão"
+ result(1)("name") === "Maurício Aragão"
+ }
+ }
+
+ "correctly rollback changes if the transaction raises an exception" in {
+
+ withConnection {
+ connection =>
+ executeQuery(connection, this.createTable)
+ executeQuery(connection, this.insert)
+
+ val future = connection.inTransaction {
+ c =>
+ c.sendQuery(this.insert).flatMap(r => c.sendQuery(BrokenInsert))
+ }
+
+ try {
+ awaitFuture(future)
+ failure("should not have arrived here")
+ } catch {
+ case e : MySQLException => {
+
+ e.errorMessage.errorCode === 1062
+ e.errorMessage.errorMessage === "Duplicate entry '1' for key 'PRIMARY'"
+
+ val result = executePreparedStatement(connection, this.select).rows.get
+ result.size === 1
+ result(0)("name") === "Maurício Aragão"
+ success("correct result")
+ }
+ }
+ }
+
+ }
+
+ "should make a connection invalid and not return it to the pool if it raises an exception" in {
+
+ withPool {
+ pool =>
+
+ executeQuery(pool, this.createTable)
+ executeQuery(pool, this.insert)
+
+ var connection : Connection = null
+
+ val future = pool.inTransaction {
+ c =>
+ connection = c
+ c.sendQuery(BrokenInsert)
+ }
+
+ try {
+ awaitFuture(future)
+ failure("this should not be reached")
+ } catch {
+ case e : MySQLException => {
+
+ pool.availables must have size(0)
+ pool.availables must not contain(connection.asInstanceOf[MySQLConnection])
+
+ success("success")
+ }
+ }
+
+ }
+
+ }
+
+ "runs commands for a transaction in a single connection" in {
+
+ val id = UUID.randomUUID().toString
+
+ withPool {
+ pool =>
+ val operations = pool.inTransaction {
+ connection =>
+ connection.sendPreparedStatement(TransactionInsert, List(id)).flatMap {
+ result =>
+ connection.sendPreparedStatement(TransactionInsert, List(id)).map {
+ failure =>
+ List(result, failure)
+ }
+ }
+ }
+
+ Await.ready(operations, Duration(5, TimeUnit.SECONDS))
+
+ operations.value.get match {
+ case Success(e) => failure("should not have executed")
+ case Failure(e) => {
+ e.asInstanceOf[MySQLException].errorMessage.errorCode === 1062
+ executePreparedStatement(pool, "select * from transaction_test where id = ?", id).rows.get.size === 0
+ success("ok")
+ }
+ }
+
+ }
+
+ }
+
+ }
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ZeroDatesSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ZeroDatesSpec.scala
new file mode 100644
index 00000000..b5a06aec
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/ZeroDatesSpec.scala
@@ -0,0 +1,58 @@
+package com.github.mauricio.async.db.mysql
+
+import org.specs2.mutable.Specification
+import scala.concurrent.duration.Duration
+import com.github.mauricio.async.db.RowData
+
+class ZeroDatesSpec extends Specification with ConnectionHelper {
+
+ val createStatement =
+ """CREATE TEMPORARY TABLE dates (
+ |`name` varchar (255) NOT NULL,
+ |`timestamp_column` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
+ |`date_column` date NOT NULL DEFAULT '0000-00-00',
+ |`datetime_column` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+ |`time_column` time NOT NULL DEFAULT '00:00:00',
+ |`year_column` year NOT NULL DEFAULT '0000'
+ |)
+ |ENGINE=MyISAM DEFAULT CHARSET=utf8;""".stripMargin
+
+ val insertStatement = "INSERT INTO dates (name) values ('Joe')"
+ val selectStatement = "SELECT * FROM dates"
+
+ def matchValues( result : RowData ) = {
+ result("name") === "Joe"
+ result("timestamp_column") must beNull
+ result("datetime_column") must beNull
+ result("date_column") must beNull
+ result("year_column") === 0
+ result("time_column") === Duration.Zero
+ }
+
+ "client" should {
+
+ "correctly parse the MySQL zeroed dates as NULL values in text protocol" in {
+
+ withConnection {
+ connection =>
+ executeQuery(connection, createStatement)
+ executeQuery(connection, insertStatement)
+
+ matchValues(executeQuery(connection, selectStatement).rows.get(0))
+ }
+ }
+
+ "correctly parse the MySQL zeroed dates as NULL values in binary protocol" in {
+
+ withConnection {
+ connection =>
+ executeQuery(connection, createStatement)
+ executeQuery(connection, insertStatement)
+
+ matchValues(executePreparedStatement(connection, selectStatement).rows.get(0))
+ }
+ }
+
+ }
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowDecoderSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowDecoderSpec.scala
new file mode 100644
index 00000000..e463be69
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/binary/BinaryRowDecoderSpec.scala
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.binary
+
+import io.netty.buffer.Unpooled
+import io.netty.util.CharsetUtil
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import com.github.mauricio.async.db.mysql.message.server.ColumnDefinitionMessage
+import org.specs2.mutable.Specification
+import java.nio.ByteOrder
+import com.github.mauricio.async.db.mysql.codec.DecoderRegistry
+
+class BinaryRowDecoderSpec extends Specification {
+
+ val registry = new DecoderRegistry(CharsetUtil.UTF_8)
+ val decoder = new BinaryRowDecoder()
+
+ val idAndName = Array[Byte]( 0, 1, 0, 0, 0, 0, 0, 0, 0, 3, 106, 111, 101)
+ val idAndNameColumns = Array(
+ createColumn("id", ColumnTypes.FIELD_TYPE_LONGLONG),
+ createColumn("name", ColumnTypes.FIELD_TYPE_VAR_STRING) )
+
+ val idNameAndNull = Array[Byte]( 16, 1, 0, 0, 0, 0, 0, 0, 0, 3, 106, 111, 101)
+ val idNameAndNullColumns = idAndNameColumns ++ List( createColumn("null_value", ColumnTypes.FIELD_TYPE_NULL) )
+
+ "binary row decoder" should {
+
+ "decoder a long and a string from the byte array" in {
+
+ val buffer = Unpooled.wrappedBuffer(idAndName).order(ByteOrder.LITTLE_ENDIAN)
+ val result = decoder.decode(buffer, idAndNameColumns)
+ buffer.release()
+ result(0) === 1L
+ result(1) === "joe"
+
+ }
+
+ "decode a row with an long, a string and a null" in {
+ val buffer = Unpooled.wrappedBuffer(idNameAndNull).order(ByteOrder.LITTLE_ENDIAN)
+ val result = decoder.decode(buffer, idNameAndNullColumns)
+ buffer.release()
+ result(0) === 1L
+ result(1) === "joe"
+ result(2) must beNull
+ }
+
+ }
+
+ def createColumn( name : String, columnType : Int ) : ColumnDefinitionMessage = {
+
+ new ColumnDefinitionMessage(
+ "root",
+ "root",
+ "users",
+ "users",
+ name,
+ name,
+ -1,
+ 0,
+ columnType,
+ 0,
+ 0,
+ registry.binaryDecoderFor(columnType, 3),
+ registry.textDecoderFor(columnType, 3)
+ )
+
+ }
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/codec/MySQLFrameDecoderSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/codec/MySQLFrameDecoderSpec.scala
new file mode 100644
index 00000000..8d8790e5
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/codec/MySQLFrameDecoderSpec.scala
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.codec
+
+import io.netty.buffer.ByteBuf
+import io.netty.util.CharsetUtil
+import com.github.mauricio.async.db.mysql.message.server._
+import com.github.mauricio.async.db.util.ByteBufferUtils
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+import org.specs2.mutable.Specification
+import com.github.mauricio.async.db.mysql.message.server.OkMessage
+import com.github.mauricio.async.db.mysql.column.ColumnTypes
+import io.netty.channel.embedded.EmbeddedChannel
+
+class MySQLFrameDecoderSpec extends Specification {
+
+ final val charset = CharsetUtil.UTF_8
+
+ "decoder" should {
+
+ "decode an OK message correctly" in {
+
+ val buffer = createOkPacket()
+
+ val decoder = this.createPipeline()
+
+ decoder.writeInbound(buffer)
+
+ val ok = decoder.readInbound().asInstanceOf[OkMessage]
+ ok.affectedRows === 10
+ ok.lastInsertId === 15
+ ok.message === "this is a test"
+ ok.statusFlags === 5
+ ok.warnings === 6
+ }
+
+ "decode an error message" in {
+
+ val content = "this is the error message"
+
+ val buffer = createErrorPacket(content)
+
+ val decoder = createPipeline()
+
+ decoder.writeInbound(buffer)
+
+ val error = decoder.readInbound().asInstanceOf[ErrorMessage]
+
+ error.errorCode === 27
+ error.errorMessage === content
+ error.sqlState === "HZAWAY"
+
+ }
+
+ "on a query process it should correctly send an OK" in {
+
+ val decoder = new MySQLFrameDecoder(charset, "[mysql-connection]")
+ decoder.hasDoneHandshake = true
+ val embedder = new EmbeddedChannel(decoder)
+ embedder.config.setAllocator(LittleEndianByteBufAllocator.INSTANCE)
+
+ decoder.queryProcessStarted()
+
+ decoder.isInQuery must beTrue
+ decoder.processingColumns must beTrue
+
+ val buffer = createOkPacket()
+
+ embedder.writeInbound(buffer) must beTrue
+ embedder.readInbound().asInstanceOf[OkMessage].message === "this is a test"
+
+ decoder.isInQuery must beFalse
+ decoder.processingColumns must beFalse
+ }
+
+ "on query process it should correctly send an error" in {
+
+ val decoder = new MySQLFrameDecoder(charset, "[mysql-connection]")
+ decoder.hasDoneHandshake = true
+ val embedder = new EmbeddedChannel(decoder)
+ embedder.config.setAllocator(LittleEndianByteBufAllocator.INSTANCE)
+
+ decoder.queryProcessStarted()
+
+ decoder.isInQuery must beTrue
+ decoder.processingColumns must beTrue
+
+ val content = "this is a crazy error"
+
+ val buffer = createErrorPacket(content)
+
+ embedder.writeInbound(buffer) must beTrue
+ embedder.readInbound().asInstanceOf[ErrorMessage].errorMessage === content
+
+ decoder.isInQuery must beFalse
+ decoder.processingColumns must beFalse
+
+ }
+
+ "on query process it should correctly handle a result set" in {
+
+ val decoder = new MySQLFrameDecoder(charset, "[mysql-connection]")
+ decoder.hasDoneHandshake = true
+ val embedder = new EmbeddedChannel(decoder)
+ embedder.config.setAllocator(LittleEndianByteBufAllocator.INSTANCE)
+
+ decoder.queryProcessStarted()
+
+ decoder.totalColumns === 0
+
+ val columnCountBuffer = ByteBufferUtils.packetBuffer()
+ columnCountBuffer.writeLength(2)
+ columnCountBuffer.writePacketLength()
+
+ embedder.writeInbound(columnCountBuffer)
+
+ decoder.totalColumns === 2
+
+ val columnId = createColumnPacket("id", ColumnTypes.FIELD_TYPE_LONG)
+ val columnName = createColumnPacket("name", ColumnTypes.FIELD_TYPE_VARCHAR)
+
+ embedder.writeInbound(columnId)
+
+ embedder.readInbound().asInstanceOf[ColumnDefinitionMessage].name === "id"
+
+ decoder.processedColumns === 1
+
+ embedder.writeInbound(columnName)
+
+ embedder.readInbound().asInstanceOf[ColumnDefinitionMessage].name === "name"
+
+ decoder.processedColumns === 2
+
+ embedder.writeInbound(this.createEOFPacket())
+
+ embedder.readInbound().asInstanceOf[ColumnProcessingFinishedMessage].eofMessage.flags === 8765
+
+ decoder.processingColumns must beFalse
+
+ val row = ByteBufferUtils.packetBuffer()
+ row.writeLenghtEncodedString("1", charset)
+ row.writeLenghtEncodedString("some name", charset)
+ row.writePacketLength()
+
+ embedder.writeInbound(row)
+
+ embedder.readInbound().isInstanceOf[ResultSetRowMessage] must beTrue
+
+ embedder.writeInbound(this.createEOFPacket())
+
+ decoder.isInQuery must beFalse
+ }
+
+ }
+
+ def createPipeline(): EmbeddedChannel = {
+ val decoder = new MySQLFrameDecoder(charset, "[mysql-connection]")
+ decoder.hasDoneHandshake = true
+ val channel = new EmbeddedChannel(decoder)
+ channel.config.setAllocator(LittleEndianByteBufAllocator.INSTANCE)
+ channel
+ }
+
+ def createOkPacket() : ByteBuf = {
+ val buffer = ByteBufferUtils.packetBuffer()
+ buffer.writeByte(0)
+ buffer.writeLength(10)
+ buffer.writeLength(15)
+ buffer.writeShort(5)
+ buffer.writeShort(6)
+ buffer.writeBytes("this is a test".getBytes(charset))
+ buffer.writePacketLength()
+ buffer
+ }
+
+ def createErrorPacket(content : String) : ByteBuf = {
+ val buffer = ByteBufferUtils.packetBuffer()
+ buffer.writeByte(0xff)
+ buffer.writeShort(27)
+ buffer.writeByte('H')
+ buffer.writeBytes("ZAWAY".getBytes(charset))
+ buffer.writeBytes(content.getBytes(charset))
+ buffer.writePacketLength()
+ buffer
+ }
+
+ def createColumnPacket( name : String, columnType : Int ) : ByteBuf = {
+ val buffer = ByteBufferUtils.packetBuffer()
+ buffer.writeLenghtEncodedString("def", charset)
+ buffer.writeLenghtEncodedString("some_schema", charset)
+ buffer.writeLenghtEncodedString("some_table", charset)
+ buffer.writeLenghtEncodedString("some_table", charset)
+ buffer.writeLenghtEncodedString(name, charset)
+ buffer.writeLenghtEncodedString(name, charset)
+ buffer.writeLength(12)
+ buffer.writeShort(0x03)
+ buffer.writeInt(10)
+ buffer.writeByte(columnType)
+ buffer.writeShort(76)
+ buffer.writeByte(0)
+ buffer.writeShort(56)
+ buffer.writePacketLength()
+ buffer
+ }
+
+ def createEOFPacket() : ByteBuf = {
+ val buffer = ByteBufferUtils.packetBuffer()
+ buffer.writeByte(0xfe)
+ buffer.writeShort(879)
+ buffer.writeShort(8765)
+
+ buffer.writePacketLength()
+
+ buffer
+ }
+
+
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/column/TimeDecoderSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/column/TimeDecoderSpec.scala
new file mode 100644
index 00000000..2560b8ea
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/column/TimeDecoderSpec.scala
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.column
+
+import java.util.concurrent.TimeUnit
+import org.specs2.mutable.Specification
+import scala.concurrent.duration.Duration
+
+class TimeDecoderSpec extends Specification {
+
+ "decoder" in {
+
+ "handle a time" in {
+
+ val time = "120:10:07"
+ val duration = Duration( 120, TimeUnit.HOURS ) +
+ Duration( 10, TimeUnit.MINUTES ) +
+ Duration( 7, TimeUnit.SECONDS )
+
+ TimeDecoder.decode(time) === duration
+ }
+
+ "handle a time with millis" in {
+
+ val time = "120:10:07.00098"
+ val duration = Duration( 120, TimeUnit.HOURS ) +
+ Duration( 10, TimeUnit.MINUTES ) +
+ Duration( 7, TimeUnit.SECONDS ) +
+ Duration( 98, TimeUnit.MILLISECONDS )
+
+ TimeDecoder.decode(time) === duration
+
+ }
+
+ }
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/encoder/PreparedStatementExecuteEncoderSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/encoder/PreparedStatementExecuteEncoderSpec.scala
new file mode 100644
index 00000000..427dde17
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/encoder/PreparedStatementExecuteEncoderSpec.scala
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.encoder
+
+import com.github.mauricio.async.db.mysql.binary.BinaryRowEncoder
+import io.netty.util.CharsetUtil
+import org.specs2.mutable.Specification
+
+class PreparedStatementExecuteEncoderSpec extends Specification {
+
+ val encoder = new PreparedStatementExecuteEncoder(new BinaryRowEncoder(CharsetUtil.UTF_8))
+
+ "binary row encoder" should {
+
+ "encode Some(value) like value" in {
+ val actual = encoder.encodeValues(List(Some(1l), Some("foo")), Set(0, 1))
+ val expected = encoder.encodeValues(List(1l, "foo"), Set(0, 1))
+
+ actual mustEqual expected
+
+ }
+
+ "encode None as null" in {
+ val actual = encoder.encodeValues(List(None), Set(0))
+ val expected = encoder.encodeValues(List(null), Set(0))
+
+ actual mustEqual expected
+ }
+
+ }
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/pool/MySQLConnectionFactorySpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/pool/MySQLConnectionFactorySpec.scala
new file mode 100644
index 00000000..72a4e4e1
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/pool/MySQLConnectionFactorySpec.scala
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.pool
+
+import com.github.mauricio.async.db.mysql.{MySQLConnection, ConnectionHelper}
+import com.github.mauricio.async.db.util.FutureUtils.awaitFuture
+import org.specs2.mutable.Specification
+import scala.util._
+import com.github.mauricio.async.db.exceptions.ConnectionNotConnectedException
+import scala.util.Failure
+import org.specs2.matcher.MatchResult
+
+class MySQLConnectionFactorySpec extends Specification with ConnectionHelper {
+
+ val factory = new MySQLConnectionFactory(this.defaultConfiguration)
+
+ "factory" should {
+
+ "fail validation if a connection has errored" in {
+
+ val connection = factory.create
+
+ val result = Try {
+ executeQuery(connection, "this is not sql")
+ }
+
+ try {
+ if (factory.validate(connection).isSuccess) {
+ throw new IllegalStateException("should not have come here")
+ }
+ } finally {
+ awaitFuture(connection.close)
+ }
+
+ ok("connection successfully rejected")
+ }
+
+ "it should take a connection from the pool and the pool should not accept it back if it is broken" in {
+ withPool {
+ pool =>
+ val connection = awaitFuture(pool.take)
+
+ pool.inUse.size === 1
+
+ awaitFuture(connection.disconnect)
+
+ try {
+ awaitFuture(pool.giveBack(connection))
+ } catch {
+ case e: ConnectionNotConnectedException => {
+ // all good
+ }
+ }
+
+ pool.inUse.size === 0
+
+ }
+ }
+
+ "be able to provide connections to the pool" in {
+ withPool {
+ pool =>
+ executeQuery(pool, "SELECT 0").rows.get(0)(0) === 0
+ }
+ }
+
+ "fail validation if a connection is disconnected" in {
+ val connection = factory.create
+
+ awaitFuture(connection.disconnect)
+
+ factory.validate(connection).isFailure must beTrue
+ }
+
+ "fail validation if a connection is still waiting for a query" in {
+ val connection = factory.create
+ connection.sendQuery("SELECT SLEEP(10)")
+
+ Thread.sleep(1000)
+
+ factory.validate(connection) match {
+ case Failure(e) => ok("connection successfully rejected")
+ case Success(c) => failure("should not have come here")
+ }
+
+ awaitFuture(connection.close) === connection
+ }
+
+ "accept a good connection" in {
+ val connection = factory.create
+
+ factory.validate(connection) match {
+ case Success(c) => ok("connection successfully accepted")
+ case Failure(e) => failure("should not have come here")
+ }
+
+ awaitFuture(connection.close) === connection
+ }
+
+ "test a valid connection and say it is ok" in {
+
+ val connection = factory.create
+
+ factory.test(connection) match {
+ case Success(c) => ok("connection successfully accepted")
+ case Failure(e) => failure("should not have come here")
+ }
+
+ awaitFuture(connection.close) === connection
+
+ }
+
+ "fail test if a connection is disconnected" in {
+ val connection = factory.create
+
+ awaitFuture(connection.disconnect)
+
+ factory.test(connection).isFailure must beTrue
+ }
+
+ }
+
+}
diff --git a/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/util/URLParserSpec.scala b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/util/URLParserSpec.scala
new file mode 100644
index 00000000..b15ab779
--- /dev/null
+++ b/mysql-async/src/test/scala/com/github/mauricio/async/db/mysql/util/URLParserSpec.scala
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2016 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.mysql.util
+
+import java.nio.charset.Charset
+
+import com.github.mauricio.async.db.{Configuration, SSLConfiguration}
+import com.github.mauricio.async.db.exceptions.UnableToParseURLException
+import io.netty.buffer.{ByteBufAllocator, PooledByteBufAllocator}
+import org.specs2.mutable.Specification
+
+import scala.concurrent.duration.Duration
+
+class URLParserSpec extends Specification {
+
+ "mysql URLParser" should {
+ import URLParser.{DEFAULT, parse, parseOrDie}
+
+
+ "have a reasonable default" in {
+ // This is a deliberate extra step, protecting the DEFAULT from frivilous changes.
+ // Any change to DEFAULT should require a change to this test.
+
+ DEFAULT === Configuration(
+ username = "root",
+ host = "127.0.0.1", //Matched JDBC default
+ port = 3306,
+ password = None,
+ database = None
+ )
+ }
+
+
+ // Divided into sections
+ // =========== jdbc:mysql ===========
+
+ "create a jdbc:mysql connection with the available fields" in {
+ val connectionUri = "jdbc:mysql://128.167.54.90:9987/my_database?user=john&password=doe"
+
+ parse(connectionUri) === DEFAULT.copy(
+ username = "john",
+ password = Some("doe"),
+ database = Some("my_database"),
+ host = "128.167.54.90",
+ port = 9987
+ )
+ }
+
+ "create a connection without port" in {
+ val connectionUri = "jdbc:mysql://128.167.54.90/my_database?user=john&password=doe"
+
+ parse(connectionUri) === DEFAULT.copy(
+ username = "john",
+ password = Some("doe"),
+ database = Some("my_database"),
+ host = "128.167.54.90"
+ )
+ }
+
+
+ "create a connection without username and password" in {
+ val connectionUri = "jdbc:mysql://128.167.54.90:9987/my_database"
+
+ parse(connectionUri) === DEFAULT.copy(
+ database = Some("my_database"),
+ host = "128.167.54.90",
+ port = 9987
+ )
+ }
+
+ "create a connection from a heroku like URL using 'mysql' protocol" in {
+ val connectionUri = "mysql://john:doe@128.167.54.90:9987/my_database"
+
+ parse(connectionUri) === DEFAULT.copy(
+ username = "john",
+ password = Some("doe"),
+ database = Some("my_database"),
+ host = "128.167.54.90",
+ port = 9987
+ )
+ }
+
+ "create a connection with the available fields and named server" in {
+ val connectionUri = "jdbc:mysql://localhost:9987/my_database?user=john&password=doe"
+
+ parse(connectionUri) === DEFAULT.copy(
+ username = "john",
+ password = Some("doe"),
+ database = Some("my_database"),
+ host = "localhost",
+ port = 9987
+ )
+ }
+
+ "create a connection from a heroku like URL with named server" in {
+ val connectionUri = "mysql://john:doe@psql.heroku.com:9987/my_database"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "psql.heroku.com"
+ configuration.port === 9987
+ }
+
+ "create a connection with the available fields and ipv6" in {
+ val connectionUri = "jdbc:mysql://[::1]:9987/my_database?user=john&password=doe"
+
+ val configuration = parse(connectionUri)
+
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "::1"
+ configuration.port === 9987
+ }
+
+ "create a connection from a heroku like URL and with ipv6" in {
+ val connectionUri = "mysql://john:doe@[::1]:9987/my_database"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "::1"
+ configuration.port === 9987
+ }
+
+ "create a connection with a missing hostname" in {
+ val connectionUri = "jdbc:mysql:/my_database?user=john&password=doe"
+
+ parse(connectionUri) === DEFAULT.copy(
+ username = "john",
+ password = Some("doe"),
+ database = Some("my_database")
+ )
+ }
+
+ "create a connection with a missing database name" in {
+ val connectionUri = "jdbc:mysql://[::1]:9987/?user=john&password=doe"
+
+ val configuration = parse(connectionUri)
+
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === None
+ configuration.host === "::1"
+ configuration.port === 9987
+ }
+
+ "create a connection with all default fields" in {
+ val connectionUri = "jdbc:mysql:"
+
+ val configuration = parse(connectionUri)
+
+ configuration.username === "root"
+ configuration.password === None
+ configuration.database === None
+ configuration.host === "127.0.0.1"
+ configuration.port === 3306
+ }
+
+ "create a connection with an empty (invalid) url" in {
+ val connectionUri = ""
+
+ val configuration = parse(connectionUri)
+
+ configuration.username === "root"
+ configuration.password === None
+ configuration.database === None
+ configuration.host === "127.0.0.1"
+ configuration.port === 3306
+ }
+
+
+ "recognise a mysql:// uri" in {
+ parse("mysql://localhost:425/dbname") mustEqual DEFAULT.copy(
+ username = "root",
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ "recognize a jdbc:mysql:// uri" in {
+ parse("jdbc:mysql://localhost:425/dbname") mustEqual DEFAULT.copy(
+ username = "root",
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ "pull the username and password from URI credentials" in {
+ parse("jdbc:mysql://user:password@localhost:425/dbname") mustEqual DEFAULT.copy(
+ username = "user",
+ password = Some("password"),
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ "pull the username and password from query string" in {
+ parse("jdbc:mysql://localhost:425/dbname?user=user&password=password") mustEqual DEFAULT.copy(
+ username = "user",
+ password = Some("password"),
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ // Included for consistency, so later changes aren't allowed to change behavior
+ "use the query string parameters to override URI credentials" in {
+ parse("jdbc:mysql://baduser:badpass@localhost:425/dbname?user=user&password=password") mustEqual DEFAULT.copy(
+ username = "user",
+ password = Some("password"),
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ "successfully default the port to the mysql port" in {
+ parse("jdbc:mysql://baduser:badpass@localhost/dbname?user=user&password=password") mustEqual DEFAULT.copy(
+ username = "user",
+ password = Some("password"),
+ database = Some("dbname"),
+ port = 3306,
+ host = "localhost"
+ )
+ }
+
+ "reject malformed ip addresses" in {
+ val connectionUri = "mysql://john:doe@128.567.54.90:9987/my_database"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "root"
+ configuration.password === None
+ configuration.database === None
+ configuration.host === "127.0.0.1"
+ configuration.port === 3306
+
+ parseOrDie(connectionUri) must throwA[UnableToParseURLException]
+ }
+
+ }
+
+}
diff --git a/postgresql-async/README.md b/postgresql-async/README.md
new file mode 100644
index 00000000..7702e907
--- /dev/null
+++ b/postgresql-async/README.md
@@ -0,0 +1,97 @@
+
+
+**Table of Contents**
+
+- [postgresql-async - an async Netty based PostgreSQL driver written in Scala 2.10 and 2.11](#postgresql-async---an-async-netty-based-postgresql-driver-written-in-scala-210)
+ - [What can it do now?](#what-can-it-do-now)
+ - [What is missing?](#what-is-missing)
+ - [Supported Scala/Java types and their destination types on PostgreSQL](#supported-scalajava-types-and-their-destination-types-on-postgresql)
+
+
+
+# postgresql-async - an async Netty based PostgreSQL driver written in Scala 2.10 and 2.11
+
+The main goal of this project is to implement a performant and fully functional async PostgreSQL driver. This project
+has no interest in JDBC, it's supposed to be a clean room implementation for people interested in talking directly
+to PostgreSQL.
+
+[PostgreSQL protocol information and definition can be found here](http://www.postgresql.org/docs/devel/static/protocol.html)
+
+This driver contains Java code from the [JDBC PostgreSQL](http://jdbc.postgresql.org/) driver under the
+`com.github.mauricio.async.db.postgresql.util` package consisting of the `ParseURL` class.
+
+## What can it do now?
+
+- connect to a database with or without authentication (supports MD5 and cleartext authentication methods)
+- receive database parameters
+- receive database notices
+- execute direct queries (without portals/prepared statements)
+- portals/prepared statements
+- parses most of the basic PostgreSQL types, other types are parsed as string
+- date, time and timestamp types are handled as JodaTime objects and **not** as **java.util.Date** objects
+- all work is done using the new `scala.concurrent.Future` and `scala.concurrent.Promise` objects
+- support for Byte arrays if using PostgreSQL >= 9.0
+- support for LISTEN/NOTIFY operations (check [ListenNotifySpec](https://github.com/mauricio/postgresql-async/blob/master/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ListenNotifySpec.scala) for an example on how to use it );
+
+## What is missing?
+
+- more authentication mechanisms
+- benchmarks
+- more tests (run the `jacoco:cover` sbt task and see where you can improve)
+- timeout handler for initial handshare and queries
+- implement byte array support for PostgreSQL <= 8
+
+## Supported Scala/Java types and their destination types on PostgreSQL
+
+All types also support their array versions, but they are returned as `IndexedSeq` of the type and not
+pure `Array` types.
+
+PostgreSQL type | Scala/Java type
+--- | --- | ---
+boolean | Boolean
+smallint | Short
+integer (or serial) | Int
+bigint (or bigserial) | Long
+numeric | BigDecimal
+real | Float
+double | Double
+text | String
+varchar | String
+bpchar | String
+timestamp | LocalDateTime
+timestamp_with_timezone | DateTime
+date | LocalDate
+time | LocalTime
+bytea | Array[Byte] (PostgreSQL 9.0 and above only)
+
+All other types are returned as String.
+
+Now from Scala/Java types to PostgreSQL types (when using prepared
+statements):
+
+Scala/Java type | PostgreSQL type
+--- | --- | ---
+Boolean | boolean
+Short | smallint
+Int | integer
+Long | bigint
+Float | float
+Double | double
+BigInteger | numeric
+BigDecimal | numeric
+String | varchar
+Array[Byte] | bytea (PostgreSQL 9.0 and above only)
+java.nio.ByteBuffer | bytea (PostgreSQL 9.0 and above only)
+io.netty.buffer.ByteBuf | bytea (PostgreSQL 9.0 and above only)
+java.util.Date | timestamp_with_timezone
+java.sql.Timestamp | timestamp_with_timezone
+java.sql.Date | date
+java.sql.Time | time
+LocalDate | date
+LocalDateTime | timestamp
+DateTime | timestamp_with_timezone
+LocalTime | time
+
+Array types are encoded with the kind of object they hold and not the array type itself. Java `Collection` and
+Scala `Traversable` objects are also assumed to be arrays of the types they hold and will be sent to PostgreSQL
+like that.
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala
new file mode 100644
index 00000000..470700c4
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnection.scala
@@ -0,0 +1,330 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql
+
+import com.github.mauricio.async.db.QueryResult
+import com.github.mauricio.async.db.column.{ColumnDecoderRegistry, ColumnEncoderRegistry}
+import com.github.mauricio.async.db.exceptions.{ConnectionStillRunningQueryException, InsufficientParametersException}
+import com.github.mauricio.async.db.general.MutableResultSet
+import com.github.mauricio.async.db.pool.TimeoutScheduler
+import com.github.mauricio.async.db.postgresql.codec.{PostgreSQLConnectionDelegate, PostgreSQLConnectionHandler}
+import com.github.mauricio.async.db.postgresql.column.{PostgreSQLColumnDecoderRegistry, PostgreSQLColumnEncoderRegistry}
+import com.github.mauricio.async.db.postgresql.exceptions._
+import com.github.mauricio.async.db.util._
+import com.github.mauricio.async.db.{Configuration, Connection}
+import java.util.concurrent.atomic.{AtomicInteger, AtomicLong, AtomicReference}
+
+import messages.backend._
+import messages.frontend._
+
+import scala.concurrent._
+import io.netty.channel.EventLoopGroup
+import java.util.concurrent.CopyOnWriteArrayList
+
+import com.github.mauricio.async.db.postgresql.util.URLParser
+
+object PostgreSQLConnection {
+ final val Counter = new AtomicLong()
+ final val ServerVersionKey = "server_version"
+ final val log = Log.get[PostgreSQLConnection]
+}
+
+class PostgreSQLConnection
+(
+ configuration: Configuration = URLParser.DEFAULT,
+ encoderRegistry: ColumnEncoderRegistry = PostgreSQLColumnEncoderRegistry.Instance,
+ decoderRegistry: ColumnDecoderRegistry = PostgreSQLColumnDecoderRegistry.Instance,
+ group : EventLoopGroup = NettyUtils.DefaultEventLoopGroup,
+ implicit val executionContext : ExecutionContext = ExecutorServiceUtils.CachedExecutionContext
+ )
+ extends PostgreSQLConnectionDelegate
+ with Connection
+ with TimeoutScheduler {
+
+ import PostgreSQLConnection._
+
+ private final val connectionHandler = new PostgreSQLConnectionHandler(
+ configuration,
+ encoderRegistry,
+ decoderRegistry,
+ this,
+ group,
+ executionContext
+ )
+
+ private final val currentCount = Counter.incrementAndGet()
+ private final val preparedStatementsCounter = new AtomicInteger()
+
+ private val parameterStatus = new scala.collection.mutable.HashMap[String, String]()
+ private val parsedStatements = new scala.collection.mutable.HashMap[String, PreparedStatementHolder]()
+ private var authenticated = false
+
+ private val connectionFuture = Promise[Connection]()
+
+ private var recentError = false
+ private val queryPromiseReference = new AtomicReference[Option[Promise[QueryResult]]](None)
+ private var currentQuery: Option[MutableResultSet[PostgreSQLColumnData]] = None
+ private var currentPreparedStatement: Option[PreparedStatementHolder] = None
+ private var version = Version(0,0,0)
+ private var notifyListeners = new CopyOnWriteArrayList[NotificationResponse => Unit]()
+
+ private var queryResult: Option[QueryResult] = None
+
+ override def eventLoopGroup : EventLoopGroup = group
+ def isReadyForQuery: Boolean = this.queryPromise.isEmpty
+
+ def connect: Future[Connection] = {
+ this.connectionHandler.connect.onFailure {
+ case e => this.connectionFuture.tryFailure(e)
+ }
+
+ this.connectionFuture.future
+ }
+
+ override def disconnect: Future[Connection] = this.connectionHandler.disconnect.map( c => this )
+ override def onTimeout = disconnect
+
+ override def isConnected: Boolean = this.connectionHandler.isConnected
+
+ def parameterStatuses: scala.collection.immutable.Map[String, String] = this.parameterStatus.toMap
+
+ override def sendQuery(query: String): Future[QueryResult] = {
+ validateQuery(query)
+
+ val promise = Promise[QueryResult]()
+ this.setQueryPromise(promise)
+
+ write(new QueryMessage(query))
+ addTimeout(promise,configuration.queryTimeout)
+ promise.future
+ }
+
+ override def sendPreparedStatement(query: String, values: Seq[Any] = List()): Future[QueryResult] = {
+ validateQuery(query)
+
+ val promise = Promise[QueryResult]()
+ this.setQueryPromise(promise)
+
+ val holder = this.parsedStatements.getOrElseUpdate(query,
+ new PreparedStatementHolder( query, preparedStatementsCounter.incrementAndGet ))
+
+ if (holder.paramsCount != values.length) {
+ this.clearQueryPromise
+ throw new InsufficientParametersException(holder.paramsCount, values)
+ }
+
+ this.currentPreparedStatement = Some(holder)
+ this.currentQuery = Some(new MutableResultSet(holder.columnDatas))
+ write(
+ if (holder.prepared)
+ new PreparedStatementExecuteMessage(holder.statementId, holder.realQuery, values, this.encoderRegistry)
+ else {
+ holder.prepared = true
+ new PreparedStatementOpeningMessage(holder.statementId, holder.realQuery, values, this.encoderRegistry)
+ })
+ addTimeout(promise,configuration.queryTimeout)
+ promise.future
+ }
+
+ override def onError( exception : Throwable ) {
+ this.setErrorOnFutures(exception)
+ }
+
+ def hasRecentError: Boolean = this.recentError
+
+ private def setErrorOnFutures(e: Throwable) {
+ this.recentError = true
+
+ log.error("Error on connection", e)
+
+ if (!this.connectionFuture.isCompleted) {
+ this.connectionFuture.failure(e)
+ this.disconnect
+ }
+
+ this.currentPreparedStatement.map(p => this.parsedStatements.remove(p.query))
+ this.currentPreparedStatement = None
+ this.failQueryPromise(e)
+ }
+
+ override def onReadyForQuery() {
+ this.connectionFuture.trySuccess(this)
+
+ this.recentError = false
+ queryResult.foreach(this.succeedQueryPromise)
+ }
+
+ override def onError(m: ErrorMessage) {
+ log.error("Error with message -> {}", m)
+
+ val error = new GenericDatabaseException(m)
+ error.fillInStackTrace()
+
+ this.setErrorOnFutures(error)
+ }
+
+ override def onCommandComplete(m: CommandCompleteMessage) {
+ this.currentPreparedStatement = None
+ queryResult = Some(new QueryResult(m.rowsAffected, m.statusMessage, this.currentQuery))
+ }
+
+ override def onParameterStatus(m: ParameterStatusMessage) {
+ this.parameterStatus.put(m.key, m.value)
+ if ( ServerVersionKey == m.key ) {
+ this.version = Version(m.value)
+ }
+ }
+
+ override def onDataRow(m: DataRowMessage) {
+ val items = new Array[Any](m.values.size)
+ var x = 0
+
+ while ( x < m.values.size ) {
+ val buf = m.values(x)
+ items(x) = if ( buf == null ) {
+ null
+ } else {
+ try {
+ val columnType = this.currentQuery.get.columnTypes(x)
+ this.decoderRegistry.decode(columnType, buf, configuration.charset)
+ } finally {
+ buf.release()
+ }
+ }
+ x += 1
+ }
+
+ this.currentQuery.get.addRow(items)
+ }
+
+ override def onRowDescription(m: RowDescriptionMessage) {
+ this.currentQuery = Option(new MutableResultSet(m.columnDatas))
+ this.setColumnDatas(m.columnDatas)
+ }
+
+ private def setColumnDatas( columnDatas : Array[PostgreSQLColumnData] ) {
+ this.currentPreparedStatement.foreach { holder =>
+ holder.columnDatas = columnDatas
+ }
+ }
+
+ override def onAuthenticationResponse(message: AuthenticationMessage) {
+
+ message match {
+ case m: AuthenticationOkMessage => {
+ log.debug("Successfully logged in to database")
+ this.authenticated = true
+ }
+ case m: AuthenticationChallengeCleartextMessage => {
+ write(this.credential(m))
+ }
+ case m: AuthenticationChallengeMD5 => {
+ write(this.credential(m))
+ }
+ }
+
+ }
+
+ override def onNotificationResponse( message : NotificationResponse ) {
+ val iterator = this.notifyListeners.iterator()
+ while ( iterator.hasNext ) {
+ iterator.next().apply(message)
+ }
+ }
+
+ def registerNotifyListener( listener : NotificationResponse => Unit ) {
+ this.notifyListeners.add(listener)
+ }
+
+ def unregisterNotifyListener( listener : NotificationResponse => Unit ) {
+ this.notifyListeners.remove(listener)
+ }
+
+ def clearNotifyListeners() {
+ this.notifyListeners.clear()
+ }
+
+ private def credential(authenticationMessage: AuthenticationChallengeMessage): CredentialMessage = {
+ if (configuration.username != null && configuration.password.isDefined) {
+ new CredentialMessage(
+ configuration.username,
+ configuration.password.get,
+ authenticationMessage.challengeType,
+ authenticationMessage.salt
+ )
+ } else {
+ throw new MissingCredentialInformationException(
+ this.configuration.username,
+ this.configuration.password,
+ authenticationMessage.challengeType)
+ }
+ }
+
+ private[this] def notReadyForQueryError(errorMessage : String, race : Boolean) = {
+ log.error(errorMessage)
+ throw new ConnectionStillRunningQueryException(
+ this.currentCount,
+ race
+ )
+ }
+
+ def validateIfItIsReadyForQuery(errorMessage: String) =
+ if (this.queryPromise.isDefined)
+ notReadyForQueryError(errorMessage, false)
+
+ private def validateQuery(query: String) {
+ this.validateIfItIsReadyForQuery("Can't run query because there is one query pending already")
+
+ if (query == null || query.isEmpty) {
+ throw new QueryMustNotBeNullOrEmptyException(query)
+ }
+ }
+
+ private def queryPromise: Option[Promise[QueryResult]] = queryPromiseReference.get()
+
+ private def setQueryPromise(promise: Promise[QueryResult]) {
+ if (!this.queryPromiseReference.compareAndSet(None, Some(promise)))
+ notReadyForQueryError("Can't run query due to a race with another started query", true)
+ }
+
+ private def clearQueryPromise : Option[Promise[QueryResult]] = {
+ this.queryPromiseReference.getAndSet(None)
+ }
+
+ private def failQueryPromise(t: Throwable) {
+ this.clearQueryPromise.foreach { promise =>
+ log.error("Setting error on future {}", promise)
+ promise.failure(t)
+ }
+ }
+
+ private def succeedQueryPromise(result: QueryResult) {
+ this.queryResult = None
+ this.currentQuery = None
+ this.clearQueryPromise.foreach {
+ _.success(result)
+ }
+ }
+
+ private def write( message : ClientMessage ) {
+ this.connectionHandler.write(message)
+ }
+
+ override def toString: String = {
+ s"${this.getClass.getSimpleName}{counter=${this.currentCount}}"
+ }
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PreparedStatementHolder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PreparedStatementHolder.scala
new file mode 100644
index 00000000..f8b78bcf
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/PreparedStatementHolder.scala
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql
+
+import com.github.mauricio.async.db.postgresql.messages.backend.PostgreSQLColumnData
+
+class PreparedStatementHolder(val query : String, val statementId : Int ) {
+
+ val (realQuery, paramsCount) = {
+ val result = new StringBuilder(query.length+16)
+ var offset = 0
+ var params = 0
+ while (offset < query.length) {
+ val next = query.indexOf('?', offset)
+ if (next == -1) {
+ result ++= query.substring(offset)
+ offset = query.length
+ } else {
+ result ++= query.substring(offset, next)
+ offset = next + 1
+ if (offset < query.length && query(offset) == '?') {
+ result += '?'
+ offset += 1
+ } else {
+ result += '$'
+ params += 1
+ result ++= params.toString
+ }
+ }
+ }
+ (result.toString, params)
+ }
+
+ var prepared : Boolean = false
+ var columnDatas : Array[PostgreSQLColumnData] = Array.empty
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageDecoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageDecoder.scala
new file mode 100644
index 00000000..5f210f72
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageDecoder.scala
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.codec
+
+import com.github.mauricio.async.db.postgresql.exceptions.{MessageTooLongException}
+import com.github.mauricio.async.db.postgresql.messages.backend.{ServerMessage, SSLResponseMessage}
+import com.github.mauricio.async.db.postgresql.parsers.{AuthenticationStartupParser, MessageParsersRegistry}
+import com.github.mauricio.async.db.util.{BufferDumper, Log}
+import java.nio.charset.Charset
+import com.github.mauricio.async.db.exceptions.NegativeMessageSizeException
+import io.netty.handler.codec.ByteToMessageDecoder
+import io.netty.channel.ChannelHandlerContext
+import io.netty.buffer.ByteBuf
+
+object MessageDecoder {
+ val log = Log.get[MessageDecoder]
+ val DefaultMaximumSize = 16777216
+}
+
+class MessageDecoder(sslEnabled: Boolean, charset: Charset, maximumMessageSize : Int = MessageDecoder.DefaultMaximumSize) extends ByteToMessageDecoder {
+
+ import MessageDecoder.log
+
+ private val parser = new MessageParsersRegistry(charset)
+
+ private var sslChecked = false
+
+ override def decode(ctx: ChannelHandlerContext, b: ByteBuf, out: java.util.List[Object]): Unit = {
+
+ if (sslEnabled & !sslChecked) {
+ val code = b.readByte()
+ sslChecked = true
+ out.add(new SSLResponseMessage(code == 'S'))
+ } else if (b.readableBytes() >= 5) {
+
+ b.markReaderIndex()
+
+ val code = b.readByte()
+ val lengthWithSelf = b.readInt()
+ val length = lengthWithSelf - 4
+
+ if ( length < 0 ) {
+ throw new NegativeMessageSizeException(code, length)
+ }
+
+ if ( length > maximumMessageSize ) {
+ throw new MessageTooLongException(code, length, maximumMessageSize)
+ }
+
+ if (b.readableBytes() >= length) {
+
+ if ( log.isTraceEnabled ) {
+ log.trace(s"Received buffer ${code}\n${BufferDumper.dumpAsHex(b)}")
+ }
+
+ val result = code match {
+ case ServerMessage.Authentication => {
+ AuthenticationStartupParser.parseMessage(b)
+ }
+ case _ => {
+ parser.parse(code, b.readSlice(length))
+ }
+ }
+
+ out.add(result)
+
+ } else {
+ b.resetReaderIndex()
+ return
+ }
+
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageEncoder.scala
new file mode 100644
index 00000000..30195a11
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/MessageEncoder.scala
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.codec
+
+import com.github.mauricio.async.db.column.ColumnEncoderRegistry
+import com.github.mauricio.async.db.exceptions.EncoderNotAvailableException
+import com.github.mauricio.async.db.postgresql.encoders._
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+import com.github.mauricio.async.db.postgresql.messages.frontend._
+import com.github.mauricio.async.db.util.{BufferDumper, Log}
+import java.nio.charset.Charset
+import scala.annotation.switch
+import io.netty.handler.codec.MessageToMessageEncoder
+import io.netty.channel.ChannelHandlerContext
+
+object MessageEncoder {
+ val log = Log.get[MessageEncoder]
+}
+
+class MessageEncoder(charset: Charset, encoderRegistry: ColumnEncoderRegistry) extends MessageToMessageEncoder[Object] {
+
+ import MessageEncoder.log
+
+ private val executeEncoder = new ExecutePreparedStatementEncoder(charset, encoderRegistry)
+ private val openEncoder = new PreparedStatementOpeningEncoder(charset, encoderRegistry)
+ private val startupEncoder = new StartupMessageEncoder(charset)
+ private val queryEncoder = new QueryMessageEncoder(charset)
+ private val credentialEncoder = new CredentialEncoder(charset)
+
+ override def encode(ctx: ChannelHandlerContext, msg: AnyRef, out: java.util.List[Object]) = {
+
+ val buffer = msg match {
+ case SSLRequestMessage => SSLMessageEncoder.encode()
+ case message: StartupMessage => startupEncoder.encode(message)
+ case message: ClientMessage => {
+ val encoder = (message.kind: @switch) match {
+ case ServerMessage.Close => CloseMessageEncoder
+ case ServerMessage.Execute => this.executeEncoder
+ case ServerMessage.Parse => this.openEncoder
+ case ServerMessage.Query => this.queryEncoder
+ case ServerMessage.PasswordMessage => this.credentialEncoder
+ case _ => throw new EncoderNotAvailableException(message)
+ }
+
+ encoder.encode(message)
+ }
+ case _ => {
+ throw new IllegalArgumentException("Can not encode message %s".format(msg))
+ }
+ }
+
+ if (log.isTraceEnabled) {
+ log.trace(s"Sending message ${msg.getClass.getName}\n${BufferDumper.dumpAsHex(buffer)}")
+ }
+
+ out.add(buffer)
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/PostgreSQLConnectionDelegate.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/PostgreSQLConnectionDelegate.scala
new file mode 100644
index 00000000..bde1f230
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/PostgreSQLConnectionDelegate.scala
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.codec
+
+import com.github.mauricio.async.db.postgresql.messages.backend._
+
+trait PostgreSQLConnectionDelegate {
+
+ def onAuthenticationResponse(message: AuthenticationMessage)
+ def onCommandComplete( message : CommandCompleteMessage )
+ def onDataRow( message : DataRowMessage )
+ def onError( message : ErrorMessage )
+ def onError( throwable : Throwable )
+ def onParameterStatus( message : ParameterStatusMessage )
+ def onReadyForQuery()
+ def onRowDescription(message : RowDescriptionMessage)
+ def onNotificationResponse(message : NotificationResponse )
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/PostgreSQLConnectionHandler.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/PostgreSQLConnectionHandler.scala
new file mode 100644
index 00000000..733cc5d1
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/codec/PostgreSQLConnectionHandler.scala
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.codec
+
+import com.github.mauricio.async.db.Configuration
+import com.github.mauricio.async.db.SSLConfiguration.Mode
+import com.github.mauricio.async.db.column.{ColumnDecoderRegistry, ColumnEncoderRegistry}
+import com.github.mauricio.async.db.postgresql.exceptions._
+import com.github.mauricio.async.db.postgresql.messages.backend._
+import com.github.mauricio.async.db.postgresql.messages.frontend._
+import com.github.mauricio.async.db.util.ChannelFutureTransformer.toFuture
+import com.github.mauricio.async.db.util._
+import java.net.InetSocketAddress
+import scala.annotation.switch
+import scala.concurrent._
+import io.netty.channel._
+import io.netty.bootstrap.Bootstrap
+import io.netty.channel
+import scala.util.Failure
+import com.github.mauricio.async.db.postgresql.messages.backend.DataRowMessage
+import com.github.mauricio.async.db.postgresql.messages.backend.CommandCompleteMessage
+import com.github.mauricio.async.db.postgresql.messages.backend.ProcessData
+import scala.util.Success
+import com.github.mauricio.async.db.postgresql.messages.backend.RowDescriptionMessage
+import com.github.mauricio.async.db.postgresql.messages.backend.ParameterStatusMessage
+import io.netty.channel.socket.nio.NioSocketChannel
+import io.netty.handler.codec.CodecException
+import io.netty.handler.ssl.{SslContextBuilder, SslHandler}
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory
+import io.netty.util.concurrent.FutureListener
+import javax.net.ssl.{SSLParameters, TrustManagerFactory}
+import java.security.KeyStore
+import java.io.FileInputStream
+
+object PostgreSQLConnectionHandler {
+ final val log = Log.get[PostgreSQLConnectionHandler]
+}
+
+class PostgreSQLConnectionHandler
+(
+ configuration: Configuration,
+ encoderRegistry: ColumnEncoderRegistry,
+ decoderRegistry: ColumnDecoderRegistry,
+ connectionDelegate : PostgreSQLConnectionDelegate,
+ group : EventLoopGroup,
+ executionContext : ExecutionContext
+ )
+ extends SimpleChannelInboundHandler[Object]
+{
+
+ import PostgreSQLConnectionHandler.log
+
+ private val properties = List(
+ "user" -> configuration.username,
+ "database" -> configuration.database,
+ "client_encoding" -> configuration.charset.name(),
+ "DateStyle" -> "ISO",
+ "extra_float_digits" -> "2")
+
+ private implicit final val _executionContext = executionContext
+ private final val bootstrap = new Bootstrap()
+ private final val connectionFuture = Promise[PostgreSQLConnectionHandler]()
+ private final val disconnectionPromise = Promise[PostgreSQLConnectionHandler]()
+ private var processData : ProcessData = null
+
+ private var currentContext : ChannelHandlerContext = null
+
+ def connect: Future[PostgreSQLConnectionHandler] = {
+ this.bootstrap.group(this.group)
+ this.bootstrap.channel(classOf[NioSocketChannel])
+ this.bootstrap.handler(new ChannelInitializer[channel.Channel]() {
+
+ override def initChannel(ch: channel.Channel): Unit = {
+ ch.pipeline.addLast(
+ new MessageDecoder(configuration.ssl.mode != Mode.Disable, configuration.charset, configuration.maximumMessageSize),
+ new MessageEncoder(configuration.charset, encoderRegistry),
+ PostgreSQLConnectionHandler.this)
+ }
+
+ })
+
+ this.bootstrap.option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true)
+ this.bootstrap.option(ChannelOption.ALLOCATOR, configuration.allocator)
+
+ this.bootstrap.connect(new InetSocketAddress(configuration.host, configuration.port)).onFailure {
+ case e => connectionFuture.tryFailure(e)
+ }
+
+ this.connectionFuture.future
+ }
+
+ def disconnect: Future[PostgreSQLConnectionHandler] = {
+
+ if ( this.isConnected ) {
+ this.currentContext.channel.writeAndFlush(CloseMessage).onComplete {
+ case Success(writeFuture) => writeFuture.channel.close().onComplete {
+ case Success(closeFuture) => this.disconnectionPromise.trySuccess(this)
+ case Failure(e) => this.disconnectionPromise.tryFailure(e)
+ }
+ case Failure(e) => this.disconnectionPromise.tryFailure(e)
+ }
+ }
+
+ this.disconnectionPromise.future
+ }
+
+ def isConnected: Boolean = {
+ if (this.currentContext != null) {
+ this.currentContext.channel.isActive
+ } else {
+ false
+ }
+ }
+
+ override def channelActive(ctx: ChannelHandlerContext): Unit = {
+ if (configuration.ssl.mode == Mode.Disable)
+ ctx.writeAndFlush(new StartupMessage(this.properties))
+ else
+ ctx.writeAndFlush(SSLRequestMessage)
+ }
+
+ override def channelRead0(ctx: ChannelHandlerContext, msg: Object): Unit = {
+
+ msg match {
+
+ case SSLResponseMessage(supported) =>
+ if (supported) {
+ val ctxBuilder = SslContextBuilder.forClient()
+ if (configuration.ssl.mode >= Mode.VerifyCA) {
+ configuration.ssl.rootCert.fold {
+ val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
+ val ks = KeyStore.getInstance(KeyStore.getDefaultType())
+ val cacerts = new FileInputStream(System.getProperty("java.home") + "/lib/security/cacerts")
+ try {
+ ks.load(cacerts, "changeit".toCharArray)
+ } finally {
+ cacerts.close()
+ }
+ tmf.init(ks)
+ ctxBuilder.trustManager(tmf)
+ } { path =>
+ ctxBuilder.trustManager(path)
+ }
+ } else {
+ ctxBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE)
+ }
+ val sslContext = ctxBuilder.build()
+ val sslEngine = sslContext.newEngine(ctx.alloc(), configuration.host, configuration.port)
+ if (configuration.ssl.mode >= Mode.VerifyFull) {
+ val sslParams = sslEngine.getSSLParameters()
+ sslParams.setEndpointIdentificationAlgorithm("HTTPS")
+ sslEngine.setSSLParameters(sslParams)
+ }
+ val handler = new SslHandler(sslEngine)
+ ctx.pipeline().addFirst(handler)
+ handler.handshakeFuture.addListener(new FutureListener[channel.Channel]() {
+ def operationComplete(future: io.netty.util.concurrent.Future[channel.Channel]) {
+ if (future.isSuccess()) {
+ ctx.writeAndFlush(new StartupMessage(properties))
+ } else {
+ connectionDelegate.onError(future.cause())
+ }
+ }
+ })
+ } else if (configuration.ssl.mode < Mode.Require) {
+ ctx.writeAndFlush(new StartupMessage(properties))
+ } else {
+ connectionDelegate.onError(new IllegalArgumentException("SSL is not supported on server"))
+ }
+
+ case m: ServerMessage => {
+
+ (m.kind : @switch) match {
+ case ServerMessage.BackendKeyData => {
+ this.processData = m.asInstanceOf[ProcessData]
+ }
+ case ServerMessage.BindComplete => {
+ }
+ case ServerMessage.Authentication => {
+ log.debug("Authentication response received {}", m)
+ connectionDelegate.onAuthenticationResponse(m.asInstanceOf[AuthenticationMessage])
+ }
+ case ServerMessage.CommandComplete => {
+ connectionDelegate.onCommandComplete(m.asInstanceOf[CommandCompleteMessage])
+ }
+ case ServerMessage.CloseComplete => {
+ }
+ case ServerMessage.DataRow => {
+ connectionDelegate.onDataRow(m.asInstanceOf[DataRowMessage])
+ }
+ case ServerMessage.Error => {
+ connectionDelegate.onError(m.asInstanceOf[ErrorMessage])
+ }
+ case ServerMessage.EmptyQueryString => {
+ val exception = new QueryMustNotBeNullOrEmptyException(null)
+ exception.fillInStackTrace()
+ connectionDelegate.onError(exception)
+ }
+ case ServerMessage.NoData => {
+ }
+ case ServerMessage.Notice => {
+ log.info("Received notice {}", m)
+ }
+ case ServerMessage.NotificationResponse => {
+ connectionDelegate.onNotificationResponse(m.asInstanceOf[NotificationResponse])
+ }
+ case ServerMessage.ParameterStatus => {
+ connectionDelegate.onParameterStatus(m.asInstanceOf[ParameterStatusMessage])
+ }
+ case ServerMessage.ParseComplete => {
+ }
+ case ServerMessage.ReadyForQuery => {
+ connectionDelegate.onReadyForQuery()
+ }
+ case ServerMessage.RowDescription => {
+ connectionDelegate.onRowDescription(m.asInstanceOf[RowDescriptionMessage])
+ }
+ case _ => {
+ val exception = new IllegalStateException("Handler not implemented for message %s".format(m.kind))
+ exception.fillInStackTrace()
+ connectionDelegate.onError(exception)
+ }
+ }
+
+ }
+ case _ => {
+ log.error("Unknown message type - {}", msg)
+ val exception = new IllegalArgumentException("Unknown message type - %s".format(msg))
+ exception.fillInStackTrace()
+ connectionDelegate.onError(exception)
+ }
+
+ }
+
+ }
+
+ override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
+ // unwrap CodecException if needed
+ cause match {
+ case t: CodecException => connectionDelegate.onError(t.getCause)
+ case _ => connectionDelegate.onError(cause)
+ }
+ }
+
+ override def channelInactive(ctx: ChannelHandlerContext): Unit = {
+ log.info("Connection disconnected - {}", ctx.channel.remoteAddress)
+ }
+
+ override def handlerAdded(ctx: ChannelHandlerContext) {
+ this.currentContext = ctx
+ }
+
+ def write( message : ClientMessage ) {
+ this.currentContext.writeAndFlush(message).onFailure {
+ case e : Throwable => connectionDelegate.onError(e)
+ }
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ArrayDecoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ArrayDecoder.scala
new file mode 100644
index 00000000..b62e9629
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ArrayDecoder.scala
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import com.github.mauricio.async.db.column.ColumnDecoder
+import com.github.mauricio.async.db.postgresql.util.{ArrayStreamingParserDelegate, ArrayStreamingParser}
+import scala.collection.IndexedSeq
+import scala.collection.mutable.ArrayBuffer
+import com.github.mauricio.async.db.general.ColumnData
+import io.netty.buffer.{Unpooled, ByteBuf}
+import java.nio.charset.Charset
+
+class ArrayDecoder(private val decoder: ColumnDecoder) extends ColumnDecoder {
+
+ override def decode( kind : ColumnData, buffer : ByteBuf, charset : Charset ): IndexedSeq[Any] = {
+
+ val bytes = new Array[Byte](buffer.readableBytes())
+ buffer.readBytes(bytes)
+ val value = new String(bytes, charset)
+
+ var stack = List.empty[ArrayBuffer[Any]]
+ var current: ArrayBuffer[Any] = null
+ var result: IndexedSeq[Any] = null
+ val delegate = new ArrayStreamingParserDelegate {
+ override def arrayEnded {
+ result = stack.head
+ stack = stack.tail
+ }
+
+ override def elementFound(element: String) {
+ val result = if ( decoder.supportsStringDecoding ) {
+ decoder.decode(element)
+ } else {
+ decoder.decode(kind, Unpooled.wrappedBuffer( element.getBytes(charset) ), charset)
+ }
+ current += result
+ }
+
+ override def nullElementFound {
+ current += null
+ }
+
+ override def arrayStarted {
+ current = new ArrayBuffer[Any]()
+
+ stack.headOption match {
+ case Some(item) => {
+ item += current
+ }
+ case None => {}
+ }
+
+ stack ::= current
+ }
+ }
+
+ ArrayStreamingParser.parse(value, delegate)
+
+ result
+ }
+
+ def decode( value : String ) : Any = throw new UnsupportedOperationException("Should not be called")
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/BooleanEncoderDecoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/BooleanEncoderDecoder.scala
new file mode 100644
index 00000000..d11e6942
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/BooleanEncoderDecoder.scala
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import com.github.mauricio.async.db.column.ColumnEncoderDecoder
+
+object BooleanEncoderDecoder extends ColumnEncoderDecoder {
+
+ override def decode(value: String): Any = {
+ if ("t" == value) {
+ true
+ } else {
+ false
+ }
+ }
+
+ override def encode(value: Any): String = {
+ val result = value.asInstanceOf[Boolean]
+
+ if (result) {
+ "t"
+ } else {
+ "f"
+ }
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ByteArrayEncoderDecoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ByteArrayEncoderDecoder.scala
new file mode 100644
index 00000000..2ae1e7a4
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ByteArrayEncoderDecoder.scala
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import com.github.mauricio.async.db.column.ColumnEncoderDecoder
+import com.github.mauricio.async.db.postgresql.exceptions.ByteArrayFormatNotSupportedException
+import com.github.mauricio.async.db.util.{ Log, HexCodec }
+import java.nio.ByteBuffer
+
+import io.netty.buffer.ByteBuf
+
+object ByteArrayEncoderDecoder extends ColumnEncoderDecoder {
+
+ final val log = Log.getByName(this.getClass.getName)
+ final val HexStart = "\\x"
+ final val HexStartChars = HexStart.toCharArray
+
+ override def decode(value: String): Array[Byte] = {
+
+ if (value.startsWith(HexStart)) {
+ HexCodec.decode(value, 2)
+ } else {
+ // Default encoding is 'escape'
+
+ // Size the buffer to the length of the string, the data can't be bigger
+ val buffer = ByteBuffer.allocate(value.length)
+
+ val ci = value.iterator
+
+ while (ci.hasNext) {
+ ci.next match {
+ case '\\' ⇒ getCharOrDie(ci) match {
+ case '\\' ⇒ buffer.put('\\'.toByte)
+ case firstDigit ⇒
+ val secondDigit = getCharOrDie(ci)
+ val thirdDigit = getCharOrDie(ci)
+ // Must always be in triplets
+ buffer.put(
+ Integer.decode(
+ new String(Array('0', firstDigit, secondDigit, thirdDigit))).toByte)
+ }
+ case c ⇒ buffer.put(c.toByte)
+ }
+ }
+
+ buffer.flip
+ val finalArray = new Array[Byte](buffer.remaining())
+ buffer.get(finalArray)
+
+ finalArray
+ }
+
+ }
+
+ /**
+ * This is required since {@link Iterator#next} when {@linke Iterator#hasNext} is false is undefined.
+ * @param ci the iterator source of the data
+ * @return the next character
+ * @throws IllegalArgumentException if there is no next character
+ */
+ private [this] def getCharOrDie(ci: Iterator[Char]): Char = {
+ if (ci.hasNext) {
+ ci.next()
+ } else {
+ throw new IllegalArgumentException("Expected escape sequence character, found nothing")
+ }
+ }
+
+ override def encode(value: Any): String = {
+ val array = value match {
+ case byteArray: Array[Byte] => byteArray
+
+ case byteBuffer: ByteBuffer if byteBuffer.hasArray => byteBuffer.array()
+
+ case byteBuffer: ByteBuffer =>
+ val arr = new Array[Byte](byteBuffer.remaining())
+ byteBuffer.get(arr)
+ arr
+
+ case byteBuf: ByteBuf if byteBuf.hasArray => byteBuf.array()
+
+ case byteBuf: ByteBuf =>
+ val arr = new Array[Byte](byteBuf.readableBytes())
+ byteBuf.getBytes(0, arr)
+ arr
+ }
+
+ HexCodec.encode(array, HexStartChars)
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/CharEncoderDecoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/CharEncoderDecoder.scala
new file mode 100644
index 00000000..8dc30a67
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/CharEncoderDecoder.scala
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import com.github.mauricio.async.db.column.ColumnEncoderDecoder
+
+object CharEncoderDecoder extends ColumnEncoderDecoder {
+
+ override def decode(value: String): Any = value.charAt(0)
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala
new file mode 100644
index 00000000..93fef482
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/ColumnTypes.scala
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+object ColumnTypes {
+ final val Untyped = 0
+ final val Bigserial = 20
+ final val BigserialArray = 1016
+ final val Char = 18
+ final val CharArray = 1002
+ final val Smallint = 21
+ final val SmallintArray = 1005
+ final val Integer = 23
+ final val IntegerArray = 1007
+ final val Numeric = 1700
+ // Decimal is the same as Numeric on PostgreSQL
+ final val NumericArray = 1231
+ final val Real = 700
+ final val RealArray = 1021
+ final val Double = 701
+ final val DoubleArray = 1022
+ final val Serial = 23
+ final val Bpchar = 1042
+ final val BpcharArray = 1014
+ final val Varchar = 1043
+ // Char is the same as Varchar on PostgreSQL
+ final val VarcharArray = 1015
+ final val Text = 25
+ final val TextArray = 1009
+ final val Timestamp = 1114
+ final val TimestampArray = 1115
+ final val TimestampWithTimezone = 1184
+ final val TimestampWithTimezoneArray = 1185
+ final val Date = 1082
+ final val DateArray = 1182
+ final val Time = 1083
+ final val TimeArray = 1183
+ final val TimeWithTimezone = 1266
+ final val TimeWithTimezoneArray = 1270
+ final val Interval = 1186
+ final val IntervalArray = 1187
+ final val Boolean = 16
+ final val BooleanArray = 1000
+ final val OID = 26
+ final val OIDArray = 1028
+
+ final val ByteA = 17
+ final val ByteA_Array = 1001
+
+ final val MoneyArray = 791
+ final val NameArray = 1003
+ final val UUID = 2950
+ final val UUIDArray = 2951
+ final val XMLArray = 143
+
+ final val Inet = 869
+ final val InetArray = 1041
+}
+
+/*
+
+ public static final int UNSPECIFIED = 0;
+ public static final int INT2 = 21;
+ public static final int INT2_ARRAY = 1005;
+ public static final int INT4 = 23;
+ public static final int INT4_ARRAY = 1007;
+ public static final int INT8 = 20;
+ public static final int INT8_ARRAY = 1016;
+ public static final int TEXT = 25;
+ public static final int TEXT_ARRAY = 1009;
+ public static final int NUMERIC = 1700;
+ public static final int NUMERIC_ARRAY = 1231;
+ public static final int FLOAT4 = 700;
+ public static final int FLOAT4_ARRAY = 1021;
+ public static final int FLOAT8 = 701;
+ public static final int FLOAT8_ARRAY = 1022;
+ public static final int BOOL = 16;
+ public static final int BOOL_ARRAY = 1000;
+ public static final int DATE = 1082;
+ public static final int DATE_ARRAY = 1182;
+ public static final int TIME = 1083;
+ public static final int TIME_ARRAY = 1183;
+ public static final int TIMETZ = 1266;
+ public static final int TIMETZ_ARRAY = 1270;
+ public static final int TIMESTAMP = 1114;
+ public static final int TIMESTAMP_ARRAY = 1115;
+ public static final int TIMESTAMPTZ = 1184;
+ public static final int TIMESTAMPTZ_ARRAY = 1185;
+ public static final int BYTEA = 17;
+ public static final int BYTEA_ARRAY = 1001;
+ public static final int VARCHAR = 1043;
+ public static final int VARCHAR_ARRAY = 1015;
+ public static final int OID = 26;
+ public static final int OID_ARRAY = 1028;
+ public static final int BPCHAR = 1042;
+ public static final int BPCHAR_ARRAY = 1014;
+ public static final int MONEY = 790;
+ public static final int MONEY_ARRAY = 791;
+ public static final int NAME = 19;
+ public static final int NAME_ARRAY = 1003;
+ public static final int BIT = 1560;
+ public static final int BIT_ARRAY = 1561;
+ public static final int VOID = 2278;
+ public static final int INTERVAL = 1186;
+ public static final int INTERVAL_ARRAY = 1187;
+ public static final int CHAR = 18; // This is not char(N), this is "char" a single byte type.
+ public static final int CHAR_ARRAY = 1002;
+ public static final int VARBIT = 1562;
+ public static final int VARBIT_ARRAY = 1563;
+ public static final int UUID = 2950;
+ public static final int UUID_ARRAY = 2951;
+ public static final int XML = 142;
+ public static final int XML_ARRAY = 143;
+
+*/
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala
new file mode 100644
index 00000000..5b4a47a7
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnDecoderRegistry.scala
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import com.github.mauricio.async.db.column._
+import com.github.mauricio.async.db.postgresql.column.ColumnTypes._
+import scala.annotation.switch
+import java.nio.charset.Charset
+import io.netty.util.CharsetUtil
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.general.ColumnData
+
+object PostgreSQLColumnDecoderRegistry {
+ val Instance = new PostgreSQLColumnDecoderRegistry()
+}
+
+class PostgreSQLColumnDecoderRegistry( charset : Charset = CharsetUtil.UTF_8 ) extends ColumnDecoderRegistry {
+
+ private final val stringArrayDecoder = new ArrayDecoder(StringEncoderDecoder)
+ private final val booleanArrayDecoder = new ArrayDecoder(BooleanEncoderDecoder)
+ private final val charArrayDecoder = new ArrayDecoder(CharEncoderDecoder)
+ private final val longArrayDecoder = new ArrayDecoder(LongEncoderDecoder)
+ private final val shortArrayDecoder = new ArrayDecoder(ShortEncoderDecoder)
+ private final val integerArrayDecoder = new ArrayDecoder(IntegerEncoderDecoder)
+ private final val bigDecimalArrayDecoder = new ArrayDecoder(BigDecimalEncoderDecoder)
+ private final val floatArrayDecoder = new ArrayDecoder(FloatEncoderDecoder)
+ private final val doubleArrayDecoder = new ArrayDecoder(DoubleEncoderDecoder)
+ private final val timestampArrayDecoder = new ArrayDecoder(PostgreSQLTimestampEncoderDecoder)
+ private final val timestampWithTimezoneArrayDecoder = new ArrayDecoder(PostgreSQLTimestampEncoderDecoder)
+ private final val dateArrayDecoder = new ArrayDecoder(DateEncoderDecoder)
+ private final val timeArrayDecoder = new ArrayDecoder(TimeEncoderDecoder.Instance)
+ private final val timeWithTimestampArrayDecoder = new ArrayDecoder(TimeWithTimezoneEncoderDecoder)
+ private final val intervalArrayDecoder = new ArrayDecoder(PostgreSQLIntervalEncoderDecoder)
+ private final val uuidArrayDecoder = new ArrayDecoder(UUIDEncoderDecoder)
+ private final val inetAddressArrayDecoder = new ArrayDecoder(InetAddressEncoderDecoder)
+
+ override def decode(kind: ColumnData, value: ByteBuf, charset: Charset): Any = {
+ decoderFor(kind.dataType).decode(kind, value, charset)
+ }
+
+ def decoderFor(kind: Int): ColumnDecoder = {
+ (kind : @switch) match {
+ case Boolean => BooleanEncoderDecoder
+ case BooleanArray => this.booleanArrayDecoder
+
+ case ColumnTypes.Char => CharEncoderDecoder
+ case CharArray => this.charArrayDecoder
+
+ case Bigserial => LongEncoderDecoder
+ case BigserialArray => this.longArrayDecoder
+
+ case Smallint => ShortEncoderDecoder
+ case SmallintArray => this.shortArrayDecoder
+
+ case ColumnTypes.Integer => IntegerEncoderDecoder
+ case IntegerArray => this.integerArrayDecoder
+
+ case OID => LongEncoderDecoder
+ case OIDArray => this.longArrayDecoder
+
+ case ColumnTypes.Numeric => BigDecimalEncoderDecoder
+ case NumericArray => this.bigDecimalArrayDecoder
+
+ case Real => FloatEncoderDecoder
+ case RealArray => this.floatArrayDecoder
+
+ case ColumnTypes.Double => DoubleEncoderDecoder
+ case DoubleArray => this.doubleArrayDecoder
+
+ case Text => StringEncoderDecoder
+ case TextArray => this.stringArrayDecoder
+
+ case Varchar => StringEncoderDecoder
+ case VarcharArray => this.stringArrayDecoder
+
+ case Bpchar => StringEncoderDecoder
+ case BpcharArray => this.stringArrayDecoder
+
+ case Timestamp => PostgreSQLTimestampEncoderDecoder
+ case TimestampArray => this.timestampArrayDecoder
+
+ case TimestampWithTimezone => PostgreSQLTimestampEncoderDecoder
+ case TimestampWithTimezoneArray => this.timestampWithTimezoneArrayDecoder
+
+ case Date => DateEncoderDecoder
+ case DateArray => this.dateArrayDecoder
+
+ case Time => TimeEncoderDecoder.Instance
+ case TimeArray => this.timeArrayDecoder
+
+ case TimeWithTimezone => TimeWithTimezoneEncoderDecoder
+ case TimeWithTimezoneArray => this.timeWithTimestampArrayDecoder
+
+ case Interval => PostgreSQLIntervalEncoderDecoder
+ case IntervalArray => this.intervalArrayDecoder
+
+ case MoneyArray => this.stringArrayDecoder
+ case NameArray => this.stringArrayDecoder
+ case UUID => UUIDEncoderDecoder
+ case UUIDArray => this.uuidArrayDecoder
+ case XMLArray => this.stringArrayDecoder
+ case ByteA => ByteArrayEncoderDecoder
+
+ case Inet => InetAddressEncoderDecoder
+ case InetArray => this.inetAddressArrayDecoder
+
+ case _ => StringEncoderDecoder
+ }
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala
new file mode 100644
index 00000000..c9f95f43
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLColumnEncoderRegistry.scala
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import java.nio.ByteBuffer
+
+import com.github.mauricio.async.db.column._
+import io.netty.buffer.ByteBuf
+import org.joda.time._
+
+import scala.collection.JavaConversions._
+
+object PostgreSQLColumnEncoderRegistry {
+ val Instance = new PostgreSQLColumnEncoderRegistry()
+}
+
+class PostgreSQLColumnEncoderRegistry extends ColumnEncoderRegistry {
+
+ private val classesSequence_ : List[(Class[_], (ColumnEncoder, Int))] = List(
+ classOf[Int] -> (IntegerEncoderDecoder -> ColumnTypes.Numeric),
+ classOf[java.lang.Integer] -> (IntegerEncoderDecoder -> ColumnTypes.Numeric),
+
+ classOf[java.lang.Short] -> (ShortEncoderDecoder -> ColumnTypes.Numeric),
+ classOf[Short] -> (ShortEncoderDecoder -> ColumnTypes.Numeric),
+
+ classOf[Long] -> (LongEncoderDecoder -> ColumnTypes.Numeric),
+ classOf[java.lang.Long] -> (LongEncoderDecoder -> ColumnTypes.Numeric),
+
+ classOf[String] -> (StringEncoderDecoder -> ColumnTypes.Varchar),
+ classOf[java.lang.String] -> (StringEncoderDecoder -> ColumnTypes.Varchar),
+
+ classOf[Float] -> (FloatEncoderDecoder -> ColumnTypes.Numeric),
+ classOf[java.lang.Float] -> (FloatEncoderDecoder -> ColumnTypes.Numeric),
+
+ classOf[Double] -> (DoubleEncoderDecoder -> ColumnTypes.Numeric),
+ classOf[java.lang.Double] -> (DoubleEncoderDecoder -> ColumnTypes.Numeric),
+
+ classOf[BigDecimal] -> (BigDecimalEncoderDecoder -> ColumnTypes.Numeric),
+ classOf[java.math.BigDecimal] -> (BigDecimalEncoderDecoder -> ColumnTypes.Numeric),
+
+ classOf[java.net.InetAddress] -> (InetAddressEncoderDecoder -> ColumnTypes.Inet),
+
+ classOf[java.util.UUID] -> (UUIDEncoderDecoder -> ColumnTypes.UUID),
+
+ classOf[LocalDate] -> ( DateEncoderDecoder -> ColumnTypes.Date ),
+ classOf[LocalDateTime] -> (TimestampEncoderDecoder.Instance -> ColumnTypes.Timestamp),
+ classOf[DateTime] -> (TimestampWithTimezoneEncoderDecoder -> ColumnTypes.TimestampWithTimezone),
+ classOf[ReadableDateTime] -> (TimestampWithTimezoneEncoderDecoder -> ColumnTypes.TimestampWithTimezone),
+ classOf[ReadableInstant] -> (DateEncoderDecoder -> ColumnTypes.Date),
+
+ classOf[ReadablePeriod] -> (PostgreSQLIntervalEncoderDecoder -> ColumnTypes.Interval),
+ classOf[ReadableDuration] -> (PostgreSQLIntervalEncoderDecoder -> ColumnTypes.Interval),
+
+ classOf[java.util.Date] -> (TimestampWithTimezoneEncoderDecoder -> ColumnTypes.TimestampWithTimezone),
+ classOf[java.sql.Date] -> ( DateEncoderDecoder -> ColumnTypes.Date ),
+ classOf[java.sql.Time] -> ( SQLTimeEncoder -> ColumnTypes.Time ),
+ classOf[java.sql.Timestamp] -> (TimestampWithTimezoneEncoderDecoder -> ColumnTypes.TimestampWithTimezone),
+ classOf[java.util.Calendar] -> (TimestampWithTimezoneEncoderDecoder -> ColumnTypes.TimestampWithTimezone),
+ classOf[java.util.GregorianCalendar] -> (TimestampWithTimezoneEncoderDecoder -> ColumnTypes.TimestampWithTimezone),
+ classOf[Array[Byte]] -> ( ByteArrayEncoderDecoder -> ColumnTypes.ByteA ),
+ classOf[ByteBuffer] -> ( ByteArrayEncoderDecoder -> ColumnTypes.ByteA ),
+ classOf[ByteBuf] -> ( ByteArrayEncoderDecoder -> ColumnTypes.ByteA )
+ )
+
+ private final val classesSequence = (classOf[LocalTime] -> (TimeEncoderDecoder.Instance -> ColumnTypes.Time)) ::
+ (classOf[ReadablePartial] -> (TimeEncoderDecoder.Instance -> ColumnTypes.Time)) ::
+ classesSequence_
+
+ private final val classes = classesSequence.toMap
+
+ override def encode(value: Any): String = {
+
+ if (value == null) {
+ return null
+ }
+
+ value match {
+ case Some(v) => encode(v)
+ case None => null
+ case _ => encodeValue(value)
+ }
+
+ }
+
+ /**
+ * Used to encode a value that is not null and not an Option.
+ */
+ private def encodeValue(value: Any): String = {
+
+ val encoder = this.classes.get(value.getClass)
+
+ if (encoder.isDefined) {
+ encoder.get._1.encode(value)
+ } else {
+ value match {
+ case i: java.lang.Iterable[_] => encodeArray(i.toIterable)
+ case i: Traversable[_] => encodeArray(i)
+ case i: Array[_] => encodeArray(i.toIterable)
+ case p: Product => encodeComposite(p)
+ case _ => {
+ this.classesSequence.find(entry => entry._1.isAssignableFrom(value.getClass)) match {
+ case Some(parent) => parent._2._1.encode(value)
+ case None => value.toString
+ }
+ }
+ }
+
+ }
+
+ }
+
+ private def encodeComposite(p: Product): String = {
+ p.productIterator.map {
+ item =>
+ if (item == null || item == None) {
+ "NULL"
+ } else {
+ if (this.shouldQuote(item)) {
+ "\"" + this.encode(item).replaceAllLiterally("\\", """\\""").replaceAllLiterally("\"", """\"""") + "\""
+ } else {
+ this.encode(item)
+ }
+ }
+ }.mkString("(", ",", ")")
+ }
+
+ private def encodeArray(collection: Traversable[_]): String = {
+ collection.map {
+ item =>
+ if (item == null || item == None) {
+ "NULL"
+ } else {
+ if (this.shouldQuote(item)) {
+ "\"" + this.encode(item).replaceAllLiterally("\\", """\\""").replaceAllLiterally("\"", """\"""") + "\""
+ } else {
+ this.encode(item)
+ }
+ }
+ }.mkString("{", ",", "}")
+ }
+
+ private def shouldQuote(value: Any): Boolean = {
+ value match {
+ case n: java.lang.Number => false
+ case n: Int => false
+ case n: Short => false
+ case n: Long => false
+ case n: Float => false
+ case n: Double => false
+ case n: java.lang.Iterable[_] => false
+ case n: Traversable[_] => false
+ case n: Array[_] => false
+ case Some(v) => shouldQuote(v)
+ case _ => true
+ }
+ }
+
+ override def kindOf(value: Any): Int = {
+ if ( value == null || value == None ) {
+ 0
+ } else {
+ value match {
+ case Some(v) => kindOf(v)
+ case v : String => ColumnTypes.Untyped
+ case _ => {
+ this.classes.get(value.getClass) match {
+ case Some( entry ) => entry._2
+ case None => ColumnTypes.Untyped
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLIntervalEncoderDecoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLIntervalEncoderDecoder.scala
new file mode 100644
index 00000000..3f25ad76
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLIntervalEncoderDecoder.scala
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ * Copyright 2013 Dylan Simon
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import com.github.mauricio.async.db.column.ColumnEncoderDecoder
+import com.github.mauricio.async.db.exceptions.DateEncoderNotAvailableException
+import com.github.mauricio.async.db.util.Log
+import org.joda.time.{Period, ReadablePeriod, ReadableDuration}
+import org.joda.time.format.{ISOPeriodFormat, PeriodFormatterBuilder}
+
+object PostgreSQLIntervalEncoderDecoder extends ColumnEncoderDecoder {
+
+ private val log = Log.getByName(this.getClass.getName)
+
+ /* Postgres accepts all ISO8601 formats. */
+ private val formatter = ISOPeriodFormat.standard
+
+ override def encode(value : Any) : String = {
+ value match {
+ case t : ReadablePeriod => formatter.print(t)
+ case t : ReadableDuration => t.toString // defaults to ISO8601
+ case _ => throw new DateEncoderNotAvailableException(value)
+ }
+ }
+
+ /* these should only be used for parsing: */
+ private def postgresYMDBuilder(builder : PeriodFormatterBuilder) = builder
+ .appendYears .appendSuffix(" year", " years").appendSeparator(" ")
+ .appendMonths .appendSuffix(" mon", " mons" ).appendSeparator(" ")
+ .appendDays .appendSuffix(" day", " days" ).appendSeparator(" ")
+
+ private val postgres_verboseParser =
+ postgresYMDBuilder(new PeriodFormatterBuilder().appendLiteral("@ "))
+ .appendHours .appendSuffix(" hour", " hours").appendSeparator(" ")
+ .appendMinutes.appendSuffix(" min", " mins" ).appendSeparator(" ")
+ .appendSecondsWithOptionalMillis.appendSuffix(" sec", " secs")
+ .toFormatter
+
+ private def postgresHMSBuilder(builder : PeriodFormatterBuilder) = builder
+ // .printZeroAlways // really all-or-nothing
+ .rejectSignedValues(true) // XXX: sign should apply to all
+ .appendHours .appendSuffix(":")
+ .appendMinutes.appendSuffix(":")
+ .appendSecondsWithOptionalMillis
+
+ private val hmsParser =
+ postgresHMSBuilder(new PeriodFormatterBuilder())
+ .toFormatter
+
+ private val postgresParser =
+ postgresHMSBuilder(postgresYMDBuilder(new PeriodFormatterBuilder()))
+ .toFormatter
+
+ /* These sql_standard parsers don't handle negative signs correctly. */
+ private def sqlDTBuilder(builder : PeriodFormatterBuilder) =
+ postgresHMSBuilder(builder
+ .appendDays.appendSeparator(" "))
+
+ private val sqlDTParser =
+ sqlDTBuilder(new PeriodFormatterBuilder())
+ .toFormatter
+
+ private val sqlParser =
+ sqlDTBuilder(new PeriodFormatterBuilder()
+ .printZeroAlways
+ .rejectSignedValues(true) // XXX: sign should apply to both
+ .appendYears.appendSeparator("-").appendMonths
+ .rejectSignedValues(false)
+ .printZeroNever
+ .appendSeparator(" "))
+ .toFormatter
+
+ /* This supports all positive intervals, and intervalstyle of postgres_verbose, and iso_8601 perfectly.
+ * If intervalstyle is set to postgres or sql_standard, some negative intervals may be rejected.
+ */
+ def decode(value : String) : Period = {
+ if (value.isEmpty) /* huh? */
+ Period.ZERO
+ else {
+ val format = (
+ if (value(0).equals('P')) /* iso_8601 */
+ formatter
+ else if (value.startsWith("@ "))
+ postgres_verboseParser
+ else {
+ /* try to guess based on what comes after the first number */
+ val i = value.indexWhere(!_.isDigit, if ("-+".contains(value(0))) 1 else 0)
+ if (i < 0 || ":.".contains(value(i))) /* simple HMS (to support group negation) */
+ hmsParser
+ else if (value(i).equals('-')) /* sql_standard: Y-M */
+ sqlParser
+ else if (value(i).equals(' ') && i+1 < value.length && value(i+1).isDigit) /* sql_standard: D H:M:S */
+ sqlDTParser
+ else
+ postgresParser
+ }
+ )
+ if ((format eq hmsParser) && value(0).equals('-'))
+ format.parsePeriod(value.substring(1)).negated
+ else if (value.endsWith(" ago")) /* only really applies to postgres_verbose, but shouldn't hurt */
+ format.parsePeriod(value.stripSuffix(" ago")).negated
+ else
+ format.parsePeriod(value)
+ }
+ }
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLTimestampEncoderDecoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLTimestampEncoderDecoder.scala
new file mode 100644
index 00000000..7c6c426d
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/PostgreSQLTimestampEncoderDecoder.scala
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import com.github.mauricio.async.db.column.ColumnEncoderDecoder
+import com.github.mauricio.async.db.exceptions.DateEncoderNotAvailableException
+import com.github.mauricio.async.db.general.ColumnData
+import com.github.mauricio.async.db.postgresql.messages.backend.PostgreSQLColumnData
+import com.github.mauricio.async.db.util.Log
+import io.netty.buffer.ByteBuf
+import java.nio.charset.Charset
+import java.sql.Timestamp
+import java.util.{Calendar, Date}
+import org.joda.time._
+import org.joda.time.format.DateTimeFormatterBuilder
+
+object PostgreSQLTimestampEncoderDecoder extends ColumnEncoderDecoder {
+
+ private val log = Log.getByName(this.getClass.getName)
+
+ private val optionalTimeZone = new DateTimeFormatterBuilder()
+ .appendPattern("Z").toParser
+
+ private val internalFormatters = 1.until(6).inclusive.map {
+ index =>
+ new DateTimeFormatterBuilder()
+ .appendPattern("yyyy-MM-dd HH:mm:ss")
+ .appendPattern("." + ("S" * index ))
+ .appendOptional(optionalTimeZone)
+ .toFormatter
+ }
+
+ private val internalFormatterWithoutSeconds = new DateTimeFormatterBuilder()
+ .appendPattern("yyyy-MM-dd HH:mm:ss")
+ .appendOptional(optionalTimeZone)
+ .toFormatter
+
+ def formatter = internalFormatters(5)
+
+ override def decode( kind : ColumnData, value : ByteBuf, charset : Charset ) : Any = {
+ val bytes = new Array[Byte](value.readableBytes())
+ value.readBytes(bytes)
+
+ val text = new String(bytes, charset)
+
+ val columnType = kind.asInstanceOf[PostgreSQLColumnData]
+
+ columnType.dataType match {
+ case ColumnTypes.Timestamp | ColumnTypes.TimestampArray => {
+ selectFormatter(text).parseLocalDateTime(text)
+ }
+ case ColumnTypes.TimestampWithTimezoneArray => {
+ selectFormatter(text).parseDateTime(text)
+ }
+ case ColumnTypes.TimestampWithTimezone => {
+ if ( columnType.dataTypeModifier > 0 ) {
+ internalFormatters(columnType.dataTypeModifier - 1).parseDateTime(text)
+ } else {
+ selectFormatter(text).parseDateTime(text)
+ }
+ }
+ }
+ }
+
+ private def selectFormatter( text : String ) = {
+ if ( text.contains(".") ) {
+ internalFormatters(5)
+ } else {
+ internalFormatterWithoutSeconds
+ }
+ }
+
+ override def decode(value : String) : Any = throw new UnsupportedOperationException("this method should not have been called")
+
+ override def encode(value: Any): String = {
+ value match {
+ case t: Timestamp => this.formatter.print(new DateTime(t))
+ case t: Date => this.formatter.print(new DateTime(t))
+ case t: Calendar => this.formatter.print(new DateTime(t))
+ case t: LocalDateTime => this.formatter.print(t)
+ case t: ReadableDateTime => this.formatter.print(t)
+ case _ => throw new DateEncoderNotAvailableException(value)
+ }
+ }
+
+ override def supportsStringDecoding : Boolean = false
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/SingleByteEncoderDecoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/SingleByteEncoderDecoder.scala
new file mode 100644
index 00000000..c111be00
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/column/SingleByteEncoderDecoder.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import com.github.mauricio.async.db.column.ColumnEncoderDecoder
+
+object SingleByteEncoderDecoder extends ColumnEncoderDecoder {
+
+ override def encode(value: Any): String = {
+ val byte = value.asInstanceOf[Byte]
+ ByteArrayEncoderDecoder.encode(Array(byte))
+ }
+
+ override def decode(value: String): Any = {
+ ByteArrayEncoderDecoder.decode(value)(0)
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/CloseMessageEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/CloseMessageEncoder.scala
new file mode 100644
index 00000000..a2534c3d
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/CloseMessageEncoder.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.encoders
+
+import com.github.mauricio.async.db.postgresql.messages.frontend.ClientMessage
+import io.netty.buffer.{Unpooled, ByteBuf}
+
+object CloseMessageEncoder extends Encoder {
+
+ override def encode(message: ClientMessage): ByteBuf = {
+ val buffer = Unpooled.buffer(5)
+ buffer.writeByte('X')
+ buffer.writeInt(4)
+
+ buffer
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/CredentialEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/CredentialEncoder.scala
new file mode 100644
index 00000000..3a8f4c5b
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/CredentialEncoder.scala
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.encoders
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{ServerMessage, AuthenticationResponseType}
+import com.github.mauricio.async.db.postgresql.messages.frontend.{CredentialMessage, ClientMessage}
+import com.github.mauricio.async.db.postgresql.util.PasswordHelper
+import com.github.mauricio.async.db.util.ByteBufferUtils
+import java.nio.charset.Charset
+import io.netty.buffer.{Unpooled, ByteBuf}
+
+class CredentialEncoder(charset: Charset) extends Encoder {
+
+ def encode(message: ClientMessage): ByteBuf = {
+
+ val credentialMessage = message.asInstanceOf[CredentialMessage]
+
+ val password = credentialMessage.authenticationType match {
+ case AuthenticationResponseType.Cleartext => {
+ credentialMessage.password.getBytes(charset)
+ }
+ case AuthenticationResponseType.MD5 => {
+ PasswordHelper.encode(
+ credentialMessage.username,
+ credentialMessage.password,
+ credentialMessage.salt.get,
+ charset)
+ }
+ }
+
+ val buffer = Unpooled.buffer(1 + 4 + password.size + 1)
+ buffer.writeByte(ServerMessage.PasswordMessage)
+ buffer.writeInt(0)
+ buffer.writeBytes(password)
+ buffer.writeByte(0)
+
+ ByteBufferUtils.writeLength(buffer)
+
+ buffer
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/Encoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/Encoder.scala
new file mode 100644
index 00000000..a6b4f714
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/Encoder.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.encoders
+
+import com.github.mauricio.async.db.postgresql.messages.frontend.ClientMessage
+import io.netty.buffer.ByteBuf
+
+trait Encoder {
+
+ def encode(message: ClientMessage): ByteBuf
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/ExecutePreparedStatementEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/ExecutePreparedStatementEncoder.scala
new file mode 100644
index 00000000..f1c605c2
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/ExecutePreparedStatementEncoder.scala
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.encoders
+
+import java.nio.charset.Charset
+
+import com.github.mauricio.async.db.column.ColumnEncoderRegistry
+import com.github.mauricio.async.db.postgresql.messages.frontend.{ClientMessage, PreparedStatementExecuteMessage}
+import io.netty.buffer.ByteBuf
+
+class ExecutePreparedStatementEncoder(
+ charset: Charset,
+ encoder : ColumnEncoderRegistry)
+ extends Encoder
+ with PreparedStatementEncoderHelper
+{
+
+ def encode(message: ClientMessage): ByteBuf = {
+
+ val m = message.asInstanceOf[PreparedStatementExecuteMessage]
+ val statementIdBytes = m.statementId.toString.getBytes(charset)
+
+ writeExecutePortal( statementIdBytes, m.query, m.values, encoder, charset )
+ }
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/PreparedStatementEncoderHelper.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/PreparedStatementEncoderHelper.scala
new file mode 100644
index 00000000..4f0716b9
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/PreparedStatementEncoderHelper.scala
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.encoders
+
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+import com.github.mauricio.async.db.util.{Log, ByteBufferUtils}
+import com.github.mauricio.async.db.column.ColumnEncoderRegistry
+import java.nio.charset.Charset
+import io.netty.buffer.{Unpooled, ByteBuf}
+import scala.collection.mutable.ArrayBuffer
+
+object PreparedStatementEncoderHelper {
+ final val log = Log.get[PreparedStatementEncoderHelper]
+}
+
+trait PreparedStatementEncoderHelper {
+
+ import PreparedStatementEncoderHelper.log
+
+ def writeExecutePortal(
+ statementIdBytes: Array[Byte],
+ query: String,
+ values: Seq[Any],
+ encoder: ColumnEncoderRegistry,
+ charset: Charset,
+ writeDescribe: Boolean = false
+ ): ByteBuf = {
+
+ if (log.isDebugEnabled) {
+ log.debug(s"Preparing execute portal to statement ($query) - values (${values.mkString(", ")}) - ${charset}")
+ }
+
+ val bindBuffer = Unpooled.buffer(1024)
+
+ bindBuffer.writeByte(ServerMessage.Bind)
+ bindBuffer.writeInt(0)
+
+ bindBuffer.writeBytes(statementIdBytes)
+ bindBuffer.writeByte(0)
+ bindBuffer.writeBytes(statementIdBytes)
+ bindBuffer.writeByte(0)
+
+ bindBuffer.writeShort(0)
+
+ bindBuffer.writeShort(values.length)
+
+ val decodedValues = if (log.isDebugEnabled) {
+ new ArrayBuffer[String](values.size)
+ } else {
+ null
+ }
+
+ for (value <- values) {
+ if (isNull(value)) {
+ bindBuffer.writeInt(-1)
+
+ if (log.isDebugEnabled) {
+ decodedValues += null
+ }
+ } else {
+ val encodedValue = encoder.encode(value)
+
+ if (log.isDebugEnabled) {
+ decodedValues += encodedValue
+ }
+
+ if (isNull(encodedValue)) {
+ bindBuffer.writeInt(-1)
+ } else {
+ val content = encodedValue.getBytes(charset)
+ bindBuffer.writeInt(content.length)
+ bindBuffer.writeBytes(content)
+ }
+
+ }
+ }
+
+ if (log.isDebugEnabled) {
+ log.debug(s"Executing portal - statement id (${statementIdBytes.mkString("-")}) - statement ($query) - encoded values (${decodedValues.mkString(", ")}) - original values (${values.mkString(", ")})")
+ }
+
+ bindBuffer.writeShort(0)
+
+ ByteBufferUtils.writeLength(bindBuffer)
+
+ if (writeDescribe) {
+ val describeLength = 1 + 4 + 1 + statementIdBytes.length + 1
+ val describeBuffer = bindBuffer
+ describeBuffer.writeByte(ServerMessage.Describe)
+ describeBuffer.writeInt(describeLength - 1)
+ describeBuffer.writeByte('P')
+ describeBuffer.writeBytes(statementIdBytes)
+ describeBuffer.writeByte(0)
+ }
+
+ val executeLength = 1 + 4 + statementIdBytes.length + 1 + 4
+ val executeBuffer = Unpooled.buffer(executeLength)
+ executeBuffer.writeByte(ServerMessage.Execute)
+ executeBuffer.writeInt(executeLength - 1)
+ executeBuffer.writeBytes(statementIdBytes)
+ executeBuffer.writeByte(0)
+ executeBuffer.writeInt(0)
+
+ val closeLength = 1 + 4 + 1 + statementIdBytes.length + 1
+ val closeBuffer = Unpooled.buffer(closeLength)
+ closeBuffer.writeByte(ServerMessage.CloseStatementOrPortal)
+ closeBuffer.writeInt(closeLength - 1)
+ closeBuffer.writeByte('P')
+ closeBuffer.writeBytes(statementIdBytes)
+ closeBuffer.writeByte(0)
+
+ val syncBuffer = Unpooled.buffer(5)
+ syncBuffer.writeByte(ServerMessage.Sync)
+ syncBuffer.writeInt(4)
+
+ Unpooled.wrappedBuffer(bindBuffer, executeBuffer, syncBuffer, closeBuffer)
+
+ }
+
+ def isNull(value: Any): Boolean = value == null || value == None
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/PreparedStatementOpeningEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/PreparedStatementOpeningEncoder.scala
new file mode 100644
index 00000000..41263bb1
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/PreparedStatementOpeningEncoder.scala
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.encoders
+
+import com.github.mauricio.async.db.column.ColumnEncoderRegistry
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+import com.github.mauricio.async.db.postgresql.messages.frontend.{ClientMessage, PreparedStatementOpeningMessage}
+import com.github.mauricio.async.db.util.{Log, ByteBufferUtils}
+import java.nio.charset.Charset
+import io.netty.buffer.{Unpooled, ByteBuf}
+
+object PreparedStatementOpeningEncoder {
+ val log = Log.get[PreparedStatementOpeningEncoder]
+}
+
+class PreparedStatementOpeningEncoder(charset: Charset, encoder : ColumnEncoderRegistry)
+ extends Encoder
+ with PreparedStatementEncoderHelper
+{
+
+ import PreparedStatementOpeningEncoder.log
+
+ override def encode(message: ClientMessage): ByteBuf = {
+
+ val m = message.asInstanceOf[PreparedStatementOpeningMessage]
+
+ val statementIdBytes = m.statementId.toString.getBytes(charset)
+ val columnCount = m.valueTypes.size
+
+ val parseBuffer = Unpooled.buffer(1024)
+
+ parseBuffer.writeByte(ServerMessage.Parse)
+ parseBuffer.writeInt(0)
+
+ parseBuffer.writeBytes(statementIdBytes)
+ parseBuffer.writeByte(0)
+ parseBuffer.writeBytes(m.query.getBytes(charset))
+ parseBuffer.writeByte(0)
+
+ parseBuffer.writeShort(columnCount)
+
+ if ( log.isDebugEnabled ) {
+ log.debug(s"Opening query (${m.query}) - statement id (${statementIdBytes.mkString("-")}) - selected types (${m.valueTypes.mkString(", ")}) - values (${m.values.mkString(", ")})")
+ }
+
+ for (kind <- m.valueTypes) {
+ parseBuffer.writeInt(kind)
+ }
+
+ ByteBufferUtils.writeLength(parseBuffer)
+
+ val executeBuffer = writeExecutePortal(statementIdBytes, m.query, m.values, encoder, charset, true)
+
+ Unpooled.wrappedBuffer(parseBuffer, executeBuffer)
+ }
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/QueryMessageEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/QueryMessageEncoder.scala
new file mode 100644
index 00000000..b368670a
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/QueryMessageEncoder.scala
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.encoders
+
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+import com.github.mauricio.async.db.postgresql.messages.frontend.{QueryMessage, ClientMessage}
+import com.github.mauricio.async.db.util.{Log, ByteBufferUtils}
+import java.nio.charset.Charset
+import io.netty.buffer.{Unpooled, ByteBuf}
+
+object QueryMessageEncoder {
+ val log = Log.get[QueryMessageEncoder]
+}
+
+class QueryMessageEncoder(charset: Charset) extends Encoder {
+
+ import QueryMessageEncoder.log
+
+ override def encode(message: ClientMessage): ByteBuf = {
+
+ val m = message.asInstanceOf[QueryMessage]
+
+ if ( log.isDebugEnabled ) {
+ log.debug("Executing direct query ({})", m.query)
+ }
+
+ val buffer = Unpooled.buffer()
+ buffer.writeByte(ServerMessage.Query)
+ buffer.writeInt(0)
+ ByteBufferUtils.writeCString(m.query, buffer, charset)
+
+ ByteBufferUtils.writeLength(buffer)
+
+ buffer
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/SSLMessageEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/SSLMessageEncoder.scala
new file mode 100644
index 00000000..aeec7435
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/SSLMessageEncoder.scala
@@ -0,0 +1,16 @@
+package com.github.mauricio.async.db.postgresql.encoders
+
+import io.netty.buffer.ByteBuf
+import io.netty.buffer.Unpooled
+
+object SSLMessageEncoder {
+
+ def encode(): ByteBuf = {
+ val buffer = Unpooled.buffer()
+ buffer.writeInt(8)
+ buffer.writeShort(1234)
+ buffer.writeShort(5679)
+ buffer
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/StartupMessageEncoder.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/StartupMessageEncoder.scala
new file mode 100644
index 00000000..206fd2d3
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/encoders/StartupMessageEncoder.scala
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.encoders
+
+import com.github.mauricio.async.db.postgresql.messages.frontend.{ClientMessage, StartupMessage}
+import com.github.mauricio.async.db.util.ByteBufferUtils
+import java.nio.charset.Charset
+import io.netty.buffer.{Unpooled, ByteBuf}
+
+class StartupMessageEncoder(charset: Charset) {
+
+ //private val log = Log.getByName("StartupMessageEncoder")
+
+ def encode(startup: StartupMessage): ByteBuf = {
+
+ val buffer = Unpooled.buffer()
+ buffer.writeInt(0)
+ buffer.writeShort(3)
+ buffer.writeShort(0)
+
+ startup.parameters.foreach {
+ pair =>
+ pair._2 match {
+ case value: String => {
+ ByteBufferUtils.writeCString(pair._1, buffer, charset)
+ ByteBufferUtils.writeCString(value, buffer, charset)
+ }
+ case Some(value) => {
+ ByteBufferUtils.writeCString(pair._1, buffer, charset)
+ ByteBufferUtils.writeCString(value.toString, buffer, charset)
+ }
+ case _ => {}
+ }
+ }
+
+ buffer.writeByte(0)
+
+ val index = buffer.writerIndex()
+
+ buffer.markWriterIndex()
+ buffer.writerIndex(0)
+ buffer.writeInt(index)
+ buffer.resetWriterIndex()
+
+ buffer
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/ByteArrayFormatNotSupportedException.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/ByteArrayFormatNotSupportedException.scala
new file mode 100644
index 00000000..457c9869
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/ByteArrayFormatNotSupportedException.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.exceptions
+
+import com.github.mauricio.async.db.exceptions.DatabaseException
+
+object ByteArrayFormatNotSupportedException {
+
+ final val Message = """The bytea 'escape' format is not yet supported, you need to use a PG version that uses the 'hex' format (version 9 and onwards)"""
+
+}
+
+class ByteArrayFormatNotSupportedException
+ extends DatabaseException( ByteArrayFormatNotSupportedException.Message )
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/ColumnDecoderNotFoundException.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/ColumnDecoderNotFoundException.scala
new file mode 100644
index 00000000..043ea118
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/ColumnDecoderNotFoundException.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.exceptions
+
+class ColumnDecoderNotFoundException(kind: Int)
+ extends IllegalArgumentException("There is no decoder available for kind %s".format(kind))
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/GenericDatabaseException.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/GenericDatabaseException.scala
new file mode 100644
index 00000000..09d2efe2
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/GenericDatabaseException.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.exceptions
+
+import com.github.mauricio.async.db.exceptions.DatabaseException
+import com.github.mauricio.async.db.postgresql.messages.backend.ErrorMessage
+
+class GenericDatabaseException(val errorMessage: ErrorMessage)
+ extends DatabaseException(errorMessage.toString)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/InvalidArrayException.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/InvalidArrayException.scala
new file mode 100644
index 00000000..054cf485
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/InvalidArrayException.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+package com.github.mauricio.async.db.postgresql.exceptions
+
+import com.github.mauricio.async.db.exceptions.DatabaseException
+
+class InvalidArrayException(message: String) extends DatabaseException(message)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/MessageTooLongException.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/MessageTooLongException.scala
new file mode 100644
index 00000000..b37912fc
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/MessageTooLongException.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.exceptions
+
+import com.github.mauricio.async.db.exceptions.DatabaseException
+
+class MessageTooLongException( code : Byte, length : Int, limit : Int )
+ extends DatabaseException("Message of type %d has size %d, higher than the limit %d".format(code, length, limit))
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/MissingCredentialInformationException.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/MissingCredentialInformationException.scala
new file mode 100644
index 00000000..c5cdfb01
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/MissingCredentialInformationException.scala
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+package com.github.mauricio.async.db.postgresql.exceptions
+
+import com.github.mauricio.async.db.exceptions.DatabaseException
+import com.github.mauricio.async.db.postgresql.messages.backend.AuthenticationResponseType
+
+class MissingCredentialInformationException(
+ val username: String,
+ val password: Option[String],
+ val authenticationResponseType: AuthenticationResponseType.AuthenticationResponseType)
+ extends DatabaseException(
+ "Username and password were required by auth type %s but are not available (username=[%s] password=[%s]".format(
+ authenticationResponseType,
+ username,
+ password
+ )
+ )
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/NotConnectedException.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/NotConnectedException.scala
new file mode 100644
index 00000000..0f64866e
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/NotConnectedException.scala
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.exceptions
+
+import com.github.mauricio.async.db.exceptions.DatabaseException
+
+class NotConnectedException(message: String) extends DatabaseException(message)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/QueryMustNotBeNullOrEmptyException.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/QueryMustNotBeNullOrEmptyException.scala
new file mode 100644
index 00000000..3b9a6d26
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/exceptions/QueryMustNotBeNullOrEmptyException.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.exceptions
+
+import com.github.mauricio.async.db.exceptions.DatabaseException
+
+/**
+ *
+ * Raised if the query string is null or empty.
+ *
+ * @param query the problematic query
+ */
+class QueryMustNotBeNullOrEmptyException(query : String)
+ extends DatabaseException("Query must not be null or empty, original query is [%s]".format(query))
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationChallengeCleartextMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationChallengeCleartextMessage.scala
new file mode 100644
index 00000000..67ad7d31
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationChallengeCleartextMessage.scala
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+object AuthenticationChallengeCleartextMessage {
+ val Instance = new AuthenticationChallengeCleartextMessage()
+}
+
+class AuthenticationChallengeCleartextMessage
+ extends AuthenticationChallengeMessage(AuthenticationResponseType.Cleartext, None)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationChallengeMD5.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationChallengeMD5.scala
new file mode 100644
index 00000000..8183851f
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationChallengeMD5.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+class AuthenticationChallengeMD5(salt: Array[Byte])
+ extends AuthenticationChallengeMessage(AuthenticationResponseType.MD5, Some(salt))
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationChallengeMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationChallengeMessage.scala
new file mode 100644
index 00000000..438b0daf
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationChallengeMessage.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+class AuthenticationChallengeMessage(
+ val challengeType: AuthenticationResponseType.AuthenticationResponseType,
+ val salt: Option[Array[Byte]])
+ extends AuthenticationMessage
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationMessage.scala
new file mode 100644
index 00000000..576773db
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationMessage.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+abstract class AuthenticationMessage extends ServerMessage(ServerMessage.Authentication)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationOkMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationOkMessage.scala
new file mode 100644
index 00000000..85a6905c
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationOkMessage.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+object AuthenticationOkMessage {
+ val Instance = new AuthenticationOkMessage()
+}
+
+class AuthenticationOkMessage extends AuthenticationMessage
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationResponseType.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationResponseType.scala
new file mode 100644
index 00000000..ca18ec07
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/AuthenticationResponseType.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+object AuthenticationResponseType extends Enumeration {
+ type AuthenticationResponseType = Value
+ val MD5, Cleartext, Ok = Value
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/BindComplete.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/BindComplete.scala
new file mode 100644
index 00000000..56f71931
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/BindComplete.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+object BindComplete extends ServerMessage(ServerMessage.BindComplete)
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/CloseComplete.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/CloseComplete.scala
new file mode 100644
index 00000000..173bc4ba
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/CloseComplete.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+object CloseComplete extends ServerMessage(ServerMessage.CloseComplete)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/CommandCompleteMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/CommandCompleteMessage.scala
new file mode 100644
index 00000000..2f689b07
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/CommandCompleteMessage.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+case class CommandCompleteMessage(val rowsAffected: Int, val statusMessage: String)
+ extends ServerMessage(ServerMessage.CommandComplete)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/DataRowMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/DataRowMessage.scala
new file mode 100644
index 00000000..80959083
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/DataRowMessage.scala
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+import io.netty.buffer.ByteBuf
+
+case class DataRowMessage(val values: Array[ByteBuf]) extends ServerMessage(ServerMessage.DataRow)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/EmptyQueryString.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/EmptyQueryString.scala
new file mode 100644
index 00000000..693d1e19
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/EmptyQueryString.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+object EmptyQueryString extends ServerMessage( ServerMessage.EmptyQueryString )
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ErrorMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ErrorMessage.scala
new file mode 100644
index 00000000..1531e541
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ErrorMessage.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+class ErrorMessage(fields: Map[Char, String])
+ extends InformationMessage(ServerMessage.Error, fields)
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/InformationMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/InformationMessage.scala
new file mode 100644
index 00000000..14dfdc6d
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/InformationMessage.scala
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+object InformationMessage {
+
+ val Severity = 'S'
+ val SQLState = 'C'
+ val Message = 'M'
+ val Detail = 'D'
+ val Hint = 'H'
+ val Position = 'P'
+ val InternalQuery = 'q'
+ val Where = 'W'
+ val File = 'F'
+ val Line = 'L'
+ val Routine = 'R'
+
+ val Fields = Map(
+ Severity -> "Severity",
+ SQLState -> "SQLSTATE",
+ Message -> "Message",
+ Detail -> "Detail",
+ Hint -> "Hint",
+ Position -> "Position",
+ InternalQuery -> "Internal Query",
+ Where -> "Where",
+ File -> "File",
+ Line -> "Line",
+ Routine -> "Routine"
+ )
+
+ def fieldName(name: Char): String = Fields.getOrElse(name, {
+ name.toString
+ })
+
+}
+
+abstract class InformationMessage(messageType: Byte, val fields: Map[Char, String])
+ extends ServerMessage(messageType) {
+
+ def message: String = this.fields('M')
+
+ override def toString: String = {
+ "%s(fields=%s)".format(this.getClass.getSimpleName, fields.map {
+ pair => InformationMessage.fieldName(pair._1) -> pair._2
+ })
+ }
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/NoData.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/NoData.scala
new file mode 100644
index 00000000..4feac705
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/NoData.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+object NoData extends ServerMessage( ServerMessage.NoData )
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/NoticeMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/NoticeMessage.scala
new file mode 100644
index 00000000..0a5c165e
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/NoticeMessage.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+class NoticeMessage(fields: Map[Char, String])
+ extends InformationMessage(ServerMessage.Notice, fields)
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/NotificationResponse.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/NotificationResponse.scala
new file mode 100644
index 00000000..ab911160
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/NotificationResponse.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013-2014 db-async-common
+ *
+ * The db-async-common project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+class NotificationResponse( val backendPid : Int, val channel : String, val payload : String )
+ extends ServerMessage(ServerMessage.NotificationResponse)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ParameterStatusMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ParameterStatusMessage.scala
new file mode 100644
index 00000000..29b1953f
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ParameterStatusMessage.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+case class ParameterStatusMessage(val key: String, val value: String)
+ extends ServerMessage(ServerMessage.ParameterStatus)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ParseComplete.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ParseComplete.scala
new file mode 100644
index 00000000..f96d3c24
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ParseComplete.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+object ParseComplete extends ServerMessage(ServerMessage.ParseComplete)
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/PostgreSQLColumnData.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/PostgreSQLColumnData.scala
new file mode 100644
index 00000000..ab76d945
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/PostgreSQLColumnData.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+import com.github.mauricio.async.db.general.ColumnData
+
+case class PostgreSQLColumnData(
+ name: String,
+ tableObjectId: Int,
+ columnNumber: Int,
+ dataType: Int,
+ dataTypeSize: Long,
+ dataTypeModifier: Int,
+ fieldFormat: Int) extends ColumnData
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ProcessData.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ProcessData.scala
new file mode 100644
index 00000000..dd38a586
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ProcessData.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+case class ProcessData(val processId: Int, val secretKey: Int)
+ extends ServerMessage(ServerMessage.BackendKeyData)
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ReadyForQueryMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ReadyForQueryMessage.scala
new file mode 100644
index 00000000..bd353dc5
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ReadyForQueryMessage.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+class ReadyForQueryMessage(transactionStatus: Char) extends ServerMessage(ServerMessage.ReadyForQuery)
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/RowDescriptionMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/RowDescriptionMessage.scala
new file mode 100644
index 00000000..f4c58a1f
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/RowDescriptionMessage.scala
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+case class RowDescriptionMessage(val columnDatas: Array[PostgreSQLColumnData])
+ extends ServerMessage(ServerMessage.RowDescription)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/SSLResponseMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/SSLResponseMessage.scala
new file mode 100644
index 00000000..905ab688
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/SSLResponseMessage.scala
@@ -0,0 +1,3 @@
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+case class SSLResponseMessage(supported: Boolean)
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ServerMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ServerMessage.scala
new file mode 100644
index 00000000..1fa5b9a2
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/backend/ServerMessage.scala
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.backend
+
+import com.github.mauricio.async.db.KindedMessage
+
+object ServerMessage {
+ final val Authentication = 'R'
+ final val BackendKeyData = 'K'
+ final val Bind = 'B'
+ final val BindComplete = '2'
+ final val CommandComplete = 'C'
+ final val Close = 'X'
+ final val CloseStatementOrPortal = 'C'
+ final val CloseComplete = '3'
+ final val DataRow = 'D'
+ final val Describe = 'D'
+ final val Error = 'E'
+ final val Execute = 'E'
+ final val EmptyQueryString = 'I'
+ final val NoData = 'n'
+ final val Notice = 'N'
+ final val NotificationResponse = 'A'
+ final val ParameterStatus = 'S'
+ final val Parse = 'P'
+ final val ParseComplete = '1'
+ final val PasswordMessage = 'p'
+ final val PortalSuspended = 's'
+ final val Query = 'Q'
+ final val RowDescription = 'T'
+ final val ReadyForQuery = 'Z'
+ final val Sync = 'S'
+}
+
+class ServerMessage(val kind: Int) extends KindedMessage
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/ClientMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/ClientMessage.scala
new file mode 100644
index 00000000..5a9ea914
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/ClientMessage.scala
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.frontend
+
+import com.github.mauricio.async.db.KindedMessage
+
+class ClientMessage(val kind: Int) extends KindedMessage
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/CloseMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/CloseMessage.scala
new file mode 100644
index 00000000..4b1f2df9
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/CloseMessage.scala
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.frontend
+
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+
+object CloseMessage extends ClientMessage(ServerMessage.Close)
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/CredentialMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/CredentialMessage.scala
new file mode 100644
index 00000000..ae8a8908
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/CredentialMessage.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.frontend
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{ServerMessage, AuthenticationResponseType}
+
+
+class CredentialMessage(
+ val username: String,
+ val password: String,
+ val authenticationType: AuthenticationResponseType.AuthenticationResponseType,
+ val salt: Option[Array[Byte]]
+ )
+ extends ClientMessage(ServerMessage.PasswordMessage)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/InitialClientMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/InitialClientMessage.scala
new file mode 100644
index 00000000..228c5e65
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/InitialClientMessage.scala
@@ -0,0 +1,3 @@
+package com.github.mauricio.async.db.postgresql.messages.frontend
+
+trait InitialClientMessage
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/PreparedStatementExecuteMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/PreparedStatementExecuteMessage.scala
new file mode 100644
index 00000000..6b6d70e0
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/PreparedStatementExecuteMessage.scala
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.frontend
+
+import com.github.mauricio.async.db.column.ColumnEncoderRegistry
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+
+class PreparedStatementExecuteMessage(statementId: Int, query: String, values: Seq[Any], encoderRegistry : ColumnEncoderRegistry)
+ extends PreparedStatementMessage(statementId, ServerMessage.Execute, query, values, encoderRegistry)
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/PreparedStatementMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/PreparedStatementMessage.scala
new file mode 100644
index 00000000..a16bba91
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/PreparedStatementMessage.scala
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.frontend
+
+import com.github.mauricio.async.db.column.ColumnEncoderRegistry
+
+class PreparedStatementMessage(
+ val statementId: Int,
+ kind: Byte,
+ val query: String,
+ val values: Seq[Any],
+ encoderRegistry: ColumnEncoderRegistry
+ )
+ extends ClientMessage(kind) {
+
+ val valueTypes: Seq[Int] = values.map {
+ value =>
+ encoderRegistry.kindOf(value)
+ }
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/PreparedStatementOpeningMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/PreparedStatementOpeningMessage.scala
new file mode 100644
index 00000000..78ed8b4b
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/PreparedStatementOpeningMessage.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.frontend
+
+import com.github.mauricio.async.db.column.ColumnEncoderRegistry
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+
+class PreparedStatementOpeningMessage(statementId: Int, query: String, values: Seq[Any], encoderRegistry : ColumnEncoderRegistry)
+ extends PreparedStatementMessage(statementId: Int, ServerMessage.Parse, query, values, encoderRegistry) {
+
+ override def toString() : String =
+ s"${this.getClass.getSimpleName}(id=${statementId},query=${query},values=${values}})"
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/QueryMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/QueryMessage.scala
new file mode 100644
index 00000000..d1dd6b1a
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/QueryMessage.scala
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.frontend
+
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+
+class QueryMessage(val query: String) extends ClientMessage(ServerMessage.Query)
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/SSLRequestMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/SSLRequestMessage.scala
new file mode 100644
index 00000000..c3bf84ff
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/SSLRequestMessage.scala
@@ -0,0 +1,5 @@
+package com.github.mauricio.async.db.postgresql.messages.frontend
+
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+
+object SSLRequestMessage extends InitialClientMessage
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/StartupMessage.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/StartupMessage.scala
new file mode 100644
index 00000000..bb53390f
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/messages/frontend/StartupMessage.scala
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.messages.frontend
+
+class StartupMessage(val parameters: List[(String, Any)]) extends InitialClientMessage
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/AuthenticationStartupParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/AuthenticationStartupParser.scala
new file mode 100644
index 00000000..edac9f6f
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/AuthenticationStartupParser.scala
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.exceptions.UnsupportedAuthenticationMethodException
+import com.github.mauricio.async.db.postgresql.messages.backend.{AuthenticationChallengeMD5, AuthenticationChallengeCleartextMessage, AuthenticationOkMessage, ServerMessage}
+import io.netty.buffer.ByteBuf
+
+object AuthenticationStartupParser extends MessageParser {
+
+ val AuthenticationOk = 0
+ val AuthenticationKerberosV5 = 2
+ val AuthenticationCleartextPassword = 3
+ val AuthenticationMD5Password = 5
+ val AuthenticationSCMCredential = 6
+ val AuthenticationGSS = 7
+ val AuthenticationGSSContinue = 8
+ val AuthenticationSSPI = 9
+
+ override def parseMessage(b: ByteBuf): ServerMessage = {
+
+ val authenticationType = b.readInt()
+
+ authenticationType match {
+ case AuthenticationOk => AuthenticationOkMessage.Instance
+ case AuthenticationCleartextPassword => AuthenticationChallengeCleartextMessage.Instance
+ case AuthenticationMD5Password => {
+ val bytes = new Array[Byte](b.readableBytes())
+ b.readBytes(bytes)
+ new AuthenticationChallengeMD5(bytes)
+ }
+ case _ => {
+ throw new UnsupportedAuthenticationMethodException(authenticationType)
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/BackendKeyDataParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/BackendKeyDataParser.scala
new file mode 100644
index 00000000..772b69d3
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/BackendKeyDataParser.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{ProcessData, ServerMessage}
+import io.netty.buffer.ByteBuf
+
+object BackendKeyDataParser extends MessageParser {
+
+ override def parseMessage(b: ByteBuf): ServerMessage = {
+ new ProcessData(b.readInt(), b.readInt())
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/CommandCompleteParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/CommandCompleteParser.scala
new file mode 100644
index 00000000..04c9edb4
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/CommandCompleteParser.scala
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{CommandCompleteMessage, ServerMessage}
+import com.github.mauricio.async.db.util.ByteBufferUtils
+import java.nio.charset.Charset
+import io.netty.buffer.ByteBuf
+
+class CommandCompleteParser(charset: Charset) extends MessageParser {
+
+ override def parseMessage(b: ByteBuf): ServerMessage = {
+
+ val result = ByteBufferUtils.readCString(b, charset)
+
+ val indexOfRowCount = result.lastIndexOf(" ")
+
+ val rowCount = if (indexOfRowCount == -1) {
+ 0
+ } else {
+ try {
+ result.substring(indexOfRowCount).trim.toInt
+ } catch {
+ case e: NumberFormatException => {
+ 0
+ }
+ }
+ }
+
+ new CommandCompleteMessage(rowCount, result)
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/DataRowParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/DataRowParser.scala
new file mode 100644
index 00000000..3e2b8b1d
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/DataRowParser.scala
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{DataRowMessage, ServerMessage}
+import io.netty.buffer.ByteBuf
+
+object DataRowParser extends MessageParser {
+
+ def parseMessage(buffer: ByteBuf): ServerMessage = {
+
+ val row = new Array[ByteBuf](buffer.readShort())
+
+ 0.until(row.length).foreach {
+ column =>
+ val length = buffer.readInt()
+
+ row(column) = if (length == -1) {
+ null
+ } else {
+ buffer.readBytes(length)
+ }
+ }
+
+ new DataRowMessage(row)
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ErrorParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ErrorParser.scala
new file mode 100644
index 00000000..8c3655d6
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ErrorParser.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{ErrorMessage, ServerMessage}
+import java.nio.charset.Charset
+
+class ErrorParser(charset: Charset) extends InformationParser(charset) {
+
+ def createMessage(fields: Map[Char, String]): ServerMessage = new ErrorMessage(fields)
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/InformationParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/InformationParser.scala
new file mode 100644
index 00000000..48b39cec
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/InformationParser.scala
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+import com.github.mauricio.async.db.util.ByteBufferUtils
+import java.nio.charset.Charset
+import io.netty.buffer.ByteBuf
+
+abstract class InformationParser(charset: Charset) extends MessageParser {
+
+ override def parseMessage(b: ByteBuf): ServerMessage = {
+
+ val fields = scala.collection.mutable.Map[Char, String]()
+
+ while (b.isReadable()) {
+ val kind = b.readByte()
+
+ if (kind != 0) {
+ fields.put(
+ kind.toChar,
+ ByteBufferUtils.readCString(b, charset)
+ )
+ }
+
+ }
+
+ createMessage(fields.toMap)
+ }
+
+ def createMessage(fields: Map[Char, String]): ServerMessage
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/MessageParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/MessageParser.scala
new file mode 100644
index 00000000..176c3f4d
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/MessageParser.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.ServerMessage
+import io.netty.buffer.ByteBuf
+
+trait MessageParser {
+
+ def parseMessage(buffer: ByteBuf): ServerMessage
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/MessageParsersRegistry.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/MessageParsersRegistry.scala
new file mode 100644
index 00000000..9346a1e9
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/MessageParsersRegistry.scala
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.exceptions.ParserNotAvailableException
+import com.github.mauricio.async.db.postgresql.messages.backend._
+import java.nio.charset.Charset
+import io.netty.buffer.ByteBuf
+
+class MessageParsersRegistry(charset: Charset) {
+
+ private val commandCompleteParser = new CommandCompleteParser(charset)
+ private val errorParser = new ErrorParser(charset)
+ private val noticeParser = new NoticeParser(charset)
+ private val parameterStatusParser = new ParameterStatusParser(charset)
+ private val rowDescriptionParser = new RowDescriptionParser(charset)
+ private val notificationResponseParser = new NotificationResponseParser(charset)
+
+ private def parserFor(t: Byte): MessageParser = {
+ t match {
+ case ServerMessage.Authentication => AuthenticationStartupParser
+ case ServerMessage.BackendKeyData => BackendKeyDataParser
+ case ServerMessage.BindComplete => ReturningMessageParser.BindCompleteMessageParser
+ case ServerMessage.CloseComplete => ReturningMessageParser.CloseCompleteMessageParser
+ case ServerMessage.CommandComplete => this.commandCompleteParser
+ case ServerMessage.DataRow => DataRowParser
+ case ServerMessage.Error => this.errorParser
+ case ServerMessage.EmptyQueryString => ReturningMessageParser.EmptyQueryStringMessageParser
+ case ServerMessage.NoData => ReturningMessageParser.NoDataMessageParser
+ case ServerMessage.Notice => this.noticeParser
+ case ServerMessage.NotificationResponse => this.notificationResponseParser
+ case ServerMessage.ParameterStatus => this.parameterStatusParser
+ case ServerMessage.ParseComplete => ReturningMessageParser.ParseCompleteMessageParser
+ case ServerMessage.RowDescription => this.rowDescriptionParser
+ case ServerMessage.ReadyForQuery => ReadyForQueryParser
+ case _ => throw new ParserNotAvailableException(t)
+ }
+ }
+
+ def parse(t: Byte, b: ByteBuf): ServerMessage = {
+ this.parserFor(t).parseMessage(b)
+ }
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/NoticeParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/NoticeParser.scala
new file mode 100644
index 00000000..d0bf2b46
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/NoticeParser.scala
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{ServerMessage, NoticeMessage}
+import java.nio.charset.Charset
+
+class NoticeParser(charset: Charset) extends InformationParser(charset) {
+
+ def createMessage(fields: Map[Char, String]): ServerMessage = new NoticeMessage(fields)
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/NotificationResponseParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/NotificationResponseParser.scala
new file mode 100644
index 00000000..e01dec7f
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/NotificationResponseParser.scala
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013-2014 db-async-common
+ *
+ * The db-async-common project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import language.implicitConversions
+import java.nio.charset.Charset
+import io.netty.buffer.ByteBuf
+import com.github.mauricio.async.db.postgresql.messages.backend.{NotificationResponse, ServerMessage}
+import com.github.mauricio.async.db.util.ChannelWrapper.bufferToWrapper
+
+class NotificationResponseParser( charset : Charset ) extends MessageParser {
+
+ def parseMessage(buffer: ByteBuf): ServerMessage = {
+ new NotificationResponse( buffer.readInt(), buffer.readCString(charset), buffer.readCString(charset) )
+ }
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ParameterStatusParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ParameterStatusParser.scala
new file mode 100644
index 00000000..f1cb601e
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ParameterStatusParser.scala
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{ParameterStatusMessage, ServerMessage}
+import com.github.mauricio.async.db.util.ByteBufferUtils
+import java.nio.charset.Charset
+import io.netty.buffer.ByteBuf
+
+class ParameterStatusParser(charset: Charset) extends MessageParser {
+
+ import ByteBufferUtils._
+
+ override def parseMessage(b: ByteBuf): ServerMessage = {
+ new ParameterStatusMessage(readCString(b, charset), readCString(b, charset))
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ReadyForQueryParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ReadyForQueryParser.scala
new file mode 100644
index 00000000..70357bac
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ReadyForQueryParser.scala
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{ReadyForQueryMessage, ServerMessage}
+import io.netty.buffer.ByteBuf
+
+object ReadyForQueryParser extends MessageParser {
+
+ override def parseMessage(b: ByteBuf): ServerMessage = {
+ new ReadyForQueryMessage(b.readByte().asInstanceOf[Char])
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ReturningMessageParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ReturningMessageParser.scala
new file mode 100644
index 00000000..088da761
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/ReturningMessageParser.scala
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend._
+import io.netty.buffer.ByteBuf
+
+object ReturningMessageParser {
+
+ val BindCompleteMessageParser = new ReturningMessageParser(BindComplete)
+ val CloseCompleteMessageParser = new ReturningMessageParser(CloseComplete)
+ val EmptyQueryStringMessageParser = new ReturningMessageParser(EmptyQueryString)
+ val NoDataMessageParser = new ReturningMessageParser(NoData)
+ val ParseCompleteMessageParser = new ReturningMessageParser(ParseComplete)
+
+}
+
+class ReturningMessageParser(val message: ServerMessage) extends MessageParser {
+
+ def parseMessage(buffer: ByteBuf): ServerMessage = this.message
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/RowDescriptionParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/RowDescriptionParser.scala
new file mode 100644
index 00000000..260aaa7c
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/parsers/RowDescriptionParser.scala
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{RowDescriptionMessage, PostgreSQLColumnData, ServerMessage}
+import com.github.mauricio.async.db.util.ByteBufferUtils
+import java.nio.charset.Charset
+import io.netty.buffer.ByteBuf
+
+/**
+
+RowDescription (B)
+Byte1('T')
+Identifies the message as a row description.
+
+Int32
+Length of message contents in bytes, including self.
+
+Int16
+Specifies the number of fields in a row (can be zero).
+
+Then, for each field, there is the following:
+
+String
+The field name.
+
+Int32
+If the field can be identified as a column of a specific table, the object ID of the table; otherwise zero.
+
+Int16
+If the field can be identified as a column of a specific table, the attribute number of the column; otherwise zero.
+
+Int32
+The object ID of the field's data type.
+
+Int16
+The data type size (see pg_type.typlen). Note that negative values denote variable-width types.
+
+Int32
+The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific.
+
+Int16
+The format code being used for the field. Currently will be zero (text) or one (binary). In a RowDescription returned from the statement variant of Describe, the format code is not yet known and will always be zero.
+ *
+ */
+
+
+class RowDescriptionParser(charset: Charset) extends MessageParser {
+
+ override def parseMessage(b: ByteBuf): ServerMessage = {
+
+ val columnsCount = b.readShort()
+ val columns = new Array[PostgreSQLColumnData](columnsCount)
+
+ 0.until(columnsCount).foreach {
+ index =>
+ columns(index) = new PostgreSQLColumnData(
+ name = ByteBufferUtils.readCString(b, charset),
+ tableObjectId = b.readInt(),
+ columnNumber = b.readShort(),
+ dataType = b.readInt(),
+ dataTypeSize = b.readShort(),
+ dataTypeModifier = b.readInt(),
+ fieldFormat = b.readShort()
+ )
+ }
+
+ new RowDescriptionMessage(columns)
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/pool/PostgreSQLConnectionFactory.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/pool/PostgreSQLConnectionFactory.scala
new file mode 100644
index 00000000..ae3c5255
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/pool/PostgreSQLConnectionFactory.scala
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.pool
+
+import com.github.mauricio.async.db.Configuration
+import com.github.mauricio.async.db.exceptions.ConnectionTimeoutedException
+import com.github.mauricio.async.db.pool.ObjectFactory
+import com.github.mauricio.async.db.postgresql.PostgreSQLConnection
+import com.github.mauricio.async.db.util.Log
+import java.nio.channels.ClosedChannelException
+import scala.concurrent.Await
+import scala.concurrent.duration._
+import scala.language.postfixOps
+import scala.util.{Success, Failure, Try}
+import scala.concurrent.ExecutionContext
+import com.github.mauricio.async.db.util.ExecutorServiceUtils
+import com.github.mauricio.async.db.util.NettyUtils
+import io.netty.channel.EventLoopGroup
+
+object PostgreSQLConnectionFactory {
+ val log = Log.get[PostgreSQLConnectionFactory]
+}
+
+/**
+ *
+ * Object responsible for creating new connection instances.
+ *
+ * @param configuration
+ */
+
+class PostgreSQLConnectionFactory(
+ val configuration : Configuration,
+ group : EventLoopGroup = NettyUtils.DefaultEventLoopGroup,
+ executionContext : ExecutionContext = ExecutorServiceUtils.CachedExecutionContext ) extends ObjectFactory[PostgreSQLConnection] {
+
+ import PostgreSQLConnectionFactory.log
+
+ def create: PostgreSQLConnection = {
+ val connection = new PostgreSQLConnection(configuration, group = group, executionContext = executionContext)
+ Await.result(connection.connect, configuration.connectTimeout)
+
+ connection
+ }
+
+ def destroy(item: PostgreSQLConnection) {
+ item.disconnect
+ }
+
+ /**
+ *
+ * Validates by checking if the connection is still connected to the database or not.
+ *
+ * @param item an object produced by this pool
+ * @return
+ */
+
+ def validate( item : PostgreSQLConnection ) : Try[PostgreSQLConnection] = {
+ Try {
+ if ( item.isTimeouted ) {
+ throw new ConnectionTimeoutedException(item)
+ }
+ if ( !item.isConnected || item.hasRecentError ) {
+ throw new ClosedChannelException()
+ }
+ item.validateIfItIsReadyForQuery("Trying to give back a connection that is not ready for query")
+ item
+ }
+ }
+
+ /**
+ *
+ * Tests whether we can still send a **SELECT 0** statement to the database.
+ *
+ * @param item an object produced by this pool
+ * @return
+ */
+
+ override def test(item: PostgreSQLConnection): Try[PostgreSQLConnection] = {
+ val result : Try[PostgreSQLConnection] = Try({
+ Await.result( item.sendQuery("SELECT 0"), configuration.testTimeout )
+ item
+ })
+
+ result match {
+ case Failure(e) => {
+ try {
+ if ( item.isConnected ) {
+ item.disconnect
+ }
+ } catch {
+ case e : Exception => log.error("Failed disconnecting object", e)
+ }
+ result
+ }
+ case Success(i) => {
+ result
+ }
+ }
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ArrayStreamingParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ArrayStreamingParser.scala
new file mode 100644
index 00000000..75166e80
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ArrayStreamingParser.scala
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.util
+
+import com.github.mauricio.async.db.postgresql.exceptions.InvalidArrayException
+import com.github.mauricio.async.db.util.Log
+import scala.collection.mutable
+import scala.collection.mutable.StringBuilder
+
+object ArrayStreamingParser {
+
+ val log = Log.getByName(ArrayStreamingParser.getClass.getName)
+
+ def parse(content: String, delegate: ArrayStreamingParserDelegate) {
+
+ var index = 0
+ var escaping = false
+ var quoted = false
+ var currentElement: StringBuilder = null
+ var opens = 0
+ var closes = 0
+
+ while (index < content.size) {
+ val char = content.charAt(index)
+
+ if (escaping) {
+ currentElement.append(char)
+ escaping = false
+ } else {
+ char match {
+ case '{' if !quoted => {
+ delegate.arrayStarted
+ opens += 1
+ }
+ case '}' if !quoted => {
+ if (currentElement != null) {
+ sendElementEvent(currentElement, quoted, delegate)
+ currentElement = null
+ }
+ delegate.arrayEnded
+ closes += 1
+ }
+ case '"' => {
+ if (quoted) {
+ sendElementEvent(currentElement, quoted, delegate)
+ currentElement = null
+ quoted = false
+ } else {
+ quoted = true
+ currentElement = new mutable.StringBuilder()
+ }
+ }
+ case ',' if !quoted => {
+ if (currentElement != null) {
+ sendElementEvent(currentElement, quoted, delegate)
+ }
+ currentElement = null
+ }
+ case '\\' => {
+ escaping = true
+ }
+ case _ => {
+ if (currentElement == null) {
+ currentElement = new mutable.StringBuilder()
+ }
+ currentElement.append(char)
+ }
+ }
+ }
+
+ index += 1
+ }
+
+ if (opens != closes) {
+ throw new InvalidArrayException("This array is unbalanced %s".format(content))
+ }
+
+ }
+
+ def sendElementEvent(builder: mutable.StringBuilder, quoted: Boolean, delegate: ArrayStreamingParserDelegate) {
+
+ val value = builder.toString()
+
+ if (!quoted && "NULL".equalsIgnoreCase(value)) {
+ delegate.nullElementFound
+ } else {
+ delegate.elementFound(value)
+ }
+
+ }
+
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ArrayStreamingParserDelegate.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ArrayStreamingParserDelegate.scala
new file mode 100644
index 00000000..1d167e5f
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/ArrayStreamingParserDelegate.scala
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.util
+
+trait ArrayStreamingParserDelegate {
+
+ def arrayStarted: Unit = {}
+
+ def arrayEnded: Unit = {}
+
+ def elementFound(element: String): Unit = {}
+
+ def nullElementFound: Unit = {}
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/PasswordHelper.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/PasswordHelper.scala
new file mode 100644
index 00000000..d532b557
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/PasswordHelper.scala
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.util
+
+import java.nio.charset.Charset
+import java.security.MessageDigest
+
+object PasswordHelper {
+
+ private final val Lookup = Array[Byte]('0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'a', 'b', 'c', 'd', 'e', 'f')
+
+ private def bytesToHex( bytes : Array[Byte], hex : Array[Byte], offset : Int ) {
+
+ var pos = offset
+ var i = 0
+
+ while ( i < 16 ) {
+ val c = bytes(i) & 0xff
+ var j = c >> 4
+ hex(pos) = Lookup(j)
+ pos += 1
+ j = (c & 0xf)
+
+ hex(pos) = Lookup(j)
+ pos += 1
+
+ i += 1
+ }
+
+ }
+
+ def encode( userText : String, passwordText : String, salt : Array[Byte], charset : Charset ) : Array[Byte] = {
+ val user = userText.getBytes(charset)
+ val password = passwordText.getBytes(charset)
+
+ val md = MessageDigest.getInstance("MD5")
+
+ md.update(password)
+ md.update(user)
+
+ val tempDigest = md.digest()
+
+ val hexDigest = new Array[Byte](35)
+
+ bytesToHex(tempDigest, hexDigest, 0)
+ md.update(hexDigest, 0, 32)
+ md.update(salt)
+
+ val passDigest = md.digest()
+
+ bytesToHex(passDigest, hexDigest, 3)
+
+ hexDigest(0) = 'm'
+ hexDigest(1) = 'd'
+ hexDigest(2) = '5'
+
+ hexDigest
+ }
+
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala
new file mode 100644
index 00000000..fcb9b3cf
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/URLParser.scala
@@ -0,0 +1,72 @@
+/**
+ *
+ */
+package com.github.mauricio.async.db.postgresql.util
+
+import java.net.URI
+import java.nio.charset.Charset
+
+import com.github.mauricio.async.db.{Configuration, SSLConfiguration}
+import com.github.mauricio.async.db.util.AbstractURIParser
+
+/**
+ * The PostgreSQL URL parser.
+ */
+object URLParser extends AbstractURIParser {
+ import AbstractURIParser._
+
+ // Alias these for anyone still making use of them
+ @deprecated("Use com.github.mauricio.async.db.AbstractURIParser.PORT", since = "0.2.20")
+ val PGPORT = PORT
+
+ @deprecated("Use com.github.mauricio.async.db.AbstractURIParser.DBNAME", since = "0.2.20")
+ val PGDBNAME = DBNAME
+
+ @deprecated("Use com.github.mauricio.async.db.AbstractURIParser.HOST", since = "0.2.20")
+ val PGHOST = HOST
+
+ @deprecated("Use com.github.mauricio.async.db.AbstractURIParser.USERNAME", since = "0.2.20")
+ val PGUSERNAME = USERNAME
+
+ @deprecated("Use com.github.mauricio.async.db.AbstractURIParser.PASSWORD", since = "0.2.20")
+ val PGPASSWORD = PASSWORD
+
+ @deprecated("Use com.github.mauricio.async.db.postgresql.util.URLParser.DEFAULT.port", since = "0.2.20")
+ val DEFAULT_PORT = "5432"
+
+ /**
+ * The default configuration for PostgreSQL.
+ */
+ override val DEFAULT = Configuration(
+ username = "postgres",
+ host = "localhost",
+ port = 5432,
+ password = None,
+ database = None,
+ ssl = SSLConfiguration()
+ )
+
+ override protected val SCHEME = "^postgres(?:ql)?$".r
+
+ private val simplePGDB = "^postgresql:(\\w+)$".r
+
+ override protected def handleJDBC(uri: URI): Map[String, String] = uri.getSchemeSpecificPart match {
+ case simplePGDB(db) => Map(DBNAME -> db)
+ case x => parse(new URI(x))
+ }
+
+ /**
+ * Assembles a configuration out of the provided property map. This is the generic form, subclasses may override to
+ * handle additional properties.
+ *
+ * @param properties the extracted properties from the URL.
+ * @param charset the charset passed in to parse or parseOrDie.
+ * @return
+ */
+ override protected def assembleConfiguration(properties: Map[String, String], charset: Charset): Configuration = {
+ // Add SSL Configuration
+ super.assembleConfiguration(properties, charset).copy(
+ ssl = SSLConfiguration(properties)
+ )
+ }
+}
diff --git a/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/package.scala b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/package.scala
new file mode 100644
index 00000000..5d321170
--- /dev/null
+++ b/postgresql-async/src/main/scala/com/github/mauricio/async/db/postgresql/util/package.scala
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package com.github.mauricio.async.db.postgresql
+
+/**
+ * Contains package level aliases and type renames.
+ */
+package object util {
+
+ /**
+ * Alias to help compatibility.
+ */
+ @deprecated("Use com.github.mauricio.async.db.postgresql.util.URLParser", since = "0.2.20")
+ val ParserURL = URLParser
+
+}
diff --git a/postgresql-async/src/test/resources/logback.xml b/postgresql-async/src/test/resources/logback.xml
new file mode 100644
index 00000000..3ddb1518
--- /dev/null
+++ b/postgresql-async/src/test/resources/logback.xml
@@ -0,0 +1,21 @@
+
+
+
+
+ [%level][%thread][%d] %msg%ex%n
+
+
+
+
+ target/postgresql-async-tests.log
+
+ [%level][%thread][%d] %msg%ex%n
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/examples/BasicExample.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/examples/BasicExample.scala
new file mode 100644
index 00000000..5f31d677
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/examples/BasicExample.scala
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.examples
+
+import com.github.mauricio.async.db.postgresql.PostgreSQLConnection
+import com.github.mauricio.async.db.postgresql.util.URLParser
+import com.github.mauricio.async.db.util.ExecutorServiceUtils.CachedExecutionContext
+import com.github.mauricio.async.db.{RowData, QueryResult, Connection}
+import scala.concurrent.duration._
+import scala.concurrent.{Await, Future}
+import scala.language.postfixOps
+
+object BasicExample {
+
+ def main(args: Array[String]) {
+
+ val configuration = URLParser.parse("jdbc:postgresql://localhost:5233/my_database?username=postgres&password=somepassword")
+ val connection: Connection = new PostgreSQLConnection(configuration)
+
+ Await.result(connection.connect, 5 seconds)
+
+ val future: Future[QueryResult] = connection.sendQuery("SELECT 0")
+
+ val mapResult: Future[Any] = future.map(queryResult => queryResult.rows match {
+ case Some(resultSet) => {
+ val row : RowData = resultSet.head
+ row(0)
+ }
+ case None => -1
+ }
+ )
+
+ val result = Await.result( mapResult, 5 seconds )
+
+ println(result)
+
+ connection.disconnect
+
+ }
+
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/general/MutableResultSetSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/general/MutableResultSetSpec.scala
new file mode 100644
index 00000000..78d36046
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/general/MutableResultSetSpec.scala
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.general
+
+import com.github.mauricio.async.db.postgresql.column.{PostgreSQLColumnDecoderRegistry, ColumnTypes}
+import com.github.mauricio.async.db.postgresql.messages.backend.PostgreSQLColumnData
+import org.specs2.mutable.Specification
+import io.netty.util.CharsetUtil
+import io.netty.buffer.{Unpooled, ByteBuf}
+
+class MutableResultSetSpec extends Specification {
+
+ val charset = CharsetUtil.UTF_8
+ val decoder = new PostgreSQLColumnDecoderRegistry
+
+ def create(name: String, dataType: Int, columnNumber: Int = 0, dataTypeSize: Int = -1) = new PostgreSQLColumnData(
+ name = name,
+ tableObjectId = 0,
+ columnNumber = columnNumber,
+ dataType = dataType,
+ dataTypeSize = dataTypeSize,
+ dataTypeModifier = 0,
+ fieldFormat = 0
+ )
+
+ "result set" should {
+
+ "correctly map column data to fields" in {
+
+ val columns = Array(
+ create(
+ name = "id",
+ dataType = ColumnTypes.Integer,
+ dataTypeSize = 4
+ ),
+ create(
+ name = "name",
+ columnNumber = 5,
+ dataType = ColumnTypes.Varchar
+ )
+ )
+
+ val text = "some data"
+ val otherText = "some other data"
+
+ val resultSet = new MutableResultSet(columns)
+
+ resultSet.addRow(Array(1, text))
+ resultSet.addRow(Array(2, otherText))
+
+ resultSet(0)(0) === 1
+ resultSet(0)("id") === 1
+
+ resultSet(0)(1) === text
+ resultSet(0)("name") === text
+
+ resultSet(1)(0) === 2
+ resultSet(1)("id") === 2
+
+ resultSet(1)(1) === otherText
+ resultSet(1)("name") === otherText
+
+ }
+
+ "should return the same order as the one given by columns" in {
+
+ val columns = Array(
+ create("id", ColumnTypes.Integer),
+ create("name", ColumnTypes.Varchar),
+ create("birthday", ColumnTypes.Date),
+ create("created_at", ColumnTypes.Timestamp),
+ create("updated_at", ColumnTypes.Timestamp)
+ )
+ val resultSet = new MutableResultSet(columns)
+
+ resultSet.columnNames must contain(allOf("id", "name", "birthday", "created_at", "updated_at")).inOrder
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ArrayTypesSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ArrayTypesSpec.scala
new file mode 100644
index 00000000..5391588c
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ArrayTypesSpec.scala
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql
+
+import com.github.mauricio.async.db.column.{TimestampWithTimezoneEncoderDecoder, InetAddressEncoderDecoder}
+import org.specs2.mutable.Specification
+import java.net.InetAddress
+
+class ArrayTypesSpec extends Specification with DatabaseTestHelper {
+ // `uniq` allows sbt to run the tests concurrently as there is no CREATE TEMP TYPE
+ def simpleCreate(uniq: String) = s"""DROP TYPE IF EXISTS dir_$uniq;
+ CREATE TYPE direction_$uniq AS ENUM ('in','out');
+ DROP TYPE IF EXISTS endpoint_$uniq;
+ CREATE TYPE endpoint_$uniq AS (ip inet, port integer);
+ create temp table type_test_table_$uniq (
+ bigserial_column bigserial not null,
+ smallint_column integer[] not null,
+ text_column text[] not null,
+ inet_column inet[] not null,
+ direction_column direction_$uniq[] not null,
+ endpoint_column endpoint_$uniq[] not null,
+ timestamp_column timestamp with time zone[] not null,
+ constraint bigserial_column_pkey primary key (bigserial_column)
+ )"""
+ def simpleDrop(uniq: String) = s"""drop table if exists type_test_table_$uniq;
+ drop type if exists endpoint_$uniq;
+ drop type if exists direction_$uniq"""
+
+ val insert =
+ """insert into type_test_table_cptat
+ (smallint_column, text_column, inet_column, direction_column, endpoint_column, timestamp_column)
+ values (
+ '{1,2,3,4}',
+ '{"some,\"comma,separated,text","another line of text","fake\,backslash","real\\,backslash\\",NULL}',
+ '{"127.0.0.1","2002:15::1"}',
+ '{"in","out"}',
+ '{"(\"127.0.0.1\",80)","(\"2002:15::1\",443)"}',
+ '{"2013-04-06 01:15:10.528-03","2013-04-06 01:15:08.528-03"}'
+ )"""
+
+ val insertPreparedStatement = """insert into type_test_table_csaups
+ (smallint_column, text_column, inet_column, direction_column, endpoint_column, timestamp_column)
+ values (?,?,?,?,?,?)"""
+
+ "connection" should {
+
+ "correctly parse the array type" in {
+
+ withHandler {
+ handler =>
+ try {
+ executeDdl(handler, simpleCreate("cptat"))
+ executeDdl(handler, insert, 1)
+ val result = executeQuery(handler, "select * from type_test_table_cptat").rows.get
+ result(0)("smallint_column") === List(1,2,3,4)
+ result(0)("text_column") === List("some,\"comma,separated,text", "another line of text", "fake,backslash", "real\\,backslash\\", null )
+ result(0)("timestamp_column") === List(
+ TimestampWithTimezoneEncoderDecoder.decode("2013-04-06 01:15:10.528-03"),
+ TimestampWithTimezoneEncoderDecoder.decode("2013-04-06 01:15:08.528-03")
+ )
+ } finally {
+ executeDdl(handler, simpleDrop("cptat"))
+ }
+ }
+
+ }
+
+ "correctly send arrays using prepared statements" in {
+ case class Endpoint(ip: InetAddress, port: Int)
+
+ val timestamps = List(
+ TimestampWithTimezoneEncoderDecoder.decode("2013-04-06 01:15:10.528-03"),
+ TimestampWithTimezoneEncoderDecoder.decode("2013-04-06 01:15:08.528-03")
+ )
+ val inets = List(
+ InetAddressEncoderDecoder.decode("127.0.0.1"),
+ InetAddressEncoderDecoder.decode("2002:15::1")
+ )
+ val directions = List("in", "out")
+ val endpoints = List(
+ Endpoint(InetAddress.getByName("127.0.0.1"), 80), // case class
+ (InetAddress.getByName("2002:15::1"), 443) // tuple
+ )
+ val numbers = List(1,2,3,4)
+ val texts = List("some,\"comma,separated,text", "another line of text", "fake,backslash", "real\\,backslash\\", null )
+
+ withHandler {
+ handler =>
+ try {
+ executeDdl(handler, simpleCreate("csaups"))
+ executePreparedStatement(
+ handler,
+ this.insertPreparedStatement,
+ Array( numbers, texts, inets, directions, endpoints, timestamps ) )
+
+ val result = executeQuery(handler, "select * from type_test_table_csaups").rows.get
+
+ result(0)("smallint_column") === numbers
+ result(0)("text_column") === texts
+ result(0)("inet_column") === inets
+ result(0)("direction_column") === "{in,out}" // user type decoding not supported
+ result(0)("endpoint_column") === """{"(127.0.0.1,80)","(2002:15::1,443)"}""" // user type decoding not supported
+ result(0)("timestamp_column") === timestamps
+ } finally {
+ executeDdl(handler, simpleDrop("csaups"))
+ }
+ }
+
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/BitSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/BitSpec.scala
new file mode 100644
index 00000000..8c17f9af
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/BitSpec.scala
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql
+
+import org.specs2.mutable.Specification
+
+class BitSpec extends Specification with DatabaseTestHelper {
+
+ "when processing bit columns" should {
+
+ "result in binary data" in {
+
+ withHandler {
+ handler =>
+ val create = """CREATE TEMP TABLE binary_test
+ (
+ id bigserial NOT NULL,
+ some_bit BYTEA NOT NULL,
+ CONSTRAINT bigserial_column_pkey PRIMARY KEY (id)
+ )"""
+
+ executeDdl(handler, create)
+ executePreparedStatement(handler,
+ "INSERT INTO binary_test (some_bit) VALUES (E'\\\\000'),(E'\\\\001')")
+
+ val rows = executePreparedStatement(handler, "select * from binary_test").rows.get
+
+ val bit0 = rows(0)("some_bit")
+ val bit1 = rows(1)("some_bit")
+
+ bit0 === Array(0)
+ bit1 === Array(1)
+ }
+
+ }
+
+ "result in binary data in BIT(2) column" in {
+
+ withHandler {
+ handler =>
+ val create = """CREATE TEMP TABLE binary_test
+ (
+ id bigserial NOT NULL,
+ some_bit BYTEA NOT NULL,
+ CONSTRAINT bigserial_column_pkey PRIMARY KEY (id)
+ )"""
+
+ executeDdl(handler, create)
+ executePreparedStatement(handler,
+ "INSERT INTO binary_test (some_bit) VALUES (E'\\\\000'),(E'\\\\001'),(E'\\\\002'),(E'\\\\003')")
+
+ val rows = executePreparedStatement(handler, "select * from binary_test").rows.get
+
+ val bit0 = rows(0)("some_bit")
+ val bit1 = rows(1)("some_bit")
+ val bit2 = rows(2)("some_bit")
+ val bit3 = rows(3)("some_bit")
+
+ bit0 === Array(0)
+ bit1 === Array(1)
+ bit2 === Array(2)
+ bit3 === Array(3)
+ }
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/DatabaseTestHelper.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/DatabaseTestHelper.scala
new file mode 100644
index 00000000..2659d372
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/DatabaseTestHelper.scala
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql
+
+import com.github.mauricio.async.db.util.Log
+import com.github.mauricio.async.db.{Connection, Configuration}
+import java.io.File
+import java.util.concurrent.{TimeoutException, TimeUnit}
+import scala.concurrent.duration._
+import scala.concurrent.{Future, Await}
+import com.github.mauricio.async.db.SSLConfiguration
+import com.github.mauricio.async.db.SSLConfiguration.Mode
+
+object DatabaseTestHelper {
+ val log = Log.get[DatabaseTestHelper]
+}
+
+trait DatabaseTestHelper {
+
+
+ def databaseName = Some("netty_driver_test")
+
+ def timeTestDatabase = Some("netty_driver_time_test")
+
+ def databasePort = 5432
+
+ def defaultConfiguration = new Configuration(
+ port = databasePort,
+ username = "postgres",
+ database = databaseName)
+
+ def timeTestConfiguration = new Configuration(
+ port = databasePort,
+ username = "postgres",
+ database = timeTestDatabase)
+
+ def withHandler[T](fn: (PostgreSQLConnection) => T): T = {
+ withHandler(this.defaultConfiguration, fn)
+ }
+
+ def withTimeHandler[T](fn: (PostgreSQLConnection) => T): T = {
+ withHandler(this.timeTestConfiguration, fn)
+ }
+
+ def withSSLHandler[T](mode: SSLConfiguration.Mode.Value, host: String = "localhost", rootCert: Option[File] = Some(new File("script/server.crt")))(fn: (PostgreSQLConnection) => T): T = {
+ val config = new Configuration(
+ host = host,
+ port = databasePort,
+ username = "postgres",
+ database = databaseName,
+ ssl = SSLConfiguration(mode = mode, rootCert = rootCert))
+ withHandler(config, fn)
+ }
+
+ def withHandler[T](configuration: Configuration, fn: (PostgreSQLConnection) => T): T = {
+
+ val handler = new PostgreSQLConnection(configuration)
+
+ try {
+ Await.result(handler.connect, Duration(5, SECONDS))
+ fn(handler)
+ } finally {
+ handleTimeout(handler, handler.disconnect)
+ }
+
+ }
+
+ def executeDdl(handler: Connection, data: String, count: Int = 0) = {
+ val rows = handleTimeout(handler, {
+ Await.result(handler.sendQuery(data), Duration(5, SECONDS)).rowsAffected
+ })
+
+ if (rows != count) {
+ throw new IllegalStateException("We expected %s rows but there were %s".format(count, rows))
+ }
+
+ rows
+ }
+
+ private def handleTimeout[R]( handler : Connection, fn : => R ) = {
+ try {
+ fn
+ } catch {
+ case e : TimeoutException => {
+ throw new IllegalStateException("Timeout executing call from handler -> %s".format( handler))
+ }
+ }
+ }
+
+ def executeQuery(handler: Connection, data: String) = {
+ handleTimeout( handler, {
+ Await.result(handler.sendQuery(data), Duration(5, SECONDS))
+ } )
+ }
+
+ def executePreparedStatement(
+ handler: Connection,
+ statement: String,
+ values: Array[Any] = Array.empty[Any]) = {
+ handleTimeout( handler, {
+ Await.result(handler.sendPreparedStatement(statement, values), Duration(5, SECONDS))
+ } )
+ }
+
+ def await[T](future: Future[T]): T = {
+ Await.result(future, Duration(5, TimeUnit.SECONDS))
+ }
+
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ListenNotifySpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ListenNotifySpec.scala
new file mode 100644
index 00000000..b1d72097
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/ListenNotifySpec.scala
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2013-2014 db-async-common
+ *
+ * The db-async-common project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql
+
+import org.specs2.mutable.Specification
+import java.util.UUID
+import com.github.mauricio.async.db.postgresql.messages.backend.NotificationResponse
+
+class ListenNotifySpec extends Specification with DatabaseTestHelper {
+
+ def generateQueueName() = "scala_pg_async_test_" + UUID.randomUUID().toString.replaceAll("-", "")
+
+ "connection" should {
+
+ "should be able to receive a notification if listening" in {
+
+ withHandler {
+ connection =>
+
+ val queue = generateQueueName()
+
+ executeQuery(connection, s"LISTEN $queue")
+
+ var payload = ""
+ var channel = ""
+
+ connection.registerNotifyListener((message) => {
+ payload = message.payload
+ channel = message.channel
+ })
+
+ executeQuery(connection, s"NOTIFY $queue, 'this-is-some-data'")
+
+ Thread.sleep(1000)
+
+ payload === "this-is-some-data"
+ channel === queue
+
+ connection.hasRecentError must beFalse
+ }
+
+ }
+
+ "should be able to receive a notification from a pg_notify call" in {
+
+ withHandler {
+ connection =>
+ val queue = generateQueueName()
+
+ executeQuery(connection, s"LISTEN $queue")
+
+ var payload = ""
+ var channel = ""
+
+ connection.registerNotifyListener((message) => {
+ payload = message.payload
+ channel = message.channel
+ })
+
+ executePreparedStatement(connection, "SELECT pg_notify(?, ?)", Array(queue, "notifying-again"))
+
+ Thread.sleep(1000)
+
+ payload === "notifying-again"
+ channel === queue
+ }
+
+ }
+
+ "should not receive any notification if not registered to the correct channel" in {
+
+ withHandler {
+ connection =>
+
+ var queue = generateQueueName()
+ var otherQueue = generateQueueName()
+
+ executeQuery(connection, s"LISTEN $queue")
+
+ var payload = ""
+ var channel = ""
+
+ connection.registerNotifyListener((message) => {
+ payload = message.payload
+ channel = message.channel
+ })
+
+ executePreparedStatement(connection, "SELECT pg_notify(?, ?)", Array(otherQueue, "notifying-again"))
+
+ Thread.sleep(1000)
+
+ payload === ""
+ channel === ""
+ }
+
+ }
+
+ "should not receive notifications if cleared the collection" in {
+
+ withHandler {
+ connection =>
+ val queue = generateQueueName()
+
+ executeQuery(connection, s"LISTEN $queue")
+
+ var payload = ""
+ var channel = ""
+
+ connection.registerNotifyListener((message) => {
+ payload = message.payload
+ channel = message.channel
+ })
+
+ connection.clearNotifyListeners()
+
+ executeQuery(connection, s"NOTIFY $queue, 'this-is-some-data'")
+
+ Thread.sleep(1000)
+
+ payload === ""
+ channel === ""
+ }
+
+ }
+
+ "should not receive notification if listener was removed" in {
+
+ withHandler {
+ connection =>
+ val queue = generateQueueName()
+
+ executeQuery(connection, s"LISTEN $queue")
+
+ var payload = ""
+ var channel = ""
+
+ val listener : NotificationResponse => Unit = (message) => {
+ payload = message.payload
+ channel = message.channel
+ }
+
+ connection.registerNotifyListener(listener)
+ connection.unregisterNotifyListener(listener)
+
+ executeQuery(connection, s"NOTIFY $queue, 'this-is-some-data'")
+
+ Thread.sleep(1000)
+
+ payload === ""
+ channel === ""
+ }
+
+ }
+
+ "should be able to receive notify without payload" in {
+ withHandler {
+ connection =>
+ val queue = generateQueueName()
+
+ executeQuery(connection, s"LISTEN $queue")
+
+ var payload = "this is some fake payload"
+ var channel = ""
+
+ val listener : NotificationResponse => Unit = (message) => {
+ payload = message.payload
+ channel = message.channel
+ }
+
+ connection.registerNotifyListener(listener)
+
+ executeQuery(connection, s"NOTIFY $queue")
+
+ Thread.sleep(1000)
+
+ payload === ""
+ channel === queue
+ }
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/MessageDecoderSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/MessageDecoderSpec.scala
new file mode 100644
index 00000000..a033e3ee
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/MessageDecoderSpec.scala
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.postgresql
+
+import com.github.mauricio.async.db.postgresql.codec.MessageDecoder
+import com.github.mauricio.async.db.postgresql.exceptions.{MessageTooLongException}
+import com.github.mauricio.async.db.postgresql.messages.backend.{ServerMessage, ErrorMessage}
+import org.specs2.mutable.Specification
+import com.github.mauricio.async.db.exceptions.NegativeMessageSizeException
+import io.netty.util.CharsetUtil
+import io.netty.buffer.Unpooled
+import java.util
+
+class MessageDecoderSpec extends Specification {
+
+ val decoder = new MessageDecoder(false, CharsetUtil.UTF_8)
+
+ "message decoder" should {
+
+ "not try to decode if there is not enought data available" in {
+
+ val buffer = Unpooled.buffer()
+
+ buffer.writeByte('R')
+ buffer.writeByte(1)
+ buffer.writeByte(2)
+ val out = new util.ArrayList[Object]()
+
+ this.decoder.decode(null, buffer, out)
+ out.isEmpty
+ }
+
+ "should not try to decode if there is a type and lenght but it's not long enough" in {
+
+ val buffer = Unpooled.buffer()
+
+ buffer.writeByte('R')
+ buffer.writeInt(30)
+ buffer.writeBytes("my-name".getBytes(CharsetUtil.UTF_8))
+
+ val out = new util.ArrayList[Object]()
+ this.decoder.decode(null, buffer, out)
+ buffer.readerIndex() === 0
+ }
+
+ "should correctly decode a message" in {
+
+ val buffer = Unpooled.buffer()
+ val text = "This is an error message"
+ val textBytes = text.getBytes(CharsetUtil.UTF_8)
+
+ buffer.writeByte('E')
+ buffer.writeInt(textBytes.length + 4 + 1 + 1)
+ buffer.writeByte('M')
+ buffer.writeBytes(textBytes)
+ buffer.writeByte(0)
+ val out = new util.ArrayList[Object]()
+ this.decoder.decode(null, buffer, out)
+ out.size === 1
+ val result = out.get(0).asInstanceOf[ErrorMessage]
+ result.message === text
+ buffer.readerIndex() === (textBytes.length + 4 + 1 + 1 + 1)
+ }
+
+ "should raise an exception if the length is negative" in {
+ val buffer = Unpooled.buffer()
+ buffer.writeByte( ServerMessage.Close )
+ buffer.writeInt( 2 )
+ val out = new util.ArrayList[Object]()
+
+ this.decoder.decode(null, buffer, out) must throwA[NegativeMessageSizeException]
+ }
+
+ "should raise an exception if the length is too big" in {
+
+ val buffer = Unpooled.buffer()
+ buffer.writeByte( ServerMessage.Close )
+ buffer.writeInt( MessageDecoder.DefaultMaximumSize + 10 )
+ val out = new util.ArrayList[Object]()
+
+ this.decoder.decode(null, buffer, out) must throwA[MessageTooLongException]
+ }
+
+ }
+
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/NumericSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/NumericSpec.scala
new file mode 100644
index 00000000..ad38a64e
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/NumericSpec.scala
@@ -0,0 +1,57 @@
+package com.github.mauricio.async.db.postgresql
+
+import org.specs2.mutable.Specification
+
+class NumericSpec extends Specification with DatabaseTestHelper {
+
+ "when processing numeric columns" should {
+
+ "support first update of num column with floating" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, "CREATE TEMP TABLE numeric_test (id BIGSERIAL, numcol NUMERIC)")
+
+ val id = executePreparedStatement(handler, "INSERT INTO numeric_test DEFAULT VALUES RETURNING id").rows.get(0)("id")
+ executePreparedStatement(handler, "UPDATE numeric_test SET numcol = ? WHERE id = ?", Array[Any](123.123, id))
+ executePreparedStatement(handler, "UPDATE numeric_test SET numcol = ? WHERE id = ?", Array[Any](1234, id))
+ executePreparedStatement(handler, "UPDATE numeric_test SET numcol = ? WHERE id = ?", Array[Any](123.123, id))
+
+ id === 1
+ }
+
+ }
+
+ "support first update of num column with integer" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, "CREATE TEMP TABLE numeric_test (id BIGSERIAL, numcol NUMERIC)")
+
+ val id = executePreparedStatement(handler, "INSERT INTO numeric_test DEFAULT VALUES RETURNING id").rows.get(0)("id")
+ executePreparedStatement(handler, "UPDATE numeric_test SET numcol = ? WHERE id = ?", Array[Any](1234, id))
+ executePreparedStatement(handler, "UPDATE numeric_test SET numcol = ? WHERE id = ?", Array[Any](123.123, id))
+
+ id === 1
+ }
+
+ }
+
+ "support using first update with queries instead of prepared statements" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, "CREATE TEMP TABLE numeric_test (id BIGSERIAL, numcol NUMERIC)")
+
+ val id = executeQuery(handler, "INSERT INTO numeric_test DEFAULT VALUES RETURNING id").rows.get(0)("id")
+ executeQuery(handler, s"UPDATE numeric_test SET numcol = 1234 WHERE id = $id")
+ executeQuery(handler, s"UPDATE numeric_test SET numcol = 123.123 WHERE id = $id")
+
+ id === 1
+ }
+
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLColumnEncoderRegistrySpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLColumnEncoderRegistrySpec.scala
new file mode 100644
index 00000000..9e1b5e94
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLColumnEncoderRegistrySpec.scala
@@ -0,0 +1,61 @@
+package com.github.mauricio.async.db.postgresql
+
+import com.github.mauricio.async.db.postgresql.column.PostgreSQLColumnEncoderRegistry
+import org.specs2.mutable.Specification
+
+class PostgreSQLColumnEncoderRegistrySpec extends Specification {
+
+ val encoder = new PostgreSQLColumnEncoderRegistry
+
+ "column encoder registry" should {
+
+ "encode Some(value) like value" in {
+
+ val actual = encoder.encode(Some(1l))
+ val expected = encoder.encode(1l)
+
+ actual mustEqual expected
+ }
+
+ "encode Some(value) in list like value in list" in {
+
+ val actual = encoder.encode(List(Some(1l), Some("foo")))
+ val expected = encoder.encode(List(1l, "foo"))
+
+ actual mustEqual expected
+ }
+
+ "encode None as null" in {
+ val actual = encoder.encode(None)
+ val expected = encoder.encode(null)
+
+ actual mustEqual expected
+ }
+
+ "determine kindOf Some(value) like kindOf value" in {
+ val actual = encoder.kindOf(Some(1l))
+ val expected = encoder.kindOf(1l)
+
+ actual mustEqual expected
+ }
+
+ "determine kindOf None like kindOf null" in {
+ val actual = encoder.kindOf(None)
+ val expected = encoder.kindOf(null)
+
+ actual mustEqual expected
+ }
+
+ "encodes Some(null) as null" in {
+ val actual = encoder.encode(Some(null))
+ actual mustEqual null
+ }
+
+ "encodes null as null" in {
+ val actual = encoder.encode(null)
+ actual mustEqual null
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala
new file mode 100644
index 00000000..0e050477
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLConnectionSpec.scala
@@ -0,0 +1,450 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql
+
+import java.nio.ByteBuffer
+
+import com.github.mauricio.async.db.column.{DateEncoderDecoder, TimeEncoderDecoder, TimestampEncoderDecoder}
+import com.github.mauricio.async.db.exceptions.UnsupportedAuthenticationMethodException
+import com.github.mauricio.async.db.postgresql.exceptions.{GenericDatabaseException, QueryMustNotBeNullOrEmptyException}
+import com.github.mauricio.async.db.postgresql.messages.backend.InformationMessage
+import com.github.mauricio.async.db.util.Log
+import com.github.mauricio.async.db.{Configuration, Connection, QueryResult}
+import io.netty.buffer.Unpooled
+import org.joda.time.LocalDateTime
+import org.specs2.mutable.Specification
+
+import scala.concurrent.duration._
+import scala.concurrent.{Await, Future}
+import scala.concurrent.ExecutionContext.Implicits.global
+
+object PostgreSQLConnectionSpec {
+ val log = Log.get[PostgreSQLConnectionSpec]
+}
+
+class PostgreSQLConnectionSpec extends Specification with DatabaseTestHelper {
+
+ import PostgreSQLConnectionSpec.log
+
+ final val sampleArray = Array[Byte](83, 97, 121, 32, 72, 101, 108, 108, 111, 32, 116, 111, 32, 77, 121, 32, 76, 105, 116, 116, 108, 101, 32, 70, 114, 105, 101, 110, 100)
+
+ val create = """create temp table type_test_table (
+ bigserial_column bigserial not null,
+ smallint_column smallint not null,
+ integer_column integer not null,
+ decimal_column decimal(10,4),
+ real_column real,
+ double_column double precision,
+ serial_column serial not null,
+ varchar_column varchar(255),
+ text_column text,
+ timestamp_column timestamp,
+ date_column date,
+ time_column time,
+ boolean_column boolean,
+ constraint bigserial_column_pkey primary key (bigserial_column)
+ ) with oids"""
+
+ val insert = """insert into type_test_table (
+ smallint_column,
+ integer_column,
+ decimal_column,
+ real_column,
+ double_column,
+ varchar_column,
+ text_column,
+ timestamp_column,
+ date_column,
+ time_column,
+ boolean_column
+ )
+ VALUES (
+ 10,
+ 11,
+ 14.999,
+ 78.34,
+ 15.68,
+ 'this is a varchar field',
+ 'this is a long text field',
+ '1984-08-06 22:13:45.888888',
+ '1984-08-06',
+ '22:13:45.888888',
+ TRUE
+ )
+ """
+
+ val select = "select *, oid from type_test_table"
+
+ val preparedStatementCreate = """create temp table prepared_statement_test (
+ id bigserial not null,
+ name varchar(255) not null,
+ constraint bigserial_column_pkey primary key (id)
+ )"""
+
+ val preparedStatementInsert = " insert into prepared_statement_test (name) values ('John Doe')"
+ val preparedStatementInsert2 = " insert into prepared_statement_test (name) values ('Mary Jane')"
+ val preparedStatementInsert3 = " insert into prepared_statement_test (name) values ('Peter Parker')"
+ val preparedStatementInsertReturning = " insert into prepared_statement_test (name) values ('John Doe') returning id"
+ val preparedStatementSelect = "select * from prepared_statement_test"
+
+ "handler" should {
+
+ "connect to the database" in {
+
+ withHandler {
+ handler =>
+ handler.isReadyForQuery must beTrue
+ }
+
+ }
+
+ "create a table in the database" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, this.create) === 0
+ }
+
+ }
+
+ "insert a row in the database" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, this.create)
+ executeDdl(handler, this.insert, 1) === 1
+
+ }
+
+ }
+
+ "select rows in the database" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, this.create)
+ executeDdl(handler, this.insert, 1)
+ val result = executeQuery(handler, this.select)
+
+ val row = result.rows.get(0)
+
+ row(0) === 1
+ row(1) === 10
+ row(2) === 11
+ row(3).toString === "14.9990"
+ row(4).toString === 78.34.toString
+ row(5) === 15.68
+ row(6) === 1
+ row(7) === "this is a varchar field"
+ row(8) === "this is a long text field"
+ row(9) === TimestampEncoderDecoder.Instance.decode("1984-08-06 22:13:45.888888")
+ row(10) === DateEncoderDecoder.decode("1984-08-06")
+ row(11) === TimeEncoderDecoder.Instance.decode("22:13:45.888888")
+ row(12) === true
+ row(13).asInstanceOf[AnyRef] must beAnInstanceOf[java.lang.Long]
+ row(13).asInstanceOf[Long] must beGreaterThan(0L)
+
+
+ }
+
+ }
+
+ "select rows that has duplicate column names" in {
+
+ withHandler {
+ handler =>
+ val result = executeQuery(handler, "SELECT 1 COL, 2 COL")
+
+ val row = result.rows.get(0)
+
+ row(0) === 1
+ row(1) === 2
+
+ }
+
+ }
+
+ "execute a prepared statement" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, this.preparedStatementCreate)
+ executeDdl(handler, this.preparedStatementInsert, 1)
+ val result = executePreparedStatement(handler, this.preparedStatementSelect)
+
+ val row = result.rows.get(0)
+
+
+ row(0) === 1
+ row(1) === "John Doe"
+
+
+ }
+
+ }
+
+ "execute a prepared statement with parameters" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, this.preparedStatementCreate)
+ executeDdl(handler, this.preparedStatementInsert, 1)
+ executeDdl(handler, this.preparedStatementInsert2, 1)
+ executeDdl(handler, this.preparedStatementInsert3, 1)
+
+ val select = "select * from prepared_statement_test where name like ?"
+
+ val queryResult = executePreparedStatement(handler, select, Array("Peter Parker"))
+ val row = queryResult.rows.get(0)
+
+ val queryResult2 = executePreparedStatement(handler, select, Array("Mary Jane"))
+ val row2 = queryResult2.rows.get(0)
+
+ row(0) === 3
+ row(1) === "Peter Parker"
+
+ row2(0) === 2
+ row2(1) === "Mary Jane"
+
+ }
+
+ }
+
+ "login using MD5 authentication" in {
+
+ val configuration = new Configuration(
+ username = "postgres_md5",
+ password = Some("postgres_md5"),
+ port = databasePort,
+ database = databaseName
+ )
+
+ withHandler(configuration, {
+ handler =>
+ val result = executeQuery(handler, "SELECT 0")
+ result.rows.get.apply(0)(0) === 0
+ })
+
+ }
+
+ "login using cleartext authentication" in {
+
+ val configuration = new Configuration(
+ username = "postgres_cleartext",
+ password = Some("postgres_cleartext"),
+ port = databasePort,
+ database = databaseName
+ )
+
+ withHandler(configuration, {
+ handler =>
+ val result = executeQuery(handler, "SELECT 0")
+ result.rows.get(0)(0) === 0
+ })
+
+ }
+
+ "fail login using kerberos authentication" in {
+
+ val configuration = new Configuration(
+ username = "postgres_kerberos",
+ password = Some("postgres_kerberos"),
+ port = databasePort,
+ database = databaseName
+ )
+
+ withHandler(configuration, {
+ handler =>
+ executeQuery(handler, "SELECT 0")
+ }) must throwAn[UnsupportedAuthenticationMethodException]
+
+ }
+
+ "fail login using with an invalid credential exception" in {
+
+ val configuration = new Configuration(
+ username = "postgres_md5",
+ password = Some("postgres_kerberos"),
+ port = databasePort,
+ database = databaseName
+ )
+ try {
+ withHandler(configuration, {
+ handler =>
+ val result = executeQuery(handler, "SELECT 0")
+ throw new IllegalStateException("should not have arrived")
+ })
+ } catch {
+ case e: GenericDatabaseException =>
+ e.errorMessage.fields(InformationMessage.Routine) === "auth_failed"
+ }
+
+ }
+
+ "transaction and flatmap example" in {
+
+ val handler: Connection = new PostgreSQLConnection(defaultConfiguration)
+ val result: Future[QueryResult] = handler.connect
+ .map(parameters => handler)
+ .flatMap(connection => connection.sendQuery("BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ"))
+ .flatMap(query => handler.sendQuery("SELECT 0"))
+ .flatMap(query => handler.sendQuery("COMMIT").map(value => query))
+
+ val queryResult: QueryResult = Await.result(result, Duration(5, SECONDS))
+
+ queryResult.rows.get(0)(0) === 0
+
+ }
+
+ "use RETURNING in an insert statement" in {
+
+ withHandler {
+ connection =>
+ executeDdl(connection, this.preparedStatementCreate)
+ val result = executeQuery(connection, this.preparedStatementInsertReturning)
+ result.rows.get(0)("id") === 1
+ }
+
+ }
+
+ "execute a prepared statement with limit" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, this.preparedStatementCreate)
+ executeDdl(handler, this.preparedStatementInsert, 1)
+ executeDdl(handler, this.preparedStatementInsert2, 1)
+ executeDdl(handler, this.preparedStatementInsert3, 1)
+
+ val result = executePreparedStatement(handler, "select * from prepared_statement_test LIMIT 1").rows.get(0)
+
+ result("name") === "John Doe"
+ }
+
+ }
+
+ "execute an empty query" in {
+
+ withHandler {
+ handler =>
+ executeQuery(handler, "").rows === None
+ } must throwA[QueryMustNotBeNullOrEmptyException]
+
+ }
+
+ "execute an whitespace query" in {
+
+ withHandler {
+ handler =>
+ executeQuery(handler, " ").rows === None
+ } must throwA[QueryMustNotBeNullOrEmptyException]
+
+ }
+
+ "execute multiple prepared statements" in {
+ withHandler {
+ handler =>
+ executeDdl(handler, this.preparedStatementCreate)
+ for (i <- 0 until 1000)
+ executePreparedStatement(handler, this.preparedStatementInsert)
+ ok
+ }
+ }
+
+ "load data from a bytea column" in {
+
+ val create = """create temp table file_samples (
+ id bigserial not null,
+ content bytea not null,
+ constraint bigserial_column_pkey primary key (id)
+ )"""
+
+ val insert = "insert into file_samples (content) values ( E'\\\\x5361792048656c6c6f20746f204d79204c6974746c6520467269656e64' ) "
+ val select = "select * from file_samples"
+
+ withHandler {
+ handler =>
+ executeDdl(handler, create)
+ executeQuery(handler, insert)
+ val rows = executeQuery(handler, select).rows.get
+
+ rows(0)("content").asInstanceOf[Array[Byte]] === sampleArray
+
+ }
+
+ }
+
+ "send data to a bytea column" in {
+
+ val create = """create temp table file_samples (
+ id bigserial not null,
+ content bytea not null,
+ constraint bigserial_column_pkey primary key (id)
+ )"""
+
+ val insert = "insert into file_samples (content) values ( ? ) "
+ val select = "select * from file_samples"
+
+ withHandler {
+ handler =>
+
+ executeDdl(handler, create)
+ log.debug("executed create")
+ executePreparedStatement(handler, insert, Array( sampleArray ))
+ executePreparedStatement(handler, insert, Array( ByteBuffer.wrap(sampleArray) ))
+ executePreparedStatement(handler, insert, Array( Unpooled.copiedBuffer(sampleArray) ))
+ log.debug("executed prepared statement")
+ val rows = executeQuery(handler, select).rows.get
+
+ rows(0)("content").asInstanceOf[Array[Byte]] === sampleArray
+ rows(1)("content").asInstanceOf[Array[Byte]] === sampleArray
+ rows(2)("content").asInstanceOf[Array[Byte]] === sampleArray
+ }
+
+ }
+
+ "insert a LocalDateTime" in {
+
+ withHandler {
+ handler =>
+ executePreparedStatement(handler, "CREATE TEMP TABLE test(t TIMESTAMP)")
+ val date1 = new LocalDateTime
+ executePreparedStatement(handler, "INSERT INTO test(t) VALUES(?)", Array(date1))
+ val result = executePreparedStatement(handler, "SELECT t FROM test")
+ val date2 = result.rows.get.head(0)
+ date1 === date2
+ }
+
+ }
+
+ "insert without return after select" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, this.preparedStatementCreate)
+ executeDdl(handler, this.preparedStatementInsert, 1)
+ executeDdl(handler, this.preparedStatementSelect, 1)
+ val result = executeQuery(handler, this.preparedStatementInsert2)
+
+ result.rows === None
+ }
+
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLSSLConnectionSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLSSLConnectionSpec.scala
new file mode 100644
index 00000000..2e38adbb
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PostgreSQLSSLConnectionSpec.scala
@@ -0,0 +1,51 @@
+package com.github.mauricio.async.db.postgresql
+
+import org.specs2.mutable.Specification
+import com.github.mauricio.async.db.SSLConfiguration.Mode
+import javax.net.ssl.SSLHandshakeException
+
+class PostgreSQLSSLConnectionSpec extends Specification with DatabaseTestHelper {
+
+ "ssl handler" should {
+
+ "connect to the database in ssl without verifying CA" in {
+
+ withSSLHandler(Mode.Require, "127.0.0.1", None) { handler =>
+ handler.isReadyForQuery must beTrue
+ }
+
+ }
+
+ "connect to the database in ssl verifying CA" in {
+
+ withSSLHandler(Mode.VerifyCA, "127.0.0.1") { handler =>
+ handler.isReadyForQuery must beTrue
+ }
+
+ }
+
+ "connect to the database in ssl verifying CA and hostname" in {
+
+ withSSLHandler(Mode.VerifyFull) { handler =>
+ handler.isReadyForQuery must beTrue
+ }
+
+ }
+
+ "throws exception when CA verification fails" in {
+
+ withSSLHandler(Mode.VerifyCA, rootCert = None) { handler =>
+ } must throwA[SSLHandshakeException]
+
+ }
+
+ "throws exception when hostname verification fails" in {
+
+ withSSLHandler(Mode.VerifyFull, "127.0.0.1") { handler =>
+ } must throwA[SSLHandshakeException]
+
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PreparedStatementSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PreparedStatementSpec.scala
new file mode 100644
index 00000000..660c1411
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/PreparedStatementSpec.scala
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql
+
+import org.specs2.mutable.Specification
+import org.joda.time.LocalDate
+import com.github.mauricio.async.db.util.Log
+import com.github.mauricio.async.db.exceptions.InsufficientParametersException
+import java.util.UUID
+import com.github.mauricio.async.db.postgresql.exceptions.GenericDatabaseException
+
+class PreparedStatementSpec extends Specification with DatabaseTestHelper {
+
+ val log = Log.get[PreparedStatementSpec]
+
+ val filler = List.fill(64)(" ").mkString("")
+
+ val messagesCreate = """CREATE TEMP TABLE messages
+ (
+ id bigserial NOT NULL,
+ content character varying(255) NOT NULL,
+ moment date NULL,
+ CONSTRAINT bigserial_column_pkey PRIMARY KEY (id )
+ )"""
+ val messagesInsert = s"INSERT INTO messages $filler (content,moment) VALUES (?,?) RETURNING id"
+ val messagesInsertReverted = s"INSERT INTO messages $filler (moment,content) VALUES (?,?) RETURNING id"
+ val messagesUpdate = "UPDATE messages SET content = ?, moment = ? WHERE id = ?"
+ val messagesSelectOne = "SELECT id, content, moment FROM messages WHERE id = ?"
+ val messagesSelectByMoment = "SELECT id, content, moment FROM messages WHERE moment = ?"
+ val messagesSelectAll = "SELECT id, content, moment FROM messages"
+ val messagesSelectEscaped = "SELECT id, content, moment FROM messages WHERE content LIKE '%??%' AND id > ?"
+
+ "prepared statements" should {
+
+ "support prepared statement with more than 64 characters" in {
+ withHandler {
+ handler =>
+
+ val firstContent = "Some Moment"
+ val secondContent = "Some Other Moment"
+ val date = LocalDate.now()
+
+ executeDdl(handler, this.messagesCreate)
+ executePreparedStatement(handler, this.messagesInsert, Array(firstContent, date))
+ executePreparedStatement(handler, this.messagesInsertReverted, Array(date, secondContent))
+
+ val rows = executePreparedStatement(handler, this.messagesSelectAll).rows.get
+
+ rows.length === 2
+
+ rows(0)("id") === 1
+ rows(0)("content") === firstContent
+ rows(0)("moment") === date
+
+ rows(1)("id") === 2
+ rows(1)("content") === secondContent
+ rows(1)("moment") === date
+
+ }
+ }
+
+ "execute a prepared statement without any parameters multiple times" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, this.messagesCreate)
+ executePreparedStatement(handler, "UPDATE messages SET content = content")
+ executePreparedStatement(handler, "UPDATE messages SET content = content")
+ ok
+ }
+
+ }
+
+ "raise an exception if the parameter count is different from the given parameters count" in {
+
+ withHandler {
+ handler =>
+ executeDdl(handler, this.messagesCreate)
+ executePreparedStatement(handler, this.messagesSelectOne) must throwAn[InsufficientParametersException]
+ }
+
+ }
+
+ "run two different prepared statements in sequence and get the right values" in {
+
+ val create = """CREATE TEMP TABLE other_messages
+ (
+ id bigserial NOT NULL,
+ other_moment date NULL,
+ other_content character varying(255) NOT NULL,
+ CONSTRAINT other_messages_bigserial_column_pkey PRIMARY KEY (id )
+ )"""
+
+ val select = "SELECT * FROM other_messages"
+ val insert = "INSERT INTO other_messages (other_moment, other_content) VALUES (?, ?)"
+
+ val moment = LocalDate.now()
+ val otherMoment = LocalDate.now().minusDays(10)
+
+ val message = "this is some message"
+ val otherMessage = "this is some other message"
+
+ withHandler {
+ handler =>
+ executeDdl(handler, this.messagesCreate)
+ executeDdl(handler, create)
+
+ foreach(1.until(4)) {
+ x =>
+ executePreparedStatement(handler, this.messagesInsert, Array(message, moment))
+ executePreparedStatement(handler, insert, Array(otherMoment, otherMessage))
+
+ val result = executePreparedStatement(handler, this.messagesSelectAll).rows.get
+ result.size === x
+ result.columnNames must contain(allOf("id", "content", "moment")).inOrder
+ result(x - 1)("moment") === moment
+ result(x - 1)("content") === message
+
+ val otherResult = executePreparedStatement(handler, select).rows.get
+ otherResult.size === x
+ otherResult.columnNames must contain(allOf("id", "other_moment", "other_content")).inOrder
+ otherResult(x - 1)("other_moment") === otherMoment
+ otherResult(x - 1)("other_content") === otherMessage
+ }
+
+ }
+
+ }
+
+ "support prepared statement with Option parameters (Some/None)" in {
+ withHandler {
+ handler =>
+
+ val firstContent = "Some Moment"
+ val secondContent = "Some Other Moment"
+ val date = LocalDate.now()
+
+ executeDdl(handler, this.messagesCreate)
+ executePreparedStatement(handler, this.messagesInsert, Array(Some(firstContent), None))
+ executePreparedStatement(handler, this.messagesInsert, Array(Some(secondContent), Some(date)))
+
+ val rows = executePreparedStatement(handler, this.messagesSelectAll).rows.get
+
+ rows.length === 2
+
+ rows(0)("id") === 1
+ rows(0)("content") === firstContent
+ rows(0)("moment") === null
+
+ rows(1)("id") === 2
+ rows(1)("content") === secondContent
+ rows(1)("moment") === date
+ }
+ }
+
+ "supports sending null first and then an actual value for the fields" in {
+ withHandler {
+ handler =>
+
+ val firstContent = "Some Moment"
+ val secondContent = "Some Other Moment"
+ val date = LocalDate.now()
+
+ executeDdl(handler, this.messagesCreate)
+ executePreparedStatement(handler, this.messagesInsert, Array(firstContent, null))
+ executePreparedStatement(handler, this.messagesInsert, Array(secondContent, date))
+
+ val rows = executePreparedStatement(handler, this.messagesSelectByMoment, Array(null)).rows.get
+ rows.size === 0
+
+ /*
+ PostgreSQL does not know how to handle NULL parameters for a query in a prepared statement,
+ you have to use IS NULL if you want to make use of it.
+
+ rows.length === 1
+
+ rows(0)("id") === 1
+ rows(0)("content") === firstContent
+ rows(0)("moment") === null
+ */
+
+ val rowsWithoutNull = executePreparedStatement(handler, this.messagesSelectByMoment, Array(date)).rows.get
+ rowsWithoutNull.size === 1
+ rowsWithoutNull(0)("id") === 2
+ rowsWithoutNull(0)("content") === secondContent
+ rowsWithoutNull(0)("moment") === date
+ }
+ }
+
+ "support prepared statement with escaped placeholders" in {
+ withHandler {
+ handler =>
+
+ val firstContent = "Some? Moment"
+ val secondContent = "Some Other Moment"
+ val date = LocalDate.now()
+
+ executeDdl(handler, this.messagesCreate)
+ executePreparedStatement(handler, this.messagesInsert, Array(Some(firstContent), None))
+ executePreparedStatement(handler, this.messagesInsert, Array(Some(secondContent), Some(date)))
+
+ val rows = executePreparedStatement(handler, this.messagesSelectEscaped, Array(0)).rows.get
+
+ rows.length === 1
+
+ rows(0)("id") === 1
+ rows(0)("content") === firstContent
+ rows(0)("moment") === null
+
+ }
+ }
+
+ "support handling of enum types" in {
+
+ withHandler {
+ handler =>
+ val create = """CREATE TEMP TABLE messages
+ |(
+ |id bigserial NOT NULL,
+ |feeling example_mood,
+ |CONSTRAINT bigserial_column_pkey PRIMARY KEY (id )
+ |);""".stripMargin
+ val insert = "INSERT INTO messages (feeling) VALUES (?) RETURNING id"
+ val select = "SELECT * FROM messages"
+
+ executeDdl(handler, create)
+
+ executePreparedStatement(handler, insert, Array("sad"))
+
+ val result = executePreparedStatement(handler, select).rows.get
+
+ result.size === 1
+ result(0)("id") === 1L
+ result(0)("feeling") === "sad"
+ }
+
+ }
+
+ "support handling JSON type" in {
+
+ if ( System.getenv("TRAVIS") == null ) {
+ withHandler {
+ handler =>
+ val create = """create temp table people
+ |(
+ |id bigserial primary key,
+ |addresses json,
+ |phones json
+ |);""".stripMargin
+
+ val insert = "INSERT INTO people (addresses, phones) VALUES (?,?) RETURNING id"
+ val select = "SELECT * FROM people"
+ val addresses = """[ {"Home" : {"city" : "Tahoe", "state" : "CA"}} ]"""
+ val phones = """[ "925-575-0415", "916-321-2233" ]"""
+
+ executeDdl(handler, create)
+ executePreparedStatement(handler, insert, Array(addresses, phones) )
+ val result = executePreparedStatement(handler, select).rows.get
+
+ result(0)("addresses") === addresses
+ result(0)("phones") === phones
+ }
+ success
+ } else {
+ pending
+ }
+ }
+
+ "support select bind value" in {
+ withHandler {
+ handler =>
+ val string = "someString"
+ val result = executePreparedStatement(handler, "SELECT CAST(? AS VARCHAR)", Array(string)).rows.get
+ result(0)(0) === string
+ }
+ }
+
+ "fail if prepared statement has more variables than it was given" in {
+ withHandler {
+ handler =>
+ executeDdl(handler, messagesCreate)
+
+ handler.sendPreparedStatement(
+ "SELECT * FROM messages WHERE content = ? AND moment = ?",
+ Array("some content")) must throwAn[InsufficientParametersException]
+ }
+ }
+
+ "run prepared statement twice with bad and good values" in {
+ withHandler {
+ handler =>
+ val content = "Some Moment"
+
+ val query = "SELECT content FROM messages WHERE id = ?"
+
+ executeDdl(handler, messagesCreate)
+ executePreparedStatement(handler, this.messagesInsert, Array(Some(content), None))
+
+ executePreparedStatement(handler, query, Array("undefined")) must throwA[GenericDatabaseException]
+ val result = executePreparedStatement(handler, query, Array(1)).rows.get
+ result(0)(0) === content
+ }
+ }
+
+ "support UUID" in {
+ if ( System.getenv("TRAVIS") == null ) {
+ withHandler {
+ handler =>
+ val create = """create temp table uuids
+ |(
+ |id bigserial primary key,
+ |my_id uuid
+ |);""".stripMargin
+
+ val insert = "INSERT INTO uuids (my_id) VALUES (?) RETURNING id"
+ val select = "SELECT * FROM uuids"
+
+ val uuid = UUID.randomUUID()
+
+ executeDdl(handler, create)
+ executePreparedStatement(handler, insert, Array(uuid) )
+ val result = executePreparedStatement(handler, select).rows.get
+
+ result(0)("my_id").asInstanceOf[UUID] === uuid
+ }
+ success
+ } else {
+ pending
+ }
+ }
+
+ "support UUID array" in {
+ if ( System.getenv("TRAVIS") == null ) {
+ withHandler {
+ handler =>
+ val create = """create temp table uuids
+ |(
+ |id bigserial primary key,
+ |my_id uuid[]
+ |);""".stripMargin
+
+ val insert = "INSERT INTO uuids (my_id) VALUES (?) RETURNING id"
+ val select = "SELECT * FROM uuids"
+
+ val uuid1 = UUID.randomUUID()
+ val uuid2 = UUID.randomUUID()
+
+ executeDdl(handler, create)
+ executePreparedStatement(handler, insert, Array(Array(uuid1, uuid2)) )
+ val result = executePreparedStatement(handler, select).rows.get
+
+ result(0)("my_id").asInstanceOf[Seq[UUID]] === Seq(uuid1, uuid2)
+ }
+ success
+ } else {
+ pending
+ }
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TestUtils.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TestUtils.scala
new file mode 100644
index 00000000..0184b2c4
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TestUtils.scala
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.postgresql
+
+import java.util.concurrent.atomic.AtomicInteger
+
+object TestUtils {
+
+ private val count = new AtomicInteger()
+
+ def nextInt: Int = {
+ count.incrementAndGet()
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TimeAndDateSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TimeAndDateSpec.scala
new file mode 100644
index 00000000..03703f21
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TimeAndDateSpec.scala
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql
+
+import org.specs2.mutable.Specification
+import org.joda.time._
+
+class TimeAndDateSpec extends Specification with DatabaseTestHelper {
+
+ "when processing times and dates" should {
+
+ "support a time object" in {
+
+ withHandler {
+ handler =>
+ val create = """CREATE TEMP TABLE messages
+ (
+ id bigserial NOT NULL,
+ moment time NOT NULL,
+ CONSTRAINT bigserial_column_pkey PRIMARY KEY (id )
+ )"""
+
+ executeDdl(handler, create)
+ executePreparedStatement(handler, "INSERT INTO messages (moment) VALUES (?)", Array[Any](new LocalTime(4, 5, 6)))
+
+ val rows = executePreparedStatement(handler, "select * from messages").rows.get
+
+ val time = rows(0)("moment").asInstanceOf[LocalTime]
+
+ time.getHourOfDay === 4
+ time.getMinuteOfHour === 5
+ time.getSecondOfMinute === 6
+ }
+
+ }
+
+ "support a time object with microseconds" in {
+
+ withHandler {
+ handler =>
+ val create = """CREATE TEMP TABLE messages
+ (
+ id bigserial NOT NULL,
+ moment time(6) NOT NULL,
+ CONSTRAINT bigserial_column_pkey PRIMARY KEY (id )
+ )"""
+
+ executeDdl(handler, create)
+ executePreparedStatement(handler, "INSERT INTO messages (moment) VALUES (?)", Array[Any](new LocalTime(4, 5, 6, 134)))
+
+ val rows = executePreparedStatement(handler, "select * from messages").rows.get
+
+ val time = rows(0)("moment").asInstanceOf[LocalTime]
+
+ time.getHourOfDay === 4
+ time.getMinuteOfHour === 5
+ time.getSecondOfMinute === 6
+ time.getMillisOfSecond === 134
+ }
+
+ }
+
+ "support a time with timezone object" in {
+
+ pending("need to find a way to implement this")
+
+ withHandler {
+ handler =>
+ val create = """CREATE TEMP TABLE messages
+ (
+ id bigserial NOT NULL,
+ moment time with time zone NOT NULL,
+ CONSTRAINT bigserial_column_pkey PRIMARY KEY (id )
+ )"""
+
+ executeDdl(handler, create)
+ executeQuery(handler, "INSERT INTO messages (moment) VALUES ('04:05:06 -3:00')")
+
+ val rows = executePreparedStatement(handler, "select * from messages").rows.get
+
+ val time = rows(0)("moment").asInstanceOf[LocalTime]
+
+ time.getHourOfDay === 4
+ time.getMinuteOfHour === 5
+ time.getSecondOfMinute === 6
+ }
+
+ }
+
+ "support timestamp with timezone" in {
+ withHandler {
+ handler =>
+
+ val create = """CREATE TEMP TABLE messages
+ (
+ id bigserial NOT NULL,
+ moment timestamp with time zone NOT NULL,
+ CONSTRAINT bigserial_column_pkey PRIMARY KEY (id )
+ )"""
+
+ executeDdl(handler, create)
+ executeQuery(handler, "INSERT INTO messages (moment) VALUES ('1999-01-08 04:05:06 -3:00')")
+ val rows = executePreparedStatement(handler, "SELECT * FROM messages").rows.get
+
+ rows.length === 1
+
+ val dateTime = rows(0)("moment").asInstanceOf[DateTime]
+
+ // Note: Since this assertion depends on Brazil locale, I think epoch time assertion is preferred
+ // dateTime.getZone.toTimeZone.getRawOffset === -10800000
+ dateTime.getMillis === 915779106000L
+ }
+ }
+
+ "support timestamp with timezone and microseconds" in {
+
+ foreach(1.until(6)) {
+ index =>
+ withHandler {
+ handler =>
+
+ val create = """CREATE TEMP TABLE messages
+ (
+ id bigserial NOT NULL,
+ moment timestamp(%d) with time zone NOT NULL,
+ CONSTRAINT bigserial_column_pkey PRIMARY KEY (id )
+ )""".format(index)
+
+ executeDdl(handler, create)
+
+ val seconds = (index.toString * index).toLong
+
+ executeQuery(handler, "INSERT INTO messages (moment) VALUES ('1999-01-08 04:05:06.%d -3:00')".format(seconds))
+ val rows = executePreparedStatement(handler, "SELECT * FROM messages").rows.get
+
+ rows.length === 1
+
+ val dateTime = rows(0)("moment").asInstanceOf[DateTime]
+
+ // Note: Since this assertion depends on Brazil locale, I think epoch time assertion is preferred
+ // dateTime.getZone.toTimeZone.getRawOffset === -10800000
+ dateTime.getMillis must be_>=(915779106000L)
+ dateTime.getMillis must be_<(915779107000L)
+ }
+ }
+ }
+
+ "support current_timestamp with timezone" in {
+ withHandler {
+ handler =>
+
+ val millis = System.currentTimeMillis
+
+ val create = """CREATE TEMP TABLE messages
+ (
+ id bigserial NOT NULL,
+ moment timestamp with time zone NOT NULL,
+ CONSTRAINT bigserial_column_pkey PRIMARY KEY (id )
+ )"""
+
+ executeDdl(handler, create)
+ executeQuery(handler, "INSERT INTO messages (moment) VALUES (current_timestamp)")
+ val rows = executePreparedStatement(handler, "SELECT * FROM messages").rows.get
+
+ rows.length === 1
+
+ val dateTime = rows(0)("moment").asInstanceOf[DateTime]
+
+ dateTime.getMillis must beCloseTo(millis, 500)
+ }
+ }
+
+ "handle sending a time with timezone and return a LocalDateTime for a timestamp without timezone column" in {
+
+ withTimeHandler {
+ conn =>
+ val date = new DateTime(2190319)
+
+ executePreparedStatement(conn, "CREATE TEMP TABLE TEST(T TIMESTAMP)")
+ executePreparedStatement(conn, "INSERT INTO TEST(T) VALUES(?)", Array(date))
+ val result = executePreparedStatement(conn, "SELECT T FROM TEST")
+ val date2 = result.rows.get.head(0)
+ date2 === date.toDateTime(DateTimeZone.UTC).toLocalDateTime
+ }
+
+ }
+
+ "supports sending a local date and later a date time object for the same field" in {
+
+ withTimeHandler {
+ conn =>
+ val date = new LocalDate(2016, 3, 5)
+
+ executePreparedStatement(conn, "CREATE TEMP TABLE TEST(T TIMESTAMP)")
+ executePreparedStatement(conn, "INSERT INTO TEST(T) VALUES(?)", Array(date))
+ val result = executePreparedStatement(conn, "SELECT T FROM TEST WHERE T = ?", Array(date))
+ result.rows.get.size === 1
+
+ val dateTime = new LocalDateTime(2016, 3, 5, 0, 0, 0, 0)
+ val dateTimeResult = executePreparedStatement(conn, "SELECT T FROM TEST WHERE T = ?", Array(dateTime))
+ dateTimeResult.rows.get.size === 1
+ }
+
+ }
+
+ "handle sending a LocalDateTime and return a LocalDateTime for a timestamp without timezone column" in {
+
+ withTimeHandler {
+ conn =>
+ val date1 = new LocalDateTime(2190319)
+
+ await(conn.sendPreparedStatement("CREATE TEMP TABLE TEST(T TIMESTAMP)"))
+ await(conn.sendPreparedStatement("INSERT INTO TEST(T) VALUES(?)", Seq(date1)))
+ val result = await(conn.sendPreparedStatement("SELECT T FROM TEST"))
+ val date2 = result.rows.get.head(0)
+
+ date2 === date1
+ }
+
+ }
+
+ "handle sending a date with timezone and retrieving the date with the same time zone" in {
+
+ withTimeHandler {
+ conn =>
+ val date1 = new DateTime(2190319)
+
+ await(conn.sendPreparedStatement("CREATE TEMP TABLE TEST(T TIMESTAMP WITH TIME ZONE)"))
+ await(conn.sendPreparedStatement("INSERT INTO TEST(T) VALUES(?)", Seq(date1)))
+ val result = await(conn.sendPreparedStatement("SELECT T FROM TEST"))
+ val date2 = result.rows.get.head(0)
+
+ date2 === date1
+ }
+ }
+
+ "support intervals" in {
+ withHandler {
+ handler =>
+
+ executeDdl(handler, "CREATE TEMP TABLE intervals (duration interval NOT NULL)")
+
+ val p = new Period(1,2,0,4,5,6,7,8) /* postgres normalizes weeks */
+ executePreparedStatement(handler, "INSERT INTO intervals (duration) VALUES (?)", Array(p))
+ val rows = executeQuery(handler, "SELECT duration FROM intervals").rows.get
+
+ rows.length === 1
+
+ rows(0)(0) === p
+ }
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TransactionSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TransactionSpec.scala
new file mode 100644
index 00000000..aad280d7
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/TransactionSpec.scala
@@ -0,0 +1,129 @@
+package com.github.mauricio.async.db.postgresql
+
+import org.specs2.mutable.Specification
+import com.github.mauricio.async.db.util.Log
+import scala.concurrent.ExecutionContext.Implicits.global
+import com.github.mauricio.async.db.postgresql.exceptions.GenericDatabaseException
+
+class TransactionSpec extends Specification with DatabaseTestHelper {
+
+ val log = Log.get[TransactionSpec]
+
+ val tableCreate = "CREATE TEMP TABLE transaction_test (x integer PRIMARY KEY)"
+
+ def tableInsert(x: Int) = "INSERT INTO transaction_test VALUES (" + x.toString + ")"
+
+ val tableSelect = "SELECT x FROM transaction_test ORDER BY x"
+
+ "transactions" should {
+
+ "commit simple inserts" in {
+ withHandler {
+ handler =>
+ executeDdl(handler, tableCreate)
+ await(handler.inTransaction {
+ conn =>
+ conn.sendQuery(tableInsert(1)).flatMap {
+ _ =>
+ conn.sendQuery(tableInsert(2))
+ }
+ })
+
+ val rows = executeQuery(handler, tableSelect).rows.get
+ rows.length === 2
+ rows(0)(0) === 1
+ rows(1)(0) === 2
+ }
+ }
+
+ "commit simple inserts with prepared statements" in {
+ withHandler {
+ handler =>
+ executeDdl(handler, tableCreate)
+ await(handler.inTransaction {
+ conn =>
+ conn.sendPreparedStatement(tableInsert(1)).flatMap {
+ _ =>
+ conn.sendPreparedStatement(tableInsert(2))
+ }
+ })
+
+ val rows = executePreparedStatement(handler, tableSelect).rows.get
+ rows.length === 2
+ rows(0)(0) === 1
+ rows(1)(0) === 2
+ }
+ }
+
+ "rollback on error" in {
+ withHandler {
+ handler =>
+ executeDdl(handler, tableCreate)
+
+ try {
+ await(handler.inTransaction {
+ conn =>
+ conn.sendQuery(tableInsert(1)).flatMap {
+ _ =>
+ conn.sendQuery(tableInsert(1))
+ }
+ })
+ failure("Should not have come here")
+ } catch {
+ case e: GenericDatabaseException => {
+ e.errorMessage.message === "duplicate key value violates unique constraint \"transaction_test_pkey\""
+ }
+ }
+
+ val rows = executeQuery(handler, tableSelect).rows.get
+ rows.length === 0
+ }
+
+ }
+
+ "rollback explicitly" in {
+ withHandler {
+ handler =>
+ executeDdl(handler, tableCreate)
+ await(handler.inTransaction {
+ conn =>
+ conn.sendQuery(tableInsert(1)).flatMap {
+ _ =>
+ conn.sendQuery("ROLLBACK")
+ }
+ })
+
+ val rows = executeQuery(handler, tableSelect).rows.get
+ rows.length === 0
+ }
+
+ }
+
+ "rollback to savepoint" in {
+ withHandler {
+ handler =>
+ executeDdl(handler, tableCreate)
+ await(handler.inTransaction {
+ conn =>
+ conn.sendQuery(tableInsert(1)).flatMap {
+ _ =>
+ conn.sendQuery("SAVEPOINT one").flatMap {
+ _ =>
+ conn.sendQuery(tableInsert(2)).flatMap {
+ _ =>
+ conn.sendQuery("ROLLBACK TO SAVEPOINT one")
+ }
+ }
+ }
+ })
+
+ val rows = executeQuery(handler, tableSelect).rows.get
+ rows.length === 1
+ rows(0)(0) === 1
+ }
+
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/ArrayDecoderSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/ArrayDecoderSpec.scala
new file mode 100644
index 00000000..e91a8462
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/ArrayDecoderSpec.scala
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import com.github.mauricio.async.db.column.IntegerEncoderDecoder
+import io.netty.buffer.Unpooled
+import io.netty.util.CharsetUtil
+import org.specs2.mutable.Specification
+
+class ArrayDecoderSpec extends Specification {
+
+ def execute( data : String ) : Any = {
+ val numbers = data.getBytes( CharsetUtil.UTF_8 )
+ val encoder = new ArrayDecoder(IntegerEncoderDecoder)
+ encoder.decode(null, Unpooled.wrappedBuffer(numbers), CharsetUtil.UTF_8)
+ }
+
+ "encoder/decoder" should {
+
+ "parse an array of numbers" in {
+ execute("{1,2,3}") === List(1, 2, 3)
+ }
+
+ "parse an array of array of numbers" in {
+ execute("{{1,2,3},{4,5,6}}") === List(List(1, 2, 3), List(4, 5, 6))
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/ByteArrayDecoderSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/ByteArrayDecoderSpec.scala
new file mode 100644
index 00000000..328e872f
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/ByteArrayDecoderSpec.scala
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import org.specs2.mutable.Specification
+
+class ByteArrayDecoderSpec extends Specification {
+
+ val escapeTestData =
+ """\000\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017\020\021\022\023\024\025\026\027""" +
+ """\030\031\032\033\034\035\036\037 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^""" +
+ """_`abcdefghijklmnopqrstuvwxyz{|}~\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216""" +
+ """\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246""" +
+ """\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276""" +
+ """\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326""" +
+ """\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356""" +
+ """\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377\377\376\375\374\373\372\371""" +
+ """\370\367\366\365\364\363\362\361\360\357\356\355\354\353\352\351\350\347\346\345\344\343\342\341""" +
+ """\340\337\336\335\334\333\332\331\330\327\326\325\324\323\322\321\320\317\316\315\314\313\312\311""" +
+ """\310\307\306\305\304\303\302\301\300\277\276\275\274\273\272\271\270\267\266\265\264\263\262\261""" +
+ """\260\257\256\255\254\253\252\251\250\247\246\245\244\243\242\241\240\237\236\235\234\233\232\231""" +
+ """\230\227\226\225\224\223\222\221\220\217\216\215\214\213\212\211\210\207\206\205\204\203\202\201""" +
+ """\200\177~}|{zyxwvutsrqponmlkjihgfedcba`_^]\\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)(""" +
+ """'&%$#"! \037\036\035\034\033\032\031\030\027\026\025\024\023\022\021\020\017\016\015\014\013\012""" +
+ """\011\010\007\006\005\004\003\002\001\000"""
+
+ val hexTestData =
+ """\x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e""" +
+ """2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e""" +
+ """5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e""" +
+ """8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbe""" +
+ """bfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedee""" +
+ """eff0f1f2f3f4f5f6f7f8f9fafbfcfdfefffffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7e6e5e4e3e2e1""" +
+ """e0dfdedddcdbdad9d8d7d6d5d4d3d2d1d0cfcecdcccbcac9c8c7c6c5c4c3c2c1c0bfbebdbcbbbab9b8b7b6b5b4b3b2b1""" +
+ """b0afaeadacabaaa9a8a7a6a5a4a3a2a1a09f9e9d9c9b9a999897969594939291908f8e8d8c8b8a898887868584838281""" +
+ """807f7e7d7c7b7a797877767574737271706f6e6d6c6b6a696867666564636261605f5e5d5c5b5a595857565554535251""" +
+ """504f4e4d4c4b4a494847464544434241403f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221""" +
+ """201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"""
+
+ val originalData = ((0 to 255) ++ ((0 to 255).reverse)).map(_.toByte).toArray
+
+ "decoder" should {
+
+ "parse escape data" in {
+ ByteArrayEncoderDecoder.decode(escapeTestData) === originalData
+ }
+
+ "parse hex data" in {
+ ByteArrayEncoderDecoder.decode(hexTestData) === originalData
+ }
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/DefaultColumnEncoderRegistrySpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/DefaultColumnEncoderRegistrySpec.scala
new file mode 100644
index 00000000..1b41f447
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/DefaultColumnEncoderRegistrySpec.scala
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import org.specs2.mutable.Specification
+
+class DefaultColumnEncoderRegistrySpec extends Specification {
+
+ val registry = new PostgreSQLColumnEncoderRegistry()
+
+ "registry" should {
+
+ "correctly render an array of strings with nulls" in {
+ val items = Array( "some", """text \ hoes " here to be seen""", null, "all, right" )
+ registry.encode( items ) === """{"some","text \\ hoes \" here to be seen",NULL,"all, right"}"""
+ }
+
+ "correctly render an array of numbers" in {
+ val items = Array(Array(1,2,3),Array(4,5,6),Array(7,null,8))
+ registry.encode( items ) === "{{1,2,3},{4,5,6},{7,NULL,8}}"
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/IntervalSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/IntervalSpec.scala
new file mode 100644
index 00000000..491e2403
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/column/IntervalSpec.scala
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ * Copyright 2013 Dylan Simon
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.column
+
+import org.specs2.mutable.Specification
+
+class IntervalSpec extends Specification {
+
+ "interval encoder/decoder" should {
+
+ def decode(s : String) : Any = PostgreSQLIntervalEncoderDecoder.decode(s)
+ def encode(i : Any) : String = PostgreSQLIntervalEncoderDecoder.encode(i)
+ def both(s : String) : String = encode(decode(s))
+
+ "parse and encode example intervals" in {
+ Seq("1-2", "1 year 2 mons", "@ 1 year 2 mons", "@ 1 year 2 mons", "P1Y2M") forall {
+ both(_) === "P1Y2M"
+ }
+ Seq("3 4:05:06", "3 days 04:05:06", "@ 3 days 4 hours 5 mins 6 secs", "P3DT4H5M6S") forall {
+ both(_) === "P3DT4H5M6S"
+ }
+ Seq("1-2 +3 4:05:06", "1 year 2 mons +3 days 04:05:06", "@ 1 year 2 mons 3 days 4 hours 5 mins 6 secs", "P1Y2M3DT4H5M6S") forall {
+ both(_) === "P1Y2M3DT4H5M6S"
+ }
+ Seq("@ 1 year 2 mons -3 days 4 hours 5 mins 6 secs ago", "P-1Y-2M3DT-4H-5M-6S") forall {
+ both(_) === "P-1Y-2M3DT-4H-5M-6S"
+ }
+ both("-1.234") === "PT-1.234S"
+ both("-4:05:06") === "PT-4H-5M-6S"
+ }
+
+ "parse and encode example intervals" in {
+ Seq("-1-2 +3 -4:05:06", "-1 year -2 mons +3 days -04:05:06") forall {
+ both(_) === "P-1Y-2M3DT-4H-5M-6S"
+ }
+ }.pendingUntilFixed("with mixed/grouped negations")
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/encoders/ExecutePreparedStatementEncoderSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/encoders/ExecutePreparedStatementEncoderSpec.scala
new file mode 100644
index 00000000..9342a703
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/encoders/ExecutePreparedStatementEncoderSpec.scala
@@ -0,0 +1,30 @@
+package com.github.mauricio.async.db.postgresql.encoders
+
+import com.github.mauricio.async.db.postgresql.column.PostgreSQLColumnEncoderRegistry
+import com.github.mauricio.async.db.postgresql.messages.frontend.PreparedStatementExecuteMessage
+import io.netty.util.CharsetUtil
+import org.specs2.mutable.Specification
+
+class ExecutePreparedStatementEncoderSpec extends Specification {
+
+ val registry = new PostgreSQLColumnEncoderRegistry()
+ val encoder = new ExecutePreparedStatementEncoder(CharsetUtil.UTF_8, registry)
+ val sampleMessage = Array[Byte](66,0,0,0,18,49,0,49,0,0,0,0,1,-1,-1,-1,-1,0,0,69,0,0,0,10,49,0,0,0,0,0,83,0,0,0,4,67,0,0,0,7,80,49,0)
+
+ "encoder" should {
+
+ "correctly handle the case where an encoder returns null" in {
+
+ val message = new PreparedStatementExecuteMessage(1, "select * from users", List(Some(null)), registry)
+
+ val result = encoder.encode(message)
+
+ val bytes = new Array[Byte](result.readableBytes())
+ result.readBytes(bytes)
+
+ bytes === sampleMessage
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/parsers/ParserESpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/parsers/ParserESpec.scala
new file mode 100644
index 00000000..37e9e575
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/parsers/ParserESpec.scala
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{ServerMessage, ErrorMessage}
+import org.specs2.mutable.Specification
+import io.netty.util.CharsetUtil
+import io.netty.buffer.Unpooled
+
+class ParserESpec extends Specification {
+
+ "ErrorParser" should {
+
+ "correctly parse an error message" in {
+
+ val content = "this is my error message"
+ val error = content.getBytes(CharsetUtil.UTF_8)
+ val buffer = Unpooled.buffer()
+ buffer.writeByte('M')
+ buffer.writeBytes(error)
+ buffer.writeByte(0)
+
+ val message = new ErrorParser(CharsetUtil.UTF_8).parseMessage(buffer).asInstanceOf[ErrorMessage]
+
+ message.message === content
+ message.kind === ServerMessage.Error
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/parsers/ParserKSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/parsers/ParserKSpec.scala
new file mode 100644
index 00000000..3f2788fc
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/parsers/ParserKSpec.scala
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{ServerMessage, ProcessData}
+import org.specs2.mutable.Specification
+import io.netty.buffer.Unpooled
+
+class ParserKSpec extends Specification {
+
+ val parser = BackendKeyDataParser
+
+ "parserk" should {
+
+ "correctly parse the message" in {
+
+ val buffer = Unpooled.buffer()
+ buffer.writeInt(10)
+ buffer.writeInt(20)
+
+ val data = parser.parseMessage(buffer).asInstanceOf[ProcessData]
+
+ data.kind === ServerMessage.BackendKeyData
+ data.processId === 10
+ data.secretKey === 20
+
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/parsers/ParserSSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/parsers/ParserSSpec.scala
new file mode 100644
index 00000000..e1fa3baf
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/parsers/ParserSSpec.scala
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.parsers
+
+import com.github.mauricio.async.db.postgresql.messages.backend.{ServerMessage, ParameterStatusMessage}
+import java.nio.charset.Charset
+import org.specs2.mutable.Specification
+import io.netty.buffer.Unpooled
+import io.netty.util.CharsetUtil
+
+class ParserSSpec extends Specification {
+
+ val parser = new ParameterStatusParser(CharsetUtil.UTF_8)
+
+ "ParameterStatusParser" should {
+
+ "correctly parse a config pair" in {
+
+ val key = "application-name"
+ val value = "my-cool-application"
+
+ val buffer = Unpooled.buffer()
+
+ buffer.writeBytes(key.getBytes(Charset.forName("UTF-8")))
+ buffer.writeByte(0)
+ buffer.writeBytes(value.getBytes(Charset.forName("UTF-8")))
+ buffer.writeByte(0)
+
+ val content = this.parser.parseMessage(buffer).asInstanceOf[ParameterStatusMessage]
+
+ content.key === key
+ content.value === value
+ content.kind === ServerMessage.ParameterStatus
+ buffer.readerIndex() === buffer.writerIndex()
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala
new file mode 100644
index 00000000..c2471a75
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/ConnectionPoolSpec.scala
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.pool
+
+import java.util.UUID
+
+import com.github.mauricio.async.db.pool.{ConnectionPool, PoolConfiguration}
+import com.github.mauricio.async.db.postgresql.exceptions.GenericDatabaseException
+import com.github.mauricio.async.db.postgresql.{PostgreSQLConnection, DatabaseTestHelper}
+import org.specs2.mutable.Specification
+import scala.concurrent.ExecutionContext.Implicits.global
+
+object ConnectionPoolSpec {
+ val Insert = "insert into transaction_test (id) values (?)"
+}
+
+class ConnectionPoolSpec extends Specification with DatabaseTestHelper {
+
+ import ConnectionPoolSpec.Insert
+
+ "pool" should {
+
+ "give you a connection when sending statements" in {
+
+ withPool{
+ pool =>
+ executeQuery(pool, "SELECT 8").rows.get(0)(0) === 8
+ Thread.sleep(1000)
+ pool.availables.size === 1
+ }
+
+ }
+
+ "give you a connection for prepared statements" in {
+ withPool{
+ pool =>
+ executePreparedStatement(pool, "SELECT 8").rows.get(0)(0) === 8
+ Thread.sleep(1000)
+ pool.availables.size === 1
+ }
+ }
+
+ "return an empty map when connect is called" in {
+ withPool {
+ pool =>
+ await(pool.connect) === pool
+ }
+ }
+
+ "runs commands for a transaction in a single connection" in {
+
+ val id = UUID.randomUUID().toString
+
+ withPool {
+ pool =>
+ val operations = pool.inTransaction {
+ connection =>
+ connection.sendPreparedStatement(Insert, List(id)).flatMap {
+ result =>
+ connection.sendPreparedStatement(Insert, List(id)).map {
+ failure =>
+ List(result, failure)
+ }
+ }
+ }
+
+ await(operations) must throwA[GenericDatabaseException]
+
+ }
+
+ }
+
+ }
+
+ def withPool[R]( fn : (ConnectionPool[PostgreSQLConnection]) => R ) : R = {
+
+ val pool = new ConnectionPool( new PostgreSQLConnectionFactory(defaultConfiguration), PoolConfiguration.Default )
+ try {
+ fn(pool)
+ } finally {
+ await(pool.disconnect)
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/SingleThreadedAsyncObjectPoolSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/SingleThreadedAsyncObjectPoolSpec.scala
new file mode 100644
index 00000000..75da1ebd
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/pool/SingleThreadedAsyncObjectPoolSpec.scala
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.pool
+
+import com.github.mauricio.async.db.pool.{AsyncObjectPool, PoolConfiguration, PoolExhaustedException, SingleThreadedAsyncObjectPool}
+import com.github.mauricio.async.db.postgresql.{DatabaseTestHelper, PostgreSQLConnection}
+import java.nio.channels.ClosedChannelException
+import java.util.concurrent.TimeUnit
+
+import org.specs2.mutable.Specification
+
+import scala.concurrent.{Await, Future}
+import scala.concurrent.duration._
+import scala.language.postfixOps
+import com.github.mauricio.async.db.exceptions.ConnectionStillRunningQueryException
+
+class SingleThreadedAsyncObjectPoolSpec extends Specification with DatabaseTestHelper {
+
+ "pool" should {
+
+ "give me a valid object when I ask for one" in {
+
+ withPool {
+ pool =>
+ val connection = get(pool)
+ val result = executeTest(connection)
+ pool.giveBack(connection)
+ result
+ }
+ }
+
+ "enqueue an action if the pool is full" in {
+
+ withPool({
+ pool =>
+
+ val connection = get(pool)
+ val promises: List[Future[PostgreSQLConnection]] = List(pool.take, pool.take, pool.take)
+
+ pool.availables.size === 0
+ pool.inUse.size === 1
+ pool.queued.size must be_<=(3)
+
+ /* pool.take call checkout that call this.mainPool.action,
+ so enqueuePromise called in executorService,
+ so there is no guaranties that all promises in queue at that moment
+ */
+ val deadline = 5.seconds.fromNow
+ while(pool.queued.size < 3 || deadline.hasTimeLeft) {
+ Thread.sleep(50)
+ }
+
+ pool.queued.size === 3
+
+ executeTest(connection)
+
+ pool.giveBack(connection)
+
+ val pools: List[Future[AsyncObjectPool[PostgreSQLConnection]]] = promises.map {
+ promise =>
+ val connection = Await.result(promise, Duration(5, TimeUnit.SECONDS))
+ executeTest(connection)
+ pool.giveBack(connection)
+ }
+
+ Await.ready(pools.last, Duration(5, TimeUnit.SECONDS))
+
+ pool.availables.size === 1
+ pool.inUse.size === 0
+ pool.queued.size === 0
+
+ }, 1, 3)
+
+ }
+
+ "exhaust the pool" in {
+
+ withPool({
+ pool =>
+ 1 to 2 foreach {
+ _ => pool.take
+ }
+ await(pool.take) must throwA[PoolExhaustedException]
+ }, 1, 1)
+
+ }
+
+ "it should remove idle connections once the time limit has been reached" in {
+
+ withPool({
+ pool =>
+ val connections = 1 to 5 map {
+ _ =>
+ val connection = get(pool)
+ executeTest(connection)
+ connection
+ }
+
+ connections.foreach(connection => await(pool.giveBack(connection)))
+
+ pool.availables.size === 5
+
+ Thread.sleep(2000)
+
+ pool.availables.isEmpty must beTrue
+
+ }, validationInterval = 1000)
+
+ }
+
+ "it should validate returned connections before sending them back to the pool" in {
+
+ withPool {
+ pool =>
+ val connection = get(pool)
+ await(connection.disconnect)
+
+ pool.inUse.size === 1
+
+ await(pool.giveBack(connection)) must throwA[ClosedChannelException]
+
+ pool.availables.size === 0
+ pool.inUse.size === 0
+ }
+
+ }
+
+ "it should not accept returned connections that aren't ready for query" in {
+
+ withPool {
+ pool =>
+ val connection = get(pool)
+ connection.sendPreparedStatement("SELECT pg_sleep(3)")
+
+ await(pool.giveBack(connection)) must throwA[ConnectionStillRunningQueryException]
+ pool.availables.size === 0
+ pool.inUse.size === 0
+ }
+
+ }
+
+ }
+
+ def withPool[T](
+ fn: (SingleThreadedAsyncObjectPool[PostgreSQLConnection]) => T,
+ maxObjects: Int = 5,
+ maxQueueSize: Int = 5,
+ validationInterval: Long = 3000
+ ): T = {
+
+ val poolConfiguration = new PoolConfiguration(
+ maxIdle = 1000,
+ maxObjects = maxObjects,
+ maxQueueSize = maxQueueSize,
+ validationInterval = validationInterval
+ )
+ val factory = new PostgreSQLConnectionFactory(this.defaultConfiguration)
+ val pool = new SingleThreadedAsyncObjectPool[PostgreSQLConnection](factory, poolConfiguration)
+
+ try {
+ fn(pool)
+ } finally {
+ await(pool.close)
+ }
+
+ }
+
+ def executeTest(connection: PostgreSQLConnection) = executeQuery(connection, "SELECT 0").rows.get(0)(0) === 0
+
+ def get(pool: SingleThreadedAsyncObjectPool[PostgreSQLConnection]): PostgreSQLConnection = {
+ val future = pool.take
+ Await.result(future, Duration(5, TimeUnit.SECONDS))
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/ArrayStreamingParserSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/ArrayStreamingParserSpec.scala
new file mode 100644
index 00000000..61be30ac
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/ArrayStreamingParserSpec.scala
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.util
+
+import org.specs2.mutable.Specification
+import scala.collection.mutable.ArrayBuffer
+
+class ArrayStreamingParserSpec extends Specification {
+
+ val parser = ArrayStreamingParser
+
+ "parser" should {
+
+ "generate the events correctly" in {
+
+ val content = "{{1,2,3},{4,5,6}}"
+
+ val delegate = new LoggingDelegate()
+ parser.parse(content, delegate)
+
+ delegate.starts === 3
+ delegate.ends === 3
+ delegate.items === ArrayBuffer("{", "{", "1", "2", "3", "}", "{", "4", "5", "6", "}", "}")
+ }
+
+ "should parse a varchar array correctly" in {
+ val content = """{{"item","is here","but\"not there"},{"so","this is your last step"},{""}}"""
+
+ val delegate = new LoggingDelegate()
+ parser.parse(content, delegate)
+
+ delegate.items === ArrayBuffer("{", "{", "item", "is here", "but\"not there", "}", "{", "so", "this is your last step", "}", "{", "", "}", "}")
+ delegate.starts === 4
+ delegate.ends === 4
+ }
+
+ "should parse a varchar array with nulls correctly" in {
+ val content = """{NULL,"first",NULL,"second","NULL",NULL}"""
+
+ val delegate = new LoggingDelegate()
+ parser.parse(content, delegate)
+
+ delegate.items === ArrayBuffer("{", null, "first", null, "second", "NULL", null, "}")
+ }
+
+ }
+
+}
+
+class LoggingDelegate extends ArrayStreamingParserDelegate {
+
+ val items = new ArrayBuffer[String]()
+ var starts = 0
+ var ends = 0
+
+ override def arrayStarted {
+ items += "{"
+ starts += 1
+ }
+
+ override def arrayEnded {
+ items += "}"
+ ends += 1
+ }
+
+ override def elementFound(element: String) {
+ items += element
+ }
+
+ override def nullElementFound {
+ items += null
+ }
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/PasswordHelperSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/PasswordHelperSpec.scala
new file mode 100644
index 00000000..7959789c
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/PasswordHelperSpec.scala
@@ -0,0 +1,34 @@
+package com.github.mauricio.async.db.postgresql.util
+
+import org.specs2.mutable.Specification
+import io.netty.util.CharsetUtil
+
+/**
+ * User: mauricio
+ * Date: 5/9/13
+ * Time: 5:43 PM
+ */
+class PasswordHelperSpec extends Specification {
+
+ val salt = Array[Byte](-31, 68, 99, 36)
+ val result = Array[Byte](109,100,53,54,102,57,55,57,98,99,51,101,100,100,54,101,56,52,57,49,100,52,101,99,49,55,100,57,97,51,102,97,97,55,56)
+
+ def printArray( name : String, bytes : Array[Byte] ) {
+ printf("%s %s -> (%s)%n", name, bytes.length, bytes.mkString(","))
+ }
+
+
+ "helper" should {
+
+ "generate the same value as the PostgreSQL code" in {
+
+ val username = "mauricio"
+ val password = "example"
+
+ PasswordHelper.encode(username, password, salt, CharsetUtil.UTF_8) === result
+
+ }
+
+ }
+
+}
diff --git a/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala
new file mode 100644
index 00000000..9d2d2828
--- /dev/null
+++ b/postgresql-async/src/test/scala/com/github/mauricio/async/db/postgresql/util/URLParserSpec.scala
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2013 Maurício Linhares
+ *
+ * Maurício Linhares licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.github.mauricio.async.db.postgresql.util
+
+import org.specs2.mutable.Specification
+import com.github.mauricio.async.db.SSLConfiguration.Mode
+import com.github.mauricio.async.db.exceptions.UnableToParseURLException
+
+class URLParserSpec extends Specification {
+
+ "postgresql URLParser" should {
+ import URLParser.{parse, parseOrDie, DEFAULT}
+
+ // Divided into sections
+ // =========== jdbc:postgresql ===========
+
+ // https://jdbc.postgresql.org/documentation/80/connect.html
+ "recognize a jdbc:postgresql:dbname uri" in {
+ val connectionUri = "jdbc:postgresql:dbname"
+
+ parse(connectionUri) mustEqual DEFAULT.copy(
+ database = Some("dbname")
+ )
+ }
+
+ "create a jdbc:postgresql connection with the available fields" in {
+ val connectionUri = "jdbc:postgresql://128.167.54.90:9987/my_database?user=john&password=doe"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "128.167.54.90"
+ configuration.port === 9987
+ }
+
+ "create a connection without port" in {
+ val connectionUri = "jdbc:postgresql://128.167.54.90/my_database?user=john&password=doe"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "128.167.54.90"
+ configuration.port === 5432
+ }
+
+
+ "create a connection without username and password" in {
+ val connectionUri = "jdbc:postgresql://128.167.54.90:9987/my_database"
+
+ val configuration = parse(connectionUri)
+ configuration.username === DEFAULT.username
+ configuration.password === None
+ configuration.database === Some("my_database")
+ configuration.host === "128.167.54.90"
+ configuration.port === 9987
+ }
+
+ //========== postgresql:// ==============
+
+ "create a connection from a heroku like URL using 'postgresql' protocol" in {
+ val connectionUri = "postgresql://john:doe@128.167.54.90:9987/my_database"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "128.167.54.90"
+ configuration.port === 9987
+ }
+
+ "create a connection with SSL enabled" in {
+ val connectionUri = "jdbc:postgresql://128.167.54.90:9987/my_database?sslmode=verify-full"
+
+ val configuration = parse(connectionUri)
+ configuration.username === DEFAULT.username
+ configuration.password === None
+ configuration.database === Some("my_database")
+ configuration.host === "128.167.54.90"
+ configuration.port === 9987
+ configuration.ssl.mode === Mode.VerifyFull
+ }
+
+ "create a connection with SSL enabled and root CA from a heroku like URL using 'postgresql' protocol" in {
+ val connectionUri = "postgresql://john:doe@128.167.54.90:9987/my_database?sslmode=verify-ca&sslrootcert=server.crt"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "128.167.54.90"
+ configuration.port === 9987
+ configuration.ssl.mode === Mode.VerifyCA
+ configuration.ssl.rootCert.map(_.getPath) === Some("server.crt")
+ }
+
+ "create a connection with the available fields and named server" in {
+ val connectionUri = "jdbc:postgresql://localhost:9987/my_database?user=john&password=doe"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "localhost"
+ configuration.port === 9987
+ }
+
+ "create a connection from a heroku like URL with named server" in {
+ val connectionUri = "postgresql://john:doe@psql.heroku.com:9987/my_database"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "psql.heroku.com"
+ configuration.port === 9987
+ }
+
+ "create a connection with the available fields and ipv6" in {
+ val connectionUri = "jdbc:postgresql://[::1]:9987/my_database?user=john&password=doe"
+
+ val configuration = parse(connectionUri)
+
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "::1"
+ configuration.port === 9987
+ }
+
+ "create a connection from a heroku like URL and with ipv6" in {
+ val connectionUri = "postgresql://john:doe@[::1]:9987/my_database"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "::1"
+ configuration.port === 9987
+ }
+
+ "create a connection with a missing hostname" in {
+ val connectionUri = "jdbc:postgresql:/my_database?user=john&password=doe"
+
+ val configuration = parse(connectionUri)
+
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === Some("my_database")
+ configuration.host === "localhost"
+ configuration.port === 5432
+ }
+
+ "create a connection with a missing database name" in {
+ val connectionUri = "jdbc:postgresql://[::1]:9987/?user=john&password=doe"
+
+ val configuration = parse(connectionUri)
+
+ configuration.username === "john"
+ configuration.password === Some("doe")
+ configuration.database === None
+ configuration.host === "::1"
+ configuration.port === 9987
+ }
+
+ "create a connection with all default fields" in {
+ val connectionUri = "jdbc:postgresql:"
+
+ val configuration = parse(connectionUri)
+
+ configuration.username === "postgres"
+ configuration.password === None
+ configuration.database === None
+ configuration.host === "localhost"
+ configuration.port === 5432
+ }
+
+ "create a connection with an empty (invalid) url" in {
+ val connectionUri = ""
+
+ val configuration = parse(connectionUri)
+
+ configuration.username === "postgres"
+ configuration.password === None
+ configuration.database === None
+ configuration.host === "localhost"
+ configuration.port === 5432
+ }
+
+
+ "recognise a postgresql:// uri" in {
+ parse("postgresql://localhost:425/dbname") mustEqual DEFAULT.copy(
+ username = "postgres",
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ "recognise a postgres:// uri" in {
+ parse("postgres://localhost:425/dbname") mustEqual DEFAULT.copy(
+ username = "postgres",
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ "recognize a jdbc:postgresql:// uri" in {
+ parse("jdbc:postgresql://localhost:425/dbname") mustEqual DEFAULT.copy(
+ username = "postgres",
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ "pull the username and password from URI credentials" in {
+ parse("jdbc:postgresql://user:password@localhost:425/dbname") mustEqual DEFAULT.copy(
+ username = "user",
+ password = Some("password"),
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ "pull the username and password from query string" in {
+ parse("jdbc:postgresql://localhost:425/dbname?user=user&password=password") mustEqual DEFAULT.copy(
+ username = "user",
+ password = Some("password"),
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ // Included for consistency, so later changes aren't allowed to change behavior
+ "use the query string parameters to override URI credentials" in {
+ parse("jdbc:postgresql://baduser:badpass@localhost:425/dbname?user=user&password=password") mustEqual DEFAULT.copy(
+ username = "user",
+ password = Some("password"),
+ database = Some("dbname"),
+ port = 425,
+ host = "localhost"
+ )
+ }
+
+ "successfully default the port to the PostgreSQL port" in {
+ parse("jdbc:postgresql://baduser:badpass@localhost/dbname?user=user&password=password") mustEqual DEFAULT.copy(
+ username = "user",
+ password = Some("password"),
+ database = Some("dbname"),
+ port = 5432,
+ host = "localhost"
+ )
+ }
+
+ "reject malformed ip addresses" in {
+ val connectionUri = "postgresql://john:doe@128.567.54.90:9987/my_database"
+
+ val configuration = parse(connectionUri)
+ configuration.username === "postgres"
+ configuration.password === None
+ configuration.database === None
+ configuration.host === "localhost"
+ configuration.port === 5432
+
+ parseOrDie(connectionUri) must throwA[UnableToParseURLException]
+ }
+
+ }
+
+}
diff --git a/project/Build.scala b/project/Build.scala
index ec6521da..b543b050 100644
--- a/project/Build.scala
+++ b/project/Build.scala
@@ -1,60 +1,128 @@
-/**
- * User: Maurício Linhares
- * Date: 2/18/12
- * Time: 6:34 PM
- */
-
import sbt._
import Keys._
-object Build extends sbt.Build {
+object ProjectBuild extends Build {
- lazy val project = Project(
- "postgresql-netty",
- file(".")
- ).settings(
- organization := "org.postgresql",
- version := "1.0.0",
- scalaVersion := "2.9.1",
- scalacOptions := Seq("-deprecation", "-encoding", "utf8"),
- resolvers ++= Configuration.resolutionRepos,
- libraryDependencies ++= Configuration.dependencies)
+ val commonName = "db-async-common"
+ val postgresqlName = "postgresql-async"
+ val mysqlName = "mysql-async"
-}
+ lazy val root = Project(
+ id = "db-async-base",
+ base = file("."),
+ settings = Configuration.baseSettings ++ Seq(
+ publish := (),
+ publishLocal := (),
+ publishArtifact := false
+ ),
+ aggregate = Seq(common, postgresql, mysql)
+ )
+ lazy val common = Project(
+ id = commonName,
+ base = file(commonName),
+ settings = Configuration.baseSettings ++ Seq(
+ name := commonName,
+ libraryDependencies ++= Configuration.commonDependencies
+ )
+ )
+
+ lazy val postgresql = Project(
+ id = postgresqlName,
+ base = file(postgresqlName),
+ settings = Configuration.baseSettings ++ Seq(
+ name := postgresqlName,
+ libraryDependencies ++= Configuration.implementationDependencies
+ )
+ ) dependsOn (common)
+
+ lazy val mysql = Project(
+ id = mysqlName,
+ base = file(mysqlName),
+ settings = Configuration.baseSettings ++ Seq(
+ name := mysqlName,
+ libraryDependencies ++= Configuration.implementationDependencies
+ )
+ ) dependsOn (common)
+
+}
object Configuration {
- object Repos {
- val JavaNet = "java-net-maven" at "http://download.java.net/maven/2"
- val SonatypeSnapshots = "sonatype-snapshots" at "http://oss.sonatype.org/content/repositories/snapshots"
- val SonatypeReleases = "sonatype-releases" at "http://oss.sonatype.org/content/repositories/releases"
- val SpringSource = "spring-source" at "http://repository.springsource.com/ivy/bundles/release"
- val SpringExternal = "spring-external" at "http://repository.springsource.com/maven/bundles/external"
- }
-
- val resolutionRepos = Seq[Resolver](
- ScalaToolsReleases,
- ScalaToolsSnapshots,
- Repos.SonatypeReleases,
- Repos.SonatypeSnapshots,
- Repos.SpringExternal,
- Repos.SpringSource,
- Repos.JavaNet
+ val commonVersion = "0.2.22-SNAPSHOT"
+ val projectScalaVersion = "2.12.1"
+ val specs2Version = "3.8.6"
+
+ val specs2Dependency = "org.specs2" %% "specs2-core" % specs2Version % "test"
+ val specs2JunitDependency = "org.specs2" %% "specs2-junit" % specs2Version % "test"
+ val specs2MockDependency = "org.specs2" %% "specs2-mock" % specs2Version % "test"
+ val logbackDependency = "ch.qos.logback" % "logback-classic" % "1.1.8" % "test"
+
+ val commonDependencies = Seq(
+ "org.slf4j" % "slf4j-api" % "1.7.22",
+ "joda-time" % "joda-time" % "2.9.7",
+ "org.joda" % "joda-convert" % "1.8.1",
+ "io.netty" % "netty-all" % "4.1.6.Final",
+ "org.javassist" % "javassist" % "3.21.0-GA",
+ specs2Dependency,
+ specs2JunitDependency,
+ specs2MockDependency,
+ logbackDependency
)
- val dependencies = Seq(
- // main dependencies
- "io.netty" % "netty" % "3.3.1.Final",
- "commons-pool" % "commons-pool" % "1.6",
- "joda-time" % "joda-time" % "2.0",
- "org.joda" % "joda-convert" % "1.2",
- "ch.qos.logback" % "logback-classic" % "1.0.0",
- "org.scala-lang" % "scala-compiler" % "2.9.1",
- "org.scala-lang" % "scala-library" % "2.9.1",
-
- // test dependencies
- "org.specs2" % "specs2_2.9.1" % "1.9" % "test"
+ val implementationDependencies = Seq(
+ specs2Dependency,
+ logbackDependency
)
-}
\ No newline at end of file
+ val baseSettings = Defaults.defaultSettings ++ Seq(
+ scalacOptions :=
+ Opts.compile.encoding("UTF8")
+ :+ Opts.compile.deprecation
+ :+ Opts.compile.unchecked
+ :+ "-feature"
+ ,
+ testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "sequential"),
+ scalacOptions in doc := Seq("-doc-external-doc:scala=http://www.scala-lang.org/archives/downloads/distrib/files/nightly/docs/library/"),
+ crossScalaVersions := Seq(projectScalaVersion, "2.10.6", "2.11.8"),
+ javacOptions := Seq("-source", "1.6", "-target", "1.6", "-encoding", "UTF8"),
+ organization := "com.github.mauricio",
+ version := commonVersion,
+ parallelExecution := false,
+ publishArtifact in Test := false,
+ publishMavenStyle := true,
+ pomIncludeRepository := {
+ _ => false
+ },
+ publishTo <<= version {
+ v: String =>
+ val nexus = "https://oss.sonatype.org/"
+ if (v.trim.endsWith("SNAPSHOT"))
+ Some("snapshots" at nexus + "content/repositories/snapshots")
+ else
+ Some("releases" at nexus + "service/local/staging/deploy/maven2")
+ },
+ pomExtra := (
+ https://github.com/mauricio/postgresql-async
+
+
+ APACHE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
+ repo
+
+
+
+ git@github.com:mauricio/postgresql-netty.git
+ scm:git:git@github.com:mauricio/postgresql-netty.git
+
+
+
+ mauricio
+ Maurício Linhares
+ https://github.com/mauricio
+
+
+ )
+ )
+
+}
diff --git a/project/build.properties b/project/build.properties
new file mode 100644
index 00000000..e0cbc71d
--- /dev/null
+++ b/project/build.properties
@@ -0,0 +1 @@
+sbt.version = 0.13.13
\ No newline at end of file
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 38e0e5a5..0e9ec632 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,5 +1,11 @@
-addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.0.0")
+addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0")
-resolvers += "sbt-idea-repo" at "http://mpeltonen.github.com/maven/"
+addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0")
-addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.0.0")
\ No newline at end of file
+addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0")
+
+addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.3.0")
+
+resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
+
+// pgpSigningKey := Some(0xB98761578C650D77L)
diff --git a/script/prepare_build.sh b/script/prepare_build.sh
new file mode 100755
index 00000000..068ab389
--- /dev/null
+++ b/script/prepare_build.sh
@@ -0,0 +1,46 @@
+#!/usr/bin/env sh
+
+SCRIPTDIR=`dirname $0`
+
+echo "Preparing MySQL configs"
+mysql -u root -e 'create database mysql_async_tests;'
+mysql -u root -e "create table mysql_async_tests.transaction_test (id varchar(255) not null, primary key (id))"
+mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'mysql_async'@'localhost' IDENTIFIED BY 'root' WITH GRANT OPTION";
+mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'mysql_async_old'@'localhost' WITH GRANT OPTION";
+mysql -u root -e "UPDATE mysql.user SET Password = OLD_PASSWORD('do_not_use_this'), plugin = 'mysql_old_password' where User = 'mysql_async_old'; flush privileges;";
+mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'mysql_async_nopw'@'localhost' WITH GRANT OPTION";
+
+echo "preparing postgresql configs"
+
+PGUSER=postgres
+PGCONF=/etc/postgresql/9.1/main
+PGDATA=/var/ramfs/postgresql/9.1/main
+
+psql -d "postgres" -c 'create database netty_driver_test;' -U $PGUSER
+psql -d "postgres" -c 'create database netty_driver_time_test;' -U $PGUSER
+psql -d "postgres" -c "alter database netty_driver_time_test set timezone to 'GMT'" -U $PGUSER
+psql -d "netty_driver_test" -c "create table transaction_test ( id varchar(255) not null, constraint id_unique primary key (id))" -U $PGUSER
+psql -d "postgres" -c "CREATE USER postgres_md5 WITH PASSWORD 'postgres_md5'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_md5;" -U $PGUSER
+psql -d "postgres" -c "CREATE USER postgres_cleartext WITH PASSWORD 'postgres_cleartext'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_cleartext;" -U $PGUSER
+psql -d "postgres" -c "CREATE USER postgres_kerberos WITH PASSWORD 'postgres_kerberos'; GRANT ALL PRIVILEGES ON DATABASE netty_driver_test to postgres_kerberos;" -U $PGUSER
+psql -d "netty_driver_test" -c "CREATE TYPE example_mood AS ENUM ('sad', 'ok', 'happy');" -U $PGUSER
+
+sudo chmod 666 $PGCONF/pg_hba.conf
+
+echo "pg_hba.conf goes as follows"
+cat "$PGCONF/pg_hba.conf"
+
+sudo echo "local all all trust" > $PGCONF/pg_hba.conf
+sudo echo "host all postgres 127.0.0.1/32 trust" >> $PGCONF/pg_hba.conf
+sudo echo "host all postgres_md5 127.0.0.1/32 md5" >> $PGCONF/pg_hba.conf
+sudo echo "host all postgres_cleartext 127.0.0.1/32 password" >> $PGCONF/pg_hba.conf
+sudo echo "host all postgres_kerberos 127.0.0.1/32 krb5" >> $PGCONF/pg_hba.conf
+
+echo "pg_hba.conf is now like"
+cat "$PGCONF/pg_hba.conf"
+
+sudo chmod 600 $PGCONF/pg_hba.conf
+
+sudo cp -f $SCRIPTDIR/server.crt $SCRIPTDIR/server.key $PGDATA
+
+sudo /etc/init.d/postgresql restart
diff --git a/script/server.crt b/script/server.crt
new file mode 100644
index 00000000..aeef86f2
--- /dev/null
+++ b/script/server.crt
@@ -0,0 +1,75 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 9913731310682600948 (0x8994a61a13e775f4)
+ Signature Algorithm: sha1WithRSAEncryption
+ Issuer: CN=localhost
+ Validity
+ Not Before: Mar 6 08:12:28 2016 GMT
+ Not After : Apr 5 08:12:28 2016 GMT
+ Subject: CN=localhost
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:ce:26:60:f9:0d:0f:f1:d6:ed:3e:79:91:55:6a:
+ 18:63:23:96:f2:60:50:3d:e3:dd:72:e8:c2:54:17:
+ 50:be:f0:9c:32:95:39:75:b1:04:a7:bb:f5:10:a4:
+ eb:d0:10:e2:17:45:d3:f9:35:8e:b4:8f:14:97:8f:
+ 27:93:d7:20:05:e2:dc:68:64:bc:fd:f2:19:17:94:
+ e8:2f:a6:b2:54:3f:df:3e:e7:8f:f1:52:15:7a:30:
+ 81:4d:bb:6f:22:8c:ca:e1:cb:6a:72:6d:fa:89:50:
+ e7:ee:07:d1:84:8a:71:07:dc:3f:6f:1f:db:10:e9:
+ 93:ad:01:c5:2b:51:ce:58:ef:12:95:00:16:e8:d4:
+ 46:07:35:ee:10:47:c4:f7:ff:47:17:52:a5:bb:5c:
+ cb:3c:f6:6b:c8:e7:d9:7c:18:39:a1:8f:e0:45:82:
+ 88:b5:27:f3:58:cb:ba:30:c0:8a:77:5b:00:bf:09:
+ 10:b1:ad:aa:f4:1b:2c:a1:f9:a5:59:57:c8:ef:de:
+ 54:ad:35:af:67:7e:29:bc:9a:2a:d2:f0:b1:9c:34:
+ 3c:bc:64:c9:4c:93:2c:7d:29:f4:1a:ac:f3:44:42:
+ a4:c9:06:1e:a4:73:e6:aa:67:d0:e4:02:02:ba:51:
+ 1e:97:44:b8:4b:4e:55:cd:e6:24:49:08:ac:9b:09:
+ 19:31
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Subject Key Identifier:
+ 2E:20:4D:E1:12:2A:B0:6F:52:7F:62:90:D4:78:7B:E3:7D:D5:60:10
+ X509v3 Authority Key Identifier:
+ keyid:2E:20:4D:E1:12:2A:B0:6F:52:7F:62:90:D4:78:7B:E3:7D:D5:60:10
+
+ X509v3 Basic Constraints:
+ CA:TRUE
+ Signature Algorithm: sha1WithRSAEncryption
+ 9b:e8:50:8b:86:0f:bf:22:c6:b4:ef:3e:c9:a2:55:fb:69:fc:
+ ae:93:7b:5e:6a:b6:ed:5b:27:c2:9e:36:d6:f1:f1:0f:67:65:
+ 87:de:05:21:6e:0e:f4:df:ac:72:61:47:f8:fd:16:9b:3d:54:
+ ef:21:cf:b7:31:ba:bf:c9:1b:2c:a0:f9:f1:6b:45:5a:98:25:
+ b9:01:99:cf:e1:79:c5:6a:20:ce:ca:ca:3f:6d:56:f3:65:51:
+ 31:98:01:b9:96:99:04:9c:ab:ae:fb:3f:f8:ad:60:66:77:54:
+ b2:81:e3:7c:6b:c4:36:ae:ae:5c:c6:1a:09:5c:d6:13:da:2b:
+ ba:ef:3f:3e:b2:13:f2:51:15:c5:1b:9c:22:be:b4:55:9b:15:
+ 70:60:3d:98:6e:ef:53:4c:c7:20:60:3f:17:f3:cc:76:47:96:
+ 27:05:84:0e:db:21:e1:76:b7:9c:38:35:19:ef:52:d4:fc:bd:
+ ec:95:2e:eb:4b:5b:0b:c8:86:d7:23:c2:76:14:f3:93:6f:c0:
+ a9:b6:ca:f8:47:3e:9d:af:11:5d:73:79:68:70:26:f9:fd:39:
+ 60:c1:c3:c7:a9:fc:48:b5:c0:e6:b4:2e:07:de:6a:ca:ed:04:
+ 67:31:b8:0b:d0:48:fd:3b:4c:12:8a:34:5c:18:3f:38:85:f2:
+ 1c:96:39:50
+-----BEGIN CERTIFICATE-----
+MIIC+zCCAeOgAwIBAgIJAImUphoT53X0MA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
+BAMMCWxvY2FsaG9zdDAeFw0xNjAzMDYwODEyMjhaFw0xNjA0MDUwODEyMjhaMBQx
+EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
+ggEBAM4mYPkND/HW7T55kVVqGGMjlvJgUD3j3XLowlQXUL7wnDKVOXWxBKe79RCk
+69AQ4hdF0/k1jrSPFJePJ5PXIAXi3GhkvP3yGReU6C+mslQ/3z7nj/FSFXowgU27
+byKMyuHLanJt+olQ5+4H0YSKcQfcP28f2xDpk60BxStRzljvEpUAFujURgc17hBH
+xPf/RxdSpbtcyzz2a8jn2XwYOaGP4EWCiLUn81jLujDAindbAL8JELGtqvQbLKH5
+pVlXyO/eVK01r2d+KbyaKtLwsZw0PLxkyUyTLH0p9Bqs80RCpMkGHqRz5qpn0OQC
+ArpRHpdEuEtOVc3mJEkIrJsJGTECAwEAAaNQME4wHQYDVR0OBBYEFC4gTeESKrBv
+Un9ikNR4e+N91WAQMB8GA1UdIwQYMBaAFC4gTeESKrBvUn9ikNR4e+N91WAQMAwG
+A1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJvoUIuGD78ixrTvPsmiVftp
+/K6Te15qtu1bJ8KeNtbx8Q9nZYfeBSFuDvTfrHJhR/j9Fps9VO8hz7cxur/JGyyg
++fFrRVqYJbkBmc/hecVqIM7Kyj9tVvNlUTGYAbmWmQScq677P/itYGZ3VLKB43xr
+xDaurlzGGglc1hPaK7rvPz6yE/JRFcUbnCK+tFWbFXBgPZhu71NMxyBgPxfzzHZH
+licFhA7bIeF2t5w4NRnvUtT8veyVLutLWwvIhtcjwnYU85NvwKm2yvhHPp2vEV1z
+eWhwJvn9OWDBw8ep/Ei1wOa0LgfeasrtBGcxuAvQSP07TBKKNFwYPziF8hyWOVA=
+-----END CERTIFICATE-----
diff --git a/script/server.key b/script/server.key
new file mode 100644
index 00000000..0e226429
--- /dev/null
+++ b/script/server.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAziZg+Q0P8dbtPnmRVWoYYyOW8mBQPePdcujCVBdQvvCcMpU5
+dbEEp7v1EKTr0BDiF0XT+TWOtI8Ul48nk9cgBeLcaGS8/fIZF5ToL6ayVD/fPueP
+8VIVejCBTbtvIozK4ctqcm36iVDn7gfRhIpxB9w/bx/bEOmTrQHFK1HOWO8SlQAW
+6NRGBzXuEEfE9/9HF1Klu1zLPPZryOfZfBg5oY/gRYKItSfzWMu6MMCKd1sAvwkQ
+sa2q9BssofmlWVfI795UrTWvZ34pvJoq0vCxnDQ8vGTJTJMsfSn0GqzzREKkyQYe
+pHPmqmfQ5AICulEel0S4S05VzeYkSQismwkZMQIDAQABAoIBAH80v3Hu1X/tl8eN
+TFjgdtv2Ahbdx6XpDaTya7doC7NG1ZuA6UvuR2kZWkdC/SAOyvSBaiPFIKHaCGLd
+OxbHEEORkV/5iYVJ9qHOiNeejTvfjepLCU9nz0ju1VsZ5aH0LtzVoIGry4UgH32J
+5YdbxhOLnLj9dzggabe/9+KbQDEveGTzkIvSJ1nbts7c8IRp6t/1nBz54BhawUjJ
+IbaEbCH/mEmiCOUP914SCAUEfmgbMhdx8dc4V9nyxK+bulF3WIEpVZU1zj5Rpyni
+P8gQ1geI64Erd8oa4DJ5C77eLuKKk0JBCkgh5x3hiAxuvN0zxHxW2Q75c6x9uDr5
+DXi20GECgYEA+NRW6heYBJw7Lt7+cQCRG5/WFOX9TmmK9EAidVPULWO4NN4wLZxa
+exW/epg8w1Y+u+BHOzFq9idJaHsoLZCmoNWMkZsP+AzeEkklee6wgur3/Zs1HqHZ
+1VA3EmvOecz++3o69zcjd0nzgk9ADhjA2dAahKTnn5RESD1dFBWU2+sCgYEA1Bcv
+PiQe6ce86FlSPr0TBFvIJl2dfjrQijL3dhZMo+1Y5VTShGBoAQKfBhJITSbsmaEz
+UQ/4rBMyTN9bwvSwsDpQZw/Y0YKiSQIOr4J0jyotY5RN2AH3AlCX8CrhoOmBaLUd
+n2SGx5keodnXn1/GPkuGPIa7xnGib/gdL2AaZFMCgYBV5AX0XByPStZrAXJW01lD
+bdLZ9+GOFYRvd0vtr/gHiupk5WU/+T6KSiGEUdR3oOeatnogBpjjSwBd3lUqFUpP
+LieNgzbp6pclPLaA9lFbf3wGwHJ/lmK47S11YF0vUgGaEMEV4KSPYql5i52SwByh
+kuH0c2+4d9dyECx26FQv7QKBgQDBtX83oWP+n6hhCpu8o5IH7BAtQlmDHhKz9oLf
+/tP28OO9abBwqWC0c4Fs2SviE4gLdRjak9zKxSmu3l3//N6XxlsDFo0wJcE1L0Tc
+dikhTSNxjNVgUcMaASQUfgXfowXH7YvltboH+UjqCH4QmTgGU5KCG4jLYaQ74gA9
+8eeI8wKBgDfclcMsJnY6FpFoR0Ub9VOrdbKtD9nXSxhTSFKjrp4JM7SBN3u6NPJK
+FgKZyQxd1bX/RBioN1prrZ3rbg+9awc65KhyfwtNxiurCBZhYObhKJv7lZyjNgsT
+EALMKvB+fdpMtPZOVtUl0MbHEBblrJ+oy4TPT/kvMuCudF/5arcZ
+-----END RSA PRIVATE KEY-----
diff --git a/src/main/scala/com/github/mauricio/postgresql/ChannelUtils.scala b/src/main/scala/com/github/mauricio/postgresql/ChannelUtils.scala
deleted file mode 100644
index 434dced6..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/ChannelUtils.scala
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.github.mauricio.postgresql
-
-import org.jboss.netty.buffer.ChannelBuffer
-import util.Log
-
-/**
- * User: Maurício Linhares
- * Date: 3/1/12
- * Time: 2:02 AM
- */
-
-object ChannelUtils {
-
- private val log = Log.getByName("ChannelUtils")
-
- def writeLength( buffer : ChannelBuffer ) {
-
- val length = buffer.writerIndex() - 1
- buffer.markWriterIndex()
- buffer.writerIndex(1)
- buffer.writeInt( length )
-
- buffer.resetWriterIndex()
-
- }
-
- def printBuffer( b : ChannelBuffer ) : Unit = {
-
- val bytes = new Array[Byte](b.readableBytes())
- b.markReaderIndex()
- b.readBytes(bytes)
- b.resetReaderIndex()
-
- println(bytes.mkString("-"))
-
- }
-
- def writeCString( content : String, b : ChannelBuffer ) : Unit = {
- b.writeBytes( content.getBytes( CharsetHelper.Unicode ) )
- b.writeByte(0)
- }
-
- def readCString( b : ChannelBuffer ) : String = {
-
- b.markReaderIndex()
-
- var byte : Byte = 0
- var count = 0
-
- do {
- byte = b.readByte()
- count+= 1
- } while ( byte != 0 )
-
- b.resetReaderIndex()
-
- val result = b.toString( b.readerIndex(), count - 1, CharsetHelper.Unicode )
-
- b.readerIndex( b.readerIndex() + count)
-
- return result
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/CharsetHelper.scala b/src/main/scala/com/github/mauricio/postgresql/CharsetHelper.scala
deleted file mode 100644
index c69b6c02..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/CharsetHelper.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.github.mauricio.postgresql
-
-import java.nio.charset.Charset
-
-/**
- * User: Maurício Linhares
- * Date: 2/28/12
- * Time: 10:46 PM
- */
-
-object CharsetHelper {
-
- val Unicode = Charset.forName("UTF-8")
-
- def toBytes( content : String ) : Array[Byte] = {
- content.getBytes( Unicode )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/Connection.scala b/src/main/scala/com/github/mauricio/postgresql/Connection.scala
deleted file mode 100644
index 83b1f5c6..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/Connection.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.github.mauricio.postgresql
-
-import util.Future
-
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 10:40 PM
- */
-
-trait Connection {
-
- def disconnect
- def isConnected : Boolean
- def sendQuery( query : String ) : Future[Throwable,QueryResult]
- def sendPreparedStatement( query : String, values : Array[Any] = Array.empty[Any] ) : Future[Throwable, QueryResult]
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/ConnectionObjectFactory.scala b/src/main/scala/com/github/mauricio/postgresql/ConnectionObjectFactory.scala
deleted file mode 100644
index d9f14281..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/ConnectionObjectFactory.scala
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.github.mauricio.postgresql
-
-import org.apache.commons.pool.PoolableObjectFactory
-import java.util.concurrent.TimeUnit
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 10:40 PM
- */
-
-class ConnectionObjectFactory(
- val host : String,
- val port : Int,
- val user: String,
- val database: String) extends PoolableObjectFactory[DatabaseConnectionHandler] {
-
- def makeObject(): DatabaseConnectionHandler = {
- val connection = new DatabaseConnectionHandler(host, port, user, database)
- connection.connect.get(5, TimeUnit.SECONDS)
- connection
- }
-
- def destroyObject(obj: DatabaseConnectionHandler) {
- obj.disconnect
- }
-
- def validateObject(obj: DatabaseConnectionHandler): Boolean = {
- obj.isConnected
- }
-
- def activateObject(obj: DatabaseConnectionHandler) {
- //no op
- }
-
- def passivateObject(obj: DatabaseConnectionHandler) {
- //no op
- }
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/ConnectionPool.scala b/src/main/scala/com/github/mauricio/postgresql/ConnectionPool.scala
deleted file mode 100644
index fdcdfd95..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/ConnectionPool.scala
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.github.mauricio.postgresql
-
-import org.apache.commons.pool.impl.StackObjectPool
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 10:38 PM
- */
-
-class ConnectionPool(
- val host : String,
- val port : Int,
- val user: String,
- val database: String) {
-
- private val factory = new ConnectionObjectFactory(host, port, user, database)
- private val pool = new StackObjectPool( this.factory, 1 )
-
- def doWithConnection[T]( fn : Connection => T ) : T = {
- val borrowed = this.pool.borrowObject()
- try {
- fn(borrowed)
- } finally {
- this.pool.returnObject(borrowed)
- }
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/DatabaseConnectionHandler.scala b/src/main/scala/com/github/mauricio/postgresql/DatabaseConnectionHandler.scala
deleted file mode 100644
index 31cdd05f..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/DatabaseConnectionHandler.scala
+++ /dev/null
@@ -1,289 +0,0 @@
-package com.github.mauricio.postgresql
-
-import messages._
-import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory
-import org.jboss.netty.bootstrap.ClientBootstrap
-import org.jboss.netty.channel._
-import java.net.InetSocketAddress
-import parsers.{ColumnData, ProcessData}
-import scala.collection.JavaConversions._
-import org.jboss.netty.buffer.ChannelBuffer
-import util.{Future, SimpleFuture, Log}
-import java.util.concurrent.ConcurrentHashMap
-import org.jboss.netty.logging.{Slf4JLoggerFactory, InternalLoggerFactory}
-
-object DatabaseConnectionHandler {
- val log = Log.get[DatabaseConnectionHandler]
- val Name = "Netty-PostgreSQL-driver-0.0.1"
- InternalLoggerFactory.setDefaultFactory(new Slf4JLoggerFactory())
-}
-
-class DatabaseConnectionHandler
- (
- val host : String,
- val port : Int,
- val user: String,
- val database: String) extends SimpleChannelHandler with Connection {
-
- import DatabaseConnectionHandler._
-
- private val properties = List(
- "user" -> user,
- "database" -> database,
- "application_name" -> DatabaseConnectionHandler.Name,
- "client_encoding" -> "UTF8",
- "DateStyle" -> "ISO",
- "extra_float_digits" -> "2")
-
- private var readyForQuery = false
- private val parameterStatus = new ConcurrentHashMap[String, String]()
- private val parsedStatements = new ConcurrentHashMap[String,Array[ColumnData]]()
- private var _processData : Option[ProcessData] = None
-
- private val factory = new NioClientSocketChannelFactory(
- ExecutorServiceUtils.CachedThreadPool,
- ExecutorServiceUtils.CachedThreadPool)
-
- private val bootstrap = new ClientBootstrap(this.factory)
- private var channelFuture : ChannelFuture = null
-
- @volatile private var connected = false
- @volatile private var connectionFuture : Option[SimpleFuture[Throwable,Map[String, String]]] = None
- @volatile private var queryFuture : Option[SimpleFuture[Throwable,QueryResult]] = None
- @volatile private var currentQuery : Option[Query] = None
- @volatile private var currentPreparedStatement : Option[String] = None
-
- def isReadyForQuery : Boolean = this.readyForQuery
-
- def connect : Future[Throwable,Map[String,String]] = {
-
- this.bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
-
- override def getPipeline(): ChannelPipeline = {
- Channels.pipeline( MessageDecoder, MessageEncoder, DatabaseConnectionHandler.this )
- }
-
- });
-
- this.bootstrap.setOption("child.tcpNoDelay", true)
- this.bootstrap.setOption("child.keepAlive", true)
-
- this.connectionFuture = Some( new SimpleFuture[Throwable, Map[String, String]]() )
-
- this.channelFuture = this.bootstrap.connect(new InetSocketAddress( this.host, this.port)).awaitUninterruptibly()
-
- this.connectionFuture.get
- }
-
- override def disconnect : Unit = {
-
- if ( this.connected ) {
- this.channelFuture.getChannel.write( CloseMessage.Instance )
- }
-
- }
-
- override def isConnected : Boolean = {
- this.connected
- }
-
- def parameterStatuses : scala.collection.immutable.Map[String, String] = this.parameterStatus.toMap
-
- def processData : Option[ProcessData] = {
- _processData
- }
-
- override def channelConnected(ctx: ChannelHandlerContext, e: ChannelStateEvent): Unit = {
- this.connected = true
- e.getChannel().write( new StartupMessage( this.properties ) )
- }
-
- override def messageReceived(ctx: ChannelHandlerContext, e: MessageEvent): Unit = {
-
- e.getMessage() match {
- case m: Message => {
-
- m.name match {
- case Message.AuthenticationOk => {
- log.info( "Authenticated to the database" )
- }
- case Message.BackendKeyData => {
- this._processData = Option( m.content.asInstanceOf[ProcessData] )
- }
- case Message.BindComplete => {
- log.debug("Finished binding statement")
- }
- case Message.CommandComplete => {
- this.onCommandComplete(m)
- }
- case Message.CloseComplete => {
- log.debug("Successfully closed portal for [{}]", this.currentPreparedStatement)
- }
- case Message.DataRow => {
- this.onDataRow(m)
- }
- case Message.Error => {
- this.onError(m)
- }
- case Message.Notice => {
- log.info( "notice -> {}", m.content.asInstanceOf[List[(Char,String)]].mkString(" ") )
- }
- case Message.ParameterStatus => {
- this.onParameterStatus(m)
- }
- case Message.ParseComplete => {
- log.debug("Finished parsing statement")
- }
- case Message.ReadyForQuery => {
- this.onReadyForQuery
- }
- case Message.RowDescription => {
- this.onRowDescription( m.content.asInstanceOf[Array[ColumnData]] )
- }
- case _ => {
- throw new IllegalStateException("Handler not implemented for message %s".format( m.name ))
- }
- }
-
- }
- case _ => {
- log.error( "Unknown message type {} -> {}", e.getMessage.getClass, e.getMessage )
- throw new IllegalArgumentException( "Unknown message type - %s".format( e.getMessage() ) )
- }
-
- }
-
- }
-
- override def sendQuery( query : String ) : Future[Throwable,QueryResult] = {
- this.readyForQuery = false
- this.queryFuture = Option(new SimpleFuture[Throwable,QueryResult]())
- this.channelFuture.getChannel.write( new QueryMessage( query ) )
- this.queryFuture.get
- }
-
- override def sendPreparedStatement( query : String, values : Array[Any] = Array.empty[Any] ) : Future[Throwable,QueryResult] = {
- this.readyForQuery = false
- this.queryFuture = Some(new SimpleFuture[Throwable,QueryResult]())
-
- var paramsCount = 0
-
- val realQuery = if ( query.contains("?") ) {
- query.foldLeft( new StringBuilder() ) {
- (builder, char) =>
- if ( char == '?' ) {
- paramsCount += 1
- builder.append("$" + paramsCount)
- } else {
- builder.append( char )
- }
- builder
- }.toString()
- } else {
- query
- }
-
- this.currentPreparedStatement = Some(realQuery)
-
- if ( !this.isParsed(realQuery) ) {
- log.debug("Query is not parsed yet -> {}",realQuery)
- this.channelFuture.getChannel.write( new PreparedStatementOpeningMessage( realQuery, values ) )
- } else {
- this.currentQuery = Some( new Query(this.parsedStatements.get(realQuery)) )
- this.channelFuture.getChannel.write( new PreparedStatementExecuteMessage( realQuery, values ) )
- }
-
- this.queryFuture.get
- }
-
- override def exceptionCaught(ctx: ChannelHandlerContext, e: ExceptionEvent) {
- this.setErrorOnFutures(e.getCause)
- }
-
- private def setErrorOnFutures( e : Throwable ) {
-
- log.error( "Error on connection", e )
-
- if ( this.connectionFuture.isDefined ) {
- this.connectionFuture.get.setError(e)
- this.connectionFuture = None
- }
-
- if ( this.queryFuture.isDefined ) {
- log.error( "Setting error on future {}", this.queryFuture.get )
- this.queryFuture.get.setError( e.getCause )
- this.queryFuture = None
- this.currentPreparedStatement = None
- }
-
- }
-
- override def channelDisconnected( ctx : ChannelHandlerContext, e : ChannelStateEvent ) : Unit = {
- log.warn( "Connection disconnected - {} - {}", e.getState, e.getValue )
- this.connected = false
- }
-
- private def onReadyForQuery {
-
- this.readyForQuery = true
-
- if ( this.connectionFuture.isDefined ) {
- this.connectionFuture.get.set( this.parameterStatus.toMap )
- this.connectionFuture = None
- }
- }
-
- private def onError( m : Message ) {
- log.error("Error with message -> {}", m.content)
-
- val error = new IllegalStateException( m.content.toString )
- error.fillInStackTrace()
-
- this.setErrorOnFutures(error)
- }
-
- private def onCommandComplete(m : Message) {
-
- if ( this.queryFuture.isDefined ) {
-
- val result = m.content.asInstanceOf[(Int,String)]
-
- val queryResult = if ( this.currentQuery.isDefined ) {
- new QueryResult( result._1, result._2, Some(this.currentQuery.get) )
- } else {
- new QueryResult( result._1, result._2, None )
- }
-
- this.queryFuture.get.set(queryResult)
- this.queryFuture = None
- this.currentPreparedStatement = None
-
- }
- }
-
- private def onParameterStatus(m : Message) {
- val pair = m.content.asInstanceOf[(String, String)]
- this.parameterStatus.put( pair._1, pair._2 )
- }
-
- private def onDataRow(m : Message) {
- this.currentQuery.get.addRawRow(m.content.asInstanceOf[Array[ChannelBuffer]])
- }
-
- private def onRowDescription( values : Array[ColumnData] ) {
- log.debug("received query description")
- this.currentQuery = Option( new Query( values ) )
-
- log.debug("Current prepared statement is {}", this.currentPreparedStatement)
-
- if ( this.currentPreparedStatement.isDefined ) {
- this.parsedStatements.put(this.currentPreparedStatement.get, values)
- log.debug("parsed statements are -> {}", this.parsedStatements)
- }
- }
-
- private def isParsed( query : String ) : Boolean = {
- this.parsedStatements.containsKey(query)
- }
-
-}
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/EncoderNotAvailableException.scala b/src/main/scala/com/github/mauricio/postgresql/EncoderNotAvailableException.scala
deleted file mode 100644
index 40bed2f3..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/EncoderNotAvailableException.scala
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.github.mauricio.postgresql
-
-/**
- * User: Maurício Linhares
- * Date: 3/4/12
- * Time: 12:19 AM
- */
-
-class EncoderNotAvailableException( message : FrontendMessage )
- extends IllegalArgumentException( "Encoder not available for name %s".format( message.kind ) )
diff --git a/src/main/scala/com/github/mauricio/postgresql/ExecutorServiceUtils.scala b/src/main/scala/com/github/mauricio/postgresql/ExecutorServiceUtils.scala
deleted file mode 100644
index 09d2e3fc..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/ExecutorServiceUtils.scala
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.github.mauricio.postgresql
-
-import java.util.concurrent.Executors
-import util.DaemonThreadsFactory
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 9:25 PM
- */
-
-object ExecutorServiceUtils {
- val CachedThreadPool = Executors.newCachedThreadPool( DaemonThreadsFactory )
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/FrontendMessage.scala b/src/main/scala/com/github/mauricio/postgresql/FrontendMessage.scala
deleted file mode 100644
index eab893e8..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/FrontendMessage.scala
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.github.mauricio.postgresql
-
-/**
- * User: Maurício Linhares
- * Date: 3/7/12
- * Time: 9:30 AM
- */
-
-class FrontendMessage ( val kind : Char )
diff --git a/src/main/scala/com/github/mauricio/postgresql/Message.scala b/src/main/scala/com/github/mauricio/postgresql/Message.scala
deleted file mode 100644
index b9545351..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/Message.scala
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.github.mauricio.postgresql
-
-object Message {
- val AuthenticationOk = 'R'
- val BackendKeyData = 'K'
- val Bind = 'B'
- val BindComplete = '2'
- val CommandComplete = 'C'
- val Close = 'X'
- val CloseStatementOrPortal = 'C'
- val CloseComplete = '3'
- val DataRow = 'D'
- val Describe = 'D'
- val Error = 'E'
- val Execute = 'E'
- val EmptyQuery = 'I'
- val NoData = 'n'
- val Notice = 'N'
- val Notification = 'A'
- val ParameterStatus = 'S'
- val Parse = 'P'
- val ParseComplete = '1'
- val PortalSuspended = 's'
- val Query = 'Q'
- val RowDescription = 'T'
- val ReadyForQuery = 'Z'
- val Startup : Char = 0
- val Sync = 'S'
-}
-
-class Message ( val name : Char, val content : Any ) {
-
- override def hashCode : Int = {
- "%s-%s".format( this.name, this.content ).hashCode()
- }
-
- override def equals( other : Any ) : Boolean = {
-
- other match {
- case o : Message => {
- this.name.equals(o.name) && this.content.equals( o.content )
- }
- case _ => false
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/MessageDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/MessageDecoder.scala
deleted file mode 100644
index 4cc6d192..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/MessageDecoder.scala
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.github.mauricio.postgresql
-
-import org.jboss.netty.handler.codec.frame.FrameDecoder
-import org.jboss.netty.buffer.ChannelBuffer
-import com.github.mauricio.postgresql.parsers.{MessageParser, AuthenticationStartupParser}
-import org.jboss.netty.channel.{ChannelHandlerContext, Channel}
-
-object MessageDecoder extends FrameDecoder {
-
- override def decode(ctx: ChannelHandlerContext, c: Channel, b: ChannelBuffer) : Object = {
-
- var code : Char = 0
- var length : Int = 0
-
- if ( b.readableBytes() >= 5 ) {
-
- b.markReaderIndex()
-
- code = b.readByte().asInstanceOf[Char]
- length = b.readInt() - 4
-
- if ( b.readableBytes() >= length ) {
-
- code match {
- case 'R' => {
- AuthenticationStartupParser.parseMessage( b )
- }
- case _ => {
- MessageParser.parse( code, b.readSlice( length ) )
- }
- }
-
- } else {
- b.resetReaderIndex()
- return null
- }
-
- } else {
- return null
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/MessageEncoder.scala b/src/main/scala/com/github/mauricio/postgresql/MessageEncoder.scala
deleted file mode 100644
index 9c551f71..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/MessageEncoder.scala
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.github.mauricio.postgresql
-
-import encoders._
-import org.jboss.netty.handler.codec.oneone.OneToOneEncoder
-import org.jboss.netty.channel.{Channel, ChannelHandlerContext}
-import org.jboss.netty.buffer.ChannelBuffer
-import util.Log
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 7:14 PM
- */
-
-object MessageEncoder extends OneToOneEncoder {
-
- val log = Log.getByName("MessageEncoder")
-
- val encoders = Map(
- Message.Close -> CloseMessageEncoder,
- Message.Execute -> ExecutePreparedStatementEncoder,
- Message.Parse -> PreparedStatementOpeningEncoder,
- Message.Startup -> StartupMessageEncoder,
- Message.Query -> QueryMessageEncoder
- )
-
- override def encode(ctx: ChannelHandlerContext, channel: Channel, msg: AnyRef): ChannelBuffer = {
-
- val buffer = msg match {
- case message : FrontendMessage => {
- val option = this.encoders.get( message.kind )
- if ( option.isDefined ) {
- option.get.encode(message)
- } else {
- throw new EncoderNotAvailableException( message )
- }
- }
- case _ => {
- throw new IllegalArgumentException( "Can not encode message %s".format( msg ) )
- }
- }
-
- buffer
-
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/Query.scala b/src/main/scala/com/github/mauricio/postgresql/Query.scala
deleted file mode 100644
index 95b10029..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/Query.scala
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.github.mauricio.postgresql
-
-import parsers.ColumnData
-import org.jboss.netty.buffer.ChannelBuffer
-import util.Log
-import java.util.ArrayList
-
-/**
- * User: Maurício Linhares
- * Date: 3/4/12
- * Time: 12:42 AM
- */
-
-object Query {
- val log = Log.get[Query]
-}
-
-class Query ( val columnTypes : Array[ColumnData] ) extends IndexedSeq[Array[Any]] {
-
- import CharsetHelper._
-
- private val rows = new ArrayList[Array[Any]]()
- private val columnMapping : Map[String, Int] = this.columnTypes.map {
- columnData =>
- (columnData.name, columnData.columnNumber - 1)
- }.toMap
-
- def length: Int = this.rows.size()
-
- def apply(idx: Int): Array[Any] = this.rows.get(idx)
-
- def update(idx: Int, elem: Array[Any]) {
- this.rows.set( idx, elem )
- }
-
- def addRawRow( row : Array[ChannelBuffer] ) {
-
- val realRow = new Array[Any](row.length)
-
- 0.until(row.length).foreach {
- index =>
-
- realRow(index) = if ( row(index) == null ) {
- null
- } else {
- this.columnTypes(index).decoder.decode( row(index).toString(Unicode) )
- }
-
- }
-
- this.rows.add(realRow)
-
- }
-
- def getValue( columnNumber : Int, rowNumber : Int ) : Any = {
- this.rows.get( rowNumber )(columnNumber)
- }
-
- def getValue( columnName : String, rowNumber : Int ) : Any = {
- this.rows.get( rowNumber )( this.columnMapping( columnName ) )
- }
-
- def apply( name : String, row : Int ) : Any = {
- this.getValue( name, row)
- }
-
- def apply( column : Int, row : Int ) : Any = {
- this.getValue(column, row)
- }
-
- def count : Int = {
- this.rows.size()
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/QueryResult.scala b/src/main/scala/com/github/mauricio/postgresql/QueryResult.scala
deleted file mode 100644
index 6c42cdcb..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/QueryResult.scala
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.github.mauricio.postgresql
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 4:01 PM
- */
-
-class QueryResult ( val rowsAffected : Int, val statusMessage : String, val rows : Option[Query] ) {
-
- override def toString : String = {
- "QueryResult{rows -> %s,status -> %s}".format( this.rowsAffected, this.statusMessage )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/RowDescription.scala b/src/main/scala/com/github/mauricio/postgresql/RowDescription.scala
deleted file mode 100644
index 49743df2..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/RowDescription.scala
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.github.mauricio.postgresql
-
-/**
- * User: Maurício Linhares
- * Date: 3/1/12
- * Time: 1:58 AM
- */
-
-class RowDescription {
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/BigDecimalEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/BigDecimalEncoderDecoder.scala
deleted file mode 100644
index 8c5f0830..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/BigDecimalEncoderDecoder.scala
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 9:42 AM
- */
-
-object BigDecimalEncoderDecoder extends ColumnEncoderDecoder {
-
- def decode( value : String ) : Any = {
- BigDecimal(value)
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/BooleanEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/BooleanEncoderDecoder.scala
deleted file mode 100644
index d8c998e5..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/BooleanEncoderDecoder.scala
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 9:50 AM
- */
-
-object BooleanEncoderDecoder extends ColumnEncoderDecoder {
-
- override def decode(value: String): Any = {
- if ( "t" == value ) {
- true
- } else {
- false
- }
- }
-
- override def encode( value : Any ) : String = {
- val result = value.asInstanceOf[Boolean]
-
- if (result) {
- "t"
- } else {
- "f"
- }
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/ColumnEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/ColumnEncoderDecoder.scala
deleted file mode 100644
index d86318d3..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/ColumnEncoderDecoder.scala
+++ /dev/null
@@ -1,163 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-import org.joda.time.{DateTime, LocalTime, LocalDate}
-
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 9:34 AM
- */
-
-class ColumnDecoderNotFoundException( kind : Int )
- extends IllegalArgumentException( "There is no decoder available for kind %s".format(kind) )
-
-object ColumnEncoderDecoder {
-
- val Bigserial = 20
- val Char = 18
- val Smallint = 21
- val Integer = 23
- val Numeric = 1700 // Decimal is the same as Numeric on PostgreSQL
- val Real = 700
- val Double = 701
- val Serial = 23
- val Bpchar = 1042
- val Varchar = 1043 // Char is the same as Varchar on PostgreSQL
- val Text = 25
- val Timestamp = 1114
- val TimestampWithTimezone = 1184
- val Date = 1082
- val Time = 1083
- val TimeWithTimezone = 1266
- val Boolean = 16
-
- private val classes = Map[Class[_], Int](
- classOf[Int] -> Integer,
- classOf[Short] -> Integer,
- classOf[java.lang.Integer] -> Integer,
- classOf[java.lang.Short] -> Integer,
-
- classOf[Long] -> Bigserial,
- classOf[java.lang.Long] -> Bigserial,
-
- classOf[String] -> Varchar,
- classOf[java.lang.String] -> Varchar,
-
- classOf[Float] -> Real,
- classOf[java.lang.Float] -> Real,
-
- classOf[Double] -> Double,
- classOf[java.lang.Double] -> Double,
-
- classOf[BigDecimal] -> Numeric,
- classOf[java.math.BigDecimal] -> Numeric,
-
- classOf[LocalDate] -> Date,
-
- classOf[LocalTime] -> Time,
-
- classOf[DateTime] -> Timestamp
- )
-
- def decoderFor(kind: Int): ColumnEncoderDecoder = {
- kind match {
- case Boolean => BooleanEncoderDecoder
- case Char => StringEncoderDecoder
- case Bigserial => LongEncoderDecoder
- case Smallint => IntegerEncoderDecoder
- case Integer => IntegerEncoderDecoder
- case Text => StringEncoderDecoder
- case Real => FloatEncoderDecoder
- case Double => DoubleEncoderDecoder
- case Varchar => StringEncoderDecoder
- case Bpchar => StringEncoderDecoder
- case Numeric => BigDecimalEncoderDecoder
- case Timestamp => TimestampEncoderDecoder
- case TimestampWithTimezone => TimestampWithTimezoneEncoderDecoder
- case Date => DateEncoderDecoder
- case Time => TimeEncoderDecoder
- case TimestampWithTimezone => TimestampWithTimezoneEncoderDecoder
- case _ => StringEncoderDecoder
- }
- }
-
- def kindFor( clazz : Class[_] ) : Int = {
- this.classes.get(clazz).getOrElse { 0 }
- }
-
- def decode( kind : Int, value : String ) : Any = {
- decoderFor(kind).decode(value)
- }
-
- def encode( value : Any ) : String = {
- decoderFor( kindFor( value.asInstanceOf[AnyRef].getClass ) ).encode(value)
- }
-
-}
-
-trait ColumnEncoderDecoder {
-
- def decode(value: String): Any
- def encode(value: Any) : String = {
- value.toString
- }
-
-}
-
-/*
-
- public static final int UNSPECIFIED = 0;
- public static final int INT2 = 21;
- public static final int INT2_ARRAY = 1005;
- public static final int INT4 = 23;
- public static final int INT4_ARRAY = 1007;
- public static final int INT8 = 20;
- public static final int INT8_ARRAY = 1016;
- public static final int TEXT = 25;
- public static final int TEXT_ARRAY = 1009;
- public static final int NUMERIC = 1700;
- public static final int NUMERIC_ARRAY = 1231;
- public static final int FLOAT4 = 700;
- public static final int FLOAT4_ARRAY = 1021;
- public static final int FLOAT8 = 701;
- public static final int FLOAT8_ARRAY = 1022;
- public static final int BOOL = 16;
- public static final int BOOL_ARRAY = 1000;
- public static final int DATE = 1082;
- public static final int DATE_ARRAY = 1182;
- public static final int TIME = 1083;
- public static final int TIME_ARRAY = 1183;
- public static final int TIMETZ = 1266;
- public static final int TIMETZ_ARRAY = 1270;
- public static final int TIMESTAMP = 1114;
- public static final int TIMESTAMP_ARRAY = 1115;
- public static final int TIMESTAMPTZ = 1184;
- public static final int TIMESTAMPTZ_ARRAY = 1185;
- public static final int BYTEA = 17;
- public static final int BYTEA_ARRAY = 1001;
- public static final int VARCHAR = 1043;
- public static final int VARCHAR_ARRAY = 1015;
- public static final int OID = 26;
- public static final int OID_ARRAY = 1028;
- public static final int BPCHAR = 1042;
- public static final int BPCHAR_ARRAY = 1014;
- public static final int MONEY = 790;
- public static final int MONEY_ARRAY = 791;
- public static final int NAME = 19;
- public static final int NAME_ARRAY = 1003;
- public static final int BIT = 1560;
- public static final int BIT_ARRAY = 1561;
- public static final int VOID = 2278;
- public static final int INTERVAL = 1186;
- public static final int INTERVAL_ARRAY = 1187;
- public static final int CHAR = 18; // This is not char(N), this is "char" a single byte type.
- public static final int CHAR_ARRAY = 1002;
- public static final int VARBIT = 1562;
- public static final int VARBIT_ARRAY = 1563;
- public static final int UUID = 2950;
- public static final int UUID_ARRAY = 2951;
- public static final int XML = 142;
- public static final int XML_ARRAY = 143;
-
-*/
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/DateEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/DateEncoderDecoder.scala
deleted file mode 100644
index d7c340be..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/DateEncoderDecoder.scala
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-import org.joda.time.LocalDate
-import org.joda.time.format.DateTimeFormat
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 6:12 PM
- */
-
-object DateEncoderDecoder extends ColumnEncoderDecoder {
-
- private val parser = DateTimeFormat.forPattern("yyyy-MM-dd")
-
- override def decode(value: String): LocalDate = {
- this.parser.parseLocalDate(value)
- }
-
- override def encode( value : Any ) : String = {
- this.parser.print( value.asInstanceOf[LocalDate] )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/DoubleEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/DoubleEncoderDecoder.scala
deleted file mode 100644
index cba71aed..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/DoubleEncoderDecoder.scala
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 9:47 AM
- */
-
-object DoubleEncoderDecoder extends ColumnEncoderDecoder {
- def decode(value: String): Double = {
- value.toDouble
- }
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/FloatEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/FloatEncoderDecoder.scala
deleted file mode 100644
index 0cec9882..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/FloatEncoderDecoder.scala
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 9:47 AM
- */
-
-object FloatEncoderDecoder extends ColumnEncoderDecoder {
- def decode(value: String): Float = {
- value.toFloat
- }
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/IntegerEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/IntegerEncoderDecoder.scala
deleted file mode 100644
index 5524922d..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/IntegerEncoderDecoder.scala
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 9:39 AM
- */
-
-object IntegerEncoderDecoder extends ColumnEncoderDecoder {
-
- def decode(value: String): Int = {
- value.toInt
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/LongEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/LongEncoderDecoder.scala
deleted file mode 100644
index dd8c9d3d..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/LongEncoderDecoder.scala
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 9:55 AM
- */
-
-object LongEncoderDecoder extends ColumnEncoderDecoder {
- def decode(value: String): Long = {
- value.toLong
- }
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/StringEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/StringEncoderDecoder.scala
deleted file mode 100644
index 1298ed61..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/StringEncoderDecoder.scala
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 9:45 AM
- */
-
-object StringEncoderDecoder extends ColumnEncoderDecoder {
- override def decode(value: String): String = {
- value
- }
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/TimeEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/TimeEncoderDecoder.scala
deleted file mode 100644
index 271d82f7..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/TimeEncoderDecoder.scala
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-import org.joda.time.format.DateTimeFormat
-import org.joda.time.LocalTime
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 6:13 PM
- */
-
-object TimeEncoderDecoder extends ColumnEncoderDecoder {
-
- val parser = DateTimeFormat.forPattern("HH:mm:ss.SSSSSS")
-
- def decode(value: String): LocalTime = {
- parser.parseLocalTime(value)
- }
-
- override def encode( value : Any ) : String = {
- this.parser.print( value.asInstanceOf[LocalTime] )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/TimeWithTimezoneEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/TimeWithTimezoneEncoderDecoder.scala
deleted file mode 100644
index 5e4179a5..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/TimeWithTimezoneEncoderDecoder.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-import org.joda.time.LocalTime
-import org.joda.time.format.DateTimeFormat
-
-/**
- * User: Maurício Linhares
- * Date: 3/11/12
- * Time: 5:35 PM
- */
-
-object TimeWithTimezoneEncoderDecoder extends ColumnEncoderDecoder {
- private val parser = DateTimeFormat.forPattern("HH:mm:ss.SSSSSSZ")
-
- override def decode(value: String): LocalTime = {
- parser.parseLocalTime(value)
- }
-
- override def encode( value : Any ) : String = {
- this.parser.print( value.asInstanceOf[LocalTime] )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/TimestampEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/TimestampEncoderDecoder.scala
deleted file mode 100644
index fdb18e7b..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/TimestampEncoderDecoder.scala
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-import org.joda.time.format.DateTimeFormat
-import org.joda.time.DateTime
-
-/**
- * User: Maurício Linhares
- * Date: 3/5/12
- * Time: 6:10 PM
- */
-
-object TimestampEncoderDecoder extends ColumnEncoderDecoder {
-
- private val parser = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
-
- override def decode(value: String): DateTime = {
- parser.parseDateTime(value)
- }
-
- override def encode( value : Any ) : String = {
- this.parser.print( value.asInstanceOf[DateTime] )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/column/TimestampWithTimezoneEncoderDecoder.scala b/src/main/scala/com/github/mauricio/postgresql/column/TimestampWithTimezoneEncoderDecoder.scala
deleted file mode 100644
index 9015102c..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/column/TimestampWithTimezoneEncoderDecoder.scala
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.mauricio.postgresql.column
-
-import org.joda.time.format.DateTimeFormat
-import org.joda.time.DateTime
-
-/**
- * User: Maurício Linhares
- * Date: 3/6/12
- * Time: 9:27 AM
- */
-
-object TimestampWithTimezoneEncoderDecoder extends ColumnEncoderDecoder {
-
- private val parser = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss.SSSSSSZ")
-
- override def decode(value: String): DateTime = {
- parser.parseDateTime(value)
- }
-
- override def encode( value : Any ) : String = {
- this.parser.print( value.asInstanceOf[DateTime] )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/encoders/CloseMessageEncoder.scala b/src/main/scala/com/github/mauricio/postgresql/encoders/CloseMessageEncoder.scala
deleted file mode 100644
index e087c8c5..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/encoders/CloseMessageEncoder.scala
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.github.mauricio.postgresql.encoders
-
-import org.jboss.netty.buffer.{ChannelBuffers, ChannelBuffer}
-import com.github.mauricio.postgresql.FrontendMessage
-
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 8:41 PM
- */
-
-object CloseMessageEncoder extends Encoder {
-
- override def encode(message: FrontendMessage): ChannelBuffer = {
- val buffer = ChannelBuffers.buffer(5)
- buffer.writeByte('X')
- buffer.writeInt(4)
-
- buffer
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/encoders/Encoder.scala b/src/main/scala/com/github/mauricio/postgresql/encoders/Encoder.scala
deleted file mode 100644
index 429b325e..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/encoders/Encoder.scala
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.github.mauricio.postgresql.encoders
-
-import org.jboss.netty.buffer.ChannelBuffer
-import com.github.mauricio.postgresql.FrontendMessage
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 7:16 PM
- */
-
-trait Encoder {
-
- def encode( message : FrontendMessage ) : ChannelBuffer
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/encoders/ExecutePreparedStatementEncoder.scala b/src/main/scala/com/github/mauricio/postgresql/encoders/ExecutePreparedStatementEncoder.scala
deleted file mode 100644
index 41983f21..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/encoders/ExecutePreparedStatementEncoder.scala
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.github.mauricio.postgresql.encoders
-
-import org.jboss.netty.buffer.{ChannelBuffers, ChannelBuffer}
-import com.github.mauricio.postgresql.{ChannelUtils, Message, CharsetHelper, FrontendMessage}
-import com.github.mauricio.postgresql.column.ColumnEncoderDecoder
-import com.github.mauricio.postgresql.messages.PreparedStatementExecuteMessage
-
-/**
- * User: Maurício Linhares
- * Date: 3/12/12
- * Time: 6:45 PM
- */
-
-object ExecutePreparedStatementEncoder extends Encoder {
- def encode(message: FrontendMessage): ChannelBuffer = {
-
- val m = message.asInstanceOf[PreparedStatementExecuteMessage]
-
- val queryBytes = CharsetHelper.toBytes(m.query)
-
- val bindBuffer = ChannelBuffers.dynamicBuffer(1024)
-
- bindBuffer.writeByte(Message.Bind)
- bindBuffer.writeInt(0)
-
- bindBuffer.writeBytes(queryBytes)
- bindBuffer.writeByte(0)
- bindBuffer.writeBytes(queryBytes)
- bindBuffer.writeByte(0)
-
- bindBuffer.writeShort(0)
-
- bindBuffer.writeShort(m.values.length)
-
- for ( value <- m.values ) {
- if ( value == null ) {
- bindBuffer.writeInt(-1)
- } else {
- val encoded = ColumnEncoderDecoder.encode(value).getBytes( CharsetHelper.Unicode )
- bindBuffer.writeInt(encoded.length)
- bindBuffer.writeBytes( encoded )
- }
- }
-
- bindBuffer.writeShort(0)
-
- ChannelUtils.writeLength(bindBuffer)
-
- val executeLength = 1 + 4 + queryBytes.length + 1 + 4
- val executeBuffer = ChannelBuffers.buffer( executeLength )
- executeBuffer.writeByte(Message.Execute)
- executeBuffer.writeInt(executeLength - 1)
-
- executeBuffer.writeBytes(queryBytes)
- executeBuffer.writeByte(0)
-
- executeBuffer.writeInt(0)
-
- val closeLength = 1 + 4 + 1 + queryBytes.length + 1
- val closeBuffer = ChannelBuffers.buffer(closeLength)
- closeBuffer.writeByte(Message.CloseStatementOrPortal)
- closeBuffer.writeInt( closeLength - 1 )
- closeBuffer.writeByte('P')
-
- closeBuffer.writeBytes(queryBytes)
- closeBuffer.writeByte(0)
-
- val syncBuffer = ChannelBuffers.buffer(5)
- syncBuffer.writeByte(Message.Sync)
- syncBuffer.writeInt(4)
-
- ChannelBuffers.wrappedBuffer(bindBuffer, executeBuffer, syncBuffer, closeBuffer)
-
- }
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/encoders/PreparedStatementOpeningEncoder.scala b/src/main/scala/com/github/mauricio/postgresql/encoders/PreparedStatementOpeningEncoder.scala
deleted file mode 100644
index 83030077..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/encoders/PreparedStatementOpeningEncoder.scala
+++ /dev/null
@@ -1,110 +0,0 @@
-package com.github.mauricio.postgresql.encoders
-
-import org.jboss.netty.buffer.{ChannelBuffers, ChannelBuffer}
-import com.github.mauricio.postgresql.util.Log
-import com.github.mauricio.postgresql.messages.{PreparedStatementOpeningMessage}
-import com.github.mauricio.postgresql.{Message, ChannelUtils, FrontendMessage, CharsetHelper}
-import com.github.mauricio.postgresql.column.ColumnEncoderDecoder
-
-
-/**
- * User: Maurício Linhares
- * Date: 3/7/12
- * Time: 9:20 AM
- */
-
-object PreparedStatementOpeningEncoder extends Encoder {
-
- private val log = Log.getByName("PreparedStatementOpeningEncoder")
-
- override def encode(message: FrontendMessage): ChannelBuffer = {
-
- val m = message.asInstanceOf[PreparedStatementOpeningMessage]
-
- val queryBytes = CharsetHelper.toBytes(m.query)
- val columnCount = m.valueTypes.size
-
- val parseBuffer = ChannelBuffers.dynamicBuffer( 1024 )
-
- parseBuffer.writeByte(Message.Parse)
- parseBuffer.writeInt(0)
-
- parseBuffer.writeBytes(queryBytes)
- parseBuffer.writeByte(0)
- parseBuffer.writeBytes(queryBytes)
- parseBuffer.writeByte(0)
-
- parseBuffer.writeShort(columnCount)
-
- for ( kind <- m.valueTypes ) {
- parseBuffer.writeInt(kind)
- }
-
- ChannelUtils.writeLength(parseBuffer)
-
- val bindBuffer = ChannelBuffers.dynamicBuffer(1024)
-
- bindBuffer.writeByte(Message.Bind)
- bindBuffer.writeInt(0)
-
- bindBuffer.writeBytes(queryBytes)
- bindBuffer.writeByte(0)
- bindBuffer.writeBytes(queryBytes)
- bindBuffer.writeByte(0)
-
- bindBuffer.writeShort(0)
-
- bindBuffer.writeShort(m.values.length)
-
- for ( value <- m.values ) {
- if ( value == null ) {
- bindBuffer.writeInt(-1)
- } else {
- val encoded = ColumnEncoderDecoder.encode(value).getBytes( CharsetHelper.Unicode )
- bindBuffer.writeInt(encoded.length)
- bindBuffer.writeBytes( encoded )
- }
- }
-
- bindBuffer.writeShort(0)
-
- ChannelUtils.writeLength(bindBuffer)
-
- val describeLength = 1 + 4 + 1 + queryBytes.length + 1
- val describeBuffer = ChannelBuffers.buffer( describeLength )
- describeBuffer.writeByte(Message.Describe)
- describeBuffer.writeInt(describeLength - 1)
-
- describeBuffer.writeByte('P')
-
- describeBuffer.writeBytes(queryBytes)
- describeBuffer.writeByte(0)
-
- val executeLength = 1 + 4 + queryBytes.length + 1 + 4
- val executeBuffer = ChannelBuffers.buffer( executeLength )
- executeBuffer.writeByte(Message.Execute)
- executeBuffer.writeInt(executeLength - 1)
-
- executeBuffer.writeBytes(queryBytes)
- executeBuffer.writeByte(0)
-
- executeBuffer.writeInt(0)
-
- val closeLength = 1 + 4 + 1 + queryBytes.length + 1
- val closeBuffer = ChannelBuffers.buffer(closeLength)
- closeBuffer.writeByte(Message.CloseStatementOrPortal)
- closeBuffer.writeInt( closeLength - 1 )
- closeBuffer.writeByte('P')
-
- closeBuffer.writeBytes(queryBytes)
- closeBuffer.writeByte(0)
-
- val syncBuffer = ChannelBuffers.buffer(5)
- syncBuffer.writeByte(Message.Sync)
- syncBuffer.writeInt(4)
-
- ChannelBuffers.wrappedBuffer(parseBuffer, bindBuffer, describeBuffer, executeBuffer, closeBuffer, syncBuffer)
-
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/encoders/QueryMessageEncoder.scala b/src/main/scala/com/github/mauricio/postgresql/encoders/QueryMessageEncoder.scala
deleted file mode 100644
index 13c5ba6b..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/encoders/QueryMessageEncoder.scala
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.github.mauricio.postgresql.encoders
-
-import com.github.mauricio.postgresql.messages.QueryMessage
-import org.jboss.netty.buffer.{ChannelBuffers, ChannelBuffer}
-import com.github.mauricio.postgresql.{FrontendMessage, ChannelUtils, CharsetHelper, Message}
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 8:32 PM
- */
-
-object QueryMessageEncoder extends Encoder {
-
- override def encode(message: FrontendMessage): ChannelBuffer = {
-
- val m = message.asInstanceOf[QueryMessage]
-
- val buffer = ChannelBuffers.dynamicBuffer()
- buffer.writeByte( Message.Query )
- buffer.writeInt(0)
- buffer.writeBytes( CharsetHelper.toBytes( m.query ) )
- buffer.writeByte(0)
-
- ChannelUtils.writeLength( buffer )
-
- buffer
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/encoders/StartupMessageEncoder.scala b/src/main/scala/com/github/mauricio/postgresql/encoders/StartupMessageEncoder.scala
deleted file mode 100644
index 22b9ae5b..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/encoders/StartupMessageEncoder.scala
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.github.mauricio.postgresql.encoders
-
-import org.jboss.netty.buffer.{ChannelBuffers, ChannelBuffer}
-import com.github.mauricio.postgresql.messages.StartupMessage
-import com.github.mauricio.postgresql.util.Log
-import com.github.mauricio.postgresql.{FrontendMessage, ChannelUtils}
-
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 7:35 PM
- */
-
-object StartupMessageEncoder extends Encoder {
-
- private val log = Log.getByName("StartupMessageEncoder")
-
- override def encode(message: FrontendMessage): ChannelBuffer = {
-
- val startup = message.asInstanceOf[StartupMessage]
-
- val buffer = ChannelBuffers.dynamicBuffer()
- buffer.writeInt(0)
- buffer.writeShort( 3 )
- buffer.writeShort( 0 )
-
- startup.parameters.foreach {
- pair =>
- ChannelUtils.writeCString( pair._1, buffer )
- ChannelUtils.writeCString( pair._2, buffer )
- }
-
- buffer.writeByte(0)
-
- val index = buffer.writerIndex()
-
- buffer.markWriterIndex()
- buffer.writerIndex(0)
- buffer.writeInt(index)
- buffer.resetWriterIndex()
-
- buffer
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/messages/CloseMessage.scala b/src/main/scala/com/github/mauricio/postgresql/messages/CloseMessage.scala
deleted file mode 100644
index 481cc64c..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/messages/CloseMessage.scala
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.github.mauricio.postgresql.messages
-
-import com.github.mauricio.postgresql.{FrontendMessage, Message}
-
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 8:39 PM
- */
-
-object CloseMessage {
-
- val Instance = new CloseMessage()
-
-}
-
-class CloseMessage extends FrontendMessage( Message.Close )
diff --git a/src/main/scala/com/github/mauricio/postgresql/messages/PreparedStatementExecuteMessage.scala b/src/main/scala/com/github/mauricio/postgresql/messages/PreparedStatementExecuteMessage.scala
deleted file mode 100644
index f380b4b2..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/messages/PreparedStatementExecuteMessage.scala
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.github.mauricio.postgresql.messages
-
-import com.github.mauricio.postgresql.Message
-
-/**
- * User: Maurício Linhares
- * Date: 3/12/12
- * Time: 6:50 PM
- */
-
-class PreparedStatementExecuteMessage( query : String, values : Seq[Any] )
- extends PreparedStatementMessage(Message.Execute, query, values)
diff --git a/src/main/scala/com/github/mauricio/postgresql/messages/PreparedStatementMessage.scala b/src/main/scala/com/github/mauricio/postgresql/messages/PreparedStatementMessage.scala
deleted file mode 100644
index 0df1a7c2..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/messages/PreparedStatementMessage.scala
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.mauricio.postgresql.messages
-
-import com.github.mauricio.postgresql.column.ColumnEncoderDecoder
-import com.github.mauricio.postgresql.FrontendMessage
-
-/**
- * User: Maurício Linhares
- * Date: 3/12/12
- * Time: 10:12 PM
- */
-
-class PreparedStatementMessage( kind : Char, val query : String, val values : Seq[Any] ) extends FrontendMessage(kind) {
-
- val valueTypes : Seq[Int] = values.map {
- value =>
- if ( value == null ) {
- 0
- } else {
- ColumnEncoderDecoder.kindFor(value.asInstanceOf[AnyRef].getClass)
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/messages/PreparedStatementOpeningMessage.scala b/src/main/scala/com/github/mauricio/postgresql/messages/PreparedStatementOpeningMessage.scala
deleted file mode 100644
index c7f9138a..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/messages/PreparedStatementOpeningMessage.scala
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.github.mauricio.postgresql.messages
-
-import com.github.mauricio.postgresql.Message
-
-/**
- * User: Maurício Linhares
- * Date: 3/12/12
- * Time: 1:00 AM
- */
-
-class PreparedStatementOpeningMessage( query : String, values : Seq[Any] )
- extends PreparedStatementMessage(Message.Parse, query, values)
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/messages/QueryMessage.scala b/src/main/scala/com/github/mauricio/postgresql/messages/QueryMessage.scala
deleted file mode 100644
index 50eb11bf..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/messages/QueryMessage.scala
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.github.mauricio.postgresql.messages
-
-import com.github.mauricio.postgresql.{FrontendMessage, Message}
-
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 8:31 PM
- */
-
-class QueryMessage( val query : String ) extends FrontendMessage( Message.Query )
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/messages/StartupMessage.scala b/src/main/scala/com/github/mauricio/postgresql/messages/StartupMessage.scala
deleted file mode 100644
index 542c009a..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/messages/StartupMessage.scala
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.github.mauricio.postgresql.messages
-
-import com.github.mauricio.postgresql.{Message, FrontendMessage}
-
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 7:34 PM
- */
-
-class StartupMessage ( val parameters : List[(String, String)] ) extends FrontendMessage(Message.Startup)
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/AuthenticationStartupParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/AuthenticationStartupParser.scala
deleted file mode 100644
index dc36c504..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/AuthenticationStartupParser.scala
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.jboss.netty.buffer.ChannelBuffer
-import com.github.mauricio.postgresql.Message
-
-object AuthenticationStartupParser extends MessageParser {
-
- private[parsers] val AUTH_REQ_OK = 0
- private[parsers] val AUTH_REQ_PASSWORD = 3
-
- override def parseMessage(b: ChannelBuffer): Message = {
-
- val authenticationType = b.readInt()
-
- authenticationType match {
- case AUTH_REQ_OK => {
- new Message( Message.AuthenticationOk, authenticationType)
- }
- case AUTH_REQ_PASSWORD => {
- new Message( Message.AuthenticationOk , authenticationType)
- }
- case _ => {
- throw new IllegalArgumentException("Unknown authentication method -> '%s'".format(authenticationType))
- }
-
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/BackendKeyDataParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/BackendKeyDataParser.scala
deleted file mode 100644
index 32a0642e..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/BackendKeyDataParser.scala
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.jboss.netty.buffer.ChannelBuffer
-import com.github.mauricio.postgresql.Message
-
-/**
- * User: Maurício Linhares
- * Date: 2/28/12
- * Time: 11:13 PM
- */
-
-object BackendKeyDataParser extends MessageParser {
-
- override def parseMessage(b: ChannelBuffer): Message = {
- new Message( Message.BackendKeyData, new ProcessData( b.readInt(), b.readInt() ) )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/BindComplete.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/BindComplete.scala
deleted file mode 100644
index ef286204..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/BindComplete.scala
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import com.github.mauricio.postgresql.Message
-
-/**
- * User: Maurício Linhares
- * Date: 3/12/12
- * Time: 2:32 AM
- */
-
-object BindComplete {
- val Instance = new BindComplete()
-}
-
-
-class BindComplete extends Message( Message.BindComplete, true )
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/CloseComplete.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/CloseComplete.scala
deleted file mode 100644
index 54d76be7..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/CloseComplete.scala
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import com.github.mauricio.postgresql.Message
-
-/**
- * User: Maurício Linhares
- * Date: 3/12/12
- * Time: 11:37 PM
- */
-
-object CloseComplete {
- val Instance = new CloseComplete()
-}
-
-class CloseComplete extends Message(Message.CloseComplete, true) {
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/ColumnData.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/ColumnData.scala
deleted file mode 100644
index f4706bf2..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/ColumnData.scala
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import com.github.mauricio.postgresql.column.ColumnEncoderDecoder
-
-/**
- * User: Maurício Linhares
- * Date: 3/1/12
- * Time: 10:33 PM
- */
-
-class ColumnData(
- val name: String,
- val tableObjectId: Int,
- val columnNumber: Int,
- val dataType: Int,
- val dataTypeSize: Int,
- val dataTypeModifier: Int,
- val fieldFormat: Int ) {
-
- val decoder = ColumnEncoderDecoder.decoderFor( this.dataType )
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/CommandCompleteParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/CommandCompleteParser.scala
deleted file mode 100644
index 48416442..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/CommandCompleteParser.scala
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.jboss.netty.buffer.ChannelBuffer
-import com.github.mauricio.postgresql.{QueryResult, ChannelUtils, Message}
-
-/**
- * User: Maurício Linhares
- * Date: 3/1/12
- * Time: 10:33 PM
- */
-
-object CommandCompleteParser extends MessageParser {
-
- override def parseMessage(b: ChannelBuffer): Message = {
-
- val result = ChannelUtils.readCString(b)
-
- val possibleRowCount = result.splitAt( result.lastIndexOf(" ") + 1 )
-
- val rowCount : Int = try {
- possibleRowCount._2.toInt
- } catch {
- case e : Exception => {
- 0
- }
- }
-
- new Message( Message.CommandComplete, ( rowCount, result ) )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/DataRowParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/DataRowParser.scala
deleted file mode 100644
index 68b1da20..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/DataRowParser.scala
+++ /dev/null
@@ -1,34 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import com.github.mauricio.postgresql.Message
-import org.jboss.netty.buffer.ChannelBuffer
-
-/**
- * User: Maurício Linhares
- * Date: 3/4/12
- * Time: 10:56 AM
- */
-
-object DataRowParser extends MessageParser {
-
- def parseMessage(buffer: ChannelBuffer): Message = {
-
- val row = new Array[ChannelBuffer](buffer.readShort())
-
- 0.until( row.length ).foreach {
- column =>
- val length = buffer.readInt()
-
- row(column) = if (length == -1) {
- null
- } else {
- val slice = buffer.slice( buffer.readerIndex(), length )
- buffer.readerIndex( buffer.readerIndex() + length )
- slice
- }
- }
-
- new Message(Message.DataRow, row)
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/ErrorParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/ErrorParser.scala
deleted file mode 100644
index e25c697d..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/ErrorParser.scala
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.jboss.netty.buffer.ChannelBuffer
-import com.github.mauricio.postgresql.Message
-import java.nio.charset.Charset
-
-object ErrorParser extends MessageParser {
-
- override def parseMessage(b: ChannelBuffer): Message = {
- new Message( Message.Error , b.toString( Charset.forName("UTF-8") ))
- }
-
-}
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/MessageParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/MessageParser.scala
deleted file mode 100644
index afdfbdf7..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/MessageParser.scala
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.jboss.netty.buffer.ChannelBuffer
-import com.github.mauricio.postgresql.Message
-
-object MessageParser {
-
- private val parsers = Map(
- Message.AuthenticationOk -> AuthenticationStartupParser,
- Message.BackendKeyData -> BackendKeyDataParser,
- Message.BindComplete -> new ReturningMessageParser(BindComplete.Instance),
- Message.CloseComplete -> new ReturningMessageParser(CloseComplete.Instance),
- Message.CommandComplete -> CommandCompleteParser,
- Message.DataRow -> DataRowParser,
- Message.Error -> ErrorParser,
- Message.Notice -> NoticeParser,
- Message.ParameterStatus -> ParameterStatusParser,
- Message.ParseComplete -> new ReturningMessageParser(ParseComplete.Instance),
- Message.RowDescription -> RowDescriptionParser,
- Message.ReadyForQuery -> ReadyForQueryParser
- )
-
- def parserFor(t: Char): MessageParser = {
- val option = this.parsers.get(t)
-
- if ( option.isDefined ) {
- option.get
- } else {
- throw new ParserNotAvailableException(t)
- }
-
- }
-
- def parse(t: Char, b: ChannelBuffer): Message = {
- this.parserFor(t).parseMessage(b)
- }
-
-}
-
-trait MessageParser {
-
- def parseMessage(buffer: ChannelBuffer): Message
-
-}
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/NoticeParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/NoticeParser.scala
deleted file mode 100644
index 0e032aec..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/NoticeParser.scala
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.jboss.netty.buffer.ChannelBuffer
-import collection.mutable.ListBuffer
-import com.github.mauricio.postgresql.{ChannelUtils, Message}
-
-/**
- * User: Maurício Linhares
- * Date: 3/1/12
- * Time: 10:06 PM
- */
-
-object NoticeParser extends MessageParser {
-
- override def parseMessage(b: ChannelBuffer): Message = {
-
- val listBuffer = ListBuffer[(Char, String)]()
-
- while ( b.readable() ) {
- val kind = b.readByte()
-
- if ( kind != 0 ) {
- listBuffer.append( ( kind.asInstanceOf[Char], ChannelUtils.readCString(b) ) )
- }
-
- }
-
- new Message( Message.Notice, listBuffer.toList )
-
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/ParameterStatusParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/ParameterStatusParser.scala
deleted file mode 100644
index 1d8df757..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/ParameterStatusParser.scala
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.jboss.netty.buffer.ChannelBuffer
-import com.github.mauricio.postgresql.{ChannelUtils, Message}
-
-/**
- * User: Maurício Linhares
- * Date: 2/25/12
- * Time: 7:06 PM
- */
-
-object ParameterStatusParser extends MessageParser {
-
- import ChannelUtils._
-
- override def parseMessage(b: ChannelBuffer): Message = {
- new Message( Message.ParameterStatus, ( readCString(b), readCString(b) ) )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/ParseComplete.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/ParseComplete.scala
deleted file mode 100644
index b5e3f4e6..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/ParseComplete.scala
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import com.github.mauricio.postgresql.Message
-
-/**
- * User: Maurício Linhares
- * Date: 3/12/12
- * Time: 2:29 AM
- */
-
-object ParseComplete {
- val Instance = new ParseComplete()
-}
-
-class ParseComplete extends Message(Message.ParseComplete, true)
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/ParserNotAvailableException.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/ParserNotAvailableException.scala
deleted file mode 100644
index 7a8dc6ba..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/ParserNotAvailableException.scala
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-/**
- * User: Maurício Linhares
- * Date: 3/4/12
- * Time: 12:16 AM
- */
-
-class ParserNotAvailableException(t: Char)
- extends RuntimeException("There is no parser available for message type '%s'".format(t))
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/ProcessData.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/ProcessData.scala
deleted file mode 100644
index 03715299..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/ProcessData.scala
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-/**
- * User: Maurício Linhares
- * Date: 2/28/12
- * Time: 11:13 PM
- */
-
-class ProcessData ( val processId : Int, val secretKey : Int ) {
-
- override def toString : String = {
- "ProcessData: %s - %s".format( this.processId, secretKey)
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/ReadyForQueryParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/ReadyForQueryParser.scala
deleted file mode 100644
index 345c5872..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/ReadyForQueryParser.scala
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import com.github.mauricio.postgresql.{Message}
-import org.jboss.netty.buffer.ChannelBuffer
-
-/**
- * User: Maurício Linhares
- * Date: 2/29/12
- * Time: 12:33 AM
- */
-
-object ReadyForQueryParser extends MessageParser {
-
- override def parseMessage(b: ChannelBuffer): Message = {
- new Message( Message.ReadyForQuery , b.readByte().asInstanceOf[Char])
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/ReturningMessageParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/ReturningMessageParser.scala
deleted file mode 100644
index 6c34fac9..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/ReturningMessageParser.scala
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.jboss.netty.buffer.ChannelBuffer
-import com.github.mauricio.postgresql.Message
-
-/**
- * User: Maurício Linhares
- * Date: 3/12/12
- * Time: 11:36 PM
- */
-
-class ReturningMessageParser( val message : Message ) extends MessageParser {
- def parseMessage(buffer: ChannelBuffer): Message = {
- this.message
- }
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/parsers/RowDescriptionParser.scala b/src/main/scala/com/github/mauricio/postgresql/parsers/RowDescriptionParser.scala
deleted file mode 100644
index 5b131854..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/parsers/RowDescriptionParser.scala
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.jboss.netty.buffer.ChannelBuffer
-import com.github.mauricio.postgresql.{ChannelUtils, Message}
-
-/**
- * User: Maurício Linhares
- * Date: 3/1/12
- * Time: 1:56 AM
- *
- * RowDescription (B)
-Byte1('T')
-Identifies the message as a row description.
-
-Int32
-Length of message contents in bytes, including self.
-
-Int16
-Specifies the number of fields in a row (can be zero).
-
-Then, for each field, there is the following:
-
-String
-The field name.
-
-Int32
-If the field can be identified as a column of a specific table, the object ID of the table; otherwise zero.
-
-Int16
-If the field can be identified as a column of a specific table, the attribute number of the column; otherwise zero.
-
-Int32
-The object ID of the field's data type.
-
-Int16
-The data type size (see pg_type.typlen). Note that negative values denote variable-width types.
-
-Int32
-The type modifier (see pg_attribute.atttypmod). The meaning of the modifier is type-specific.
-
-Int16
-The format code being used for the field. Currently will be zero (text) or one (binary). In a RowDescription returned from the statement variant of Describe, the format code is not yet known and will always be zero.
- *
- */
-
-
-object RowDescriptionParser extends MessageParser {
-
- import ChannelUtils._
-
- override def parseMessage(b: ChannelBuffer): Message = {
-
- val columnsCount = b.readShort()
- val columns = new Array[ColumnData](columnsCount)
-
- 0.until( columnsCount ).foreach {
- index =>
- columns(index) = new ColumnData(
- name = readCString( b ),
- tableObjectId = b.readInt(),
- columnNumber = b.readShort(),
- dataType = b.readInt(),
- dataTypeSize = b.readShort(),
- dataTypeModifier = b.readInt(),
- fieldFormat = b.readShort()
- )
- }
-
- new Message( Message.RowDescription, columns )
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/util/DaemonThreadsFactory.scala b/src/main/scala/com/github/mauricio/postgresql/util/DaemonThreadsFactory.scala
deleted file mode 100644
index a139bd90..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/util/DaemonThreadsFactory.scala
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.github.mauricio.postgresql.util
-
-import java.util.concurrent.{Executors, ThreadFactory}
-
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 2:22 PM
- */
-
-object DaemonThreadsFactory extends ThreadFactory {
- def newThread(r: Runnable): Thread = {
-
- val thread = Executors.defaultThreadFactory().newThread(r)
- thread.setDaemon(true)
-
- return thread
- }
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/util/Future.scala b/src/main/scala/com/github/mauricio/postgresql/util/Future.scala
deleted file mode 100644
index f902e8df..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/util/Future.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.github.mauricio.postgresql.util
-
-import java.util.concurrent.TimeUnit
-
-/**
- * User: Maurício Linhares
- * Date: 3/10/12
- * Time: 6:44 PM
- */
-
-trait Future[L,R] {
-
- def get : Either[L,R]
- def get( time : Long, unit : TimeUnit ) : Either[L,R]
- def isDone : Boolean
- def isError : Boolean
- def onComplete( fn : Either[L,R] => Unit )
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/util/FutureResult.scala b/src/main/scala/com/github/mauricio/postgresql/util/FutureResult.scala
deleted file mode 100644
index 1d3b32f0..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/util/FutureResult.scala
+++ /dev/null
@@ -1,40 +0,0 @@
-package com.github.mauricio.postgresql.util
-
-/**
- * User: Maurício Linhares
- * Date: 3/10/12
- * Time: 10:41 AM
- */
-
-sealed abstract class FutureResult[+S] {
-
- def apply() : S = this.get
- def isSuccess : Boolean
- def isFailure : Boolean
- def get : S
- def getFailure : Throwable
-
- def fold[R]( failure : Throwable => R, success : S => R ) = this match {
- case FutureSuccess( a ) => success(a)
- case FutureFailure( e ) => failure(e)
- }
-
-}
-
-final case class FutureSuccess[+S]( value : S ) extends FutureResult[S] {
-
- override def isSuccess : Boolean = true
- override def isFailure : Boolean = false
- override def get = value
- override def getFailure = throw new IllegalStateException("This option is a success, there is no failure here")
-
-}
-
-final case class FutureFailure[+S]( value : Throwable ) extends FutureResult[S] {
-
- override def isSuccess : Boolean = true
- override def isFailure : Boolean = false
- override def get = throw new IllegalStateException("This option is an error, there is no success here", value)
- override def getFailure = value
-
-}
\ No newline at end of file
diff --git a/src/main/scala/com/github/mauricio/postgresql/util/Log.scala b/src/main/scala/com/github/mauricio/postgresql/util/Log.scala
deleted file mode 100644
index 3bff5e58..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/util/Log.scala
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.github.mauricio.postgresql.util
-
-import org.slf4j.{Logger, LoggerFactory}
-
-
-/**
- * User: Maurício Linhares
- * Date: 2/25/12
- * Time: 8:01 PM
- */
-
-object Log {
-
- def get[T](implicit manifest : Manifest[T] ) = {
- LoggerFactory.getLogger( manifest.erasure.getName )
- }
-
- def getByName( name : String ) = {
- LoggerFactory.getLogger(name)
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/util/SimpleFuture.scala b/src/main/scala/com/github/mauricio/postgresql/util/SimpleFuture.scala
deleted file mode 100644
index a5f2c127..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/util/SimpleFuture.scala
+++ /dev/null
@@ -1,93 +0,0 @@
-package com.github.mauricio.postgresql.util
-
-import java.util.concurrent.{ConcurrentLinkedQueue, TimeoutException, TimeUnit}
-import scala.collection.JavaConversions._
-
-/**
- * User: Maurício Linhares
- * Date: 3/10/12
- * Time: 12:29 AM
- */
-
-object SimpleFuture {
- val log = Log.get[SimpleFuture[Throwable,Nothing]]
-}
-
-class SimpleFuture[L >: Throwable,R] extends Future[L,R] {
-
- import SimpleFuture._
-
- @volatile private var done = false
- @volatile private var result : R = _
- @volatile private var error : L = _
- @volatile private var callbacks = new ConcurrentLinkedQueue[Either[L,R] => Unit]()
-
- override def onComplete( fn : Either[L,R] => Unit ) {
- this.callbacks.add( fn )
- if (this.isDone) {
- fn(this.getResult)
- }
- }
-
- override def isDone = this.done
- override def isError = this.error != null
-
- def set( value : R ) {
- this.result = value
- this.done = true
- this.fireCallbacks
- }
-
- def setError( error : L ) {
- this.error = error
- this.done = true
- this.fireCallbacks
-
- log.error("Received error", error)
- }
-
- override def get : Either[L,R] = {
- while ( !this.done ) {
- ThreadHelpers.safeSleep(500)
- }
- this.getResult
- }
-
- override def get( time : Long, unit : TimeUnit ) : Either[L,R] = {
- val totalTime = unit.toMillis(time)
- val increment = 500
- var sum = 0
-
- while (!this.done && sum <= totalTime) {
- ThreadHelpers.safeSleep(increment)
- sum += increment
- }
-
- log.debug("Done is {}", this.done)
-
- if (this.done) {
- return this.getResult
- } else {
- throw new TimeoutException("Lock reached the timeout limit")
- }
-
- }
-
- private def fireCallbacks {
-
- val either = this.getResult
-
- for ( callback <- this.callbacks ) {
- callback(either)
- }
- }
-
- private def getResult : Either[L,R] = {
- if ( this.error != null ) {
- Left(this.error)
- } else {
- Right(this.result)
- }
- }
-
-}
diff --git a/src/main/scala/com/github/mauricio/postgresql/util/ThreadHelpers.scala b/src/main/scala/com/github/mauricio/postgresql/util/ThreadHelpers.scala
deleted file mode 100644
index 864bf564..00000000
--- a/src/main/scala/com/github/mauricio/postgresql/util/ThreadHelpers.scala
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.github.mauricio.postgresql.util
-
-/**
- * User: Maurício Linhares
- * Date: 3/10/12
- * Time: 10:06 AM
- */
-
-object ThreadHelpers {
-
- def safeSleep( milis : Long ) {
-
- val current = System.currentTimeMillis()
- var slept = false
- while ( !slept ) {
- Thread.sleep(milis)
- if ( (current + milis) <= System.currentTimeMillis() ) {
- slept = true
- }
- }
-
- }
-
-}
diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml
deleted file mode 100644
index 060e49d8..00000000
--- a/src/test/resources/logback.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- [%level][%thread][%file:%line] %msg%n
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/test/scala/com/github/mauricio/postgresql/DatabaseConnectionHandlerSpec.scala b/src/test/scala/com/github/mauricio/postgresql/DatabaseConnectionHandlerSpec.scala
deleted file mode 100644
index 2a9ce164..00000000
--- a/src/test/scala/com/github/mauricio/postgresql/DatabaseConnectionHandlerSpec.scala
+++ /dev/null
@@ -1,222 +0,0 @@
-package com.github.mauricio.postgresql
-
-import column.{TimeEncoderDecoder, DateEncoderDecoder, TimestampEncoderDecoder}
-import org.specs2.mutable.Specification
-import java.util.concurrent.TimeUnit
-
-/**
- * User: Maurício Linhares
- * Date: 3/1/12
- * Time: 12:38 AM
- */
-
-class DatabaseConnectionHandlerSpec extends Specification {
-
- val create = """create temp table type_test_table (
- bigserial_column bigserial not null,
- smallint_column smallint not null,
- integer_column integer not null,
- decimal_column decimal(10,4),
- real_column real,
- double_column double precision,
- serial_column serial not null,
- varchar_column varchar(255),
- text_column text,
- timestamp_column timestamp,
- date_column date,
- time_column time,
- boolean_column boolean,
- constraint bigserial_column_pkey primary key (bigserial_column)
- )"""
-
- val insert = """insert into type_test_table (
- smallint_column,
- integer_column,
- decimal_column,
- real_column,
- double_column,
- varchar_column,
- text_column,
- timestamp_column,
- date_column,
- time_column,
- boolean_column
- )
- VALUES (
- 10,
- 11,
- 14.999,
- 78.34,
- 15.68,
- 'this is a varchar field',
- 'this is a long text field',
- '1984-08-06 22:13:45.888888',
- '1984-08-06',
- '22:13:45.888888',
- TRUE
- )
- """
-
- val select = "select * from type_test_table"
-
- val preparedStatementCreate = """create temp table prepared_statement_test (
- id bigserial not null,
- name varchar(255) not null,
- constraint bigserial_column_pkey primary key (id)
- )"""
-
- val preparedStatementInsert = " insert into prepared_statement_test (name) values ('John Doe')"
- val preparedStatementInsert2 = " insert into prepared_statement_test (name) values ('Mary Jane')"
- val preparedStatementInsert3 = " insert into prepared_statement_test (name) values ('Peter Parker')"
- val preparedStatementSelect = "select * from prepared_statement_test"
-
- def withHandler[T]( fn : (DatabaseConnectionHandler) => T ) : T = {
-
- val handler = new DatabaseConnectionHandler( "localhost", 5433, "postgres", "netty_driver_test" )
-
- try {
- handler.connect.get
- fn(handler)
- } finally {
- handler.disconnect
- }
-
- }
-
-
- "handler" should {
-
- "connect to the database" in {
-
- withHandler {
- handler =>
- handler.isReadyForQuery must beTrue
- }
-
- }
-
- "create a table in the database" in {
-
- withHandler {
- handler =>
- val result = handler.sendQuery( this.create ).get( 5, TimeUnit.SECONDS )
-
- result match {
- case Right( result ) => result.rowsAffected === 0
- }
-
- }
-
- }
-
- "insert a row in the database" in {
-
- withHandler {
- handler =>
- handler.sendQuery( this.create ).get( 5, TimeUnit.SECONDS )
- val result = handler.sendQuery( this.insert ).get( 5, TimeUnit.SECONDS )
-
- result match {
- case Right( result ) => result.rowsAffected === 1
- }
- }
-
- }
-
- "select rows in the database" in {
-
- withHandler {
- handler =>
- handler.sendQuery( this.create ).get( 5, TimeUnit.SECONDS)
- handler.sendQuery( this.insert ).get( 5, TimeUnit.SECONDS )
- val result = handler.sendQuery( this.select ).get( 5, TimeUnit.SECONDS )
-
- result match {
- case Right( queryResult ) => {
-
- val rows = queryResult.rows.get
-
- List(
- rows(0,0) === 1,
- rows(1,0) === 10,
- rows(2,0) === 11,
- rows(3,0) === 14.999,
- rows(4,0).toString === 78.34.toString,
- rows(5,0) === 15.68,
- rows(6,0) === 1,
- rows(7,0) === "this is a varchar field",
- rows(8,0) === "this is a long text field",
- rows(9,0) === TimestampEncoderDecoder.decode("1984-08-06 22:13:45.888888"),
- rows(10,0) === DateEncoderDecoder.decode("1984-08-06"),
- rows(11,0) === TimeEncoderDecoder.decode("22:13:45.888888"),
- rows(12,0) === true
- )
-
- }
-
-
- }
-
- }
-
- }
-
- "execute a prepared statement" in {
-
- withHandler {
- handler =>
- handler.sendQuery(this.preparedStatementCreate).get(5, TimeUnit.SECONDS)
- handler.sendQuery(this.preparedStatementInsert).get(5, TimeUnit.SECONDS)
- val result = handler.sendPreparedStatement( this.preparedStatementSelect ).get(5, TimeUnit.SECONDS)
-
- result match {
- case Right( queryResult ) => {
- val rows = queryResult.rows.get
-
- List(
- rows(0,0) === 1,
- rows(1,0) === "John Doe"
- )
- }
- }
-
- }
-
- }
-
- "execute a prepared statement with parameters" in {
-
- withHandler {
- handler =>
- handler.sendQuery(this.preparedStatementCreate).get(5, TimeUnit.SECONDS)
- handler.sendQuery(this.preparedStatementInsert).get(5, TimeUnit.SECONDS)
- handler.sendQuery(this.preparedStatementInsert2).get(5, TimeUnit.SECONDS)
- handler.sendQuery(this.preparedStatementInsert3).get(5, TimeUnit.SECONDS)
-
- val select = "select * from prepared_statement_test where name like ?"
-
- val queryResult = handler.sendPreparedStatement( select, Array("Peter Parker") ).get(5, TimeUnit.SECONDS)
- val rows = queryResult match {
- case Right( result ) => { result.rows.get }
- }
-
- val queryResult2 = handler.sendPreparedStatement( select, Array("Mary Jane") ).get(5, TimeUnit.SECONDS)
- val rows2 = queryResult2 match {
- case Right( result ) => { result.rows.get }
- }
-
- List(
- rows(0,0) === 3,
- rows(1,0) === "Peter Parker",
- rows.count === 1,
- rows2.count === 1,
- rows2(0,0) === 2,
- rows2(1,0) === "Mary Jane"
- )
- }
-
- }
-
- }
-
-}
diff --git a/src/test/scala/com/github/mauricio/postgresql/MessageDecoderSpec.scala b/src/test/scala/com/github/mauricio/postgresql/MessageDecoderSpec.scala
deleted file mode 100644
index ad092765..00000000
--- a/src/test/scala/com/github/mauricio/postgresql/MessageDecoderSpec.scala
+++ /dev/null
@@ -1,64 +0,0 @@
-package com.github.mauricio.postgresql
-
-import org.specs2.mutable.Specification
-import org.jboss.netty.buffer.ChannelBuffers
-
-/**
- * User: Maurício Linhares
- * Date: 2/28/12
- * Time: 10:33 PM
- */
-
-class MessageDecoderSpec extends Specification {
-
- val decoder = MessageDecoder
-
- "message decoder" should {
-
- "not try to decode if there is not enought data available" in {
-
- val buffer = ChannelBuffers.dynamicBuffer()
-
- buffer.writeByte('R')
- buffer.writeByte(1)
- buffer.writeByte(2)
-
- this.decoder.decode( null, null, buffer ) must beNull
- }
-
- "should not try to decode if there is a type and lenght but it's not long enough" in {
-
- val buffer = ChannelBuffers.dynamicBuffer()
-
- buffer.writeByte('R')
- buffer.writeInt( 30 )
- buffer.writeBytes( "my-name".getBytes(CharsetHelper.Unicode) )
-
- List(
- this.decoder.decode( null, null, buffer ) must beNull,
- buffer.readerIndex() === 0
- )
- }
-
- "should correctly decode a message" in {
-
- val buffer = ChannelBuffers.dynamicBuffer()
- val text = "This is an error message"
- val textBytes = text.getBytes( CharsetHelper.Unicode )
-
- buffer.writeByte('E')
- buffer.writeInt( textBytes.length + 4 )
- buffer.writeBytes( textBytes )
-
- val result = this.decoder.decode( null, null, buffer ).asInstanceOf[Message]
-
- List(
- result.content === text,
- buffer.readerIndex() === (textBytes.length + 5 )
- )
- }
-
- }
-
-
-}
diff --git a/src/test/scala/com/github/mauricio/postgresql/TestUtils.scala b/src/test/scala/com/github/mauricio/postgresql/TestUtils.scala
deleted file mode 100644
index cbfe4313..00000000
--- a/src/test/scala/com/github/mauricio/postgresql/TestUtils.scala
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.github.mauricio.postgresql
-
-import java.util.concurrent.atomic.AtomicInteger
-
-/**
- * User: Maurício Linhares
- * Date: 3/3/12
- * Time: 9:49 PM
- */
-
-object TestUtils {
-
- private val count = new AtomicInteger()
-
- def nextInt : Int = {
- count.incrementAndGet()
- }
-
-}
diff --git a/src/test/scala/com/github/mauricio/postgresql/parsers/ParserESpec.scala b/src/test/scala/com/github/mauricio/postgresql/parsers/ParserESpec.scala
deleted file mode 100644
index 383cb9ba..00000000
--- a/src/test/scala/com/github/mauricio/postgresql/parsers/ParserESpec.scala
+++ /dev/null
@@ -1,30 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.specs2.mutable.Specification
-import org.jboss.netty.buffer.ChannelBuffers
-import com.github.mauricio.postgresql.Message
-
-/**
- * User: Maurício Linhares
- * Date: 2/28/12
- * Time: 9:54 PM
- */
-
-class ParserESpec extends Specification {
-
- "ErrorParser" should {
-
- "correctly parse an error message" in {
-
- val error = "this is my error message"
- val buffer = ChannelBuffers.dynamicBuffer()
- buffer.writeBytes( error.getBytes )
-
- val message = ErrorParser.parseMessage( buffer )
-
- List(message.content === error, message.name === Message.Error)
- }
-
- }
-
-}
diff --git a/src/test/scala/com/github/mauricio/postgresql/parsers/ParserKSpec.scala b/src/test/scala/com/github/mauricio/postgresql/parsers/ParserKSpec.scala
deleted file mode 100644
index db64770d..00000000
--- a/src/test/scala/com/github/mauricio/postgresql/parsers/ParserKSpec.scala
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.specs2.mutable.Specification
-import org.jboss.netty.buffer.ChannelBuffers
-import com.github.mauricio.postgresql.Message
-
-/**
- * User: Maurício Linhares
- * Date: 2/28/12
- * Time: 11:33 PM
- */
-
-class ParserKSpec extends Specification {
-
- val parser = BackendKeyDataParser
-
- "parserk" should {
-
- "correctly parse the message" in {
-
- val buffer = ChannelBuffers.dynamicBuffer()
- buffer.writeInt(10)
- buffer.writeInt(20)
-
- val message = parser.parseMessage( buffer )
- val data = message.content.asInstanceOf[ProcessData]
-
- List(
- message.name === Message.BackendKeyData,
- data.processId === 10,
- data.secretKey === 20
- )
- }
-
- }
-
-}
diff --git a/src/test/scala/com/github/mauricio/postgresql/parsers/ParserSSpec.scala b/src/test/scala/com/github/mauricio/postgresql/parsers/ParserSSpec.scala
deleted file mode 100644
index 32dfde2a..00000000
--- a/src/test/scala/com/github/mauricio/postgresql/parsers/ParserSSpec.scala
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.github.mauricio.postgresql.parsers
-
-import org.specs2.mutable.Specification
-import org.jboss.netty.buffer.ChannelBuffers
-import java.nio.charset.Charset
-import com.github.mauricio.postgresql.Message
-
-/**
- * User: Maurício Linhares
- * Date: 2/28/12
- * Time: 9:59 PM
- */
-
-class ParserSSpec extends Specification {
-
- val parser = ParameterStatusParser
-
- "ParameterStatusParser" should {
-
- "correctly parse a config pair" in {
-
- val key = "application-name"
- val value = "my-cool-application"
-
- val buffer = ChannelBuffers.dynamicBuffer()
-
- buffer.writeBytes( key.getBytes( Charset.forName("UTF-8") ) )
- buffer.writeByte(0)
- buffer.writeBytes( value.getBytes( Charset.forName("UTF-8") ) )
- buffer.writeByte(0)
-
- val message = this.parser.parseMessage( buffer )
- val content = message.content.asInstanceOf[(String,String)]
-
- List(
- content._1 === key,
- content._2 === value,
- message.name === Message.ParameterStatus,
- buffer.readerIndex() === buffer.writerIndex() )
- }
-
- }
-
-}
diff --git a/src/test/scala/com/github/mauricio/postgresql/util/FutureResultSpec.scala b/src/test/scala/com/github/mauricio/postgresql/util/FutureResultSpec.scala
deleted file mode 100644
index f05d0c3a..00000000
--- a/src/test/scala/com/github/mauricio/postgresql/util/FutureResultSpec.scala
+++ /dev/null
@@ -1,50 +0,0 @@
-package com.github.mauricio.postgresql.util
-
-import org.specs2.mutable.Specification
-
-/**
- * User: Maurício Linhares
- * Date: 3/10/12
- * Time: 1:32 PM
- */
-
-class FutureResultSpec extends Specification {
-
- "future" should {
-
- "return the value when it is a success" in {
-
- val success = FutureSuccess("ME!")
- success() === "ME!"
- }
-
- "raise an exception when trying to get an error a success" in {
-
- try {
- val result = FutureSuccess("ME!")
- result.getFailure
- } catch {
- case e : IllegalStateException => success
- }
- }
-
- "grab an exception from the failure" in {
- val exception = new RuntimeException()
- val failure = FutureFailure(exception)
- failure.getFailure === exception
- }
-
- "raise an exception when trying to grab a value from a failure" in {
- val exception = new RuntimeException()
- val failure = FutureFailure(exception)
-
- try {
- failure()
- } catch {
- case e : IllegalStateException => success
- }
- }
-
- }
-
-}