├── notify.pony ├── util.pony ├── Makefile ├── error.pony ├── README.md ├── option.pony ├── stmt.pony ├── mysql.pony ├── result.pony ├── mypony.c └── test.pony /notify.pony: -------------------------------------------------------------------------------- 1 | interface Notify 2 | fun fail(err: Error) => None 3 | -------------------------------------------------------------------------------- /util.pony: -------------------------------------------------------------------------------- 1 | primitive Util 2 | fun copy_cpointer(cs: Pointer[U8] iso, len: USize = 0): String => 3 | recover String.copy_cpointer(consume cs, len) end 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: build 2 | 3 | mypony: 4 | gcc -g -Wall -c mypony.c -o mypony.o 5 | ar rv libmypony.a mypony.o 6 | 7 | build: mypony 8 | ponyc -p . 9 | 10 | debug: mypony 11 | ponyc -dp . 12 | 13 | clean: 14 | rm -f *.o *.a 15 | -------------------------------------------------------------------------------- /error.pony: -------------------------------------------------------------------------------- 1 | class Error 2 | let _meth: String 3 | let _msg: String 4 | let _num: U32 5 | 6 | new create(meth: String, msg: String, num: U32 = 0) => 7 | _meth = meth 8 | _msg = msg 9 | _num = num 10 | 11 | fun method(): String => _meth 12 | fun message(): String => _msg 13 | fun number(): (U32 | None) => if _num == 0 then None else _num end 14 | 15 | fun string(): String => 16 | match _num 17 | | 0 => _meth + ": " + _msg 18 | else 19 | _meth + ": " + _num.string() + " " + _msg 20 | end 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pony-MySQL 2 | 3 | These are [Pony](http://www.ponylang.org/) bindings for [libmysqlclient](http://dev.mysql.com/doc/refman/5.7/en/c-api.html). 4 | 5 | The bindings only support the prepared statements API. Usage is as follows: 6 | 7 | ```pony 8 | use "mysql" 9 | 10 | class MyNotify is Notify 11 | let _env: Env 12 | 13 | new create(env: Env) => 14 | _env = env 15 | 16 | fun fail(err: Error) => 17 | _env.out.print(err.method() + " failed: " + err.string()) 18 | 19 | actor Main 20 | new create(env: Env) => 21 | try 22 | let mysql = MySQL(MyNotify(env)).tcp("host", "user", "pass", "db") 23 | mysql(CharsetName) = "utf8" 24 | let query = "SELECT * FROM some_table where some_field = ?" 25 | let some_field: QueryParam = "some_value" 26 | with stmt = mysql.prepare(query) do 27 | with res = stmt.execute([some_field]) do 28 | match res.fetch_map() 29 | | let row: Map[String, QueryResult] => _do_something(row) 30 | | None => env.out.print("no more results") 31 | end 32 | end 33 | end 34 | end 35 | 36 | fun _do_something(m: Map[String, QueryResult]) => 37 | ... 38 | ``` 39 | 40 | If you don't want to use `with` expressions you must call `close()` on `Stmt` 41 | and `Result` objects to free their resources. 42 | 43 | To run the unit tests, export environment variables `MYPONY_HOST`, `MYPONY_USER` 44 | and `MYPONY_PASS` with the appropriate information. 45 | -------------------------------------------------------------------------------- /option.pony: -------------------------------------------------------------------------------- 1 | primitive ConnectTimeout fun apply(): USize => 0 2 | primitive Compress fun apply(): USize => 1 3 | primitive NamedPipe fun apply(): USize => 2 4 | primitive InitCommand fun apply(): USize => 3 5 | primitive ReadDefaultFile fun apply(): USize => 4 6 | primitive ReadDefaultGroup fun apply(): USize => 5 7 | primitive CharsetDir fun apply(): USize => 6 8 | primitive CharsetName fun apply(): USize => 7 9 | primitive LocalInFile fun apply(): USize => 8 10 | primitive Protocol fun apply(): USize => 9 11 | primitive SharedMemoryBaseName fun apply(): USize => 10 12 | primitive ReadTimeout fun apply(): USize => 11 13 | primitive WriteTimeout fun apply(): USize => 12 14 | primitive UseResult fun apply(): USize => 13 15 | primitive UseRemoteConnection fun apply(): USize => 14 16 | primitive UseEmbeddedConnection fun apply(): USize => 15 17 | primitive GuessConnection fun apply(): USize => 16 18 | primitive ClientIP fun apply(): USize => 17 19 | primitive SecureAuth fun apply(): USize => 18 20 | primitive ReportDataTruncation fun apply(): USize => 19 21 | primitive Reconnect fun apply(): USize => 20 22 | primitive SSLVerifyServerCert fun apply(): USize => 21 23 | primitive PluginDir fun apply(): USize => 22 24 | primitive DefaultAuth fun apply(): USize => 23 25 | primitive Bind fun apply(): USize => 24 26 | primitive SSLKey fun apply(): USize => 25 27 | primitive SSLCert fun apply(): USize => 26 28 | primitive SSLCA fun apply(): USize => 27 29 | primitive SSLCAPath fun apply(): USize => 28 30 | primitive SSLCipher fun apply(): USize => 29 31 | primitive SSLCRL fun apply(): USize => 30 32 | primitive SSLCRLPath fun apply(): USize => 31 33 | primitive ConnectAttrReset fun apply(): USize => 32 34 | primitive ConnectAttrAdd fun apply(): USize => 33 35 | primitive ConnectAttrDelete fun apply(): USize => 34 36 | primitive ServerPublicKey fun apply(): USize => 35 37 | primitive EnableCleartextPlugin fun apply(): USize => 36 38 | primitive CanHandleExpiredPasswords fun apply(): USize => 37 39 | 40 | type ClientOption is 41 | ( ConnectTimeout 42 | //| Compress 43 | //| NamedPipe 44 | //| InitCommand 45 | //| ReadDefaultFile 46 | //| ReadDefaultGroup 47 | //| CharsetDir 48 | | CharsetName 49 | //| LocalInFile 50 | //| Protocol 51 | //| SharedMemoryBaseName 52 | //| ReadTimeout 53 | //| WriteTimeout 54 | //| UseResult 55 | //| UseRemoteConnection 56 | //| UseEmbeddedConnection 57 | //| GuessConnection 58 | //| ClientIP 59 | //| SecureAuth 60 | //| ReportDataTruncation 61 | //| Reconnect 62 | //| SSLVerifyServerCert 63 | //| PluginDir 64 | //| DefaultAuth 65 | //| Bind 66 | //| SSLKey 67 | //| SSLCert 68 | //| SSLCA 69 | //| SSLCAPath 70 | //| SSLCipher 71 | //| SSLCRL 72 | //| SSLCRLPath 73 | //| ConnectAttrReset 74 | //| ConnectAttrAdd 75 | //| ConnectAttrDelete 76 | //| ServerPublicKey 77 | //| EnableCleartextPlugin 78 | //| CanHandleExpiredPasswords 79 | ) 80 | -------------------------------------------------------------------------------- /stmt.pony: -------------------------------------------------------------------------------- 1 | use "time" 2 | 3 | class Stmt 4 | let _stmt: Pointer[_Stmt] 5 | let _notify: Notify 6 | let _params: Pointer[_Bind] 7 | let _result: Pointer[_Bind] 8 | let _res: Pointer[_Res] 9 | let _num_params: USize 10 | 11 | new _create(stmt: Pointer[_Stmt], notify: Notify) => 12 | _stmt = stmt 13 | _notify = notify 14 | _num_params = @mysql_stmt_param_count[USize](_stmt) 15 | _params = @mypony_alloc_bind[Pointer[_Bind]](_num_params) 16 | _res = @mysql_stmt_result_metadata[Pointer[_Res]](_stmt) 17 | _result = @mypony_alloc_result[Pointer[_Bind]](_stmt, _res) 18 | 19 | fun ref execute(args: Array[QueryParam] = Array[QueryParam]()): Result ? => 20 | let num_args = args.size() 21 | if num_args != _num_params then 22 | let err = "argument count (" + num_args.string() + ") " 23 | + "!= param count (" + _num_params.string() + ")" 24 | _notify.fail(Error("execute", err)) 25 | error 26 | end 27 | if num_args > 0 then 28 | _bind_params(args)? 29 | end 30 | if @mysql_stmt_execute[ISize](_stmt) != 0 then 31 | _execute_error()? 32 | end 33 | if @mypony_bind_result[ISize](_stmt, _res, _result) != 0 then 34 | _execute_error()? 35 | end 36 | Result._create(_stmt, _notify, _params, _result, _res) 37 | 38 | fun reset() ? => 39 | if @mysql_stmt_reset[U8](_stmt) != 0 then 40 | error 41 | end 42 | 43 | fun errno(): U32 => 44 | @mysql_stmt_errno[U32](_stmt) 45 | 46 | fun error_message(): String => 47 | Util.copy_cpointer(@mysql_stmt_error[Pointer[U8] iso^](_stmt)) 48 | 49 | fun _bind_params(args: Array[QueryParam]) ? => 50 | for (i, arg) in args.pairs() do 51 | match arg 52 | | None => 53 | @mypony_null_param[None](_params, i) 54 | | let x: I8 => 55 | @mypony_tiny_param[None](_params, x, i) 56 | | let x: I16 => 57 | @mypony_short_param[None](_params, x, i) 58 | | let x: I32 => 59 | @mypony_long_param[None](_params, x, i) 60 | | let x: (I64 | ILong | ISize) => 61 | @mypony_longlong_param[None](_params, x.i64(), i) 62 | | let x: U8 => 63 | @mypony_utiny_param[None](_params, x, i) 64 | | let x: U16 => 65 | @mypony_ushort_param[None](_params, x, i) 66 | | let x: U32 => 67 | @mypony_ulong_param[None](_params, x, i) 68 | | let x: (U64 | ULong | USize) => 69 | @mypony_ulonglong_param[None](_params, x.u64(), i) 70 | | let x: F32 => 71 | @mypony_float_param[None](_params, x, i) 72 | | let x: F64 => 73 | @mypony_double_param[None](_params, x, i) 74 | | let x: String => 75 | @mypony_string_param[None](_params, x.cstring(), x.size(), i) 76 | | let d: PosixDate => 77 | @mypony_time_param[None]( 78 | _params, d.year.u32(), d.month.u32(), d.day_of_month.u32(), 79 | d.hour.u32(), d.min.u32(), d.sec.u32(), i 80 | ) 81 | | let b: Array[U8] => 82 | @mypony_blob_param[None](_params, b.cpointer(), b.size(), i) 83 | end 84 | end 85 | if @mypony_stmt_bind_param[I8](_stmt, _params) != 0 then 86 | _execute_error()? 87 | end 88 | 89 | fun _execute_error() ? => 90 | _notify.fail(Error("execute", error_message(), errno())) 91 | error 92 | 93 | fun dispose() => 94 | @mysql_stmt_free_result[None](_stmt) 95 | @mysql_stmt_close[None](_stmt) 96 | -------------------------------------------------------------------------------- /mysql.pony: -------------------------------------------------------------------------------- 1 | use "time" 2 | use "debug" 3 | 4 | use "lib:mysqlclient" 5 | use "lib:mypony" 6 | 7 | primitive _MySQL 8 | primitive _Res 9 | primitive _Field 10 | primitive _Stmt 11 | primitive _Prep 12 | primitive _Bind 13 | 14 | //type _Time is (U32, U32, U32, U32, U32, U32, U32, U8, I32) 15 | struct _Time 16 | var year: U32 = 0 17 | var month: U32 = 0 18 | var day: U32 = 0 19 | var hour: U32 = 0 20 | var minute: U32 = 0 21 | var second: U32 = 0 22 | var second_part: ULong = 0 23 | var neg: U8 = 0 24 | var time_type: I32 = 0 25 | 26 | primitive _Return 27 | fun ok(): ISize => 0 28 | fun no_data(): ISize => 100 29 | fun data_truncated(): ISize => 101 30 | 31 | type QueryParam is 32 | ( None 33 | | I8 | I16 | I32 | I64 | ILong | ISize 34 | | U8 | U16 | U32 | U64 | ULong | USize 35 | | F32 | F64 36 | | String 37 | | PosixDate 38 | | Array[U8] 39 | ) 40 | 41 | type SignedQueryResult is (I8 | I16 | I32 | I64 | ILong | ISize) 42 | type UnsignedQueryResult is (U8 | U16 | U32 | U64 | ULong | USize) 43 | type FloatQueryResult is (F32 | F64) 44 | type Blob is Array[U8] val 45 | type QueryResult is 46 | ( SignedQueryResult 47 | | UnsignedQueryResult 48 | | FloatQueryResult 49 | | String 50 | | Blob 51 | | PosixDate 52 | | None 53 | ) 54 | 55 | // Do this here as mysql_server_init() is not thread-safe. This avoids 56 | // the need for a global mutex around mysql_init(). 57 | primitive _Init 58 | fun _init() => 59 | let c: I32 = 0 60 | @mysql_server_init[None](c, Pointer[Pointer[U8]], Pointer[Pointer[U8]]) 61 | 62 | class MySQL 63 | var _mysql: Pointer[_MySQL] 64 | let _notify: Notify 65 | 66 | new create(notify: Notify) => 67 | _mysql = Pointer[_MySQL] 68 | _notify = notify 69 | 70 | fun ref connect(host: (String | None) = None, 71 | user: (String | None) = None, 72 | pass: (String | None) = None, 73 | db: (String | None) = None, 74 | port: (U16 | None) = 0, 75 | sock: (String | None) = None): MySQL ? => 76 | let host' = match host | let s: String => s.cstring() else Pointer[U8] end 77 | let user' = match user | let s: String => s.cstring() else Pointer[U8] end 78 | let pass' = match pass | let s: String => s.cstring() else Pointer[U8] end 79 | let db' = match db | let s: String => s.cstring() else Pointer[U8] end 80 | let port' = match port | let p: U16 => p else 0 end 81 | let sock' = match sock | let s: String => s.cstring() else Pointer[U8] end 82 | let flags: USize = 0 83 | _mysql_init()? 84 | let r = @mysql_real_connect[Pointer[_MySQL]]( 85 | _mysql, host', user', pass', db', port', sock', flags 86 | ) 87 | if r.is_null() then 88 | _notify.fail(Error("connect", error_message(), errno())) 89 | error 90 | end 91 | this 92 | 93 | fun ref tcp(host: String, 94 | user: (String | None) = None, 95 | pass: (String | None) = None, 96 | db: (String | None) = None, 97 | port: (U16 | None) = None): MySQL ? => 98 | connect(host, user, pass, db, port, None)? 99 | 100 | fun ref unix(sock: String, 101 | user: (String | None) = None, 102 | pass: (String | None) = None, 103 | db: (String | None) = None): MySQL ? => 104 | connect(None, user, pass, db, None, sock)? 105 | 106 | fun ping(): Bool => 107 | @mysql_ping[I32](_mysql) == 0 108 | 109 | fun ref prepare(query: String): Stmt ? => 110 | let stmt = @mypony_stmt_init[Pointer[_Stmt]](_mysql) 111 | if stmt.is_null() then 112 | _notify.fail(Error("prepare", error_message(), errno())) 113 | error 114 | end 115 | if @mysql_stmt_prepare[I32](stmt, query.cstring(), query.size()) != 0 then 116 | _notify.fail(Error("prepare", error_message(), errno())) 117 | error 118 | end 119 | Stmt._create(stmt, _notify) 120 | 121 | fun ref update(opt: ClientOption, value: Stringable) => 122 | var s = value.string() 123 | @mysql_options[None](_mysql, opt(), addressof s) 124 | 125 | fun errno(): U32 => 126 | @mysql_errno[U32](_mysql) 127 | 128 | fun error_message(): String => 129 | Util.copy_cpointer(@mysql_error[Pointer[U8] iso^](_mysql)) 130 | 131 | fun ref _mysql_init() ? => 132 | _mysql = @mysql_init[Pointer[_MySQL]](_mysql) 133 | if _mysql.is_null() then 134 | _notify.fail(Error("init", error_message(), errno())) 135 | error 136 | end 137 | 138 | fun dispose() => 139 | @mysql_close[None](_mysql) 140 | 141 | fun library_end() => 142 | @mysql_server_end[None]() 143 | -------------------------------------------------------------------------------- /result.pony: -------------------------------------------------------------------------------- 1 | use "collections" 2 | use "time" 3 | use "debug" 4 | 5 | primitive _T 6 | fun decimal(): USize => 0 7 | fun tiny(): USize => 1 8 | fun short(): USize => 2 9 | fun long(): USize => 3 10 | fun float(): USize => 4 11 | fun double(): USize => 5 12 | fun null(): USize => 6 13 | fun timestamp(): USize => 7 14 | fun long_long(): USize => 8 15 | fun int24(): USize => 9 16 | fun date(): USize => 10 17 | fun time(): USize => 11 18 | fun datetime(): USize => 12 19 | fun year(): USize => 13 20 | fun new_date(): USize => 14 21 | fun var_char(): USize => 15 22 | fun bit(): USize => 16 23 | fun new_decimal(): USize => 246 24 | fun tiny_blob(): USize => 249 25 | fun medium_blob(): USize => 250 26 | fun long_blob(): USize => 251 27 | fun blob(): USize => 252 28 | fun var_string(): USize => 253 29 | fun string(): USize => 254 30 | 31 | class Result 32 | let _stmt: Pointer[_Stmt] 33 | let _notify: Notify 34 | let _params: Pointer[_Bind] 35 | let _result: Pointer[_Bind] 36 | let _res: Pointer[_Res] 37 | 38 | new _create(stmt: Pointer[_Stmt], notify: Notify, 39 | params: Pointer[_Bind], result: Pointer[_Bind], 40 | res: Pointer[_Res]) => 41 | _stmt = stmt 42 | _notify = notify 43 | _params = params 44 | _result = result 45 | _res = res 46 | 47 | fun affected_rows(): U64 => 48 | @mysql_stmt_affected_rows[U64](_stmt) 49 | 50 | fun num_rows(): U64 => 51 | @mysql_stmt_num_rows[U64](_stmt) 52 | 53 | fun num_fields(): U32 => 54 | @mysql_num_fields[U32](_res) 55 | 56 | fun ref map_rows(): ResultMapIter => 57 | ResultMapIter._create(this, _notify) 58 | 59 | fun ref fetch_map(): (Map[String, QueryResult] | None) ? => 60 | let that = this 61 | let f = {(n: USize)(that): Map[String, QueryResult] ? => 62 | that._map_row(n)? 63 | } 64 | _fetch_with[Map[String, QueryResult]](f)? 65 | 66 | fun ref array_rows(): ResultArrayIter => 67 | ResultArrayIter._create(this, _notify) 68 | 69 | fun ref fetch_array(): (Array[QueryResult] | None) ? => 70 | let that = this 71 | let f = {(n: USize)(that): Array[QueryResult] ? => 72 | that._array_row(n)? 73 | } 74 | _fetch_with[Array[QueryResult]](f)? 75 | 76 | fun _fetch_with[T](f: {(USize): T ?}): (T | None) ? => 77 | match @mysql_stmt_fetch[ISize](_stmt) 78 | | _Return.no_data() => None 79 | | _Return.ok() => f(@mypony_bind_count[USize](_result))? 80 | | _Return.data_truncated() => 81 | _notify.fail(Error("fetch", "data truncated")) 82 | error 83 | else 84 | _notify.fail(Error("fetch", error_message(), errno())) 85 | error 86 | end 87 | 88 | fun _map_row(n: USize): Map[String, QueryResult] ? => 89 | let row = Map[String, QueryResult] 90 | for i in Range(0, n) do 91 | let field = @mysql_fetch_field_direct[Pointer[_Field]](_res, i) 92 | let name = Util.copy_cpointer(@mypony_field_name[Pointer[U8] iso^](field)) 93 | if @mypony_bind_is_null[Bool](_result, i) then 94 | row(name) = None 95 | else 96 | row(name) = _convert_result(i)? 97 | end 98 | end 99 | row 100 | 101 | fun _array_row(n: USize): Array[QueryResult] ? => 102 | let row = Array[QueryResult](n) 103 | for i in Range(0, n) do 104 | if @mypony_bind_is_null[Bool](_result, i) then 105 | row.push(None) 106 | else 107 | row.push(_convert_result(i)?) 108 | end 109 | end 110 | row 111 | 112 | fun _convert_result(i: USize): QueryResult ? => 113 | let t = @mypony_bind_buffer_type[USize](_result, i) 114 | if @mypony_bind_is_unsigned[Bool](_result, i) then 115 | _convert_unsigned(t, i)? 116 | else 117 | _convert(t, i)? 118 | end 119 | 120 | fun _convert_unsigned(t: USize, i: USize): (UnsignedQueryResult | PosixDate) ? => 121 | match t 122 | | _T.tiny() | _T.year() => @mypony_u8_result[U8](_result, i) 123 | | _T.short() => @mypony_u16_result[U16](_result, i) 124 | | _T.int24() | _T.long() => @mypony_u32_result[U32](_result, i) 125 | | _T.long() => @mypony_u32_result[U32](_result, i) 126 | | _T.long_long() => @mypony_u64_result[U64](_result, i) 127 | | _T.timestamp() => 128 | let time = _Time 129 | @mypony_time_result[None](_result, i, time) 130 | _date(consume time) 131 | else 132 | _notify.fail(Error("fetch", "invalid unsigned type: " + t.string())) 133 | error 134 | end 135 | 136 | fun _convert(t: USize, i: USize): 137 | (SignedQueryResult | FloatQueryResult | String | Blob | PosixDate) ? => 138 | match t 139 | | _T.tiny() | _T.year() => 140 | @mypony_i8_result[I8](_result, i) 141 | | _T.short() => 142 | @mypony_i16_result[I16](_result, i) 143 | | _T.int24() | _T.long() => 144 | @mypony_i32_result[I32](_result, i) 145 | | _T.long() => 146 | @mypony_i32_result[I32](_result, i) 147 | | _T.long_long() => 148 | @mypony_i64_result[I64](_result, i) 149 | | _T.float() => 150 | @mypony_f32_result[F32](_result, i) 151 | | _T.double() => 152 | @mypony_f64_result[F64](_result, i) 153 | | _T.date() | _T.time() | _T.datetime() | _T.timestamp() => 154 | let time = _Time 155 | @mypony_time_result[None](_result, i, time) 156 | _date(consume time) 157 | | _T.decimal() | _T.new_decimal() | _T.string() | _T.var_string() 158 | | _T.bit() => 159 | var len: USize = 0 160 | let cs = 161 | @mypony_string_result[Pointer[U8] iso^](_result, addressof len, i) 162 | Util.copy_cpointer(consume cs, len) 163 | | _T.tiny_blob() | _T.medium_blob() | _T.long_blob() | _T.blob() => 164 | var len: USize = 0 165 | let cs = 166 | @mypony_string_result[Pointer[U8] iso^](_result, addressof len, i) 167 | Util.copy_cpointer(consume cs, len).array() 168 | else 169 | _notify.fail(Error("fetch", "unknown type " + t.string())) 170 | error 171 | end 172 | 173 | fun _date(tm: _Time): PosixDate => 174 | let date = PosixDate() 175 | date.year = tm.year.i32() 176 | date.month = tm.month.i32() 177 | date.day_of_month = tm.day.i32() 178 | date.hour = tm.hour.i32() 179 | date.min = tm.minute.i32() 180 | date.sec = tm.second.i32() 181 | date 182 | 183 | fun errno(): U32 => 184 | @mysql_stmt_errno[U32](_stmt) 185 | 186 | fun error_message(): String => 187 | Util.copy_cpointer(@mysql_stmt_error[Pointer[U8] iso^](_stmt)) 188 | 189 | class ResultMapIter 190 | let _result: Result 191 | let _notify: Notify 192 | var _cur: U64 193 | let _max: U64 194 | 195 | new _create(result: Result, notify: Notify) => 196 | _result = result 197 | _notify = notify 198 | _cur = 0 199 | _max = result.num_rows() 200 | 201 | fun has_next(): Bool => 202 | _cur < _max 203 | 204 | fun ref next(): Map[String, QueryResult] ? => 205 | _cur = _cur + 1 206 | let row = _result.fetch_map()? 207 | match row 208 | | let m: Map[String, QueryResult] => m 209 | else 210 | _notify.fail(Error("next", "fetch_map returned None in iterator")) 211 | error 212 | end 213 | 214 | class ResultArrayIter 215 | let _result: Result 216 | let _notify: Notify 217 | var _cur: U64 218 | let _max: U64 219 | 220 | new _create(result: Result, notify: Notify) => 221 | _result = result 222 | _notify = notify 223 | _cur = 0 224 | _max = result.num_rows() 225 | 226 | fun has_next(): Bool => 227 | _cur < _max 228 | 229 | fun ref next(): Array[QueryResult] ? => 230 | _cur = _cur + 1 231 | let row = _result.fetch_array()? 232 | match row 233 | | let r: Array[QueryResult] => r 234 | else 235 | _notify.fail(Error("next", "fetch_array returned None in iterator")) 236 | error 237 | end 238 | -------------------------------------------------------------------------------- /mypony.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | struct mypony_bind { 8 | unsigned long n; 9 | MYSQL_BIND *bind; 10 | unsigned long *length; 11 | my_bool *is_null; 12 | my_bool is_unsigned; 13 | }; 14 | 15 | struct mypony_bind * 16 | mypony_alloc_bind(unsigned long count) 17 | { 18 | pony_ctx_t *ctx = pony_ctx(); 19 | struct mypony_bind *mbp = pony_alloc(ctx, sizeof *mbp); 20 | 21 | if (mbp == NULL) 22 | return NULL; 23 | 24 | mbp->n = count; 25 | mbp->bind = pony_alloc(ctx, count * sizeof(MYSQL_BIND)); 26 | mbp->length = pony_alloc(ctx, count * sizeof(unsigned long)); 27 | mbp->is_null = pony_alloc(ctx, count * sizeof(my_bool)); 28 | 29 | return mbp; 30 | } 31 | 32 | unsigned long 33 | mypony_bind_count(struct mypony_bind *mbp) 34 | { 35 | return mbp->n; 36 | } 37 | 38 | unsigned long 39 | mypony_bind_is_null(struct mypony_bind *mbp, unsigned long i) 40 | { 41 | return mbp->is_null[i]; 42 | } 43 | 44 | my_bool 45 | mypony_bind_is_unsigned(struct mypony_bind *mbp, unsigned long i) 46 | { 47 | return mbp->bind[i].is_unsigned; 48 | } 49 | 50 | unsigned long 51 | mypony_bind_buffer_type(struct mypony_bind *mbp, unsigned long i) 52 | { 53 | return mbp->bind[i].buffer_type; 54 | } 55 | 56 | MYSQL_STMT * 57 | mypony_stmt_init(MYSQL *mysql) 58 | { 59 | my_bool t = 1; 60 | MYSQL_STMT *stmt = mysql_stmt_init(mysql); 61 | mysql_stmt_attr_set(stmt, STMT_ATTR_UPDATE_MAX_LENGTH, &t); 62 | return stmt; 63 | } 64 | 65 | my_bool 66 | mypony_stmt_bind_param(MYSQL_STMT *stmt, struct mypony_bind *mbp) 67 | { 68 | return mysql_stmt_bind_param(stmt, mbp->bind); 69 | } 70 | 71 | struct mypony_bind * 72 | mypony_alloc_result(MYSQL_STMT *stmt, MYSQL_RES *res) 73 | { 74 | unsigned long i, n; 75 | struct mypony_bind *result; 76 | 77 | if (res == NULL) 78 | return NULL; 79 | 80 | n = mysql_num_fields(res); 81 | result = mypony_alloc_bind(n); 82 | for (i = 0; i < n; i++) { 83 | MYSQL_BIND *bind = &result->bind[i]; 84 | MYSQL_FIELD *field = mysql_fetch_field_direct(res, i); 85 | bind->buffer_type = field->type; 86 | bind->length = &result->length[i]; 87 | bind->is_null = &result->is_null[i]; 88 | bind->is_unsigned = ((field->flags & UNSIGNED_FLAG) != 0); 89 | } 90 | return result; 91 | } 92 | 93 | int 94 | mypony_bind_result(MYSQL_STMT *stmt, MYSQL_RES *res, struct mypony_bind *result) 95 | { 96 | unsigned long i; 97 | pony_ctx_t *ctx; 98 | 99 | if (res == NULL) 100 | return 0; 101 | if (mysql_stmt_store_result(stmt)) 102 | return -1; 103 | 104 | ctx = pony_ctx(); 105 | for (i = 0; i < result->n; i++) { 106 | MYSQL_BIND *bind = &result->bind[i]; 107 | MYSQL_FIELD *field = mysql_fetch_field_direct(res, i); 108 | /* 109 | * sizes from 110 | * https://dev.mysql.com/doc/refman/5.6/en/mysql-stmt-fetch.html 111 | */ 112 | switch (bind->buffer_type) { 113 | case MYSQL_TYPE_NULL: 114 | break; 115 | case MYSQL_TYPE_TINY: 116 | case MYSQL_TYPE_YEAR: 117 | bind->buffer_length = 1; 118 | bind->buffer = pony_alloc(ctx, bind->buffer_length); 119 | break; 120 | case MYSQL_TYPE_SHORT: 121 | bind->buffer_length = 2; 122 | bind->buffer = pony_alloc(ctx, bind->buffer_length); 123 | case MYSQL_TYPE_INT24: 124 | case MYSQL_TYPE_LONG: 125 | case MYSQL_TYPE_FLOAT: 126 | bind->buffer_length = 4; 127 | bind->buffer = pony_alloc(ctx, bind->buffer_length); 128 | break; 129 | case MYSQL_TYPE_LONGLONG: 130 | case MYSQL_TYPE_DOUBLE: 131 | bind->buffer_length = 8; 132 | bind->buffer = pony_alloc(ctx, bind->buffer_length); 133 | break; 134 | case MYSQL_TYPE_DECIMAL: 135 | case MYSQL_TYPE_STRING: 136 | case MYSQL_TYPE_VAR_STRING: 137 | case MYSQL_TYPE_TINY_BLOB: 138 | case MYSQL_TYPE_BLOB: 139 | case MYSQL_TYPE_MEDIUM_BLOB: 140 | case MYSQL_TYPE_LONG_BLOB: 141 | case MYSQL_TYPE_NEWDECIMAL: 142 | case MYSQL_TYPE_BIT: 143 | bind->buffer_length = field->max_length * sizeof(char); 144 | bind->buffer = pony_alloc(ctx, bind->buffer_length); 145 | break; 146 | case MYSQL_TYPE_TIME: 147 | case MYSQL_TYPE_DATE: 148 | case MYSQL_TYPE_DATETIME: 149 | case MYSQL_TYPE_TIMESTAMP: 150 | bind->buffer_length = sizeof(MYSQL_TIME); 151 | bind->buffer = pony_alloc(ctx, bind->buffer_length); 152 | break; 153 | default: 154 | return -1; 155 | } 156 | } 157 | return mysql_stmt_bind_result(stmt, result->bind); 158 | } 159 | 160 | static void 161 | mypony_bind_param(struct mypony_bind *params, enum enum_field_types type, 162 | void *data, unsigned long len, my_bool is_unsigned, 163 | unsigned long i) 164 | { 165 | MYSQL_BIND *bind = ¶ms->bind[i]; 166 | 167 | params->length[i] = len; 168 | bind->length = ¶ms->length[i]; 169 | bind->is_unsigned = is_unsigned; 170 | bind->buffer_type = type; 171 | bind->buffer_length = len; 172 | bind->buffer = malloc(len); 173 | memcpy(bind->buffer, data, len); 174 | } 175 | 176 | void 177 | mypony_null_param(struct mypony_bind *params, unsigned long i) 178 | { 179 | mypony_bind_param(params, MYSQL_TYPE_NULL, NULL, 0, 0, i); 180 | } 181 | 182 | void 183 | mypony_time_param(struct mypony_bind *params, 184 | unsigned int year, unsigned int month, unsigned int day, 185 | unsigned int hour, unsigned int minute, unsigned int second, 186 | unsigned long i) 187 | { 188 | MYSQL_TIME t; 189 | size_t siz = sizeof(t); 190 | 191 | memset(&t, 0, siz); 192 | 193 | t.year = year; 194 | t.month = month; 195 | t.day = day; 196 | t.hour = hour; 197 | t.minute = minute; 198 | t.second = second; 199 | 200 | mypony_bind_param(params, MYSQL_TYPE_DATETIME, &t, siz, 0, i); 201 | } 202 | 203 | #define STRING_PARAM(name, c) \ 204 | void \ 205 | mypony_##name##_param(struct mypony_bind *params, char *s, \ 206 | unsigned long len, unsigned long i) \ 207 | { \ 208 | mypony_bind_param(params, c, s, len, 0, i); \ 209 | } 210 | 211 | #define SIGNED_PARAM(name, type, c) \ 212 | void \ 213 | mypony_##name##_param(struct mypony_bind *params, type p, unsigned long i) \ 214 | { \ 215 | mypony_bind_param(params, c, &p, sizeof(p), 0, i); \ 216 | } 217 | 218 | #define UNSIGNED_PARAM(name, type, c) \ 219 | void \ 220 | mypony_u##name##_param(struct mypony_bind *params, type p, unsigned long i) \ 221 | { \ 222 | mypony_bind_param(params, c, &p, sizeof(p), 1, i); \ 223 | } 224 | 225 | STRING_PARAM(blob, MYSQL_TYPE_BLOB) 226 | STRING_PARAM(string, MYSQL_TYPE_STRING) 227 | 228 | SIGNED_PARAM(tiny, int8_t, MYSQL_TYPE_TINY) 229 | SIGNED_PARAM(short, int16_t, MYSQL_TYPE_SHORT) 230 | SIGNED_PARAM(long, int32_t, MYSQL_TYPE_LONG) 231 | SIGNED_PARAM(longlong, int64_t, MYSQL_TYPE_LONGLONG) 232 | SIGNED_PARAM(float, float, MYSQL_TYPE_FLOAT) 233 | SIGNED_PARAM(double, double, MYSQL_TYPE_DOUBLE) 234 | 235 | UNSIGNED_PARAM(tiny, uint8_t, MYSQL_TYPE_TINY) 236 | UNSIGNED_PARAM(short, uint16_t, MYSQL_TYPE_SHORT) 237 | UNSIGNED_PARAM(long, uint32_t, MYSQL_TYPE_LONG) 238 | UNSIGNED_PARAM(longlong, uint64_t, MYSQL_TYPE_LONGLONG) 239 | 240 | #define RESULT(name, type) \ 241 | type \ 242 | mypony_##name##_result(struct mypony_bind *result, unsigned long i) \ 243 | { \ 244 | return *(type *)result->bind[i].buffer; \ 245 | } 246 | 247 | RESULT(u8, uint8_t) 248 | RESULT(u16, uint16_t) 249 | RESULT(u32, uint32_t) 250 | RESULT(u64, uint64_t) 251 | 252 | RESULT(i8, int8_t) 253 | RESULT(i16, int16_t) 254 | RESULT(i32, int32_t) 255 | RESULT(i64, int64_t) 256 | RESULT(f32, float) 257 | RESULT(f64, double) 258 | 259 | char * 260 | mypony_string_result(struct mypony_bind *result, unsigned long *len, 261 | unsigned long i) 262 | { 263 | *len = result->length[i]; 264 | return result->bind[i].buffer; 265 | } 266 | 267 | void 268 | mypony_time_result(struct mypony_bind *result, unsigned long i, MYSQL_TIME *t) 269 | { 270 | memcpy(t, result->bind[i].buffer, sizeof(MYSQL_TIME)); 271 | } 272 | 273 | const char * 274 | mypony_field_name(MYSQL_FIELD *field) 275 | { 276 | return field->name; 277 | } 278 | 279 | int 280 | mypony_field_type(MYSQL_FIELD *field) 281 | { 282 | return field->type; 283 | } 284 | 285 | const char * 286 | mypony_string_at(char **a, unsigned int i) 287 | { 288 | return a[i]; 289 | } 290 | -------------------------------------------------------------------------------- /test.pony: -------------------------------------------------------------------------------- 1 | use "ponytest" 2 | use "collections" 3 | use "time" 4 | 5 | class TestNotify is Notify 6 | let _env: Env 7 | 8 | new create(env: Env) => 9 | _env = env 10 | 11 | fun failed(err: Error) => 12 | _env.out.print(err.method() + " failed: " + err.string()) 13 | 14 | class DB 15 | let name: String = "mypony" 16 | let _host: String = "localhost" 17 | let _user: String = "root" 18 | let _pass: String = "" 19 | 20 | fun connect(env: Env, db: (String | None)): MySQL ? => 21 | let m = _parse_env(env) 22 | let host = try m("host")? else _host end 23 | let user = try m("user")? else _user end 24 | let pass = try m("pass")? else _pass end 25 | MySQL(TestNotify(env)).tcp(host, user, pass, db)? 26 | 27 | fun run(env: Env, query: String, 28 | params: Array[QueryParam] = Array[QueryParam](), 29 | f: ({(Result)} | None) = None) ? => 30 | _run(None, env, query, params, f)? 31 | 32 | fun apply(env: Env, query: String, 33 | params: Array[QueryParam] = Array[QueryParam](), 34 | f: ({(Result) ?} | None) = None) ? => 35 | _run(name, env, query, params, f)? 36 | 37 | fun _run(db: (String | None), env: Env, query: String, 38 | params: Array[QueryParam] = Array[QueryParam](), 39 | f: ({(Result) ?} | None) = None) ? => 40 | with mysql = DB.connect(env, db)? do 41 | with stmt = mysql.prepare(query)? do 42 | let res = stmt.execute(params)? 43 | match f 44 | | let f': {(Result)} => f'(res) 45 | end 46 | end 47 | end 48 | 49 | fun _parse_env(env: Env): Map[String, String] => 50 | let m = Map[String, String] 51 | for v in env.vars.values() do 52 | let a = v.split("=", 2) 53 | try 54 | match a(0)? 55 | | "MYPONY_HOST" => m("host") = a(1)? 56 | | "MYPONY_USER" => m("user") = a(1)? 57 | | "MYPONY_PASS" => m("pass") = a(1)? 58 | | "MYPONY_DB" => m("db") = a(1)? 59 | end 60 | end 61 | end 62 | m 63 | 64 | actor Main is TestList 65 | new create(env: Env) => 66 | try 67 | create_database(env)? 68 | create_table(env)? 69 | truncate_table(env)? 70 | end 71 | PonyTest(env, this) 72 | 73 | new make() => None 74 | 75 | fun tag create_database(env: Env) ? => 76 | let query = "CREATE DATABASE IF NOT EXISTS " + DB.name 77 | DB.run(env, query)? 78 | 79 | fun tag create_table(env: Env) ? => 80 | let query = "CREATE TABLE IF NOT EXISTS t ( 81 | my_tiny TINYINT, 82 | my_small SMALLINT, 83 | my_int INTEGER, 84 | my_bigint BIGINT, 85 | 86 | my_utiny TINYINT UNSIGNED, 87 | my_usmall SMALLINT UNSIGNED, 88 | my_uint INTEGER UNSIGNED, 89 | my_ubigint BIGINT UNSIGNED, 90 | 91 | my_string VARCHAR(255), 92 | my_blob BLOB, 93 | 94 | my_date TIMESTAMP 95 | )" 96 | DB(env, query)? 97 | 98 | fun tag truncate_table(env: Env) ? => 99 | let query = "TRUNCATE TABLE t" 100 | DB(env, query)? 101 | 102 | fun tag tests(test: PonyTest) => 103 | test(_TestPing) 104 | test(_TestInsert) 105 | test(_TestOption) 106 | test(_TestNumResults) 107 | test(_TestFetchArray) 108 | test(_TestFetchMap) 109 | 110 | class iso _TestPing is UnitTest 111 | fun name(): String => "mysql/ping" 112 | 113 | fun apply(h: TestHelper) ? => 114 | let mysql = DB.connect(h.env, "mypony")? 115 | h.assert_true(mysql.ping()) 116 | 117 | class iso _TestInsert is UnitTest 118 | fun name(): String => "mysql/create_table" 119 | fun exclusion_group(): String => "mysql/with_inserted_data" 120 | 121 | fun ref apply(h: TestHelper) ? => 122 | let query = "INSERT INTO t ( 123 | my_tiny, 124 | my_small, 125 | my_int, 126 | my_bigint, 127 | 128 | my_utiny, 129 | my_usmall, 130 | my_uint, 131 | my_ubigint, 132 | 133 | my_string, 134 | my_blob, 135 | 136 | my_date 137 | ) VALUES ( 138 | ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? 139 | )" 140 | let my_tiny: QueryParam = I8(1) 141 | let my_small: QueryParam = I16(2) 142 | let my_int: QueryParam = I32(3) 143 | let my_bigint: QueryParam = I64(4) 144 | let my_utiny: QueryParam = U8(5) 145 | let my_usmall: QueryParam = U16(6) 146 | let my_uint: QueryParam = U32(7) 147 | let my_ubigint: QueryParam = U64(8) 148 | let my_string: QueryParam = "pony" 149 | let my_blob: QueryParam = [U8(80); U8(79); U8(78); U8(89)] 150 | let my_date: QueryParam = PosixDate(Time.now()._1) 151 | let params: Array[QueryParam] = [ 152 | my_tiny 153 | my_small 154 | my_int 155 | my_bigint 156 | 157 | my_utiny 158 | my_usmall 159 | my_uint 160 | my_ubigint 161 | 162 | my_string 163 | my_blob 164 | my_date 165 | ] 166 | DB(h.env, query, params, {(res: Result)(h) => 167 | h.assert_eq[U64](1, res.affected_rows()) 168 | })? 169 | 170 | class iso _TestOption is UnitTest 171 | fun name(): String => "mysql/option" 172 | 173 | fun ref apply(h: TestHelper) ? => 174 | let mysql = DB.connect(h.env, "mypony")? 175 | mysql(ConnectTimeout) = I32(42) 176 | mysql(CharsetName) = "utf8" 177 | let query = " 178 | SELECT 179 | VARIABLE_VALUE 180 | FROM 181 | INFORMATION_SCHEMA.SESSION_VARIABLES 182 | WHERE 183 | VARIABLE_NAME = ? 184 | OR VARIABLE_NAME = ?" 185 | let tmout: QueryParam = "connect_timeout" 186 | let charset: QueryParam = "character_set_client" 187 | DB(h.env, query, [tmout; charset], {(res: Result)(h) ? => 188 | h.assert_eq[U64](2, res.num_rows()) 189 | match res.fetch_array()? 190 | | let r: Array[QueryResult] => h.assert_eq[String]("42", r(0)? as String) 191 | else 192 | h.assert_true(false, "fetch_array returned None on first row") 193 | end 194 | match res.fetch_array()? 195 | | let r: Array[QueryResult] => h.assert_eq[String]("utf8", r(0)? as String) 196 | else 197 | h.assert_true(false, "fetch_array returned None on second row") 198 | end 199 | })? 200 | 201 | class iso _TestNumResults is UnitTest 202 | fun name(): String => "mysql/num_results" 203 | fun exclusion_group(): String => "mysql/with_inserted_data" 204 | 205 | fun ref apply(h: TestHelper) ? => 206 | let query = "SELECT * FROM t" 207 | DB(h.env, query, Array[QueryParam](), {(res: Result)(h) => 208 | h.assert_eq[U32](11, res.num_fields()) 209 | h.assert_eq[U64](1, res.num_rows()) 210 | })? 211 | 212 | class iso _TestFetchArray is UnitTest 213 | fun name(): String => "mysql/fetch_array" 214 | fun exclusion_group(): String => "mysql/with_inserted_data" 215 | 216 | fun ref apply(h: TestHelper) ? => 217 | let query = "SELECT * FROM t" 218 | DB(h.env, query, Array[QueryParam](), {(res: Result)(h) => 219 | try 220 | let err = {(i: USize)(h) => 221 | h.assert_true(false, "cast error in column " + i.string()) 222 | } 223 | match res.fetch_array()? 224 | | let r: Array[QueryResult] => 225 | try h.assert_eq[I8](1, r(0)? as I8) else err(0) end 226 | try h.assert_eq[I16](2, r(1)? as I16) else err(1) end 227 | try h.assert_eq[I32](3, r(2)? as I32) else err(2) end 228 | try h.assert_eq[I64](4, r(3)? as I64) else err(3) end 229 | try h.assert_eq[U8](5, r(4)? as U8) else err(4) end 230 | try h.assert_eq[U16](6, r(5)? as U16) else err(5) end 231 | try h.assert_eq[U32](7, r(6)? as U32) else err(6) end 232 | try h.assert_eq[U64](8, r(7)? as U64) else err(7) end 233 | try h.assert_eq[String]("pony", r(8)? as String) else err(8) end 234 | try 235 | let b = r(9)? as Blob 236 | h.assert_eq[U8](80, b(0)?) 237 | h.assert_eq[U8](79, b(1)?) 238 | h.assert_eq[U8](78, b(2)?) 239 | h.assert_eq[U8](89, b(3)?) 240 | else 241 | err(9) 242 | end 243 | try 244 | let d = r(10)? as PosixDate 245 | let now = PosixDate(Time.now()._1) 246 | h.assert_eq[I32](now.year, d.year) 247 | h.assert_eq[I32](now.month, d.month) 248 | h.assert_eq[I32](now.day_of_month, d.day_of_month) 249 | else 250 | err(10) 251 | end 252 | | None => 253 | h.assert_true(false, "fetch_array returned None on first row") 254 | end 255 | h.assert_is[(Array[QueryResult] | None)](None, res.fetch_array()?) 256 | else 257 | h.assert_true(false, "fetch failure") 258 | end 259 | })? 260 | 261 | class iso _TestFetchMap is UnitTest 262 | fun name(): String => "mysql/fetch_map" 263 | fun exclusion_group(): String => "mysql/with_inserted_data" 264 | 265 | fun ref apply(h: TestHelper) ? => 266 | let query = "SELECT * FROM t" 267 | DB(h.env, query, Array[QueryParam](), {(res: Result)(h) => 268 | try 269 | let err = {(i: USize)(h) => 270 | h.assert_true(false, "cast error in column " + i.string()) 271 | } 272 | match res.fetch_map()? 273 | | let m: Map[String, QueryResult] => 274 | try h.assert_eq[I8](1, m("my_tiny")? as I8) else err(0) end 275 | try h.assert_eq[I16](2, m("my_small")? as I16) else err(1) end 276 | try h.assert_eq[I32](3, m("my_int")? as I32) else err(2) end 277 | try h.assert_eq[I64](4, m("my_bigint")? as I64) else err(3) end 278 | try h.assert_eq[U8](5, m("my_utiny")? as U8) else err(4) end 279 | try h.assert_eq[U16](6, m("my_usmall")? as U16) else err(5) end 280 | try h.assert_eq[U32](7, m("my_uint")? as U32) else err(6) end 281 | try h.assert_eq[U64](8, m("my_ubigint")? as U64) else err(7) end 282 | try 283 | h.assert_eq[String]("pony", m("my_string")? as String) 284 | else 285 | err(8) 286 | end 287 | try 288 | let b = m("my_blob")? as Blob 289 | h.assert_eq[U8](80, b(0)?) 290 | h.assert_eq[U8](79, b(1)?) 291 | h.assert_eq[U8](78, b(2)?) 292 | h.assert_eq[U8](89, b(3)?) 293 | else 294 | err(9) 295 | end 296 | try 297 | let d = m("my_date")? as PosixDate 298 | let now = PosixDate(Time.now()._1) 299 | h.assert_eq[I32](now.year, d.year) 300 | h.assert_eq[I32](now.month, d.month) 301 | h.assert_eq[I32](now.day_of_month, d.day_of_month) 302 | else 303 | err(10) 304 | end 305 | | None => 306 | h.assert_true(false, "fetch_array returned None on first row") 307 | end 308 | h.assert_is[(Array[QueryResult] | None)](None, res.fetch_array()?) 309 | else 310 | h.assert_true(false, "fetch failure") 311 | end 312 | })? 313 | --------------------------------------------------------------------------------