From 5933cef7ecb030e0b2d9aafed560972d3e14ce86 Mon Sep 17 00:00:00 2001 From: Salvador Ortiz Date: Thu, 24 Mar 2016 03:17:55 -0600 Subject: [PATCH] mysql: Add real prepared statements --- lib/DBDish/mysql/Connection.pm6 | 33 +++- lib/DBDish/mysql/Native.pm6 | 121 +++++++++----- lib/DBDish/mysql/StatementHandle.pm6 | 230 +++++++++++++++++++-------- 3 files changed, 271 insertions(+), 113 deletions(-) diff --git a/lib/DBDish/mysql/Connection.pm6 b/lib/DBDish/mysql/Connection.pm6 index c12a9f18..b8b3bd20 100644 --- a/lib/DBDish/mysql/Connection.pm6 +++ b/lib/DBDish/mysql/Connection.pm6 @@ -10,11 +10,38 @@ has MYSQL $!mysql_client is required; submethod BUILD(:$!mysql_client, :$!parent!) { } +method !handle-errors($code) { + if $code { + self!set-err($code, $!mysql_client.mysql_error); + } else { + self.reset-err; + } +} method prepare(Str $statement, *%args) { - self.reset-err; - DBDish::mysql::StatementHandle.new( - :$!mysql_client, :parent(self), :$statement, :$!RaiseError, |%args + with $!mysql_client.mysql_stmt_init -> $stmt { + with self!handle-errors( + $stmt.mysql_stmt_prepare($statement, $statement.encode.bytes) + ) { + unless my $param-count = $stmt.mysql_stmt_param_count { + # Use unprepared path, is faster; + $stmt.mysql_stmt_close; + } + DBDish::mysql::StatementHandle.new( + :$!mysql_client, :parent(self), :$stmt, :$param-count, + :$statement, :$!RaiseError, |%args + ); + } else { .fail } + } else { + self!set-err(-1, "Can't allocate memory"); + } +} + +method execute(Str $statement, *%args) { + my $sth = DBDish::mysql::StatementHandle.new( + :$!mysql_client, :parent(self), :$statement, :$!RaiseError, |%args ); + LEAVE { $sth.finish if $sth }; + $sth.execute; } method mysql_insertid() { diff --git a/lib/DBDish/mysql/Native.pm6 b/lib/DBDish/mysql/Native.pm6 index af823136..8826e3db 100644 --- a/lib/DBDish/mysql/Native.pm6 +++ b/lib/DBDish/mysql/Native.pm6 @@ -9,6 +9,37 @@ sub MyLibName { } constant LIB = &MyLibName; +#From mysql_com.h +enum mysql-field-type is export ( + MYSQL_TYPE_DECIMAL => 0, + MYSQL_TYPE_TINY => 1, + MYSQL_TYPE_SHORT => 2, + MYSQL_TYPE_LONG => 3, + MYSQL_TYPE_FLOAT => 4, + MYSQL_TYPE_DOUBLE => 5, + MYSQL_TYPE_NULL => 6, + MYSQL_TYPE_TIMESTAMP => 7, + MYSQL_TYPE_LONGLONG => 8, + MYSQL_TYPE_INT24 => 9, + MYSQL_TYPE_DATE => 10, + MYSQL_TYPE_TIME => 11, + MYSQL_TYPE_DATETIME => 12, + MYSQL_TYPE_YEAR => 13, + MYSQL_TYPE_NEWDATE => 14, + MYSQL_TYPE_VARCHAR => 15, + MYSQL_TYPE_BIT => 16, + MYSQL_TYPE_NEWDECIMAL => 246, + MYSQL_TYPE_ENUM => 247, + MYSQL_TYPE_SET => 248, + MYSQL_TYPE_TINY_BLOB => 249, + MYSQL_TYPE_MEDIUM_BLOB => 250, + MYSQL_TYPE_LONG_BLOB => 251, + MYSQL_TYPE_BLOB => 252, + MYSQL_TYPE_VAR_STRING => 253, + MYSQL_TYPE_STRING => 254, + MYSQL_TYPE_GEOMETRY => 255 +); + class MYSQL_FIELD is repr('CStruct') is export { has Str $.name; has Str $.org_name; @@ -33,8 +64,36 @@ class MYSQL_FIELD is repr('CStruct') is export { has Pointer $.extension; } +constant my_bool = int8; + class MYSQL_RES is repr('CPointer') { ... } +class MYSQL_BIND is repr('CStruct') is export { + #has Pointer[ulong] $!length; + has uint64 $.length is rw; + #has Pointer[my_bool] $.is_null; + has uint64 $.is_null is rw; + #has Pointer $.buffer is rw; + has uint64 $.buffer is rw; + has Pointer[my_bool] $.error; + has Pointer[uint8] $.row_ptr; + has Pointer $.store_param_func; + has Pointer $.fetch_result; + has Pointer $.skip_result; + has ulong $.buffer_length is rw; + has ulong $.offset; + has size_t $.param_number is rw; + has size_t $.pack_length; + has uint32 $.buffer_type is rw; + has my_bool $.error_value; + has my_bool $.is_unsigned; + has my_bool $.long_data_user; + has my_bool $.is_null_value; + has Pointer $.extension; +} + +#note "MYSQL_BIND size: ", nativesizeof(MYSQL_BIND), nativesizeof(CArray[MYSQL_BIND]); + class MyRow does Positional is export { has CArray[Pointer] $.car; has MYSQL_RES $.rs; @@ -72,11 +131,25 @@ class MyRow does Positional is export { } } -class MYSQL_STMT is export is repr('CPointer') { }; +class MYSQL_STMT is export is repr('CPointer') { + method mysql_stmt_prepare(::?CLASS:D: Str, ulong --> int32) is native(LIB) { * } + method mysql_stmt_param_count(::?CLASS:D: --> ulong) is native(LIB) { * } + method mysql_stmt_bind_param(::?CLASS:D: MYSQL_BIND --> int32) is native(LIB) { * } + method mysql_stmt_execute(::?CLASS:D: --> int32) is native(LIB) { * } + method mysql_stmt_free_result(::?CLASS:D: --> my_bool) is native(LIB) { * } + method mysql_stmt_reset(::?CLASS:D: --> my_bool) is native(LIB) { * } + method mysql_stmt_field_count(::?CLASS:D: --> int32) is native(LIB) { * } + method mysql_stmt_close(::?CLASS:D: --> my_bool) is native(LIB) { * } + method mysql_stmt_affected_rows(::?CLASS:D: --> uint64) is native(LIB) { * } + method mysql_stmt_result_metadata(::?CLASS:D: --> MYSQL_RES) is native(LIB) { * } + method mysql_stmt_store_result(::?CLASS:D: --> int32) is native(LIB) { * } + method mysql_stmt_fetch(::?CLASS:D: --> int32) is native(LIB) { * } + method mysql_stmt_bind_result(::?CLASS:D: + MYSQL_BIND --> my_bool) is native(LIB) { * } +}; class MYSQL_RES is export { - - method fetch_row(--> MyRow) { + method fetch_row(MYSQL_RES:D: --> MyRow) { sub mysql_fetch_row(MYSQL_RES $result_set ) returns CArray[Pointer] is native(LIB) { * } sub mysql_fetch_lengths(MYSQL_RES) @@ -113,8 +186,8 @@ class MYSQL is export is repr('CPointer') { # Native methods method mysql_affected_rows(MYSQL:D: --> int32) is native(LIB) { * } method mysql_close(MYSQL:D: ) is native(LIB) { * } - method mysql_errno( MYSQL:D: --> int32) is native(LIB) { * } - method mysql_error( MYSQL:D: --> Str) is native(LIB) { * } + method mysql_errno(MYSQL:D: --> int32) is native(LIB) { * } + method mysql_error(MYSQL:D: --> Str) is native(LIB) { * } method mysql_field_count( MYSQL:D: --> uint32) is native(LIB) { * } method mysql_init(MYSQL:U: --> MYSQL) is native(LIB) { * } method mysql_insert_id(MYSQL:D: --> uint64) is native(LIB) { * } @@ -128,41 +201,10 @@ class MYSQL is export is repr('CPointer') { method mysql_store_result(MYSQL:D: --> MYSQL_RES) is native(LIB) { * } method mysql_use_result(MYSQL:D: --> MYSQL_RES) is native(LIB) { * } method mysql_warning_count(MYSQL:D: --> uint32) is native(LIB) { * } - method mysql_stmt_init(MYSQL:D: --> MYSQL_STMT) is native(LIB) { * } method mysql_ping(MYSQL:D: --> int32) is native(LIB) { * } + method mysql_stmt_init(MYSQL:D: --> MYSQL_STMT) is native(LIB) { * } } -#From mysql_com.h -enum mysql-field-type is export ( - MYSQL_TYPE_DECIMAL => 0, - MYSQL_TYPE_TINY => 1, - MYSQL_TYPE_SHORT => 2, - MYSQL_TYPE_LONG => 3, - MYSQL_TYPE_FLOAT => 4, - MYSQL_TYPE_DOUBLE => 5, - MYSQL_TYPE_NULL => 6, - MYSQL_TYPE_TIMESTAMP => 7, - MYSQL_TYPE_LONGLONG => 8, - MYSQL_TYPE_INT24 => 9, - MYSQL_TYPE_DATE => 10, - MYSQL_TYPE_TIME => 11, - MYSQL_TYPE_DATETIME => 12, - MYSQL_TYPE_YEAR => 13, - MYSQL_TYPE_NEWDATE => 14, - MYSQL_TYPE_VARCHAR => 15, - MYSQL_TYPE_BIT => 16, - MYSQL_TYPE_NEWDECIMAL => 246, - MYSQL_TYPE_ENUM => 247, - MYSQL_TYPE_SET => 248, - MYSQL_TYPE_TINY_BLOB => 249, - MYSQL_TYPE_MEDIUM_BLOB => 250, - MYSQL_TYPE_LONG_BLOB => 251, - MYSQL_TYPE_BLOB => 252, - MYSQL_TYPE_VAR_STRING => 253, - MYSQL_TYPE_STRING => 254, - MYSQL_TYPE_GEOMETRY => 255 -); - constant %mysql-type-conv is export = map( {+mysql-field-type::{.key} => .value}, ( MYSQL_TYPE_DECIMAL => Rat, @@ -192,9 +234,4 @@ constant %mysql-type-conv is export = map( MYSQL_TYPE_BLOB => Buf, )).hash; -sub mysql_stmt_prepare( OpaquePointer $mysql_stmt, Str, ulong $length ) - returns OpaquePointer - is native(LIB) - is export - { ... } diff --git a/lib/DBDish/mysql/StatementHandle.pm6 b/lib/DBDish/mysql/StatementHandle.pm6 index 4d239182..8ac189ba 100644 --- a/lib/DBDish/mysql/StatementHandle.pm6 +++ b/lib/DBDish/mysql/StatementHandle.pm6 @@ -3,13 +3,22 @@ need DBDish; unit class DBDish::mysql::StatementHandle does DBDish::StatementHandle; use DBDish::mysql::Native; +use NativeHelpers::Blob; +use NativeHelpers::CStruct; has MYSQL $!mysql_client is required; +has MYSQL_STMT $!stmt; +has $!param-count; has $!statement; has MYSQL_RES $!result_set; has $!field_count; has $.mysql_warning_count is rw = 0; has Bool $.Prefetch; +# For prepared stmts +has $!binds; +has @!out-bufs; +has $!retlen; +has $!isnull; method !handle-errors { if $!mysql_client.mysql_errno -> $code { @@ -19,69 +28,126 @@ method !handle-errors { } } -submethod BUILD(:$!mysql_client!, :$!parent!, :$!statement, Bool :$!Prefetch = True) { } - -method execute(*@params is copy) { - my $statement = ''; - my @chunks = $!statement.split('?', @params + 1); - my $last-chunk = @chunks.pop; - for @chunks { - $statement ~= $_; - my $param = @params.shift; - if $param.defined { - if $param ~~ Real { - $statement ~= $param - } - else { - $statement ~= self.quote($param); - } - } - else { - $statement ~= 'NULL'; - } +submethod BUILD(:$!mysql_client!, :$!parent!, :$!stmt = MYSQL_STMT, :$!param-count = 0, + :$!statement, Bool :$!Prefetch = True +) { } + +method !get-meta(MYSQL_RES $res) { + my $lengths = Buf[int64].allocate($!field_count); + loop (my $i = 0; $i < $!field_count; $i++) { + with $res.mysql_fetch_field { + @!column-name.push: .name; + if (my \t = %mysql-type-conv{.type}) === Any { + warn "No type map defined for mysql type #{.type} at column $i"; + t = Str; + } + @!column-type.push: t; + $lengths[$i] = .length; + } + else { die 'mysql: Opps! mysql_fetch_field'; } } - $statement ~= $last-chunk; + $lengths; +} + + +method execute(*@params) { + self!set-err(-1, + "Wrong number of arguments to method execute: got @params.elems(), expected $!param-count" + ) if @params != $!param-count; + self!enter-execute; - if my $status = $!mysql_client.mysql_query($statement) { # 0 means OK - self!set-err($status, $!mysql_client.mysql_error); - } else { - $.mysql_warning_count = $!mysql_client.mysql_warning_count; - my $rows = 0; my $was-select = True; - without $!field_count { # First execution - if $!field_count = $!mysql_client.mysql_field_count { - # Was SELECT, so should be a result set. - with $!result_set = self!get_result { - loop (my $i = 0; $i < $!field_count; $i++) { - with $_.mysql_fetch_field { - @!column-name.push: .name; - if (my \t = %mysql-type-conv{.type}) === Any { - warn "No type map defined for mysql type #{.type} at column $i"; - t = Str; - } - @!column-type.push: t; - } - else { die 'mysql: Opps! mysql_fetch_field'; } - } - $rows = $!mysql_client.mysql_affected_rows; - } - else { - .fail without self!handle-errors; - } - } - } - if $!field_count == 0 { # Not a SELECT - $rows = $!mysql_client.mysql_affected_rows; - $was-select = False; - } - elsif $!Executed { - # Get the new one - $!result_set = self!get_result; - .fail without self!handle-errors; - $rows = $!mysql_client.mysql_affected_rows; - } - $rows++ if $rows == -1; - self!done-execute($rows, $was-select); + my $rows = 0; my $was-select = True; + if $!param-count { + my @Bufs; + my $par = LinearArray[MYSQL_BIND].new($!param-count); + my $lengths = Buf[int64].allocate($!param-count); + my $lb = BPointer($lengths).Int; + LEAVE { $par.dispose if $par } + for @params.kv -> $k, $v { # The binding dance + with $v { + my $buf = do { + when Blob { $_ } + when Str { .encode } + default { .Str.encode } + }; + given $par[$k] { + .buffer_length = $lengths[$k] = $buf.bytes; + .buffer = BPointer(@Bufs[$k] = $buf).Int; + .length = $lb + $k * 8; + .buffer_type = $v ~~ Blob ?? MYSQL_TYPE_BLOB !! MYSQL_TYPE_STRING; + } + } else { # Null; + $par[$k].buffer_type = MYSQL_TYPE_NULL; + } + } + $!stmt.mysql_stmt_bind_param($par.typed-pointer) + or $!stmt.mysql_stmt_execute + or do without $!field_count { + if ($!field_count = $!stmt.mysql_stmt_field_count) + && $!stmt.mysql_stmt_result_metadata -> $res + { # Need to bind outputs, reuse params structs. + $lengths = self!get-meta($res); + $!isnull = Buf[int64].allocate($!field_count); + my $nb = BPointer($!isnull).Int; + my $stmt_buf = LinearArray[MYSQL_BIND].new($!field_count); + $lb = BPointer($lengths).Int; + @Bufs = (); + for ^$!field_count -> $col { + given $stmt_buf[$col] { + .buffer = BPointer( + @Bufs[$col] = Buf.allocate( + .buffer_length = $lengths[$col] + ) + ).Int; + .length = $lb + $col * 8; + .is_null = $nb + $col * 8; + .buffer_type = @!column-type[$col] ~~ Blob + ?? MYSQL_TYPE_BLOB !! MYSQL_TYPE_STRING; + } + } + #dd $stmt_buf.typed-pointer; + @!out-bufs := @Bufs; + $!binds = $stmt_buf; + $!retlen = $lengths; + $!result_set = $res; # To be free at finish time; + $!stmt.mysql_stmt_bind_result($!binds.typed-pointer) + or $!Prefetch + and $!stmt.mysql_stmt_store_result; + } + } + without self!handle-errors { .fail } + $rows = $!stmt.mysql_stmt_affected_rows; + return self!done-execute($rows, $!field_count > 0); + # Try the old path + } elsif my $status = $!mysql_client.mysql_query($!statement) { # 0 means OK + self!set-err($status, $!mysql_client.mysql_error).fail; } + $!stmt = Nil; # Mark unused, was closed at prepare time + $.mysql_warning_count = $!mysql_client.mysql_warning_count; + without $!field_count { # First execution + if $!field_count = $!mysql_client.mysql_field_count { + # Was SELECT, so should be a result set. + with self!get_result { + self!get-meta($!result_set = $_); + $rows = $!mysql_client.mysql_affected_rows; + } + else { + .fail without self!handle-errors; + } + } + } + if $!field_count == 0 { # Not a SELECT + $rows = $!mysql_client.mysql_affected_rows; + $was-select = False; + } + elsif $!Executed { + # Get the new one + $!result_set = self!get_result; + .fail without self!handle-errors; + $rows = $!mysql_client.mysql_affected_rows; + } + $rows++ if $rows == -1; + self!done-execute($rows, $was-select); } method escape(|a) { @@ -103,14 +169,34 @@ method !get_result { method _row { my $list = (); if $!field_count -> $fields { - if my $row = $!result_set.fetch_row { - $list = do for ^$fields { $row.want($_, @!column-type[$_]) } - $!affected_rows++ unless $!Prefetch; - } - else { - .fail without self!handle-errors; - self.finish; - } + my $row; + with $!stmt { + my $res = .mysql_stmt_fetch; + if $res == 0 { # Has data + $list = do for ^$fields { + my $t = @!column-type[$_]; + my $val = $t; + unless $!isnull[$_] { + my $len = $!retlen[$_]; + if $t ~~ Blob { + $val = @!out-bufs[$_].subbuf(0,$len); + } else { + $val = @!out-bufs[$_].subbuf(0,$len).decode; + $val = $t($val) if $t !~~ Str; + } + } + $val; + } + $row = True; + } + } elsif $row = $!result_set.fetch_row { + $list = do for ^$fields { $row.want($_, @!column-type[$_]) } + $!affected_rows++ unless $!Prefetch; + } + unless $row { + .fail without self!handle-errors; + self.finish; + } } $list; } @@ -120,9 +206,17 @@ method mysql_insertid() { } method _free() { + with $!stmt { + $!stmt.mysql_stmt_close; + $_ = Nil; + } } method finish() { + with $!stmt { + .mysql_stmt_free_result; + .mysql_stmt_reset; + } with $!result_set { .mysql_free_result; $_ = Nil;