//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
// $NoKeywords: $
//=============================================================================


#include "stdafx.h"

// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"

namespace GCSDK
{
const char *GetInsertArgString()
{
	static char s_str[1024];
	static bool s_bInit = false;

	if ( !s_bInit )
	{
		for ( int i = 0; i < 1023; i++ )
		{
			s_str[i] = i % 2 == 0 ? '?' : ',';
		}

		s_str[1023] = NULL;
		s_bInit = true;
	}

	return s_str;
}

uint32 GetInsertArgStringChars( uint32 nNumParams )
{
	AssertMsg( nNumParams <= GetInsertArgStringMaxParams(), "Error: Requested more characters than are provided by the GetInsertArgString" );
	if( nNumParams == 0 )
		return 0;

	return nNumParams * 2 - 1;
}

uint32 GetInsertArgStringMaxParams()
{
	return 512;
}

//-----------------------------------------------------------------------------
// Purpose: Converts array of field data to text for SQL IN clause
// Input:	columnInfo - schema of column being converted
//			pubData - pointer to array of data to convert
//			cubData - size of array of data
//			rgchResult - pointer to output buffer
//			cubResultLen - size of output buffer
//			bForPreparedStatement - Should we prepare the text for a prepared statement or directly place the values?
//-----------------------------------------------------------------------------
void ConvertFieldArrayToInText( const CColumnInfo &columnInfo, byte *pubData, int cubData, char *rgchResult, int cubResultLen, bool bForPreparedStatement )
{
	int32 cubLength = columnInfo.GetFixedSize();
	Assert( cubData % cubLength == 0 );
	int32 nArrayCount = cubData / cubLength;

	int32 len = 0;
	rgchResult[len++] = '(';
	for( int i = 0; i < nArrayCount; ++i )
	{
		if ( bForPreparedStatement )
		{
			if ( i < nArrayCount - 1 )
			{
				rgchResult[len++] = '?';
				rgchResult[len++] = ',';	
			}
			else
			{
				rgchResult[len++] = '?';
				rgchResult[len++] = ')';	
			}
		}
		else
		{
			switch ( columnInfo.GetType() )
			{
			case k_EGCSQLType_int8:
				if ( i < nArrayCount - 1 )
					len += Q_snprintf( rgchResult + len, cubResultLen - len, "%d,", *( (byte *) pubData ) );
				else
					len += Q_snprintf( rgchResult + len, cubResultLen - len, "%d)", *( (byte *) pubData ) );
				break;
			case k_EGCSQLType_int16:
				if ( i < nArrayCount - 1 )
					len += Q_snprintf( rgchResult + len, cubResultLen - len, "%d,", *( (short *) pubData ) );
				else
					len += Q_snprintf( rgchResult + len, cubResultLen - len, "%d)", *( (short *) pubData ) );
				break;
			case k_EGCSQLType_int32:
				if ( i < nArrayCount - 1 )
					len += Q_snprintf( rgchResult + len, cubResultLen - len, "%d,", *( (int *) pubData ) );
				else
					len += Q_snprintf( rgchResult + len, cubResultLen - len, "%d)", *( (int *) pubData ) );
				break;
			case k_EGCSQLType_int64:
				if ( i < nArrayCount - 1 )
					len += Q_snprintf( rgchResult + len, cubResultLen - len, "%lld,", *( (int64 *) pubData ) );
				else
					len += Q_snprintf( rgchResult + len, cubResultLen - len, "%lld)", *( (int64 *) pubData ) );
				break;
			default:
				AssertMsg( false, "Unsupported data type for non prepares statement with IN clause\n" );
				rgchResult[0] = 0;
				return;
			}
		}

		if( len >= cubResultLen - 1 )
		{
			AssertMsg( false, "Generation of IN clause foverflowed\n" );
			rgchResult[0] = 0;
			return;
		}
		pubData += cubLength;
	}

	rgchResult[len] = 0;
	return;
}


//-----------------------------------------------------------------------------
// Purpose: Converts field data to text equivalent for SQL statement
// Input:	eFieldType - The type of the field to convert to text
//			pubRecord - pointer to record data to convert
//			cubRecord - size of record data
//			rgchField - pointer to output buffer
//			cchField - size of output buffer
//-----------------------------------------------------------------------------
void ConvertFieldToText( EGCSQLType eFieldType, uint8 *pubRecord, int cubRecord, char *rgchField, int cchField, bool bQuoteString )
{
	char rgchTmp[k_cMedBuff];

	switch ( eFieldType )
	{
	case k_EGCSQLType_int8:
		Q_snprintf( rgchField, cchField, "%d", *( (byte *) pubRecord ) );
		break;
	case k_EGCSQLType_int16:
		Q_snprintf( rgchField, cchField, "%d", *( (short *) pubRecord ) );
		break;
	case k_EGCSQLType_int32:
		Q_snprintf( rgchField, cchField, "%d", *( (int *) pubRecord ) );
		break;
	case k_EGCSQLType_int64:
		Q_snprintf( rgchField, cchField, "%lld", *( (int64 *) pubRecord ) );
		break;
	case k_EGCSQLType_float:
		Q_snprintf( rgchField, cchField, "%f", *((float*) pubRecord) );
			break;
	case k_EGCSQLType_double:
		Q_snprintf( rgchField, cchField, "%f", *((double*) pubRecord) );
		break;
	case k_EGCSQLType_String:
		if ( pubRecord && *pubRecord )
		{
			Assert( cubRecord + 1 < Q_ARRAYSIZE( rgchTmp ) );

			Q_memcpy( rgchTmp, (char *) pubRecord, cubRecord );
			rgchTmp[cubRecord] = 0;

			if ( bQuoteString )
			{
				EscapeStringValue( rgchTmp, Q_ARRAYSIZE( rgchTmp ) );
				Q_snprintf( rgchField, cchField, "'%s'", rgchTmp );
			}
			else
			{
				Q_strncpy( rgchField, rgchTmp, cchField );
			}
		}
		else
		{
			if ( bQuoteString )
			{
				Q_strncpy( rgchField, "''", cchField );
			}
			else
			{
				Q_strncpy( rgchField, "", cchField );
			}
		}
		break;
	case k_EGCSQLType_Blob:
	case k_EGCSQLType_Image:
		Q_strncpy( rgchField, "0x", cchField );
		Q_binarytohex( pubRecord, cubRecord, rgchField + 2, cchField - 2 );
		break;
	default:
		Assert( false );
		break;
	}
}


//-----------------------------------------------------------------------------
// Purpose: Returns the text SQL type for a given field
// Input:	field - field to determine type for
//			pchBuf - pointer to output buffer
//			cchBuf - size of output buffer
// Output:	returns pchBuf for convenience of one-line usage
//-----------------------------------------------------------------------------
char *SQLTypeFromField( const CColumnInfo &colInfo, char *pchBuf, int cchBuf )
{
	EGCSQLType eType = colInfo.GetType();
	*pchBuf = 0;
	switch ( eType )
	{
	case k_EGCSQLType_int8:
		Q_strncpy( pchBuf, "TINYINT", cchBuf );
		break;
	case k_EGCSQLType_int16:
		Q_strncpy( pchBuf, "SMALLINT", cchBuf );
		break;
	case k_EGCSQLType_int32:
		Q_strncpy( pchBuf, "INT", cchBuf );
		break;
	case k_EGCSQLType_int64:
		Q_strncpy( pchBuf, "BIGINT", cchBuf );
		break;
	case k_EGCSQLType_float:
		Q_strncpy( pchBuf, "REAL", cchBuf );
		break;
	case k_EGCSQLType_double:
		Q_strncpy( pchBuf, "FLOAT", cchBuf );
		break;
	case k_EGCSQLType_String:
		Q_snprintf( pchBuf, cchBuf, "VARCHAR(%d)", colInfo.GetMaxSize() );
		break;
	case k_EGCSQLType_Blob:
		Q_snprintf( pchBuf, cchBuf, "VARBINARY(%d)", colInfo.GetMaxSize() );
		break;
	case k_EGCSQLType_Image:
		Q_strncpy( pchBuf, "IMAGE", cchBuf );
		break;
	default:
		Assert( false );
		break;
	}

	return pchBuf;
}



//-----------------------------------------------------------------------------
// Purpose: Escapes any single quotes to a string value to double single quotes
// Input:	rgchField - text to escape
//			cchField - size of text buffer
// Notes:	The text will be escaped and expanded in place in the buffer.
//			In the worst case, the text may expand by 2x.  (If the field is all
//			single quotes.)  So, you must pass in a buffer which is at least
//			twice as long as the text length so we can guarantee to be able to
//			escape the string.
//-----------------------------------------------------------------------------
void EscapeStringValue( char *rgchField, int cchField )
{
	// TODO - what else do we need to escape?  %() ...
	char *pubCur = rgchField;
	int nLen = 0;
	int cSingleQuotes = 0;

	// This function gets called on every text field we write but most text fields
	// don't need to be escaped, so try to be as fast as possible in the normal case.

	// first, walk through the string and count the string length and number of single quotes
	while ( *pubCur )
	{
		if ( '\'' == *pubCur )
			cSingleQuotes++;
		nLen ++;
		pubCur++;
	}

	// if no single quotes, nothing to do
	if ( !cSingleQuotes )
		return;

	// caller must pass in a buffer that's long enough for expansion
	Assert( nLen + cSingleQuotes + 1 <= cchField );
	if ( !( nLen + cSingleQuotes + 1 <= cchField ) )
		return;

	// We know exactly how many characters the string will expand by (the # of single quotes).  Walk backward
	// and copy the characters into the right places.  This touches each character only once.
	pubCur = rgchField + nLen + cSingleQuotes;
	*pubCur = 0;
	pubCur--;
	while ( pubCur > rgchField && cSingleQuotes > 0 )
	{
		// read pointer is offset from write pointer by # of remaining single quotes
		char *pubRead = pubCur - cSingleQuotes;
		Assert( pubRead >= rgchField );
		// copy each character
		*pubCur = *pubRead;
		if ( '\'' == *pubRead )
		{
			// if the character is a single quote, back up one more and insert another single quote to escape it
			pubCur --;
			*pubCur = '\'';
			// decrement # of single quotes remaining
			cSingleQuotes --;
			Assert( cSingleQuotes >= 0 );
		}
		pubCur--;
	}
}
//-----------------------------------------------------------------------------
// Purpose: Adds constraint information to a SQL command to add or remove constraint
// Input:	pchTableName - name of table
//			pchColumnName - name of column
//			nColFlagConstraint - flag with which constraint to 
//			bForAdd - whether constraint is being added or removed
//			pchCmd - buffer to append SQL command to
//			cchCmd - size of buffer
//-----------------------------------------------------------------------------
void AppendConstraint( const char *pchTableName, const char *pchColumnName, int nColFlagConstraint, bool bForAdd, 
					  bool bClustered, CFmtStrMax & sCmd, int nFillFactor )
{
	Assert( pchTableName && pchTableName[0] );
	Assert( pchColumnName && pchColumnName[0] );

	switch ( nColFlagConstraint )
	{
	case k_nColFlagPrimaryKey:
		sCmd.AppendFormat( " CONSTRAINT %s_%s_PrimaryKey", pchTableName, pchColumnName);
		if ( bForAdd )
		{
			sCmd += " PRIMARY KEY ";
			if ( bClustered )
			{
				sCmd.AppendFormat( " CLUSTERED WITH (FILLFACTOR = %d) ", nFillFactor );
			}
			else
			{
				sCmd += "NONCLUSTERED";
			}
		}
		break;
	case k_nColFlagUnique:
		/* do nothing - the uniqueness will be handled by creation of an index */
		break;
	case k_nColFlagAutoIncrement:
		sCmd += " IDENTITY";
		break;
	default:
		AssertMsg( false, "CSQLThread::AppendContraint: invalid constraint type" );
		break;
	}
}


//-----------------------------------------------------------------------------
// Purpose: Adds constraint information to a SQL command to add or remove constraint
// Input:	pRecordInfo - record info describing table
//			pColumnInfo - record info describing column
//			bForAdd - whether constraint is being added or removed
//			pchCmd - buffer to append SQL command to
//			cchCmd - size of buffer
//-----------------------------------------------------------------------------
void AppendConstraints( const CRecordInfo *pRecordInfo, const CColumnInfo *pColumnInfo, bool bForAdd, CFmtStrMax & sCmd )
{
	Assert( pRecordInfo != NULL );
	Assert( pColumnInfo != NULL );

	if ( pColumnInfo->BIsPrimaryKey() )
	{
		// any column in a PK can't be NULL.
		if ( bForAdd )
		{
			sCmd += " NOT NULL";
		}

		// only add primary key constraint here if it is a single-column PK
		if ( pRecordInfo->GetPrimaryKeyType() == k_EPrimaryKeyTypeSingle )
		{
			// get the fields on the primary key
			const CUtlVector< FieldSet_t > &refFields = pRecordInfo->GetIndexFields( );
			int nFillFactor = refFields.Element( pRecordInfo->GetPKIndex() ).GetFillFactor();
			AppendConstraint( pRecordInfo->GetName(), pColumnInfo->GetName(), k_nColFlagPrimaryKey, bForAdd, pColumnInfo->BIsClustered(), sCmd, nFillFactor );
		}
	}
	else if ( pColumnInfo->BIsUnique() )
	{
		AppendConstraint( pRecordInfo->GetName(), pColumnInfo->GetName(), k_nColFlagUnique, bForAdd, pColumnInfo->BIsClustered(), sCmd, 0 );
	}
	
	if ( pColumnInfo->BIsAutoIncrement() )
	{
		AppendConstraint( pRecordInfo->GetName(), pColumnInfo->GetName(), k_nColFlagAutoIncrement, bForAdd, pColumnInfo->BIsClustered(), sCmd, 0 );
	}
}

//-----------------------------------------------------------------------------
// Purpose: Generates the "CONSTRAINT ..." text for the table primary key
//-----------------------------------------------------------------------------
void BuildTablePKConstraintText( TSQLCmdStr *psStatement, CRecordInfo *pRecordInfo )
{
	const FieldSet_t& vecFields = pRecordInfo->GetPKFields( );

	psStatement->sprintf( "CONSTRAINT %s_PrimaryKey PRIMARY KEY %s ( ",
		pRecordInfo->GetName(), 
		vecFields.IsClustered() ? "CLUSTERED" : "NONCLUSTERED" );

	for ( int nField = 0; nField < vecFields.GetCount(); nField++ )
	{
		// what field is the next column in our index?
		int nThisField = vecFields.GetField( nField );
		const CColumnInfo& columnInfo = pRecordInfo->GetColumnInfo(nThisField);

		if (nField != 0)
		{
			*psStatement += ", ";
		}
		*psStatement += columnInfo.GetName();
	}

	// close our list
	*psStatement += ") ";

	if ( vecFields.GetFillFactor() != 0 )
	{
		// non-default fill factor, so specify it
		psStatement->AppendFormat( " WITH FILLFACTOR = %d ",
			vecFields.GetFillFactor() );
	}
}

//-----------------------------------------------------------------------------
// Purpose: Adds constraint information to a SQL command to add or remove table-level constraints
// Input:	pRecordInfo - record info describing table
//			pchCmd - buffer to append SQL command to
//			cchCmd - size of buffer
//-----------------------------------------------------------------------------

void AppendTableConstraints( CRecordInfo *pRecordInfo, CFmtStrMax & sCmd )
{
	// the only supported table constraint is for PKs or FKs
	if ( pRecordInfo->GetPrimaryKeyType() == k_EPrimaryKeyTypeMulti )
	{
		TSQLCmdStr tmp;
		BuildTablePKConstraintText( &tmp, pRecordInfo );
		sCmd += ", ";
		sCmd += tmp;
	}

	// Look for FKs required on this table
	// the only supported table constraint is for PKs or FKs
	int cFKs = pRecordInfo->GetFKCount();
	for( int i=0; i < cFKs; ++i )
	{
		FKData_t &fkData = pRecordInfo->GetFKData( i );

		CFmtStr sColumns, sParentColumns;
		FOR_EACH_VEC( fkData.m_VecColumnRelations, nCol )
		{
			FKColumnRelation_t &colRelation = fkData.m_VecColumnRelations[nCol];
			if ( nCol > 0)
			{
				sColumns += ",";
				sParentColumns += ",";
			}
			sColumns += colRelation.m_rgchCol;
			sParentColumns += colRelation.m_rgchParentCol;
		}

		TSQLCmdStr sTmp;
		sTmp.sprintf( ", CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s) ON DELETE %s ON UPDATE %s",
			fkData.m_rgchName, sColumns.Access(), fkData.m_rgchParentTableName, sParentColumns.Access(),
			PchNameFromEForeignKeyAction( fkData.m_eOnDeleteAction ), PchNameFromEForeignKeyAction( fkData.m_eOnUpdateAction ) );

		// add to the command
		sCmd += sTmp;
	}
}




//-----------------------------------------------------------------------------
// Purpose: Builds a SQL INSERT statement
// Input:	psStatement - The string to put the statement into
//			pRecordInfo - record info describing table inserting into
//-----------------------------------------------------------------------------
void BuildInsertStatementText( TSQLCmdStr *psStatement, const CRecordInfo *pRecordInfo )
{
	psStatement->sprintf("INSERT INTO %s.%s (", GSchemaFull().GetDefaultSchemaNameForCatalog( pRecordInfo->GetESchemaCatalog() ), pRecordInfo->GetName() );

	// build a string of the field names
	int cColumns = pRecordInfo->GetNumColumns();
	int nInsertable = 0;
	bool bAddedBefore = false;
	for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
	{
		const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn );
		if ( !columnInfo.BIsInsertable() )
			continue;

		nInsertable++;

		if ( bAddedBefore )
			psStatement->Append( ',' );
		bAddedBefore = true;
		psStatement->Append( columnInfo.GetName() );
	}
	
	psStatement->AppendFormat( ") VALUES (%.*s)", GetInsertArgStringChars( nInsertable ), GetInsertArgString() );
}


//-----------------------------------------------------------------------------
// Purpose: Builds a SQL INSERT statement
//			IMPORTANT NOTE - This Insert statement will use the Microsoft SQL Server 
//			specific clause 'OUTPUT Inserted.ColumnName' 
//			The result of that will be that the SQL statement will return to us 
//			the columns that could not be specified by the Insert.
//			At the time of writing, that is primarily AutoIncrement columns,
//			however in theory we should be able to recover any computed column 
//			from SQL server, with the caveats specified at : 
//			http://msdn.microsoft.com/en-us/library/ms177564.aspx 
//
// Input:	psStatement - The output statement string
//			pRecordInfo - record info describing table inserting into
//-----------------------------------------------------------------------------

void BuildInsertAndReadStatementText( TSQLCmdStr *psStatement, CUtlVector<int> *pvecOutputFields, const CRecordInfo *pRecordInfo )
{
	psStatement->sprintf("INSERT INTO %s.%s (", GSchemaFull().GetDefaultSchemaNameForCatalog( pRecordInfo->GetESchemaCatalog() ), pRecordInfo->GetName() );

	// build a string of the field names
	int nInsertable = 0;
	int cColumns = pRecordInfo->GetNumColumns();
	bool bAddedBefore = false;
	for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
	{
		const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn );
		if ( !columnInfo.BIsInsertable() )
			continue;

		nInsertable++;

		if ( bAddedBefore )
			psStatement->Append( ',' );
		bAddedBefore = true;
		psStatement->Append( columnInfo.GetName() );
	}

	bAddedBefore = false ;	
	int nOutputColumn = 0;
	for( int iColumn = 0; iColumn < cColumns; iColumn++ )
	{
		const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn ) ;
	
		//
		//	If we can't Insert it - we want SQL Server to tell us what value was stored
		//	in the column !!
		//
		if( !columnInfo.BIsInsertable() )
		{
			if( bAddedBefore )
				psStatement->Append( ", INSERTED." );
			else
				psStatement->Append( ") OUTPUT INSERTED." );
			bAddedBefore = true ;
			psStatement->Append( columnInfo.GetName() );
			pvecOutputFields->AddToTail( iColumn );
			nOutputColumn++;
		}
	}

	// add field values to SQL statement
	psStatement->AppendFormat( " VALUES (%.*s)", GetInsertArgStringChars( nInsertable ), GetInsertArgString() );
}


//-----------------------------------------------------------------------------
// Purpose: Builds a SQL MERGE statement update or insert using in-flight values table
// Input:	psStatement - The string to put the statement into
//			pRecordInfo - record info describing table inserting into
//-----------------------------------------------------------------------------
void BuildMergeStatementTextOnPKWhenMatchedUpdateWhenNotMatchedInsert( TSQLCmdStr *psStatement, const CRecordInfo *pRecordInfo )
{
	psStatement->sprintf( "MERGE INTO %s.%s WITH( HOLDLOCK, ROWLOCK ) T USING ( VALUES (%.*s) ) AS S(",
		GSchemaFull().GetDefaultSchemaNameForCatalog( pRecordInfo->GetESchemaCatalog() ), pRecordInfo->GetName(),
		GetInsertArgStringChars( pRecordInfo->GetNumColumns() ), GetInsertArgString() );

	{
		int cColumns = pRecordInfo->GetNumColumns();
		for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
		{
			const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn );
			if ( iColumn )
				psStatement->Append( ',' );
			psStatement->Append( columnInfo.GetName() );
		}
	}

	psStatement->Append( ") ON " );

	// build a string of the PK columns
	const FieldSet_t &fsPK = pRecordInfo->GetIndexFields()[pRecordInfo->GetPKIndex()];
	{
		int cColumns = fsPK.GetCount();
		for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
		{
			const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( fsPK.GetField( iColumn ) );
			if ( iColumn )
				psStatement->Append( " AND " );
			psStatement->Append( "T." );
			psStatement->Append( columnInfo.GetName() );
			psStatement->Append( "=S." );
			psStatement->Append( columnInfo.GetName() );
		}
	}

	psStatement->Append( " WHEN MATCHED THEN UPDATE SET " );

	// build the update string
	{
		int cColumns = pRecordInfo->GetNumColumns();
		bool bAddedBefore = false;
		for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
		{
			bool bThisColumnIsPartOfPK = false;
			for ( int ipkCheck = 0; ipkCheck < fsPK.GetCount(); ++ipkCheck )
			{
				if ( iColumn == fsPK.GetField( ipkCheck ) )
				{
					bThisColumnIsPartOfPK = true;
					break;
				}
			}
			if ( bThisColumnIsPartOfPK )
				continue;

			const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn );
			if ( bAddedBefore )
				psStatement->Append( ',' );
			bAddedBefore = true;
			psStatement->Append( columnInfo.GetName() );
			psStatement->Append( "=S." );
			psStatement->Append( columnInfo.GetName() );
		}
	}

	psStatement->Append( " WHEN NOT MATCHED BY TARGET THEN INSERT (" );

	// build a string of the field names
	{
		int cColumns = pRecordInfo->GetNumColumns();
		bool bAddedBefore = false;
		for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
		{
			const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn );
			if ( !columnInfo.BIsInsertable() )
				continue;

			if ( bAddedBefore )
				psStatement->Append( ',' );
			bAddedBefore = true;
			psStatement->Append( columnInfo.GetName() );
		}
	}

	psStatement->Append( ") VALUES (" );
	{
		int cColumns = pRecordInfo->GetNumColumns();
		bool bAddedBefore = false;
		for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
		{
			const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn );
			if ( !columnInfo.BIsInsertable() )
				continue;

			if ( bAddedBefore )
				psStatement->Append( ',' );
			bAddedBefore = true;
			psStatement->Append( "S." );
			psStatement->Append( columnInfo.GetName() );
		}
	}
	psStatement->Append( ");" );
}


//-----------------------------------------------------------------------------
// Purpose: Builds a SQL MERGE statement using CTE_MergeParams as supplied table holding rows
// Input:	psStatement - The string to put the statement into
//			pRecordInfo - record info describing table inserting into
//-----------------------------------------------------------------------------
void BuildMergeStatementTextOnPKWhenNotMatchedInsert( TSQLCmdStr *psStatement, const CRecordInfo *pRecordInfo )
{
	psStatement->sprintf( "MERGE INTO %s.%s WITH( HOLDLOCK, ROWLOCK ) T USING ( VALUES (%.*s) ) AS S(",
		GSchemaFull().GetDefaultSchemaNameForCatalog( pRecordInfo->GetESchemaCatalog() ), pRecordInfo->GetName(),
		GetInsertArgStringChars( pRecordInfo->GetNumColumns() ), GetInsertArgString() );

	{
		int cColumns = pRecordInfo->GetNumColumns();
		for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
		{
			const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn );
			if ( iColumn )
				psStatement->Append( ',' );
			psStatement->Append( columnInfo.GetName() );
		}
	}

	psStatement->Append( ") ON " );

	// build a string of the PK columns
	const FieldSet_t &fsPK = pRecordInfo->GetIndexFields()[pRecordInfo->GetPKIndex()];
	{
		int cColumns = fsPK.GetCount();
		for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
		{
			const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( fsPK.GetField( iColumn ) );
			if ( iColumn )
				psStatement->Append( " AND " );
			psStatement->Append( "T." );
			psStatement->Append( columnInfo.GetName() );
			psStatement->Append( "=S." );
			psStatement->Append( columnInfo.GetName() );
		}
	}

	psStatement->Append( " WHEN NOT MATCHED BY TARGET THEN INSERT (" );

	// build a string of the field names
	{
		int cColumns = pRecordInfo->GetNumColumns();
		bool bAddedBefore = false;
		for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
		{
			const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn );
			if ( !columnInfo.BIsInsertable() )
				continue;

			if ( bAddedBefore )
				psStatement->Append( ',' );
			bAddedBefore = true;
			psStatement->Append( columnInfo.GetName() );
		}
	}

	psStatement->Append( ") VALUES (" );
	{
		int cColumns = pRecordInfo->GetNumColumns();
		bool bAddedBefore = false;
		for ( int iColumn = 0; iColumn < cColumns; iColumn++ )
		{
			const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn );
			if ( !columnInfo.BIsInsertable() )
				continue;

			if ( bAddedBefore )
				psStatement->Append( ',' );
			bAddedBefore = true;
			psStatement->Append( "S." );
			psStatement->Append( columnInfo.GetName() );
		}
	}
	psStatement->Append( ");" );
}

void BuildSelectStatementText( TSQLCmdStr *psStatement, const CColumnSet & selectSet, const char *pchTopClause )
{
	*psStatement = "SELECT ";

	if( pchTopClause )
	{
		psStatement->Append( pchTopClause );
		psStatement->Append( ' ' );
	}

	// build a string of the field names
	bool bAddedBefore = false;
	FOR_EACH_COLUMN_IN_SET( selectSet, nColumnIndex )
	{
		const CColumnInfo &columnInfo = selectSet.GetColumnInfo( nColumnIndex );
		if ( bAddedBefore )
			psStatement->Append( ',' );
		bAddedBefore = true;
		psStatement->Append( columnInfo.GetName() );
	}

	psStatement->Append( " FROM ");
	psStatement->Append( GSchemaFull().GetDefaultSchemaNameForCatalog( selectSet.GetRecordInfo()->GetESchemaCatalog() ) );
	psStatement->Append( '.' );
	psStatement->Append( selectSet.GetRecordInfo()->GetName() );
}


//-----------------------------------------------------------------------------
// Purpose: Builds a SQL UPDATE statement
// Input:	pRecordInfo - record info describing table inserting into
//			bForPreparedStatement - if true, inserts values as '?' for later
//				binding.  If false, values are inserted in text.
//			pchStatement - pointer to buffer to build statement in
//			cchStatement - size of buffer
//			pSQLRecord - pointer to record with data to update
//			iColumnMatch - column to use for WHERE condition
//			pvMatch - data value to use for WHERE condition
//			cubMatch - size of pvMatch data
//			rgiColumnUpdate - array of column #'s to update
//			ciColumnUpdate - count of column #'s to update
//-----------------------------------------------------------------------------
void BuildUpdateStatementText( TSQLCmdStr *psStatement, const CColumnSet & updateColumns )
{
	// build the UPDATE statement
	psStatement->sprintf( "UPDATE %s.%s SET ", GSchemaFull().GetDefaultSchemaNameForCatalog( updateColumns.GetRecordInfo()->GetESchemaCatalog() ), updateColumns.GetRecordInfo()->GetName() );

	// add each field we're updating to the UPDATE statement
	FOR_EACH_COLUMN_IN_SET( updateColumns, nColumnIndex )
	{
		const CColumnInfo &columnInfo = updateColumns.GetColumnInfo( nColumnIndex );

		if( nColumnIndex > 0 )
			psStatement->Append( ',' );
		psStatement->Append( columnInfo.GetName() );
		psStatement->Append( "=?" );
	}
}


//-----------------------------------------------------------------------------
// Purpose: Builds a SQL UPDATE statement
//-----------------------------------------------------------------------------
void BuildDeleteStatementText( TSQLCmdStr *psStatement, const CRecordInfo *pRecordInfo )
{
	psStatement->sprintf( "DELETE FROM %s.%s", GSchemaFull().GetDefaultSchemaNameForCatalog( pRecordInfo->GetESchemaCatalog() ), pRecordInfo->GetName() );
}


//-----------------------------------------------------------------------------
// Purpose: Builds a where clause for the provided fields
//-----------------------------------------------------------------------------
void AppendWhereClauseText( TSQLCmdStr *psClause, const CColumnSet & columnSet )
{
	// add each field we're updating to the UPDATE statement
	FOR_EACH_COLUMN_IN_SET( columnSet, nColumnIndex )
	{
		const CColumnInfo &columnInfo = columnSet.GetColumnInfo( nColumnIndex );

		if( nColumnIndex > 0 )
			psClause->Append( " AND ");
		psClause->Append( columnInfo.GetName() );
		psClause->Append( "=?" );
	}
}

//-----------------------------------------------------------------------------
// Purpose: Builds an OUTPUT [fields] INTO [table] for the provided fields/data
//-----------------------------------------------------------------------------
void BuildOutputClauseText( TSQLCmdStr *psClause, const CColumnSet & columnSet )
{
	*psClause = " OUTPUT ";

	FOR_EACH_COLUMN_IN_SET( columnSet, nColumnIndex )
	{
		const CColumnInfo &columnInfo = columnSet.GetColumnInfo( nColumnIndex );

		if( nColumnIndex > 0 )
			psClause->Append( ", ");
		
		psClause->Append( " ? AS " );
		psClause->Append( columnInfo.GetName() );
	}

	psClause->Append( " INTO " );
	psClause->Append( columnSet.GetRecordInfo()->GetName() );
}

////-----------------------------------------------------------------------------
//// Purpose: our own special "upsert" into a column with a uniqueness constraint
////-----------------------------------------------------------------------------
//EResult UpdateOrInsertUnique( CSQLAccess &sqlAccess, int iTable, int iField, CRecordBase *pRecordBase, int iIndexID )
//{
//	// attempt an update - if it fails due to duplicate primary key, they can't use this
//	// url (it's taken) - if it succeeds but affects 0 rows, they didn't have a vanity url
//	// and we need to do an insert (which could again fail due to primary key constraints)
//	int cRecordsUpdated = 0;	
//	bool bRet = sqlAccess.BYieldingUpdateFieldFromRecordWithIndex( iTable, &cRecordsUpdated, iField, pRecordBase, iIndexID );
//	if ( !bRet )
//	{ 
//		// ODBC is the suck - give me Spring JDBC templates, please.
//		if ( sqlAccess.GetLastError()->IsDuplicateInsertAttempt() )  
//		{
//			return k_EResultDuplicateName;
//		}
//		return k_EResultFail;
//	}
//	else if ( 0 == cRecordsUpdated )
//	{
//		// the user didn't have an entry, so insert one.
//		bRet = sqlAccess.BYieldingInsertRecord( iTable, pRecordBase );
//		if ( !bRet )
//		{ 
//			// ODBC is the suck - give me Spring JDBC templates, please.
//			if ( sqlAccess.GetLastError()->IsDuplicateInsertAttempt() )  
//			{
//				return k_EResultDuplicateName;
//			}
//			return k_EResultFail;
//		}
//	}
//	return k_EResultOK;
//}
//

} // namespace GCSDK