diff options
| author | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
|---|---|---|
| committer | FluorescentCIAAfricanAmerican <[email protected]> | 2020-04-22 12:56:21 -0400 |
| commit | 3bf9df6b2785fa6d951086978a3e66f49427166a (patch) | |
| tree | 2c0f1f0c63c4832882bc93814ebd2c2b1c6224e5 /gcsdk/sqlaccess | |
| download | archived-source-engine-2018-hl2-src-3bf9df6b2785fa6d951086978a3e66f49427166a.tar.xz archived-source-engine-2018-hl2-src-3bf9df6b2785fa6d951086978a3e66f49427166a.zip | |
Diffstat (limited to 'gcsdk/sqlaccess')
| -rw-r--r-- | gcsdk/sqlaccess/columnset.cpp | 368 | ||||
| -rw-r--r-- | gcsdk/sqlaccess/record.cpp | 856 | ||||
| -rw-r--r-- | gcsdk/sqlaccess/recordinfo.cpp | 915 | ||||
| -rw-r--r-- | gcsdk/sqlaccess/schema.cpp | 1505 | ||||
| -rw-r--r-- | gcsdk/sqlaccess/schemafull.cpp | 409 | ||||
| -rw-r--r-- | gcsdk/sqlaccess/schemaupdate.cpp | 1696 | ||||
| -rw-r--r-- | gcsdk/sqlaccess/sqlaccess.cpp | 953 | ||||
| -rw-r--r-- | gcsdk/sqlaccess/sqlrecord.cpp | 538 | ||||
| -rw-r--r-- | gcsdk/sqlaccess/sqlutil.cpp | 918 |
9 files changed, 8158 insertions, 0 deletions
diff --git a/gcsdk/sqlaccess/columnset.cpp b/gcsdk/sqlaccess/columnset.cpp new file mode 100644 index 0000000..4798aa1 --- /dev/null +++ b/gcsdk/sqlaccess/columnset.cpp @@ -0,0 +1,368 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Sets of columns in SQL queries +// +// $NoKeywords: $ +//============================================================================= + +#include "stdafx.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +namespace GCSDK +{ + +//----------------------------------------------------------------------------- +// Purpose: Constructs a column set with no columns in it +//----------------------------------------------------------------------------- +CColumnSet::CColumnSet( const CRecordInfo *pRecordInfo ) +: m_pRecordInfo( pRecordInfo ) +{ +} + + +//----------------------------------------------------------------------------- +// Purpose: Constructs a column set with a single column in it +// Inputs: nColumn - the column to add +//----------------------------------------------------------------------------- + +CColumnSet::CColumnSet( const CRecordInfo *pRecordInfo, int col1 ) + : m_pRecordInfo( pRecordInfo ) +{ + m_vecColumns.AddToTail( col1 ); +} + +CColumnSet::CColumnSet( const CRecordInfo *pRecordInfo, int col1, int col2 ) + : m_pRecordInfo( pRecordInfo ) +{ + m_vecColumns.EnsureCapacity( 2 ); + m_vecColumns.AddToTail( col1 ); + m_vecColumns.AddToTail( col2 ); +} + +CColumnSet::CColumnSet( const CRecordInfo *pRecordInfo, int col1, int col2, int col3 ) + : m_pRecordInfo( pRecordInfo ) +{ + m_vecColumns.EnsureCapacity( 3 ); + m_vecColumns.AddToTail( col1 ); + m_vecColumns.AddToTail( col2 ); + m_vecColumns.AddToTail( col3 ); +} + +CColumnSet::CColumnSet( const CRecordInfo *pRecordInfo, int col1, int col2, int col3, int col4 ) + : m_pRecordInfo( pRecordInfo ) +{ + m_vecColumns.EnsureCapacity( 4 ); + m_vecColumns.AddToTail( col1 ); + m_vecColumns.AddToTail( col2 ); + m_vecColumns.AddToTail( col3 ); + m_vecColumns.AddToTail( col4 ); +} + +CColumnSet::CColumnSet( const CRecordInfo *pRecordInfo, int col1, int col2, int col3, int col4, int col5 ) + : m_pRecordInfo( pRecordInfo ) +{ + m_vecColumns.EnsureCapacity( 5 ); + m_vecColumns.AddToTail( col1 ); + m_vecColumns.AddToTail( col2 ); + m_vecColumns.AddToTail( col3 ); + m_vecColumns.AddToTail( col4 ); + m_vecColumns.AddToTail( col5 ); +} + +CColumnSet::CColumnSet( const CRecordInfo *pRecordInfo, int col1, int col2, int col3, int col4, int col5, int col6 ) + : m_pRecordInfo( pRecordInfo ) +{ + m_vecColumns.EnsureCapacity( 6 ); + m_vecColumns.AddToTail( col1 ); + m_vecColumns.AddToTail( col2 ); + m_vecColumns.AddToTail( col3 ); + m_vecColumns.AddToTail( col4 ); + m_vecColumns.AddToTail( col5 ); + m_vecColumns.AddToTail( col6 ); +} + +CColumnSet::CColumnSet( const CRecordInfo *pRecordInfo, int col1, int col2, int col3, int col4, int col5, int col6, int col7 ) + : m_pRecordInfo( pRecordInfo ) +{ + m_vecColumns.EnsureCapacity( 7 ); + m_vecColumns.AddToTail( col1 ); + m_vecColumns.AddToTail( col2 ); + m_vecColumns.AddToTail( col3 ); + m_vecColumns.AddToTail( col4 ); + m_vecColumns.AddToTail( col5 ); + m_vecColumns.AddToTail( col6 ); + m_vecColumns.AddToTail( col7 ); +} + +CColumnSet::CColumnSet( const CRecordInfo *pRecordInfo, int col1, int col2, int col3, int col4, int col5, int col6, int col7, int col8 ) +: m_pRecordInfo( pRecordInfo ) +{ + m_vecColumns.EnsureCapacity( 8 ); + m_vecColumns.AddToTail( col1 ); + m_vecColumns.AddToTail( col2 ); + m_vecColumns.AddToTail( col3 ); + m_vecColumns.AddToTail( col4 ); + m_vecColumns.AddToTail( col5 ); + m_vecColumns.AddToTail( col6 ); + m_vecColumns.AddToTail( col7 ); + m_vecColumns.AddToTail( col8 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Copy constructor +//----------------------------------------------------------------------------- +CColumnSet::CColumnSet( const CColumnSet & rhs ) +{ + MEM_ALLOC_CREDIT_("CColumnSet"); + m_vecColumns.CopyArray( rhs.m_vecColumns.Base(), rhs.m_vecColumns.Count() ); + m_pRecordInfo = rhs.m_pRecordInfo; +} + + +//----------------------------------------------------------------------------- +// Purpose: Assignment operator +//----------------------------------------------------------------------------- +CColumnSet & CColumnSet::operator=( const CColumnSet & rhs ) +{ + MEM_ALLOC_CREDIT_("CColumnSet"); + m_vecColumns.CopyArray( rhs.m_vecColumns.Base(), rhs.m_vecColumns.Count() ); + m_pRecordInfo = rhs.m_pRecordInfo; + return *this; +} + + +//----------------------------------------------------------------------------- +// Purpose: Addition operator. lhs ColumnSet will be a union of the two +// ColumnSets +//----------------------------------------------------------------------------- +CColumnSet & CColumnSet::operator+=( const CColumnSet & rhs ) +{ + Assert( this->GetRecordInfo() == rhs.GetRecordInfo() ); + FOR_EACH_COLUMN_IN_SET( rhs, i ) + { + BAddColumn( rhs.GetColumn( i ) ); + } + + return *this; +} + +//----------------------------------------------------------------------------- +// Purpose: Addition operator. Returns a union of lhs and rhs +//----------------------------------------------------------------------------- +const CColumnSet CColumnSet::operator+( const CColumnSet & rhs ) const +{ + return CColumnSet( *this ) += rhs; +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a column to the set if it is +// Inputs: nColumn - THe column to add +//----------------------------------------------------------------------------- +void CColumnSet::BAddColumn( int nColumn ) +{ + if( nColumn >= 0 && nColumn < m_pRecordInfo->GetNumColumns() ) + { + //not sure best way to handle the 'is already set case' + if( !IsSet( nColumn ) ) + m_vecColumns.AddToTail( nColumn ); + } + else + { + AssertMsg3( false, "Attempting to set an out of range column on schema type %s, %d (of %d)", GetRecordInfo()->GetName(), nColumn, m_pRecordInfo->GetNumColumns() ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Removes a column from the set +// Inputs: nColumn - THe column to remove +//----------------------------------------------------------------------------- +void CColumnSet::BRemoveColumn( int nColumn ) +{ + m_vecColumns.FindAndRemove( nColumn ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if a column is in the set +// Inputs: nColumn - THe column to test +//----------------------------------------------------------------------------- +bool CColumnSet::IsSet( int nColumn ) const +{ + int nIndex = m_vecColumns.Find( nColumn ); + return m_vecColumns.IsValidIndex( nIndex ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns the number of columns in the set +//----------------------------------------------------------------------------- +uint32 CColumnSet::GetColumnCount() const +{ + return m_vecColumns.Count(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns the column index of the Nth column in the set +// Inputs: nIndex - the position in the set to return a column index for. +//----------------------------------------------------------------------------- +int CColumnSet::GetColumn( int nIndex ) const +{ + return m_vecColumns[nIndex]; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns a CColumnInfo object for the nth column in the set +// Inputs: nIndex - the position in the set to return a column info for. +//----------------------------------------------------------------------------- +const CColumnInfo & CColumnSet::GetColumnInfo( int nIndex ) const +{ + return m_pRecordInfo->GetColumnInfo( GetColumn( nIndex ) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Empties the column set +//----------------------------------------------------------------------------- +void CColumnSet::MakeEmpty() +{ + m_vecColumns.RemoveAll(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Makes the column set be the full set of all columns in the record info +//----------------------------------------------------------------------------- +void CColumnSet::MakeFull() +{ + MakeEmpty(); + const int nNumColumns = m_pRecordInfo->GetNumColumns(); + m_vecColumns.EnsureCapacity( nNumColumns ); + for( int nColumn = 0; nColumn < m_pRecordInfo->GetNumColumns(); nColumn++ ) + { + //do a direct add to avoid the exponential cost since we know we won't have conflicts + m_vecColumns.AddToTail( nColumn ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Makes the column set be the full set of all insertable columns in +// the record info +//----------------------------------------------------------------------------- +void CColumnSet::MakeInsertable() +{ + MakeEmpty(); + for( int nColumn = 0; nColumn < m_pRecordInfo->GetNumColumns(); nColumn++ ) + { + const CColumnInfo & columnInfo = m_pRecordInfo->GetColumnInfo( nColumn ); + if( columnInfo.BIsInsertable() ) + { + //do a direct add to avoid the exponential cost since we know we won't have conflicts + m_vecColumns.AddToTail( nColumn ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Makes the column set be the full set of all noninsertable columns in +// the record info +//----------------------------------------------------------------------------- +void CColumnSet::MakeNoninsertable() +{ + MakeEmpty(); + for( int nColumn = 0; nColumn < m_pRecordInfo->GetNumColumns(); nColumn++ ) + { + const CColumnInfo & columnInfo = m_pRecordInfo->GetColumnInfo( nColumn ); + if( !columnInfo.BIsInsertable() ) + { + //do a direct add to avoid the exponential cost since we know we won't have conflicts + m_vecColumns.AddToTail( nColumn ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Makes the column set be the full set of all primary key columns in +// the record info +//----------------------------------------------------------------------------- +void CColumnSet::MakePrimaryKey() +{ + MakeEmpty(); + for( int nColumn = 0; nColumn < m_pRecordInfo->GetNumColumns(); nColumn++ ) + { + const CColumnInfo & columnInfo = m_pRecordInfo->GetColumnInfo( nColumn ); + if( columnInfo.BIsPrimaryKey() ) + { + //do a direct add to avoid the exponential cost since we know we won't have conflicts + m_vecColumns.AddToTail( nColumn ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Makes the column set be the full set of all primary key columns in +// the record info +//----------------------------------------------------------------------------- +void CColumnSet::MakeInverse( const CColumnSet & columnSet ) +{ + MakeEmpty(); + for( int nColumn = 0; nColumn < m_pRecordInfo->GetNumColumns(); nColumn++ ) + { + if( !columnSet.IsSet( nColumn ) ) + { + //do a direct add to avoid the exponential cost since we know we won't have conflicts + m_vecColumns.AddToTail( nColumn ); + } + } +} + +//----------------------------------------------------------------------------- +// determines if the current column set has all fields set. Useful for detection of new columns being added to the schema +//----------------------------------------------------------------------------- +bool CColumnSet::BAreAllFieldsSet() const +{ + for( int nColumn = 0; nColumn < m_pRecordInfo->GetNumColumns(); nColumn++ ) + { + if( !IsSet( nColumn ) ) + return false; + } + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Returns a Column Set which is the inverse of the given column set +// STATIC - Difference from MakeInverse is that it has a return value +//----------------------------------------------------------------------------- +CColumnSet CColumnSet::Inverse( const CColumnSet & columnSet ) +{ + CColumnSet set( columnSet.GetRecordInfo() ); + set.MakeInverse( columnSet ); + return set; +} + +//----------------------------------------------------------------------------- +// Purpose: Claims the memory for CColumnSet +//----------------------------------------------------------------------------- +#ifdef DBGFLAG_VALIDATE +void CColumnSet::Validate( CValidator &validator, const char *pchName ) +{ + // these are INSIDE the function instead of outside so the interface + // doesn't change + VALIDATE_SCOPE(); + + ValidateObj( m_vecColumns ); +} +#endif + + +} // namespace GCSDK diff --git a/gcsdk/sqlaccess/record.cpp b/gcsdk/sqlaccess/record.cpp new file mode 100644 index 0000000..6812b50 --- /dev/null +++ b/gcsdk/sqlaccess/record.cpp @@ -0,0 +1,856 @@ +//========= 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 +{ + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CRecordBase::~CRecordBase() +{ + Cleanup(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Copy constructor +// Input: that - CRecord to copy from +//----------------------------------------------------------------------------- +CRecordBase::CRecordBase( const CRecordBase &that ) +{ + *this = that; +} + + +//----------------------------------------------------------------------------- +// Purpose: Assignment operator - COPIES the record data +// Input: that - CRecord to copy from +//----------------------------------------------------------------------------- +CRecordBase& CRecordBase::operator = ( const CRecordBase & that ) +{ + Assert( GetITable() == that.GetITable() ); + + // COPY that record + Copy( that ); + + return *this; +} + + +//----------------------------------------------------------------------------- +// Purpose: Copies the data in the record. This is overridden by CRecordVar and +// CRecordExternal +// Input: that - CRecord to copy from +//----------------------------------------------------------------------------- +void CRecordBase::Copy( const CRecordBase & that ) +{ + Cleanup(); + Q_memcpy( PubRecordFixed(), that.PubRecordFixed(), GetPSchema()->CubRecordFixed() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Return the record info for this record's schema +//----------------------------------------------------------------------------- +const CRecordInfo *CRecordBase::GetPRecordInfo() const +{ + return GetPSchema()->GetRecordInfo(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Copies the data in the var record. +// Input: that - CRecord to copy from +//----------------------------------------------------------------------------- +void CRecordVar::Copy( const CRecordBase & baseThat ) +{ + const CRecordVar & that = (const CRecordVar &)baseThat; + + // COPY that record + Cleanup(); + m_pSchema = that.m_pSchema; + Q_memcpy( PubRecordFixed(), that.PubRecordFixed(), GetPSchema()->CubRecordFixed() ); + + SetFlag( k_EAllocatedVarBlock, false ); + if ( VarFieldBlockInfo_t *pVarBlockInfo = GetPSchema()->PVarFieldBlockInfoFromRecord( PubRecordFixed() ) ) + { + if ( pVarBlockInfo->m_cubBlock ) + { + void *pvNewBlock = malloc( pVarBlockInfo->m_cubBlock ); + Q_memcpy( pvNewBlock, pVarBlockInfo->m_pubBlock, pVarBlockInfo->m_cubBlock ); + pVarBlockInfo->m_pubBlock = ( uint8 * )pvNewBlock; + SetFlag( k_EAllocatedVarBlock, true ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Copies the data in the var record. +// Input: that - CRecord to copy from +//----------------------------------------------------------------------------- +void CRecordExternal::Copy( const CRecordBase & baseThat ) +{ + const CRecordExternal & that = (const CRecordExternal &)baseThat; + + Cleanup(); + m_pSchema = that.m_pSchema; + + m_pubRecordFixedExternal = ( uint8 * )malloc( m_pSchema->CubRecordFixed() ); + Q_memcpy( m_pubRecordFixedExternal, that.PubRecordFixed(), m_pSchema->CubRecordFixed() ); + SetFlag( k_EAllocatedFixed, true ); + + SetFlag( k_EAllocatedVarBlock, false ); + if ( VarFieldBlockInfo_t *pVarBlockInfo = m_pSchema->PVarFieldBlockInfoFromRecord( PubRecordFixed() ) ) + { + if ( pVarBlockInfo->m_cubBlock ) + { + void *pvNewBlock = malloc( pVarBlockInfo->m_cubBlock ); + Q_memcpy( pvNewBlock, pVarBlockInfo->m_pubBlock, pVarBlockInfo->m_cubBlock ); + pVarBlockInfo->m_pubBlock = ( uint8 * )pvNewBlock; + SetFlag( k_EAllocatedVarBlock, true ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Initialize to an empty record +// Input: pSchema - Schema for the record this will hold +//----------------------------------------------------------------------------- +void CRecordExternal::Init( CSchema *pSchema ) +{ + Cleanup(); + m_pSchema = pSchema; + m_pubRecordFixedExternal = ( uint8 * )malloc( m_pSchema->CubRecordFixed() ); + Q_memset( m_pubRecordFixedExternal, 0, m_pSchema->CubRecordFixed() ); + SetFlag( k_EAllocatedFixed, true ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Initialize pointing to a record expanded in memory +// Input: pSchema - Schema for the record this will hold +// pubRecord - Pointer to fixed record data +// bTakeOwnership - Should we delete the record when destroyed +// Output: Size of the record's data +//----------------------------------------------------------------------------- +int CRecordBase::InitFromBytes( uint8 *pubRecord ) +{ + Cleanup(); + + Q_memcpy( PubRecordFixed(), pubRecord, GetPSchema()->CubRecordFixed() ); + int cubRead = GetPSchema()->CubRecordFixed(); + return cubRead; +} + +int CRecordVar::InitFromBytes( uint8 *pubRecord ) +{ + Cleanup(); + + Q_memcpy( PubRecordFixed(), pubRecord, GetPSchema()->CubRecordFixed() ); + int cubRead = GetPSchema()->CubRecordFixed(); + if ( VarFieldBlockInfo_t *pVarBlockInfo = GetPSchema()->PVarFieldBlockInfoFromRecord( PubRecordFixed() ) ) + { + if ( pVarBlockInfo->m_cubBlock ) + { + void *pvNewBlock = malloc( pVarBlockInfo->m_cubBlock ); + Q_memcpy( pvNewBlock, pVarBlockInfo->m_pubBlock, pVarBlockInfo->m_cubBlock ); + pVarBlockInfo->m_pubBlock = ( uint8 * )pvNewBlock; + SetFlag( k_EAllocatedVarBlock, true ); + cubRead += pVarBlockInfo->m_cubBlock; + } + } + + return cubRead; +} + + +int CRecordExternal::Init( CSchema *pSchema, uint8 *pubRecord, bool bTakeOwnership ) +{ + m_pSchema = pSchema; + m_pubRecordFixedExternal = pubRecord; + SetFlag( k_EAllocatedFixed, bTakeOwnership ); + SetFlag( k_EAllocatedVarBlock, bTakeOwnership ); + int cubRead = m_pSchema->CubRecordFixed() + CubRecordVarBlock(); + + return cubRead; +} + +CSchema *CRecordBase::GetPSchema() +{ + return GetPSchemaImpl(); +} + +CSchema *CRecordBase::GetPSchemaImpl() +{ + CSchema *pSchema = NULL; + int i = GetITable(); + if ( i != -1 ) + pSchema = &GSchemaFull().GetSchema( i ); + return pSchema; +} + +//----------------------------------------------------------------------------- +// Purpose: Render a field to a buffer +// Input: unColumn - field to render +// cchBuffer - size of render buffer +// pchBuffer - buffer to render into +//----------------------------------------------------------------------------- +void CRecordBase::RenderField( uint32 unColumn, int cchBuffer, char *pchBuffer ) const +{ + Q_strncpy( pchBuffer, "", cchBuffer ); + + uint8 *pubData; + uint32 cubData; + if ( !BGetField( unColumn, &pubData, &cubData ) ) + return; + + // Get the column info and figure out how to interpret the data + ConvertFieldToText( GetPRecordInfo()->GetColumnInfo( unColumn ).GetType(), pubData, cubData, pchBuffer, cchBuffer, false ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Reset to base state, freeing any memory we are responsible for +//----------------------------------------------------------------------------- +void CRecordBase::Cleanup() +{ +} + +void CRecordVar::Cleanup() +{ + // Must do this before freeing memory that encloses it + // (eg releasing the net packet) + if ( BFlagSet( k_EAllocatedVarBlock ) ) + { + void *pvVarBlock = m_pSchema->PVarFieldBlockInfoFromRecord( PubRecordFixed() )->m_pubBlock; + free( pvVarBlock ); + m_pSchema->PVarFieldBlockInfoFromRecord( PubRecordFixed() )->m_pubBlock = NULL; + SetFlag( k_EAllocatedVarBlock, false ); + } +} + +void CRecordExternal::Cleanup() +{ + // clean up the variable-length memory we might have allocated + if ( BFlagSet( k_EAllocatedVarBlock ) ) + { + void *pvVarBlock = m_pSchema->PVarFieldBlockInfoFromRecord( PubRecordFixed() )->m_pubBlock; + free( pvVarBlock ); + m_pSchema->PVarFieldBlockInfoFromRecord( PubRecordFixed() )->m_pubBlock = NULL; + SetFlag( k_EAllocatedVarBlock, false ); + } + + // clean up the external memory we might have allocated + if ( BFlagSet( k_EAllocatedFixed ) ) + free( m_pubRecordFixedExternal ); + SetFlag( k_EAllocatedFixed, false ); + m_pubRecordFixedExternal = NULL; + + // clean up the lowest layer, not calling CRecordVar + CRecordBase::Cleanup(); +} + +//----------------------------------------------------------------------------- +// Purpose: Deserializes a block of memory into this record +// Input: pubData - Memory block to deserialize from +//----------------------------------------------------------------------------- +void CRecordExternal::DeSerialize( uint8 *pubData ) +{ + InitFromBytes( pubData ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Calculates the size of this record when serialized +// Output: Size of serialized message +//----------------------------------------------------------------------------- +uint32 CRecordBase::CubSerialized() +{ + return CubRecordFixed() + CubRecordVarBlock(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get pointer to fixed part of record +// Output: pubRecordFixed +//----------------------------------------------------------------------------- +uint8* CRecordBase::PubRecordFixed() +{ + return ( uint8 * )( this + 1 ); +} + +uint8* CRecordExternal::PubRecordFixed() +{ + Assert( m_pubRecordFixedExternal ); + return m_pubRecordFixedExternal; +} + +uint8* CRecordVar::PubRecordFixed() +{ + return ( uint8 * )( this + 1 ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get pointer to fixed part of record +// Output: pubRecordFixed +//----------------------------------------------------------------------------- +const uint8* CRecordBase::PubRecordFixed() const +{ + return const_cast<CRecordBase *>( this )->PubRecordFixed(); +} + +const uint8* CRecordVar::PubRecordFixed() const +{ + return const_cast<CRecordVar *>( this )->PubRecordFixed(); +} + +const uint8* CRecordExternal::PubRecordFixed() const +{ + return const_cast<CRecordExternal *>( this )->PubRecordFixed(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get size of fixed part of record +// Output: size in bytes of fixed part +//----------------------------------------------------------------------------- +uint32 CRecordBase::CubRecordFixed() const +{ + return GetPSchema()->CubRecordFixed(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get pointer to variable part of record +// Output: Pointer to variable-length block -- may be NULL if this record +// has no var-length fields or they are all empty +//----------------------------------------------------------------------------- +uint8* CRecordBase::PubRecordVarBlock() +{ + VarFieldBlockInfo_t *pVarFieldBlockInfo = GetPSchema()->PVarFieldBlockInfoFromRecord( PubRecordFixed() ); + if ( pVarFieldBlockInfo ) + { + return pVarFieldBlockInfo->m_pubBlock; + } + else + { + return NULL; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Get pointer to variable part of record +// Output: Pointer to variable-length block -- may be NULL if this record +// has no var-length fields or they are all empty +//----------------------------------------------------------------------------- +const uint8* CRecordBase::PubRecordVarBlock() const +{ + return const_cast<CRecordBase *>( this )->PubRecordVarBlock(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get size of variable part of record +// Output: Size in bytes of var-length block - may be zero if this record +// has no var-length fields or they are all empty +//----------------------------------------------------------------------------- +uint32 CRecordBase::CubRecordVarBlock() const +{ + VarFieldBlockInfo_t *pVarFieldBlockInfo = GetPSchema()->PVarFieldBlockInfoFromRecord( PubRecordFixed() ); + if ( pVarFieldBlockInfo ) + { + return pVarFieldBlockInfo->m_cubBlock; + } + else + { + return 0; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Get size of variable part of record +// Output: Size in bytes of var-length block - may be zero if this record +// has no var-length fields or they are all empty +//----------------------------------------------------------------------------- +bool CRecordBase::BAssureRecordVarStorage( uint32 cVariableBytes ) +{ + // get the variable field block + VarFieldBlockInfo_t *pVarFieldBlockInfo = GetPSchema()->PVarFieldBlockInfoFromRecord( PubRecordFixed() ); + if ( pVarFieldBlockInfo ) + { + // if we have it, see if it's got enough storage + if ( pVarFieldBlockInfo->m_cubBlock >= cVariableBytes ) + { + // already there + return true; + } + + // allocate it + uint8* pubData = (uint8*) malloc( cVariableBytes ); + if ( pubData == NULL ) + return false; + + // do we have something right now? + if ( pVarFieldBlockInfo->m_cubBlock != 0 ) + { + // sure do. copy it over. + Q_memcpy( pubData, pVarFieldBlockInfo->m_pubBlock, pVarFieldBlockInfo->m_cubBlock ); + + // free what was there + free( pVarFieldBlockInfo->m_pubBlock ); + } + + // hook up our buffer + pVarFieldBlockInfo->m_cubBlockFree = cVariableBytes - pVarFieldBlockInfo->m_cubBlock; + pVarFieldBlockInfo->m_cubBlock = cVariableBytes; + pVarFieldBlockInfo->m_pubBlock = pubData; + + return true; + } + else + { + // we don't have one; + // we've got no variable length fields, and so can't preallocate for them! + return false; + } +} + +//----------------------------------------------------------------------------- +// Purpose: Initialize this whole record to random data +// Input: unPrimaryIndex - Primary index to set +//----------------------------------------------------------------------------- +void CRecordExternal::InitRecordRandom( uint32 unPrimaryIndex ) +{ + bool bRealloced = false; + GetPSchema()->InitRecordRandom( PubRecordFixed(), unPrimaryIndex, &bRealloced, BFlagSet( k_EAllocatedVarBlock ) ); + + if ( bRealloced ) + SetFlag( k_EAllocatedVarBlock, true ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Set a field in this record to random bits +// Input: iField - Field to set +//----------------------------------------------------------------------------- +void CRecordExternal::SetFieldRandom( int iField ) +{ + bool bRealloced = false; + GetPSchema()->SetFieldRandom( PubRecordFixed(), iField, &bRealloced, BFlagSet( k_EAllocatedVarBlock ) ); + + if ( bRealloced ) + SetFlag( k_EAllocatedVarBlock, true ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get a field (var or fixed) from this record +// Input: iField - Field to set +// ppubData - Receives pointer to fields data +// pcubField - Receives count of bytes of data (will count the null for strings) +// Output: true if succeeds +//----------------------------------------------------------------------------- +bool CRecordBase::BGetField( int iField, uint8 **ppubData, uint32 *pcubField ) const +{ + return GetPSchema()->BGetFieldData( PubRecordFixed(), iField, ppubData, pcubField ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets the data for a field, whether fixed or variable length +// Input: iField - index of field to set +// pubData - pointer to field data to copy from +// cubData - size in bytes of that data +// Output: true if successful +//----------------------------------------------------------------------------- +bool CRecordBase::BSetField( int iField, void *pvData, uint32 cubData ) +{ + bool bRealloced = false; + bool bResult = BSetField( iField, pvData, cubData, &bRealloced ); + Assert( !bRealloced ); + + return bResult; +} + +bool CRecordBase::BSetField( int iField, void *pvData, uint32 cubData, bool *pbRealloced ) +{ + uint8 *pubData = reinterpret_cast<uint8 *>( pvData ); + + if ( !GetPSchema()->BSetFieldData( PubRecordFixed(), iField, pubData, cubData, pbRealloced ) ) + return false; + + return true; +} + +bool CRecordVar::BSetField( int iField, void *pvData, uint32 cubData ) +{ + bool bRealloced = false; + bool bResult = CRecordBase::BSetField( iField, pvData, cubData, &bRealloced ); + + if ( bRealloced ) + SetFlag( k_EAllocatedVarBlock, true ); + return bResult; +} + + +//----------------------------------------------------------------------------- +// Purpose: Erases a field, setting it to 0 length (if possible) and filling with nulls +// Input: iField - index of field to wipe +// NOTE: This relies on CSchema::BSetFieldData nulling out the rest of a field when it is set to 0 length! +//----------------------------------------------------------------------------- +void CRecordBase::WipeField( int iField ) +{ + bool bRealloced = false; + + // Empty Data + uint32 un = 0; + + // Length should be 0, except for non-variable length strings where length should be 1 (for an empty string "") + int cub = 0; + Field_t &field = GetPSchema()->GetField( iField ); + Assert( !field.BIsVariableLength() ); + if ( field.BIsStringType() ) + cub = 1; + + GetPSchema()->BSetFieldData( PubRecordFixed(), iField, ( uint8 * ) &un, cub, &bRealloced ); + + Assert( !bRealloced ); +} + +void CRecordVar::WipeField( int iField ) +{ + bool bRealloced = false; + + // Empty Data + uint32 un = 0; + + // Length should be 0, except for non-variable length strings where length should be 1 (for an empty string "") + int cub = 0; + Field_t &field = GetPSchema()->GetField( iField ); + if ( field.BIsStringType() && !field.BIsVariableLength() ) + cub = 1; + + GetPSchema()->BSetFieldData( PubRecordFixed(), iField, ( uint8 * ) &un, cub, &bRealloced ); + + if ( bRealloced ) + SetFlag( k_EAllocatedVarBlock, true ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get a string field - will return empty string instead of NULL if field has no datas +// Input: iField - Field to get +// pcubField - Receives count of bytes of data (will count the null for strings) +// Output: const pointer to string data (to an empty string if no data) +//----------------------------------------------------------------------------- +const char * CRecordBase::GetStringField( int iField, uint32 *pcubField ) +{ + uint8 * pubData = NULL; + *pcubField = 0; + + if ( BGetField( iField, &pubData, pcubField ) && *pcubField > 0 ) + return ( const char * ) pubData; + else + return ""; +} + + +//----------------------------------------------------------------------------- +// Purpose: Get an int field +// Input: iField - Field to get +// Output: Int (0 if no data) +//----------------------------------------------------------------------------- +int CRecordBase::GetInt( int iField ) +{ + return ( int ) GetUint32( iField ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get a uint16 field +// Input: iField - Field to get +// Output: uint16 (0 if no data) +//----------------------------------------------------------------------------- +uint16 CRecordBase::GetUint16( int iField ) +{ + uint8 * pubData = NULL; + uint32 cubField = 0; + + DbgVerify( BGetField( iField, &pubData, &cubField ) ); + Assert( 0 < cubField ); + + if ( NULL != pubData ) + return *( uint16 * ) pubData; + else + return 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: Get a uint32 field +// Input: iField - Field to get +// Output: uint32 (0 if no data) +//----------------------------------------------------------------------------- +uint32 CRecordBase::GetUint32( int iField ) +{ + uint8 * pubData = NULL; + uint32 cubField = 0; + + DbgVerify( BGetField( iField, &pubData, &cubField ) ); + Assert( 0 < cubField ); + + if ( NULL != pubData ) + return *( uint32 * ) pubData; + else + return 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: Get a uint64 field +// Input: iField - Field to get +// Output: uint64 (0 if no data) +//----------------------------------------------------------------------------- +uint64 CRecordBase::GetUint64( int iField ) +{ + uint8 * pubData = NULL; + uint32 cubField = 0; + + DbgVerify( BGetField( iField, &pubData, &cubField ) ); + Assert( 0 < cubField ); + + if ( NULL != pubData ) + return *( uint64 * ) pubData; + else + return 0; +} + + + +const char * CRecordBase::ReadVarCharField( const CVarCharField &field ) const +{ + Assert( false ); + return NULL; +} + +const uint8 * CRecordBase::ReadVarDataField( const CVarField &field, uint32 *pcubField ) const +{ + Assert( false ); + return NULL; +} + +// These may cause a realloc +bool CRecordBase::SetVarCharField( CVarCharField &field, const char *pchString, bool bTruncate, int32 iField ) +{ + Assert( false ); + return false ; +} + +void CRecordBase::SetVarDataField( CVarField &field, const void *pvData, uint32 cubData ) +{ + Assert( false ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Read data from a varchar field +// Input: field - opaque field object to read from +// Output: pointer to data - may be NULL if that field is empty. +//----------------------------------------------------------------------------- +const char * CRecordVar::ReadVarCharField( const CVarCharField &field ) const +{ + Assert ( GetPSchema()->BHasVariableFields() ); + + uint8 *pubData; + uint32 cubData; + if ( GetPSchema()->BGetVarField( PubRecordFixed(), &field, &pubData, &cubData ) ) + return (const char *)pubData; + else + return ""; +} + + +//----------------------------------------------------------------------------- +// Purpose: Read data from a vardata field +// Input: field - opaque field object to read from +// Output: pointer to data - may be NULL if that field is empty. +//----------------------------------------------------------------------------- +const uint8 *CRecordVar::ReadVarDataField( const CVarField &field, uint32 *pcubField ) const +{ + Assert ( GetPSchema()->BHasVariableFields() ); + + uint8 *pubData; + *pcubField = 0; + if ( GetPSchema()->BGetVarField( PubRecordFixed(), &field, &pubData, pcubField ) ) + return pubData; + else + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Update (in memory) a varchar field +// Input: field - opaque field object to update +// pchString - string data to set +//----------------------------------------------------------------------------- +bool CRecordVar::SetVarCharField( CVarCharField &field, const char *pchString, bool bTruncate, int32 iField ) +{ + Assert ( GetPSchema()->BHasVariableFields() ); + if( iField < 0 ) + { + AssertMsg1( false, "Encountered a bad call to SetVarCharField with an invalid field specified: %d", iField ); + return false; + } + + bool bTruncated = false; + int cchLen = Q_strlen( pchString ) + 1; + + // since we're a VARCHAR field, cbMaxLength is the length in characters + const int cchMaxLength = m_pSchema->GetField( iField ).m_cchMaxLength; + if ( ( cchMaxLength > 0 ) && ( cchLen > cchMaxLength ) ) + { + if( bTruncate ) + { + bTruncated = true; + cchLen = cchMaxLength; + } + else + { + // caller should check his data and not pass stuff that wont fit + AssertMsg4( false, "Overflow in SetVarCharField (%u > %u) for column %s in table %s", cchLen, cchMaxLength, m_pSchema->GetField( iField ).m_rgchName, m_pSchema->GetPchName() ); + return false; + } + } + + bool bRealloced = false; + bool fSuccess = GetPSchema()->BSetVarField( PubRecordFixed(), &field, pchString, cchLen, &bRealloced, BFlagSet( k_EAllocatedVarBlock ) ); + if( fSuccess && bTruncated ) + { + //make sure the last character is NULL if we truncated + VarFieldBlockInfo_t *pBlock = GetPSchema()->PVarFieldBlockInfoFromRecord( PubRecordFixed() ); + uint8 *pubVarBlock = pBlock->m_pubBlock; + char* pField = ( char* )( pubVarBlock + field.m_dubOffset ); + pField[ cchLen - 1 ] = '\0'; + } + + if ( bRealloced ) + SetFlag( k_EAllocatedVarBlock, true ); + return fSuccess; +} + + +//----------------------------------------------------------------------------- +// Purpose: Update (in memory) a vardata field +// Input: field - opaque field object to update +// pvData - pointer to data to put there +// cubData - size in bytes of the data +//----------------------------------------------------------------------------- +void CRecordVar::SetVarDataField( CVarField &field, const void *pvData, uint32 cubData ) +{ + Assert ( GetPSchema()->BHasVariableFields() ); + + bool bRealloced = false; + GetPSchema()->BSetVarField( PubRecordFixed(), &field, pvData, cubData, &bRealloced, BFlagSet( k_EAllocatedVarBlock ) ); + + if ( bRealloced ) + SetFlag( k_EAllocatedVarBlock, true ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Set or clear the specified flag in m_nFlags +// Input: eFlag - flag (single bit) to change +// bSet - Set it, else clear it +//----------------------------------------------------------------------------- +void CRecordVar::SetFlag( int eFlag, bool bSet ) +{ + if ( bSet ) + m_nFlags |= eFlag; + else + m_nFlags &= ~eFlag; +} + + +//----------------------------------------------------------------------------- +// Purpose: Get the state of the specified flag +// Input: eFlag - flag (single bit) to check +//----------------------------------------------------------------------------- +bool CRecordVar::BFlagSet( int eFlag ) const +{ + return 0 != ( m_nFlags & eFlag ); +} + + +#ifdef DBGFLAG_VALIDATE +//----------------------------------------------------------------------------- +// Purpose: Run a global validation pass on all of our data structures and memory +// allocations. +// Input: validator - Our global validator object +// pchName - Our name (typically a member var in our container) +//----------------------------------------------------------------------------- +void CRecordBase::Validate( CValidator &validator, const char *pchName ) +{ + VALIDATE_SCOPE(); +} + +void CRecordVar::Validate( CValidator &validator, const char *pchName ) +{ + VALIDATE_SCOPE(); + + if ( BFlagSet( k_EAllocatedVarBlock ) ) + { + validator.ClaimMemory( GetPSchema()->PVarFieldBlockInfoFromRecord( PubRecordFixed() )->m_pubBlock ); + } + +} + +void CRecordExternal::Validate( CValidator &validator, const char *pchName ) +{ + if ( BFlagSet( k_EAllocatedFixed ) ) + { + validator.ClaimMemory( m_pubRecordFixedExternal ); + } + + CRecordBase::Validate( validator, pchName ); +} + +void CRecordBase::ValidateStatics( CValidator &validator, const char *pchName ) +{ + VALIDATE_SCOPE_STATIC( "CRecordBase class statics" ); +} + +#endif // DBGFLAG_VALIDATE + + +//----------------------------------------------------------------------------- +// Purpose: Return the schema for this record type +//----------------------------------------------------------------------------- +CSchema *CRecordType::GetSchema() const +{ + return &GSchemaFull().GetSchema( GetITable() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Return the CRecordInfo for this record type +//----------------------------------------------------------------------------- +CRecordInfo *CRecordType::GetRecordInfo() const +{ + return GetSchema()->GetRecordInfo(); +} + +} // namespace GCSDK diff --git a/gcsdk/sqlaccess/recordinfo.cpp b/gcsdk/sqlaccess/recordinfo.cpp new file mode 100644 index 0000000..57fb435 --- /dev/null +++ b/gcsdk/sqlaccess/recordinfo.cpp @@ -0,0 +1,915 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//============================================================================= + + +#include "stdafx.h" + +//#include "sqlaccess.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +namespace GCSDK +{ +// Memory pool for CRecordInfo +CThreadSafeClassMemoryPool<CRecordInfo> CRecordInfo::sm_MemPoolRecordInfo( 10, UTLMEMORYPOOL_GROW_FAST ); + +#ifdef _DEBUG +// validation tracking +CUtlRBTree<CRecordInfo *, int > CRecordInfo::sm_mapPMemPoolRecordInfo( DefLessFunc( CRecordInfo *) ); +CThreadMutex CRecordInfo::sm_mutexMemPoolRecordInfo; +#endif + + +//----------------------------------------------------------------------------- +// determine if this fieldset is equal to the other one +//----------------------------------------------------------------------------- +/* static */ +bool FieldSet_t::CompareFieldSets( const FieldSet_t& refThis, CRecordInfo* pRecordInfoThis, + const FieldSet_t& refOther, CRecordInfo* pRecordInfoOther ) +{ + // same number of columns? + int cColumns = refThis.GetCount(); + if ( refOther.GetCount() != cColumns ) + return false; + + int cIncludedColumns = refThis.GetIncludedCount(); + if ( refOther.GetIncludedCount() != cIncludedColumns ) + return false; + + // do the regular columns first; this is order-dependent + for ( int m = 0; m < cColumns; m++ ) + { + int nThisField = refThis.GetField( m ); + const CColumnInfo& refThisColumn = pRecordInfoThis->GetColumnInfo( nThisField ); + + int nOtherField = refOther.GetField( m ); + const CColumnInfo& refOtherColumn = pRecordInfoOther->GetColumnInfo( nOtherField ); + + if ( refOtherColumn != refThisColumn ) + { + return false; + } + } + + // do the included columns now; order independent + for ( int m = 0; m < cIncludedColumns; m++ ) + { + int nThisField = refThis.GetIncludedField( m ); + const CColumnInfo& refThisColumn = pRecordInfoThis->GetColumnInfo( nThisField ); + bool bFoundMatch = false; + + for ( int n = 0; n < cIncludedColumns; n++ ) + { + int nOtherField = refOther.GetIncludedField( n ); + const CColumnInfo& refOtherColumn = pRecordInfoOther->GetColumnInfo( nOtherField ); + + if ( refOtherColumn == refThisColumn ) + { + bFoundMatch = true; + break; + } + } + + if ( !bFoundMatch ) + return false; + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CRecordInfo::CRecordInfo() +: m_MapIColumnInfo( 0, 0, CaselessStringLessThan ) +{ + m_rgchName[0] = 0; + m_bPreparedForUse = false; + m_bAllColumnsAdded = false; + m_bHaveChecksum = false; + m_bHaveColumnNameIndex = false; + m_nHasPrimaryKey = k_EPrimaryKeyTypeNone; + m_iPKIndex = -1; + m_cubFixedSize = 0; + m_nChecksum = 0; + m_eSchemaCatalog = k_ESchemaCatalogInvalid; + m_nTableID = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: Initializes this record info from DS equivalent information +//----------------------------------------------------------------------------- +void CRecordInfo::InitFromDSSchema( CSchema *pSchema ) +{ + // copy the name over + SetName( pSchema->GetPchName() ); + + // copy each of the fields, preallocating capacity + int cFields = pSchema->GetCField(); + m_VecColumnInfo.EnsureCapacity( cFields ); + for ( int iField = 0; iField < cFields; iField++ ) + { + Field_t &field = pSchema->GetField( iField ); + AddColumn( field.m_rgchSQLName, iField+1, field.m_EType, field.m_cubLength, field.m_nColFlags, field.m_cchMaxLength ); + } + + m_nTableID = pSchema->GetITable(); + + // copy the list of PK index fields + m_iPKIndex = pSchema->GetPKIndex( ); + + // copy the list of Indexes + m_VecIndexes = pSchema->GetIndexes( ); + + // which schema? + m_eSchemaCatalog = pSchema->GetESchemaCatalog(); + + // copy full-text column list + // and the index of the catalog it will create on + m_vecFTSFields = pSchema->GetFTSColumns(); + m_nFullTextCatalogIndex = pSchema->GetFTSIndexCatalog(); + + // Copy over the FK data + int cFKs = pSchema->GetFKCount(); + for ( int i = 0; i < cFKs; ++i ) + { + FKData_t &fkData = pSchema->GetFKData( i ); + AddFK( fkData ); + } + + // prepare for use + PrepareForUse( ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a new column to this record info +// Input: pchName - column name +// nSQLColumn - column index in SQL to bind to (1-based) +// eType - data type of column +// cubFixedSize - for fixed-size fields, the size +// nColFlags - attributes +//----------------------------------------------------------------------------- +void CRecordInfo::AddColumn( const char *pchName, int nSQLColumn, EGCSQLType eType, int cubFixedSize, int nColFlags, int cchMaxSize ) +{ + Assert( !m_bPreparedForUse ); + if ( m_bPreparedForUse ) + return; + uint32 unColumn = m_VecColumnInfo.AddToTail(); + CColumnInfo &columnInfo = m_VecColumnInfo[unColumn]; + + columnInfo.Set( pchName, nSQLColumn, eType, cubFixedSize, nColFlags, cchMaxSize ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a new FK to this record info +//----------------------------------------------------------------------------- +void CRecordInfo::AddFK( const FKData_t &fkData ) +{ + m_VecFKData.AddToTail( fkData ); +} + + +//----------------------------------------------------------------------------- +// Purpose: compare function to sort by column name +//----------------------------------------------------------------------------- +int __cdecl CompareColumnInfo( const CColumnInfo *pColumnInfoLeft, const CColumnInfo *pColumnInfoRight ) +{ + const char *pchLeft = ( (CColumnInfo *) pColumnInfoLeft )->GetName(); + const char *pchRight = ( (CColumnInfo *) pColumnInfoRight )->GetName(); + Assert( pchLeft && pchLeft[0] ); + Assert( pchRight && pchRight[0] ); + return Q_stricmp( pchLeft, pchRight ); +} + + +//----------------------------------------------------------------------------- +// Purpose: compares this record info to another record info +//----------------------------------------------------------------------------- +bool CRecordInfo::EqualTo( CRecordInfo* pOther ) +{ + int nOurs = GetChecksum(); + int nTheirs = pOther->GetChecksum(); + + // if this much isn't equal, we're no good + if (nOurs != nTheirs) + return false; + + if ( !CompareIndexLists( pOther ) ) + return false; + + if ( !CompareFKs( pOther ) ) + return false; + + return CompareFTSIndexLists( pOther ); +} + +//----------------------------------------------------------------------------- +// Purpose: format the index list into a string +//----------------------------------------------------------------------------- +void CRecordInfo::GetIndexFieldList( CFmtStr1024 *pstr, int nIndents ) const +{ + // table name at first + pstr->sprintf( "Table %s:\n", this->GetName() ); + + // for each of the indexes ... + for ( int n = 0; n < m_VecIndexes.Count(); n++ ) + { + const FieldSet_t& fs = m_VecIndexes[n]; + + // indent enough + for ( int x = 0; x < nIndents; x++ ) + { + pstr->Append( "\t" ); + } + + // show if it is clustered or not + pstr->AppendFormat( "Index %d (%s): %sclustered, %sunique {", n, + fs.GetIndexName(), + fs.IsClustered() ? "" : "non-", + fs.IsUnique() ? "" : "non-" ); + + // then show all the columns + for (int m = 0; m < fs.GetCount(); m++ ) + { + int x = fs.GetField( m ); + const char* pstrName = m_VecColumnInfo[x].GetName(); + pstr->AppendFormat( "%s %s", ( m == 0 ) ? "" : ",", pstrName ); + } + + // then the included columns, too + for ( int m = 0; m < fs.GetIncludedCount(); m++ ) + { + int x = fs.GetIncludedField( m ); + const char* pstrName = m_VecColumnInfo[x].GetName(); + pstr->AppendFormat( ", *%s", pstrName ); + } + pstr->Append( " }\n" ); + } + + return; +} + + +//----------------------------------------------------------------------------- +// Purpose: Get the number of foreign key constraints defined for the table +//----------------------------------------------------------------------------- +int CRecordInfo::GetFKCount() +{ + return m_VecFKData.Count(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get data for a foreign key by index (valid for 0...GetFKCount()-1) +//----------------------------------------------------------------------------- +FKData_t &CRecordInfo::GetFKData( int iIndex ) +{ + return m_VecFKData[iIndex]; +} + + +//----------------------------------------------------------------------------- +// Purpose: format the FK list into a string +//----------------------------------------------------------------------------- +void CRecordInfo::GetFKListString( CFmtStr1024 *pstr, int nIndents ) +{ + // table name at first + pstr->sprintf( "Table %s Foreign Keys: \n", this->GetName() ); + + + + if ( m_VecFKData.Count() == 0 ) + { + // indent enough + pstr->AppendIndent( nIndents ); + pstr->Append( "No foreign keys for table\n" ); + } + else + { + for ( int n = 0; n < m_VecFKData.Count(); n++ ) + { + // indent enough + pstr->AppendIndent( nIndents ); + + FKData_t &fkData = m_VecFKData[n]; + CFmtStr sColumns, sParentColumns; + FOR_EACH_VEC( fkData.m_VecColumnRelations, i ) + { + FKColumnRelation_t &colRelation = fkData.m_VecColumnRelations[i]; + if ( i > 0) + { + sColumns += ","; + sParentColumns += ","; + } + sColumns += colRelation.m_rgchCol; + sParentColumns += colRelation.m_rgchParentCol; + } + + pstr->AppendFormat( "CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s) ON DELETE %s ON UPDATE %s\n", + fkData.m_rgchName, sColumns.Access(), fkData.m_rgchParentTableName, sParentColumns.Access(), + PchNameFromEForeignKeyAction( fkData.m_eOnDeleteAction ), PchNameFromEForeignKeyAction( fkData.m_eOnUpdateAction ) ); + } + } + + return; +} + + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CRecordInfo::AddFTSFields( CUtlVector< int > &vecFields ) +{ + AssertMsg( m_vecFTSFields.Count() == 0, "Only one FTS index per table" ); + FOR_EACH_VEC( vecFields, n ) + { + int nField = vecFields[n]; + m_vecFTSFields.AddToTail( nField ); + } + + return; +} + +//----------------------------------------------------------------------------- +// Purpose: compares FK lists in this record with those of another +//----------------------------------------------------------------------------- +bool CRecordInfo::CompareFKs( CRecordInfo *pOther ) +{ + if ( pOther->m_VecFKData.Count() != m_VecFKData.Count() ) + return false; + + for( int i=0; i < m_VecFKData.Count(); ++i ) + { + FKData_t &fkDataMine = m_VecFKData[i]; + + bool bFoundInOther = false; + for ( int j=0; j < pOther->m_VecFKData.Count(); ++j ) + { + FKData_t &fkDataOther = pOther->m_VecFKData[j]; + + if ( fkDataMine == fkDataOther ) + { + bFoundInOther = true; + break; + } + } + + if ( !bFoundInOther ) + return false; + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Locate an index by its properties (ignoring the name). +// Returns position of index in the index array, or -1 if not found. +//----------------------------------------------------------------------------- +int CRecordInfo::FindIndex( CRecordInfo *pRec, const FieldSet_t& fieldSet ) +{ + for ( int i = 0; i < m_VecIndexes.Count(); i++ ) + { + if ( FieldSet_t::CompareFieldSets( m_VecIndexes[i], this, fieldSet, pRec ) ) + return i; + } + + // Not found + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: Locate an index with the given name. +// Returns position of index in the index array, or -1 if not found. +//----------------------------------------------------------------------------- +int CRecordInfo::FindIndexByName( const char *pszName ) const +{ + for ( int i = 0; i < m_VecIndexes.Count(); i++ ) + { + if ( V_stricmp( m_VecIndexes[i].GetIndexName(), pszName )== 0 ) + return i; + } + + // Not found + return -1; +} + +//----------------------------------------------------------------------------- +// Purpose: compares index lists in this record with those of another +//----------------------------------------------------------------------------- +bool CRecordInfo::CompareIndexLists( CRecordInfo* pOther ) +{ + // compare the index lists (but don't use CRCs) + + // different size? can't be the same + if ( pOther->GetIndexFieldCount() != GetIndexFieldCount() ) + { + return false; + } + + // We have to loop through both lists of indexes and try to find a match. + // We also must make sure the match is exact, and that no previous match + // can alias another attempt at a match. Pretty messy, but with no available + // identity over index objects, we're forced to a suboptimal solution. + + int nIndexes = GetIndexFieldCount(); + + // get a copy of the other index vector, which we'll remove items from as + // matches are found. + + CUtlVector<FieldSet_t> vecOtherIndexes; + vecOtherIndexes.CopyArray( pOther->GetIndexFields().Base(), nIndexes ); + + for ( int nOurs = 0; nOurs < nIndexes; nOurs++ ) + { + int nOtherMatchIndex = -1; + const FieldSet_t& refOurs = GetIndexFields()[nOurs]; + + // rip through copy of other to find one that matches + for ( int nOther = 0; nOther < vecOtherIndexes.Count(); nOther++ ) + { + const FieldSet_t& refOther = vecOtherIndexes[nOther]; + if ( FieldSet_t::CompareFieldSets( refOurs, this, refOther, pOther ) ) + { + nOtherMatchIndex = nOther; + break; + } + } + + if ( nOtherMatchIndex >= 0 ) + { + // this works! remove it from other copy + vecOtherIndexes.Remove( nOtherMatchIndex ); + } + else + { + // something didn't match, so bail out early + return false; + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: compares full-text indexes for this record with those of another +// column order in an FTS is irrelevant, so this is a simple match +//----------------------------------------------------------------------------- +bool CRecordInfo::CompareFTSIndexLists( CRecordInfo* pOther ) const +{ + // compare full-text index columns + if ( m_vecFTSFields.Count() != pOther->m_vecFTSFields.Count() ) + { + // counts don't match, so obviously no good + return false; + } + for ( int nColumnIndex = 0; nColumnIndex < m_vecFTSFields.Count(); nColumnIndex++ ) + { + bool bFound = false; + for ( int nInnerIndex = 0; nInnerIndex < pOther->m_vecFTSFields.Count(); nInnerIndex++ ) + { + if ( m_vecFTSFields[nInnerIndex] == pOther->m_vecFTSFields[nColumnIndex] ) + { + bFound = true; + break; + } + } + + if ( !bFound ) + return false; + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns the checksum for this record info +//----------------------------------------------------------------------------- +int CRecordInfo::GetChecksum() +{ + Assert( m_bPreparedForUse ); + + // calculate it now if we haven't already + if ( !m_bHaveChecksum ) + CalculateChecksum(); + + return m_nChecksum; +} + + +//----------------------------------------------------------------------------- +// Purpose: Prepares this object for use after all columns have been added +//----------------------------------------------------------------------------- +void CRecordInfo::PrepareForUse() +{ + Assert( !m_bPreparedForUse ); + Assert( 0 == m_cubFixedSize ); + Assert( 0 == m_nChecksum ); + + SetAllColumnsAdded(); + + FOR_EACH_VEC( m_VecColumnInfo, nColumn ) + { + CColumnInfo &columnInfo = m_VecColumnInfo[nColumn]; + + // keep track of total fixed size of all columns + if ( !columnInfo.BIsVariableLength() ) + m_cubFixedSize += columnInfo.GetFixedSize(); + + if ( columnInfo.BIsPrimaryKey() ) + { + // a PK column! if we have seen one before, + // know we have a-column PK; otherwise, a single column PK + if (m_nHasPrimaryKey == k_EPrimaryKeyTypeNone) + m_nHasPrimaryKey = k_EPrimaryKeyTypeSingle; + else + m_nHasPrimaryKey = k_EPrimaryKeyTypeMulti; + } + } + + // make sure count matches the enum + /* + Assert( ( m_nHasPrimaryKey == k_EPrimaryKeyTypeNone && m_VecPKFields.Count() == 0 ) || + ( m_nHasPrimaryKey == k_EPrimaryKeyTypeMulti && m_VecPKFields.Count() > 1) || + ( m_nHasPrimaryKey == k_EPrimaryKeyTypeSingle && m_VecPKFields.Count() == 1) ); + */ + + m_bPreparedForUse = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns index of column with specified name +// Input: pchName - column name +// punColumn - pointer to fill in with index +// Output: return true if found, false otherwise +//----------------------------------------------------------------------------- +bool CRecordInfo::BFindColumnByName( const char *pchName, int *punColumn ) +{ + Assert( m_bAllColumnsAdded ); + Assert( pchName && *pchName ); + Assert( punColumn ); + + *punColumn = -1; + + // if we haven't already built the name index, build it now + if ( !m_bHaveColumnNameIndex ) + BuildColumnNameIndex(); + + *punColumn = m_MapIColumnInfo.Find( pchName ); + return ( m_MapIColumnInfo.InvalidIndex() != *punColumn ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets the name of this record info +// Input: pchName - name +// Notes: record info that describes a table will have a name (the table name); +// record info that describes a result set will not +//----------------------------------------------------------------------------- +void CRecordInfo::SetName( const char *pchName ) +{ + Assert( pchName && *pchName ); + Assert( !m_bPreparedForUse ); // don't change this after prepared for use + Q_strncpy( m_rgchName, pchName, Q_ARRAYSIZE( m_rgchName ) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Builds the column name index for fast lookup by name +//----------------------------------------------------------------------------- +void CRecordInfo::BuildColumnNameIndex() +{ + AUTO_LOCK( m_Mutex ); + + if ( m_bHaveColumnNameIndex ) + return; + + Assert( m_bAllColumnsAdded ); + + Assert( 0 == m_MapIColumnInfo.Count() ); + + FOR_EACH_VEC( m_VecColumnInfo, nColumn ) + { + // build name->column index map + CColumnInfo &columnInfo = m_VecColumnInfo[nColumn]; + m_MapIColumnInfo.Insert( columnInfo.GetName(), nColumn ); + } + m_bHaveColumnNameIndex = true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Calculates the checksum for this record info +//----------------------------------------------------------------------------- +void CRecordInfo::CalculateChecksum() +{ + AUTO_LOCK( m_Mutex ); + + if ( m_bHaveChecksum ) + return; + + // build the column name index if necessary + if ( !m_bHaveColumnNameIndex ) + BuildColumnNameIndex(); + + CRC32_t crc32; + CRC32_Init( &crc32 ); + + FOR_EACH_MAP( m_MapIColumnInfo, iMapItem ) + { + uint32 unColumn = m_MapIColumnInfo[iMapItem]; + CColumnInfo &columnInfo = m_VecColumnInfo[unColumn]; + // calculate checksum of all of our columns + columnInfo.CalculateChecksum(); + int nChecksum = columnInfo.GetChecksum(); + CRC32_ProcessBuffer( &crc32, (void*) &nChecksum, sizeof( nChecksum ) ); + } + + // keep checksum for entire record info + CRC32_Final( &crc32 ); + m_nChecksum = crc32; + m_bHaveChecksum = true; +} + +//----------------------------------------------------------------------------- +// Purpose: add another index disallowing duplicates. If a duplicate item is +// found, we'll set the flags on the new item from the existing one. +//----------------------------------------------------------------------------- +int CRecordInfo::AddIndex( const FieldSet_t& fieldSet ) +{ + for ( int n = 0; n < m_VecIndexes.Count(); n++ ) + { + FieldSet_t& fs = m_VecIndexes[n]; + if ( FieldSet_t::CompareFieldSets( fieldSet, this, fs, this ) ) + { + fs.SetClustered( fs.IsClustered() ); + return -1; + } + } + + int nRet = m_VecIndexes.AddToTail( fieldSet ); + return nRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns true if there is an IDENTITY column in the record info +//----------------------------------------------------------------------------- +bool CRecordInfo::BHasIdentity() const +{ + FOR_EACH_VEC( m_VecColumnInfo, nColumn) + { + if( m_VecColumnInfo[nColumn].BIsAutoIncrement() ) + return true; + } + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CColumnInfo::CColumnInfo() +{ + m_rgchName[0] = 0; + m_nSQLColumn = 0; + m_eType = k_EGCSQLTypeInvalid; + m_nColFlags = 0; + m_cubFixedSize = 0; + m_cchMaxSize = 0; + m_nChecksum = 0; + m_bHaveChecksum = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Sets column info for this column +// Input: pchName - column name +// nSQLColumn - column index in SQL to bind to (1-based) +// eType - data type of column +// cubFixedSize - for fixed-size fields, the size +// nColFlags - attributes +//----------------------------------------------------------------------------- +void CColumnInfo::Set( const char *pchName, int nSQLColumn, EGCSQLType eType, int cubFixedSize, int nColFlags, int cchMaxSize ) +{ + Assert( !m_rgchName[0] ); + Q_strncpy( m_rgchName, pchName, Q_ARRAYSIZE( m_rgchName ) ); + m_nSQLColumn = nSQLColumn; + m_eType = eType; + + m_nColFlags = nColFlags; + ValidateColFlags(); + + if ( !BIsVariableLength() ) + { + Assert( cubFixedSize > 0 ); + m_cubFixedSize = cubFixedSize; + m_cchMaxSize = 0; + } + else + { + // it's variable length, so we need a max length + m_cchMaxSize = cchMaxSize; + m_cubFixedSize = 0; + } +} + + + +//----------------------------------------------------------------------------- +// Purpose: returns whether this column is variable length +//----------------------------------------------------------------------------- +bool CColumnInfo::BIsVariableLength() const +{ + return m_eType == k_EGCSQLType_Blob || m_eType == k_EGCSQLType_String || m_eType == k_EGCSQLType_Image; +} + +//----------------------------------------------------------------------------- +// Purpose: convert column flags to a visible representation +//----------------------------------------------------------------------------- +void CColumnInfo::GetColFlagDescription( char* pstrOut, int cubOutLength ) const +{ + if ( m_nColFlags == 0 ) + Q_strncpy( pstrOut, "(none)", cubOutLength ); + else + { + pstrOut[0] = 0; + if ( m_nColFlags & k_nColFlagIndexed ) + Q_strncat( pstrOut, "(Indexed)", cubOutLength ); + if ( m_nColFlags & k_nColFlagUnique ) + Q_strncat( pstrOut, "(Unique)", cubOutLength ); + if ( m_nColFlags & k_nColFlagPrimaryKey ) + Q_strncat( pstrOut, "(PrimaryKey)", cubOutLength ); + if ( m_nColFlags & k_nColFlagAutoIncrement ) + Q_strncat( pstrOut, "(AutoIncrement)", cubOutLength ); + if ( m_nColFlags & k_nColFlagClustered ) + Q_strncat( pstrOut, "(Clustered)", cubOutLength ); + } + + return; +} + + + +//----------------------------------------------------------------------------- +// Purpose: sets column flag bits +// Input: nColFlag - bits to set. (Other bits are not cleared.) +//----------------------------------------------------------------------------- +void CColumnInfo::SetColFlagBits( int nColFlag ) +{ + ValidateColFlags(); + m_nColFlags |= nColFlag; // set these bits + ValidateColFlags(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Calculates the checksum for this column +//----------------------------------------------------------------------------- +void CColumnInfo::CalculateChecksum() +{ + if ( m_bHaveChecksum ) + return; + + // calculate checksum of this column for easy comparsion + CRC32_t crc32; + CRC32_Init( &crc32 ); + CRC32_ProcessBuffer( &crc32, (void*) m_rgchName, Q_strlen( m_rgchName ) ); + CRC32_ProcessBuffer( &crc32, (void*) &m_nColFlags, sizeof( m_nColFlags ) ); + CRC32_ProcessBuffer( &crc32, (void*) &m_eType, sizeof( m_eType ) ); + CRC32_ProcessBuffer( &crc32, (void*) &m_cubFixedSize, sizeof( m_cubFixedSize ) ); + CRC32_ProcessBuffer( &crc32, (void*) &m_cchMaxSize, sizeof( m_cchMaxSize ) ); + CRC32_Final( &crc32 ); + + m_nChecksum = crc32; + m_bHaveChecksum = true; +} + +//----------------------------------------------------------------------------- +// determine if this CColumnInfo is the same as the referenced +//----------------------------------------------------------------------------- +bool CColumnInfo::operator==( const CColumnInfo& refOther ) const +{ + if ( m_eType != refOther.m_eType ) + return false; + if ( m_cubFixedSize != refOther.m_cubFixedSize ) + return false; + if ( m_cchMaxSize != refOther.m_cchMaxSize ) + return false; + if ( m_nColFlags != refOther.m_nColFlags ) + return false; + if ( 0 != Q_strcmp( m_rgchName, refOther.m_rgchName ) ) + return false; + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Validates that column flags are set in valid combinations +//----------------------------------------------------------------------------- +void CColumnInfo::ValidateColFlags() const +{ + // Check that column flags follow rules about how columns get expressed in SQL + + if ( m_nColFlags & k_nColFlagPrimaryKey ) + { + // a primary key must also be unique and indexed + Assert( m_nColFlags & k_nColFlagUnique ); + Assert( m_nColFlags & k_nColFlagIndexed ); + } + + // a column with uniqueness constraint must also be indexed + if ( m_nColFlags & k_nColFlagUnique ) + Assert( m_nColFlags & k_nColFlagIndexed ); +} + + +CRecordInfo *CRecordInfo::Alloc() +{ + CRecordInfo *pRecordInfo = sm_MemPoolRecordInfo.Alloc(); + +#ifdef _DEBUG + AUTO_LOCK( sm_mutexMemPoolRecordInfo ); + sm_mapPMemPoolRecordInfo.Insert( pRecordInfo ); +#endif + + return pRecordInfo; +} + + +void CRecordInfo::DestroyThis() +{ +#ifdef _DEBUG + AUTO_LOCK( sm_mutexMemPoolRecordInfo ); + sm_mapPMemPoolRecordInfo.Remove( this ); +#endif + + sm_MemPoolRecordInfo.Free( this ); +} + + +#ifdef DBGFLAG_VALIDATE + + +void CRecordInfo::ValidateStatics( CValidator &validator, const char *pchName ) +{ + VALIDATE_SCOPE_STATIC( "CRecordInfo class statics" ); + + ValidateObj( sm_MemPoolRecordInfo ); + +#ifdef _DEBUG + AUTO_LOCK( sm_mutexMemPoolRecordInfo ); + ValidateObj( sm_mapPMemPoolRecordInfo ); + FOR_EACH_MAP_FAST( sm_mapPMemPoolRecordInfo, i ) + { + sm_mapPMemPoolRecordInfo[i]->Validate( validator, "sm_mapPMemPoolRecordInfo[i]" ); + } +#endif +} + + +void CRecordInfo::Validate( CValidator &validator, const char *pchName ) +{ + VALIDATE_SCOPE(); + + m_VecIndexes.Validate( validator, "m_VecIndexes" ); + + ValidateObj( m_VecFKData ); + FOR_EACH_VEC( m_VecFKData, i ) + { + ValidateObj( m_VecFKData[i] ); + } + + for ( int iIndex = 0; iIndex < m_VecIndexes.Count(); iIndex++ ) + { + ValidateObj( m_VecIndexes[iIndex] ); + } + ValidateObj( m_vecFTSFields ); + + ValidateObj( m_VecColumnInfo ); + FOR_EACH_VEC( m_VecColumnInfo, nColumn ) + { + CColumnInfo &columnInfo = GetColumnInfo( nColumn ); + ValidateObj( columnInfo ); + } + ValidateObj( m_MapIColumnInfo ); +} + +void CColumnInfo::Validate( CValidator &validator, const char *pchName ) +{ + VALIDATE_SCOPE(); + +} + +#endif // DBGFLAG_VALIDATE + +} // namespace GCSDK diff --git a/gcsdk/sqlaccess/schema.cpp b/gcsdk/sqlaccess/schema.cpp new file mode 100644 index 0000000..e92be8e --- /dev/null +++ b/gcsdk/sqlaccess/schema.cpp @@ -0,0 +1,1505 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//============================================================================= + + +#include "stdafx.h" +//#include "sqlaccess/sqlaccess.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +namespace GCSDK +{ +#ifndef STEAM +bool isspace( char ch ) +{ + return ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r'; +} + +int Q_strnlen( const char *str, int count ) +{ + // can't make more meaningful checks, because this routine is used itself + // to check the NUL-terminatedness of strings + if ( !str || count < 0 ) + return -1; + + for ( const char *pch = str; pch < str + count; pch++ ) + { + if ( *pch == '\0' ) + return pch - str; + } + + return -1; +} + +#endif + +//----------------------------------------------------------------------------- +// Purpose: convert an ESchemaCatalog into a string for diagnostics and logging. +// this can't be in enum_names because of data type dependencies. +//----------------------------------------------------------------------------- +const char* PchNameFromESchemaCatalog( ESchemaCatalog e ) +{ + switch (e) + { + case k_ESchemaCatalogInvalid: + return "k_ESchemaCatalogInvalid"; + break; + + case k_ESchemaCatalogMain: + return "k_ESchemaCatalogMain"; + break; + } + + AssertMsg1( false, "unknown ESchemaCatalog (%d)", e ); + return "Unknown"; +} + + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CSchema::CSchema() +{ + m_iTable = -1; + m_rgchName[0] = 0; + m_cubRecord = 0; + m_bTestTable = false; + m_cRecordMax = 0; + m_bHasVarFields = false; + m_nHasPrimaryKey = k_EPrimaryKeyTypeNone; + m_iPKIndex = -1; + m_pRecordInfo = NULL; + m_wipePolicy = k_EWipePolicyPreserveAlways; + m_bAllowWipeInProd = false; + m_bPrepopulatedTable = false; + m_nFullTextIndexCatalog = -1; + m_eSchemaCatalog = k_ESchemaCatalogInvalid; +} + + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CSchema::~CSchema() +{ + SAFE_RELEASE( m_pRecordInfo ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Calculates offset of each field within a record structure, and the +// maximum length of a record structure. +//----------------------------------------------------------------------------- +void CSchema::CalcOffsets() +{ + int dubOffsetCur = 0; + + for ( int iField = 0; iField < m_VecField.Count(); iField++ ) + { + m_VecField[iField].m_dubOffset = dubOffsetCur; + dubOffsetCur += m_VecField[iField].m_cubLength; + + if ( m_VecField[iField].BIsVariableLength() ) + m_bHasVarFields = true; + } + + m_cubRecord = dubOffsetCur; + + if ( m_bHasVarFields ) + m_cubRecord += sizeof( VarFieldBlockInfo_t ); +} + + +//----------------------------------------------------------------------------- +// Purpose: called to make final calculations when all fields/indexes/etc have +// been added and the schema is ready to be used +//----------------------------------------------------------------------------- +void CSchema::PrepareForUse() +{ + // Create a record description (new form of schema information, for SQL) that corresponds to this schema object. + // This contains essentially the information as the CSchema object, we keep both for the moment to bridge the DS and SQL worlds. + Assert( !m_pRecordInfo ); + SAFE_RELEASE( m_pRecordInfo ); + m_pRecordInfo = CRecordInfo::Alloc(); + m_pRecordInfo->InitFromDSSchema( this ); +} + + + + +//----------------------------------------------------------------------------- +// Purpose: For a record that has variable-length fields, gets the info block from +// the tail end +// Input : pvRecord - Record data +//----------------------------------------------------------------------------- +VarFieldBlockInfo_t* CSchema::PVarFieldBlockInfoFromRecord( const void *pvRecord ) const +{ + if ( !m_bHasVarFields ) + return NULL; + + uint8 *pubRecord = ( uint8* )pvRecord; + return ( VarFieldBlockInfo_t * )( pubRecord + m_cubRecord ) - 1; +} + + +//----------------------------------------------------------------------------- +// Purpose: Return the total size of the variable-length block for this record +// For records that have no variable-length fields, it will return zero +// Input : pvRecord - Record data +//----------------------------------------------------------------------------- +uint32 CSchema::CubRecordVariable( const void *pvRecord ) const +{ + VarFieldBlockInfo_t *pVarFieldsBlockInfo = PVarFieldBlockInfoFromRecord( pvRecord ); + if ( pVarFieldsBlockInfo ) + return pVarFieldsBlockInfo->m_cubBlock; + else + return 0; +} + + + +void CSchema::RenderField( uint8 *pubRecord, int iField, int cchBuffer, char *pchBuffer ) +{ + Field_t *pField = &m_VecField[iField]; + + uint8 *pubData; + uint32 cubData; + char chEmpty = 0; + + if ( pField->BIsVariableLength() ) + { + if ( !BGetVarField( pubRecord, ( VarField_t * )( pubRecord + pField->m_dubOffset ), &pubData, &cubData ) ) + { + // just render a single byte + pubData = ( uint8* )&chEmpty; + cubData = 1; + } + } + else + { + pubData = pubRecord + pField->m_dubOffset; + cubData = m_VecField[iField].m_cubLength; + } + + ConvertFieldToText( pField->m_EType, pubData, cubData, pchBuffer, cchBuffer ); +} + +//----------------------------------------------------------------------------- +// Purpose: Renders a text version of a record to the console. +// Input : pubRecord - Location of the record data +//----------------------------------------------------------------------------- +void CSchema::RenderRecord( uint8 *pubRecord ) +{ + char rgchT[k_cMedBuff]; + + // First the header + EmitInfo( SPEW_CONSOLE, 2, 2, "%d\t*** Record header: # of lines ***\n", 1 + m_VecField.Count() ); + + // Render each field in turn + for ( int iField = 0; iField < m_VecField.Count(); iField++ ) + { + Field_t *pField = &m_VecField[iField]; + + RenderField( pubRecord, iField, Q_ARRAYSIZE(rgchT), rgchT ); + + EmitInfo( SPEW_CONSOLE, 2, 2, "\t%s\t\t// Field %d (%s)\n", rgchT, iField, pField->m_rgchName ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Get data and size of a field, whether fixed or variable length +// Input: pvRecord - Fixed-length part of record +// iField - index of field to get +// ppubField - receives pointer to field data +// pcubField - receives size in bytes of that data +// Output: true if successful +//----------------------------------------------------------------------------- +bool CSchema::BGetFieldData( const void *pvRecord, int iField, uint8 **ppubField, uint32 *pcubField ) const +{ + *ppubField = NULL; + *pcubField = 0; + const Field_t &field = m_VecField[iField]; + if ( field.BIsVariableLength() ) + { + return BGetVarField( pvRecord, ( VarField_t * )( ( uint8 * ) pvRecord + field.m_dubOffset ), ppubField, pcubField ); + } + else + { + *ppubField = ( ( uint8 * ) pvRecord + field.m_dubOffset ); + *pcubField = field.CubFieldUpdateSize(); + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Set data and size of a field, whether fixed or variable length +// Input: pvRecord - Fixed-length part of record +// iField - index of field to set +// pubField - pointer to field data to copy from +// cubField - size in bytes of that data +// Output: true if successful +//----------------------------------------------------------------------------- +bool CSchema::BSetFieldData( void *pvRecord, int iField, uint8 *pubField, uint32 cubField, bool *pbVarBlockRealloced ) +{ + *pbVarBlockRealloced = false; + + Field_t &field = m_VecField[iField]; + + if ( field.BIsVariableLength() ) + { + return BSetVarField( pvRecord, ( VarField_t * )( ( uint8 * ) pvRecord + field.m_dubOffset ), pubField, cubField, pbVarBlockRealloced, /*bFreeOnRealloc*/ true ); + } + else // fixed length + { + uint8 *pubFieldWrite = ( ( uint8 * ) pvRecord + field.m_dubOffset ); + + // Must fit in field and last byte for strings MUST be NULL + if ( cubField > field.m_cubLength || + ( k_EGCSQLType_String == field.m_EType && Q_strnlen( reinterpret_cast< char* >( pubField ), cubField ) == -1 ) ) + { + Assert( false ); + return false; + } + + if ( k_EGCSQLType_Blob != field.m_EType && k_EGCSQLType_Image != field.m_EType ) + { + // Copy the data (string or binary, doesn't matter) + if ( cubField > 0 ) + Q_memcpy( pubFieldWrite, pubField, cubField ); + + // Null termination and overwrite any old data + if ( cubField < field.m_cubLength ) + Q_memset( pubFieldWrite + cubField, 0, field.m_cubLength - cubField ); + } + else + { + // Only support fixed char or binary + Assert( false ); + return false; + } + } + + return true; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Get data from a variable-length field +// Input: pvRecord - Fixed-length part of record +// pVarField - fixed part of field in that record +// ppubField - receives pointer to field data +// pcubField - receives size in bytes of that data +//----------------------------------------------------------------------------- +bool CSchema::BGetVarField( const void *pvRecord, const VarField_t *pVarField, uint8 **ppubField, uint32 *pcubField ) const +{ + Assert( m_bHasVarFields ); + + *ppubField = 0; + *pcubField = 0; + + VarFieldBlockInfo_t *pVarFieldBlockInfo = PVarFieldBlockInfoFromRecord( pvRecord ); + + if ( !pVarField->m_cubField ) + { + *pcubField = 0; + *ppubField = NULL; + return true; + } + + if ( pVarFieldBlockInfo->m_cubBlock ) + { + uint8 *pubVarBlock = pVarFieldBlockInfo->m_pubBlock; + + *ppubField = pubVarBlock + pVarField->m_dubOffset; + *pcubField = pVarField->m_cubField; + + // Sanity check + Assert( *pcubField <= k_cubVarFieldMax ); + + return true; + } + else + { + // Should never happen + Assert( false ); + return false; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Update a variable-length field in a record (may realloc the var block) +// Input: pvRecord - Fixed-length part of record +// pVarField - fixed part of field in that record +// pvData - data to place in the field +// cubData - size of that data +// pbRealloced - set to indicate if the var block was realloced +// bFreeOnRealloc - If we have to grow or shrink the var block, should we free the old memory? +// Usually true, unless it lives in a NetPacket or something like that +//----------------------------------------------------------------------------- +bool CSchema::BSetVarField( void *pvRecord, VarField_t *pVarField, const void *pvData, uint32 cubData, bool *pbRealloced, bool bFreeOnRealloc ) +{ + VarFieldBlockInfo_t *pVarFieldBlockInfo = PVarFieldBlockInfoFromRecord( pvRecord ); + *pbRealloced = false; + + + if ( cubData > k_cubVarFieldMax ) + { + // field size is too big + Assert( false ); + return false; + } + + // if no block exists, allocate and copy into it + if ( pVarFieldBlockInfo->m_cubBlock == 0 ) + { + // Nothing to do? + if ( !cubData ) + return true; + + // create it + void *pvBlock = PvAlloc( cubData ); + *pbRealloced = true; + + // copy the data + Q_memcpy( pvBlock, pvData, cubData ); + + // set the record's structure to point at our new data + pVarFieldBlockInfo->m_cubBlock = cubData; + pVarFieldBlockInfo->m_pubBlock = ( uint8 * )pvBlock; + + // set the field to point at its landing place + pVarField->m_cubField = cubData; + pVarField->m_dubOffset = 0; + } + else + { + // there is some block available. + + // is this field changing size? + if ( cubData != 0 && ( cubData == pVarField->m_cubField ) ) + { + // no size change - no need to reallocate anything + Q_memcpy( pVarFieldBlockInfo->m_pubBlock + pVarField->m_dubOffset, pvData, cubData ); + } + else + { + // size change - realloc + *pbRealloced = true; + uint32 cubFieldOld = pVarField->m_cubField; + uint32 dubOffsetOld = pVarField->m_dubOffset; + uint32 cubBlockNew = pVarFieldBlockInfo->m_cubBlock - pVarField->m_cubField + cubData; + + if ( ( cubBlockNew + m_cubRecord ) > k_cubRecordMax ) + { + // total record size is too big + Assert( false ); + return false; + } + + void *pvBlockNew = NULL; + + if ( cubBlockNew ) + { + // if this field has never been placed in the block (that is, it's not changing an old value) + // and we have enough space, fastest to put it at the end in the free space. + if ( pVarField->m_cubField == 0 && pVarField->m_dubOffset == 0 && cubData <= pVarFieldBlockInfo->m_cubBlockFree ) + { + uint8 *pubLastUsed = pVarFieldBlockInfo->m_pubBlock; + for ( int iField = 0; iField < m_VecField.Count(); ++iField ) + { + Field_t &field = m_VecField[iField]; + if ( field.BIsVariableLength() ) + { + // Needs updating + VarField_t *pVarFieldCur = ( VarField_t * )( ( uint8 * )pvRecord + field.m_dubOffset ); + pubLastUsed += pVarFieldCur->m_cubField; + } + } + + // copy it there + Q_memcpy( pubLastUsed, pvData, cubData ); + + // set up the field + pVarField->m_cubField = cubData; + pVarField->m_dubOffset = static_cast<int>( (pubLastUsed - pVarFieldBlockInfo->m_pubBlock) ); + + // note that we used some up + pVarFieldBlockInfo->m_cubBlockFree -= cubData; + } + else + { + // yes ... rellocate + pvBlockNew = PvAlloc( cubBlockNew ); + + uint8 *pubBlockOldCursor = pVarFieldBlockInfo->m_pubBlock; + uint8 *pubBlockNewCursor = ( uint8* )pvBlockNew; + + // copy data, skipping over this field (will put at end) + while ( pubBlockOldCursor < ( pVarFieldBlockInfo->m_pubBlock + pVarFieldBlockInfo->m_cubBlock ) ) + { + if ( pVarField->m_cubField && ( (int)pVarField->m_dubOffset == ( pubBlockOldCursor - pVarFieldBlockInfo->m_pubBlock ) ) ) + { + pubBlockOldCursor += pVarField->m_cubField; + } + else + { + *pubBlockNewCursor++ = *pubBlockOldCursor++; + } + } + + // put this field data at the end + Q_memcpy( pubBlockNewCursor, pvData, cubData ); + + // free the old block + if ( bFreeOnRealloc ) + FreePv( pVarFieldBlockInfo->m_pubBlock ); + + // Update the block info + pVarFieldBlockInfo->m_cubBlock = cubBlockNew; + pVarFieldBlockInfo->m_pubBlock = ( uint8 * )pvBlockNew; + pVarFieldBlockInfo->m_cubBlockFree = 0; + + // update this field + pVarField->m_cubField = cubData; + if ( cubData > 0 ) + pVarField->m_dubOffset = static_cast<int>( pubBlockNewCursor - pVarFieldBlockInfo->m_pubBlock ); + else + pVarField->m_dubOffset = 0; + + // update other fields + for ( int iField = 0; iField < m_VecField.Count(); ++iField ) + { + Field_t &field = m_VecField[iField]; + if ( field.BIsVariableLength() ) + { + // Needs updating + VarField_t *pVarFieldCur = ( VarField_t * )( ( uint8 * )pvRecord + field.m_dubOffset ); + + // Except the one we just changed + if ( pVarFieldCur == pVarField ) + continue; + + // And empty fields + if ( !pVarFieldCur->m_cubField ) + continue; + + if ( pVarFieldCur->m_dubOffset > dubOffsetOld ) + pVarFieldCur->m_dubOffset -= cubFieldOld; + } + } + } + } + else + { + // all of the variable data is gone, so + // free the old block + if ( bFreeOnRealloc ) + FreePv( pVarFieldBlockInfo->m_pubBlock ); + + // ... update the block info + pVarFieldBlockInfo->m_cubBlock = 0; + pVarFieldBlockInfo->m_pubBlock = NULL; + pVarFieldBlockInfo->m_cubBlockFree = 0; + + // ... and update this field + pVarField->m_dubOffset = 0; + pVarField->m_cubField = cubData; + } + } + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: If this is a variable-length record, and we just read it from +// a stream, then the var block is after the fixed-length part of the +// record. So, we need to update the record's pointer to reflect that +// Input: pvRecord - Beginning of the serialized record +//----------------------------------------------------------------------------- +void CSchema::FixupDeserializedRecord( void *pvRecord ) +{ + // Nothing to do if not a variable record + if ( !BHasVariableFields() ) + return; + + uint8 *pubRecord = ( uint8 * )pvRecord; + VarFieldBlockInfo_t *pVarBlockInfo = PVarFieldBlockInfoFromRecord( pvRecord ); + pVarBlockInfo->m_pubBlock = pubRecord + m_cubRecord; +} + + +//----------------------------------------------------------------------------- +// Purpose: Initializes a new record with random data. +// Input: pubRecord - The record's in-memory data that we'll fill out +// unPrimaryIndex - Primary index of the record +// pbRealloced - Set to 'true' if the var-fields block for this record was reallocated +// (or allocated for the first time) +// bFreeOnRealloc - If true, we should Free() the var block after reallocating a new block +// (should be false if that block lives inside a NetPacket) +//----------------------------------------------------------------------------- +void CSchema::InitRecordRandom( uint8 *pubRecord, uint32 unPrimaryIndex, bool *pbVarBlockRealloced, bool bFreeVarBlockOnRealloc ) +{ + // Fill out each field in turn + for ( int iField = 0; iField < m_VecField.Count(); iField++ ) + { + bool bRealloced = false; + SetFieldRandom( pubRecord, iField, &bRealloced, bFreeVarBlockOnRealloc ); + if ( bRealloced ) + { + *pbVarBlockRealloced = true; + // if we just allocated it, we can free it next time we need to + bFreeVarBlockOnRealloc = true; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Sets a single field of a record to a random value. +// Input: pubRecord - Where the record lives in memory +// iField - Which field to set +// pbRealloced - Set to 'true' if the var-fields block for this record was reallocated +// (or allocated for the first time) +// bFreeOnRealloc - If true, we should Free() the var block after reallocating a new block +// (should be false if that block lives inside a NetPacket) +//----------------------------------------------------------------------------- +void CSchema::SetFieldRandom( uint8 *pubRecord, int iField, bool *pbVarBlockRealloced, bool bFreeVarBlockOnRealloc ) +{ + Field_t &field = m_VecField[iField]; + *pbVarBlockRealloced = false; + + // Strings get random text + if ( k_EGCSQLType_String == field.m_EType ) + { + // Generate up to cubLength - 1 chars + uint32 cch = UNRandFast() % field.m_cubLength; + uint32 ich = 0; + for ( ; ich < cch; ich++ ) + *( ( char * ) pubRecord + field.m_dubOffset + ich ) = CHRandFast(); + + // Null termination + for ( ; ich < field.m_cubLength; ich++ ) + *( ( char * ) pubRecord + field.m_dubOffset + ich ) = 0; + } + else if ( field.BIsVariableLength() ) + { + // Need temp buffer to randomize before setting + // kept reasonably small to prevent spamming the console + uint8 rgubBuff[512]; + uint32 cubData = UNRandFast() % sizeof(rgubBuff); + + // For strings, put in (cubData-1) random characters (each randomly in the range [32,126]) + // then a trailing NULL + if ( field.BIsStringType() ) + { + uint32 ich = 0; + for ( ; ich < (cubData-1); ich++ ) + rgubBuff[ich] = CHRandFast(); + + rgubBuff[ich] = 0; + } + else + { + // binary - just fill in random bytes + for ( uint32 iub = 0; iub < cubData; iub++ ) + { + rgubBuff[iub] = ( uint8 )( UNRandFast() % 256 ); + } + } + + // Set the variable field in the record + BSetVarField( pubRecord, ( VarField_t * ) ( pubRecord + field.m_dubOffset ), rgubBuff, cubData, pbVarBlockRealloced, bFreeVarBlockOnRealloc ); + } + // Binaries are filled with completely random bytes + else + { + for ( uint32 iub = 0; iub < field.m_cubLength; iub++ ) + { + *( pubRecord + field.m_dubOffset + iub ) = ( uint8 ) ( UNRandFast() % 256 ); + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns checksum of our contents +// Output : checksum +//----------------------------------------------------------------------------- +uint32 CSchema::CalcChecksum() +{ + CRC32_t crc32; + CRC32_Init( &crc32 ); + + FOR_EACH_VEC( m_VecField, nField ) + { + CRC32_ProcessBuffer( &crc32, &m_VecField[nField], sizeof( m_VecField[nField] ) ); + } + + // keep checksum for entire record info + CRC32_Final( &crc32 ); + + return (uint32)crc32; +} + + +void CSchema::AddIntField( char *pchName, char *pchSQLName, EGCSQLType eType, int cubSize ) +{ + int nExpectedSize = -1; + switch ( eType ) + { + case k_EGCSQLType_int8: + nExpectedSize = 1; + break; + + case k_EGCSQLType_int16: + nExpectedSize = 2; + break; + + case k_EGCSQLType_int32: + case k_EGCSQLType_float: + nExpectedSize = 4; + break; + + case k_EGCSQLType_int64: + case k_EGCSQLType_double: + nExpectedSize = 8; + break; + } + + AssertMsg2( nExpectedSize != -1, "Unexpected EType in AddIntField: %d for column %s", eType, pchSQLName ); + AssertMsg3( nExpectedSize == cubSize, "Unexpected size for in AddIntField for column %s: %d doesn't match %d ", pchSQLName, nExpectedSize, cubSize ); + + AddField( pchName, pchSQLName, eType, cubSize, 0 ); +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a field from our intrinsic schema to this schema. +// Input: pchName - Name of the field +// pchSQLName - Name of the field in SQL database +// eType - Type of the field +// cubSize - Size of the field +// pfnCompare - Function used to compare fields (NULL if none specified) +//----------------------------------------------------------------------------- +void CSchema::AddField( char *pchName, char *pchSQLName, EGCSQLType eType, uint32 cubSize, int cchMaxLength ) +{ + int iFieldNew = m_VecField.AddToTail(); + Field_t &field = m_VecField[iFieldNew]; + + field.m_EType = eType; + field.m_cubLength = cubSize; + field.m_nColFlags = 0; + Q_strncpy( field.m_rgchName, pchName, Q_ARRAYSIZE( field.m_rgchName ) ); + Q_strncpy( field.m_rgchSQLName, pchSQLName, Q_ARRAYSIZE( field.m_rgchSQLName ) ); + field.m_cchMaxLength = cchMaxLength; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Marks a given field as the primary index +// Input: +// bClustered - true to create a clustered index +// pchName - Name of the field to make primary index +//----------------------------------------------------------------------------- +int CSchema::PrimaryKey( bool bClustered, int nFillFactor, const char *pchName ) +{ + int iField = FindIField( pchName ); + AssertFatalMsg2( k_iFieldNil != iField, "Could not find index column \"%s\" on table \"%s\"", pchName, m_rgchName ); + + Assert( m_nHasPrimaryKey == k_EPrimaryKeyTypeNone ); // may not already have a primary key defined + Assert( m_iPKIndex == k_iFieldNil ); + + // must not have been indexed in any way before + Assert( 0 == ( m_VecField[ iField ].m_nColFlags & ( k_nColFlagPrimaryKey | k_nColFlagIndexed | k_nColFlagUnique | k_nColFlagClustered ) ) ); + + // note that we have a single-column PK + m_nHasPrimaryKey = k_EPrimaryKeyTypeSingle; + + // set our flags + m_VecField[ iField ].m_nColFlags |= ( k_nColFlagPrimaryKey | k_nColFlagIndexed | k_nColFlagUnique ); + if ( bClustered ) + m_VecField[ iField ].m_nColFlags |= k_nColFlagClustered; + + // add to list of primary keys and remember the indexID + CUtlVector<int> vecColumns; + vecColumns.AddToTail( iField ); + + FieldSet_t vecIndex( true /* unique */ , bClustered, vecColumns, NULL ); + vecIndex.SetFillFactor( nFillFactor ); + m_iPKIndex = m_VecIndexes.AddToTail( vecIndex ); + + return m_iPKIndex; +} + +//----------------------------------------------------------------------------- +// Purpose: Marks a set of fields as the primary index +// Input: +// bClustered - clustered index created if true +// nFillFactor - fill facto to use; 0 is database default +// pchName - Name of the field to make primary index +//----------------------------------------------------------------------------- +int CSchema::PrimaryKeys( bool bClustered, int nFillFactor, const char *pchNames ) +{ + Assert( pchNames != NULL ); // no bogus parameters, please + Assert( m_nHasPrimaryKey == k_EPrimaryKeyTypeNone ); // may not already have a primary key defined + Assert( m_iPKIndex == k_iFieldNil ); // no primary key defined + + int nFlags = k_nColFlagPrimaryKey | k_nColFlagIndexed | k_nColFlagUnique; + if ( bClustered ) + nFlags |= k_nColFlagClustered; + + // go add all those fields as Indexed + int nNewIndex = AddIndexToFieldList( pchNames, NULL, nFlags, nFillFactor ); + if ( nNewIndex != k_iFieldNil ) + { + m_nHasPrimaryKey = k_EPrimaryKeyTypeMulti; // remember that we have multiple keys + m_iPKIndex = nNewIndex; + } + + return nNewIndex; +} + +//----------------------------------------------------------------------------- +// Purpose: Marks a set of fields as the primary index +// Input: +// pchName - Names of the fields for the primary index; comma-separated if multiple +// pchIndexName - Name of the index object (not in SQL) +// nFlags - flags for CColumnInfo on this object +// nFillFactor - fill facto to use; 0 is database default +//----------------------------------------------------------------------------- +int CSchema::AddIndexToFieldList( const char *pchNames, const char *pchIndexName, int nFlags, int nFillFactor ) +{ + // need a copy of string list since the argument is const and read-only + int cNamesLen = Q_strlen( pchNames ) + 1; + char* pchNamesCopy = (char*) PvAlloc( cNamesLen ); + Q_strncpy( pchNamesCopy, pchNames, cNamesLen ); + + if (pchNames == NULL) + { + // not enough memory! + AssertFatal( false ); + return k_iFieldNil; + } + + CUtlVector<int> vecKeys; + + char* pchCurrent = pchNamesCopy; + do + { + // find next token + char* pchEnd = strchr(pchCurrent, ','); + if ( pchEnd != NULL ) + { + *pchEnd++ = 0; + } + + // skip whitespace + while (isspace(*pchCurrent)) + pchCurrent++; + + // find the name; we expect a C++ name, not a SQL name + int iField = FindIField( pchCurrent ); + AssertFatalMsg2( k_iFieldNil != iField, "Could not find index column \"%s\" on table \"%s\"", pchCurrent, m_rgchName ); + + // mark as a primary key + m_VecField[iField].m_nColFlags |= nFlags ; + + // add it to our collection + vecKeys.AddToTail( iField ); + + // move past the end of this token in the string + pchCurrent = pchEnd; + } while ( pchCurrent != NULL ); + + // release our copy + FreePv(pchNamesCopy); + + // create a fieldset with our list of indexes + // and add it to our indexes collection + bool bUnique = 0 != (nFlags & k_nColFlagUnique); + bool bClustered = 0 != (nFlags & k_nColFlagClustered); + FieldSet_t vecIndex( bUnique, bClustered, vecKeys, pchIndexName ); + vecIndex.SetFillFactor( nFillFactor ); + int iReturn = m_VecIndexes.AddToTail( vecIndex ); + return iReturn; +} + + +//----------------------------------------------------------------------------- +// Purpose: Marks a given field as indexed. +// Input: pchName - Name of the field to index +// pchIndexName - Name of the index object (not in SQL) +//----------------------------------------------------------------------------- +int CSchema::IndexField( const char *pchIndexName, const char *pchName ) +{ + Assert( pchName != NULL ); + Assert( pchIndexName != NULL ); + + // find the field by name + int iField = FindIField( pchName ); + AssertFatalMsg2( k_iFieldNil != iField, "Could not find index column \"%s\" on table \"%s\"", pchName, m_rgchName ); + + // add an index to it + return AddIndexToFieldNumber( iField, pchIndexName, false /* Not clustered */ ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Marks a given field as indexed. +// Input: pchIndexName - Name of the index object (not in SQL) +// pchName - Name of the fields to index; comma separated if multiple +//----------------------------------------------------------------------------- +int CSchema::IndexFields( const char *pchIndexName, const char *pchNames ) +{ + Assert( pchNames != NULL ); + Assert( pchIndexName != NULL ); + + // go add all those fields as Indexed + int nNewIndex = AddIndexToFieldList( pchNames, pchIndexName, k_nColFlagIndexed, 0 ); + return nNewIndex; +} + + +//----------------------------------------------------------------------------- +// Purpose: Includes a column (or columns) in an index for the INCLUDE clause +// Input: pchIndexName - Name of the index object (not in SQL) +// pchName - Name of the fields to index; comma separated if multiple +//----------------------------------------------------------------------------- +void CSchema::AddIncludedFields( const char *pchIndexName, const char *pchNames ) +{ + Assert( pchNames != NULL ); + Assert( pchIndexName != NULL ); + + // find that index by name + int nIndexIndex = -1; + FOR_EACH_VEC( m_VecIndexes, i ) + { + FieldSet_t &refSet = m_VecIndexes.Element(i); + const char *pstrMatch = refSet.GetIndexName(); + if ( Q_stricmp( pstrMatch, pchIndexName ) == 0 ) + { + nIndexIndex = i; + break; + } + } + + // must have found it + AssertFatalMsg1( nIndexIndex != -1, "Index \"%s\" not found", pchIndexName ); + FieldSet_t &refSet = m_VecIndexes.Element( nIndexIndex ); + + // need a copy of string list since the argument is const and read-only + int cNamesLen = Q_strlen( pchNames ) + 1; + char* pchNamesCopy = (char*) PvAlloc( cNamesLen ); + Q_strncpy( pchNamesCopy, pchNames, cNamesLen ); + + char* pchCurrent = pchNamesCopy; + do + { + // find next token + char* pchEnd = strchr(pchCurrent, ','); + if ( pchEnd != NULL ) + { + *pchEnd++ = 0; + } + + // skip whitespace + while (isspace(*pchCurrent)) + pchCurrent++; + + // find the name; we expect a C++ name, not a SQL name + int iField = FindIField( pchCurrent ); + AssertFatalMsg2( k_iFieldNil != iField, "Could not find index column \"%s\" on table \"%s\"", pchCurrent, m_rgchName ); + + // add it to our collection + refSet.AddIncludedColumn( iField ); + + // move past the end of this token in the string + pchCurrent = pchEnd; + } while ( pchCurrent != NULL ); + + // release our copy + FreePv( pchNamesCopy ); + + return; +} + +//----------------------------------------------------------------------------- +// Purpose: Marks a given field as indexed. +// Input: pchIndexName - Name of the index object (not in SQL) +// pchName - Name of the fields to index; comma separated if multiple +//----------------------------------------------------------------------------- +int CSchema::UniqueFields( const char *pchIndexName, const char *pchNames ) +{ + Assert( pchNames != NULL ); + Assert( pchIndexName != NULL ); + + // go add all those fields as Indexed + int nNewIndex = AddIndexToFieldList( pchNames, pchIndexName, k_nColFlagIndexed | k_nColFlagUnique, 0 ); + return nNewIndex; +} + + +//----------------------------------------------------------------------------- +// Purpose: Marks a given field a having a clustered index. +// Input: pchName - Name of the field to index +// pchIndexName - Name of the index object (not in SQL) +//----------------------------------------------------------------------------- +int CSchema::ClusteredIndexField( int nFillFactor, const char *pchIndexName, const char *pchName ) +{ + Assert( pchName != NULL ); + Assert( pchIndexName != NULL ); + + // can't previously have some other clustered index + FOR_EACH_VEC( m_VecIndexes, iIndex ) + { + if ( m_VecIndexes[iIndex].IsClustered() ) + { + AssertFatalMsg3( false, "On table %s, can't make index %s clustered because %s is already the clustered index\n", + m_rgchName, pchIndexName, m_VecIndexes[iIndex].GetIndexName() ); + return -1; + } + } + + // find the field by name + int iField = FindIField( pchName ); + AssertFatalMsg2( k_iFieldNil != iField, "Could not find index column \"%s\" on table \"%s\"", pchName, m_rgchName ); + + // add an index to it + int iIndex = AddIndexToFieldNumber( iField, pchIndexName, true /* clustered */ ); + m_VecIndexes[iIndex].SetFillFactor( nFillFactor ); + return iIndex; +} + + +//----------------------------------------------------------------------------- +// Purpose: Marks a given set of fields as having a clustered index. +// Input: pchIndexName - Name of the index object (not in SQL) +// pchName - Name of the fields to index; comma separated if multiple +//----------------------------------------------------------------------------- +int CSchema::ClusteredIndexFields( int nFillFactor, const char *pchIndexName, const char *pchNames ) +{ + Assert( pchNames != NULL ); + Assert( pchIndexName != NULL ); + + // can't previously have some other clustered index + FOR_EACH_VEC( m_VecIndexes, iIndex ) + { + if ( m_VecIndexes[iIndex].IsClustered() ) + { + AssertFatalMsg3( false, "On table %s, can't make index %s clustered because %s is already the clustered index\n", + m_rgchName, pchIndexName, m_VecIndexes[iIndex].GetIndexName() ); + return -1; + } + } + + // go add all those fields as Indexed + int nNewIndex = AddIndexToFieldList( pchNames, pchIndexName, k_nColFlagClustered | k_nColFlagIndexed, nFillFactor ); + return nNewIndex; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CSchema::AddFullTextIndex( CSchemaFull *pSchemaFull, const char *pchCatalogName, const char *pchColumnName ) +{ + Assert( pchCatalogName != NULL ); + Assert( pchColumnName != NULL ); + + // need a copy of string list since the argument is const and read-only + int cNamesLen = Q_strlen( pchColumnName ) + 1; + char* pchNamesCopy = (char*) PvAlloc( cNamesLen ); + Q_strncpy( pchNamesCopy, pchColumnName, cNamesLen ); + + if ( pchNamesCopy == NULL ) + { + // not enough memory! + AssertFatal( false ); + return; + } + + char* pchCurrent = pchNamesCopy; + do + { + // find next token + char* pchEnd = strchr(pchCurrent, ','); + if ( pchEnd != NULL ) + { + *pchEnd++ = 0; + } + + // skip whitespace + while (isspace(*pchCurrent)) + pchCurrent++; + + // find the name; we expect a C++ name, not a SQL name + int iField = FindIField( pchCurrent ); + AssertFatalMsg2( k_iFieldNil != iField, "Could not find index column \"%s\" on table \"%s\"", pchCurrent, m_rgchName ); + + // add it to our collection + m_VecFullTextIndexes.AddToTail( iField ); + + // move past the end of this token in the string + pchCurrent = pchEnd; + } while ( pchCurrent != NULL ); + + // release our copy + FreePv(pchNamesCopy); + + // make a note of the catalog we want + int nCatalogID = pSchemaFull->GetFTSCatalogByName( m_eSchemaCatalog, pchCatalogName ); + AssertFatalMsg2( nCatalogID != -1, "Could not find fulltext catalog \"%s\" on table \"%s\"", pchCatalogName, m_rgchName ); + m_nFullTextIndexCatalog = nCatalogID; + + return; +} + + +//----------------------------------------------------------------------------- +// Purpose: Marks a field as indexed given its field number +//----------------------------------------------------------------------------- +int CSchema::AddIndexToFieldNumber( int iField, const char *pchIndexName, bool bClustered ) +{ + // mark the field as indexed + m_VecField[iField].m_nColFlags |= k_nColFlagIndexed; + + // meant to be clustered? + if ( bClustered ) + m_VecField[iField].m_nColFlags |= k_nColFlagClustered; + + // add to list of indexes, and remember the indexID + CUtlVector<int> vecColumns; + vecColumns.AddToTail( iField ); + + // false: not unique + // false: not clustered + FieldSet_t vecIndex( false, bClustered, vecColumns, pchIndexName); + int iIndex = m_VecIndexes.AddToTail( vecIndex ); + + return iIndex; +} + + +//----------------------------------------------------------------------------- +// Purpose: Marks a given field as unique. +// Input: pchName - Name of the field to index +// pchIndexName - Name of the index object +//----------------------------------------------------------------------------- +int CSchema::UniqueField( const char *pchIndexName, const char *pchName ) +{ + Assert( pchName != NULL ); + Assert( pchIndexName != NULL ); + + int iField = FindIField( pchName ); + AssertFatalMsg2( k_iFieldNil != iField, "Could not find index column \"%s\" on table \"%s\"", pchName, m_rgchName ); + + m_VecField[iField].m_nColFlags |= ( k_nColFlagUnique | k_nColFlagIndexed ); + + // add to list of indexes, and remember the indexID + CUtlVector<int> vecColumns; + vecColumns.AddToTail( iField ); + + // true: unique + // false: not clustered + FieldSet_t vecIndex( true, false, vecColumns, pchIndexName ); + int iIndex = m_VecIndexes.AddToTail( vecIndex ); + + return iIndex; +} + + +//----------------------------------------------------------------------------- +// Purpose: Marks a given field as auto increment. +// Input: pchName - Name of the field +//----------------------------------------------------------------------------- +void CSchema::AutoIncrementField( char *pchName ) +{ + int iField = FindIField( pchName ); + AssertFatalMsg2( k_iFieldNil != iField, "Could not find index column \"%s\" on table \"%s\"", pchName, m_rgchName ); + m_VecField[iField].m_nColFlags |= k_nColFlagAutoIncrement; +} + + +//----------------------------------------------------------------------------- +// Purpose: Finds the field with a given name. +// Input: pchName - Name of the field to search for +// Output: Index of the matching field (k_iFieldNil if there isn't one) +//----------------------------------------------------------------------------- +int CSchema::FindIField( const char *pchName ) +{ + for ( int iField = 0; iField < m_VecField.Count(); iField++ ) + { + if ( 0 == Q_strncmp( pchName, m_VecField[iField].m_rgchName, k_cSQLObjectNameMax ) ) + return iField; + } + + return k_iFieldNil; +} + + +//----------------------------------------------------------------------------- +// Purpose: Finds the field with a given SQL name. +// Input: pchName - Name of the field to search for +// Output: Index of the matching field (k_iFieldNil if there isn't one) +//----------------------------------------------------------------------------- +int CSchema::FindIFieldSQL( const char *pchName ) +{ + for ( int iField = 0; iField < m_VecField.Count(); iField++ ) + { + if ( 0 == Q_strncmp( pchName, m_VecField[iField].m_rgchSQLName, k_cSQLObjectNameMax ) ) + return iField; + } + + return k_iFieldNil; +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a schema conversion instruction (for use in converting from +// a different Schema to this one). +//----------------------------------------------------------------------------- +void CSchema::AddDeleteField( const char *pchFieldName ) +{ + DeleteField_t &deleteField = m_VecDeleteField[m_VecDeleteField.AddToTail()]; + Q_strncpy( deleteField.m_rgchFieldName, pchFieldName, sizeof( deleteField.m_rgchFieldName ) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a schema conversion instruction (for use in converting from +// a different Schema to this one). +//----------------------------------------------------------------------------- +void CSchema::AddRenameField( const char *pchFieldNameOld, const char *pchFieldNameNew ) +{ + RenameField_t &renameField = m_VecRenameField[m_VecRenameField.AddToTail()]; + Q_strncpy( renameField.m_rgchFieldNameOld, pchFieldNameOld, sizeof( renameField.m_rgchFieldNameOld ) ); + renameField.m_iFieldDst = FindIField( pchFieldNameNew ); + Assert( k_iFieldNil != renameField.m_iFieldDst ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a schema conversion instruction (for use in converting from +// a different Schema to this one). +// Input: pchFieldNameOld - Name of the field in the old schema +// pchFieldNameMew - Name of the field in the new schema +// pfnAlterField - Function to translate data from the old format to +// the new +//----------------------------------------------------------------------------- +void CSchema::AddAlterField( const char *pchFieldNameOld, const char *pchFieldNameNew, PfnAlterField_t pfnAlterField ) +{ + Assert( pfnAlterField ); + AlterField_t &alterField = m_VecAlterField[m_VecAlterField.AddToTail()]; + Q_strncpy( alterField.m_rgchFieldNameOld, pchFieldNameOld, sizeof( alterField.m_rgchFieldNameOld ) ); + alterField.m_iFieldDst = FindIField( pchFieldNameNew ); + Assert( k_iFieldNil != alterField.m_iFieldDst ); + alterField.m_pfnAlterFunc = pfnAlterField; +} + + +//----------------------------------------------------------------------------- +// Purpose: Figures out how to map a field from another Schema into us. +// First we check our conversion instructions to see if any apply, +// and then we look for a straightforward match. +// Input: pchFieldName - Name of the field we're trying to map +// piFieldDst - [Return] Index of the field to map it to +// ppfnAlterField - [Return] Optional function to convert data +// Output: true if we know what to do with this field (if false, the conversion +// is undefined and dangerous). +//----------------------------------------------------------------------------- +bool CSchema::BCanConvertField( const char *pchFieldName, int *piFieldDst, PfnAlterField_t *ppfnAlterField ) +{ + *ppfnAlterField = NULL; + + // Should this field be deleted? + for ( int iDeleteField = 0; iDeleteField < m_VecDeleteField.Count(); iDeleteField++ ) + { + if ( 0 == Q_strcmp( pchFieldName, m_VecDeleteField[iDeleteField].m_rgchFieldName ) ) + { + *piFieldDst = k_iFieldNil; + return true; + } + } + + // Should this field be renamed? + for ( int iRenameField = 0; iRenameField < m_VecRenameField.Count(); iRenameField++ ) + { + if ( 0 == Q_strcmp( pchFieldName, m_VecRenameField[iRenameField].m_rgchFieldNameOld ) ) + { + *piFieldDst = m_VecRenameField[iRenameField].m_iFieldDst; + return true; + } + } + + // Was this field altered? + for ( int iAlterField = 0; iAlterField < m_VecAlterField.Count(); iAlterField++ ) + { + if ( 0 == Q_strcmp( pchFieldName, m_VecAlterField[iAlterField].m_rgchFieldNameOld ) ) + { + *piFieldDst = m_VecAlterField[iAlterField].m_iFieldDst; + *ppfnAlterField = m_VecAlterField[iAlterField].m_pfnAlterFunc; + return true; + } + } + + // Find out which of our fields this field maps to (if it doesn't map + // to any of them, we don't know what to do with it). + *piFieldDst = FindIField( pchFieldName ); + return ( k_iFieldNil != *piFieldDst ); +} + +//----------------------------------------------------------------------------- +// Purpose: Return the size to use when writing to a field. +//----------------------------------------------------------------------------- +int Field_t::CubFieldUpdateSize() const +{ + switch ( m_EType ) + { + case k_EGCSQLType_String: + case k_EGCSQLType_Blob: + case k_EGCSQLType_Image: + // Nobody should call this function for + // var-length fields + Assert( false ); + return m_cubLength; + + case k_EGCSQLType_int8: + case k_EGCSQLType_int16: + case k_EGCSQLType_int32: + case k_EGCSQLType_int64: + case k_EGCSQLType_float: + case k_EGCSQLType_double: + return m_cubLength; + + default: + Assert(false); + return 0; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Tell whether or not a field is of string type. +//----------------------------------------------------------------------------- +bool Field_t::BIsStringType() const +{ + return ( k_EGCSQLType_String == m_EType ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Tell whether or not the field is of variable-length type. +//----------------------------------------------------------------------------- +bool Field_t::BIsVariableLength() const +{ + return ( k_EGCSQLType_String == m_EType ) || ( k_EGCSQLType_Blob == m_EType ) || ( k_EGCSQLType_Image == m_EType ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Add a foreign key +//----------------------------------------------------------------------------- +void CSchema::AddFK( const char* pchName, const char* pchColumn, const char* pchParentTable, const char* pchParentColumn, EForeignKeyAction eOnDeleteAction, EForeignKeyAction eOnUpdateAction ) +{ + int iTail = m_VecFKData.AddToTail(); + FKData_t &fkData = m_VecFKData[iTail]; + + Q_snprintf( fkData.m_rgchName, Q_ARRAYSIZE( fkData.m_rgchName ), "%s_%s", GetPchName(), pchName ); + Q_strncpy( fkData.m_rgchParentTableName, pchParentTable, Q_ARRAYSIZE( fkData.m_rgchParentTableName ) ); + fkData.m_eOnDeleteAction = eOnDeleteAction; + fkData.m_eOnUpdateAction = eOnUpdateAction; + + // Now we need to split up the column name strings and add their data + FKColumnRelation_t colRelation; + Q_memset( &colRelation, 0, sizeof( FKColumnRelation_t ) ); + uint iMyColumn = 0; + uint iParentColumn = 0; + uint iParentString = 0; + for( uint i=0; i<(uint)Q_strlen( pchColumn )+1; ++i ) + { + if ( pchColumn[i] != ',' && pchColumn[i] != 0 ) + { + colRelation.m_rgchCol[ iMyColumn++ ] = pchColumn[i]; + } + else + { + Assert( Q_strlen( colRelation.m_rgchCol ) ); + // Should have a matching column name in the parent string + while( iParentString < (uint)Q_strlen( pchParentColumn ) ) + { + if ( pchParentColumn[iParentString] != ',' ) + { + colRelation.m_rgchParentCol[ iParentColumn++ ] = pchParentColumn[iParentString]; + ++iParentString; + } + else + { + ++iParentString; + break; + } + } + + AssertMsg( Q_strlen( colRelation.m_rgchParentCol ), "Column counts for FK do not match between child/parent\n" ); + fkData.m_VecColumnRelations.AddToTail( colRelation ); + Q_memset( &colRelation, 0, sizeof( FKColumnRelation_t ) ); + // Reset positions + iMyColumn = 0; + iParentColumn = 0; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Caches the insert statement for each table so we don't need to +// generate it for each row we insert. +//----------------------------------------------------------------------------- +const char *CSchema::GetInsertStatementText() const +{ + if ( m_sInsertStatementText.IsEmpty() ) + { + TSQLCmdStr sBuilder; + BuildInsertStatementText( &sBuilder, GetRecordInfo() ); + m_sInsertStatementText.Set( sBuilder ); + } + + return m_sInsertStatementText; +} + +//----------------------------------------------------------------------------- +// Purpose: Caches the insert via MERGE statement for each table so we don't need to +// generate it for each row we insert. +//----------------------------------------------------------------------------- +const char *CSchema::GetMergeStatementTextOnPKWhenMatchedUpdateWhenNotMatchedInsert() +{ + if ( m_sMergeStatementTextOnPKWhenMatchedUpdateWhenNotMatchedInsert.IsEmpty() ) + { + TSQLCmdStr sBuilder; + BuildMergeStatementTextOnPKWhenMatchedUpdateWhenNotMatchedInsert( &sBuilder, GetRecordInfo() ); + m_sMergeStatementTextOnPKWhenMatchedUpdateWhenNotMatchedInsert.Set( sBuilder ); + } + + return m_sMergeStatementTextOnPKWhenMatchedUpdateWhenNotMatchedInsert; +} + + +//----------------------------------------------------------------------------- +// Purpose: Caches the insert via MERGE statement for each table so we don't need to +// generate it for each row we insert. +//----------------------------------------------------------------------------- +const char *CSchema::GetMergeStatementTextOnPKWhenNotMatchedInsert() +{ + if ( m_sMergeStatementTextOnPKWhenNotMatchedInsert.IsEmpty() ) + { + TSQLCmdStr sBuilder; + BuildMergeStatementTextOnPKWhenNotMatchedInsert( &sBuilder, GetRecordInfo() ); + m_sMergeStatementTextOnPKWhenNotMatchedInsert.Set( sBuilder ); + } + + return m_sMergeStatementTextOnPKWhenNotMatchedInsert; +} + +//----------------------------------------------------------------------------- +// Purpose: Get the number of foreign key constraints defined for the table +//----------------------------------------------------------------------------- +int CSchema::GetFKCount() +{ + return m_VecFKData.Count(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get data for a foreign key by index (valid for 0...GetFKCount()-1) +//----------------------------------------------------------------------------- +FKData_t &CSchema::GetFKData( int iIndex ) +{ + return m_VecFKData[iIndex]; +} + + +#ifdef DBGFLAG_VALIDATE +//----------------------------------------------------------------------------- +// Purpose: Run a global validation pass on all of our data structures and memory +// allocations. +// Input: validator - Our global validator object +// pchName - Our name (typically a member var in our container) +//----------------------------------------------------------------------------- +void CSchema::Validate( CValidator &validator, const char *pchName ) +{ + // 1. + // Claim our memory + VALIDATE_SCOPE(); + + m_VecField.Validate( validator, "m_VecField" ); + m_VecDeleteField.Validate( validator, "m_VecDeleteField" ); + m_VecRenameField.Validate( validator, "m_VecRenameField" ); + m_VecIndexes.Validate( validator, "m_VecIndexes" ); + m_VecFullTextIndexes.Validate( validator, "m_VecFullTextIndexes" ); + ValidateObj( m_VecFKData ); + FOR_EACH_VEC( m_VecFKData, i ) + { + ValidateObj( m_VecFKData[i] ); + } + + for ( int iIndex = 0; iIndex < m_VecIndexes.Count(); iIndex++ ) + { + ValidateObj( m_VecIndexes[iIndex] ); + } + + ValidateObj( m_sInsertStatementText ); + + // 2. + // Validate that our fields make sense +#if defined(_DEBUG) + uint32 dubOffset = 0; + for ( int iField = 0; iField < m_VecField.Count(); iField++ ) + { + Assert( dubOffset == m_VecField[iField].m_dubOffset ); + dubOffset += m_VecField[iField].m_cubLength; + } +#endif // defined(_DEBUG) +} + + +//----------------------------------------------------------------------------- +// Purpose: Validates that a given record from our table is in a good state. +// Input: pubRecord - Record to validate +//----------------------------------------------------------------------------- +void CSchema::ValidateRecord( uint8 *pubRecord ) +{ + // Make sure each record is in a consistent state + for ( int iField = 0; iField < m_VecField.Count(); iField++ ) + { + Field_t &field = m_VecField[iField]; + + // Ensure that strings are null-terminated, and that everything after the terminator is 0 + if ( k_EGCSQLType_String == field.m_EType ) + { + char *pchField = ( char * ) pubRecord + field.m_dubOffset; + Assert( 0 == pchField[field.m_cubLength - 1] ); + for ( uint32 ich = 0; ich < field.m_cubLength; ich++ ) + { + if ( 0 == pchField[ich] ) + { + while ( ich < field.m_cubLength ) + { + Assert( 0 == pchField[ich] ); + ich++; + } + } + } + } + } +} +#endif // DBGFLAG_VALIDATE +} // namespace GCSDK diff --git a/gcsdk/sqlaccess/schemafull.cpp b/gcsdk/sqlaccess/schemafull.cpp new file mode 100644 index 0000000..d8e9490 --- /dev/null +++ b/gcsdk/sqlaccess/schemafull.cpp @@ -0,0 +1,409 @@ +//========= 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 +{ +CSchemaFull g_SchemaFull; +CSchemaFull & GSchemaFull() +{ + return g_SchemaFull; +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CSchemaFull::CSchemaFull() +{ + m_pubScratchBuffer = NULL; + m_cubScratchBuffer = 0; + m_unCheckSum = 0; + + m_mapFTSEnabled.SetLessFunc( DefLessFunc( enum ESchemaCatalog ) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CSchemaFull::~CSchemaFull() +{ + Uninit(); +} + + + +//----------------------------------------------------------------------------- +// Purpose: Call this after you've finished setting up the SchemaFull (either +// by loading it or by in GenerateIntrinsic). It calculates our checksum +// and allocates our scratch buffer. +//----------------------------------------------------------------------------- +void CSchemaFull::FinishInit() +{ + // Calculate our checksum + m_unCheckSum = 0; + + for ( int iSchema = 0; iSchema < m_VecSchema.Count(); iSchema++ ) + m_unCheckSum += m_VecSchema[iSchema].CalcChecksum(); + + // Allocate our scratch buffer + Assert( NULL == m_pubScratchBuffer ); + // Include some slop for field IDs and sizes in a sparse record + // 2k is way overkill but still no big deal + m_cubScratchBuffer = k_cubRecordMax + 2048; + m_pubScratchBuffer = ( uint8 * ) malloc( m_cubScratchBuffer ); +} + +//----------------------------------------------------------------------------- +// Purpose: Call this after you've finished setting up the SchemaFull (either +// by loading it or by in GenerateIntrinsic). It calculates our checksum +// and allocates our scratch buffer. +//----------------------------------------------------------------------------- + +void CSchemaFull::SetITable( CSchema* pSchema, int iTable ) +{ + // make sure we don't have this schema anywhere already + for ( int iSchema = 0; iSchema < m_VecSchema.Count(); iSchema++ ) + { + if ( pSchema != &m_VecSchema[iSchema] ) + AssertFatalMsg( m_VecSchema[iSchema].GetITable() != iTable, "Duplicate iTable in schema definition.\n" ); + } + + // set the pSchema object + pSchema->SetITable( iTable ); +} + + + +//----------------------------------------------------------------------------- +// Purpose: Uninits the schema. Need to call this explicitly before app shutdown +// on static instances of this object, as the CSchema objects +// point to memory in static memory pools which may destruct +// before static instances of this object. +//----------------------------------------------------------------------------- +void CSchemaFull::Uninit() +{ + m_VecSchema.RemoveAll(); + if ( NULL != m_pubScratchBuffer ) + { + free( m_pubScratchBuffer ); + m_pubScratchBuffer = NULL; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Get the scratch buffer. It is large enough to handle any +// record, sparse or otherwise +// +//----------------------------------------------------------------------------- +uint8* CSchemaFull::GetPubScratchBuffer( ) +{ + return m_pubScratchBuffer; +} + + +//----------------------------------------------------------------------------- +// Purpose: This is used during the generation of our intrinsic schema. We've +// added a new schema to ourselves, and we need to make sure that it +// matches the corresponding C class. +// Input: pSchema - Schema to check +// cField - Number of fields the schema should contain. +// cubRecord - Size of a record in the schema +//----------------------------------------------------------------------------- +void CSchemaFull::CheckSchema( CSchema *pSchema, int cField, uint32 cubRecord ) +{ + // We generate our structures and our schema using macros that operate on the + // same source. We check a couple of things to make sure that they're properly in sync. + + // This will fail if the schema's definition specifies the wrong iTable + if ( pSchema != &m_VecSchema[pSchema->GetITable()] ) + { + EmitError( SPEW_SQL, "Table %s has a bad iTable\n", pSchema->GetPchName() ); + } + + // This will fail if there are missing lines in the schema definition + if ( pSchema->GetCField() != cField ) + { + EmitError( SPEW_SQL, "Badly formed table %s (blank line in schema def?)\n", pSchema->GetPchName() ); + AssertFatal( false ); + } + + // This is unlikely to fail. It indicates some kind of size mismatch (maybe a packing problem?) + if ( pSchema->CubRecordFixed() != cubRecord ) + { + // You may hit this if END_FIELDDATA_HAS_VAR_FIELDS is not used properly + EmitError( SPEW_SQL, "Table %s has an inconsistent size (class = %d, schema = %d)\n", + pSchema->GetPchName(), cubRecord, pSchema->CubRecordFixed() ); + AssertFatal( false ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Finds the table with a given name. +// Input: pchName - Name of the table to search for +// Output: Index of the matching table ( k_iTableNil if there isn't one) +//----------------------------------------------------------------------------- +int CSchemaFull::FindITable( const char *pchName ) +{ + for ( int iSchema = 0; iSchema < m_VecSchema.Count(); iSchema++ ) + { + if ( 0 == Q_strcmp( pchName, m_VecSchema[iSchema].GetPchName() ) ) + return iSchema; + } + + return k_iTableNil; +} + + +//----------------------------------------------------------------------------- +// Purpose: Finds the table with a given iTable (iSchema) +// Input: iTable - +// Output: NULL or a const char * to the name (for temporary use only) +//----------------------------------------------------------------------------- +const char * CSchemaFull::PchTableFromITable( int iTable ) +{ + if ( iTable < 0 || iTable >= m_VecSchema.Count() ) + return NULL; + else + return m_VecSchema[ iTable ].GetPchName(); +} + + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +void CSchemaFull::AddFullTextCatalog( enum ESchemaCatalog eCatalog, const char *pstrCatalogName, int nFileGroup ) +{ + CFTSCatalogInfo info; + info.m_eCatalog = eCatalog; + info.m_nFileGroup = nFileGroup; + info.m_pstrName = strdup(pstrCatalogName); + + m_vecFTSCatalogs.AddToTail( info ); +} + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CSchemaFull::GetFTSCatalogByName( enum ESchemaCatalog eCatalog, const char *pstrCatalogName ) +{ + int nIndex = -1; + FOR_EACH_VEC( m_vecFTSCatalogs, i ) + { + CFTSCatalogInfo &refInfo = m_vecFTSCatalogs[ i ]; + if ( 0 == Q_stricmp( pstrCatalogName, refInfo.m_pstrName ) ) + { + nIndex = i; + break; + } + } + + return nIndex; +} + + +//----------------------------------------------------------------------------- +// Purpose: turn on FTS for the named schema catalog. Called by the +// InitIntrinsic() function. +//----------------------------------------------------------------------------- +void CSchemaFull::EnableFTS( enum ESchemaCatalog eCatalog ) +{ + // mark it enabled in the map + m_mapFTSEnabled.Insert( eCatalog, true ); +} + + +//----------------------------------------------------------------------------- +// Purpose: is FTS enabled for the supplied schema catalog? +//----------------------------------------------------------------------------- +bool CSchemaFull::GetFTSEnabled( enum ESchemaCatalog eCatalog ) +{ + int iEntry = m_mapFTSEnabled.Find( eCatalog ); + if ( iEntry == m_mapFTSEnabled.InvalidIndex() ) + return false; + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a schema conversion instruction (for use in converting from +// a different SchemaFull to this one). +//----------------------------------------------------------------------------- +void CSchemaFull::AddDeleteTable( const char *pchTableName ) +{ + DeleteTable_t &deleteTable = m_VecDeleteTable[m_VecDeleteTable.AddToTail()]; + Q_strncpy( deleteTable.m_rgchTableName, pchTableName, sizeof( deleteTable.m_rgchTableName ) ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a schema conversion instruction (for use in converting from +// a different SchemaFull to this one). +//----------------------------------------------------------------------------- +void CSchemaFull::AddRenameTable( const char *pchTableNameOld, const char *pchTableNameNew ) +{ + RenameTable_t &renameTable = m_VecRenameTable[m_VecRenameTable.AddToTail()]; + Q_strncpy( renameTable.m_rgchTableNameOld, pchTableNameOld, sizeof( renameTable.m_rgchTableNameOld ) ); + renameTable.m_iTableDst = FindITable( pchTableNameNew ); + Assert( k_iTableNil != renameTable.m_iTableDst ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a schema conversion instruction (for use in converting from +// a different SchemaFull to this one). +//----------------------------------------------------------------------------- +void CSchemaFull::AddDeleteField( const char *pchTableName, const char *pchFieldName ) +{ + int iSchema = FindITable( pchTableName ); + AssertFatal( k_iTableNil != iSchema ); + + m_VecSchema[iSchema].AddDeleteField( pchFieldName ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a schema conversion instruction (for use in converting from +// a different SchemaFull to this one). +//----------------------------------------------------------------------------- +void CSchemaFull::AddRenameField( const char *pchTableName, const char *pchFieldNameOld, const char *pchFieldNameNew ) +{ + int iSchema = FindITable( pchTableName ); + AssertFatal( k_iTableNil != iSchema ); + + m_VecSchema[iSchema].AddRenameField( pchFieldNameOld, pchFieldNameNew ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a schema conversion instruction (for use in converting from +// a different SchemaFull to this one). +//----------------------------------------------------------------------------- +void CSchemaFull::AddAlterField( const char *pchTableName, const char *pchFieldNameOld, const char *pchFieldnameNew, PfnAlterField_t pfnAlterField ) +{ + int iSchema = FindITable( pchTableName ); + AssertFatal( k_iTableNil != iSchema ); + + m_VecSchema[iSchema].AddAlterField( pchFieldNameOld, pchFieldnameNew, pfnAlterField ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Add a trigger to the desired schema +//----------------------------------------------------------------------------- +void CSchemaFull::AddTrigger( ESchemaCatalog eCatalog, const char *pchTableName, const char *pchTriggerName, ETriggerType eTriggerType, const char *pchTriggerText ) +{ + CTriggerInfo trigger; + trigger.m_eTriggerType = eTriggerType; + trigger.m_eSchemaCatalog = eCatalog; + Q_strncpy( trigger.m_szTriggerName, pchTriggerName, Q_ARRAYSIZE( trigger.m_szTriggerName ) ); + Q_strncpy( trigger.m_szTriggerTableName, pchTableName, Q_ARRAYSIZE( trigger.m_szTriggerTableName ) ); + trigger.m_strText = pchTriggerText; + + // add it to our list + m_VecTriggers.AddToTail( trigger ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Figures out how to map a table from another SchemaFull into us. +// First we check our conversion instructions to see if any apply, +// and then we look for a straightforward match. +// Input: pchTableName - Name of the table we're trying to map +// piTableDst - [Return] Index of the table to map it to +// Output: true if we know what to do with this table (if false, the conversion +// is undefined and dangerous). +//----------------------------------------------------------------------------- +bool CSchemaFull::BCanConvertTable( const char *pchTableName, int *piTableDst ) +{ + // Should this table be deleted? + for ( int iDeleteTable = 0; iDeleteTable < m_VecDeleteTable.Count(); iDeleteTable++ ) + { + if ( 0 == Q_strcmp( pchTableName, m_VecDeleteTable[iDeleteTable].m_rgchTableName ) ) + { + *piTableDst = k_iTableNil; + return true; + } + } + + // Should this table be renamed? + for ( int iRenameTable = 0; iRenameTable < m_VecRenameTable.Count(); iRenameTable++ ) + { + if ( 0 == Q_strcmp( pchTableName, m_VecRenameTable[iRenameTable].m_rgchTableNameOld ) ) + { + *piTableDst = m_VecRenameTable[iRenameTable].m_iTableDst; + return true; + } + } + + // Find out which of our tables this table maps to (if it doesn't map + // to any of them, we don't know what to do with it). + *piTableDst = FindITable( pchTableName ); + return ( k_iTableNil != *piTableDst ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets the default SQL schema name for a catalog +//----------------------------------------------------------------------------- +const char *CSchemaFull::GetDefaultSchemaNameForCatalog( ESchemaCatalog eCatalog ) +{ + // For all catalogs it's actually the same + if ( m_strDefaultSchemaName.IsEmpty() ) + { + m_strDefaultSchemaName.Set( CFmtStr( "App%u", GGCBase()->GetAppID() ) ); + } + + return m_strDefaultSchemaName.Get(); +} + + +#ifdef DBGFLAG_VALIDATE +//----------------------------------------------------------------------------- +// Purpose: Run a global validation pass on all of our data structures and memory +// allocations. +// Input: validator - Our global validator object +// pchName - Our name (typically a member var in our container) +//----------------------------------------------------------------------------- +void CSchemaFull::Validate( CValidator &validator, const char *pchName ) +{ + VALIDATE_SCOPE(); + + ValidateObj( m_VecSchema ); + for ( int iSchema = 0; iSchema < m_VecSchema.Count(); iSchema++ ) + { + ValidateObj( m_VecSchema[iSchema] ); + } + + ValidateObj( m_VecDeleteTable ); + ValidateObj( m_VecRenameTable ); + + ValidateObj( m_mapFTSEnabled ); + + ValidateObj( m_vecFTSCatalogs ); + FOR_EACH_VEC( m_vecFTSCatalogs, i ) + { + ValidateObj( m_vecFTSCatalogs[i] ); + } + + ValidateObj( m_VecTriggers ); + FOR_EACH_VEC( m_VecTriggers, i ) + { + ValidateObj( m_VecTriggers[i] ); + } + + validator.ClaimMemory( m_pubScratchBuffer ); +} +#endif // DBGFLAG_VALIDATE + +} // namespace GCSDK diff --git a/gcsdk/sqlaccess/schemaupdate.cpp b/gcsdk/sqlaccess/schemaupdate.cpp new file mode 100644 index 0000000..b15b5f0 --- /dev/null +++ b/gcsdk/sqlaccess/schemaupdate.cpp @@ -0,0 +1,1696 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Contains the job that's responsible for updating the database schema +// +//============================================================================= + +#include "stdafx.h" +#include "gcsdk/sqlaccess/schemaupdate.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +namespace GCSDK +{ +#ifndef SQL_SUCCESS +#define SQL_SUCCESS 0 +#define SQL_SUCCESS_WITH_INFO 1 +#define SQL_NO_DATA 100 +#define SQL_ERROR (-1) +#endif // SQLSUCCESS + +// this comes from sql.h. The GC really shouldn't depend on the MSSQL headers +#define SQL_INDEX_CLUSTERED 1 + +inline bool SQL_OK( SQLRETURN nRet ) +{ + return ( ( SQL_SUCCESS == nRet ) || (SQL_SUCCESS_WITH_INFO == nRet ) ); +} + +#define SQL_FAILED( ret ) ( !SQL_OK( ret ) ) + +#define EXIT_ON_SQL_FAILURE( ret ) \ +{ \ + if ( !SQL_OK( ret ) ) \ +{ \ + goto Exit; \ +} \ +} + +#define EXIT_ON_BOOLSQL_FAILURE( ret ) \ +{ \ + if ( !(ret) ) \ +{ \ + nRet = SQL_ERROR; \ + goto Exit; \ +} \ +} + +#define RETURN_SQL_ERROR_ON_FALSE( ret ) \ + if( !(ret) ) \ + {\ + return SQL_ERROR;\ + } + + +//----------------------------------------------------------------------------- +// Purpose: Emits a message and appends it to a string +//----------------------------------------------------------------------------- +void EmitAndAppend( CFmtStr1024 & sTarget, const CGCEmitGroup& Group, int iLevel, int iLevelLog, PRINTF_FORMAT_STRING const char *pchMessage, ... ) +{ + va_list args; + va_start( args, pchMessage ); + + if( sTarget.Length() < 1024 ) + sTarget.AppendFormatV( pchMessage, args ); + + EmitInfoV( Group, iLevel, iLevelLog, pchMessage, args ); + + va_end( args ); +} + +//----------------------------------------------------------------------------- +// builds a command of the form: +// CREATE [CLUSTERED] [UNIQUE] INDEX <index_name> ON <table_name> +// (col1, col2, ...) +// [INCLUDE (icol1, icol2, ...)] +// [WITH (FILLFACTOR = n)] +//----------------------------------------------------------------------------- +CUtlString GetAddIndexSQL( CRecordInfo *pRecordInfo, const FieldSet_t &refFields ) +{ + CFmtStrMax sCmd; + sCmd.sprintf( "CREATE %s%sINDEX %s ON App%u.%s (", + refFields.IsClustered() ? "CLUSTERED " : "", + refFields.IsUnique() ? "UNIQUE " : "", + refFields.GetIndexName(), + GGCBase()->GetAppID(), + pRecordInfo->GetName() ); + + // add real columns + for ( int n = 0; n < refFields.GetCount(); n++ ) + { + int nField = refFields.GetField( n ); + const CColumnInfo &refInfo = pRecordInfo->GetColumnInfo( nField ); + + sCmd.AppendFormat( "%s%s", + (n > 0) ? "," : "", + refInfo.GetName() ); + } + sCmd += ")"; + + // do we have any included columns? + if ( refFields.GetIncludedCount() > 0 ) + { + // yes, add those + sCmd += "\nINCLUDE ("; + + for ( int n = 0; n < refFields.GetIncludedCount(); n++ ) + { + int nField = refFields.GetIncludedField( n ); + const CColumnInfo &refInfo = pRecordInfo->GetColumnInfo( nField ); + + sCmd.AppendFormat( "%s%s", + (n > 0) ? "," : "", + refInfo.GetName() ); + } + sCmd += ")"; + } + + // do we need a fill factor? + if ( refFields.GetFillFactor() != 0) + { + sCmd.AppendFormat("\nWITH (FILLFACTOR = %d)", + refFields.GetFillFactor() ); + } + + return CUtlString( sCmd.String() ); +} + +CUtlString GetAlterColumnText( CRecordInfo *pRecordInfo, const CColumnInfo *pColumnInfoDesired ) +{ + Assert( pRecordInfo ); + Assert( pColumnInfoDesired ); + + char rgchTmp[128]; + CUtlString sCmd; + sCmd.Format( "ALTER TABLE App%u.%s ALTER COLUMN %s %s %s", GGCBase()->GetAppID(), pRecordInfo->GetName(), pColumnInfoDesired->GetName(), + SQLTypeFromField( *pColumnInfoDesired, rgchTmp, Q_ARRAYSIZE( rgchTmp ) ), + pColumnInfoDesired->BIsPrimaryKey() ? "NOT NULL" : "" + ); + return sCmd; +} + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CSchemaUpdate::CSchemaUpdate() +{ + m_mapPRecordInfoDesired.SetLessFunc( CaselessStringLessThan ); + m_eConversionMode = k_EConversionModeInspectOnly; + + m_bConversionNeeded = false; + m_cTablesDesiredMissing = 0; + m_cTablesActualDifferent = 0; + m_cTablesActualUnknown = 0; + m_cTablesNeedingChange = 0; + m_cColumnsDesiredMissing = 0; + m_cColumnsActualDifferent = 0; + m_cColumnsActualUnknown = 0; + m_bSkippedAChange = false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CSchemaUpdate::~CSchemaUpdate() +{ + // release all the record info's we're holding onto + FOR_EACH_MAP_FAST( m_mapPRecordInfoDesired, iRecordInfo ) + { + SAFE_RELEASE( m_mapPRecordInfoDesired[iRecordInfo] ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a record info that describes a desired table that should +// exist in the database +// Input: pRecordInfo - pointer to record info +//----------------------------------------------------------------------------- +void CSchemaUpdate::AddRecordInfoDesired( CRecordInfo *pRecordInfo ) +{ + Assert( pRecordInfo ); + const char *pchName = pRecordInfo->GetName(); + Assert( pchName && pchName[0] ); + Assert( m_mapPRecordInfoDesired.InvalidIndex() == m_mapPRecordInfoDesired.Find( pchName ) ); + // addref the record info since we're hanging onto it + pRecordInfo->AddRef(); + // insert it in our map, indexed by name + m_mapPRecordInfoDesired.Insert( pchName, pRecordInfo ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a CFTSCatalogInfo that tells us about a FTS catalog that +// should exist in the database +//----------------------------------------------------------------------------- +void CSchemaUpdate::AddFTSInfo( const CFTSCatalogInfo &refFTSInfo ) +{ + m_listFTSCatalogInfo.AddToTail( refFTSInfo ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a CFTSCatalogInfo that tells us about a FTS catalog that +// should exist in the database +//----------------------------------------------------------------------------- +void CSchemaUpdate::AddTriggerInfos( const CUtlVector< CTriggerInfo > &refTriggerInfo ) +{ + m_vecTriggerInfo = refTriggerInfo; +} + + +//----------------------------------------------------------------------------- +// Purpose: Validates and updates the database schema +//----------------------------------------------------------------------------- +bool CJobUpdateSchema::BYieldingRunJob( void * ) +{ + // update the main schema + EmitInfo( SPEW_GC, 2, 2, "Updating main schema...\n" ); + if ( !BYieldingUpdateSchema( k_ESchemaCatalogMain ) ) + { + m_pGC->SetStartupComplete( false ); + return false; + } + + // Could fail, but we shouldn't stop from starting up if it does + BYieldingUpdateSchema( k_ESchemaCatalogOGS ); + + bool bSuccess = m_pGC->BYieldingFinishStartup(); + m_pGC->SetStartupComplete( bSuccess ); + if ( bSuccess ) + { + bSuccess = m_pGC->BYieldingPostStartup(); + } + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CJobUpdateSchema::BYieldingUpdateSchema( ESchemaCatalog eSchemaCatalog ) +{ + if( !YieldingBuildTypeMap( eSchemaCatalog ) ) + return false; + + // make an object to communicate desired database schema & results + CSchemaUpdate *pSchemaUpdate = new CSchemaUpdate(); + + // do safe conversions only + // TODO - do one round of inspection only first so we can send watchdog alert about what's about + // to happen, then do conversions. Also force conversions in dev system. + pSchemaUpdate->m_eConversionMode = k_EConversionModeConvertSafe; + + // Add all the tables to desired schema + for ( int iTable = 0; iTable < m_iTableCount; iTable++ ) + { + // MERGE COMMENT: "schema" cannot be used as a variable name because it is an empty #define resulting in error C2059 + CSchema &gc_schema = GSchemaFull().GetSchema( iTable ); + + // is it in the schema we want? + if ( gc_schema.GetESchemaCatalog() == eSchemaCatalog ) + pSchemaUpdate->AddRecordInfoDesired( gc_schema.GetRecordInfo() ); + } + + // add all the FTS catalogs to the desired schema + for ( int n = 0; n < GSchemaFull().GetCFTSCatalogs(); n++ ) + { + const CFTSCatalogInfo &refInfo = GSchemaFull().GetFTSCatalogInfo( n ); + pSchemaUpdate->AddFTSInfo( refInfo ); + } + + pSchemaUpdate->AddTriggerInfos( GSchemaFull().GetTriggerInfos() ); + + SQLRETURN nRet = YieldingEnsureDatabaseSchemaCorrect( eSchemaCatalog, pSchemaUpdate ); + if( !SQL_OK( nRet ) ) + { + AssertMsg( false, "SQL Schema Update failed" ); + return false; + } + + SAFE_RELEASE( pSchemaUpdate ); + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Examines actual running schema of database, compares to specified desired +// schema, and changes the actual schema to correspond to desired schema +// Input: pSQLThread - SQL thread to execute on +// pSchemaUpdate - pointer to object with desired schema +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingEnsureDatabaseSchemaCorrect( ESchemaCatalog eSchemaCatalog, CSchemaUpdate *pSchemaUpdate ) +{ + Assert( pSchemaUpdate ); + + CMapPRecordInfo &mapPRecordInfoDesired = pSchemaUpdate->m_mapPRecordInfoDesired; + const CUtlVector< CTriggerInfo > &vecTriggerInfoDesired = pSchemaUpdate->m_vecTriggerInfo; + m_eConversionMode = pSchemaUpdate->m_eConversionMode; + + bool bDoConversion = true; +// bool bDoConversion = ( ( k_EConversionModeConvertSafe == eConversionMode ) || +// ( k_EConversionModeConvertIrreversible == eConversionMode ) ); + + CMapPRecordInfo mapPRecordInfoActual; + mapPRecordInfoActual.SetLessFunc( CaselessStringLessThan ); + CUtlVector<CRecordInfo *> vecPRecordInfoDesiredMissing; + CUtlVector<CRecordInfo *> vecPRecordInfoActualDifferent; + CUtlVector<CRecordInfo *> vecPRecordInfoActualUnknown; + CUtlVector< CTriggerInfo > vecTriggerInfoActual; + CUtlVector< CTriggerInfo > vecTriggerInfoMissing; + CUtlVector< CTriggerInfo > vecTriggerInfoDifferent; + + pSchemaUpdate->m_cTablesDesiredMissing = 0; + pSchemaUpdate->m_cTablesActualDifferent = 0; + pSchemaUpdate->m_cTablesActualUnknown = 0; + pSchemaUpdate->m_cTablesNeedingChange = 0; + pSchemaUpdate->m_cColumnsDesiredMissing = 0; + pSchemaUpdate->m_cColumnsActualDifferent = 0; + pSchemaUpdate->m_cColumnsActualUnknown = 0; + pSchemaUpdate->m_sDetail.Clear(); + + CFmtStr1024 &sDetail = pSchemaUpdate->m_sDetail; + + CFastTimer tickCounterOverall; + tickCounterOverall.Start(); + + // + // Do some up-front bookkeeping to see how many tables need to be created and/or altered + // + + int nSchemaID; + SQLRETURN nRet = YieldingGetSchemaID( eSchemaCatalog, &nSchemaID ); + EXIT_ON_SQL_FAILURE( nRet ); + + // Determine the actual running schema + nRet = YieldingGetRecordInfoForAllTables( eSchemaCatalog, nSchemaID, mapPRecordInfoActual ); + EXIT_ON_SQL_FAILURE( nRet ); + + nRet = YieldingGetTriggers( eSchemaCatalog, nSchemaID, vecTriggerInfoActual ); + EXIT_ON_SQL_FAILURE( nRet ); + + // Look through the list of desired tables, find any that are missing or different from the actual schema + FOR_EACH_MAP_FAST( mapPRecordInfoDesired, iRecordInfoDesired ) + { + // is this desired table in the currently connected catalog? + CRecordInfo *pRecordInfoDesired = mapPRecordInfoDesired[iRecordInfoDesired]; + if ( pRecordInfoDesired->GetESchemaCatalog() == eSchemaCatalog ) + { + // yes. do something about it + int iRecordInfoActual = mapPRecordInfoActual.Find( pRecordInfoDesired->GetName() ); + if ( mapPRecordInfoDesired.InvalidIndex() == iRecordInfoActual ) + { + // This table is desired but does not exist + vecPRecordInfoDesiredMissing.AddToTail( pRecordInfoDesired ); + } + else + { + // Table with same name exists in desired & actual schemas; is it exactly the same? + CRecordInfo *pRecordInfoActual = mapPRecordInfoActual[iRecordInfoActual]; + if ( !pRecordInfoDesired->EqualTo( pRecordInfoActual ) ) + { + // This desired table exists but the actual table is different than desired + vecPRecordInfoActualDifferent.AddToTail( pRecordInfoActual ); + } + } + } + } + + // Now, look through the list of actual tables and find any that do not exist in the list of desired tables + FOR_EACH_MAP_FAST( mapPRecordInfoActual, iRecordInfoActual ) + { + CRecordInfo *pRecordInfoActual = mapPRecordInfoActual[iRecordInfoActual]; + int iRecordInfoDesired = mapPRecordInfoDesired.Find( pRecordInfoActual->GetName() ); + if ( !mapPRecordInfoDesired.IsValidIndex( iRecordInfoDesired ) ) + { + // This table exists but is not in the list of desired tables + // maybe it's an old table. + + vecPRecordInfoActualUnknown.AddToTail( pRecordInfoActual ); + } + } + + // find a list of missing triggers + FOR_EACH_VEC( vecTriggerInfoDesired, iDesired ) + { + // not of this catalog? skip it + if ( vecTriggerInfoDesired[ iDesired ].m_eSchemaCatalog != eSchemaCatalog ) + continue; + + // it is our catalog, so try and match + bool bMatched = false; + FOR_EACH_VEC( vecTriggerInfoActual, iActual ) + { + // is it the same table and trigger name? + if ( vecTriggerInfoActual[ iActual ] == vecTriggerInfoDesired[ iDesired ] ) + { + // yes! test the text for differences + if ( vecTriggerInfoActual[ iActual ].IsDifferent( vecTriggerInfoDesired[ iDesired ] ) ) + { + vecTriggerInfoDifferent.AddToTail( vecTriggerInfoDesired[ iDesired ] ); + } + else + { + // we have a match! + vecTriggerInfoActual[ iActual ].m_bMatched = true; + bMatched = true; + } + break; + } + } + + if ( !bMatched ) + { + vecTriggerInfoMissing.AddToTail( vecTriggerInfoDesired[ iDesired ] ); + } + } + + // + // Now do the actual conversion + // + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "Database conversion: %s\n", + bDoConversion ? "beginning" : "inspection only" ); + + + // find tables which need to be created + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "# of specified tables that do not currently exist in database: %d\n", + vecPRecordInfoDesiredMissing.Count() ); + if ( vecPRecordInfoDesiredMissing.Count() > 0 ) + pSchemaUpdate->m_bConversionNeeded = true; + + // Create any tables which need to be created + for ( int iTable = 0; iTable < vecPRecordInfoDesiredMissing.Count(); iTable++ ) + { + CRecordInfo *pRecordInfoDesired = vecPRecordInfoDesiredMissing[iTable]; + if ( bDoConversion ) + { + CFastTimer tickCounter; + tickCounter.Start(); + + // Create the table + nRet = YieldingCreateTable( eSchemaCatalog, pRecordInfoDesired ); + EXIT_ON_SQL_FAILURE( nRet ); + tickCounter.End(); + int nElapsedMilliseconds = tickCounter.GetDuration().GetMilliseconds(); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tCreated table: %s: %d millisec\n", pRecordInfoDesired->GetName(), nElapsedMilliseconds ); + } + else + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t%s\n", pRecordInfoDesired->GetName() ); + } + + // find tables which are different + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "[GC]: # of specified tables that differ from schema in database: %d\n", + vecPRecordInfoActualDifferent.Count() ); + +#ifdef _DEBUG + // are some different? if so, list their names only for now. + // This is in _debug only because it's useful for debugging the below loop, + // but spewey for everyday life (as long as the below loop is working). + if ( vecPRecordInfoActualDifferent.Count() > 0 ) + { + FOR_EACH_VEC( vecPRecordInfoActualDifferent, i ) + { + CRecordInfo *pRecordInfoActual = vecPRecordInfoActualDifferent[i]; + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "[GC]: table %s is different\n", + pRecordInfoActual->GetName() ); + } + } +#endif + + pSchemaUpdate->m_bSkippedAChange = false; + + // Alter any table which needs to be altered + for ( int iTable = 0; iTable < vecPRecordInfoActualDifferent.Count(); iTable++ ) + { + CRecordInfo *pRecordInfoActual = vecPRecordInfoActualDifferent[iTable]; + int iRecordInfoDesired = mapPRecordInfoDesired.Find( pRecordInfoActual->GetName() ); + Assert( mapPRecordInfoDesired.InvalidIndex() != iRecordInfoDesired ); + CRecordInfo *pRecordInfoDesired = mapPRecordInfoDesired[iRecordInfoDesired]; + + CUtlVector<const CColumnInfo *> vecPColumnInfoDesiredMissing; + CUtlVector<const CColumnInfo *> vecPColumnInfoActualDifferent; + CUtlVector<const CColumnInfo *> vecPColumnInfoActualUnknown; + + // We know something is different between the actual & desired schema for this table, but don't yet know what + + // Compare each column + for ( int iColumnDesired = 0; iColumnDesired < pRecordInfoDesired->GetNumColumns(); iColumnDesired ++ ) + { + const CColumnInfo &columnInfoDesired = pRecordInfoDesired->GetColumnInfo( iColumnDesired ); + int iColumnActual = -1; + bool bRet = pRecordInfoActual->BFindColumnByName( columnInfoDesired.GetName(), &iColumnActual ); + if ( bRet ) + { + const CColumnInfo &columnInfoActual = pRecordInfoActual->GetColumnInfo( iColumnActual ); + if ( columnInfoActual.GetChecksum() != columnInfoDesired.GetChecksum() ) + { + // The actual column is different than the desired column + vecPColumnInfoActualDifferent.AddToTail( &columnInfoActual ); + } + } + else + { + // The desired column is missing from the actual table + vecPColumnInfoDesiredMissing.AddToTail( &columnInfoDesired ); + } + } + for ( int iColumnActual = 0; iColumnActual < pRecordInfoActual->GetNumColumns(); iColumnActual ++ ) + { + const CColumnInfo &columnInfoActual = pRecordInfoActual->GetColumnInfo( iColumnActual ); + int iColumnDesired = -1; + bool bRet = pRecordInfoDesired->BFindColumnByName( columnInfoActual.GetName(), &iColumnDesired ); + if ( !bRet ) + { + // this column exists in the running schema, but not in the desired schema (e.g. old column) + vecPColumnInfoActualUnknown.AddToTail( &columnInfoActual ); + } + } + + if ( ( vecPColumnInfoDesiredMissing.Count() > 0 ) || ( vecPColumnInfoActualDifferent.Count() > 0 ) ) + { + pSchemaUpdate->m_bConversionNeeded = true; + pSchemaUpdate->m_cTablesNeedingChange++; + } + + // Add any desired columns which are missing from the actual schema + if ( vecPColumnInfoDesiredMissing.Count() > 0 ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\tDesired columns missing in table %s:\n", pRecordInfoActual->GetName() ); + + for ( int iColumn = 0; iColumn < vecPColumnInfoDesiredMissing.Count(); iColumn++ ) + { + const CColumnInfo *pColumnInfoDesired = vecPColumnInfoDesiredMissing[iColumn]; + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\t\t%s\n", pColumnInfoDesired->GetName() ); + if ( bDoConversion ) + { + CFastTimer tickCounter; + tickCounter.Start(); + // Add the column + nRet = YieldingAlterTableAddColumn( eSchemaCatalog, pRecordInfoActual, pColumnInfoDesired ); + EXIT_ON_SQL_FAILURE( nRet ); + tickCounter.End(); + int nElapsedMilliseconds = tickCounter.GetDuration().GetMilliseconds(); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\t\t\tCreated column %s: %d millisec\n", pColumnInfoDesired->GetName(), nElapsedMilliseconds ); + } + } + } + + // Check for any stray indices that aren't found in the specification? + bool bIndexMismatch = false; + for ( int idx = 0 ; idx < pRecordInfoActual->GetIndexFieldCount() ; ++idx ) + { + const FieldSet_t &fs = pRecordInfoActual->GetIndexFields()[idx]; + if ( pRecordInfoDesired->FindIndex( pRecordInfoActual, fs ) >= 0 ) + continue; + if ( pRecordInfoDesired->FindIndexByName( fs.GetIndexName() ) >= 0 ) + continue; // we already handled this above + bIndexMismatch = true; + + if ( idx == pRecordInfoActual->GetPKIndex() ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tTable %s primary key in database differs from specification.\n", + pRecordInfoDesired->GetName() ); + nRet = YieldingProcessUnsafeConversion( eSchemaCatalog, + CFmtStr("ALTER TABLE App%u.%s DROP CONSTRAINT %s", GGCBase()->GetAppID(), pRecordInfoActual->GetName(), fs.GetIndexName() ).String(), + CFmtStr("%s: remove old primary key.", pRecordInfoDesired->GetName() ).String() ); + EXIT_ON_SQL_FAILURE( nRet ); + } + else + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tTable %s index %s exists in database but is not in specification. (Possible performance problem?).\n", + pRecordInfoDesired->GetName(), fs.GetIndexName() ); + nRet = YieldingProcessUnsafeConversion( eSchemaCatalog, + CFmtStr("DROP INDEX %s ON App%u.%s", fs.GetIndexName(), GGCBase()->GetAppID(), pRecordInfoActual->GetName() ).String(), + CFmtStr("%s: cleanup stray index %s.", pRecordInfoDesired->GetName(), fs.GetIndexName() ).String() ); + EXIT_ON_SQL_FAILURE( nRet ); + } + } + + // Change any columns which are different between desired and actual schema + if ( vecPColumnInfoActualDifferent.Count() > 0 ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tColumns that differ between specification and database in table %s:\n", + pRecordInfoActual->GetName() ); + for ( int iColumn = 0; iColumn < vecPColumnInfoActualDifferent.Count(); iColumn++ ) + { + const CColumnInfo *pColumnInfoActual = vecPColumnInfoActualDifferent[iColumn]; + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\t%s", pColumnInfoActual->GetName() ); + int iColumnDesired = -1; + DbgVerify( pRecordInfoDesired->BFindColumnByName( pColumnInfoActual->GetName(), &iColumnDesired ) ); + const CColumnInfo *pColumnInfoDesired = &pRecordInfoDesired->GetColumnInfo( iColumnDesired ); + + // if type or size changed, alter the column + if ( ( pColumnInfoDesired->GetType() != pColumnInfoActual->GetType() ) || + // fixed length field, and the sizes differ + ( ! pColumnInfoDesired->BIsVariableLength() && + ( pColumnInfoDesired->GetFixedSize() != pColumnInfoActual->GetFixedSize() ) ) || + // variable length field, and the sizes differ + // fixed length field, and the sizes differ + ( pColumnInfoDesired->BIsVariableLength() && + ( pColumnInfoDesired->GetMaxSize() != pColumnInfoActual->GetMaxSize() ) ) ) + { + if ( k_EConversionModeConvertIrreversible != m_eConversionMode ) + { + pSchemaUpdate->m_bSkippedAChange = true; + if ( pColumnInfoDesired->GetType() != pColumnInfoActual->GetType() ) + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t(data types differ: desired=%s, actual=%s)", PchNameFromEGCSQLType( pColumnInfoDesired->GetType() ), PchNameFromEGCSQLType( pColumnInfoActual->GetType() ) ); + if ( pColumnInfoDesired->GetFixedSize() != pColumnInfoActual->GetFixedSize() ) + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t(column sizes differ: desired=%d, actual=%d)", pColumnInfoDesired->GetFixedSize(), pColumnInfoActual->GetFixedSize() ); + if ( pColumnInfoDesired->GetMaxSize() != pColumnInfoActual->GetMaxSize() ) + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t(maximum column sizes differ: desired=%d, actual=%d)", pColumnInfoDesired->GetMaxSize(), pColumnInfoActual->GetMaxSize() ); + } + nRet = YieldingChangeColumnTypeOrLength( eSchemaCatalog, pRecordInfoActual, pColumnInfoDesired ); + EXIT_ON_SQL_FAILURE( nRet ); + } + + // If column constraints/indexes are different, make appropriate adjustments + // do this second so it has a chance to succeed - otherwise, we may create an index here that + // prevents us from performing an alter table + if ( pColumnInfoDesired->GetColFlags() != pColumnInfoActual->GetColFlags() ) + { + if ( k_EConversionModeConvertIrreversible != m_eConversionMode ) + { + char szDesiredFlags[k_nKiloByte]; + char szActualFlags[k_nKiloByte]; + pColumnInfoDesired->GetColFlagDescription( szDesiredFlags, Q_ARRAYSIZE( szDesiredFlags ) ); + pColumnInfoActual->GetColFlagDescription( szActualFlags, Q_ARRAYSIZE( szActualFlags ) ); + + pSchemaUpdate->m_bSkippedAChange = true; + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t(column flags differ: desired=\"%s\", actual=\"%s\")", + szDesiredFlags, szActualFlags ); + } + nRet = YieldingChangeColumnProperties( eSchemaCatalog, pRecordInfoActual, pColumnInfoActual, pColumnInfoDesired ); + EXIT_ON_SQL_FAILURE( nRet ); + } + + if ( pSchemaUpdate->m_bSkippedAChange ) + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t(Not attempting unsafe change).\n" ); + } + } + + // Scan for any new / changed indices + for ( int idx = 0 ; idx < pRecordInfoDesired->GetIndexFieldCount() ; ++idx ) + { + const FieldSet_t &fs = pRecordInfoDesired->GetIndexFields()[idx]; + int iActualIdx = pRecordInfoActual->FindIndex( pRecordInfoDesired, fs ); + if ( iActualIdx >= 0 ) + continue; + bIndexMismatch = true; + + // The exact index we want doesn't exist. Is it the primary key? + CUtlString sCommand; + CUtlString sComment; + if ( idx == pRecordInfoDesired->GetPKIndex() ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tTable %s primary key in specification differs from database.\n", + pRecordInfoDesired->GetName() ); + sComment.Format( "%s: create new primary key constraint", pRecordInfoDesired->GetName() ); + TSQLCmdStr cmd; + BuildTablePKConstraintText( &cmd, pRecordInfoDesired ); + for ( int i = 0 ; i < fs.GetCount() ; ++i ) // make sure they are non-NULL + { + int idxField = fs.GetField(i); + const CColumnInfo *pColInfo = &pRecordInfoDesired->GetColumnInfo( idxField ); + Assert( pColInfo->BIsPrimaryKey() ); + sCommand += GetAlterColumnText( pRecordInfoDesired, pColInfo ); + sCommand += ";\n"; + } + + CUtlString sCreatePK; + sCreatePK.Format( "GO\nALTER TABLE App%u.%s ADD %s\n", GGCBase()->GetAppID(), pRecordInfoDesired->GetName(), cmd.String() ); + sCommand += sCreatePK; + } + else + { + + // Another common thing that could happen is that an index is changed. + // Look for an existing index with the same name as a way to try to + // detect this common case and provide a more specific message. (Otherwise, + // we will report it as a missing index, and an extra unwanted index --- which + // is correct but a bit more confusing.) + + iActualIdx = pRecordInfoActual->FindIndexByName( fs.GetIndexName() ); + if ( iActualIdx < 0 ) + { + sComment.Format("%s: add index %s", pRecordInfoDesired->GetName(), fs.GetIndexName() ); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tTable %s index %s is specified but not present in database.\n", + pRecordInfoDesired->GetName(), fs.GetIndexName() ); + } + else + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tTable %s index %s differs between specification and database.\n", + pRecordInfoDesired->GetName(), fs.GetIndexName() ); + sComment.Format( "%s: fix index %s", pRecordInfoDesired->GetName(), fs.GetIndexName() ); + sCommand.Format( "DROP INDEX %s ON App%u.%s;\n", fs.GetIndexName(), GGCBase()->GetAppID(), pRecordInfoDesired->GetName() ); + } + sCommand += GetAddIndexSQL( pRecordInfoDesired, fs ); + } + nRet = YieldingProcessUnsafeConversion( eSchemaCatalog, sCommand, sComment ); + EXIT_ON_SQL_FAILURE( nRet ); + } + + // Just to be safe, let's run the old code, too. + if ( !bIndexMismatch && !pRecordInfoActual->CompareIndexLists( pRecordInfoDesired ) ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tIndex sets in table %s differ between specification and database [GC]:\n", + pRecordInfoDesired->GetName() ); + CFmtStr1024 sTemp; + + pRecordInfoDesired->GetIndexFieldList( &sTemp, 3 ); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\tDesired %s", sTemp.Access() ); + + pRecordInfoActual->GetIndexFieldList( &sTemp, 3 ); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\tActual %s", sTemp.Access() ); + } + + // what about foreign key constraints? + if ( ! pRecordInfoActual->CompareFKs( pRecordInfoDesired ) ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tForeign Key constraints in table %s differs between specification and database [GC]:\n", + pRecordInfoDesired->GetName() ); + + CFmtStr1024 sTemp; + pRecordInfoDesired->GetFKListString( &sTemp, 3 ); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\tDesired %s", sTemp.Access() ); + + pRecordInfoActual->GetFKListString( &sTemp, 3 ); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\tActual %s", sTemp.Access() ); + } + + + + if ( vecPColumnInfoActualUnknown.Count() > 0 ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tColumns in database [GC] table %s that are not in specification (ignored):\n", + pRecordInfoActual->GetName() ); + + // Since this can actually destroy data, let's not ever, ever run it automatically. + CUtlString sCommand; + sCommand.Format( "ALTER TABLE App%u.%s DROP COLUMN ", GGCBase()->GetAppID(), pRecordInfoActual->GetName() ); + + for ( int iColumn = 0; iColumn < vecPColumnInfoActualUnknown.Count(); iColumn++ ) + { + const CColumnInfo *pColumnInfo = vecPColumnInfoActualUnknown[iColumn]; + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\t%s\n", pColumnInfo->GetName() ); + if ( iColumn != 0 ) + sCommand += ", "; + sCommand += pColumnInfo->GetName(); + } + AddDataDestroyingConversion( sCommand, + CFmtStr( "-- Drop extra column(s) in %s\n", pRecordInfoActual->GetName() ) ); + } + + pSchemaUpdate->m_cColumnsDesiredMissing += vecPColumnInfoDesiredMissing.Count(); + pSchemaUpdate->m_cColumnsActualDifferent += vecPColumnInfoActualDifferent.Count(); + pSchemaUpdate->m_cColumnsActualUnknown += vecPColumnInfoActualUnknown.Count(); + } + + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "[GC]: # of tables that currently exist in database but were unspecified: %d\n", + vecPRecordInfoActualUnknown.Count() ); + for ( int iTable = 0; iTable < vecPRecordInfoActualUnknown.Count(); iTable++ ) + { + CRecordInfo *pRecordInfo = vecPRecordInfoActualUnknown[iTable]; + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t%s\n", pRecordInfo->GetName() ); + AddDataDestroyingConversion( + CFmtStr( "DROP TABLE App%u.%s\n", GGCBase()->GetAppID(), pRecordInfo->GetName() ), + CFmtStr( "-- Drop extra table %s\n", pRecordInfo->GetName() ) ); + } + + // then, the triggers + if ( vecTriggerInfoMissing.Count() > 0 ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "[GC]: # of specified triggers that do not currently exist: %d\n", + vecTriggerInfoMissing.Count() ); + + FOR_EACH_VEC( vecTriggerInfoMissing, iMissing ) + { + CFastTimer tickCounter; + tickCounter.Start(); + + // Create the trigger + nRet = YieldingCreateTrigger( eSchemaCatalog, vecTriggerInfoMissing[ iMissing] ); + EXIT_ON_SQL_FAILURE( nRet ); + tickCounter.End(); + int nElapsedMilliseconds = tickCounter.GetDuration().GetMilliseconds(); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "[GC]: Created trigger %s on table %s: %d millisec\n", + vecTriggerInfoMissing[ iMissing ].m_szTriggerName, + vecTriggerInfoMissing[ iMissing ].m_szTriggerTableName, nElapsedMilliseconds ); + } + } + + // different triggers + FOR_EACH_VEC( vecTriggerInfoDifferent, iDifferent ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "[GC]: Trigger %s on table %s differs from the desired trigger\n", vecTriggerInfoMissing[ iDifferent ].m_szTriggerName, + vecTriggerInfoMissing[ iDifferent ].m_szTriggerTableName); + + // a different trigger text is a forced failure. + nRet = SQL_ERROR; + } + + // extra triggers + FOR_EACH_VEC( vecTriggerInfoActual, iActual ) + { + // if it was never matched, it isn't in the schema anywhere + if ( ! vecTriggerInfoActual[ iActual ].m_bMatched ) + { + SQLRETURN nSQLReturn = YieldingDropTrigger( eSchemaCatalog, vecTriggerInfoActual[ iActual ] ); + + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "[GC]: Trigger %s on table %s is not in the declared schema ... Drop %s", + vecTriggerInfoActual[ iActual ].m_szTriggerName, + vecTriggerInfoActual[ iActual ].m_szTriggerTableName, + SQL_OK( nSQLReturn ) ? "OK" : "FAILED!" ) ; + + if ( !SQL_OK ( nSQLReturn ) ) + { + // it broke; latch the failure + nRet = nSQLReturn; + } + } + } + + tickCounterOverall.End(); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "[GC]: Total time: %d milliseconds\n", + tickCounterOverall.GetDuration().GetMilliseconds() ); + +Exit: + + // Spew suggested SQL to clean up stuff + if ( !m_sRecommendedSQL.IsEmpty() || !m_sDataDestroyingCleanupSQL.IsEmpty() ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\tThe following SQL might work to fixup schema differences.\n" ); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t** \n" ); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t** DISCLAIMER ** This conversion code is not well tested. Review the SQL and use at your own risk.\n" ); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t** \n" ); + { + CUtlVectorAutoPurge<char*> vecLines; + V_SplitString( m_sRecommendedSQL, "\n", vecLines ); + FOR_EACH_VEC( vecLines, i ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\t%s\n", vecLines[i] ); + } + m_sRecommendedSQL.Clear(); + } + if ( !m_sDataDestroyingCleanupSQL.IsEmpty() ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\t-- WARNING: The following operations will *destroy* data that is in the database but not in the specification.\n" ); + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\t-- If you have manually created extra tables or columns in your database, it will appear here!\n" ); + CUtlVectorAutoPurge<char*> vecLines; + V_SplitString( m_sDataDestroyingCleanupSQL, "\n", vecLines ); + FOR_EACH_VEC( vecLines, i ) + { + EmitAndAppend( sDetail, SPEW_GC, 2, 2, "\t\t%s\n", vecLines[i] ); + } + m_sDataDestroyingCleanupSQL.Clear(); + } + } + + pSchemaUpdate->m_cTablesDesiredMissing = vecPRecordInfoDesiredMissing.Count(); + pSchemaUpdate->m_cTablesActualDifferent = vecPRecordInfoActualDifferent.Count(); + pSchemaUpdate->m_cTablesActualUnknown = vecPRecordInfoActualUnknown.Count(); + + FOR_EACH_MAP_FAST( mapPRecordInfoActual, iRecordInfoActual ) + SAFE_RELEASE( mapPRecordInfoActual[iRecordInfoActual] ); + + return nRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Builds a record info for each table in database that describes the +// columns in that table +// Input: pSQLConnection - SQL connection to execute on +// mapPRecordInfo - map to add the table record infos to +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingGetSchemaID( ESchemaCatalog eSchemaCatalog, int *pSchemaID ) +{ + CSQLAccess sqlAccess( eSchemaCatalog ); + CFmtStr1024 sDefaultSchema; + if( !sqlAccess.BYieldingExecuteString( FILE_AND_LINE, "SELECT default_schema_name FROM sys.database_principals WHERE name=CURRENT_USER", + &sDefaultSchema ) ) + return SQL_ERROR; + + CFmtStr sExpectedDefaultSchema( "App%u", GGCBase()->GetAppID() ); + if ( 0 != Q_stricmp( sDefaultSchema, sExpectedDefaultSchema ) ) + { + AssertMsg2( false, "SQL connection has the wrong default schema. Expected: %s. Actual %s", sExpectedDefaultSchema.Get(), sDefaultSchema.Get() ); + return SQL_ERROR; + } + + sqlAccess.AddBindParam( sDefaultSchema ); + if( !sqlAccess.BYieldingExecuteScalarInt( FILE_AND_LINE, "SELECT SCHEMA_ID(?)", pSchemaID ) ) + return SQL_ERROR; + + return SQL_SUCCESS; +} + + +//----------------------------------------------------------------------------- +// Purpose: Builds a record info for each table in database that describes the +// columns in that table +// Input: pSQLConnection - SQL connection to execute on +// mapPRecordInfo - map to add the table record infos to +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingGetRecordInfoForAllTables( ESchemaCatalog eSchemaCatalog, int nSchemaID, CMapPRecordInfo &mapPRecordInfo ) +{ + CSQLAccess sqlAccess( eSchemaCatalog ); + + // create a query that returns all tables in the database + sqlAccess.AddBindParam( nSchemaID ); + RETURN_SQL_ERROR_ON_FALSE( sqlAccess.BYieldingExecute( FILE_AND_LINE, "SELECT object_id, name FROM sys.objects WHERE type_desc = 'USER_TABLE' AND is_ms_shipped = 0 AND schema_id = ?" ) ); + + FOR_EACH_SQL_RESULT( sqlAccess, 0, tableIDRecord ) + { + int nTableID; + RETURN_SQL_ERROR_ON_FALSE( tableIDRecord.BGetIntValue( 0, &nTableID ) ); + + CFmtStr1024 sTableName; + RETURN_SQL_ERROR_ON_FALSE( tableIDRecord.BGetStringValue( 1, &sTableName) ); + + YieldingGetColumnInfoForTable( eSchemaCatalog, mapPRecordInfo, nTableID, sTableName ); + } + + + // now, get the column indexes and constraints for each table + FOR_EACH_MAP_FAST( mapPRecordInfo, iRecordInfo ) + { + CRecordInfo *pRecordInfo = mapPRecordInfo[iRecordInfo]; + pRecordInfo->SetAllColumnsAdded(); + + // determine indexes and constraints + YieldingGetColumnIndexes( eSchemaCatalog, pRecordInfo ); + + // determine FK constraints + YieldingGetTableFKConstraints( eSchemaCatalog, pRecordInfo ); + + // do final calculations then ready to use + pRecordInfo->PrepareForUse(); + } + + return SQL_SUCCESS; +} + +SQLRETURN CJobUpdateSchema::YieldingGetColumnInfoForTable( ESchemaCatalog eSchemaCatalog, CMapPRecordInfo &mapPRecordInfo, int nTableID, const char *pchTableName ) +{ + CSQLAccess sqlAccess( eSchemaCatalog ); + + sqlAccess.AddBindParam( nTableID ); + RETURN_SQL_ERROR_ON_FALSE( sqlAccess.BYieldingExecute( FILE_AND_LINE, "SELECT name, column_id, user_type_id, max_length, is_identity FROM sys.columns WHERE object_id = ?") ); + + CRecordInfo *pRecordInfo = CRecordInfo::Alloc(); + pRecordInfo->SetName( pchTableName ); + pRecordInfo->SetTableID( nTableID ); + FOR_EACH_SQL_RESULT( sqlAccess, 0, columnInfo ) + { + CFmtStr1024 sColumnName; + int nType; + int nColumnID; + int nMaxLength; + bool bIdentity; + RETURN_SQL_ERROR_ON_FALSE( columnInfo.BGetStringValue( 0, &sColumnName) ); + RETURN_SQL_ERROR_ON_FALSE( columnInfo.BGetIntValue( 1, &nColumnID ) ); + RETURN_SQL_ERROR_ON_FALSE( columnInfo.BGetIntValue( 2, &nType ) ); + RETURN_SQL_ERROR_ON_FALSE( columnInfo.BGetIntValue( 3, &nMaxLength ) ); + RETURN_SQL_ERROR_ON_FALSE( columnInfo.BGetBoolValue( 4, &bIdentity) ); + + int nColFlags = 0; + if( bIdentity ) + nColFlags |= k_nColFlagAutoIncrement; + + pRecordInfo->AddColumn( sColumnName, nColumnID, GetEGCSQLTypeForMSSQLType( nType ), nMaxLength, nColFlags, nMaxLength ); + } + mapPRecordInfo.Insert( pRecordInfo->GetName(), pRecordInfo ); + + return SQL_SUCCESS; +} + + +//----------------------------------------------------------------------------- +// purpose: get a list of triggers from the database. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingGetTriggers( ESchemaCatalog eSchemaCatalog, int nSchemaID, CUtlVector< CTriggerInfo > &vecTriggerInfo ) +{ + CSQLAccess sqlAccess( eSchemaCatalog ); + + // get some description and the text of the triggers on this database. + // Find the name of the trigger, the name of the table it servers, the type of + // trigger, and its text. Doesn't bring back any disabled or MS-shipped triggers, + // and gets only DML triggers and not DDL triggers. + const char *pchStatement = "SELECT ST.name AS TriggerName, SOT.name AS TableName, ST.is_instead_of_trigger, SC.Text, SC.ColID" + " FROM sys.triggers AS ST" + " JOIN sys.syscomments AS SC ON SC.id = ST.object_id" + " JOIN sys.objects AS SO ON SO.object_id = ST.object_id" + " JOIN sys.objects AS SOT on SOT.object_id = ST.parent_id" + " WHERE ST.type_desc = 'SQL_TRIGGER'" + " AND ST.is_ms_shipped = 0 AND ST.is_disabled = 0" + " AND ST.parent_class = 1" + " AND SO.schema_id = ?" + " ORDER BY TableName, TriggerName, SC.ColID"; + sqlAccess.AddBindParam( nSchemaID ); + RETURN_SQL_ERROR_ON_FALSE( sqlAccess.BYieldingExecute( FILE_AND_LINE, pchStatement ) ); + + // should be one results set + Assert( 1 == sqlAccess.GetResultSetCount() ); + + FOR_EACH_SQL_RESULT( sqlAccess, 0, sqlRecord ) + { + // get the text of the procedure + const char *pchText; + DbgVerify( sqlRecord.BGetStringValue( 3, &pchText ) ); + + // is this instead of? + bool bIsInsteadOf; + DbgVerify( sqlRecord.BGetBoolValue( 2, &bIsInsteadOf ) ); + + // get the table name + const char *pchTableName; + DbgVerify( sqlRecord.BGetStringValue( 1, &pchTableName ) ); + + // and the trigger name + const char *pchTriggerName; + DbgVerify( sqlRecord.BGetStringValue( 0, &pchTriggerName ) ); + + // finally, grab the collation id + int16 nColID; + DbgVerify( sqlRecord.BGetInt16Value( 4, &nColID ) ); + + if ( nColID == 1 ) + { + // the collation ID is one, so we know this is a new one + CTriggerInfo info; + info.m_strText = pchText; + Q_strncpy( info.m_szTriggerName, pchTriggerName, Q_ARRAYSIZE( info.m_szTriggerName ) ); + Q_strncpy( info.m_szTriggerTableName, pchTableName, Q_ARRAYSIZE( info.m_szTriggerTableName ) ); + info.m_eSchemaCatalog = eSchemaCatalog; + vecTriggerInfo.AddToTail( info ); + } + else + { + // the collation ID is not one, so we're concatenating. + Assert( vecTriggerInfo.Count() - 1 >= 0 ); + + // the name could not have changed + Assert( 0 == Q_strcmp( vecTriggerInfo[vecTriggerInfo.Count() - 1].m_szTriggerName, pchTriggerName ) ); + } + } + + return SQL_SUCCESS; +} + + +//----------------------------------------------------------------------------- +// Purpose: retrieves the index information for the specified table, then adds this +// information to the record info. This is a SQL Server-specific implementation +// which gets data describing index features not available through plain ODBC +// queries. +// Input: pSQLConnection - SQL connection to execute on +// pRecordInfo - CRecordInfo to add the index info into +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingGetColumnIndexes( ESchemaCatalog eSchemaCatalog, CRecordInfo *pRecordInfo ) +{ + // query the system management views for all of the indexes on this table + static const char *pstrStatement = + "SELECT SI.Name AS INDEX_NAME, SC.Name AS COLUMN_NAME, SI.Type AS [TYPE], IS_INCLUDED_COLUMN, SIC.KEY_Ordinal AS [ORDINAL_POSITION], IS_UNIQUE, IS_PRIMARY_KEY, SI.INDEX_ID" + " FROM sys.indexes SI " + " JOIN sys.index_columns SIC" + " ON SIC.Object_id = SI.Object_Id" + " AND SIC.Index_ID = SI.Index_id" + " JOIN sys.objects SO" + " ON SIC.Object_ID = SO.Object_ID" + " JOIN sys.columns SC" + " ON SC.Object_ID = SO.Object_ID" + " AND SC.column_id = SIC.column_id" + " WHERE SO.Object_ID = ? " + "ORDER BY SIC.Index_id, SIC.Key_Ordinal "; + CSQLAccess sqlAccess( eSchemaCatalog ); + sqlAccess.AddBindParam( pRecordInfo->GetTableID() ); + RETURN_SQL_ERROR_ON_FALSE( sqlAccess.BYieldingExecute( FILE_AND_LINE, pstrStatement ) ); + + // builds a list of columns in a particular index. + CUtlVector<int> vecColumns; + CUtlVector<int> vecIncluded; + int nLastIndexID = -1; + bool bIsClustered = false; + bool bIsIndexUnique = false; + bool bIsPrimaryKey = false; + CFmtStr1024 sIndexName, sColumnName; + + FOR_EACH_SQL_RESULT( sqlAccess, 0, typeRecord ) + { + // Starting a new index? + int nIndexID; + DbgVerify( typeRecord.BGetIntValue( 7, &nIndexID ) ); + if ( nIndexID != nLastIndexID ) + { + // first column! is it our first time through? + if ( vecColumns.Count() > 0 ) + { + // yes. let's add what we had from the previous index + // to the fieldset + FieldSet_t fs( bIsIndexUnique, bIsClustered, vecColumns, sIndexName ); + fs.AddIncludedColumns( vecIncluded ); + int idx = pRecordInfo->AddIndex( fs ); + if ( bIsPrimaryKey ) + { + Assert( bIsIndexUnique ); + Assert( pRecordInfo->GetPKIndex() < 0 ); + pRecordInfo->SetPKIndex( idx ); + } + + // reset the vector + vecColumns.RemoveAll(); + vecIncluded.RemoveAll(); + bIsIndexUnique = false; + bIsClustered = false; + bIsPrimaryKey = false; + } + nLastIndexID = nIndexID; + } + + int nTypeID, nOrdinalPosition; + bool bIsIncluded, bIsColumnUnique; + + DbgVerify( typeRecord.BGetStringValue( 0, &sIndexName ) ); + DbgVerify( typeRecord.BGetStringValue( 1, &sColumnName ) ); + DbgVerify( typeRecord.BGetIntValue( 2, &nTypeID ) ); + DbgVerify( typeRecord.BGetBoolValue( 3, &bIsIncluded ) ); + DbgVerify( typeRecord.BGetIntValue( 4, &nOrdinalPosition ) ); + DbgVerify( typeRecord.BGetBoolValue( 5, &bIsColumnUnique ) ); + DbgVerify( typeRecord.BGetBoolValue( 6, &bIsPrimaryKey ) ); + + RETURN_SQL_ERROR_ON_FALSE( sColumnName.Length() > 0 ); + + int nColumnIndexed = -1; + RETURN_SQL_ERROR_ON_FALSE( pRecordInfo->BFindColumnByName( sColumnName, &nColumnIndexed ) ); + + CColumnInfo & columnInfo = pRecordInfo->GetColumnInfo( nColumnIndexed ); + int nColFlags = 0; + + if ( bIsIncluded ) + { + Assert( nOrdinalPosition == 0 ); + // it's included; no flags + vecIncluded.AddToTail( nColumnIndexed ); + } + else + { + Assert( nOrdinalPosition != 0 ); + if ( bIsPrimaryKey ) + { + // if we're working a primary key, mark those flags + nColFlags = k_nColFlagPrimaryKey | k_nColFlagIndexed | k_nColFlagUnique; + // PKs are always unique + bIsIndexUnique = true; + } + else + { + // if we're working a "regular" index, we need to know the uniqueness of the index ... + + nColFlags = k_nColFlagIndexed; + if ( bIsColumnUnique ) + { + nColFlags |= k_nColFlagUnique; + bIsIndexUnique = true; + } + } + + // clustering type + if ( nTypeID == SQL_INDEX_CLUSTERED ) + { + nColFlags |= k_nColFlagClustered; + bIsClustered = true; + } + columnInfo.SetColFlagBits( nColFlags ); + + // add this column to our list for the index set + vecColumns.AddToTail( nColumnIndexed ); + } + } + + // anything left over? + if ( vecColumns.Count() > 0 ) + { + // yep, add that, too + FieldSet_t fs( bIsIndexUnique, bIsClustered, vecColumns, sIndexName ); + fs.AddIncludedColumns( vecIncluded ); + int idx = pRecordInfo->AddIndex( fs ); + if ( bIsPrimaryKey ) + { + Assert( bIsIndexUnique ); + Assert( pRecordInfo->GetPKIndex() < 0 ); + pRecordInfo->SetPKIndex( idx ); + } + } + + return SQL_SUCCESS; +} + +//----------------------------------------------------------------------------- +// Purpose: Finds the schema info on any FK constraints defined for the table +// Input: pRecordInfo - CRecordInfo to add the index info into (and lookup table name out of) +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingGetTableFKConstraints( ESchemaCatalog eSchemaCatalog, CRecordInfo *pRecordInfo ) +{ + // Used below, declared up here because of goto + FKData_t fkData; + + CSQLAccess sqlAccess( eSchemaCatalog ); + const char *pchStatement = "SELECT fk.name AS FKName, current_table.name AS TableName, parent_table.name AS ParentTableName, " + "table_column.name AS ColName, parent_table_column.name AS ParentColName, " + "fk.delete_referential_action_desc AS OnDelete, " + "fk.update_referential_action_desc AS OnUpdate " + "FROM sys.objects AS current_table " + "JOIN sys.foreign_keys AS fk ON fk.parent_object_id=current_table.object_id AND fk.is_ms_shipped=0 " + "JOIN sys.foreign_key_columns AS fk_col ON fk_col.constraint_object_id=fk.object_id " + "JOIN sys.objects AS parent_table ON parent_table.object_id=fk_col.referenced_object_id " + "JOIN sys.columns AS table_column ON table_column.object_id=fk_col.parent_object_id AND table_column.column_id=fk_col.parent_column_id " + "JOIN sys.columns AS parent_table_column ON parent_table_column.object_id=fk_col.referenced_object_id AND parent_table_column.column_id=fk_col.referenced_column_id " + "WHERE current_table.object_id = ? ORDER BY fk.name"; + sqlAccess.AddBindParam( pRecordInfo->GetTableID() ); + RETURN_SQL_ERROR_ON_FALSE( sqlAccess.BYieldingExecute( FILE_AND_LINE, pchStatement ) ); + + FOR_EACH_SQL_RESULT( sqlAccess, 0, sqlRecord ) + { + // get all the data for the FK + const char *pchFKName; + DbgVerify( sqlRecord.BGetStringValue( 0, &pchFKName ) ); + + const char *pchTableName; + DbgVerify( sqlRecord.BGetStringValue( 1, &pchTableName ) ); + + AssertMsg( Q_stricmp( pchTableName, pRecordInfo->GetName() ) == 0, "FOREIGN KEY schema conversion found FK for table not matching search!\n" ); + + const char *pchParentTable; + DbgVerify( sqlRecord.BGetStringValue( 2, &pchParentTable ) ); + + const char *pchColName; + DbgVerify( sqlRecord.BGetStringValue( 3, &pchColName ) ); + + const char *pchParentColName; + DbgVerify( sqlRecord.BGetStringValue( 4, &pchParentColName ) ); + + const char *pchOnDelete; + DbgVerify( sqlRecord.BGetStringValue( 5, &pchOnDelete ) ); + + const char *pchOnUpdate; + DbgVerify( sqlRecord.BGetStringValue( 6, &pchOnUpdate ) ); + + // Is this more data for the FK we are already tracking? If so just append the column data, + // otherwise, assuming some data exists, add the key to the record info + if ( Q_strcmp( fkData.m_rgchName, pchFKName ) == 0 ) + { + int iColRelation = fkData.m_VecColumnRelations.AddToTail(); + FKColumnRelation_t &colRelation = fkData.m_VecColumnRelations[iColRelation]; + Q_strncpy( colRelation.m_rgchCol, pchColName, Q_ARRAYSIZE( colRelation.m_rgchCol ) ); + Q_strncpy( colRelation.m_rgchParentCol, pchParentColName, Q_ARRAYSIZE( colRelation.m_rgchParentCol ) ); + } + else + { + if ( Q_strlen( fkData.m_rgchName ) ) + { + pRecordInfo->AddFK( fkData ); + } + + // Do initial setup of the new key + Q_strncpy( fkData.m_rgchName, pchFKName, Q_ARRAYSIZE( fkData.m_rgchName ) ); + Q_strncpy( fkData.m_rgchParentTableName, pchParentTable, Q_ARRAYSIZE( fkData.m_rgchParentTableName ) ); + fkData.m_eOnDeleteAction = EForeignKeyActionFromName( pchOnDelete ); + fkData.m_eOnUpdateAction = EForeignKeyActionFromName( pchOnUpdate ); + int iColRelation = fkData.m_VecColumnRelations.AddToTail(); + FKColumnRelation_t &colRelation = fkData.m_VecColumnRelations[iColRelation]; + Q_strncpy( colRelation.m_rgchCol, pchColName, Q_ARRAYSIZE( colRelation.m_rgchCol ) ); + Q_strncpy( colRelation.m_rgchParentCol, pchParentColName, Q_ARRAYSIZE( colRelation.m_rgchParentCol ) ); + } + } + + + // Add the last key we were building data for + if ( Q_strlen( fkData.m_rgchName ) ) + { + pRecordInfo->AddFK( fkData ); + } + + return SQL_SUCCESS; +} + + +//----------------------------------------------------------------------------- +// Purpose: Creates a table in the DB to match the recordinfo +// Input: pRecordInfo - CRecordInfo to create a table for +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingCreateTable( ESchemaCatalog eSchemaCatalog, CRecordInfo *pRecordInfo ) +{ + CFmtStrMax sCmd; + SQLRETURN nRet = 0; + const char *pchName = pRecordInfo->GetName(); + Assert( pchName && pchName[0] ); + CSQLAccess sqlAccess( eSchemaCatalog ); + + // build the create table command + sCmd.sprintf( "CREATE TABLE %s (", pchName ); + + // add all the columns + for ( int iColumn = 0; iColumn < pRecordInfo->GetNumColumns(); iColumn++ ) + { + char rgchType[k_cSmallBuff]; + const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( iColumn ); + char *pchType = SQLTypeFromField( columnInfo, rgchType, Q_ARRAYSIZE( rgchType ) ); + Assert( pchType[0] ); + + // add the name and type of this column + sCmd.AppendFormat( "%s %s", columnInfo.GetName(), pchType ); + + // add any constraints + AppendConstraints( pRecordInfo, &columnInfo, true, sCmd ); + + if ( iColumn < ( pRecordInfo->GetNumColumns() - 1 ) ) + sCmd += ", "; + } + + AppendTableConstraints( pRecordInfo, sCmd ); + sCmd += ")"; + + + // create the table + // metadata operations aren't transactional, so we'll set bAutoTransaction = true + EXIT_ON_BOOLSQL_FAILURE( sqlAccess.BYieldingExecute( FILE_AND_LINE, sCmd ) ); + + // add any indexes + for ( int n = 0; n < pRecordInfo->GetIndexFields( ).Count(); n++ ) + { + const FieldSet_t& refSet = pRecordInfo->GetIndexFields( )[ n ]; + + // already added the PK index, so skip it + if ( n == pRecordInfo->GetPKIndex() ) + continue; + + // call YieldingAddIndex to do the work + nRet = YieldingAddIndex( eSchemaCatalog, pRecordInfo, refSet ); + EXIT_ON_SQL_FAILURE( nRet ); + } + +Exit: + return nRet; +} + +//----------------------------------------------------------------------------- +// Purpose: Adds an index of multiple columns to a table. +// Input: pSQLConnection - connection to use for command +// pRecordInfo - record info describing table +// refFields - description of index to add +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingAddIndex( ESchemaCatalog eSchemaCatalog, CRecordInfo *pRecordInfo, const FieldSet_t &refFields ) +{ + CSQLAccess sqlAccess( eSchemaCatalog ); + CUtlString sCmd = GetAddIndexSQL( pRecordInfo, refFields ); + RETURN_SQL_ERROR_ON_FALSE( sqlAccess.BYieldingExecute( FILE_AND_LINE, sCmd ) ); + return SQL_SUCCESS; +} + + +//----------------------------------------------------------------------------- +// Purpose: Depending on the conversion mode, either really perfom the +// conversion, or just log the SQL text so a human being can review +// it and do it later. +//----------------------------------------------------------------------------- + +SQLRETURN CJobUpdateSchema::YieldingProcessUnsafeConversion( ESchemaCatalog eSchemaCatalog, const char *pszSQL, const char *pszComment ) +{ + if ( k_EConversionModeConvertIrreversible != m_eConversionMode ) + { + if ( pszComment ) + { + m_sRecommendedSQL.Append( "-- " ); + m_sRecommendedSQL.Append( pszComment ); + m_sRecommendedSQL.Append( "\n" ); + } + m_sRecommendedSQL.Append( pszSQL ); + m_sRecommendedSQL.Append("\nGO\n \n"); // that space is a kludge, because we are using V_splitlines, which will ignore consecutive seperators + return SQL_SUCCESS; + } + CSQLAccess sqlAccess( eSchemaCatalog ); + RETURN_SQL_ERROR_ON_FALSE( sqlAccess.BYieldingExecute( FILE_AND_LINE, pszSQL ) ); + return SQL_SUCCESS; +} + +//----------------------------------------------------------------------------- +// Purpose: Add a SQL command to cleanup the schema that will actually destroy (supposedly unused) data. +//----------------------------------------------------------------------------- + +void CJobUpdateSchema::AddDataDestroyingConversion( const char *pszSQL, const char *pszComment ) +{ + if ( pszComment ) + { + m_sDataDestroyingCleanupSQL.Append( "-- " ); + m_sDataDestroyingCleanupSQL.Append( pszComment ); + m_sDataDestroyingCleanupSQL.Append( "\n" ); + } + m_sDataDestroyingCleanupSQL.Append( pszSQL ); + m_sDataDestroyingCleanupSQL.Append("\nGO\n \n"); // that space is a kludge, because we are using V_splitlines, which will ignore consecutive seperators +} + +//----------------------------------------------------------------------------- +// Purpose: Adds a column to a table +// Input: pRecordInfo - record info describing table to add a column to +// pColumnInfo - column info describing column to add +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingAlterTableAddColumn( ESchemaCatalog eSchemaCatalog, CRecordInfo *pRecordInfo, const CColumnInfo *pColumnInfo ) +{ + CSQLAccess sqlAccess( eSchemaCatalog ); + CFmtStrMax sCmd; + char rgchType[k_cSmallBuff]; + const char *pchTableName = pRecordInfo->GetName(); + Assert( pchTableName ); + const char *pchColumnName = pColumnInfo->GetName(); + Assert( pchColumnName ); + DbgVerify( SQLTypeFromField( *pColumnInfo, rgchType, Q_ARRAYSIZE( rgchType ) )[0] ); + + // build the alter table command + sCmd.sprintf( "ALTER TABLE %s ADD %s %s", pchTableName, pchColumnName, rgchType ); + + // add any constraints + AppendConstraints( pRecordInfo, pColumnInfo, true, sCmd ); + + // !KLUDGE! This is guaranteed to fail if it has "not null" in it. + if ( V_strstr( sCmd, "NOT NULL" ) ) + { + EmitError( SPEW_SQL, "Cannot add column %s to table %s with NOT NULL constraint\n", pchColumnName, pchTableName ); + EmitInfo( SPEW_SQL, SPEW_ALWAYS, LOG_ALWAYS, "Here is the SQL that should be run to add the column:\n" ); + EmitInfo( SPEW_SQL, SPEW_ALWAYS, LOG_ALWAYS, " %s\n", sCmd.String() ); + return SQL_ERROR; + } + + // execute the command. + RETURN_SQL_ERROR_ON_FALSE( sqlAccess.BYieldingExecute( FILE_AND_LINE, sCmd ) ); + return SQL_SUCCESS; +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds a constraint to an existing column +// Input: hDBC - SQL connection to execute on +// pRecordInfo - record info describing table +// pColumnInfo - column info describing column to add contraint for +// nColFlagConstraint - constraint to add +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingAddConstraint( ESchemaCatalog eSchemaCatalog, CRecordInfo *pRecordInfo, const CColumnInfo *pColumnInfo, int nColFlagConstraint ) +{ + Assert( pRecordInfo ); + Assert( pColumnInfo ); + + CFmtStrMax sCmd; + sCmd.sprintf( "ALTER TABLE App%u.%s ADD", GGCBase()->GetAppID(), pRecordInfo->GetName() ); + + Assert( !pColumnInfo->BIsClustered() ); + AppendConstraint( pRecordInfo->GetName(), pColumnInfo->GetName(), nColFlagConstraint, true, pColumnInfo->BIsClustered(), sCmd, 0 ); + + sCmd.AppendFormat( " (%s)", pColumnInfo->GetName() ); + + return YieldingProcessUnsafeConversion( eSchemaCatalog, sCmd ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Changes type or length of existing column +// Input: hDBC - SQL connection to execute on +// pRecordInfo - record info describing table +// pColumnInfoDesired - column info describing new column type or length +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingChangeColumnTypeOrLength( ESchemaCatalog eSchemaCatalog, CRecordInfo *pRecordInfo, const CColumnInfo *pColumnInfoDesired ) +{ + CUtlString sCmd = GetAlterColumnText( pRecordInfo, pColumnInfoDesired ); + return YieldingProcessUnsafeConversion( eSchemaCatalog, sCmd ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Changes constraints/indexes on a column +// Input: hDBC - SQL connection to execute on +// pRecordInfo - record info describing table +// pColumnInfoActual - column info describing existing column properties +// pColumnInfoActual - column info describing desired new column properties +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingChangeColumnProperties( ESchemaCatalog eSchemaCatalog, CRecordInfo *pRecordInfo, const CColumnInfo *pColumnInfoActual, + const CColumnInfo *pColumnInfoDesired ) +{ + CSQLAccess sqlAccess( eSchemaCatalog ); + Assert( pRecordInfo ); + Assert( pColumnInfoActual ); + Assert( pColumnInfoDesired ); + + SQLRETURN nRet = SQL_SUCCESS; + + pColumnInfoActual->ValidateColFlags(); + pColumnInfoDesired->ValidateColFlags(); + + Assert( pColumnInfoActual->GetColFlags() != pColumnInfoDesired->GetColFlags() ); + + // all the operations we might have to perform; note we have have to drop one + // thing and add another + bool bAddExplicitIndex = false, bRemoveExplicitIndex = false; + bool bAddUniqueConstraint = false, bRemoveUniqueConstraint = false; + bool bAddPrimaryKeyConstraint = false, bRemovePrimaryKeyConstraint = false; + + // determine which operations need to be performed + if ( !pColumnInfoActual->BIsPrimaryKey() && pColumnInfoDesired->BIsPrimaryKey() ) + { + bAddPrimaryKeyConstraint = true; + } + else if ( pColumnInfoActual->BIsPrimaryKey() && !pColumnInfoDesired->BIsPrimaryKey() ) + { + bRemovePrimaryKeyConstraint = true; + } + + if ( !pColumnInfoActual->BIsExplicitlyUnique() && pColumnInfoDesired->BIsExplicitlyUnique() ) + { + bAddUniqueConstraint = true; + } + else if ( pColumnInfoActual->BIsExplicitlyUnique() && !pColumnInfoDesired->BIsExplicitlyUnique() ) + { + bRemoveUniqueConstraint = true; + } + + if ( !pColumnInfoActual->BIsExplicitlyIndexed() && pColumnInfoDesired->BIsExplicitlyIndexed() ) + { + bAddExplicitIndex = true; + } + else if ( pColumnInfoActual->BIsExplicitlyIndexed() && !pColumnInfoDesired->BIsExplicitlyIndexed() ) + { + bRemoveExplicitIndex = true; + } + + // sanity check + Assert( !( bAddUniqueConstraint && bAddPrimaryKeyConstraint ) ); // primary key constraint adds implicit uniqueness constraint; it's a bug if we decide to do both + Assert( !( bAddUniqueConstraint && bAddExplicitIndex ) ); // uniqueness constraint adds implicit index; it's a bug if we decide to do both + Assert( !( bAddPrimaryKeyConstraint && bAddExplicitIndex ) ); // primary key constraint adds implicit index; it's a bug if we decide to do both + + // The index conversion stuff already handles dropping of unexpected indices + // and primary key constraints. I'm not even sure that this works or is used anymore. + if ( bAddUniqueConstraint ) + { + nRet = YieldingAddConstraint( eSchemaCatalog, pRecordInfo, pColumnInfoActual, k_nColFlagUnique ); + EXIT_ON_SQL_FAILURE( nRet ); + } + +Exit: + return nRet; +} + + + + +//----------------------------------------------------------------------------- +// Purpose: Creates specified trigger +// Input: pSQLConnection - SQL connection to execute on +// refTriggerInfo - trigger to be created +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingCreateTrigger( ESchemaCatalog eSchemaCatalog, CTriggerInfo &refTriggerInfo ) +{ + char rgchCmd[ k_cchSQLStatementTextMax]; + CSQLAccess sqlAccess( eSchemaCatalog ); + + // build the create command + Q_snprintf( rgchCmd, Q_ARRAYSIZE( rgchCmd ), + "CREATE TRIGGER [%s] ON [%s] " + "%s " + "AS BEGIN" + "%s\n" + "END", + refTriggerInfo.m_szTriggerName, + refTriggerInfo.m_szTriggerTableName, + refTriggerInfo.GetTriggerTypeString(), + refTriggerInfo.m_strText.Get() ); + + RETURN_SQL_ERROR_ON_FALSE( sqlAccess.BYieldingExecute( FILE_AND_LINE, rgchCmd ) ); + return SQL_SUCCESS; +} + + +//----------------------------------------------------------------------------- +// Purpose: drops specified trigger +// Input: pSQLConnection - SQL connection to execute on +// refTriggerInfo - trigger to be dropped +// Output: SQL return code. +//----------------------------------------------------------------------------- +SQLRETURN CJobUpdateSchema::YieldingDropTrigger( ESchemaCatalog eSchemaCatalog, CTriggerInfo &refTriggerInfo ) +{ + char rgchCmd[ k_cchSQLStatementTextMax]; + CSQLAccess sqlAccess( eSchemaCatalog ); + + // build the create command + Q_snprintf( rgchCmd, Q_ARRAYSIZE( rgchCmd ), + "DROP TRIGGER [%s];", + refTriggerInfo.m_szTriggerName ); + + RETURN_SQL_ERROR_ON_FALSE( sqlAccess.BYieldingExecute( FILE_AND_LINE, rgchCmd ) ); + return SQL_SUCCESS; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Used for SQL/EGCSQLType conversion +//----------------------------------------------------------------------------- +struct SQLTypeMapping_t +{ + const char *m_pchTypeName; + EGCSQLType m_eType; +}; + +static SQLTypeMapping_t g_rSQLTypeMapping[] = +{ + { "tinyint", k_EGCSQLType_int8 }, + { "smallint", k_EGCSQLType_int16 }, + { "int", k_EGCSQLType_int32 }, + { "bigint", k_EGCSQLType_int64 }, + { "real", k_EGCSQLType_float }, + { "float", k_EGCSQLType_double }, + { "text", k_EGCSQLType_String }, + { "ntext", k_EGCSQLType_String }, + { "char", k_EGCSQLType_String }, + { "varchar", k_EGCSQLType_String }, + { "nchar", k_EGCSQLType_String }, + { "nvarchar", k_EGCSQLType_String }, + { "sysname", k_EGCSQLType_String }, + { "varbinary", k_EGCSQLType_Blob }, + { "image", k_EGCSQLType_Image }, +}; +static uint32 g_cSQLTypeMapping = Q_ARRAYSIZE( g_rSQLTypeMapping ); + + +//----------------------------------------------------------------------------- +// Purpose: Returns the EGCSQLType for the specified SQL type name (or Invalid +// for unsupported types.) +//----------------------------------------------------------------------------- +EGCSQLType ETypeFromMSSQLDataType( const char *pchType ) +{ + for( uint32 unMapping = 0; unMapping < g_cSQLTypeMapping; unMapping++ ) + { + if( !Q_stricmp( pchType, g_rSQLTypeMapping[ unMapping ].m_pchTypeName ) ) + return g_rSQLTypeMapping[ unMapping ].m_eType; + } + return k_EGCSQLTypeInvalid; +} + + +//----------------------------------------------------------------------------- +// Purpose: Prepares the type map for use in schema upgrades +//----------------------------------------------------------------------------- +bool CJobUpdateSchema::YieldingBuildTypeMap( ESchemaCatalog eSchemaCatalog ) +{ + CSQLAccess sqlAccess( eSchemaCatalog ); + if( !sqlAccess.BYieldingExecute( FILE_AND_LINE, "select name, system_type_id from sys.types" ) ) + return false; + + FOR_EACH_SQL_RESULT( sqlAccess, 0, typeRecord ) + { + CFmtStr1024 sTypeName; + byte nTypeID; + DbgVerify( typeRecord.BGetStringValue( 0, &sTypeName ) ); + DbgVerify( typeRecord.BGetByteValue( 1, &nTypeID ) ); + + EGCSQLType eType = ETypeFromMSSQLDataType( sTypeName ); + if( eType != k_EGCSQLTypeInvalid ) + m_mapSQLTypeToEType.Insert( nTypeID, eType ); + } + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns the EGCSQLType for the specified type ID out of the database +//----------------------------------------------------------------------------- +EGCSQLType CJobUpdateSchema::GetEGCSQLTypeForMSSQLType( int nType ) +{ + int nIndex = m_mapSQLTypeToEType.Find( nType ); + if( m_mapSQLTypeToEType.IsValidIndex( nIndex ) ) + return m_mapSQLTypeToEType[ nIndex ]; + else + return k_EGCSQLTypeInvalid; +} + +} // namespace GCSDK diff --git a/gcsdk/sqlaccess/sqlaccess.cpp b/gcsdk/sqlaccess/sqlaccess.cpp new file mode 100644 index 0000000..130a446 --- /dev/null +++ b/gcsdk/sqlaccess/sqlaccess.cpp @@ -0,0 +1,953 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Provides access to SQL at a high level +// +//============================================================================= + +#include "stdafx.h" +#include "gcsdk/sqlaccess/sqlaccess.h" +#include "gcsdk/gcsqlquery.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +template< typename LISTENER_FUNC > +static void RunAndClearListenerList( std::vector< LISTENER_FUNC > &vecListeners ) +{ + // Let us not underestimate the ability of random listeners to re-enter everything. + std::vector< LISTENER_FUNC > listenerCopy; + listenerCopy.swap( vecListeners ); + vecListeners.clear(); + + // Why would you consider such a thing + DO_NOT_YIELD_THIS_SCOPE(); + + for ( const auto &listener : listenerCopy ) + { + listener(); + } +} + + +namespace GCSDK +{ +//------------------------------------------------------------------------------------ +// Purpose: Constructor +//------------------------------------------------------------------------------------ +CSQLAccess::CSQLAccess( ESchemaCatalog eSchemaCatalog ) + : m_eSchemaCatalog( eSchemaCatalog) + , m_pCurrentQuery( NULL ) + , m_bInTransaction( false ) +{ + m_pQueryGroup = CGCSQLQueryGroup::Alloc(); +} + + +//------------------------------------------------------------------------------------ +// Purpose: Destructor +//------------------------------------------------------------------------------------ +CSQLAccess::~CSQLAccess( ) +{ + SAFE_RELEASE( m_pQueryGroup ); + Assert( !m_pCurrentQuery ); + SAFE_DELETE( m_pCurrentQuery ); + AssertMsg( !m_bInTransaction, "GCSDK::CSQLAccess object being destroyed with a transaction pending. Use BCommitTransaction or RollbackTransaction to match your BBeginTransaction call." ); +} + + +//------------------------------------------------------------------------------------ +// Purpose: Perform a query +//------------------------------------------------------------------------------------ +bool CSQLAccess::BYieldingExecute( const char *pchName, const char *pchSQLCommand, uint32 *pcRowsAffected, bool bSpewOnError ) +{ + if ( NULL == pchName ) + { + pchName = pchSQLCommand; + } + + bool bStandalone = !BInTransaction(); + if( bStandalone ) + { + BBeginTransaction( pchName ); + } + + CurrentQuery()->SetCommand( pchSQLCommand ); + m_pQueryGroup->AddQuery( m_pCurrentQuery ); + m_pCurrentQuery = NULL; + + bool bSuccess = true; + if( bStandalone ) + { + bSuccess = BCommitTransaction(); + if( bSuccess && pcRowsAffected ) + { + *pcRowsAffected = m_pQueryGroup->GetResults()->GetRowsAffected( 0 ); + } + } + return bSuccess; +} + + +//------------------------------------------------------------------------------------ +// Purpose: Starts a transaction +//------------------------------------------------------------------------------------ +bool CSQLAccess::BBeginTransaction( const char *pchName ) +{ + Assert( !m_bInTransaction ); + if( m_bInTransaction ) + return false; + m_pQueryGroup->Clear(); + m_pQueryGroup->SetName( pchName ); + m_bInTransaction = true; + return true; +} + +//------------------------------------------------------------------------------------ +// Purpose: Returns the string last passed to BBeginTransaction +//------------------------------------------------------------------------------------ +const char *CSQLAccess::PchTransactionName( ) const +{ + return m_pQueryGroup->PchName(); +} + + +//------------------------------------------------------------------------------------ +// Purpose: Commits a transaction to the database +//------------------------------------------------------------------------------------ +bool CSQLAccess::BCommitTransaction( bool bAllowEmpty ) +{ + Assert( BInTransaction() ); + if( !BInTransaction() ) + return false; + + if( !m_pCurrentQuery && !m_pQueryGroup->GetStatementCount() ) + { + if( bAllowEmpty ) + { + // No-op success + m_bInTransaction = false; + RunListeners_Commit(); + return true; + } + else + { + AssertMsg1( false, "BCommitTransaction with empty transaction at %s", m_pQueryGroup->PchName() ); + return false; + } + } + + AssertMsg1( !m_pCurrentQuery, "Unexecuted query present in BCommitTransaction: %s", m_pCurrentQuery->PchCommand() ); + if( m_pCurrentQuery ) + return false; + + m_bInTransaction = false; + + if( !GJobCur().BYieldingRunQuery( m_pQueryGroup, m_eSchemaCatalog ) ) + { + // Notify listeners that the transaction did not succeed + RunListeners_Rollback(); + return false; + } + + // The transaction presumably did make the database, so we do not notify rollback listeners beyond here. + RunListeners_Commit(); + + if( !m_pQueryGroup->GetResults() ) + return false; + + return true; +} + + +//------------------------------------------------------------------------------------ +// Purpose: Rolls back a transaction and clears any queries +//------------------------------------------------------------------------------------ +void CSQLAccess::RollbackTransaction() +{ + bool bWasTransaction = BInTransaction(); + + Assert( bWasTransaction ); + SAFE_DELETE( m_pCurrentQuery ); + m_bInTransaction = false; + + if ( bWasTransaction ) + { + RunListeners_Rollback(); + } + else + { + m_vecCommitListeners.clear(); + m_vecRollbackListeners.clear(); + } +} + +//------------------------------------------------------------------------------------ +// Purpose: Adds a listener to be called synchronously should the transaction successfully commit +//------------------------------------------------------------------------------------ +void CSQLAccess::AddCommitListener( std::function<void (void)> &&listener ) +{ + if ( !BInTransaction() ) + { + AssertMsg( BInTransaction(), "Adding a listener to a non-transaction access, will never fire" ); + return; + } + + m_vecCommitListeners.push_back( std::move( listener ) ); +} + +//------------------------------------------------------------------------------------ +// Purpose: Adds a listener to be called synchronously should the transaction fail or explicitly rollback +//------------------------------------------------------------------------------------ +void CSQLAccess::AddRollbackListener( std::function<void (void)> &&listener ) +{ + if ( !BInTransaction() ) + { + AssertMsg( BInTransaction(), "Adding a listener to a non-transaction access, will never fire" ); + return; + } + + m_vecRollbackListeners.push_back( std::move( listener ) ); +} + +//------------------------------------------------------------------------------------ +// Purpose: Notifies listeners of successful commit. +//------------------------------------------------------------------------------------ +void CSQLAccess::RunListeners_Commit() +{ + RunAndClearListenerList( m_vecCommitListeners ); + // Clear the unused set + m_vecRollbackListeners.clear(); +} + +//------------------------------------------------------------------------------------ +// Purpose: Notifies listeners of a implicitly or explicitly rolled back transactions and clears the listener list. +//------------------------------------------------------------------------------------ +void CSQLAccess::RunListeners_Rollback() +{ + RunAndClearListenerList( m_vecRollbackListeners ); + // Clear the unused set + m_vecCommitListeners.clear(); +} + +//------------------------------------------------------------------------------------ +// Purpose: Perform a query that returns a single string +//------------------------------------------------------------------------------------ +CSQLAccess::EReadSingleResultResult CSQLAccess::BYieldingExecuteSingleResultDataInternal( const char *pchName, const char *pchSQLCommand, EGCSQLType eType, uint8 **ppubData, uint32 *punSize, uint32 *pcRowsAffected, bool bHasDefaultValue ) +{ + AssertMsg( !BInTransaction(), "BYieldingExecuteSingleResultData is not supported in a transaction" ); + if( BInTransaction() ) + return eReadSingle_Error; + + bool bRet = BYieldingExecute( pchName, pchSQLCommand, pcRowsAffected ); + if ( !bRet ) + return eReadSingle_Error; + + if( m_pQueryGroup->GetResults()->GetResultSetCount() != 1 ) + { + AssertMsg1( false, "Expected single result set, found %d", m_pQueryGroup->GetResults()->GetResultSetCount() ); + return eReadSingle_Error; + } + + IGCSQLResultSet *pResultSet = m_pQueryGroup->GetResults()->GetResultSet( 0 ); + + // If we have a default value, getting back zero rows is acceptable. + if( pResultSet->GetRowCount() == 0 && bHasDefaultValue ) + { + return eReadSingle_UseDefault; + } + + // If we either have more than one row or no default value specified, that's an error. + if( pResultSet->GetRowCount() != 1 ) + { + AssertMsg1( false, "Expected single result, found %d", pResultSet->GetRowCount() ); + return eReadSingle_Error; + } + + if( pResultSet->GetColumnCount() != 1 ) + { + AssertMsg1( false, "Expected single column, found %d", pResultSet->GetColumnCount() ); + return eReadSingle_Error; + } + if( pResultSet->GetColumnType( 0 ) != eType ) + { + AssertMsg2( false, "Expected column of type %s, found %s", PchNameFromEGCSQLType( eType ), PchNameFromEGCSQLType( pResultSet->GetColumnType( 0 ) ) ); + return eReadSingle_Error; + } + + return pResultSet->GetData( 0, 0, ppubData, punSize ) + ? eReadSingle_ResultFound + : eReadSingle_Error; +} + + + + +//------------------------------------------------------------------------------------ +// Purpose: Perform a query that returns a single string +//------------------------------------------------------------------------------------ +bool CSQLAccess::BYieldingExecuteString( const char *pchName, const char *pchSQLCommand, CFmtStr1024 *psResult, uint32 *pcRowsAffected ) +{ + uint8 *pubData; + uint32 cubData; + if( CSQLAccess::BYieldingExecuteSingleResultDataInternal( pchName, pchSQLCommand, k_EGCSQLType_String, &pubData, &cubData, pcRowsAffected, false ) != eReadSingle_ResultFound ) + return false; + + *psResult = (char *)pubData; + + return true; +} + +//------------------------------------------------------------------------------------ +// Purpose: Perform a query that returns a single int +//------------------------------------------------------------------------------------ +bool CSQLAccess::BYieldingExecuteScalarInt( const char *pchName, const char *pchSQLCommand, int *pnResult, uint32 *pcRowsAffected ) +{ + return BYieldingExecuteSingleResult<int32, uint32>( pchName, pchSQLCommand, k_EGCSQLType_int32, pnResult, pcRowsAffected ); +} + +bool CSQLAccess::BYieldingExecuteScalarIntWithDefault( const char *pchName, const char *pchSQLCommand, int *pnResult, int iDefaultValue, uint32 *pcRowsAffected ) +{ + return BYieldingExecuteSingleResultWithDefault<int32, uint32>( pchName, pchSQLCommand, k_EGCSQLType_int32, pnResult, iDefaultValue, pcRowsAffected ); +} + +//------------------------------------------------------------------------------------ +// Purpose: Perform a query that returns a single uint32 +//------------------------------------------------------------------------------------ +bool CSQLAccess::BYieldingExecuteScalarUint32( const char *pchName, const char *pchSQLCommand, uint32 *punResult, uint32 *pcRowsAffected ) +{ + return BYieldingExecuteSingleResult<uint32, uint32>( pchName, pchSQLCommand, k_EGCSQLType_int32, punResult, pcRowsAffected ); +} + +bool CSQLAccess::BYieldingExecuteScalarUint32WithDefault( const char *pchName, const char *pchSQLCommand, uint32 *punResult, uint32 unDefaultValue, uint32 *pcRowsAffected ) +{ + return BYieldingExecuteSingleResultWithDefault<uint32, uint32>( pchName, pchSQLCommand, k_EGCSQLType_int32, punResult, unDefaultValue, pcRowsAffected ); +} + +//------------------------------------------------------------------------------------ +// Purpose: A bunch of pass throughs to the query itself +//------------------------------------------------------------------------------------ +void CSQLAccess::AddBindParam( const char *pchValue ) +{ + CurrentQuery()->AddBindParam( pchValue ); +} + +void CSQLAccess::AddBindParam( const int16 nValue ) +{ + CurrentQuery()->AddBindParam( nValue ); +} + +void CSQLAccess::AddBindParam( const uint16 uValue ) +{ + CurrentQuery()->AddBindParam( uValue ); +} + +void CSQLAccess::AddBindParam( const int32 nValue ) +{ + CurrentQuery()->AddBindParam( nValue ); +} + +void CSQLAccess::AddBindParam( const uint32 uValue ) +{ + CurrentQuery()->AddBindParam( uValue ); +} + +void CSQLAccess::AddBindParam( const uint64 ulValue ) +{ + CurrentQuery()->AddBindParam( ulValue ); +} + +void CSQLAccess::AddBindParam( const uint8 *ubValue, const int cubValue ) +{ + CurrentQuery()->AddBindParam( ubValue, cubValue ); +} + +void CSQLAccess::AddBindParam( const float fValue ) +{ + CurrentQuery()->AddBindParam( fValue ); +} + +void CSQLAccess::AddBindParam( const double dValue ) +{ + CurrentQuery()->AddBindParam( dValue ); +} + +void CSQLAccess::AddBindParamRaw( EGCSQLType eType, const byte *pubData, uint32 cubData ) +{ + CurrentQuery()->AddBindParamRaw( eType, pubData, cubData ); +} + +void CSQLAccess::ClearParams() +{ + if( m_pCurrentQuery ) + { + delete m_pCurrentQuery; + m_pCurrentQuery = NULL; + } +} + + +IGCSQLResultSetList *CSQLAccess::GetResults() +{ + return m_pQueryGroup->GetResults(); +} + + +//------------------------------------------------------------------------------------ +// Purpose: Returns the number of result sets +//------------------------------------------------------------------------------------ +uint32 CSQLAccess::GetResultSetCount() +{ + if( m_pQueryGroup->GetResults() ) + return m_pQueryGroup->GetResults()->GetResultSetCount(); + else + return 0; +} + + +//------------------------------------------------------------------------------------ +// Purpose: Returns the number of rows in a result set +//------------------------------------------------------------------------------------ +uint32 CSQLAccess::GetResultSetRowCount( uint32 unResultSet ) +{ + if( m_pQueryGroup->GetResults() && unResultSet < m_pQueryGroup->GetResults()->GetResultSetCount() ) + return m_pQueryGroup->GetResults()->GetResultSet( unResultSet )->GetRowCount(); + else + return 0; +} + + +//------------------------------------------------------------------------------------ +// Purpose: Returns a CSQLRecord object that represents a row in a result set +//------------------------------------------------------------------------------------ +CSQLRecord CSQLAccess::GetResultRecord( uint32 unResultSet, uint32 unRow ) +{ + if( m_pQueryGroup->GetResults() && unResultSet < m_pQueryGroup->GetResults()->GetResultSetCount() ) + { + IGCSQLResultSet *pResultSet = m_pQueryGroup->GetResults()->GetResultSet( unResultSet ); + if( unRow < pResultSet->GetRowCount() ) + return CSQLRecord( unRow, pResultSet ); + } + return CSQLRecord(); // if there was a problem return an empty record +} + +//----------------------------------------------------------------------------- +// Purpose: Inserts a new record into the DS +// Input: pRecordBase - record to insert +// Output: true if successful, false otherwise +//----------------------------------------------------------------------------- +bool CSQLAccess::BYieldingInsertRecord( const CRecordBase *pRecordBase ) +{ + ClearParams(); + + const CRecordInfo *pRecordInfo = pRecordBase->GetPSchema()->GetRecordInfo(); + int cColumns = pRecordInfo->GetNumColumns(); + for ( int nColumn = 0; nColumn < cColumns; nColumn++ ) + { + const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( nColumn ); + if ( !columnInfo.BIsInsertable() ) + continue; + + uint8 *pubData; + uint32 cubData; + DbgVerify( pRecordBase->BGetField( nColumn, &pubData, &cubData ) ); + + CurrentQuery()->AddBindParamRaw( columnInfo.GetType(), pubData, cubData ); + } + + uint32 nRows; + const char *pchStatement = pRecordBase->GetPSchema()->GetInsertStatementText(); + + bool bRet = BYieldingExecute( pchStatement, pchStatement, &nRows ); + return ( nRows == 1 || BInTransaction() ) && bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Inserts a new record into the DS if such row doesn't exist +// Input: pRecordBase - record to insert +// Output: true if successful, false otherwise +//----------------------------------------------------------------------------- +bool CSQLAccess::BYieldingInsertWhenNotMatchedOnPK( CRecordBase *pRecordBase ) +{ + ClearParams(); + + const CRecordInfo *pRecordInfo = pRecordBase->GetPSchema()->GetRecordInfo(); + int cColumns = pRecordInfo->GetNumColumns(); + for ( int nColumn = 0; nColumn < cColumns; nColumn++ ) + { + const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( nColumn ); + if ( !columnInfo.BIsInsertable() ) + { + Assert( columnInfo.BIsInsertable() ); + return false; + } + + uint8 *pubData; + uint32 cubData; + DbgVerify( pRecordBase->BGetField( nColumn, &pubData, &cubData ) ); + + CurrentQuery()->AddBindParamRaw( columnInfo.GetType(), pubData, cubData ); + } + + uint32 nRows; + const char *pchStatement = pRecordBase->GetPSchema()->GetMergeStatementTextOnPKWhenNotMatchedInsert(); + + bool bRet = BYieldingExecute( pchStatement, pchStatement, &nRows ); + return ( nRows == 1 || nRows == 0 || BInTransaction() ) && bRet; +} + +//----------------------------------------------------------------------------- +// Purpose: Inserts a new record into the DS if such row doesn't exist +// updates an existing row if such row is matched by PK +// Input: pRecordBase - record to insert +// Output: true if successful, false otherwise +//----------------------------------------------------------------------------- +bool CSQLAccess::BYieldingInsertOrUpdateOnPK( CRecordBase *pRecordBase ) +{ + ClearParams(); + + const CRecordInfo *pRecordInfo = pRecordBase->GetPSchema()->GetRecordInfo(); + int cColumns = pRecordInfo->GetNumColumns(); + for ( int nColumn = 0; nColumn < cColumns; nColumn++ ) + { + const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( nColumn ); + if ( !columnInfo.BIsInsertable() ) + { + Assert( columnInfo.BIsInsertable() ); + return false; + } + + uint8 *pubData; + uint32 cubData; + DbgVerify( pRecordBase->BGetField( nColumn, &pubData, &cubData ) ); + + CurrentQuery()->AddBindParamRaw( columnInfo.GetType(), pubData, cubData ); + } + + uint32 nRows; + const char *pchStatement = pRecordBase->GetPSchema()->GetMergeStatementTextOnPKWhenMatchedUpdateWhenNotMatchedInsert(); + + bool bRet = BYieldingExecute( pchStatement, pchStatement, &nRows ); + return ( nRows == 1 || BInTransaction() ) && bRet; +} + +//----------------------------------------------------------------------------- +// Purpose: Inserts a new record into the DB and reads non-insertable fields back +// into the record. +// Input: pRecordBase - record to insert +// Output: true if successful, false otherwise +//----------------------------------------------------------------------------- +bool CSQLAccess::BYieldingInsertWithIdentity( CRecordBase* pRecordBase ) +{ + AssertMsg( !BInTransaction(), "BYieldingInsertWithIdentity is not supported in a transaction" ); + if( BInTransaction() ) + return false; + ClearParams(); + + TSQLCmdStr sStatement; + CUtlVector<int> vecOutputFields; + CRecordInfo *pRecordInfo = pRecordBase->GetPSchema()->GetRecordInfo(); + BuildInsertAndReadStatementText( &sStatement, &vecOutputFields, pRecordInfo ); + + AssertMsg( vecOutputFields.Count() > 0, "BYieldingInsertAndReadRecord called for a record type with no non-insertable columns" ); + if ( vecOutputFields.Count() == 0 ) + return false; + + int cColumns = pRecordInfo->GetNumColumns(); + for ( int nColumn = 0; nColumn < cColumns; nColumn++ ) + { + const CColumnInfo &columnInfo = pRecordInfo->GetColumnInfo( nColumn ); + if ( !columnInfo.BIsInsertable() ) + { + continue; + } + + uint8 *pubData; + uint32 cubData; + DbgVerify( pRecordBase->BGetField( nColumn, &pubData, &cubData ) ); + + CurrentQuery()->AddBindParamRaw( columnInfo.GetType(), pubData, cubData ); + } + + bool bRet = BYieldingExecute( sStatement, sStatement ); + if( !bRet ) + return false; + + Assert( 1 == GetResultSetCount() ); + if ( 1 != GetResultSetCount() ) + return false; + + IGCSQLResultSet *pResultSet = m_pQueryGroup->GetResults()->GetResultSet( 0 ); + Assert( 1 == pResultSet->GetRowCount() ); + if ( 1 != pResultSet->GetRowCount() ) + return false; + + Assert( (uint32)vecOutputFields.Count() == pResultSet->GetColumnCount() ); + if ( (uint32)vecOutputFields.Count() != pResultSet->GetColumnCount() ) + return false; + + for( uint32 nColumn = 0; nColumn < pResultSet->GetColumnCount(); nColumn++ ) + { + uint8 *pubData; + uint32 cubData; + DbgVerify( pResultSet->GetData( 0, nColumn, &pubData, &cubData ) ); + + int nSchColumn = vecOutputFields[nColumn]; + Assert( pResultSet->GetColumnType( nColumn ) == pRecordInfo->GetColumnInfo( nSchColumn ).GetType() ); + DbgVerify( pRecordBase->BSetField( nSchColumn, pubData, cubData ) ); + } + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Reads a list of records from the DB according to the specified where +// clause +// Input: pRecordBase - record to read +// readSet - The set of columns to read +// whereSet - The set of columns to query on +// Output: true if successful, false otherwise +//----------------------------------------------------------------------------- +EResult CSQLAccess::YieldingReadRecordWithWhereColumns( CRecordBase *pRecord, const CColumnSet & readSet, const CColumnSet & whereSet, const char* pchOrderClause ) +{ + AssertMsg( !BInTransaction(), "BYieldingReadRecordWithWhereColumns is not supported in a transaction" ); + if( BInTransaction() ) + return k_EResultInvalidState; + + //if there is an order by clause, only take the top one, if there isn't, then validate that we have a single instance + const char* pszTopClause = ( pchOrderClause ) ? "TOP (1)" : "TOP (2)"; + + TSQLCmdStr sStatement; + BuildSelectStatementText( &sStatement, readSet, pszTopClause ); + + // if we actually have some columns for the where clause, + // append a where clause. + if( whereSet.GetColumnCount() ) + { + sStatement.Append( " WHERE " ); + AppendWhereClauseText( &sStatement, whereSet ); + AddRecordParameters( *pRecord, whereSet ); + } + //append the order by if they added one + if( pchOrderClause ) + { + sStatement.Append( " ORDER BY " ); + sStatement.Append( pchOrderClause ); + } + + Assert(!readSet.IsEmpty() ); + if( !BYieldingExecute( sStatement, sStatement ) ) + return k_EResultFail; + + if ( GetResultSetCount() != 1 ) + { + AssertMsg( GetResultSetCount() == 1, "Unexpected number of result sets returned from select statement" ); + return k_EResultFail; + } + + // make sure the types are the same + IGCSQLResultSet *pResultSet = m_pQueryGroup->GetResults()->GetResultSet( 0 ); + if ( pResultSet->GetRowCount() == 0 ) + return k_EResultNoMatch; + + //note that since we only take the top one when there is an order by clause, we don't need to handle that case down here, only if top 2 is selected + if( pResultSet->GetRowCount() != 1 ) + { + // Make sure we aren't failing because there are multiple matching records. + // That is probably a misuse of the API or some unexpected condition. + AssertMsg1( false, "BYieldingReadRecordWithWhereColumns from %s failing because multiple records match WHERE clause", readSet.GetRecordInfo()->GetName() ); + return k_EResultLimitExceeded; + } + FOR_EACH_COLUMN_IN_SET( readSet, nColumnIndex ) + { + EGCSQLType eRecordType = readSet.GetColumnInfo( nColumnIndex ).GetType(); + EGCSQLType eResultType = pResultSet->GetColumnType( nColumnIndex ); + + AssertMsg2( eResultType == eRecordType, "Column %d type mismatch in %s", nColumnIndex, readSet.GetRecordInfo()->GetName() ); + if( eRecordType != eResultType ) + return k_EResultInvalidParam; + } + + CSQLRecord sqlRecord = GetResultRecord( 0, 0 ); + + FOR_EACH_COLUMN_IN_SET( readSet, nColumnIndex ) + { + uint8 *pubData; + uint32 cubData; + + DbgVerify( sqlRecord.BGetColumnData( nColumnIndex, &pubData, (int*)&cubData ) ); + DbgVerify( pRecord->BSetField( readSet.GetColumn( nColumnIndex), pubData, cubData ) ); + } + + return k_EResultOK; +} + +//----------------------------------------------------------------------------- +// Purpose: Updates a record in the DB +// Input: record - data source for columns to match against (whereColumns) and +// columns to assign (updateColumns) +// whereColumns - columns to match against +// updateColumns - columns to update +// Output: true if successful, false otherwise +//----------------------------------------------------------------------------- +bool CSQLAccess::BYieldingUpdateRecord( const CRecordBase & record, const CColumnSet & whereColumns, const CColumnSet & updateColumns, const CSQLOutputParams *pOptionalOutputParams /* = NULL */ ) +{ + return BYieldingUpdateRecords( record, whereColumns, record, updateColumns, pOptionalOutputParams ); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CSQLAccess::BYieldingUpdateRecords( const CRecordBase & whereRecord, const CColumnSet & whereColumns, const CRecordBase & updateRecord, const CColumnSet & updateColumns, const CSQLOutputParams *pOptionalOutputParams /* = NULL */ ) +{ + ClearParams(); + + Assert( whereColumns.GetRecordInfo() == updateColumns.GetRecordInfo() ); + if ( whereColumns.GetRecordInfo() != updateColumns.GetRecordInfo() ) + return false; + Assert( whereColumns.GetRecordInfo() == whereRecord.GetPSchema()->GetRecordInfo() ); + if ( whereColumns.GetRecordInfo() != whereRecord.GetPSchema()->GetRecordInfo() ) + return false; + Assert( whereColumns.GetRecordInfo() == updateRecord.GetPSchema()->GetRecordInfo() ); + if ( whereColumns.GetRecordInfo() != updateRecord.GetPSchema()->GetRecordInfo() ) + return false; + + AssertMsg( !updateColumns.IsEmpty(), "Someone is calling BYieldingUpdateRecord with no columns to update." ); + if ( updateColumns.IsEmpty() ) + return false; + + // add the columns we're updating as bound params + TSQLCmdStr sStatement; + BuildUpdateStatementText( &sStatement, updateColumns ); + + AddRecordParameters( updateRecord, updateColumns ); + + // did the users specify an OUTPUT block? + if ( pOptionalOutputParams ) + { + TSQLCmdStr sOutput; + BuildOutputClauseText( &sOutput, pOptionalOutputParams->GetColumnSet() ); + sStatement.Append( sOutput ); + + AddRecordParameters( pOptionalOutputParams->GetRecord(), pOptionalOutputParams->GetColumnSet() ); + } + + if ( !whereColumns.IsEmpty() ) + { + sStatement.Append( " WHERE " ); + AppendWhereClauseText( &sStatement, whereColumns ); + + // add the columns we're querying on as bound params + AddRecordParameters( whereRecord, whereColumns ); + } + + return BYieldingExecute( sStatement, sStatement ); +} + +//----------------------------------------------------------------------------- +// Purpose: Deletes this record's row in the table +// Input: record - record to delete +// whereColumns - columns to use when searching for this record +//----------------------------------------------------------------------------- +bool CSQLAccess::BYieldingDeleteRecords( const CRecordBase & record, const CColumnSet & whereColumns ) +{ + Assert( whereColumns.GetRecordInfo() == record.GetPSchema()->GetRecordInfo() ); + if ( whereColumns.GetRecordInfo() != record.GetPSchema()->GetRecordInfo() ) + return false; + + ClearParams(); + AddRecordParameters( record, whereColumns ); + + TSQLCmdStr sStatement; + BuildDeleteStatementText( &sStatement, record.GetPRecordInfo() ); + sStatement.Append( " WHERE " ); + AppendWhereClauseText( &sStatement, whereColumns ); + + uint32 unRowsAffected; + if( !BYieldingExecute( sStatement, sStatement, &unRowsAffected ) ) + return false; + + return unRowsAffected > 0 || BInTransaction(); +} + +//-------------------------------------------------------------------------------------------------------------------------------- +// CSQLUpdateOrInsert +//-------------------------------------------------------------------------------------------------------------------------------- + +CSQLUpdateOrInsert::CSQLUpdateOrInsert( const char* pszName, int nTable, const CColumnSet & whereColumns, const CColumnSet & updateColumns, const char* pszWhereClause, const char* pszUpdateClause ) +{ + const CRecordInfo* pRecordInfo = GSchemaFull().GetSchema( nTable ).GetRecordInfo(); + + //how many columns do we have + const int nNumColumns = pRecordInfo->GetNumColumns(); + + TSQLCmdStr sStatement; + sStatement = "MERGE INTO "; + sStatement.Append( GSchemaFull().GetDefaultSchemaNameForCatalog( pRecordInfo->GetESchemaCatalog() ) ); + sStatement.Append( '.' ); + sStatement.Append( pRecordInfo->GetName() ); + sStatement.Append( " WITH(HOLDLOCK) AS D USING(VALUES(" ); + sStatement.AppendFormat( "%.*s", GetInsertArgStringChars( nNumColumns ), GetInsertArgString() ); + sStatement.Append( "))AS S(" ); + + //add each column that we are adding the values for, along with the parameter from the structure + for( int nCurrColumn = 0; nCurrColumn < nNumColumns; nCurrColumn++ ) + { + const CColumnInfo& colInfo = pRecordInfo->GetColumnInfo( nCurrColumn ); + if( nCurrColumn != 0 ) + sStatement.Append( ',' ); + sStatement.Append( colInfo.GetName() ); + } + + //our where clause + sStatement.Append( ")ON " ); + + if( pszWhereClause ) + { + sStatement.Append( pszWhereClause ); + } + else + { + FOR_EACH_COLUMN_IN_SET( whereColumns, nCurrColumn ) + { + const char* pszColName = pRecordInfo->GetColumnInfo( whereColumns.GetColumn( nCurrColumn ) ).GetName(); + if( nCurrColumn > 0 ) + sStatement.Append( " AND " ); + sStatement.AppendFormat( "D.%s=S.%s", pszColName, pszColName ); + } + } + + //our update clause (if they have provided fields that they want to update) + if( pszUpdateClause || !updateColumns.IsEmpty() ) + { + sStatement.Append( " WHEN MATCHED THEN UPDATE SET " ); + if( pszUpdateClause ) + { + sStatement.Append( pszUpdateClause ); + } + else + { + FOR_EACH_COLUMN_IN_SET( updateColumns, nCurrColumn ) + { + const char* pszColName = pRecordInfo->GetColumnInfo( updateColumns.GetColumn( nCurrColumn ) ).GetName(); + if( nCurrColumn > 0 ) + sStatement.Append( ',' ); + sStatement.AppendFormat( "%s=S.%s", pszColName, pszColName ); + } + } + } + + //our insert clause + sStatement.Append( " WHEN NOT MATCHED THEN INSERT(" ); + bool bFirstColumn = true; + for( int nCurrColumn = 0; nCurrColumn < nNumColumns; nCurrColumn++ ) + { + const CColumnInfo& colInfo = pRecordInfo->GetColumnInfo( nCurrColumn ); + if( !colInfo.BIsInsertable() ) + continue; + + if( !bFirstColumn ) + sStatement.Append( ',' ); + bFirstColumn = false; + sStatement.Append( colInfo.GetName() ); + } + + sStatement.Append( ")VALUES(" ); + bFirstColumn = true; + for( int nCurrColumn = 0; nCurrColumn < nNumColumns; nCurrColumn++ ) + { + const CColumnInfo& colInfo = pRecordInfo->GetColumnInfo( nCurrColumn ); + if( !colInfo.BIsInsertable() ) + continue; + + if( !bFirstColumn ) + sStatement.Append( ',' ); + bFirstColumn = false; + sStatement.AppendFormat( "S.%s", colInfo.GetName() ); + } + sStatement.Append( ");" ); + + //save our results so we can execute it in the future + m_nTable = nTable; + m_sName = pszName; + m_sQuery = sStatement; +} + +bool CSQLUpdateOrInsert::BYieldingExecute( CSQLAccess& sqlAccess, const CRecordBase& record, uint32 *out_punRowsAffected /* = NULL */ ) const +{ + AssertMsg2( record.GetITable() == m_nTable, "Error: Merge was compiled for table %s, but was attempted to be executed against %s", GSchemaFull().GetSchema( m_nTable ).GetRecordInfo()->GetName(), record.GetPRecordInfo()->GetName() ); + + const CRecordInfo* pRecordInfo = record.GetPRecordInfo(); + //how many columns do we have + const int nNumColumns = pRecordInfo->GetNumColumns(); + + sqlAccess.ClearParams(); + for( int nCurrColumn = 0; nCurrColumn < nNumColumns; nCurrColumn++ ) + { + const CColumnInfo& colInfo = pRecordInfo->GetColumnInfo( nCurrColumn ); + uint8 *pubData; + uint32 cubData; + DbgVerify( record.BGetField( nCurrColumn, &pubData, &cubData ) ); + sqlAccess.AddBindParamRaw( colInfo.GetType(), pubData, cubData ); + } + + return sqlAccess.BYieldingExecute( m_sName, m_sQuery, out_punRowsAffected ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Adds bind parameters to the list based on a set of fields in a record +// Input: record - record to insert +// columnSet - The set of columns to add as params +//----------------------------------------------------------------------------- +void CSQLAccess::AddRecordParameters( const CRecordBase &record, const CColumnSet & columnSet ) +{ + Assert( record.GetPSchema()->GetRecordInfo() == columnSet.GetRecordInfo() ); + if ( record.GetPSchema()->GetRecordInfo() != columnSet.GetRecordInfo() ) + return; + + FOR_EACH_COLUMN_IN_SET( columnSet, nColumnIndex ) + { + const CColumnInfo &columnInfo = columnSet.GetColumnInfo( nColumnIndex ); + uint8 *pubData; + uint32 cubData; + DbgVerify( record.BGetField( columnSet.GetColumn( nColumnIndex ), &pubData, &cubData ) ); + EGCSQLType eType = columnInfo.GetType(); + CurrentQuery()->AddBindParamRaw( eType, pubData, cubData ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Deletes all records from a table +// Input: iTable - table to wipe +// Output: true if the operation was successful +// Note: PERFORMANCE WARNING: this is slow on big tables, not intended for use +// in production +//----------------------------------------------------------------------------- +bool CSQLAccess::BYieldingWipeTable( int iTable ) +{ + // make a wipe operation + CRecordInfo *pRecordInfo = GSchemaFull().GetSchema( iTable ).GetRecordInfo(); + + CUtlString buf; + buf.Format( "DELETE FROM %s", pRecordInfo->GetName() ); + return BYieldingExecute( buf.String(), buf.String() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Returns the current query to add stuff to, creating it if there isn't +// already a current query +//----------------------------------------------------------------------------- +CGCSQLQuery *CSQLAccess::CurrentQuery() +{ + if( m_pCurrentQuery ) + return m_pCurrentQuery; + + m_pCurrentQuery = new CGCSQLQuery(); + return m_pCurrentQuery; +} + + +} // namespace GCSDK diff --git a/gcsdk/sqlaccess/sqlrecord.cpp b/gcsdk/sqlaccess/sqlrecord.cpp new file mode 100644 index 0000000..5b285b4 --- /dev/null +++ b/gcsdk/sqlaccess/sqlrecord.cpp @@ -0,0 +1,538 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//============================================================================= + + +#include "stdafx.h" + +//#include "sqlaccess.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +namespace GCSDK +{ + +//----------------------------------------------------------------------------- +// Purpose: Constructor +//----------------------------------------------------------------------------- +CSQLRecord::CSQLRecord( uint32 unRow, IGCSQLResultSet *pResultSet ) +{ + Init( unRow, pResultSet ); +} +CSQLRecord::CSQLRecord() +: m_pResultSet( NULL ), m_unRow( 0 ) +{ +} + + +//----------------------------------------------------------------------------- +// Purpose: Destructor +//----------------------------------------------------------------------------- +CSQLRecord::~CSQLRecord() +{ +} + + +//----------------------------------------------------------------------------- +// Purpose: Initializes a blank record +// Input: iTable - table that this record will belong to +//----------------------------------------------------------------------------- +void CSQLRecord::Init( uint32 unRow, IGCSQLResultSet *pResultSet ) +{ + if( unRow >= pResultSet->GetRowCount() ) + { + m_pResultSet = NULL; + m_unRow = 0; + } + else + { + m_pResultSet = pResultSet; + m_unRow = unRow; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets data for a field in this record +// Input: unColumn - field to get +// pubField - pointer to get filled in with pointer to data +// cubField - pointer to get filled in with size of data +// Output: true if successful, false if data not present +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetColumnData( uint32 unColumn, uint8 **ppubField, int *pcubField ) +{ + size_t sz; + bool bRet = BGetColumnData( unColumn, ppubField, &sz ); + *pcubField = static_cast< int >( sz ); + return bRet; +} + +//----------------------------------------------------------------------------- +// Purpose: Gets data for a field in this record +// Input: unColumn - field to get +// pubField - pointer to get filled in with pointer to data +// cubField - pointer to get filled in with size of data +// Output: true if successful, false if data not present +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetColumnData( uint32 unColumn, uint8 **ppubField, size_t *pcubField ) +{ + Assert( ppubField ); + Assert( pcubField ); + *ppubField = NULL; + *pcubField = 0; + + Assert( m_pResultSet ); + + if ( !BValidateColumnIndex( unColumn ) ) + return false; + + *pcubField = 0; + return m_pResultSet->GetData( m_unRow, unColumn, ppubField, (uint32*)pcubField ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets string data for a field in this record +// Input: unColumn - field to get +// ppchVal - pointer to pointer to fill in to string data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetStringValue( uint32 unColumn, const char **ppchVal ) +{ + Assert( ppchVal ); + *ppchVal = NULL; + + uint8 *pubData = NULL; + int cubData = 0; + Assert( k_EGCSQLType_String == m_pResultSet->GetColumnType( unColumn ) ); + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + *ppchVal = (const char *) pubData; + + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets string data for a field in this record +// Input: unColumn - field to get +// ppchVal - pointer to pointer to fill in to string data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetStringValue( uint32 unColumn, CFmtStr1024 *psVal ) +{ + Assert( psVal ); + *psVal = ""; + + uint8 *pubData = NULL; + int cubData = 0; + Assert( k_EGCSQLType_String == m_pResultSet->GetColumnType( unColumn ) ); + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + *psVal = (const char *) pubData; + + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets int data for a field in this record +// Input: unColumn - field to get +// pnVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetIntValue( uint32 unColumn, int *pnVal ) +{ + Assert( pnVal ); + *pnVal = 0; + + uint8 *pubData = NULL; + int cubData = 0; + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + { + switch( m_pResultSet->GetColumnType( unColumn ) ) + { + case k_EGCSQLType_int64: + { + int64 ul = *((int64 *)pubData); + if ( ul >= LONG_MIN && ul <= LONG_MAX ) + { + *pnVal = (int)ul; + return true; + } + else + { + AssertMsg1(false, "GetIntValue tried to catch %lld in an int, which is too small", ul ); + return false; + } + } + break; + + case k_EGCSQLType_int32: + *pnVal = *((int32 *)pubData); + return true; + + case k_EGCSQLType_int16: + *pnVal = *((int16 *)pubData); + return true; + + case k_EGCSQLType_int8: + *pnVal = *((int8 *)pubData); + return true; + + default: + AssertMsg1(false, "GetIntValue tried to catch a %s, which is the wrong type", PchNameFromEGCSQLType( m_pResultSet->GetColumnType( unColumn ) ) ); + return false; + } + } + + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets float data for a field in this record +// Input: unColumn - field to get +// pnVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetFloatValue( uint32 unColumn, float *pfVal ) +{ + Assert( pfVal ); + *pfVal = 0.0f; + + uint8 *pubData = NULL; + int cubData = 0; + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + { + Assert( k_EGCSQLType_float == m_pResultSet->GetColumnType( unColumn ) ); + AssertMsg2( sizeof( float ) == cubData, "GetValue expected %llu bytes, found %d", (uint64)sizeof( float ), cubData ); + if ( sizeof( float ) != cubData ) + return false; + *pfVal = *( (float *) pubData ); + } + + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets double data for a field in this record +// Input: unColumn - field to get +// pnVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetDoubleValue( uint32 unColumn, double *pdVal ) +{ + Assert( pdVal ); + *pdVal = 0.0f; + + uint8 *pubData = NULL; + int cubData = 0; + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + { + Assert( k_EGCSQLType_double == m_pResultSet->GetColumnType( unColumn ) ); + AssertMsg2( sizeof( double ) == cubData, "GetValue expected %llu bytes, found %d", (uint64)sizeof( double ), cubData ); + if ( sizeof( double ) != cubData ) + return false; + *pdVal = *( (double *) pubData ); + } + + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets int data for a field in this record +// Input: unColumn - field to get +// pVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetByteValue( uint32 unColumn, byte *pVal ) +{ + Assert( pVal ); + *pVal = 0; + + uint8 *pubData = NULL; + int cubData = 0; + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + { + Assert( k_EGCSQLType_int8 == m_pResultSet->GetColumnType( unColumn ) ); + AssertMsg1( 1 == cubData, "GetValue expected 1 bytes, found %d", cubData ); + if ( 1 != cubData ) + return false; + *pVal = *( (byte *) pubData ); + } + + return bRet; +} + +//----------------------------------------------------------------------------- +// Purpose: Gets int data for a field in this record +// Input: unColumn - field to get +// pVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetBoolValue( uint32 unColumn, bool *pVal ) +{ + int32 b; + if ( !BGetIntValue( unColumn, &b ) ) + return false; + + // convert to boolean + *pVal = ( b != 0 ); + + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets int16 data for a field in this record +// Input: unColumn - field to get +// pnVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetInt16Value( uint32 unColumn, int16 *pnVal ) +{ + Assert( pnVal ); + *pnVal = 0; + + uint8 *pubData = NULL; + int cubData = 0; + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + { + Assert( k_EGCSQLType_int16 == m_pResultSet->GetColumnType( unColumn ) ); + AssertMsg1( 2 == cubData, "GetValue expected 2 bytes, found %d", cubData ); + if ( 2 != cubData ) + return false; + *pnVal = *( (int16 *) pubData ); + } + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets int64 data for a field in this record +// Input: unColumn - field to get +// puVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetInt64Value( uint32 unColumn, int64 *puVal ) +{ + Assert( puVal ); + *puVal = 0; + + uint8 *pubData = NULL; + int cubData = 0; + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + { + Assert( k_EGCSQLType_int64 == m_pResultSet->GetColumnType( unColumn ) ); + AssertMsg1( 8 == cubData, "GetValue expected 8 bytes, found %d", cubData ); + if ( 8 != cubData ) + return false; + *puVal = *( (int64 *) pubData ); + } + + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets uint64 data for a field in this record +// Input: unColumn - field to get +// puVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetUint64Value( uint32 unColumn, uint64 *puVal ) +{ + Assert( puVal ); + *puVal = 0; + + uint8 *pubData = NULL; + int cubData = 0; + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + { + Assert( k_EGCSQLType_int64 == m_pResultSet->GetColumnType( unColumn ) ); + AssertMsg1( 8 == cubData, "GetValue expected 8 bytes, found %d", cubData ); + if ( 8 != cubData ) + return false; + *puVal = *( (uint64 *) pubData ); + } + + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets uint32 data for a field in this record +// Input: unColumn - field to get +// puVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetUint32Value( uint32 unColumn, uint32 *puVal ) +{ + Assert( puVal ); + *puVal = 0; + + uint8 *pubData = NULL; + int cubData = 0; + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + { + Assert( k_EGCSQLType_int32 == m_pResultSet->GetColumnType( unColumn ) ); + AssertMsg1( 4 == cubData, "GetValue expected 4 bytes, found %d", cubData ); + if ( 4 != cubData ) + return false; + *puVal = *( (uint32 *) pubData ); + } + + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets uint16 data for a field in this record +// Input: unColumn - field to get +// puVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetUint16Value( uint32 unColumn, uint16 *puVal ) +{ + Assert( puVal ); + *puVal = 0; + + uint8 *pubData = NULL; + int cubData = 0; + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + { + Assert( k_EGCSQLType_int16 == m_pResultSet->GetColumnType( unColumn ) ); + AssertMsg1( 2 == cubData, "GetValue expected 2 bytes, found %d", cubData ); + if ( 2 != cubData ) + return false; + *puVal = *( (uint16 *) pubData ); + } + + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets uint8 data for a field in this record +// Input: unColumn - field to get +// puVal - pointer to fill in with data +// Output: true if successful, false if data not present or not of correct type +//----------------------------------------------------------------------------- +bool CSQLRecord::BGetUint8Value( uint32 unColumn, uint8 *puVal ) +{ + Assert( puVal ); + *puVal = 0; + + uint8 *pubData = NULL; + int cubData = 0; + bool bRet = BGetColumnData( unColumn, &pubData, &cubData ); + if ( bRet ) + { + Assert( k_EGCSQLType_int8 == m_pResultSet->GetColumnType( unColumn ) ); + AssertMsg1( 1 == cubData, "GetValue expected 1 byte, found %d", cubData ); + if ( 1 != cubData ) + return false; + *puVal = *( (uint8 *) pubData ); + } + + return bRet; +} + + +//----------------------------------------------------------------------------- +// Purpose: Validates column index +// Input: unColumn - field to validate +// Output: true if valid, false otherwise +//----------------------------------------------------------------------------- +bool CSQLRecord::BValidateColumnIndex( uint32 unColumn ) +{ + if ( unColumn >= m_pResultSet->GetColumnCount() ) + { + AssertMsg2( false, "CSQLRecord::BValidateColumnIndex: invalid column index %d. # columns: %d", unColumn, + m_pResultSet->GetColumnCount() ); + return false; + } + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Advances the CSQLRecord to the next row +// Output: returns false if this call would advance the record past the last row. +// And makes the record invalid. +//----------------------------------------------------------------------------- +bool CSQLRecord::NextRow() +{ + Assert( m_pResultSet ); + m_unRow++; + + if( m_unRow >= m_pResultSet->GetRowCount() ) + m_pResultSet = NULL; + return IsValid(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Render a field to a buffer +// Input: unColumn - field to render +// cchBuffer - size of render buffer +// pchBuffer - buffer to render into +//----------------------------------------------------------------------------- +void CSQLRecord::RenderField( uint32 unColumn, int cchBuffer, char *pchBuffer ) +{ + Q_strncpy( pchBuffer, "", cchBuffer ); + + uint8 *pubData; + int cubData; + if ( !BGetColumnData( unColumn, &pubData, &cubData ) ) + return; + + // Get the column info and figure out how to interpret the data + ConvertFieldToText( m_pResultSet->GetColumnType( unColumn ), pubData, cubData, pchBuffer, cchBuffer, false ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Copies a CSQLRecord to CRecordBase +//----------------------------------------------------------------------------- +bool CSQLRecord::BWriteToRecord( CRecordBase *pRecord, const CColumnSet & csWriteFields ) +{ + bool bSuccess = true; + FOR_EACH_COLUMN_IN_SET( csWriteFields, unSQLColumn ) + { + uint32 unRecordColumn = csWriteFields.GetColumn( unSQLColumn ); + + uint8 *pubData; + size_t cubData; + if( !BGetColumnData( unSQLColumn, &pubData, &cubData ) ) + { + bSuccess = false; + } + else + { + bSuccess = pRecord->BSetField( unRecordColumn, pubData, cubData ) && bSuccess; + } + + } + return bSuccess; +} + + + +} // namespace GCSDK diff --git a/gcsdk/sqlaccess/sqlutil.cpp b/gcsdk/sqlaccess/sqlutil.cpp new file mode 100644 index 0000000..6744d21 --- /dev/null +++ b/gcsdk/sqlaccess/sqlutil.cpp @@ -0,0 +1,918 @@ +//========= 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 |