diff options
Diffstat (limited to 'ext/mysqlnd/mysqlnd_result.c')
| -rw-r--r-- | ext/mysqlnd/mysqlnd_result.c | 1194 | 
1 files changed, 1194 insertions, 0 deletions
diff --git a/ext/mysqlnd/mysqlnd_result.c b/ext/mysqlnd/mysqlnd_result.c new file mode 100644 index 0000000000..2d42d65469 --- /dev/null +++ b/ext/mysqlnd/mysqlnd_result.c @@ -0,0 +1,1194 @@ +/* +  +----------------------------------------------------------------------+ +  | PHP Version 6                                                        | +  +----------------------------------------------------------------------+ +  | Copyright (c) 2006-2007 The PHP Group                                | +  +----------------------------------------------------------------------+ +  | This source file is subject to version 3.01 of the PHP license,      | +  | that is bundled with this package in the file LICENSE, and is        | +  | available through the world-wide-web at the following url:           | +  | http://www.php.net/license/3_01.txt                                  | +  | If you did not receive a copy of the PHP license and are unable to   | +  | obtain it through the world-wide-web, please send a note to          | +  | license@php.net so we can mail you a copy immediately.               | +  +----------------------------------------------------------------------+ +  | Authors: Georg Richter <georg@mysql.com>                             | +  |          Andrey Hristov <andrey@mysql.com>                           | +  |          Ulf Wendel <uwendel@mysql.com>                              | +  +----------------------------------------------------------------------+ +*/ + +/* $Id$ */ +#include "php.h" +#include "mysqlnd.h" +#include "mysqlnd_wireprotocol.h" +#include "mysqlnd_priv.h" +#include "mysqlnd_result.h" +#include "mysqlnd_result_meta.h" +#include "mysqlnd_statistics.h" +#include "mysqlnd_charset.h" +#include "mysqlnd_debug.h" +#include "ext/standard/basic_functions.h" + +#define MYSQLND_SILENT + + +/* {{{ mysqlnd_unbuffered_free_last_data */ +void mysqlnd_unbuffered_free_last_data(MYSQLND_RES *result TSRMLS_DC) +{ +	MYSQLND_RES_UNBUFFERED *unbuf = result->unbuf; + +	DBG_ENTER("mysqlnd_unbuffered_free_last_data"); + +	if (!unbuf) { +		DBG_VOID_RETURN; +	} + +	if (unbuf->last_row_data) { +		unsigned int i, ctor_called_count = 0; +		zend_bool copy_ctor_called; +		MYSQLND_STATS *global_stats = result->conn? &result->conn->stats:NULL; +		for (i = 0; i < result->field_count; i++) { +			mysqlnd_palloc_zval_ptr_dtor(&(unbuf->last_row_data[i]), +										 result->zval_cache, result->type, +										 ©_ctor_called TSRMLS_CC); +			if (copy_ctor_called) { +				ctor_called_count++; +			} +		} +		/* By using value3 macros we hold a mutex only once, there is no value2 */ +		MYSQLND_INC_CONN_STATISTIC_W_VALUE3(global_stats, +											STAT_COPY_ON_WRITE_PERFORMED, +											ctor_called_count, +											STAT_COPY_ON_WRITE_SAVED, +											result->field_count - ctor_called_count, +											STAT_COPY_ON_WRITE_PERFORMED, 0); +		 +		/* Free last row's zvals */ +		efree(unbuf->last_row_data); +		unbuf->last_row_data = NULL; +	} +	if (unbuf->last_row_buffer) { +		/* Nothing points to this buffer now, free it */ +		efree(unbuf->last_row_buffer); +		unbuf->last_row_buffer = NULL; +	} + +	DBG_VOID_RETURN; +} +/* }}} */ + +/* {{{ mysqlnd_free_buffered_data */ +void mysqlnd_free_buffered_data(MYSQLND_RES *result TSRMLS_DC) +{ +	MYSQLND_THD_ZVAL_PCACHE  *zval_cache = result->zval_cache; +	MYSQLND_RES_BUFFERED *set = result->data; +	unsigned int field_count = result->field_count; +	unsigned int row; + +	DBG_ENTER("mysqlnd_free_buffered_data"); +	DBG_INF_FMT("Freeing "MYSQLND_LLU_SPEC" row(s)", result->data->row_count); + +	DBG_INF_FMT("before: real_usage=%lu  usage=%lu", zend_memory_usage(TRUE TSRMLS_CC), zend_memory_usage(FALSE TSRMLS_CC)); +	for (row = 0; row < result->data->row_count; row++) { +		unsigned int col; +		zval **current_row = current_row = set->data[row]; +		zend_uchar *current_buffer = set->row_buffers[row]; + +		for (col = 0; col < field_count; col++) { +			zend_bool copy_ctor_called; +			mysqlnd_palloc_zval_ptr_dtor(&(current_row[col]), zval_cache, +										 result->type, ©_ctor_called TSRMLS_CC); +#if MYSQLND_DEBUG_MEMORY +			DBG_INF_FMT("Copy_ctor_called=%d", copy_ctor_called); +#endif +			MYSQLND_INC_GLOBAL_STATISTIC(copy_ctor_called? STAT_COPY_ON_WRITE_PERFORMED: +														   STAT_COPY_ON_WRITE_SAVED); +		} +#if MYSQLND_DEBUG_MEMORY +		DBG_INF("Freeing current_row & current_buffer"); +#endif +		pefree(current_row, set->persistent); +		pefree(current_buffer, set->persistent); +	} +	DBG_INF("Freeing data & row_buffer"); +	pefree(set->data, set->persistent); +	pefree(set->row_buffers, set->persistent); +	set->data			= NULL; +	set->row_buffers	= NULL; +	set->data_cursor	= NULL; +	set->row_count	= 0; +	if (set->qcache) { +		mysqlnd_qcache_free_cache_reference(&set->qcache); +	} +	DBG_INF("Freeing set"); +	pefree(set, set->persistent); + +	DBG_INF_FMT("after: real_usage=%lu  usage=%lu", zend_memory_usage(TRUE TSRMLS_CC), zend_memory_usage(FALSE TSRMLS_CC)); +	DBG_VOID_RETURN; +} +/* }}} */ + + +/* {{{ mysqlnd_res::free_result_buffers */ +void +MYSQLND_METHOD(mysqlnd_res, free_result_buffers)(MYSQLND_RES *result TSRMLS_DC) +{ +	DBG_ENTER("mysqlnd_res::free_result_buffers"); +	DBG_INF_FMT("%s", result->unbuf? "unbuffered":(result->data? "buffered":"unknown")); + +	if (result->unbuf) { +		mysqlnd_unbuffered_free_last_data(result TSRMLS_CC); +		efree(result->unbuf); +		result->unbuf = NULL; +	} else if (result->data) { +		mysqlnd_free_buffered_data(result TSRMLS_CC); +		result->data = NULL; +	} + +	if (result->lengths) { +		efree(result->lengths); +		result->lengths = NULL; +	} + +	DBG_VOID_RETURN; +} +/* }}} */ + + +/* {{{ mysqlnd_internal_free_result_contents */ +static +void mysqlnd_internal_free_result_contents(MYSQLND_RES *result TSRMLS_DC) +{ +	DBG_ENTER("mysqlnd_internal_free_result_contents"); + +	result->m.free_result_buffers(result TSRMLS_CC); + +	if (result->row_packet) { +		DBG_INF("Freeing packet"); +		PACKET_FREE(result->row_packet); +		result->row_packet = NULL; +	} + +	result->conn = NULL; + +	if (result->meta) { +		result->meta->m->free_metadata(result->meta, FALSE TSRMLS_CC); +		result->meta = NULL; +	} + +	if (result->zval_cache) { +		DBG_INF("Freeing zval cache reference"); +		mysqlnd_palloc_free_thd_cache_reference(&result->zval_cache); +		result->zval_cache = NULL; +	} + +	DBG_VOID_RETURN; +} +/* }}} */ + + +/* {{{ mysqlnd_internal_free_result */ +static +void mysqlnd_internal_free_result(MYSQLND_RES *result TSRMLS_DC) +{ +	DBG_ENTER("mysqlnd_internal_free_result"); +	/* +	  result->conn is an address if this is an unbuffered query. +	  In this case, decrement the reference counter in the connection +	  object and if needed free the latter. +	*/ +	if (result->conn) { +		result->conn->m->free_reference(result->conn TSRMLS_CC); +		result->conn = NULL; +	} + +	result->m.free_result_contents(result TSRMLS_CC); +	efree(result); + +	DBG_VOID_RETURN; +} +/* }}} */ + + +/* {{{ mysqlnd_res::read_result_metadata */ +static enum_func_status +MYSQLND_METHOD(mysqlnd_res, read_result_metadata)(MYSQLND_RES *result, MYSQLND *conn TSRMLS_DC) +{ +	DBG_ENTER("mysqlnd_res::read_result_metadata"); + +	/* +	  Make it safe to call it repeatedly for PS - +	  better free and allocate a new because the number of field might change  +	  (select *) with altered table. Also for statements which skip the PS +	  infrastructure! +	*/ +	if (result->meta) { +		result->meta->m->free_metadata(result->meta, FALSE TSRMLS_CC); +		result->meta = NULL; +	} + +	result->meta = mysqlnd_result_meta_init(result->field_count TSRMLS_CC); + +	/* 1. Read all fields metadata */ + +	/* It's safe to reread without freeing */ +	if (FAIL == result->meta->m->read_metadata(result->meta, conn TSRMLS_CC)) { +		result->m.free_result_contents(result TSRMLS_CC); +		DBG_RETURN(FAIL); +	} + +	/* +	  2. Follows an EOF packet, which the client of mysqlnd_read_result_metadata() +		 should consume. +	  3. If there is a result set, it follows. The last packet will have 'eof' set +	  	 If PS, then no result set follows. +	*/ + +	DBG_RETURN(PASS); +} +/* }}} */ + + +/* {{{ mysqlnd_query_read_result_set_header */ +enum_func_status +mysqlnd_query_read_result_set_header(MYSQLND *conn, MYSQLND_STMT *stmt TSRMLS_DC) +{ +	enum_func_status ret; +	php_mysql_packet_rset_header rset_header; + +	DBG_ENTER("mysqlnd_query_read_result_set_header"); +	DBG_INF_FMT("stmt=%d", stmt? stmt->stmt_id:0); + +	ret = FAIL; +	PACKET_INIT_ALLOCA(rset_header, PROT_RSET_HEADER_PACKET); +	do { +		if (FAIL == (ret = PACKET_READ_ALLOCA(rset_header, conn))) { +			php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error reading result set's header"); +			break; +		} + +		if (rset_header.error_info.error_no) { +			/* +			  Cover a protocol design error: error packet does not +			  contain the server status. Therefore, the client has no way +			  to find out whether there are more result sets of +			  a multiple-result-set statement pending. Luckily, in 5.0 an +			  error always aborts execution of a statement, wherever it is +			  a multi-statement or a stored procedure, so it should be +			  safe to unconditionally turn off the flag here. +			*/ +			conn->upsert_status.server_status &= ~SERVER_MORE_RESULTS_EXISTS; +			conn->upsert_status.affected_rows = -1; +			/* +			  This will copy the error code and the messages, as they +			  are buffers in the struct +			*/ +			conn->error_info = rset_header.error_info; +			ret = FAIL; +			break; +		} +		conn->error_info.error_no = 0; + +		switch (rset_header.field_count) { +			case MYSQLND_NULL_LENGTH: {	/* LOAD DATA LOCAL INFILE */ +				zend_bool is_warning; +				DBG_INF("LOAD DATA"); +				conn->last_query_type = QUERY_LOAD_LOCAL; +				conn->state = CONN_SENDING_LOAD_DATA; +				ret = mysqlnd_handle_local_infile(conn, rset_header.info_or_local_file, &is_warning TSRMLS_CC); +				conn->state = (ret == PASS || is_warning == TRUE)? CONN_READY:CONN_QUIT_SENT; +				MYSQLND_INC_CONN_STATISTIC(&conn->stats, STAT_NON_RSET_QUERY); +				break; +			} +			case 0:				/* UPSERT */ +				DBG_INF("UPSERT"); +				conn->last_query_type = QUERY_UPSERT; +				conn->field_count = rset_header.field_count; +				conn->upsert_status.warning_count = rset_header.warning_count; +				conn->upsert_status.server_status = rset_header.server_status; +				conn->upsert_status.affected_rows = rset_header.affected_rows; +				conn->upsert_status.last_insert_id = rset_header.last_insert_id; +				SET_NEW_MESSAGE(conn->last_message, conn->last_message_len, +								rset_header.info_or_local_file, rset_header.info_or_local_file_len, +								conn->persistent); +				/* Result set can follow UPSERT statement, check server_status */ +				if (conn->upsert_status.server_status & SERVER_MORE_RESULTS_EXISTS) { +					conn->state = CONN_NEXT_RESULT_PENDING; +				} else { +					conn->state = CONN_READY; +				} +				ret = PASS; +				MYSQLND_INC_CONN_STATISTIC(&conn->stats, STAT_NON_RSET_QUERY); +				break; +			default:{			/* Result set	*/ +				php_mysql_packet_eof fields_eof; +				MYSQLND_RES *result; +				enum_mysqlnd_collected_stats stat = STAT_LAST; + +				DBG_INF("Result set pending"); +				SET_EMPTY_MESSAGE(conn->last_message, conn->last_message_len, conn->persistent); + +				MYSQLND_INC_CONN_STATISTIC(&conn->stats, STAT_RSET_QUERY); +				memset(&conn->upsert_status, 0, sizeof(conn->upsert_status)); +				conn->last_query_type = QUERY_SELECT; +				conn->state = CONN_FETCHING_DATA; +				/* PS has already allocated it */ +				if (!stmt) { +					conn->field_count = rset_header.field_count; +					result = +						conn->current_result= +							mysqlnd_result_init(rset_header.field_count, +												mysqlnd_palloc_get_thd_cache_reference(conn->zval_cache) +												TSRMLS_CC); +				} else { +					if (!stmt->result) { +						DBG_INF("This is 'SHOW'/'EXPLAIN'-like query."); +						/* +						  This is 'SHOW'/'EXPLAIN'-like query. Current implementation of +						  prepared statements can't send result set metadata for these queries +						  on prepare stage. Read it now. +						*/ +						conn->field_count = rset_header.field_count; +						result = +							stmt->result = +								mysqlnd_result_init(rset_header.field_count, +													mysqlnd_palloc_get_thd_cache_reference(conn->zval_cache) +													TSRMLS_CC); +					} else { +						/* +						  Update result set metadata if it for some reason changed between +						  prepare and execute, i.e.: +						  - in case of 'SELECT ?' we don't know column type unless data was +							supplied to mysql_stmt_execute, so updated column type is sent +							now. +						  - if data dictionary changed between prepare and execute, for +							example a table used in the query was altered. +						  Note, that now (4.1.3) we always send metadata in reply to +						  COM_STMT_EXECUTE (even if it is not necessary), so either this or +						  previous branch always works. +						*/	 +					} +					result = stmt->result; +				} + +				if (FAIL == (ret = result->m.read_result_metadata(result, conn TSRMLS_CC))) { +					/* For PS, we leave them in Prepared state */ +					if (!stmt) { +						efree(conn->current_result); +						conn->current_result = NULL; +					} +					DBG_ERR("Error ocurred while reading metadata"); +					break; +				} + +				/* Check for SERVER_STATUS_MORE_RESULTS if needed */ +				PACKET_INIT_ALLOCA(fields_eof, PROT_EOF_PACKET); +				if (FAIL == (ret = PACKET_READ_ALLOCA(fields_eof, conn))) { +					DBG_ERR("Error ocurred while reading the EOF packet"); +					result->m.free_result_contents(result TSRMLS_CC); +					efree(result); +					if (!stmt) { +						conn->current_result = NULL; +					} else { +						stmt->result = NULL; +						memset(stmt, 0, sizeof(MYSQLND_STMT)); +						stmt->state = MYSQLND_STMT_INITTED; +					} +				} else { +					DBG_INF_FMT("warns=%u status=%u", fields_eof.warning_count, fields_eof.server_status); +					conn->upsert_status.warning_count = fields_eof.warning_count; +					conn->upsert_status.server_status = fields_eof.server_status; +					if (fields_eof.server_status & MYSQLND_SERVER_QUERY_NO_GOOD_INDEX_USED) { +						stat = STAT_BAD_INDEX_USED; +					} else if (fields_eof.server_status & MYSQLND_SERVER_QUERY_NO_INDEX_USED) { +						stat = STAT_NO_INDEX_USED; +					} +					if (stat != STAT_LAST) { +						char *backtrace = mysqlnd_get_backtrace(TSRMLS_C); +#if A0 +						php_log_err(backtrace TSRMLS_CC); +#endif +						efree(backtrace); +						MYSQLND_INC_CONN_STATISTIC(&conn->stats, stat); +					} +				} + +				PACKET_FREE_ALLOCA(fields_eof); + +				break; +			} +		} +	} while (0); +	PACKET_FREE_ALLOCA(rset_header); + +	DBG_INF(ret == PASS? "PASS":"FAIL"); +	DBG_RETURN(ret); +} +/* }}} */ + + +/* {{{ mysqlnd_fetch_lengths_buffered */ +/* +  Do lazy initialization for buffered results. As PHP strings have +  length inside, this function makes not much sense in the context +  of PHP, to be called as separate function. But let's have it for +  completeness. +*/ +static  +unsigned long * mysqlnd_fetch_lengths_buffered(MYSQLND_RES * const result) +{ +	int i; +	zval **previous_row; + +	/* +	  If: +	  - unbuffered result +	  - first row has not been read +	  - last_row has been read +	*/ +	if (result->data->data_cursor == NULL || +		result->data->data_cursor == result->data->data || +		((result->data->data_cursor - result->data->data) > result->data->row_count)) +	{ +		return NULL;/* No rows or no more rows */ +	} + +	previous_row = *(result->data->data_cursor - 1); +	for (i = 0; i < result->field_count; i++) { +		result->lengths[i] = (Z_TYPE_P(previous_row[i]) == IS_NULL)? 0:Z_STRLEN_P(previous_row[i]); +	} + +	return result->lengths; +} +/* }}} */ + + +/* {{{ mysqlnd_fetch_lengths_unbuffered */ +static +unsigned long * mysqlnd_fetch_lengths_unbuffered(MYSQLND_RES * const result) +{ +	return result->lengths; +} +/* }}} */ + + +/* {{{ mysqlnd_res::fetch_lengths */ +PHPAPI unsigned long * mysqlnd_fetch_lengths(MYSQLND_RES * const result) +{ +	return result->m.fetch_lengths? result->m.fetch_lengths(result):NULL; +} +/* }}} */ + + +/* {{{ mysqlnd_fetch_row_unbuffered */ +static enum_func_status +mysqlnd_fetch_row_unbuffered(MYSQLND_RES *result, void *param, unsigned int flags, +							zend_bool *fetched_anything TSRMLS_DC) +{ +	enum_func_status		ret; +	zval 					*row = (zval *) param; +	unsigned int			i, +							field_count = result->field_count; +	php_mysql_packet_row	*row_packet = result->row_packet; +	unsigned long			*lengths = result->lengths; + +	DBG_ENTER("mysqlnd_fetch_row_unbuffered"); +	DBG_INF_FMT("flags=%d", flags); + +	if (result->unbuf->eof_reached) { +		/* No more rows obviously */ +		*fetched_anything = FALSE; +		DBG_RETURN(PASS); +	} +	if (result->conn->state != CONN_FETCHING_DATA) { +		SET_CLIENT_ERROR(result->conn->error_info, CR_COMMANDS_OUT_OF_SYNC, +						 UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);  +		DBG_RETURN(FAIL); +	} +	/* Let the row packet fill our buffer and skip additional mnd_malloc + memcpy */ +	row_packet->skip_extraction = row? FALSE:TRUE; + +	/* +	  If we skip rows (row == NULL) we have to +	  mysqlnd_unbuffered_free_last_data() before it. The function returns always true. +	*/ +	if (PASS == (ret = PACKET_READ(row_packet, result->conn)) && !row_packet->eof) { +		result->unbuf->row_count++; +		*fetched_anything = TRUE; + +		mysqlnd_unbuffered_free_last_data(result TSRMLS_CC); + +		result->unbuf->last_row_data = row_packet->fields; +		result->unbuf->last_row_buffer = row_packet->row_buffer; +		row_packet->fields = NULL; +		row_packet->row_buffer = NULL; + +		MYSQLND_INC_CONN_STATISTIC(&result->conn->stats, STAT_ROWS_FETCHED_FROM_CLIENT_NORMAL_UNBUF); + +		if (!row_packet->skip_extraction) { +			HashTable *row_ht = Z_ARRVAL_P(row); + +			for (i = 0; i < field_count; i++) { +				zval *data = result->unbuf->last_row_data[i]; +				int len = (Z_TYPE_P(data) == IS_NULL)? 0:Z_STRLEN_P(data); +				MYSQLND_RES_METADATA *meta = result->meta; + +				if (lengths) { +					lengths[i] = len; +				} + +				/* Forbid ZE to free it, we will clean it */ +				ZVAL_ADDREF(data); + +				if ((flags & MYSQLND_FETCH_BOTH) == MYSQLND_FETCH_BOTH) { +					ZVAL_ADDREF(data); +				} +				if (flags & MYSQLND_FETCH_NUM) { +					zend_hash_next_index_insert(row_ht, &data, sizeof(zval *), NULL); +				} +				if (flags & MYSQLND_FETCH_ASSOC) { +					/* zend_hash_quick_update needs length + trailing zero */ +					/* QQ: Error handling ? */ +					/* +					  zend_hash_quick_update does not check, as add_assoc_zval_ex do, whether +					  the index is a numeric and convert it to it. This however means constant +					  hashing of the column name, which is not needed as it can be precomputed. +					*/ +					if (meta->zend_hash_keys[i].is_numeric == FALSE) { +#if PHP_MAJOR_VERSION >= 6 +						if (UG(unicode)) { +							zend_u_hash_quick_update(Z_ARRVAL_P(row), IS_UNICODE, +													 meta->zend_hash_keys[i].ustr, +													 meta->zend_hash_keys[i].ulen + 1, +													 meta->zend_hash_keys[i].key, +													 (void *) &data, sizeof(zval *), NULL); +						} else +#endif +						{ +							zend_hash_quick_update(Z_ARRVAL_P(row), +												   meta->fields[i].name, +												   meta->fields[i].name_length + 1, +												   meta->zend_hash_keys[i].key, +												   (void *) &data, sizeof(zval *), NULL); +						} +					} else { +						zend_hash_index_update(Z_ARRVAL_P(row), +											   meta->zend_hash_keys[i].key, +											   (void *) &data, sizeof(zval *), NULL); +					} +				} +				if (meta->fields[i].max_length < len) { +					meta->fields[i].max_length = len; +				} +			} +		} +	} else if (ret == FAIL) { +		if (row_packet->error_info.error_no) { +			result->conn->error_info = row_packet->error_info; +			DBG_ERR_FMT("errorno=%d error=%s", row_packet->error_info.error_no, row_packet->error_info.error);  +		} +		*fetched_anything = FALSE; +		result->conn->state = CONN_READY; +		result->unbuf->eof_reached = TRUE; /* so next time we won't get an error */ +	} else if (row_packet->eof) { +		/* Mark the connection as usable again */ +		DBG_INF_FMT("warns=%u status=%u", row_packet->warning_count, row_packet->server_status); +		result->unbuf->eof_reached = TRUE; +		result->conn->upsert_status.warning_count = row_packet->warning_count; +		result->conn->upsert_status.server_status = row_packet->server_status; +		/* +		  result->row_packet will be cleaned when +		  destroying the result object +		*/ +		if (result->conn->upsert_status.server_status & SERVER_MORE_RESULTS_EXISTS) { +			result->conn->state = CONN_NEXT_RESULT_PENDING; +		} else { +			result->conn->state = CONN_READY; +		} +		mysqlnd_unbuffered_free_last_data(result TSRMLS_CC); +		*fetched_anything = FALSE; +	} + +	DBG_INF_FMT("ret=%s fetched=%d", ret == PASS? "PASS":"FAIL", *fetched_anything); +	DBG_RETURN(PASS); +} +/* }}} */ + + +/* {{{ mysqlnd_res::use_result */ +MYSQLND_RES * +MYSQLND_METHOD(mysqlnd_res, use_result)(MYSQLND_RES * const result, zend_bool ps TSRMLS_DC) +{ +	DBG_ENTER("mysqlnd_res::use_result"); +	DBG_INF_FMT("ps=%d", ps); + +	result->type			= MYSQLND_RES_NORMAL; +	result->m.fetch_row		= result->m.fetch_row_normal_unbuffered; +	result->m.fetch_lengths	= mysqlnd_fetch_lengths_unbuffered; +	result->unbuf			= mnd_ecalloc(1, sizeof(MYSQLND_RES_UNBUFFERED)); + +	/* +	  Will be freed in the mysqlnd_internal_free_result_contents() called +	  by the resource destructor. mysqlnd_fetch_row_unbuffered() expects +	  this to be not NULL. +	*/ +	PACKET_INIT(result->row_packet, PROT_ROW_PACKET, php_mysql_packet_row *); +	result->row_packet->field_count = result->field_count; +	result->row_packet->binary_protocol = FALSE; +	result->row_packet->fields_metadata = result->meta->fields; +	result->row_packet->bit_fields_count = result->meta->bit_fields_count; +	result->row_packet->bit_fields_total_len = result->meta->bit_fields_total_len; +	result->lengths = mnd_ecalloc(result->field_count, sizeof(unsigned long)); + +	/* No multithreading issues as we don't share the connection :) */ + +	DBG_RETURN(result); +} +/* }}} */ + + +/* {{{ mysqlnd_fetch_row_buffered */ +static enum_func_status +mysqlnd_fetch_row_buffered(MYSQLND_RES *result, void *param, unsigned int flags, +						   zend_bool *fetched_anything TSRMLS_DC) +{ +	unsigned int i; +	zval *row = (zval *) param; + +	DBG_ENTER("mysqlnd_fetch_row_buffered"); +	DBG_INF_FMT("flags=%u row=%p", flags, row); + +	/* If we haven't read everything */ +	if (result->data->data_cursor && +		(result->data->data_cursor - result->data->data) < result->data->row_count) +	{ +		zval **current_row = *result->data->data_cursor; +		for (i = 0; i < result->field_count; i++) { +			zval *data = current_row[i]; + +			/* +			  Let us later know what to do with this zval. If ref_count > 1, we will just +			  decrease it, otherwise free it. zval_ptr_dtor() make this very easy job. +			*/ +			ZVAL_ADDREF(data); +			 +			if ((flags & MYSQLND_FETCH_BOTH) == MYSQLND_FETCH_BOTH) { +				ZVAL_ADDREF(data); +			} +			if (flags & MYSQLND_FETCH_NUM) { +				zend_hash_next_index_insert(Z_ARRVAL_P(row), &data, sizeof(zval *), NULL); +			} +			if (flags & MYSQLND_FETCH_ASSOC) { +				/* zend_hash_quick_update needs length + trailing zero */ +				/* QQ: Error handling ? */ +				/* +				  zend_hash_quick_update does not check, as add_assoc_zval_ex do, whether +				  the index is a numeric and convert it to it. This however means constant +				  hashing of the column name, which is not needed as it can be precomputed. +				*/ +				if (result->meta->zend_hash_keys[i].is_numeric == FALSE) { +#if PHP_MAJOR_VERSION >= 6 +					if (UG(unicode)) { +						zend_u_hash_quick_update(Z_ARRVAL_P(row), IS_UNICODE, +												 result->meta->zend_hash_keys[i].ustr, +												 result->meta->zend_hash_keys[i].ulen + 1, +												 result->meta->zend_hash_keys[i].key, +												 (void *) &data, sizeof(zval *), NULL); +					} else +#endif +					{ +						zend_hash_quick_update(Z_ARRVAL_P(row), +											   result->meta->fields[i].name, +											   result->meta->fields[i].name_length + 1, +											   result->meta->zend_hash_keys[i].key, +											   (void *) &data, sizeof(zval *), NULL); +					} +				} else { +					zend_hash_index_update(Z_ARRVAL_P(row), +										   result->meta->zend_hash_keys[i].key, +										   (void *) &data, sizeof(zval *), NULL); +				} +			} +		} +		result->data->data_cursor++; +		*fetched_anything = TRUE; +		MYSQLND_INC_GLOBAL_STATISTIC(STAT_ROWS_FETCHED_FROM_CLIENT_NORMAL_BUF); +	} else { +		result->data->data_cursor = NULL; +		*fetched_anything = FALSE; +		DBG_INF("EOF reached"); +	} +	DBG_INF_FMT("ret=PASS fetched=%d", *fetched_anything); +	DBG_RETURN(PASS); +} +/* }}} */ + + +#define STORE_RESULT_PREALLOCATED_SET 32 + +/* {{{ mysqlnd_store_result_fetch_data */ +enum_func_status +mysqlnd_store_result_fetch_data(MYSQLND * const conn, MYSQLND_RES *result, +								MYSQLND_RES_METADATA *meta, +								zend_bool binary_protocol, +								zend_bool update_max_length, +								zend_bool to_cache TSRMLS_DC) +{ +	enum_func_status ret; +	php_mysql_packet_row row_packet; +	unsigned int next_extend = STORE_RESULT_PREALLOCATED_SET, free_rows; +	MYSQLND_RES_BUFFERED *set; + +	DBG_ENTER("mysqlnd_store_result_fetch_data"); +	DBG_INF_FMT("conn=%llu binary_proto=%d update_max_len=%d to_cache=%d", +				conn->thread_id, binary_protocol, update_max_length, to_cache); + +	free_rows = next_extend; + +	result->data	= set = mnd_pecalloc(1, sizeof(MYSQLND_RES_BUFFERED), to_cache); +	set->data		= mnd_pemalloc(STORE_RESULT_PREALLOCATED_SET * sizeof(zval **), to_cache); +	set->row_buffers= mnd_pemalloc(STORE_RESULT_PREALLOCATED_SET * sizeof(zend_uchar *), to_cache); +	set->persistent	= to_cache; +	set->qcache		= to_cache? mysqlnd_qcache_get_cache_reference(conn->qcache):NULL; +	set->references	= 1; + +	PACKET_INIT_ALLOCA(row_packet, PROT_ROW_PACKET); +	row_packet.field_count = meta->field_count; +	row_packet.binary_protocol = binary_protocol; +	row_packet.fields_metadata = meta->fields; +	row_packet.bit_fields_count		= meta->bit_fields_count; +	row_packet.bit_fields_total_len = meta->bit_fields_total_len; +	/* Let the row packet fill our buffer and skip additional malloc + memcpy */ +	while (FAIL != (ret = PACKET_READ_ALLOCA(row_packet, conn)) && !row_packet.eof) { +		int i; +		zval **current_row; + +		if (!free_rows) { +			mynd_ulonglong total_rows = free_rows = next_extend = next_extend * 5 / 3; /* extend with 33% */ +			total_rows += set->row_count; +			set->data = mnd_perealloc(set->data, total_rows * sizeof(zval **), set->persistent); + +			set->row_buffers = mnd_perealloc(set->row_buffers, +										 total_rows * sizeof(zend_uchar *), set->persistent); +		} +		free_rows--; +		current_row = set->data[set->row_count] = row_packet.fields; +		set->row_buffers[set->row_count] = row_packet.row_buffer; +		set->row_count++; + +		/* So row_packet's destructor function won't efree() it */ +		row_packet.fields = NULL; +		row_packet.row_buffer = NULL; + + +		if (update_max_length == TRUE) { +			for (i = 0; i < row_packet.field_count; i++) { +				/* +				  NULL fields are 0 length, 0 is not more than 0 +				  String of zero size, definitely can't be the next max_length. +				  Thus for NULL and zero-length we are quite efficient. +				*/ +				if (Z_TYPE_P(current_row[i]) >= IS_STRING) { +					unsigned long len = Z_STRLEN_P(current_row[i]); +					if (meta->fields[i].max_length < len) { +						meta->fields[i].max_length = len; +					} +				} +			} +		} +		/* +		  No need to FREE_ALLOCA as we can reuse the +		  'lengths' and 'fields' arrays. For lengths its absolutely safe. +		  'fields' is reused because the ownership of the strings has been +		  transfered above.  +		*/ +	} +	MYSQLND_INC_CONN_STATISTIC_W_VALUE(&conn->stats, +									   binary_protocol? STAT_ROWS_BUFFERED_FROM_CLIENT_PS: +														STAT_ROWS_BUFFERED_FROM_CLIENT_NORMAL, +									   set->row_count); + +	/* Finally clean */ +	if (row_packet.eof) {  +		conn->upsert_status.warning_count = row_packet.warning_count; +		conn->upsert_status.server_status = row_packet.server_status; +	} +	/* save some memory */ +	if (free_rows) { +		set->data = mnd_perealloc(set->data, +							  (size_t) set->row_count * sizeof(zval **), +							  set->persistent); +		set->row_buffers = mnd_perealloc(set->row_buffers, +									 (size_t) set->row_count * sizeof(zend_uchar *), +									 set->persistent); +	} + +	if (conn->upsert_status.server_status & SERVER_MORE_RESULTS_EXISTS) { +		conn->state = CONN_NEXT_RESULT_PENDING; +	} else { +		conn->state = CONN_READY; +	} + +	if (ret == FAIL) { +		set->error_info = row_packet.error_info; +	} else { +		/* Position at the first row */ +		set->data_cursor = set->data;	 + +		/* libmysql's documentation says it should be so for SELECT statements */ +		conn->upsert_status.affected_rows = result->data->row_count; +	} +	PACKET_FREE_ALLOCA(row_packet); + +	DBG_INF_FMT("ret=%s row_count=%u warns=%u status=%u", ret == PASS? "PASS":"FAIL", +				set->row_count, conn->upsert_status.warning_count, conn->upsert_status.server_status); +	DBG_RETURN(ret); +} +/* }}} */ + + +/* {{{ mysqlnd_res::store_result */ +MYSQLND_RES * +MYSQLND_METHOD(mysqlnd_res, store_result)(MYSQLND_RES * result, +										  MYSQLND * const conn, +										  zend_bool ps_protocol TSRMLS_DC) +{ +	enum_func_status ret; +	zend_bool to_cache = FALSE; + +	DBG_ENTER("mysqlnd_res::store_result"); +	DBG_INF_FMT("conn=%d ps_protocol=%d", conn->thread_id, ps_protocol); + +	result->conn			= NULL;	/* store result does not reference  the connection */ +	result->type			= MYSQLND_RES_NORMAL; +	result->m.fetch_row		= result->m.fetch_row_normal_buffered; +	result->m.fetch_lengths	= mysqlnd_fetch_lengths_buffered; + +	conn->state = CONN_FETCHING_DATA; + +	result->lengths = mnd_ecalloc(result->field_count, sizeof(unsigned long)); + +	ret = mysqlnd_store_result_fetch_data(conn, result, result->meta, +										  ps_protocol, TRUE, to_cache TSRMLS_CC); +	if (PASS == ret) { +		/* libmysql's documentation says it should be so for SELECT statements */ +		conn->upsert_status.affected_rows = result->data->row_count; +	} else { +		conn->error_info = result->data->error_info; +		result->m.free_result_internal(result TSRMLS_CC); +		result = NULL; +	} + +	DBG_RETURN(result); +} +/* }}} */ + + +/* {{{ mysqlnd_res::skip_result */ +static enum_func_status +MYSQLND_METHOD(mysqlnd_res, skip_result)(MYSQLND_RES * const result TSRMLS_DC) +{ +	zend_bool fetched_anything; + +	DBG_ENTER("mysqlnd_res::skip_result"); +	/* +	  Unbuffered sets +	  A PS could be prepared - there is metadata and thus a stmt->result but the +	  fetch_row function isn't actually set (NULL), thus we have to skip these. +	*/ +	if (!result->data && result->conn && result->unbuf && +		!result->unbuf->eof_reached && result->m.fetch_row) +	{ +		DBG_INF("skipping result"); +		/* We have to fetch all data to clean the line */ +		MYSQLND_INC_CONN_STATISTIC(&result->conn->stats, +									result->type == MYSQLND_RES_NORMAL? STAT_FLUSHED_NORMAL_SETS: +																		STAT_FLUSHED_PS_SETS); + +		while ((PASS == result->m.fetch_row(result, NULL, 0, &fetched_anything TSRMLS_CC)) && +			   fetched_anything == TRUE) +		{ +			/* do nothing */; +		} +	} +	DBG_RETURN(PASS); +} +/* }}} */ + + +/* {{{ mysqlnd_res::free_result */ +static enum_func_status +MYSQLND_METHOD(mysqlnd_res, free_result)(MYSQLND_RES *result, zend_bool implicit TSRMLS_DC) +{ +	DBG_ENTER("mysqlnd_res::free_result"); +	DBG_INF_FMT("implicit=%d", implicit); + +	result->m.skip_result(result TSRMLS_CC); +	MYSQLND_INC_CONN_STATISTIC(result->conn? &result->conn->stats : NULL, +							   implicit == TRUE?	STAT_FREE_RESULT_IMPLICIT: +							   						STAT_FREE_RESULT_EXPLICIT); + +	result->m.free_result_internal(result TSRMLS_CC); +	DBG_RETURN(PASS); +} +/* }}} */ + + +/* {{{ mysqlnd_res::data_seek */ +static enum_func_status +MYSQLND_METHOD(mysqlnd_res, data_seek)(MYSQLND_RES *result, mynd_ulonglong row TSRMLS_DC) +{ +	DBG_ENTER("mysqlnd_res::data_seek"); +	DBG_INF_FMT("row=%lu", row); + +	if (!result->data) { +		return FAIL; +	} + +	/* libmysql just moves to the end, it does traversing of a linked list */ +	if (row >= result->data->row_count) { +		result->data->data_cursor = NULL; +	} else { +		result->data->data_cursor = result->data->data + row; +	} + +	DBG_RETURN(PASS); +} +/* }}} */ + + +/* {{{ mysqlnd_res::num_fields */ +mynd_ulonglong +MYSQLND_METHOD(mysqlnd_res, num_rows)(const MYSQLND_RES * const res) +{ +	/* Be compatible with libmysql. We count row_count, but will return 0 */ +	return res->data? res->data->row_count:0; +} +/* }}} */ + + +/* {{{ mysqlnd_res::num_fields */ +unsigned int +MYSQLND_METHOD(mysqlnd_res, num_fields)(const MYSQLND_RES * const res) +{ +	return res->field_count; +} +/* }}} */ + + +/* {{{ mysqlnd_res::fetch_field */ +static MYSQLND_FIELD * +MYSQLND_METHOD(mysqlnd_res, fetch_field)(MYSQLND_RES * const result TSRMLS_DC) +{ +	DBG_ENTER("mysqlnd_res::fetch_field"); +	DBG_RETURN(result->meta? result->meta->m->fetch_field(result->meta TSRMLS_CC):NULL); +} +/* }}} */ + + +/* {{{ mysqlnd_res::fetch_field_direct */ +static MYSQLND_FIELD * +MYSQLND_METHOD(mysqlnd_res, fetch_field_direct)(const MYSQLND_RES * const result, +												MYSQLND_FIELD_OFFSET fieldnr TSRMLS_DC) +{ +	DBG_ENTER("mysqlnd_res::fetch_field_direct"); +	DBG_RETURN(result->meta? result->meta->m->fetch_field_direct(result->meta, fieldnr TSRMLS_CC):NULL); +} +/* }}} */ + + +/* {{{ mysqlnd_res::field_seek */ +static MYSQLND_FIELD_OFFSET +MYSQLND_METHOD(mysqlnd_res, field_seek)(MYSQLND_RES * const result, +										MYSQLND_FIELD_OFFSET field_offset) +{ +	MYSQLND_FIELD_OFFSET return_value = 0; +	if (result->meta) { +		return_value = result->meta->current_field; +		result->meta->current_field = field_offset; +	} +	return return_value; +} +/* }}} */ + + +/* {{{ mysqlnd_res::field_tell */ +static MYSQLND_FIELD_OFFSET +MYSQLND_METHOD(mysqlnd_res, field_tell)(const MYSQLND_RES * const result) +{ +	return result->meta? result->meta->m->field_tell(result->meta):0; +} +/* }}} */ + + +/* {{{ mysqlnd_res::fetch_into */ +static void +MYSQLND_METHOD(mysqlnd_res, fetch_into)(MYSQLND_RES *result, unsigned int flags, +										zval *return_value, +										enum_mysqlnd_extension extension TSRMLS_DC ZEND_FILE_LINE_DC) +{ +	zend_bool fetched_anything; + +	DBG_ENTER("mysqlnd_res::fetch_into"); +	DBG_INF_FMT("flags=%u mysqlnd_extension=%d", flags, extension); + +	if (!result->m.fetch_row) { +		RETVAL_NULL(); +		DBG_VOID_RETURN; +	} +	/* +	  Hint Zend how many elements we will have in the hash. Thus it won't +	  extend and rehash the hash constantly. +	*/ +	mysqlnd_array_init(return_value, mysqlnd_num_fields(result) * 2); +	if (FAIL == result->m.fetch_row(result, (void *)return_value, flags, &fetched_anything TSRMLS_CC)) { +		php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error while reading a row"); +		RETVAL_FALSE; +	} else if (fetched_anything == FALSE) { +		zval_dtor(return_value); +		switch (extension) { +			case MYSQLND_MYSQLI: +				RETVAL_NULL(); +				break; +			case MYSQLND_MYSQL: +				RETVAL_FALSE; +				break; +			default:exit(0); +		} +	} +	/* +	  return_value is IS_NULL for no more data and an array for data. Thus it's ok +	  to return here. +	*/ +	DBG_VOID_RETURN; +} +/* }}} */ + + +/* {{{ mysqlnd_res::fetch_all */ +static void +MYSQLND_METHOD(mysqlnd_res, fetch_all)(MYSQLND_RES *result, unsigned int flags, +									   zval *return_value TSRMLS_DC ZEND_FILE_LINE_DC) +{ +	zval  *row; +	ulong i = 0; + +	DBG_ENTER("mysqlnd_res::fetch_all"); +	DBG_INF_FMT("flags=%u", flags); + +	/* mysqlnd_res::fetch_all works with buffered resultsets only */ +	if (result->conn || !result->data || +		!result->data->row_count || !result->data->data_cursor || +		result->data->data_cursor >= result->data->data + result->data->row_count) +	{ +		RETVAL_NULL(); +		DBG_VOID_RETURN; +	}	 + +	mysqlnd_array_init(return_value, (uint) result->data->row_count); + +	while (result->data->data_cursor && +		   (result->data->data_cursor - result->data->data) < result->data->row_count) +	{ +		MAKE_STD_ZVAL(row); +		mysqlnd_fetch_into(result, flags, row, MYSQLND_MYSQLI); +		add_index_zval(return_value, i++, row); +	} + +	DBG_VOID_RETURN; +} +/* }}} */ + + +/* {{{ mysqlnd_res::fetch_into */ +static void +MYSQLND_METHOD(mysqlnd_res, fetch_field_data)(MYSQLND_RES *result, unsigned int offset, +											  zval *return_value TSRMLS_DC) +{ +	zval row; +	zval **entry; +	uint i = 0; + +	DBG_ENTER("mysqlnd_res::fetch_field_data"); +	DBG_INF_FMT("offset=%u", offset); + +	if (!result->m.fetch_row) { +		RETVAL_NULL(); +		DBG_VOID_RETURN; +	} +	/* +	  Hint Zend how many elements we will have in the hash. Thus it won't +	  extend and rehash the hash constantly. +	*/ +	INIT_PZVAL(&row); +	mysqlnd_fetch_into(result, MYSQLND_FETCH_NUM, &row, MYSQLND_MYSQL); +	if (Z_TYPE(row) != IS_ARRAY) { +		zval_dtor(&row); +		RETVAL_NULL(); +		DBG_VOID_RETURN; +	} +	zend_hash_internal_pointer_reset(Z_ARRVAL(row)); +	while (i++ < offset) { +		zend_hash_move_forward(Z_ARRVAL(row)); +		zend_hash_get_current_data(Z_ARRVAL(row), (void **)&entry); +	} +	 +	zend_hash_get_current_data(Z_ARRVAL(row), (void **)&entry); + +	*return_value = **entry; +	zval_copy_ctor(return_value); +	ZVAL_REFCOUNT(return_value) = 1; +	zval_dtor(&row); + +	DBG_VOID_RETURN; +} +/* }}} */ + + +/* {{{ mysqlnd_result_init */ +MYSQLND_RES *mysqlnd_result_init(unsigned int field_count, MYSQLND_THD_ZVAL_PCACHE *cache TSRMLS_DC) +{ +	MYSQLND_RES *ret = mnd_ecalloc(1, sizeof(MYSQLND_RES)); + +	DBG_ENTER("mysqlnd_result_init"); +	DBG_INF_FMT("field_count=%u cache=%p", field_count, cache); + +	ret->field_count	= field_count; +	ret->zval_cache		= cache; + +	ret->m.use_result	= MYSQLND_METHOD(mysqlnd_res, use_result); +	ret->m.store_result	= MYSQLND_METHOD(mysqlnd_res, store_result); +	ret->m.free_result	= MYSQLND_METHOD(mysqlnd_res, free_result); +	ret->m.seek_data	= MYSQLND_METHOD(mysqlnd_res, data_seek); +	ret->m.num_rows		= MYSQLND_METHOD(mysqlnd_res, num_rows); +	ret->m.num_fields	= MYSQLND_METHOD(mysqlnd_res, num_fields); +	ret->m.fetch_into	= MYSQLND_METHOD(mysqlnd_res, fetch_into); +	ret->m.fetch_all	= MYSQLND_METHOD(mysqlnd_res, fetch_all); +	ret->m.fetch_field_data	= MYSQLND_METHOD(mysqlnd_res, fetch_field_data); +	ret->m.seek_field	= MYSQLND_METHOD(mysqlnd_res, field_seek); +	ret->m.field_tell	= MYSQLND_METHOD(mysqlnd_res, field_tell); +	ret->m.fetch_field	= MYSQLND_METHOD(mysqlnd_res, fetch_field); +	ret->m.fetch_field_direct = MYSQLND_METHOD(mysqlnd_res, fetch_field_direct); + +	ret->m.skip_result	= MYSQLND_METHOD(mysqlnd_res, skip_result); +	ret->m.free_result_buffers	= MYSQLND_METHOD(mysqlnd_res, free_result_buffers); +	ret->m.free_result_internal = mysqlnd_internal_free_result; +	ret->m.free_result_contents = mysqlnd_internal_free_result_contents; + +	ret->m.read_result_metadata = MYSQLND_METHOD(mysqlnd_res, read_result_metadata); +	ret->m.fetch_row_normal_buffered	= mysqlnd_fetch_row_buffered; +	ret->m.fetch_row_normal_unbuffered	= mysqlnd_fetch_row_unbuffered; + +	DBG_RETURN(ret); +} +/* }}} */ + +/* + * Local variables: + * tab-width: 4 + * c-basic-offset: 4 + * End: + * vim600: noet sw=4 ts=4 fdm=marker + * vim<600: noet sw=4 ts=4 + */  | 
