MS query notifications patch

Richard Hughes <[email protected]>
Newsgroups gmane.comp.db.tds.freetds
Message-ID <[email protected]>
Hi all,

I needed support for SQL Server 2005's Query Notifications feature, so
here's a patch against tip.

I've only given the patch a light sprinkling of testing so far, but
could I ask somebody to review it and confirm that I'm on the right
track so that I can create a v2 patch that is (hopefully) mergeable?
Both the Microsoft API and the TDS implementation feel extremely
kludgy to me and hence some of that kludginess needed to be reflected
in the patch; sorry about that, but don't shoot the messenger.

About Query Notifications:

Query Notifications are designed to support client-side caching of
datasets by providing a mechanism to allow the application to be
notified whenever its cache may have become out of date (i.e. because
somebody updated the underlying table(s)). Fundamentally the feature
works by delivering a query to the server and, rather than executing
it, the server does internal magic to register that query and a
user-specified cookie. When the result set that the query would have
returned (had it actually been executed) changes, the cookie is
delivered back to the caller through a WAITFOR/RECEIVE statement.

User-level doc: http://technet.microsoft.com/en-us/library/ms130764.aspx
Protocol doc: http://msdn.microsoft.com/en-us/library/dd304949.aspx

Only the registering of the query and cookie with the server needs
FreeTDS changes. The WAITFOR statement is just executed as a normal
statement with a normal result set.

Richard.

_______________________________________________
FreeTDS mailing list
[email protected]
http://lists.ibiblio.org/mailman/listinfo/freetds
freetds-mssql-query-notifications-v1.diff (application/octet-stream, 15.7 KB)
diff --git a/include/freetds/odbc.h b/include/freetds/odbc.h
index 4413b61..1a75080 100644
--- a/include/freetds/odbc.h
+++ b/include/freetds/odbc.h
@@ -379,6 +379,9 @@ struct _hsattr
 	/* SQLGetStmtAttr only */
 /*	TDS_DESC *imp_row_desc; */
 /*	TDS_DESC *imp_param_desc; */
+	DSTR qn_msgtext;
+	DSTR qn_options;
+	SQLLEN qn_timeout;
 };
 
 typedef enum
diff --git a/include/freetds/tds.h b/include/freetds/tds.h
index 413f1d8..d9d0fc5 100644
--- a/include/freetds/tds.h
+++ b/include/freetds/tds.h
@@ -1278,6 +1278,7 @@ TDSRET tds_submit_query_params(TDSSOCKET * tds, const char *query, TDSPARAMINFO
 TDSRET tds_submit_queryf(TDSSOCKET * tds, const char *queryf, ...);
 TDSRET tds_submit_prepare(TDSSOCKET * tds, const char *query, const char *id, TDSDYNAMIC ** dyn_out, TDSPARAMINFO * params);
 TDSRET tds_submit_execdirect(TDSSOCKET * tds, const char *query, TDSPARAMINFO * params);
+TDSRET tds72_submit_query_notification(TDSSOCKET * tds, const char *query, const char *msgtext, const char *options, TDS_INT timeout);
 TDSRET tds71_submit_prepexec(TDSSOCKET * tds, const char *query, const char *id, TDSDYNAMIC ** dyn_out, TDSPARAMINFO * params);
 TDSRET tds_submit_execute(TDSSOCKET * tds, TDSDYNAMIC * dyn);
 TDSRET tds_send_cancel(TDSSOCKET * tds);
diff --git a/include/odbcss.h b/include/odbcss.h
index 6582559..bb5f5b3 100644
--- a/include/odbcss.h
+++ b/include/odbcss.h
@@ -22,3 +22,6 @@
 #define SQL_DIAG_SS_MSGSTATE	(-1150)
 #define SQL_DIAG_SS_LINE	(-1154)
 
+#define SQL_SOPT_SS_QUERYNOTIFICATION_TIMEOUT  1233
+#define SQL_SOPT_SS_QUERYNOTIFICATION_MSGTEXT  1234
+#define SQL_SOPT_SS_QUERYNOTIFICATION_OPTIONS  1235
diff --git a/src/odbc/native.c b/src/odbc/native.c
index decfcb9..eb8f6d1 100644
--- a/src/odbc/native.c
+++ b/src/odbc/native.c
@@ -36,6 +36,7 @@
 #include <assert.h>
 
 #include <freetds/odbc.h>
+#include <freetds/string.h>
 
 #ifdef DMALLOC
 #include <dmalloc.h>
@@ -228,6 +229,11 @@ prepare_call(struct _hstmt * stmt)
 	else
 		return SQL_ERROR;
 
+	if (tds_dstr_isempty(&stmt->attr.qn_msgtext) != tds_dstr_isempty(&stmt->attr.qn_options))
+		return SQL_SUCCESS_WITH_INFO;
+	if (!tds_dstr_isempty(&stmt->attr.qn_msgtext) && !IS_TDS72_PLUS(stmt->dbc->tds_socket->conn))
+		return SQL_SUCCESS_WITH_INFO;
+
 	if ((rc = to_native(stmt->dbc, stmt, buf)) != SQL_SUCCESS)
 		return rc;
 
diff --git a/src/odbc/odbc.c b/src/odbc/odbc.c
index 2f57ac3..613b17e 100644
--- a/src/odbc/odbc.c
+++ b/src/odbc/odbc.c
@@ -55,6 +55,7 @@
 #include <freetds/convert.h>
 #include "replacements.h"
 #include "sqlwparams.h"
+#include <odbcss.h>
 
 #ifdef DMALLOC
 #include <dmalloc.h>
@@ -71,9 +72,9 @@ static SQLRETURN _SQLFreeEnv(SQLHENV henv);
 static SQLRETURN _SQLFreeStmt(SQLHSTMT hstmt, SQLUSMALLINT fOption, int force);
 static SQLRETURN _SQLFreeDesc(SQLHDESC hdesc);
 static SQLRETURN _SQLExecute(TDS_STMT * stmt);
-static SQLRETURN _SQLSetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER ValuePtr, SQLINTEGER StringLength);
+static SQLRETURN _SQLSetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER ValuePtr, SQLINTEGER StringLength WIDE);
 static SQLRETURN _SQLGetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER Value, SQLINTEGER BufferLength,
-					 SQLINTEGER * StringLength);
+					 SQLINTEGER * StringLength WIDE);
 static SQLRETURN _SQLColAttribute(SQLHSTMT hstmt, SQLUSMALLINT icol, SQLUSMALLINT fDescType, SQLPOINTER rgbDesc,
 					  SQLSMALLINT cbDescMax, SQLSMALLINT FAR * pcbDesc, SQLLEN FAR * pfDesc _WIDE);
 static SQLRETURN _SQLFetch(TDS_STMT * stmt, SQLSMALLINT FetchOrientation, SQLLEN FetchOffset);
@@ -1070,10 +1071,10 @@ SQLParamOptions(SQLHSTMT hstmt, SQLULEN crow, SQLULEN FAR * pirow)
 	tdsdump_log(TDS_DBG_FUNC, "SQLParamOptions(%p, %lu, %p)\n", hstmt, (unsigned long int)crow, pirow);
 
 	/* emulate for ODBC 2 DM */
-	res = _SQLSetStmtAttr(hstmt, SQL_ATTR_PARAMS_PROCESSED_PTR, pirow, 0);
+	res = _SQLSetStmtAttr(hstmt, SQL_ATTR_PARAMS_PROCESSED_PTR, pirow, 0 _wide0);
 	if (res != SQL_SUCCESS)
 		return res;
-	return _SQLSetStmtAttr(hstmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER) (TDS_INTPTR) crow, 0);
+	return _SQLSetStmtAttr(hstmt, SQL_ATTR_PARAMSET_SIZE, (SQLPOINTER) (TDS_INTPTR) crow, 0 _wide0);
 }
 
 ODBC_FUNC(SQLPrimaryKeys, (P(SQLHSTMT,hstmt), PCHARIN(CatalogName,SQLSMALLINT),
@@ -1744,6 +1745,9 @@ _SQLAllocStmt(SQLHDBC hdbc, SQLHSTMT FAR * phstmt)
 	assert(stmt->ird->header.sql_desc_rows_processed_ptr == NULL);
 	stmt->attr.simulate_cursor = SQL_SC_NON_UNIQUE;
 	stmt->attr.use_bookmarks = SQL_UB_OFF;
+	tds_dstr_init(&stmt->attr.qn_msgtext);
+	tds_dstr_init(&stmt->attr.qn_options);
+	stmt->attr.qn_timeout = 432000;
 
 	stmt->sql_rowset_size = 1;
 
@@ -1760,7 +1764,7 @@ _SQLAllocStmt(SQLHDBC hdbc, SQLHSTMT FAR * phstmt)
 	*phstmt = (SQLHSTMT) stmt;
 
 	if (dbc->attr.cursor_type != SQL_CURSOR_FORWARD_ONLY)
-		_SQLSetStmtAttr(stmt, SQL_CURSOR_TYPE, (SQLPOINTER) (TDS_INTPTR) dbc->attr.cursor_type, SQL_IS_INTEGER);
+		_SQLSetStmtAttr(stmt, SQL_CURSOR_TYPE, (SQLPOINTER) (TDS_INTPTR) dbc->attr.cursor_type, SQL_IS_INTEGER _wide0);
 
 	ODBC_EXIT_(dbc);
 }
@@ -3281,7 +3285,12 @@ _SQLExecute(TDS_STMT * stmt)
 
 	stmt->row_count = TDS_NO_COUNT;
 
-	if (stmt->prepared_query_is_rpc) {
+	if (!tds_dstr_isempty(&stmt->attr.qn_msgtext)) {
+		char *name = stmt->query;
+		if (!name)
+			name = stmt->prepared_query;
+		ret = tds72_submit_query_notification(tds, stmt->query, tds_dstr_cstr(&stmt->attr.qn_msgtext), tds_dstr_cstr(&stmt->attr.qn_options), stmt->attr.qn_timeout);
+	} else if (stmt->prepared_query_is_rpc) {
 		/* TODO support stmt->apd->header.sql_desc_array_size for RPC */
 		/* get rpc name */
 		/* TODO change method */
@@ -4203,6 +4212,8 @@ _SQLFreeStmt(SQLHSTMT hstmt, SQLUSMALLINT fOption, int force)
 		odbc_errs_reset(&stmt->errs);
 		odbc_unlock_statement(stmt);
 		tds_dstr_free(&stmt->cursor_name);
+		tds_dstr_free(&stmt->attr.qn_msgtext);
+		tds_dstr_free(&stmt->attr.qn_options);
 		desc_free(stmt->ird);
 		desc_free(stmt->ipd);
 		desc_free(stmt->orig_ard);
@@ -4283,7 +4294,7 @@ _SQLFreeDesc(SQLHDESC hdesc)
 }
 
 static SQLRETURN
-_SQLGetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER Value, SQLINTEGER BufferLength, SQLINTEGER * StringLength)
+_SQLGetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER Value, SQLINTEGER BufferLength, SQLINTEGER * StringLength WIDE)
 {
 	void *src;
 	size_t size;
@@ -4438,6 +4449,20 @@ _SQLGetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER Value, SQLINTEG
 		size = sizeof(stmt->sql_rowset_size);
 		src = &stmt->sql_rowset_size;
 		break;
+	case SQL_SOPT_SS_QUERYNOTIFICATION_TIMEOUT:
+		size = sizeof(stmt->attr.qn_timeout);
+		src = &stmt->attr.qn_timeout;
+		break;
+	case SQL_SOPT_SS_QUERYNOTIFICATION_MSGTEXT:
+		{
+			SQLRETURN rc = odbc_set_string_oct(stmt->dbc, Value, BufferLength, StringLength, tds_dstr_cstr(&stmt->attr.qn_msgtext), tds_dstr_len(&stmt->attr.qn_msgtext));
+			ODBC_EXIT(stmt, rc);
+		}
+	case SQL_SOPT_SS_QUERYNOTIFICATION_OPTIONS:
+		{
+			SQLRETURN rc = odbc_set_string_oct(stmt->dbc, Value, BufferLength, StringLength, tds_dstr_cstr(&stmt->attr.qn_options), tds_dstr_len(&stmt->attr.qn_options));
+			ODBC_EXIT(stmt, rc);
+		}
 		/* TODO SQL_COLUMN_SEARCHABLE, although ODBC2 */
 	default:
 		odbc_errs_add(&stmt->errs, "HY092", NULL);
@@ -4458,7 +4483,7 @@ SQLGetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER Value, SQLINTEGE
 	tdsdump_log(TDS_DBG_FUNC, "SQLGetStmtAttr(%p, %d, %p, %d, %p)\n", 
 			hstmt, (int)Attribute, Value, (int)BufferLength, StringLength);
 
-	return _SQLGetStmtAttr(hstmt, Attribute, Value, BufferLength, StringLength);
+	return _SQLGetStmtAttr(hstmt, Attribute, Value, BufferLength, StringLength _wide0);
 }
 
 #ifdef ENABLE_ODBC_WIDE
@@ -4468,7 +4493,7 @@ SQLGetStmtAttrW(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER Value, SQLINTEG
 	tdsdump_log(TDS_DBG_FUNC, "SQLGetStmtAttr(%p, %d, %p, %d, %p)\n",
 			hstmt, (int)Attribute, Value, (int)BufferLength, StringLength);
 
-	return _SQLGetStmtAttr(hstmt, Attribute, Value, BufferLength, StringLength);
+	return _SQLGetStmtAttr(hstmt, Attribute, Value, BufferLength, StringLength, 1);
 }
 #endif
 #endif
@@ -4479,7 +4504,7 @@ SQLGetStmtOption(SQLHSTMT hstmt, SQLUSMALLINT fOption, SQLPOINTER pvParam)
 	tdsdump_log(TDS_DBG_FUNC, "SQLGetStmtOption(%p, %d, %p)\n", 
 			hstmt, fOption, pvParam);
 
-	return _SQLGetStmtAttr(hstmt, (SQLINTEGER) fOption, pvParam, SQL_MAX_OPTION_STRING_LENGTH, NULL);
+	return _SQLGetStmtAttr(hstmt, (SQLINTEGER) fOption, pvParam, SQL_MAX_OPTION_STRING_LENGTH, NULL _wide0);
 }
 
 SQLRETURN ODBC_PUBLIC ODBC_API
@@ -6253,7 +6278,7 @@ SQLSetConnectOptionW(SQLHDBC hdbc, SQLUSMALLINT fOption, SQLULEN vParam)
 #endif
 
 static SQLRETURN
-_SQLSetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER ValuePtr, SQLINTEGER StringLength)
+_SQLSetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER ValuePtr, SQLINTEGER StringLength WIDE)
 {
 	SQLULEN ui = (SQLULEN) (TDS_INTPTR) ValuePtr;
 	SQLUSMALLINT *usip = (SQLUSMALLINT *) ValuePtr;
@@ -6525,6 +6550,47 @@ _SQLSetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER ValuePtr, SQLIN
 		}
 		stmt->sql_rowset_size = ui;
 		break;
+	case SQL_SOPT_SS_QUERYNOTIFICATION_TIMEOUT:
+		if (*lp < 1) {
+			odbc_errs_add(&stmt->errs, "HY024", NULL);
+			break;
+		}
+		stmt->attr.qn_timeout = *lp;
+		break;
+	case SQL_SOPT_SS_QUERYNOTIFICATION_MSGTEXT:
+		if (!IS_VALID_LEN(StringLength)) {
+			odbc_errs_add(&stmt->errs, "HY090", NULL);
+			break;
+		}
+		{
+			DSTR s;
+
+			tds_dstr_init(&s);
+			if (!odbc_dstr_copy_oct(stmt->dbc, &s, StringLength, (ODBC_CHAR*) ValuePtr)) {
+				odbc_errs_add(&stmt->errs, "HY001", NULL);
+				break;
+			}
+			tds_dstr_free(&stmt->attr.qn_msgtext);
+			stmt->attr.qn_msgtext = s;
+		}
+		break;
+	case SQL_SOPT_SS_QUERYNOTIFICATION_OPTIONS:
+		if (!IS_VALID_LEN(StringLength)) {
+			odbc_errs_add(&stmt->errs, "HY090", NULL);
+			break;
+		}
+		{
+			DSTR s;
+
+			tds_dstr_init(&s);
+			if (!odbc_dstr_copy_oct(stmt->dbc, &s, StringLength, (ODBC_CHAR*) ValuePtr)) {
+				odbc_errs_add(&stmt->errs, "HY001", NULL);
+				break;
+			}
+			tds_dstr_free(&stmt->attr.qn_options);
+			stmt->attr.qn_options = s;
+		}
+		break;
 	default:
 		odbc_errs_add(&stmt->errs, "HY092", NULL);
 		break;
@@ -6539,7 +6605,7 @@ SQLSetStmtAttr(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER ValuePtr, SQLINT
 	tdsdump_log(TDS_DBG_FUNC, "SQLSetStmtAttr(%p, %d, %p, %d)\n", 
 			hstmt, (int)Attribute, ValuePtr, (int)StringLength);
 
-	return _SQLSetStmtAttr(hstmt, Attribute, ValuePtr, StringLength);
+	return _SQLSetStmtAttr(hstmt, Attribute, ValuePtr, StringLength _wide0);
 }
 
 #ifdef ENABLE_ODBC_WIDE
@@ -6549,7 +6615,7 @@ SQLSetStmtAttrW(SQLHSTMT hstmt, SQLINTEGER Attribute, SQLPOINTER ValuePtr, SQLIN
 	tdsdump_log(TDS_DBG_FUNC, "SQLSetStmtAttr(%p, %d, %p, %d)\n",
 			hstmt, (int)Attribute, ValuePtr, (int)StringLength);
 
-	return _SQLSetStmtAttr(hstmt, Attribute, ValuePtr, StringLength);
+	return _SQLSetStmtAttr(hstmt, Attribute, ValuePtr, StringLength, 1);
 }
 #endif
 #endif
@@ -6560,7 +6626,7 @@ SQLSetStmtOption(SQLHSTMT hstmt, SQLUSMALLINT fOption, SQLULEN vParam)
 	tdsdump_log(TDS_DBG_FUNC, "SQLSetStmtOption(%p, %u, %u)\n", hstmt, fOption, (unsigned)vParam);
 
 	/* XXX: Lost precision */
-	return _SQLSetStmtAttr(hstmt, (SQLINTEGER) fOption, (SQLPOINTER) (TDS_INTPTR) vParam, SQL_NTS);
+	return _SQLSetStmtAttr(hstmt, (SQLINTEGER) fOption, (SQLPOINTER) (TDS_INTPTR) vParam, SQL_NTS _wide0);
 }
 
 ODBC_FUNC(SQLSpecialColumns, (P(SQLHSTMT,hstmt), P(SQLUSMALLINT,fColType), PCHARIN(CatalogName,SQLSMALLINT),
@@ -7269,10 +7335,10 @@ SQLSetScrollOptions(SQLHSTMT hstmt, SQLUSMALLINT fConcurrency, SQLLEN crowKeyset
 		ODBC_EXIT_(stmt);
 	}
 
-	_SQLSetStmtAttr(hstmt, SQL_ATTR_CURSOR_TYPE, (SQLPOINTER) (TDS_INTPTR) cursor_type, 0);
-	_SQLSetStmtAttr(hstmt, SQL_ATTR_CONCURRENCY, (SQLPOINTER) (TDS_INTPTR) fConcurrency, 0);
-	_SQLSetStmtAttr(hstmt, SQL_ATTR_KEYSET_SIZE, (SQLPOINTER) (TDS_INTPTR) crowKeyset, 0);
-	_SQLSetStmtAttr(hstmt, SQL_ROWSET_SIZE, (SQLPOINTER) (TDS_INTPTR) crowRowset, 0);
+	_SQLSetStmtAttr(hstmt, SQL_ATTR_CURSOR_TYPE, (SQLPOINTER) (TDS_INTPTR) cursor_type, 0 _wide0);
+	_SQLSetStmtAttr(hstmt, SQL_ATTR_CONCURRENCY, (SQLPOINTER) (TDS_INTPTR) fConcurrency, 0 _wide0);
+	_SQLSetStmtAttr(hstmt, SQL_ATTR_KEYSET_SIZE, (SQLPOINTER) (TDS_INTPTR) crowKeyset, 0 _wide0);
+	_SQLSetStmtAttr(hstmt, SQL_ROWSET_SIZE, (SQLPOINTER) (TDS_INTPTR) crowRowset, 0 _wide0);
 
 	ODBC_EXIT_(stmt);
 }
diff --git a/src/tds/query.c b/src/tds/query.c
index a076255..2c1ac3e 100644
--- a/src/tds/query.c
+++ b/src/tds/query.c
@@ -312,6 +312,76 @@ tds_start_query(TDSSOCKET *tds, unsigned char packet_type)
 }
 
 /**
+ * tds72_submit_query_notification() sends a request for the server to call
+ * us back when the results of the query would have changed; although this
+ * involves sending the query to the server, the server does not execute it.
+ * \tds
+ * \param query   query to analyze for change detection. There are specific
+ *                limitations on the statement which depend on the server;
+ *                consult your server's documentation
+ * \param msgtext arbitrary extra data to be passed along with the
+ *                notification request and which will be delivered back to
+ *                the client when the notification happens
+ * \param options specification of how the notification is to be delivered.
+ *                Consult your server's documentatin for syntax
+ * \param timeout number of seconds to keep the notification request active
+ *                before automatically expiring it (with a notification)
+ * \return TDS_FAIL or TDS_SUCCESS
+ */
+TDSRET
+tds72_submit_query_notification(TDSSOCKET * tds, const char *query, const char *msgtext, const char *options, TDS_INT timeout)
+{
+	size_t query_len;
+	int qn_len;
+	const char *converted_msgtext;
+	const char *converted_options;
+	size_t converted_msgtext_len;
+	size_t converted_options_len;
+
+	CHECK_TDS_EXTRA(tds);
+	if (!query || !msgtext || !options || !IS_TDS72_PLUS(tds->conn))
+		return TDS_FAIL;
+
+	if (tds_set_state(tds, TDS_QUERYING) != TDS_QUERYING)
+		return TDS_FAIL;
+
+	converted_msgtext = tds_convert_string(tds, tds->conn->char_convs[client2ucs2], msgtext, (int)strlen(msgtext), &converted_msgtext_len);
+	if (!converted_msgtext) {
+		tds_set_state(tds, TDS_IDLE);
+		return TDS_FAIL;
+	}
+
+	converted_options = tds_convert_string(tds, tds->conn->char_convs[client2ucs2], options, (int)strlen(options), &converted_options_len);
+	if (!converted_options) {
+		tds_convert_string_free(query, converted_msgtext);
+		tds_set_state(tds, TDS_IDLE);
+		return TDS_FAIL;
+	}
+	qn_len = 2 + converted_msgtext_len + 2 + converted_options_len + 4;
+
+	query_len = strlen(query);
+	tds->out_flag = TDS_QUERY;
+	tds_put_int(tds, 4 + 18 + 6 + qn_len);         /* total length */
+	tds_put_int(tds, 18);                          /* length: transaction descriptor */
+	tds_put_smallint(tds, 2);                      /* type: transaction descriptor */
+	tds_put_n(tds, tds->conn->tds72_transaction, 8);  /* transaction */
+	tds_put_int(tds, 1);                           /* request count */
+	tds_put_int(tds, 6 + qn_len);                  /* length: query notification */
+	tds_put_smallint(tds, 1);                      /* type: query notification */
+	tds_put_smallint(tds, converted_msgtext_len);  /* notifyid */
+	tds_put_n(tds, converted_msgtext, converted_msgtext_len);
+	tds_put_smallint(tds, converted_options_len);  /* ssbdeployment */
+	tds_put_n(tds, converted_options, converted_options_len);
+	tds_put_int(tds, timeout);                     /* timeout */
+	tds_put_string(tds, query, (int)query_len);
+
+	tds_convert_string_free(query, converted_msgtext);
+	tds_convert_string_free(query, converted_options);
+
+	return tds_query_flush_packet(tds);
+}
+
+/**
  * tds_submit_query_params() sends a language string to the database server for
  * processing.  TDS 4.2 is a plain text message with a packet type of 0x01,
  * TDS 7.0 is a unicode string with packet type 0x01, and TDS 5.0 uses a
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.