├── .gitignore ├── spec ├── fake_ngx.lua ├── type_fixtures.lua └── functional_spec.lua ├── Makefile ├── .travis ├── platform.sh └── setup_lua.sh ├── bump.sh ├── .travis.yml ├── cassandra-0.5-7.rockspec ├── LICENSE ├── CHANGELOG.md ├── src ├── cassandra │ ├── constants.lua │ ├── decoding.lua │ ├── protocol.lua │ └── encoding.lua └── cassandra.lua └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # luacov 4 | luacov.* 5 | -------------------------------------------------------------------------------- /spec/fake_ngx.lua: -------------------------------------------------------------------------------- 1 | socket = require("socket") 2 | 3 | return { 4 | get_phase = function() 5 | return "" 6 | end, 7 | socket=socket, 8 | time=os.time 9 | } 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test coverage lint clean 2 | 3 | clean: 4 | @rm -f luacov.* 5 | 6 | test: 7 | @busted spec/ 8 | 9 | coverage: clean 10 | @busted --coverage spec/ 11 | @luacov cassandra 12 | 13 | lint: 14 | @luacheck cassandra*.rockspec --globals ngx 15 | -------------------------------------------------------------------------------- /.travis/platform.sh: -------------------------------------------------------------------------------- 1 | if [ -z "$PLATFORM" ]; then 2 | PLATFORM=$TRAVIS_OS_NAME; 3 | fi 4 | 5 | if [ "$PLATFORM" == "osx" ]; then 6 | PLATFORM="macosx"; 7 | fi 8 | 9 | if [ -z "$PLATFORM" ]; then 10 | if [ "$(uname)" == "Linux" ]; then 11 | PLATFORM="linux"; 12 | else 13 | PLATFORM="macosx"; 14 | fi; 15 | fi 16 | -------------------------------------------------------------------------------- /bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$#" -ne 1 ]; then 4 | echo "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | pat="[0-9]*\.[0-9]*-[0-9]*" 9 | if [[ $1 =~ $pat ]]; then 10 | echo Bumping to: $1 11 | sed -i '' -e s/$pat/$1/g src/cassandra.lua 12 | sed -i '' -e s/$pat/$1/g cassandra*.rockspec 13 | mv cassandra*.rockspec cassandra-$1.rockspec 14 | 15 | badge_version="${1/-/--}" 16 | sed -i '' -e s/version-[0-9]*\.[0-9]*--[0-9]*/version-$badge_version/g README.md 17 | else 18 | echo Invalid version: $1 19 | exit 1 20 | fi 21 | 22 | echo "Don't forget to review the diff and update the CHANGELOG" 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | 3 | env: 4 | global: 5 | - LUAROCKS=2.2.0 6 | matrix: 7 | - LUA=lua5.1 8 | - LUA=lua5.2 9 | - LUA=lua5.3 10 | - LUA=luajit 11 | 12 | branches: 13 | only: 14 | - master 15 | 16 | before_install: 17 | - bash .travis/setup_lua.sh 18 | - sudo luarocks install luafilesystem --from=https://rocks.moonscript.org/dev 19 | - sudo luarocks install busted 20 | - sudo luarocks install luasocket 21 | - sudo luarocks install luacov-coveralls 22 | 23 | services: 24 | - cassandra 25 | 26 | script: "busted --coverage" 27 | 28 | after_success: 29 | - luacov-coveralls -i cassandra 30 | 31 | notifications: 32 | recipients: 33 | - jbochi@gmail.com 34 | email: 35 | on_success: change 36 | on_failure: always 37 | -------------------------------------------------------------------------------- /cassandra-0.5-7.rockspec: -------------------------------------------------------------------------------- 1 | package = "cassandra" 2 | version = "0.5-7" 3 | source = { 4 | url = "git://github.com/jbochi/lua-resty-cassandra", 5 | tag = "v0.5-7" 6 | } 7 | description = { 8 | summary = "Pure Lua Cassandra - CQL client", 9 | detailed = [[ 10 | Pure Cassandra driver for Lua supporting CQL 3, 11 | using binary protocol v2. 12 | ]], 13 | homepage = "https://github.com/jbochi/lua-resty-cassandra", 14 | license = "MIT/X11" 15 | } 16 | dependencies = { 17 | "lua >= 5.1" 18 | } 19 | build = { 20 | type = "builtin", 21 | modules = { 22 | cassandra = "src/cassandra.lua", 23 | ["cassandra.constants"] = "src/cassandra/constants.lua", 24 | ["cassandra.protocol"] = "src/cassandra/protocol.lua", 25 | ["cassandra.decoding"] = "src/cassandra/decoding.lua", 26 | ["cassandra.encoding"] = "src/cassandra/encoding.lua" 27 | }, 28 | copy_directories = { "spec" } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Juarez Bochi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased][unreleased] 2 | #### Added 3 | 4 | #### Fixed 5 | 6 | ## [0.5-7] - 2015/05/05 7 | #### Added 8 | - Accepting multiple hosts with different ports when connecting (#55) 9 | 10 | ## [0.5-6] - 2015/04/19 11 | #### Fixed 12 | - Accessing nil rows in auto_paging (#50) 13 | 14 | ## [0.5-5] - 2015/03/28 15 | #### Added 16 | - Expose error code (#46) 17 | 18 | #### Fixed 19 | - Fix documentation for set_keepalive() 20 | 21 | ## [0.5-4] - 2015/03/04 22 | #### Added 23 | - Better travis configuration (coverage, lua 5.1, 5.2 and luajit) 24 | 25 | #### Fixed 26 | - `auto_pagination` option not returning the latest page in most cases 27 | 28 | ## [0.5-3] - 2015/03/01 29 | #### Fixed 30 | - Tests (file structure) 31 | 32 | ## [0.5-2] - 2015/02/24 33 | #### Fixed 34 | - Installation (missing rockspec files) 35 | 36 | ## [0.5] - 2015/02/24 37 | #### Added 38 | - Support for pagination. 39 | - Support for batch types. 40 | - Better, complete documentation. 41 | - The `version` property is now effective. 42 | 43 | #### Fixed 44 | - Batch statement queries without parameters 45 | - Require contact_points to not be nil #39 46 | - Seed random number generator only once, on module import 47 | 48 | ## [0.4] - 2015/02/06 49 | #### Added 50 | - Batch support (#7). 51 | - Smarter session creation: choose between cosocket and luasocket on each new session (#29). 52 | 53 | ## [0.3] - 2015/01/22 54 | #### Added 55 | - Allow result rows access by name or position index (#27). 56 | - More explicit error messages. 57 | 58 | #### Fixed 59 | - Query tracing. 60 | 61 | ## [0.2] - 2014/07/28 62 | #### Added 63 | - Choose a random contact point from nodes list (#18). 64 | - Add support for tracing on write (#2). 65 | 66 | #### Fixed 67 | - Calls to `setkeepalive` and `getreusedtimes` while using luasocket now return an error. 68 | 69 | ## 0.1 - 2014-07-26 70 | - First release. 71 | 72 | [unreleased]: https://github.com/jbochi/lua-resty-cassandra/compare/v0.5-3...HEAD 73 | [0.5-3]: https://github.com/jbochi/lua-resty-cassandra/compare/v0.5-3...v0.5-2 74 | [0.5-2]: https://github.com/jbochi/lua-resty-cassandra/compare/v0.5...v0.5-2 75 | [0.5]: https://github.com/jbochi/lua-resty-cassandra/compare/v0.4...v0.5 76 | [0.4]: https://github.com/jbochi/lua-resty-cassandra/compare/v0.3...v0.4 77 | [0.3]: https://github.com/jbochi/lua-resty-cassandra/compare/v0.2...v0.3 78 | [0.2]: https://github.com/jbochi/lua-resty-cassandra/compare/v0.1...v0.2 79 | -------------------------------------------------------------------------------- /.travis/setup_lua.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # A script for setting up environment for travis-ci testing. 4 | # Sets up Lua and Luarocks. 5 | # LUA must be "lua5.1", "lua5.2" or "luajit". 6 | # luajit2.0 - master v2.0 7 | # luajit2.1 - master v2.1 8 | 9 | LUAJIT_BASE="LuaJIT-2.0.3" 10 | 11 | source .travis/platform.sh 12 | 13 | LUAJIT="no" 14 | 15 | if [ "$PLATFORM" == "macosx" ]; then 16 | if [ "$LUA" == "luajit" ]; then 17 | LUAJIT="yes"; 18 | fi 19 | if [ "$LUA" == "luajit2.0" ]; then 20 | LUAJIT="yes"; 21 | fi 22 | if [ "$LUA" == "luajit2.1" ]; then 23 | LUAJIT="yes"; 24 | fi; 25 | elif [ "$(expr substr $LUA 1 6)" == "luajit" ]; then 26 | LUAJIT="yes"; 27 | fi 28 | 29 | if [ "$LUAJIT" == "yes" ]; then 30 | 31 | if [ "$LUA" == "luajit" ]; then 32 | curl http://luajit.org/download/$LUAJIT_BASE.tar.gz | tar xz; 33 | else 34 | git clone http://luajit.org/git/luajit-2.0.git $LUAJIT_BASE; 35 | fi 36 | 37 | cd $LUAJIT_BASE 38 | 39 | if [ "$LUA" == "luajit2.1" ]; then 40 | git checkout v2.1; 41 | fi 42 | 43 | make && sudo make install 44 | 45 | if [ "$LUA" == "luajit2.1" ]; then 46 | sudo ln -s /usr/local/bin/luajit-2.1.0-alpha /usr/local/bin/luajit 47 | sudo ln -s /usr/local/bin/luajit /usr/local/bin/lua; 48 | else 49 | sudo ln -s /usr/local/bin/luajit /usr/local/bin/lua; 50 | fi; 51 | 52 | else 53 | if [ "$LUA" == "lua5.1" ]; then 54 | curl http://www.lua.org/ftp/lua-5.1.5.tar.gz | tar xz 55 | cd lua-5.1.5; 56 | elif [ "$LUA" == "lua5.2" ]; then 57 | curl http://www.lua.org/ftp/lua-5.2.3.tar.gz | tar xz 58 | cd lua-5.2.3; 59 | elif [ "$LUA" == "lua5.3" ]; then 60 | curl http://www.lua.org/ftp/lua-5.3.0.tar.gz | tar xz 61 | cd lua-5.3.0; 62 | fi 63 | sudo make $PLATFORM install; 64 | fi 65 | 66 | cd $TRAVIS_BUILD_DIR; 67 | 68 | LUAROCKS_BASE=luarocks-$LUAROCKS 69 | 70 | # curl http://luarocks.org/releases/$LUAROCKS_BASE.tar.gz | tar xz 71 | 72 | git clone https://github.com/keplerproject/luarocks.git $LUAROCKS_BASE 73 | cd $LUAROCKS_BASE 74 | 75 | git checkout v$LUAROCKS 76 | 77 | if [ "$LUA" == "luajit" ]; then 78 | ./configure --lua-suffix=jit --with-lua-include=/usr/local/include/luajit-2.0; 79 | elif [ "$LUA" == "luajit2.0" ]; then 80 | ./configure --lua-suffix=jit --with-lua-include=/usr/local/include/luajit-2.0; 81 | elif [ "$LUA" == "luajit2.1" ]; then 82 | ./configure --lua-suffix=jit --with-lua-include=/usr/local/include/luajit-2.1; 83 | else 84 | ./configure; 85 | fi 86 | 87 | make build && sudo make install 88 | 89 | cd $TRAVIS_BUILD_DIR 90 | 91 | rm -rf $LUAROCKS_BASE 92 | 93 | if [ "$LUAJIT" == "yes" ]; then 94 | rm -rf $LUAJIT_BASE; 95 | elif [ "$LUA" == "lua5.1" ]; then 96 | rm -rf lua-5.1.5; 97 | elif [ "$LUA" == "lua5.2" ]; then 98 | rm -rf lua-5.2.3; 99 | elif [ "$LUA" == "lua5.3" ]; then 100 | rm -rf lua-5.3.0; 101 | fi 102 | -------------------------------------------------------------------------------- /src/cassandra/constants.lua: -------------------------------------------------------------------------------- 1 | local error_codes = { 2 | SERVER = 0x0000, 3 | PROTOCOL = 0x000A, 4 | BAD_CREDENTIALS = 0x0100, 5 | UNAVAILABLE_EXCEPTION = 0x1000, 6 | OVERLOADED = 0x1001, 7 | IS_BOOTSTRAPPING = 0x1002, 8 | TRUNCATE_ERROR = 0x1003, 9 | WRITE_TIMEOUT = 0x1100, 10 | READ_TIMEOUT = 0x1200, 11 | SYNTAX_ERROR = 0x2000, 12 | UNAUTHORIZED = 0x2100, 13 | INVALID = 0x2200, 14 | CONFIG_ERROR = 0x2300, 15 | ALREADY_EXISTS = 0x2400, 16 | UNPREPARED = 0x2500 17 | } 18 | 19 | return { 20 | version_codes = { 21 | REQUEST=0x02, 22 | RESPONSE=0x82 23 | }, 24 | op_codes = { 25 | ERROR=0x00, 26 | STARTUP=0x01, 27 | READY=0x02, 28 | AUTHENTICATE=0x03, 29 | -- 0x04 30 | OPTIONS=0x05, 31 | SUPPORTED=0x06, 32 | QUERY=0x07, 33 | RESULT=0x08, 34 | PREPARE=0x09, 35 | EXECUTE=0x0A, 36 | REGISTER=0x0B, 37 | EVENT=0x0C, 38 | BATCH=0x0D, 39 | AUTH_CHALLENGE=0x0E, 40 | AUTH_RESPONSE=0x0F, 41 | AUTH_SUCCESS=0x10, 42 | }, 43 | consistency = { 44 | ANY=0x0000, 45 | ONE=0x0001, 46 | TWO=0x0002, 47 | THREE=0x0003, 48 | QUORUM=0x0004, 49 | ALL=0x0005, 50 | LOCAL_QUORUM=0x0006, 51 | EACH_QUORUM=0x0007, 52 | SERIAL=0x0008, 53 | LOCAL_SERIAL=0x0009, 54 | LOCAL_ONE=0x000A 55 | }, 56 | batch_types = { 57 | LOGGED=0, 58 | UNLOGGED=1, 59 | COUNTER=2 60 | }, 61 | query_flags = { 62 | VALUES=0x01, 63 | PAGE_SIZE=0x04, 64 | PAGING_STATE=0x08 65 | }, 66 | rows_flags = { 67 | GLOBAL_TABLES_SPEC=0x01, 68 | HAS_MORE_PAGES=0x02, 69 | -- 0x03 70 | NO_METADATA=0x04 71 | }, 72 | result_kinds = { 73 | VOID=0x01, 74 | ROWS=0x02, 75 | SET_KEYSPACE=0x03, 76 | PREPARED=0x04, 77 | SCHEMA_CHANGE=0x05 78 | }, 79 | error_codes = error_codes, 80 | error_codes_translation = { 81 | [error_codes.SERVER]="Server error", 82 | [error_codes.PROTOCOL]="Protocol error", 83 | [error_codes.BAD_CREDENTIALS]="Bad credentials", 84 | [error_codes.UNAVAILABLE_EXCEPTION]="Unavailable exception", 85 | [error_codes.OVERLOADED]="Overloaded", 86 | [error_codes.IS_BOOTSTRAPPING]="Is_bootstrapping", 87 | [error_codes.TRUNCATE_ERROR]="Truncate_error", 88 | [error_codes.WRITE_TIMEOUT]="Write_timeout", 89 | [error_codes.READ_TIMEOUT]="Read_timeout", 90 | [error_codes.SYNTAX_ERROR]="Syntax_error", 91 | [error_codes.UNAUTHORIZED]="Unauthorized", 92 | [error_codes.INVALID]="Invalid", 93 | [error_codes.CONFIG_ERROR]="Config_error", 94 | [error_codes.ALREADY_EXISTS]="Already_exists", 95 | [error_codes.UNPREPARED]="Unprepared" 96 | }, 97 | types = { 98 | custom=0x00, 99 | ascii=0x01, 100 | bigint=0x02, 101 | blob=0x03, 102 | boolean=0x04, 103 | counter=0x05, 104 | decimal=0x06, 105 | double=0x07, 106 | float=0x08, 107 | int=0x09, 108 | text=0x0A, 109 | timestamp=0x0B, 110 | uuid=0x0C, 111 | varchar=0x0D, 112 | varint=0x0E, 113 | timeuuid=0x0F, 114 | inet=0x10, 115 | list=0x20, 116 | map=0x21, 117 | set=0x22 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /spec/type_fixtures.lua: -------------------------------------------------------------------------------- 1 | local cassandra = require "cassandra" 2 | 3 | return { 4 | {name='ascii', value='string'}, 5 | {name='ascii', insert_value=cassandra.null, read_value=nil}, 6 | {name='bigint', insert_value=cassandra.bigint(42000000000), read_value=42000000000}, 7 | {name='bigint', insert_value=cassandra.bigint(-42000000000), read_value=-42000000000}, 8 | {name='bigint', insert_value=cassandra.bigint(-42), read_value=-42}, 9 | {name='blob', value="\005\042"}, 10 | {name='blob', value=string.rep("blob", 10000)}, 11 | {name='boolean', value=true}, 12 | {name='boolean', value=false}, 13 | -- counters are not here because they are used with UPDATE instead of INSERT 14 | -- todo: decimal, 15 | {name='double', insert_value=cassandra.double(1.0000000000000004), read_test=function(value) return math.abs(value - 1.0000000000000004) < 0.000000000000001 end}, 16 | {name='double', insert_value=cassandra.double(-1.0000000000000004), read_value=-1.0000000000000004}, 17 | {name='double', insert_value=cassandra.double(0), read_test=function(value) return math.abs(value - 0) < 0.000000000000001 end}, 18 | {name='double', insert_value=cassandra.double(314151), read_test=function(value) return math.abs(value - 314151) < 0.000000000000001 end}, 19 | {name='float', insert_value=3.14151, read_test=function(value) return math.abs(value - 3.14151) < 0.0000001 end}, 20 | {name='float', insert_value=cassandra.float(3.14151), read_test=function(value) return math.abs(value - 3.14151) < 0.0000001 end}, 21 | {name='float', insert_value=cassandra.float(0), read_test=function(value) return math.abs(value - 0) < 0.0000001 end}, 22 | {name='float', insert_value=-3.14151, read_test=function(value) return math.abs(value + 3.14151) < 0.0000001 end}, 23 | {name='float', insert_value=cassandra.float(314151), read_test=function(value) return math.abs(value - 314151) < 0.0000001 end}, 24 | {name='int', value=4200}, 25 | {name='int', value=-42}, 26 | {name='text', value='string'}, 27 | {name='timestamp', insert_value=cassandra.timestamp(1405356926), read_value=1405356926}, 28 | {name='uuid', insert_value=cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11"), read_value="1144bada-852c-11e3-89fb-e0b9a54a6d11"}, 29 | {name='varchar', value='string'}, 30 | {name='blob', value=string.rep("string", 10000)}, 31 | {name='varint', value=4200}, 32 | {name='varint', value=-42}, 33 | {name='timeuuid', insert_value=cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11"), read_value="1144bada-852c-11e3-89fb-e0b9a54a6d11"}, 34 | {name='inet', insert_value=cassandra.inet("127.0.0.1"), read_value="127.0.0.1"}, 35 | {name='inet', insert_value=cassandra.inet("2001:0db8:85a3:0042:1000:8a2e:0370:7334"), read_value="2001:0db8:85a3:0042:1000:8a2e:0370:7334"}, 36 | {name='list', insert_value=cassandra.list({'abc', 'def'}), read_value={'abc', 'def'}}, 37 | {name='list', insert_value=cassandra.list({4, 2, 7}), read_value={4, 2, 7}}, 38 | {name='map', insert_value=cassandra.map({k1='v1', k2='v2'}), read_value={k1='v1', k2='v2'}}, 39 | {name='map', insert_value=cassandra.map({k1=3, k2=4}), read_value={k1=3, k2=4}}, 40 | {name='map', insert_value=cassandra.map({}), read_value=nil}, 41 | {name='set', insert_value=cassandra.set({'abc', 'def'}), read_value={'abc', 'def'}} 42 | } 43 | -------------------------------------------------------------------------------- /src/cassandra/decoding.lua: -------------------------------------------------------------------------------- 1 | local constants = require("cassandra.constants") 2 | 3 | local _M = {} 4 | 5 | local function read_boolean(bytes) 6 | return string.byte(bytes) == 1 7 | end 8 | 9 | local function read_raw(value) 10 | return value 11 | end 12 | 13 | local function string_to_number(str, signed) 14 | local number = 0 15 | local exponent = 1 16 | for i = #str, 1, -1 do 17 | number = number + string.byte(str, i) * exponent 18 | exponent = exponent * 256 19 | end 20 | if signed and number > exponent / 2 then 21 | -- 2's complement 22 | number = number - exponent 23 | end 24 | return number 25 | end 26 | 27 | local function read_signed_number(bytes) 28 | return string_to_number(bytes, true) 29 | end 30 | 31 | local function read_raw_bytes(buffer, n_bytes) 32 | local bytes = string.sub(buffer.str, buffer.pos, buffer.pos + n_bytes - 1) 33 | buffer.pos = buffer.pos + n_bytes 34 | return bytes 35 | end 36 | 37 | local function read_short(buffer) 38 | return string_to_number(read_raw_bytes(buffer, 2), false) 39 | end 40 | 41 | local function read_bigint(bytes) 42 | local b1, b2, b3, b4, b5, b6, b7, b8 = string.byte(bytes, 1, 8) 43 | if b1 < 0x80 then 44 | return ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 45 | else 46 | return ((((((((b1 - 0xFF) * 0x100 + (b2 - 0xFF)) * 0x100 + (b3 - 0xFF)) * 0x100 + (b4 - 0xFF)) * 0x100 + (b5 - 0xFF)) * 0x100 + (b6 - 0xFF)) * 0x100 + (b7 - 0xFF)) * 0x100 + (b8 - 0xFF)) - 1 47 | end 48 | end 49 | 50 | local function read_double(bytes) 51 | local b1, b2, b3, b4, b5, b6, b7, b8 = string.byte(bytes, 1, 8) 52 | local sign = b1 > 0x7F 53 | local exponent = (b1 % 0x80) * 0x10 + math.floor(b2 / 0x10) 54 | local mantissa = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8 55 | if sign then 56 | sign = -1 57 | else 58 | sign = 1 59 | end 60 | local number 61 | if mantissa == 0 and exponent == 0 then 62 | number = sign * 0.0 63 | elseif exponent == 0x7FF then 64 | if mantissa == 0 then 65 | number = sign * math.huge 66 | else 67 | number = 0.0/0.0 68 | end 69 | else 70 | number = sign * math.ldexp(1.0 + mantissa / 0x10000000000000, exponent - 0x3FF) 71 | end 72 | return number 73 | end 74 | 75 | local function read_float(bytes) 76 | local b1, b2, b3, b4 = string.byte(bytes, 1, 4) 77 | local exponent = (b1 % 0x80) * 0x02 + math.floor(b2 / 0x80) 78 | local mantissa = math.ldexp(((b2 % 0x80) * 0x100 + b3) * 0x100 + b4, -23) 79 | if exponent == 0xFF then 80 | if mantissa > 0 then 81 | return 0 / 0 82 | else 83 | mantissa = math.huge 84 | exponent = 0x7F 85 | end 86 | elseif exponent > 0 then 87 | mantissa = mantissa + 1 88 | else 89 | exponent = exponent + 1 90 | end 91 | if b1 >= 0x80 then 92 | mantissa = -mantissa 93 | end 94 | return math.ldexp(mantissa, exponent - 0x7F) 95 | end 96 | 97 | local function read_inet(bytes) 98 | local buffer = {} 99 | if #bytes == 16 then 100 | -- ipv6 101 | for i = 1, #bytes, 2 do 102 | buffer[#buffer + 1] = string.format("%02x", string.byte(bytes, i)) .. 103 | string.format("%02x", string.byte(bytes, i + 1)) 104 | end 105 | return table.concat(buffer, ":") 106 | end 107 | for i = 1, #bytes do 108 | buffer[#buffer + 1] = string.format("%d", string.byte(bytes, i)) 109 | end 110 | return table.concat(buffer, ".") 111 | end 112 | 113 | local function read_list(bytes, type) 114 | local element_type = type.value 115 | local buffer = _M.create_buffer(bytes) 116 | local n = read_short(buffer) 117 | local elements = {} 118 | for _ = 1, n do 119 | elements[#elements + 1] = _M.read_value(buffer, element_type, true) 120 | end 121 | return elements 122 | end 123 | 124 | local function read_map(bytes, type) 125 | local key_type = type.value[1] 126 | local value_type = type.value[2] 127 | local buffer = _M.create_buffer(bytes) 128 | local n = read_short(buffer) 129 | local map = {} 130 | for _ = 1, n do 131 | local key = _M.read_value(buffer, key_type, true) 132 | local value = _M.read_value(buffer, value_type, true) 133 | map[key] = value 134 | end 135 | return map 136 | end 137 | 138 | -- 139 | -- Public interface 140 | -- 141 | 142 | function _M.create_buffer(str) 143 | return {str=str, pos=1} 144 | end 145 | 146 | function _M.read_raw_byte(buffer) 147 | return string.byte(read_raw_bytes(buffer, 1)) 148 | end 149 | 150 | function _M.read_int(buffer) 151 | return string_to_number(read_raw_bytes(buffer, 4), true) 152 | end 153 | 154 | function _M.read_string(buffer) 155 | local str_size = read_short(buffer) 156 | return read_raw_bytes(buffer, str_size) 157 | end 158 | 159 | function _M.read_bytes(buffer) 160 | local size = _M.read_int(buffer, true) 161 | if size < 0 then 162 | return nil 163 | end 164 | return read_raw_bytes(buffer, size) 165 | end 166 | 167 | function _M.read_short_bytes(buffer) 168 | local size = read_short(buffer) 169 | return read_raw_bytes(buffer, size) 170 | end 171 | 172 | function _M.read_option(buffer) 173 | local type_id = read_short(buffer) 174 | local type_value = nil 175 | if type_id == constants.types.custom then 176 | type_value = _M.read_string(buffer) 177 | elseif type_id == constants.types.list then 178 | type_value = _M.read_option(buffer) 179 | elseif type_id == constants.types.map then 180 | type_value = {_M.read_option(buffer), _M.read_option(buffer)} 181 | elseif type_id == constants.types.set then 182 | type_value = _M.read_option(buffer) 183 | end 184 | return {id=type_id, value=type_value} 185 | end 186 | 187 | function _M.read_uuid(bytes) 188 | local buffer = {} 189 | for i = 1, #bytes do 190 | buffer[i] = string.format("%02x", string.byte(bytes, i)) 191 | end 192 | table.insert(buffer, 5, "-") 193 | table.insert(buffer, 8, "-") 194 | table.insert(buffer, 11, "-") 195 | table.insert(buffer, 14, "-") 196 | return table.concat(buffer) 197 | end 198 | 199 | local decoders = { 200 | -- custom=0x00, 201 | [constants.types.ascii]=read_raw, 202 | [constants.types.bigint]=read_bigint, 203 | [constants.types.blob]=read_raw, 204 | [constants.types.boolean]=read_boolean, 205 | [constants.types.counter]=read_bigint, 206 | -- decimal=0x06, 207 | [constants.types.double]=read_double, 208 | [constants.types.float]=read_float, 209 | [constants.types.int]=read_signed_number, 210 | [constants.types.text]=read_raw, 211 | [constants.types.timestamp]=read_bigint, 212 | [constants.types.uuid]=_M.read_uuid, 213 | [constants.types.varchar]=read_raw, 214 | [constants.types.varint]=read_signed_number, 215 | [constants.types.timeuuid]=_M.read_uuid, 216 | [constants.types.inet]=read_inet, 217 | [constants.types.list]=read_list, 218 | [constants.types.map]=read_map, 219 | [constants.types.set]=read_list 220 | } 221 | 222 | function _M.read_value(buffer, type, short) 223 | local bytes 224 | if short then 225 | bytes = _M.read_short_bytes(buffer) 226 | else 227 | bytes = _M.read_bytes(buffer) 228 | end 229 | if bytes == nil then 230 | return nil 231 | end 232 | return decoders[type.id](bytes, type) 233 | end 234 | 235 | return _M 236 | -------------------------------------------------------------------------------- /src/cassandra.lua: -------------------------------------------------------------------------------- 1 | -- Implementation of CQL Binary protocol V2 available at: 2 | -- https://git-wip-us.apache.org/repos/asf?p=cassandra.git;a=blob_plain;f=doc/native_protocol_v2.spec;hb=HEAD 3 | 4 | local protocol = require("cassandra.protocol") 5 | local encoding = require("cassandra.encoding") 6 | local constants = require("cassandra.constants") 7 | 8 | local CQL_VERSION = "3.0.0" 9 | 10 | math.randomseed(ngx and ngx.time() or os.time()) 11 | 12 | local _M = { 13 | version="0.5-7", 14 | consistency=constants.consistency, 15 | batch_types=constants.batch_types 16 | } 17 | 18 | -- create functions for type annotations 19 | for key, _ in pairs(constants.types) do 20 | _M[key] = function(value) 21 | return {type=key, value=value} 22 | end 23 | end 24 | 25 | _M.null = {type="null", value=nil} 26 | 27 | --- 28 | --- SOCKET METHODS 29 | --- 30 | 31 | local mt = {__index=_M} 32 | 33 | function _M.new() 34 | local tcp 35 | if ngx and ngx.get_phase ~= nil and ngx.get_phase() ~= "init" then 36 | -- openresty 37 | tcp = ngx.socket.tcp 38 | else 39 | -- fallback to luasocket 40 | -- It's also a fallback for openresty in the 41 | -- "init" phase that doesn't support Cosockets 42 | tcp = require("socket").tcp 43 | end 44 | 45 | local sock, err = tcp() 46 | if not sock then 47 | return nil, err 48 | end 49 | 50 | return setmetatable({sock=sock}, mt) 51 | end 52 | 53 | local function shuffle(t) 54 | -- see: http://en.wikipedia.org/wiki/Fisher-Yates_shuffle 55 | local n = #t 56 | while n >= 2 do 57 | local k = math.random(n) 58 | t[n], t[k] = t[k], t[n] 59 | n = n - 1 60 | end 61 | return t 62 | end 63 | 64 | local function startup(self) 65 | local body = encoding.string_map_representation({["CQL_VERSION"]=CQL_VERSION}) 66 | local response, err = protocol.send_frame_and_get_response(self, 67 | constants.op_codes.STARTUP, body) 68 | if not response then 69 | return nil, err 70 | end 71 | if response.op_code ~= constants.op_codes.READY then 72 | error("Server is not ready") 73 | end 74 | return true 75 | end 76 | 77 | local function split_by_port(str) 78 | local fields = {} 79 | str:gsub("([^:]+)", function(c) fields[#fields+1] = c end) 80 | return fields[1], fields[2] 81 | end 82 | 83 | function _M:connect(contact_points, port) 84 | if port == nil then port = 9042 end 85 | if contact_points == nil then 86 | return nil, "no contact points provided" 87 | elseif type(contact_points) == 'table' then 88 | -- shuffle the contact points so we don't try 89 | -- to connect always on the same order, avoiding 90 | -- pressure on the same node cordinator 91 | shuffle(contact_points) 92 | else 93 | contact_points = {contact_points} 94 | end 95 | local sock = self.sock 96 | if not sock then 97 | return nil, "session does not have a socket, create a new session first." 98 | end 99 | local ok, err 100 | for _, contact_point in ipairs(contact_points) do 101 | -- Extract port if string is of the form "host:port" 102 | local host, host_port = split_by_port(contact_point) 103 | -- Default port is the one given as parameter 104 | if not host_port then 105 | host_port = port 106 | end 107 | ok, err = sock:connect(host, host_port) 108 | if ok then 109 | self.host = host 110 | break 111 | end 112 | end 113 | if not ok then 114 | return false, err 115 | end 116 | if not self.initialized then 117 | --todo: not tested 118 | startup(self) 119 | self.initialized = true 120 | end 121 | return true 122 | end 123 | 124 | function _M:set_timeout(timeout) 125 | local sock = self.sock 126 | if not sock then 127 | return nil, "not initialized" 128 | end 129 | 130 | return sock:settimeout(timeout) 131 | end 132 | 133 | function _M:set_keepalive(...) 134 | local sock = self.sock 135 | if not sock then 136 | return nil, "not initialized" 137 | elseif sock.setkeepalive then 138 | return sock:setkeepalive(...) 139 | end 140 | return nil, "luasocket does not support reusable sockets" 141 | end 142 | 143 | function _M:get_reused_times() 144 | local sock = self.sock 145 | if not sock then 146 | return nil, "not initialized" 147 | elseif sock.getreusedtimes then 148 | return sock:getreusedtimes() 149 | end 150 | return nil, "luasocket does not support reusable sockets" 151 | end 152 | 153 | function _M:close() 154 | local sock = self.sock 155 | if not sock then 156 | return nil, "not initialized" 157 | end 158 | 159 | return sock:close() 160 | end 161 | 162 | --- 163 | --- CLIENT METHODS 164 | --- 165 | 166 | local batch_statement_mt = { 167 | __index={ 168 | add=function(self, query, args) 169 | table.insert(self.queries, {query=query, args=args}) 170 | end, 171 | representation=function(self) 172 | return encoding.batch_representation(self.queries, self.type) 173 | end, 174 | is_batch_statement = true 175 | } 176 | } 177 | 178 | function _M.BatchStatement(batch_type) 179 | if not batch_type then 180 | batch_type = constants.batch_types.LOGGED 181 | end 182 | 183 | return setmetatable({type=batch_type, queries={}}, batch_statement_mt) 184 | end 185 | 186 | function _M:prepare(query, options) 187 | if not options then options = {} end 188 | local body = encoding.long_string_representation(query) 189 | local response, err = protocol.send_frame_and_get_response(self, 190 | constants.op_codes.PREPARE, body, options.tracing) 191 | if not response then 192 | return nil, err 193 | end 194 | if response.op_code ~= constants.op_codes.RESULT then 195 | error("Result expected") 196 | end 197 | return protocol.parse_prepared_response(response) 198 | end 199 | 200 | -- Default query options 201 | local default_options = { 202 | consistency_level=constants.consistency.ONE, 203 | page_size=5000, 204 | auto_paging=false, 205 | tracing=false 206 | } 207 | 208 | function _M:execute(query, args, options) 209 | local op_code = protocol.query_op_code(query) 210 | if not options then options = {} end 211 | 212 | -- Default options 213 | for k, v in pairs(default_options) do 214 | if options[k] == nil then 215 | options[k] = v 216 | end 217 | end 218 | 219 | if options.auto_paging then 220 | local page = 0 221 | return function(query, paging_state) 222 | -- Latest fetched rows have been returned for sure, end the iteration 223 | if not paging_state and page > 0 then return nil end 224 | 225 | local rows, err = self:execute(query, args, { 226 | page_size=options.page_size, 227 | paging_state=paging_state 228 | }) 229 | page = page + 1 230 | 231 | -- If we have some results, retrieve the paging_state 232 | local paging_state 233 | if rows ~= nil then 234 | paging_state = rows.meta.paging_state 235 | end 236 | 237 | -- Allow the iterator to return the latest page of rows or an error 238 | if err or (paging_state == nil and rows and #rows > 0) then 239 | paging_state = false 240 | end 241 | 242 | return paging_state, rows, page, err 243 | end, query, nil 244 | end 245 | 246 | local frame_body = protocol.frame_body(query, args, options) 247 | 248 | -- Send frame 249 | local response, err = protocol.send_frame_and_get_response(self, op_code, frame_body, options.tracing) 250 | 251 | -- Check response errors 252 | if not response then 253 | return nil, err 254 | elseif response.op_code ~= constants.op_codes.RESULT then 255 | error("Result expected") 256 | end 257 | 258 | return protocol.parse_response(response) 259 | end 260 | 261 | function _M:set_keyspace(keyspace_name) 262 | return self:execute("USE " .. keyspace_name) 263 | end 264 | 265 | function _M:get_trace(result) 266 | if not result.tracing_id then 267 | return nil, "No tracing available" 268 | end 269 | local rows, err = self:execute([[ 270 | SELECT coordinator, duration, parameters, request, started_at 271 | FROM system_traces.sessions WHERE session_id = ?]], 272 | {_M.uuid(result.tracing_id)}) 273 | if not rows then 274 | return nil, "Unable to get trace: " .. err 275 | end 276 | if #rows == 0 then 277 | return nil, "Trace not found" 278 | end 279 | local trace = rows[1] 280 | trace.events, err = self:execute([[ 281 | SELECT event_id, activity, source, source_elapsed, thread 282 | FROM system_traces.events WHERE session_id = ?]], 283 | {_M.uuid(result.tracing_id)}) 284 | if not trace.events then 285 | return nil, "Unable to get trace events: " .. err 286 | end 287 | return trace 288 | end 289 | 290 | return _M 291 | -------------------------------------------------------------------------------- /src/cassandra/protocol.lua: -------------------------------------------------------------------------------- 1 | local constants = require("cassandra.constants") 2 | local encoding = require("cassandra.encoding") 3 | local decoding = require("cassandra.decoding") 4 | 5 | local error_mt = {} 6 | error_mt = { 7 | __tostring = function(self) 8 | return self.message 9 | end, 10 | __concat = function (a, b) 11 | if getmetatable(a) == error_mt then 12 | return a.message .. b 13 | else 14 | return a .. b.message 15 | end 16 | end 17 | } 18 | 19 | local function cassandra_error(message, code, raw_message) 20 | local err = {message=message, code=code, raw_message=raw_message} 21 | setmetatable(err, error_mt) 22 | return err 23 | end 24 | 25 | local function read_error(buffer) 26 | local error_code = decoding.read_int(buffer) 27 | local error_code_translation = constants.error_codes_translation[error_code] 28 | local error_message = decoding.read_string(buffer) 29 | return cassandra_error( 30 | 'Cassandra returned error (' .. error_code_translation .. '): "' .. error_message .. '"', 31 | error_code, 32 | error_message 33 | ) 34 | end 35 | 36 | local function read_frame(self) 37 | local header, err = self.sock:receive(8) 38 | if not header then 39 | return nil, string.format("Failed to read frame header from %s: %s", self.host, err) 40 | end 41 | local header_buffer = decoding.create_buffer(header) 42 | local version = decoding.read_raw_byte(header_buffer) 43 | local flags = decoding.read_raw_byte(header_buffer) 44 | local stream = decoding.read_raw_byte(header_buffer) 45 | local op_code = decoding.read_raw_byte(header_buffer) 46 | local length = decoding.read_int(header_buffer) 47 | local body, tracing_id 48 | if length > 0 then 49 | body, err = self.sock:receive(length) 50 | if not body then 51 | return nil, string.format("Failed to read frame body from %s: %s", self.host, err) 52 | end 53 | else 54 | body = "" 55 | end 56 | if version ~= constants.version_codes.RESPONSE then 57 | error("Invalid response version") 58 | end 59 | local body_buffer = decoding.create_buffer(body) 60 | if flags == 0x02 then -- tracing 61 | tracing_id = decoding.read_uuid(string.sub(body, 1, 16)) 62 | body_buffer.pos = 17 63 | end 64 | if op_code == constants.op_codes.ERROR then 65 | return nil, read_error(body_buffer) 66 | end 67 | return { 68 | flags=flags, 69 | stream=stream, 70 | op_code=op_code, 71 | buffer=body_buffer, 72 | tracing_id=tracing_id 73 | } 74 | end 75 | 76 | local function hasbit(x, p) 77 | return x % (p + p) >= p 78 | end 79 | 80 | local function setbit(x, p) 81 | return hasbit(x, p) and x or x + p 82 | end 83 | 84 | local function parse_metadata(buffer) 85 | -- Flags parsing 86 | local flags = decoding.read_int(buffer) 87 | local global_tables_spec = hasbit(flags, constants.rows_flags.GLOBAL_TABLES_SPEC) 88 | local has_more_pages = hasbit(flags, constants.rows_flags.HAS_MORE_PAGES) 89 | local columns_count = decoding.read_int(buffer) 90 | 91 | -- Paging metadata 92 | local paging_state 93 | if has_more_pages then 94 | paging_state = decoding.read_bytes(buffer) 95 | end 96 | 97 | -- global_tables_spec metadata 98 | local global_keyspace_name, global_table_name 99 | if global_tables_spec then 100 | global_keyspace_name = decoding.read_string(buffer) 101 | global_table_name = decoding.read_string(buffer) 102 | end 103 | 104 | -- Columns metadata 105 | local columns = {} 106 | for _ = 1, columns_count do 107 | local ksname = global_keyspace_name 108 | local tablename = global_table_name 109 | if not global_tables_spec then 110 | ksname = decoding.read_string(buffer) 111 | tablename = decoding.read_string(buffer) 112 | end 113 | local column_name = decoding.read_string(buffer) 114 | columns[#columns + 1] = { 115 | keyspace=ksname, 116 | table=tablename, 117 | name=column_name, 118 | type=decoding.read_option(buffer) 119 | } 120 | end 121 | 122 | return { 123 | columns_count=columns_count, 124 | columns=columns, 125 | has_more_pages=has_more_pages, 126 | paging_state=paging_state 127 | } 128 | end 129 | 130 | local function parse_rows(buffer, metadata) 131 | local columns = metadata.columns 132 | local columns_count = metadata.columns_count 133 | local rows_count = decoding.read_int(buffer) 134 | local values = {} 135 | local row_mt = { 136 | __index = function(t, i) 137 | -- allows field access by position/index, not column name only 138 | local column = columns[i] 139 | if column then 140 | return t[column.name] 141 | end 142 | return nil 143 | end, 144 | __len = function() return columns_count end 145 | } 146 | for _ = 1, rows_count do 147 | local row = {} 148 | setmetatable(row, row_mt) 149 | for j = 1, columns_count do 150 | local value = decoding.read_value(buffer, columns[j].type) 151 | row[columns[j].name] = value 152 | end 153 | values[#values + 1] = row 154 | end 155 | assert(buffer.pos == #(buffer.str) + 1) 156 | return values 157 | end 158 | 159 | local function query_representation(query) 160 | if type(query) == "string" then 161 | return encoding.long_string_representation(query) 162 | elseif query.is_batch_statement then 163 | return query:representation() 164 | else 165 | return encoding.short_bytes_representation(query.id) 166 | end 167 | end 168 | 169 | -- 170 | -- Protocol exposed methods 171 | -- 172 | 173 | local _M = {} 174 | 175 | function _M.parse_prepared_response(response) 176 | local buffer = response.buffer 177 | local kind = decoding.read_int(buffer) 178 | local result = {} 179 | if kind == constants.result_kinds.PREPARED then 180 | local id = decoding.read_short_bytes(buffer) 181 | local metadata = parse_metadata(buffer) 182 | local result_metadata = parse_metadata(buffer) 183 | assert(buffer.pos == #(buffer.str) + 1) 184 | result = { 185 | type="PREPARED", 186 | id=id, 187 | metadata=metadata, 188 | result_metadata=result_metadata 189 | } 190 | else 191 | error("Invalid result kind") 192 | end 193 | if response.tracing_id then result.tracing_id = response.tracing_id end 194 | return result 195 | end 196 | 197 | function _M.parse_response(response) 198 | local result 199 | local buffer = response.buffer 200 | local kind = decoding.read_int(buffer) 201 | if kind == constants.result_kinds.VOID then 202 | result = { 203 | type="VOID" 204 | } 205 | elseif kind == constants.result_kinds.ROWS then 206 | local metadata = parse_metadata(buffer) 207 | result = parse_rows(buffer, metadata) 208 | result.type = "ROWS" 209 | result.meta = { 210 | has_more_pages=metadata.has_more_pages, 211 | paging_state=metadata.paging_state 212 | } 213 | elseif kind == constants.result_kinds.SET_KEYSPACE then 214 | result = { 215 | type="SET_KEYSPACE", 216 | keyspace=decoding.read_string(buffer) 217 | } 218 | elseif kind == constants.result_kinds.SCHEMA_CHANGE then 219 | result = { 220 | type="SCHEMA_CHANGE", 221 | change=decoding.read_string(buffer), 222 | keyspace=decoding.read_string(buffer), 223 | table=decoding.read_string(buffer) 224 | } 225 | else 226 | error(string.format("Invalid result kind: %x", kind)) 227 | end 228 | 229 | if response.tracing_id then 230 | result.tracing_id = response.tracing_id 231 | end 232 | return result 233 | end 234 | 235 | function _M.query_op_code(query) 236 | if type(query) == "string" then 237 | return constants.op_codes.QUERY 238 | elseif query.is_batch_statement then 239 | return constants.op_codes.BATCH 240 | else 241 | return constants.op_codes.EXECUTE 242 | end 243 | end 244 | 245 | function _M.frame_body(query, args, options) 246 | -- Determine if query is a query, statement, or batch 247 | local query_repr = query_representation(query) 248 | 249 | -- Flags of the 250 | local flags_repr = 0 251 | 252 | if args then 253 | flags_repr = setbit(flags_repr, constants.query_flags.VALUES) 254 | end 255 | 256 | local result_page_size = "" 257 | if options.page_size > 0 then 258 | flags_repr = setbit(flags_repr, constants.query_flags.PAGE_SIZE) 259 | result_page_size = encoding.int_representation(options.page_size) 260 | end 261 | 262 | local paging_state = "" 263 | if options.paging_state then 264 | flags_repr = setbit(flags_repr, constants.query_flags.PAGING_STATE) 265 | paging_state = encoding.bytes_representation(options.paging_state) 266 | end 267 | 268 | -- : [<...>][][] 269 | local query_parameters = encoding.short_representation(options.consistency_level) .. 270 | string.char(flags_repr) .. encoding.values_representation(args) .. 271 | result_page_size .. paging_state 272 | 273 | -- frame body: 274 | return query_repr .. query_parameters 275 | end 276 | 277 | function _M.send_frame_and_get_response(self, op_code, body, tracing) 278 | local version = string.char(constants.version_codes.REQUEST) 279 | local flags = tracing and '\002' or '\000' 280 | local stream_id = '\000' 281 | local length = encoding.int_representation(#body) 282 | local frame = version .. flags .. stream_id .. string.char(op_code) .. length .. body 283 | 284 | local bytes, err = self.sock:send(frame) 285 | if not bytes then 286 | return nil, string.format("Failed to read frame header from %s: %s", self.host, err) 287 | end 288 | return read_frame(self) 289 | end 290 | 291 | return _M 292 | -------------------------------------------------------------------------------- /src/cassandra/encoding.lua: -------------------------------------------------------------------------------- 1 | local constants = require("cassandra.constants") 2 | 3 | local _M = {} 4 | 5 | local function big_endian_representation(num, bytes) 6 | if num < 0 then 7 | -- 2's complement 8 | num = math.pow(0x100, bytes) + num 9 | end 10 | local t = {} 11 | while num > 0 do 12 | local rest = math.fmod(num, 0x100) 13 | table.insert(t, 1, string.char(rest)) 14 | num = (num-rest) / 0x100 15 | end 16 | local padding = string.rep(string.char(0), bytes - #t) 17 | return padding .. table.concat(t) 18 | end 19 | 20 | local function int_representation(num) 21 | return big_endian_representation(num, 4) 22 | end 23 | 24 | local function short_representation(num) 25 | return big_endian_representation(num, 2) 26 | end 27 | 28 | local function bigint_representation(n) 29 | local first_byte 30 | if n >= 0 then 31 | first_byte = 0 32 | else 33 | first_byte = 0xFF 34 | end 35 | return string.char(first_byte, -- only 53 bits from double 36 | math.floor(n / 0x1000000000000) % 0x100, 37 | math.floor(n / 0x10000000000) % 0x100, 38 | math.floor(n / 0x100000000) % 0x100, 39 | math.floor(n / 0x1000000) % 0x100, 40 | math.floor(n / 0x10000) % 0x100, 41 | math.floor(n / 0x100) % 0x100, 42 | n % 0x100) 43 | end 44 | 45 | local function uuid_representation(value) 46 | local str = string.gsub(value, "-", "") 47 | local buffer = {} 48 | for i = 1, #str, 2 do 49 | local byte_str = string.sub(str, i, i + 1) 50 | buffer[#buffer + 1] = string.char(tonumber(byte_str, 16)) 51 | end 52 | return table.concat(buffer) 53 | end 54 | 55 | local function string_representation(str) 56 | return short_representation(#str) .. str 57 | end 58 | 59 | local function long_string_representation(str) 60 | return int_representation(#str) .. str 61 | end 62 | 63 | local function bytes_representation(bytes) 64 | return int_representation(#bytes) .. bytes 65 | end 66 | 67 | local function short_bytes_representation(bytes) 68 | return short_representation(#bytes) .. bytes 69 | end 70 | 71 | local function string_map_representation(map) 72 | local buffer = {} 73 | local n = 0 74 | for k, v in pairs(map) do 75 | buffer[#buffer + 1] = string_representation(k) 76 | buffer[#buffer + 1] = string_representation(v) 77 | n = n + 1 78 | end 79 | return short_representation(n) .. table.concat(buffer) 80 | end 81 | 82 | local function boolean_representation(value) 83 | if value then return "\001" else return "\000" end 84 | end 85 | 86 | -- 'inspired' by https://github.com/fperrad/lua-MessagePack/blob/master/src/MessagePack.lua 87 | local function double_representation(number) 88 | local sign = 0 89 | if number < 0.0 then 90 | sign = 0x80 91 | number = -number 92 | end 93 | local mantissa, exponent = math.frexp(number) 94 | if mantissa ~= mantissa then 95 | return string.char(0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- nan 96 | elseif mantissa == math.huge then 97 | if sign == 0 then 98 | return string.char(0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- +inf 99 | else 100 | return string.char(0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- -inf 101 | end 102 | elseif mantissa == 0.0 and exponent == 0 then 103 | return string.char(sign, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) -- zero 104 | else 105 | exponent = exponent + 0x3FE 106 | mantissa = (mantissa * 2.0 - 1.0) * math.ldexp(0.5, 53) 107 | return string.char(sign + math.floor(exponent / 0x10), 108 | (exponent % 0x10) * 0x10 + math.floor(mantissa / 0x1000000000000), 109 | math.floor(mantissa / 0x10000000000) % 0x100, 110 | math.floor(mantissa / 0x100000000) % 0x100, 111 | math.floor(mantissa / 0x1000000) % 0x100, 112 | math.floor(mantissa / 0x10000) % 0x100, 113 | math.floor(mantissa / 0x100) % 0x100, 114 | mantissa % 0x100) 115 | end 116 | end 117 | 118 | local function float_representation(number) 119 | if number == 0 then 120 | return string.char(0x00, 0x00, 0x00, 0x00) 121 | elseif number ~= number then 122 | return string.char(0xFF, 0xFF, 0xFF, 0xFF) 123 | else 124 | local sign = 0x00 125 | if number < 0 then 126 | sign = 0x80 127 | number = -number 128 | end 129 | local mantissa, exponent = math.frexp(number) 130 | exponent = exponent + 0x7F 131 | if exponent <= 0 then 132 | mantissa = math.ldexp(mantissa, exponent - 1) 133 | exponent = 0 134 | elseif exponent > 0 then 135 | if exponent >= 0xFF then 136 | return string.char(sign + 0x7F, 0x80, 0x00, 0x00) 137 | elseif exponent == 1 then 138 | exponent = 0 139 | else 140 | mantissa = mantissa * 2 - 1 141 | exponent = exponent - 1 142 | end 143 | end 144 | mantissa = math.floor(math.ldexp(mantissa, 23) + 0.5) 145 | return string.char( 146 | sign + math.floor(exponent / 2), 147 | (exponent % 2) * 0x80 + math.floor(mantissa / 0x10000), 148 | math.floor(mantissa / 0x100) % 0x100, 149 | mantissa % 0x100) 150 | end 151 | end 152 | 153 | local function inet_representation(value) 154 | local digits = {} 155 | -- ipv6 156 | for d in string.gmatch(value, "([^:]+)") do 157 | if #d == 4 then 158 | for i = 1, #d, 2 do 159 | digits[#digits + 1] = string.char(tonumber(string.sub(d, i, i + 1), 16)) 160 | end 161 | end 162 | end 163 | -- ipv4 164 | if #digits == 0 then 165 | for d in string.gmatch(value, "(%d+)") do 166 | table.insert(digits, string.char(d)) 167 | end 168 | end 169 | return table.concat(digits) 170 | end 171 | 172 | local function list_representation(elements) 173 | local buffer = {short_representation(#elements)} 174 | for _, value in ipairs(elements) do 175 | buffer[#buffer + 1] = _M.value_representation(value, true) 176 | end 177 | return table.concat(buffer) 178 | end 179 | 180 | local function set_representation(elements) 181 | return list_representation(elements) 182 | end 183 | 184 | local function map_representation(map) 185 | local buffer = {} 186 | local size = 0 187 | for key, value in pairs(map) do 188 | buffer[#buffer + 1] = _M.value_representation(key, true) 189 | buffer[#buffer + 1] = _M.value_representation(value, true) 190 | size = size + 1 191 | end 192 | table.insert(buffer, 1, short_representation(size)) 193 | return table.concat(buffer) 194 | end 195 | 196 | local function identity_representation(value) 197 | return value 198 | end 199 | 200 | local encoders = { 201 | -- custom=0x00, 202 | [constants.types.ascii]=identity_representation, 203 | [constants.types.bigint]=bigint_representation, 204 | [constants.types.blob]=identity_representation, 205 | [constants.types.boolean]=boolean_representation, 206 | [constants.types.counter]=bigint_representation, 207 | -- decimal=0x06, 208 | [constants.types.double]=double_representation, 209 | [constants.types.float]=float_representation, 210 | [constants.types.int]=int_representation, 211 | [constants.types.text]=identity_representation, 212 | [constants.types.timestamp]=bigint_representation, 213 | [constants.types.uuid]=uuid_representation, 214 | [constants.types.varchar]=identity_representation, 215 | [constants.types.varint]=int_representation, 216 | [constants.types.timeuuid]=uuid_representation, 217 | [constants.types.inet]=inet_representation, 218 | [constants.types.list]=list_representation, 219 | [constants.types.map]=map_representation, 220 | [constants.types.set]=set_representation 221 | } 222 | 223 | local function infer_type(value) 224 | if type(value) == 'number' and math.floor(value) == value then 225 | return constants.types.int 226 | elseif type(value) == 'number' then 227 | return constants.types.float 228 | elseif type(value) == 'boolean' then 229 | return constants.types.boolean 230 | elseif type(value) == 'table' and value.type == 'null' then 231 | return _M.null 232 | elseif type(value) == 'table' and value.type then 233 | return constants.types[value.type] 234 | else 235 | return constants.types.varchar 236 | end 237 | end 238 | 239 | -- 240 | -- Public interface 241 | -- 242 | 243 | _M.int_representation = int_representation 244 | _M.short_representation = short_representation 245 | _M.bytes_representation = bytes_representation 246 | _M.string_map_representation = string_map_representation 247 | _M.short_bytes_representation = short_bytes_representation 248 | _M.long_string_representation = long_string_representation 249 | 250 | function _M.value_representation(value, short) 251 | local infered_type = infer_type(value) 252 | if type(value) == 'table' and value.type and value.value then 253 | value = value.value 254 | end 255 | if infered_type == _M.null then 256 | if short then 257 | return short_representation(-1) 258 | else 259 | return int_representation(-1) 260 | end 261 | end 262 | local representation = encoders[infered_type](value) 263 | if short then 264 | return short_bytes_representation(representation) 265 | end 266 | return bytes_representation(representation) 267 | end 268 | 269 | function _M.values_representation(args) 270 | if not args then 271 | return "" 272 | end 273 | local values = {} 274 | values[#values + 1] = short_representation(#args) 275 | for _, value in ipairs(args) do 276 | values[#values + 1] = _M.value_representation(value) 277 | end 278 | return table.concat(values) 279 | end 280 | 281 | function _M.batch_representation(queries, batch_type) 282 | local b = {} 283 | -- 284 | b[#b + 1] = string.char(batch_type) 285 | -- (number of queries) 286 | b[#b + 1] = short_representation(#queries) 287 | -- (operations) 288 | for _, query in ipairs(queries) do 289 | local kind 290 | local string_or_id 291 | if type(query.query) == "string" then 292 | kind = boolean_representation(false) 293 | string_or_id = long_string_representation(query.query) 294 | else 295 | kind = boolean_representation(true) 296 | string_or_id = short_bytes_representation(query.query.id) 297 | end 298 | 299 | -- The behaviour is sligthly different than from 300 | -- for : 301 | -- [...] (n cannot be 0), otherwise is being mixed up with page_size 302 | -- for batch : 303 | -- ... (n can be 0, but is required) 304 | if query.args then 305 | b[#b + 1] = kind .. string_or_id .. _M.values_representation(query.args) 306 | else 307 | b[#b + 1] = kind .. string_or_id .. short_representation(0) 308 | end 309 | end 310 | 311 | -- ... 312 | return table.concat(b) 313 | end 314 | 315 | return _M 316 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repo is **not** being actively maintained. I highly recommend that you try [lua-cassandra](https://github.com/thibaultcha/lua-cassandra) 2 | 3 | # lua-resty-cassandra 4 | 5 | [![Build Status][badge-travis-image]][badge-travis-url] 6 | [![Coverage Status][badge-coveralls-image]][badge-coveralls-url] 7 | ![Module Version][badge-version-image] 8 | [![Join the chat at https://gitter.im/jbochi/lua-resty-cassandra](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jbochi/lua-resty-cassandra?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 9 | 10 | Pure Lua Cassandra client using CQL binary protocol v2. 11 | 12 | It is 100% non-blocking if used in Nginx/Openresty but can also be used with luasocket. 13 | 14 | ## Installation 15 | 16 | #### Luarocks 17 | 18 | Installation through [luarocks][luarocks-url] is recommended: 19 | 20 | ```bash 21 | $ luarocks install cassandra 22 | ``` 23 | 24 | #### Manual 25 | 26 | Copy the `src/` folder and require `cassandra.lua`. 27 | 28 | ## Usage 29 | 30 | Overview: 31 | 32 | ```lua 33 | local cassandra = require "cassandra" 34 | 35 | local session = cassandra.new() 36 | session:set_timeout(1000) -- 1000ms timeout 37 | 38 | local connected, err = session:connect("127.0.0.1", 9042) 39 | 40 | session:set_keyspace("lua_tests") 41 | 42 | -- simple query 43 | local table_created, err = session:execute [[ 44 | CREATE TABLE users( 45 | user_id uuid PRIMARY KEY, 46 | name varchar, 47 | age int 48 | ) 49 | ]] 50 | 51 | -- query with arguments 52 | local ok, err = session:execute([[ 53 | INSERT INTO users(name, age, user_id) VALUES(?, ?, ?) 54 | ]], {"John O'Reilly", 42, cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11")}) 55 | 56 | 57 | -- select statement 58 | local users, err = session:execute("SELECT name, age, user_id from users") 59 | 60 | assert(1 == #users) 61 | local user = users[1] 62 | ngx.say(user.name) -- "John O'Reilly" 63 | ngx.say(user.user_id) -- "1144bada-852c-11e3-89fb-e0b9a54a6d11" 64 | ngx.say(user.age) -- 42 65 | ``` 66 | 67 | You can check more examples in the [tests](https://github.com/jbochi/lua-resty-cassandra/blob/master/spec/functional_spec.lua) or [here][anchor-examples]. 68 | 69 | ## Socket methods 70 | 71 | ### session, err = cassandra.new() 72 | 73 | Creates a new session. Create a socket with the cosocket API if available, fallback on luasocket otherwise. 74 | 75 | > **Return values:** 76 | > 77 | > * `session`: A lua-resty-cassandra session. 78 | > * `err`: Any error encountered during the socket creation. 79 | 80 | ### session:set_timeout(timeout) 81 | 82 | Sets timeout (in miliseconds). Uses Nginx [tcpsock:settimeout](http://wiki.nginx.org/HttpLuaModule#tcpsock:settimeout). 83 | 84 | > **Parameters:** 85 | > 86 | > * `timeout`: A number being the timeout in miliseconds 87 | 88 | ### ok, err = session:connect(contact_points, port) 89 | 90 | Connects to a single or multiple hosts at the given port. 91 | 92 | > **Parameters:** 93 | > 94 | > * `contact_points`: A string or an array of strings (hosts) to connect to. 95 | > * **Note:** If you wish to give a different port to one of those hosts, format the string as: "host:port" for that specific contact point. The specified `port` value will overwrite the `port` argument of `connect` for that contact point. 96 | > * `port`: The port number. **Default:** `9042`. 97 | 98 | > **Return values:** 99 | > 100 | > * `ok`: true if connected, false otherwise. Nil of the session doesn't have a socket. 101 | > * `err`: Any encountered error. 102 | 103 | ### ok, err = session:set_keepalive(max_idle_timeout, pool_size) -- Nginx only 104 | 105 | Puts the current Cassandra connection immediately into the ngx_lua cosocket connection pool. 106 | 107 | **Note**: Only call this method in the place you would have called the close method instead. Calling this method will immediately turn the current cassandra session object into the closed state. Any subsequent operations other than connect() on the current objet will return the closed error. 108 | 109 | > **Parameters:** 110 | > 111 | > * `max_idle_timeout`: Max idle timeout (in ms) when the connection is in the pool 112 | > * `pool_size`: Maximal size of the pool every nginx worker process. 113 | 114 | > **Return values:** 115 | > 116 | > * `ok`: `1` if success, nil otherwise. 117 | > * `err`: Encountered error if any 118 | 119 | ### times, err = session:get_reused_times() -- Nginx only 120 | 121 | This method returns the (successfully) reused times for the current connection. In case of error, it returns `nil` and a string describing the error. 122 | 123 | **Note:** If the current connection does not come from the built-in connection pool, then this method always returns `0`, that is, the connection has never been reused (yet). If the connection comes from the connection pool, then the return value is always non-zero. So this method can also be used to determine if the current connection comes from the pool. 124 | 125 | > **Return values:** 126 | > 127 | > * `times`: Number of times the current connection was successfully reused, nil if error 128 | > * `err`: Encountered error if any 129 | 130 | ### ok, err = session:close() 131 | 132 | Closes the current connection and returns the status. 133 | 134 | > **Return values:** 135 | > 136 | > * `ok`: `1` if success, nil otherwise. 137 | > * `err`: Encountered error if any 138 | 139 | ## Client methods 140 | 141 | All errors returned by functions in this section are tables with the following properties: 142 | 143 | > * `code`: A string from one of the `error_codes` in `cassandra.contants`. 144 | > * `raw_message`: The error message being returned by Cassandra. 145 | > * `message`: A constructed error message with `code` + `raw_message`. 146 | 147 | Error tables implement the `__tostring` method and are thus printable. A stringified error table will outputs its `message` property. 148 | 149 | ### ok, err = session:set_keyspace(keyspace_name) 150 | 151 | Sets session keyspace to the given `keyspace_name`. 152 | 153 | > **Parameters:** 154 | > 155 | > * `keyspace_name`: Name of the keyspace to use. 156 | 157 | > **Return values:** 158 | > 159 | > See `:execute()` 160 | 161 | ### stmt, err = session:prepare(query, options) 162 | 163 | Prepare a statement for later execution. 164 | 165 | > **Parameters:** 166 | > 167 | > * `query`: A string representing a query to prepare. 168 | > * `options`: The same options available on `:execute()`. 169 | 170 | > **Return values:** 171 | > 172 | > * `stmt`: A prepareed statement to be used by `:execute()`, nil if the preparation failed. 173 | > * `err`: Encountered error if any. 174 | 175 | ### result, err = session:execute(query, args, options) 176 | 177 | Execute a query or previously prepared statement. 178 | 179 | > **Parameters:** 180 | > 181 | > * `query`: A string representing a query or a previously prepared statement. 182 | > * `args`: An array of arguments to bind to the query. Those arguments can be type annotated (example: `cassandra.bigint(4)`. If there is no annotation, the driver will try to infer a type. Since integer numbers are serialized as int with 4 bytes, Cassandra would return an error if we tried to insert it in a bigint column. 183 | > * `options` is a table of options: 184 | > * `consistency_level`: for example `cassandra.consistency.ONE` 185 | > * `tracing`: if set to true, enables tracing for this query. In this case, the result table will contain a key named `tracing_id` with an uuid of the tracing session. 186 | > * `page_size`: Maximum size of the page to fetch (default: 5000). 187 | > * `auto_paging`: If set to true, `execute` will return an iterator. See the [example below][anchor-examples] on how to use auto pagination. 188 | 189 | > **Return values:** 190 | > 191 | > * `result`: A table containing the result of this query if successful, ni otherwise. The table can contain additional keys: 192 | > * `type`: Type of the result set, can either be "VOID", "ROWS", "SET_KEYSPACE" or "SCHEMA_CHANGE". 193 | > * `meta`: If the result type is "ROWS" and the result has more pages that haven't been returned, this property will contain 2 values: `has_more_pages` and `paging_state`. See the [example below][anchor-examples] on how to use pagination. 194 | > * `err`: Encountered error if any. 195 | 196 | ### batch, err = cassandra.BatchStatement(type) 197 | 198 | Initialized a batch statement. See the [example below][anchor-examples] on how to use batch statements and [this](http://www.datastax.com/documentation/cql/3.1/cql/cql_reference/batch_r.html) for informations about the type of batch to use. 199 | 200 | > **Parameters:** 201 | > 202 | > * `type`: The type of batch statement. Can be ony of those: 203 | > * `cassandra.batch_types.LOGGED` (default) 204 | > * `cassandra.batch_types.UNLOGGED` 205 | > * `cassandra.batch_types.COUNTER` 206 | 207 | > **Return values:** 208 | > 209 | > * `batch`: An empty batch statement on which to add operations. 210 | > * `err`: Encountered error if any. 211 | 212 | ### batch:add(query, args) 213 | 214 | Add an operation to a batch statement. See the [example below][anchor-examples] on how to use batch statements. 215 | 216 | > **Parameters:** 217 | > 218 | > * `query`: A string representing a query or a previously prepared statement. 219 | > * `args`: An array of arguments to bind to the query, similar to `:execute()`. 220 | 221 | ### trace, err = session:get_trace(result) 222 | 223 | Return the trace of a given result, if possible. 224 | 225 | > **Parameters:** 226 | > 227 | > * `result`: A previous query result. 228 | 229 | > **Return values:** 230 | > 231 | > `trace`: is a table with the following keys (from `system_traces.sessions` and `system_traces.events` [system tracing tables](http://www.datastax.com/dev/blog/advanced-request-tracing-in-cassandra-1-2): 232 | > 233 | > * coordinator 234 | > * duration 235 | > * parameters 236 | > * request 237 | > * started_at 238 | > * events: an array of tables with the following keys: 239 | > * event_id 240 | > * activity 241 | > * source 242 | > * source_elapsed 243 | > * thread 244 | > 245 | > `err`: Encountered error if any. 246 | 247 | ## Examples 248 | 249 | Batches: 250 | 251 | ```lua 252 | -- Create a batch statement 253 | local batch = cassandra.BatchStatement() 254 | 255 | -- Add a query 256 | batch:add("INSERT INTO users (name, age, user_id) VALUES (?, ?, ?)", 257 | {"James", 32, cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93")}) 258 | 259 | -- Add a prepared statement 260 | local stmt, err = session:prepare("INSERT INTO users (name, age, user_id) VALUES (?, ?, ?)") 261 | batch:add(stmt, {"John", 45, cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11")}) 262 | 263 | -- Execute the batch 264 | local result, err = session:execute(batch) 265 | ``` 266 | 267 | Pagination might be very useful to build web services: 268 | 269 | ```lua 270 | -- Assuming our users table contains 1000 rows 271 | 272 | local query = "SELECT * FROM users" 273 | local rows, err = session:execute(query, nil, {page_size = 500}) -- default page_size is 5000 274 | 275 | assert.same(500, #rows) -- rows contains the 500 first rows 276 | 277 | if rows.meta.has_more_pages then 278 | local next_rows, err = session:execute(query, nil, {paging_state = rows.meta.paging_state}) 279 | 280 | assert.same(500, #next_rows) -- next_rows contains the next (and last) 500 rows 281 | end 282 | ``` 283 | 284 | Automated pagination: 285 | 286 | ```lua 287 | -- Assuming our users table now contains 10.000 rows 288 | 289 | local query = "SELECT * FROM users" 290 | 291 | for _, rows, page, err in session:execute(query, nil, {auto_paging=true}) do 292 | assert.same(5000, #rows) -- rows contains 5000 rows on each iteration in this case 293 | -- page: will be 1 on the first iteration, 2 on the second 294 | -- err: in case any fetch returns an error 295 | -- _: (the first for argument) is the current paging_state used to fetch the rows 296 | end 297 | ``` 298 | 299 | ## Running unit tests 300 | 301 | We use `busted` and require `luasocket` to mock `ngx.socket.tcp()`. To run the tests, start a local cassandra instance and run: 302 | 303 | ```bash 304 | $ luarocks install busted 305 | $ make test 306 | ``` 307 | 308 | ## Running coverage 309 | 310 | ```bash 311 | $ luarocks install luacov 312 | $ make coverage 313 | ``` 314 | 315 | Report will be in `./luacov.report.out`. 316 | 317 | ## Running linting 318 | 319 | ```bash 320 | $ luarocks install luacheck 321 | $ make lint 322 | ``` 323 | 324 | ## Contributors 325 | 326 | Juarez Bochi (@jbochi) 327 | 328 | Thibault Charbonnier (@thibaultCha) -> Several contributions, including paging support, improved batch statements, better documentation, specs and code style. 329 | 330 | Leandro Moreira (@leandromoreira) -> Added support for doubles 331 | 332 | Marco Palladino (@thefosk) 333 | 334 | [badge-travis-url]: https://travis-ci.org/jbochi/lua-resty-cassandra 335 | [badge-travis-image]: https://img.shields.io/travis/jbochi/lua-resty-cassandra.svg?style=flat 336 | 337 | [badge-coveralls-url]: https://coveralls.io/r/jbochi/lua-resty-cassandra?branch=master 338 | [badge-coveralls-image]: https://coveralls.io/repos/jbochi/lua-resty-cassandra/badge.svg?branch=master 339 | 340 | [badge-version-image]: https://img.shields.io/badge/version-0.5--7-green.svg?style=flat 341 | 342 | [luarocks-url]: https://luarocks.org 343 | 344 | [anchor-examples]: #examples 345 | -------------------------------------------------------------------------------- /spec/functional_spec.lua: -------------------------------------------------------------------------------- 1 | package.path = "src/?.lua;spec/?.lua;" .. package.path 2 | 3 | _G.ngx = require("fake_ngx") 4 | local cassandra = require("cassandra") 5 | local constants = require("cassandra.constants") 6 | 7 | describe("cassandra", function() 8 | 9 | before_each(function() 10 | session = cassandra.new() 11 | session:set_timeout(1000) 12 | 13 | connected, err = session:connect("127.0.0.1", 9042) 14 | assert.falsy(err) 15 | 16 | local res, err = session:execute [[ 17 | CREATE KEYSPACE IF NOT EXISTS lua_tests 18 | WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 } 19 | ]] 20 | assert.falsy(err) 21 | 22 | session:set_keyspace("lua_tests") 23 | end) 24 | 25 | teardown(function() 26 | local res, err = session:execute("DROP KEYSPACE lua_tests") 27 | assert.falsy(err) 28 | end) 29 | 30 | describe("Connection #socket", function() 31 | it("should be possible to connect", function() 32 | assert.truthy(connected) 33 | end) 34 | 35 | it("should return an error if attempting to connect without an initialized session", function() 36 | local connected, err = cassandra.connect({}, "127.0.0.1") 37 | assert.falsy(connected) 38 | assert.same("session does not have a socket, create a new session first.", err) 39 | end) 40 | 41 | it("should not require port for connection", function() 42 | local new_session = cassandra.new() 43 | new_session:set_timeout(1000) 44 | local connected, err = new_session:connect("127.0.0.1") 45 | assert.falsy(err) 46 | assert.truthy(connected) 47 | end) 48 | 49 | it("should require a host for connection", function() 50 | local new_session = cassandra.new() 51 | new_session:set_timeout(1000) 52 | local connected, err = new_session:connect(nil) 53 | assert.falsy(connected) 54 | assert.truthy(err) 55 | end) 56 | 57 | it("should be possible to send a list of hosts for connection", function() 58 | local new_session = cassandra.new() 59 | new_session:set_timeout(1000) 60 | local connected, err = new_session:connect({"localhost", "127.0.0.1"}) 61 | assert.falsy(err) 62 | assert.truthy(connected) 63 | end) 64 | 65 | it("should be possible to send a list of hosts with ports for connection", function() 66 | -- If a contact point is of form "host:port", this port will overwrite the one given as parameter of `connect` 67 | local new_session = cassandra.new() 68 | new_session:set_timeout(1000) 69 | local connected, err = new_session:connect({"127.0.0.1:9042"}, 9999) 70 | assert.falsy(err) 71 | assert.truthy(connected) 72 | end) 73 | 74 | it("should try another host if it fails to connect", function() 75 | local new_session = cassandra.new() 76 | new_session:set_timeout(1000) 77 | local connected, err = new_session:connect({"0.0.0.1", "0.0.0.2", "0.0.0.3", "127.0.0.1"}) 78 | assert.falsy(err) 79 | assert.truthy(connected) 80 | end) 81 | 82 | it("should return error if it fails to connect to all hosts", function() 83 | local new_session = cassandra.new() 84 | new_session:set_timeout(1000) 85 | local connected, err = new_session:connect({"0.0.0.1", "0.0.0.2", "0.0.0.3"}) 86 | assert.truthy(err) 87 | assert.falsy(connected) 88 | end) 89 | 90 | it("should fallback to luasocket when creating a session", function() 91 | local ngx_mock = _G.ngx 92 | _G.ngx = nil 93 | local new_session = cassandra.new() 94 | new_session:set_timeout(1000) 95 | local connected, err = new_session:connect("127.0.0.1") 96 | assert.falsy(err) 97 | assert.truthy(connected) 98 | _G.ngx = ngx_mock 99 | end) 100 | end) 101 | 102 | describe("Query results", function() 103 | local rows, err 104 | 105 | before_each(function() 106 | rows, err = session:execute [[ 107 | SELECT cql_version, native_protocol_version, release_version FROM system.local 108 | ]] 109 | assert.falsy(err) 110 | assert.truthy(rows) 111 | end) 112 | 113 | it("should have a length", function() 114 | assert.same(1, #rows) 115 | end) 116 | 117 | describe("Result row", function() 118 | local row 119 | 120 | before_each(function() 121 | row = rows[1] 122 | end) 123 | 124 | it("should access a row column by column name", function() 125 | assert.truthy(row.native_protocol_version == "2" or row.native_protocol_version == "3") 126 | end) 127 | 128 | it("should access a row column by index position", function() 129 | assert.same(row[1], row.cql_version) 130 | assert.same(row[2], row.native_protocol_version) 131 | assert.same(row[3], row.release_version) 132 | end) 133 | 134 | if (_VERSION >= "Lua 5.2") then 135 | it("should have the correct number of columns", function() 136 | assert.same(#row, 3) 137 | end) 138 | end 139 | 140 | it("should be iterable by key and value", function() 141 | local columns = {cql_version="cql_version", 142 | native_protocol_version="native_protocol_version", 143 | release_version="release_version"} 144 | local n_columns = 0 145 | for key, _ in pairs(row) do 146 | assert.same(columns[key], key) 147 | n_columns = n_columns + 1 148 | end 149 | assert.same(n_columns, 3) 150 | end) 151 | end) 152 | end) 153 | 154 | describe("Query tracing", function() 155 | it("should be queryable with tracing", function() 156 | local rows, err = session:execute([[ 157 | SELECT cql_version, native_protocol_version, release_version FROM system.local 158 | ]], nil, {tracing=true}) 159 | 160 | assert.falsy(err) 161 | assert.truthy(rows.tracing_id) 162 | end) 163 | end) 164 | 165 | describe("Prepared statements", function() 166 | it("should support prepared statements", function() 167 | local stmt, err = session:prepare("SELECT native_protocol_version FROM system.local") 168 | assert.falsy(err) 169 | assert.truthy(stmt) 170 | local rows = session:execute(stmt) 171 | assert.same(1, #rows) 172 | assert.truthy(rows[1].native_protocol_version == "2" or rows[1].native_protocol_version == "3") 173 | end) 174 | 175 | it("should support variadic arguments in prepared statements", function() 176 | local stmt, err = session:prepare("SELECT * FROM system.local WHERE key IN ?") 177 | assert.falsy(err) 178 | assert.truthy(stmt) 179 | local rows = session:execute(stmt, {cassandra.list({"local", "not local"})}) 180 | assert.same(1, #rows) 181 | assert.truthy(rows[1].key == "local") 182 | end) 183 | 184 | it("should support tracing for prepared statements", function() 185 | local stmt, err = session:prepare("SELECT native_protocol_version FROM system.local", {tracing=true}) 186 | assert.falsy(err) 187 | assert.truthy(stmt) 188 | assert.truthy(stmt.tracing_id) 189 | end) 190 | end) 191 | 192 | describe("Keyspace", function() 193 | it("should catch (rich) errors", function() 194 | local ok, err = session:set_keyspace("invalid_keyspace") 195 | assert.same(constants.error_codes.INVALID, err.code) 196 | assert.same("Keyspace 'invalid_keyspace' does not exist", err.raw_message) 197 | assert.same([[Cassandra returned error (Invalid): "Keyspace 'invalid_keyspace' does not exist"]], tostring(err)) 198 | 199 | -- Test concatenation of an error 200 | assert.same([[Cassandra returned error (Invalid): "Keyspace 'invalid_keyspace' does not exist"]] .. "foo", err .. "foo") 201 | assert.same("foo" .. [[Cassandra returned error (Invalid): "Keyspace 'invalid_keyspace' does not exist"]], "foo" .. err) 202 | assert.same([[Cassandra returned error (Invalid): "Keyspace 'invalid_keyspace' does not exist"]]..[[Cassandra returned error (Invalid): "Keyspace 'invalid_keyspace' does not exist"]], err .. err) 203 | end) 204 | 205 | it("should be possible to use a namespace", function() 206 | local ok, err = session:set_keyspace("lua_tests") 207 | assert.falsy(err) 208 | assert.truthy(ok) 209 | end) 210 | end) 211 | 212 | describe("Real use-case", function() 213 | 214 | setup(function() 215 | table_created, err = session:execute [[ 216 | CREATE TABLE IF NOT EXISTS users ( 217 | user_id uuid PRIMARY KEY, 218 | name varchar, 219 | age int 220 | ) 221 | ]] 222 | assert.falsy(err) 223 | end) 224 | 225 | after_each(function() 226 | session:execute("TRUNCATE users") 227 | end) 228 | 229 | teardown(function() 230 | session:execute("DROP TABLE users") 231 | end) 232 | 233 | describe("DDL statements", function() 234 | it("should be possible to create a table", function() 235 | assert.same("users", table_created.table) 236 | end) 237 | 238 | it("should not be possible to create a table twice", function() 239 | local table_created, err = session:execute [[ 240 | CREATE TABLE users ( 241 | user_id uuid PRIMARY KEY, 242 | name varchar, 243 | age int 244 | ) 245 | ]] 246 | assert.is_not_true(table_created) 247 | assert.same(constants.error_codes.ALREADY_EXISTS, err.code) 248 | assert.same('Cannot add already existing column family "users" to keyspace "lua_tests"', err.raw_message) 249 | assert.same('Cassandra returned error (Already_exists): "Cannot add already existing column family "users" to keyspace "lua_tests""', tostring(err)) 250 | end) 251 | end) 252 | 253 | describe("DML statements", function() 254 | it("should be possible to insert a row", function() 255 | local ok, err = session:execute [[ 256 | INSERT INTO users (name, age, user_id) 257 | VALUES ('John O''Reilly', 42, 2644bada-852c-11e3-89fb-e0b9a54a6d93) 258 | ]] 259 | assert.falsy(err) 260 | assert.truthy(ok) 261 | end) 262 | 263 | it("should be possible to insert a row with tracing", function() 264 | local query = [[ 265 | INSERT INTO users (name, age, user_id) 266 | VALUES ('John O''Reilly', 42, 2644bada-852c-11e3-89fb-e0b9a54a6d93) 267 | ]] 268 | local result, err = session:execute(query, {}, {tracing=true}) 269 | assert.falsy(err) 270 | assert.truthy(result) 271 | assert.truthy(result.tracing_id) 272 | local tracing, err = session:get_trace(result) 273 | assert.falsy(err) 274 | assert.truthy(query, tracing.query) 275 | assert.truthy(tracing.started_at > 0) 276 | assert.truthy(#tracing.events > 0) 277 | end) 278 | 279 | it("should be possible to set consistency level", function() 280 | local ok, err = session:execute([[ 281 | INSERT INTO users (name, age, user_id) 282 | VALUES ('John O''Reilly', 42, 2644bada-852c-11e3-89fb-e0b9a54a6d93) 283 | ]], {}, {consistency_level=cassandra.consistency.TWO}) 284 | assert.same('Cassandra returned error (Unavailable exception): "Cannot achieve consistency level TWO"', tostring(err)) 285 | end) 286 | 287 | it("should support queries with arguments", function() 288 | local ok, err = session:execute([[ 289 | INSERT INTO users (name, age, user_id) 290 | VALUES (?, ?, ?) 291 | ]], {"John O'Reilly", 42, cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93")}) 292 | assert.falsy(err) 293 | 294 | local users, err = session:execute("SELECT name, age, user_id from users") 295 | 296 | assert.same(1, #users) 297 | local user = users[1] 298 | assert.same("John O'Reilly", user.name) 299 | assert.same("2644bada-852c-11e3-89fb-e0b9a54a6d93", user.user_id) 300 | assert.same(42, user.age) 301 | assert.truthy(ok) 302 | end) 303 | end) 304 | end) 305 | 306 | describe("Types #types", function() 307 | 308 | local types = require "spec.type_fixtures" 309 | 310 | for _, type in ipairs(types) do 311 | describe("Type " .. type.name, function() 312 | 313 | setup(function() 314 | session:execute([[ 315 | CREATE TABLE type_test_table ( 316 | key varchar PRIMARY KEY, 317 | value ]] .. type.name .. [[ 318 | ) 319 | ]]) 320 | end) 321 | 322 | teardown(function() 323 | session:execute("DROP TABLE type_test_table") 324 | end) 325 | 326 | it("should be possible to insert and get value back", function() 327 | local ok, err = session:execute([[ 328 | INSERT INTO type_test_table (key, value) 329 | VALUES (?, ?) 330 | ]], {"key", type.insert_value ~= nil and type.insert_value or type.value}) 331 | assert.falsy(err) 332 | 333 | local rows, err = session:execute("SELECT value FROM type_test_table WHERE key = 'key'") 334 | assert.same(1, #rows) 335 | if type.read_test then 336 | assert.truthy(type.read_test(rows[1].value)) 337 | else 338 | assert.same(type.read_value ~= nil and type.read_value or type.value, rows[1].value) 339 | end 340 | end) 341 | end) 342 | end 343 | end) 344 | 345 | describe("Pagination #pagination", function() 346 | 347 | setup(function() 348 | session:execute [[ 349 | CREATE TABLE IF NOT EXISTS pagination_test_table( 350 | key int PRIMARY KEY, 351 | value varchar 352 | ) 353 | ]] 354 | for i = 1, 200 do 355 | session:execute([[ INSERT INTO pagination_test_table(key, value) 356 | VALUES(?,?) ]], { i, "test" }) 357 | end 358 | end) 359 | 360 | teardown(function() 361 | session:execute("DROP TABLE pagination_test_table") 362 | end) 363 | 364 | it("should have a high default value and signal that everything is fetched", function() 365 | local rows, err = session:execute("SELECT * FROM pagination_test_table") 366 | assert.falsy(err) 367 | assert.same(200, #rows) 368 | assert.is_not_true(rows.has_more_pages) 369 | end) 370 | 371 | it("should support a page_size option", function() 372 | local rows, err = session:execute("SELECT * FROM pagination_test_table", nil, {page_size=200}) 373 | assert.falsy(err) 374 | assert.same(200, #rows) 375 | end) 376 | 377 | it("should return metadata flags", function() 378 | -- Incomplete page 379 | local rows, err = session:execute("SELECT * FROM pagination_test_table", nil, {page_size=100}) 380 | assert.falsy(err) 381 | assert.is_true(rows.meta.has_more_pages) 382 | assert.truthy(rows.meta.paging_state) 383 | 384 | -- Complete page 385 | local rows, err = session:execute("SELECT * FROM pagination_test_table", nil, {page_size=500}) 386 | assert.falsy(err) 387 | assert.is_not_true(rows.meta.has_more_pages) 388 | assert.falsy(rows.meta.paging_state) 389 | end) 390 | 391 | it("should fetch the next page by passing a paging_state option", function() 392 | local rows_1, err = session:execute("SELECT * FROM pagination_test_table", nil, {page_size=100}) 393 | assert.falsy(err) 394 | assert.same(100, #rows_1) 395 | 396 | local rows_2, err = session:execute("SELECT * FROM pagination_test_table", nil, { 397 | page_size=500, 398 | paging_state=rows_1.meta.paging_state 399 | }) 400 | assert.falsy(err) 401 | assert.same(100, #rows_2) 402 | assert.are_not.same(rows_1, rows_2) 403 | end) 404 | 405 | describe("auto_paging", function() 406 | it("should return an iterator if given an auto_paging option", function() 407 | local page_tracker = 0 408 | for _, rows, page, err in session:execute("SELECT * FROM pagination_test_table", nil, {page_size=10, auto_paging=true}) do 409 | assert.falsy(err) 410 | page_tracker = page_tracker + 1 411 | assert.same(page_tracker, page) 412 | assert.same(10, #rows) 413 | end 414 | 415 | assert.same(20, page_tracker) 416 | end) 417 | 418 | it("should return the latest page of a set", function() 419 | local page_tracker = 0 420 | for _, rows, page, err in session:execute("SELECT * FROM pagination_test_table", nil, {page_size=199, auto_paging=true}) do 421 | assert.falsy(err) 422 | page_tracker = page_tracker + 1 423 | assert.same(page_tracker, page) 424 | end 425 | 426 | assert.same(2, page_tracker) 427 | 428 | -- Even if all results are fetched in the first query 429 | page_tracker = 0 430 | for _, rows, page, err in session:execute("SELECT * FROM pagination_test_table", nil, {auto_paging=true}) do 431 | assert.falsy(err) 432 | page_tracker = page_tracker + 1 433 | assert.same(page_tracker, page) 434 | assert.same(200, #rows) 435 | end 436 | 437 | assert.same(1, page_tracker) 438 | end) 439 | 440 | it("should return valid parameters if no results found", function() 441 | -- No results ~= no rows. This test validates the behaviour of err being 442 | -- returned if no results are returned (most likely because of an invalid query) 443 | local page_tracker = 0 444 | for _, rows, page, err in session:execute("SELECT * FROM pagination_test_table WHERE id = 500", nil, {auto_paging=true}) do 445 | assert.truthy(err) -- id is not a valid column 446 | page_tracker = page_tracker + 1 447 | end 448 | 449 | -- Assert the loop has been run once. 450 | assert.same(1, page_tracker) 451 | end) 452 | 453 | end) 454 | end) 455 | 456 | describe("Counters #counters", function() 457 | 458 | setup(function() 459 | session:execute [[ 460 | CREATE TABLE counter_test_table ( 461 | key varchar PRIMARY KEY, 462 | value counter 463 | ) 464 | ]] 465 | end) 466 | 467 | teardown(function() 468 | session:execute("DROP TABLE counter_test_table") 469 | end) 470 | 471 | after_each(function() 472 | session:execute("TRUNCATE counter_test_table") 473 | end) 474 | 475 | it("should be possible to increment and get value back", function() 476 | session:execute([[ 477 | UPDATE counter_test_table SET value = value + ? WHERE key = ? 478 | ]], {{type="counter", value=10}, "key"}) 479 | local rows, err = session:execute("SELECT value FROM counter_test_table WHERE key = 'key'") 480 | assert.falsy(err) 481 | assert.same(1, #rows) 482 | assert.same(10, rows[1].value) 483 | end) 484 | 485 | it("should be possible to decrement and get value back", function() 486 | session:execute([[ 487 | UPDATE counter_test_table SET value = value + ? WHERE key = ? 488 | ]], {{type="counter", value=-10}, "key"}) 489 | local rows, err = session:execute("SELECT value FROM counter_test_table WHERE key = 'key'") 490 | assert.falsy(err) 491 | assert.same(1, #rows) 492 | assert.same(-10, rows[1].value) 493 | end) 494 | end) 495 | 496 | describe("Batch statements #batch", function() 497 | 498 | setup(function() 499 | session:execute [[ 500 | CREATE TABLE IF NOT EXISTS users ( 501 | user_id uuid PRIMARY KEY, 502 | name varchar, 503 | age int 504 | ) 505 | ]] 506 | session:execute [[ 507 | CREATE TABLE counter_test_table ( 508 | key varchar PRIMARY KEY, 509 | value counter 510 | ) 511 | ]] 512 | end) 513 | 514 | teardown(function() 515 | session:execute("DROP TABLE users") 516 | session:execute("DROP TABLE counter_test_table") 517 | end) 518 | 519 | after_each(function() 520 | session:execute("TRUNCATE users") 521 | session:execute("TRUNCATE counter_test_table") 522 | end) 523 | 524 | it("should support logged batch statements", function() 525 | local batch = cassandra.BatchStatement() 526 | 527 | -- Query 528 | batch:add("INSERT INTO users (name, age, user_id) VALUES ('Marc', 28, c3dad2e5-40bd-4f01-cfc2-465787df746d)") 529 | 530 | -- Binded query 531 | batch:add("INSERT INTO users (name, age, user_id) VALUES (?, ?, ?)", 532 | {"James", 32, cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93")}) 533 | 534 | -- Prepared statement 535 | local stmt, err = session:prepare("INSERT INTO users (name, age, user_id) VALUES (?, ?, ?)") 536 | assert.falsy(err) 537 | batch:add(stmt, {"John", 45, cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11")}) 538 | 539 | local result, err = session:execute(batch) 540 | assert.falsy(err) 541 | assert.truthy(result) 542 | 543 | local users, err = session:execute("SELECT name, age, user_id from users") 544 | assert.falsy(err) 545 | assert.same(3, #users) 546 | assert.same("Marc", users[1].name) 547 | assert.same("James", users[2].name) 548 | assert.same("John", users[3].name) 549 | end) 550 | 551 | it("should support unlogged batch statements", function() 552 | local batch = cassandra.BatchStatement(cassandra.batch_types.UNLOGGED) 553 | 554 | batch:add("INSERT INTO users (name, age, user_id) VALUES (?, ?, ?)", 555 | {"James", 32, cassandra.uuid("2644bada-852c-11e3-89fb-e0b9a54a6d93")}) 556 | batch:add("INSERT INTO users (name, age, user_id) VALUES (?, ?, ?)", 557 | {"John", 45, cassandra.uuid("1144bada-852c-11e3-89fb-e0b9a54a6d11")}) 558 | 559 | local result, err = session:execute(batch) 560 | assert.falsy(err) 561 | assert.truthy(result) 562 | 563 | local users, err = session:execute("SELECT name, age, user_id from users") 564 | assert.falsy(err) 565 | assert.same(2, #users) 566 | assert.same("James", users[1].name) 567 | assert.same("John", users[2].name) 568 | end) 569 | 570 | it("should support counter batch statements", function() 571 | local batch = cassandra.BatchStatement(cassandra.batch_types.COUNTER) 572 | 573 | -- Query 574 | batch:add("UPDATE counter_test_table SET value = value + 1 WHERE key = 'key'") 575 | 576 | -- Binded queries 577 | batch:add("UPDATE counter_test_table SET value = value + 1 WHERE key = ?", {"key"}) 578 | batch:add("UPDATE counter_test_table SET value = value + 1 WHERE key = ?", {"key"}) 579 | 580 | -- Prepared statement 581 | local stmt, err = session:prepare [[ 582 | UPDATE counter_test_table SET value = value + 1 WHERE key = ? 583 | ]] 584 | assert.falsy(err) 585 | batch:add(stmt, {"key"}) 586 | 587 | local result, err = session:execute(batch) 588 | assert.falsy(err) 589 | assert.truthy(result) 590 | 591 | local rows, err = session:execute [[ 592 | SELECT value from counter_test_table WHERE key = 'key' 593 | ]] 594 | assert.falsy(err) 595 | assert.same(4, rows[1].value) 596 | end) 597 | end) 598 | end) 599 | --------------------------------------------------------------------------------