qcubed/framework

View on GitHub
includes/codegen/QDatabaseCodeGen.class.php

Summary

Maintainability
F
2 wks
Test Coverage
<?php
    require(__QCUBED_CORE__ . '/codegen/QSqlColumn.class.php');
    require(__QCUBED_CORE__ . '/codegen/QIndex.class.php');
    require(__QCUBED_CORE__ . '/codegen/QManyToManyReference.class.php');
    require(__QCUBED_CORE__ . '/codegen/QReference.class.php');
    require(__QCUBED_CORE__ . '/codegen/QReverseReference.class.php');
    require(__QCUBED_CORE__ . '/codegen/QSqlTable.class.php');
    require(__QCUBED_CORE__ . '/codegen/QTypeTable.class.php');
    require(__QCUBED_CORE__ . '/codegen/QModelConnectorOptions.class.php');

    /**
     * @package Codegen
     */
    class QDatabaseCodeGen extends QCodeGen {
        public $objSettingsXml;    // Make public so templates can use it directly.

        // Objects
        /** @var array|QSqlTable[] Array of tables in the database */
        protected $objTableArray;
        protected $strExcludedTableArray;
        protected $objTypeTableArray;
        protected $strAssociationTableNameArray;
        /** @var QDatabaseBase The database we are dealing with */
        protected $objDb;

        protected $intDatabaseIndex;
        /** @var string The delimiter to be used for parsing comments on the DB tables for being used as the name of ModelConnector's Label */
        protected $strCommentConnectorLabelDelimiter;

        // Table Suffixes
        protected $strTypeTableSuffixArray;
        protected $intTypeTableSuffixLengthArray;
        protected $strAssociationTableSuffix;
        protected $intAssociationTableSuffixLength;

        // Table Prefix
        protected $strStripTablePrefix;
        protected $intStripTablePrefixLength;

        // Exclude Patterns & Lists
        protected $strExcludePattern;
        protected $strExcludeListArray;

        // Include Patterns & Lists
        protected $strIncludePattern;
        protected $strIncludeListArray;

        // Uniquely Associated Objects
        protected $strAssociatedObjectPrefix;
        protected $strAssociatedObjectSuffix;

        // Manual Query (e.g. "Beta 2 Query") Suppor
        protected $blnManualQuerySupport = false;

        // Relationship Scripts
        protected $strRelationships;
        protected $blnRelationshipsIgnoreCase;

        protected $strRelationshipsScriptPath;
        protected $strRelationshipsScriptFormat;
        protected $blnRelationshipsScriptIgnoreCase;

        protected $strRelationshipLinesQcubed = array();
        protected $strRelationshipLinesSql = array();

        // Type Table Items, Table Name and Column Name RegExp Patterns
        protected $strPatternTableName = '[[:alpha:]_][[:alnum:]_]*';
        protected $strPatternColumnName = '[[:alpha:]_][[:alnum:]_]*';
        protected $strPatternKeyName = '[[:alpha:]_][[:alnum:]_]*';

        protected $blnGenerateControlId;
        protected $objModelConnectorOptions;
        protected $blnAutoInitialize;
        protected $blnPrivateColumnVars;

        /**
         * @param $strTableName
         * @return QSqlTable|QTypeTable
         * @throws QCallerException
         */
        public function GetTable($strTableName) {
            $strTableName = strtolower($strTableName);
            if (array_key_exists($strTableName, $this->objTableArray))
                return $this->objTableArray[$strTableName];
            if (array_key_exists($strTableName, $this->objTypeTableArray)) 
                return $this->objTypeTableArray[$strTableName];;    // deal with table special
            throw new QCallerException(sprintf('Table does not exist or could not be processed: %s. %s', $strTableName, $this->strErrors));
        }

        public function GetColumn($strTableName, $strColumnName) {
            try {
                $objTable = $this->GetTable($strTableName);
            } catch (QCallerException $objExc) {
                $objExc->IncrementOffset();
                throw $objExc;
            }
            $strColumnName = strtolower($strColumnName);
            if (array_key_exists($strColumnName, $objTable->ColumnArray))
                return $objTable->ColumnArray[$strColumnName];
            throw new QCallerException(sprintf('Column does not exist in %s: %s', $strTableName, $strColumnName));
        }

        /**
         * Given a CASE INSENSITIVE table and column name, it will return TRUE if the Table/Column
         * exists ANYWHERE in the already analyzed database
         *
         * @param string $strTableName
         * @param string $strColumnName
         * @return boolean true if it is found/validated
         */
        public function ValidateTableColumn($strTableName, $strColumnName) {
            $strTableName = trim(strtolower($strTableName));
            $strColumnName = trim(strtolower($strColumnName));

            if (array_key_exists($strTableName, $this->objTableArray))
                $strTableName = $this->objTableArray[$strTableName]->Name;
            else if (array_key_exists($strTableName, $this->objTypeTableArray))
                $strTableName = $this->objTypeTableArray[$strTableName]->Name;
            else if (array_key_exists($strTableName, $this->strAssociationTableNameArray))
                $strTableName = $this->strAssociationTableNameArray[$strTableName];
            else
                return false;

            $objFieldArray = $this->objDb->GetFieldsForTable($strTableName);

            foreach ($objFieldArray as $objField) {
                if (trim(strtolower($objField->Name)) == $strColumnName)
                    return true;
            }

            return false;
        }

        public function GetTitle() {
            if (!QApplication::$Database) {
                return '';
            }
            
            if (array_key_exists($this->intDatabaseIndex, QApplication::$Database)) {
                $objDatabase = QApplication::$Database[$this->intDatabaseIndex];
                return sprintf('Database Index #%s (%s / %s / %s)', $this->intDatabaseIndex, $objDatabase->Adapter, $objDatabase->Server, $objDatabase->Database);
            } else
                return sprintf('Database Index #%s (N/A)', $this->intDatabaseIndex);
        }

        public function GetConfigXml() {
            $strCrLf = "\r\n";
            $strToReturn = sprintf('        <database index="%s">%s', $this->intDatabaseIndex, $strCrLf);
            $strToReturn .= sprintf('            <className prefix="%s" suffix="%s"/>%s', $this->strClassPrefix, $this->strClassSuffix, $strCrLf);
            $strToReturn .= sprintf('            <associatedObjectName prefix="%s" suffix="%s"/>%s', $this->strAssociatedObjectPrefix, $this->strAssociatedObjectSuffix, $strCrLf);
            $strToReturn .= sprintf('            <typeTableIdentifier suffix="%s"/>%s', implode(',', $this->strTypeTableSuffixArray), $strCrLf);
            $strToReturn .= sprintf('            <associationTableIdentifier suffix="%s"/>%s', $this->strAssociationTableSuffix, $strCrLf);
            $strToReturn .= sprintf('            <stripFromTableName prefix="%s"/>%s', $this->strStripTablePrefix, $strCrLf);
            $strToReturn .= sprintf('            <excludeTables pattern="%s" list="%s"/>%s', $this->strExcludePattern, implode(',', $this->strExcludeListArray), $strCrLf);
            $strToReturn .= sprintf('            <includeTables pattern="%s" list="%s"/>%s', $this->strIncludePattern, implode(',', $this->strIncludeListArray), $strCrLf);
            $strToReturn .= sprintf('            <manualQuery support="%s"/>%s', ($this->blnManualQuerySupport) ? 'true' : 'false', $strCrLf);
            $strToReturn .= sprintf('            <relationships>%s', $strCrLf);
            if ($this->strRelationships)
                $strToReturn .= sprintf('            %s%s', $this->strRelationships, $strCrLf);
            $strToReturn .= sprintf('            </relationships>%s', $strCrLf);
            $strToReturn .= sprintf('            <relationshipsScript filepath="%s" format="%s"/>%s', $this->strRelationshipsScriptPath, $this->strRelationshipsScriptFormat, $strCrLf);
            $strToReturn .= sprintf('        </database>%s', $strCrLf);
            return $strToReturn;
        }

        public function GetReportLabel() {
            // Setup Report Label
            $intTotalTableCount = count($this->objTableArray) + count($this->objTypeTableArray);
            if ($intTotalTableCount == 0)
                $strReportLabel = 'There were no tables available to attempt code generation.';
            else if ($intTotalTableCount == 1)
                $strReportLabel = 'There was 1 table available to attempt code generation:';
            else
                $strReportLabel = 'There were ' . $intTotalTableCount . ' tables available to attempt code generation:';

            return $strReportLabel;
        }

        public function GenerateAll() {
            $strReport = '';

            // Iterate through all the tables, generating one class at a time
            if ($this->objTableArray) foreach ($this->objTableArray as $objTable) {
                if ($this->GenerateTable($objTable)) {
                    $intCount = $objTable->ReferenceCount;
                    if ($intCount == 0)
                        $strCount = '(with no relationships)';
                    else if ($intCount == 1)
                        $strCount = '(with 1 relationship)';
                    else
                        $strCount = sprintf('(with %s relationships)', $intCount);
                    $strReport .= sprintf("Successfully generated DB ORM Class:   %s %s\r\n", $objTable->ClassName, $strCount);
                } else
                    $strReport .= sprintf("FAILED to generate DB ORM Class:       %s\r\n", $objTable->ClassName);
            }

            // Iterate through all the TYPE tables, generating one TYPE class at a time
            if ($this->objTypeTableArray) foreach ($this->objTypeTableArray as $objTypeTable) {
                if ($this->GenerateTypeTable($objTypeTable))
                    $strReport .= sprintf("Successfully generated DB Type Class:  %s\n", $objTypeTable->ClassName);
                else
                    $strReport .= sprintf("FAILED to generate DB Type class:      %s\n", $objTypeTable->ClassName);
            }

            return $strReport;
        }

        public static function GenerateAggregateHelper($objCodeGenArray) {
            $strToReturn = array();

            if (count($objCodeGenArray)) {
                // Standard ORM Tables
                $objTableArray = array();
                foreach ($objCodeGenArray as $objCodeGen) {
                    $objCurrentTableArray = $objCodeGen->TableArray;
                    foreach ($objCurrentTableArray as $objTable)
                        $objTableArray[$objTable->ClassName] = $objTable;
                }

                $mixArgumentArray = array('objTableArray' => $objTableArray);
                if ($objCodeGenArray[0]->GenerateFiles('aggregate_db_orm', $mixArgumentArray))
                    $strToReturn[] = 'Successfully generated Aggregate DB ORM file(s)';
                else
                    $strToReturn[] = 'FAILED to generate Aggregate DB ORM file(s)';

                // Type Tables
                $objTableArray = array();
                foreach ($objCodeGenArray as $objCodeGen) {
                    $objCurrentTableArray = $objCodeGen->TypeTableArray;
                    foreach ($objCurrentTableArray as $objTable)
                        $objTableArray[$objTable->ClassName] = $objTable;
                }

                $mixArgumentArray = array('objTableArray' => $objTableArray);
                if ($objCodeGenArray[0]->GenerateFiles('aggregate_db_type', $mixArgumentArray))
                    $strToReturn[] = 'Successfully generated Aggregate DB Type file(s)';
                else
                    $strToReturn[] = 'FAILED to generate Aggregate DB Type file(s)';
            }

            return $strToReturn;
        }

        public function __construct($objSettingsXml) {
            parent::__construct($objSettingsXml);
            // Make settings file accessible to templates
            //$this->objSettingsXml = $objSettingsXml;

            // Setup Local Arrays
            $this->strAssociationTableNameArray = array();
            $this->objTableArray = array();
            $this->objTypeTableArray = array();
            $this->strExcludedTableArray = array();

            // Set the DatabaseIndex
            $this->intDatabaseIndex = QCodeGen::LookupSetting($objSettingsXml, null, 'index', QType::Integer);

            // Append Suffix/Prefixes
            $this->strClassPrefix = QCodeGen::LookupSetting($objSettingsXml, 'className', 'prefix');
            $this->strClassSuffix = QCodeGen::LookupSetting($objSettingsXml, 'className', 'suffix');
            $this->strAssociatedObjectPrefix = QCodeGen::LookupSetting($objSettingsXml, 'associatedObjectName', 'prefix');
            $this->strAssociatedObjectSuffix = QCodeGen::LookupSetting($objSettingsXml, 'associatedObjectName', 'suffix');

            // Table Type Identifiers
            $strTypeTableSuffixList = QCodeGen::LookupSetting($objSettingsXml, 'typeTableIdentifier', 'suffix');
            $strTypeTableSuffixArray = explode(',', $strTypeTableSuffixList);
            foreach ($strTypeTableSuffixArray as $strTypeTableSuffix) {
                $this->strTypeTableSuffixArray[] = trim($strTypeTableSuffix);
                $this->intTypeTableSuffixLengthArray[] = strlen(trim($strTypeTableSuffix));
            }
            $this->strAssociationTableSuffix = QCodeGen::LookupSetting($objSettingsXml, 'associationTableIdentifier', 'suffix');
            $this->intAssociationTableSuffixLength = strlen($this->strAssociationTableSuffix);

            // Stripping TablePrefixes
            $this->strStripTablePrefix = QCodeGen::LookupSetting($objSettingsXml, 'stripFromTableName', 'prefix');
            $this->intStripTablePrefixLength = strlen($this->strStripTablePrefix);

            // Exclude/Include Tables
            $this->strExcludePattern = QCodeGen::LookupSetting($objSettingsXml, 'excludeTables', 'pattern');
            $strExcludeList = QCodeGen::LookupSetting($objSettingsXml, 'excludeTables', 'list');
            $this->strExcludeListArray = explode(',',$strExcludeList);
            array_walk($this->strExcludeListArray, 'array_trim');

            // Include Patterns
            $this->strIncludePattern = QCodeGen::LookupSetting($objSettingsXml, 'includeTables', 'pattern');
            $strIncludeList = QCodeGen::LookupSetting($objSettingsXml, 'includeTables', 'list');
            $this->strIncludeListArray = explode(',',$strIncludeList);
            array_walk($this->strIncludeListArray, 'array_trim');

            // ManualQuery Support
            $this->blnManualQuerySupport = QCodeGen::LookupSetting($objSettingsXml, 'manualQuery', 'support', QType::Boolean);

            // Relationship Scripts
            $this->strRelationships = QCodeGen::LookupSetting($objSettingsXml, 'relationships');
            $this->strRelationshipsScriptPath = QCodeGen::LookupSetting($objSettingsXml, 'relationshipsScript', 'filepath');
            $this->strRelationshipsScriptFormat = QCodeGen::LookupSetting($objSettingsXml, 'relationshipsScript', 'format');

            // Column Comment for ModelConnectorLabel setting.
            $this->strCommentConnectorLabelDelimiter = QCodeGen::LookupSetting($objSettingsXml, 'columnCommentForModelConnector', 'delimiter');

            // Check to make sure things that are required are there
            if (!$this->intDatabaseIndex)
                $this->strErrors .= "CodeGen Settings XML Fatal Error: databaseIndex was invalid or not set\r\n";

            // Aggregate RelationshipLinesQcubed and RelationshipLinesSql arrays
            if ($this->strRelationships) {
                $strLines = explode("\n", strtolower($this->strRelationships));
                if ($strLines) foreach ($strLines as $strLine) {
                    $strLine = trim($strLine);

                    if (($strLine) &&
                        (strlen($strLine) > 2) &&
                        (substr($strLine, 0, 2) != '//') &&
                        (substr($strLine, 0, 2) != '--') &&
                        (substr($strLine, 0, 1) != '#')) {
                        $this->strRelationshipLinesQcubed[$strLine] = $strLine;
                    }
                }
            }

            if ($this->strRelationshipsScriptPath) {
                if (!file_exists($this->strRelationshipsScriptPath))
                    $this->strErrors .= sprintf("CodeGen Settings XML Fatal Error: relationshipsScript filepath \"%s\" does not exist\r\n", $this->strRelationshipsScriptPath);
                else {
                    $strScript = strtolower(trim(file_get_contents($this->strRelationshipsScriptPath)));
                    switch (strtolower($this->strRelationshipsScriptFormat)) {
                        case 'qcubed':
                            $strLines = explode("\n", $strScript);
                            if ($strLines) foreach ($strLines as $strLine) {
                                $strLine = trim($strLine);

                                if (($strLine) &&
                                    (strlen($strLine) > 2) &&
                                    (substr($strLine, 0, 2) != '//') &&
                                    (substr($strLine, 0, 2) != '--') &&
                                    (substr($strLine, 0, 1) != '#')) {
                                    $this->strRelationshipLinesQcubed[$strLine] = $strLine;
                                }
                            }
                            break;

                        case 'sql':
                            // Separate all commands in the script (separated by ";")
                            $strCommands = explode(';', $strScript);
                            if ($strCommands) foreach ($strCommands as $strCommand) {
                                $strCommand = trim($strCommand);

                                if ($strCommand) {
                                    // Take out all comment lines in the script
                                    $strLines = explode("\n", $strCommand);
                                    $strCommand = '';
                                    foreach ($strLines as $strLine) {
                                        $strLine = trim($strLine);
                                        if (($strLine) &&
                                            (substr($strLine, 0, 2) != '//') &&
                                            (substr($strLine, 0, 2) != '--') &&
                                            (substr($strLine, 0, 1) != '#')) {
                                            $strLine = str_replace('    ', ' ', $strLine);
                                            $strLine = str_replace('        ', ' ', $strLine);
                                            $strLine = str_replace('       ', ' ', $strLine);
                                            $strLine = str_replace('      ', ' ', $strLine);
                                            $strLine = str_replace('     ', ' ', $strLine);
                                            $strLine = str_replace('    ', ' ', $strLine);
                                            $strLine = str_replace('   ', ' ', $strLine);
                                            $strLine = str_replace('  ', ' ', $strLine);
                                            $strLine = str_replace('  ', ' ', $strLine);
                                            $strLine = str_replace('  ', ' ', $strLine);
                                            $strLine = str_replace('  ', ' ', $strLine);
                                            $strLine = str_replace('  ', ' ', $strLine);

                                            $strCommand .= $strLine . ' ';
                                        }
                                    }

                                    $strCommand = trim($strCommand);
                                    if ((strpos($strCommand, 'alter table') === 0) &&
                                        (strpos($strCommand, 'foreign key') !== false))
                                        $this->strRelationshipLinesSql[$strCommand] = $strCommand;
                                }
                            }
                            break;

                        default:
                            $this->strErrors .= sprintf("CodeGen Settings XML Fatal Error: relationshipsScript format \"%s\" is invalid (must be either \"qcubed\" or \"sql\")\r\n", $this->strRelationshipsScriptFormat);
                            break;
                    }
                }
            }

            $this->blnGenerateControlId = QCodeGen::LookupSetting($objSettingsXml, 'generateControlId', 'support', QType::Boolean);
            $this->objModelConnectorOptions = new QModelConnectorOptions();

            $this->blnAutoInitialize = QCodeGen::LookupSetting($objSettingsXml, 'createOptions', 'autoInitialize', QType::Boolean);
            $this->blnPrivateColumnVars = QCodeGen::LookupSetting($objSettingsXml, 'createOptions', 'privateColumnVars', QType::Boolean);

            if ($this->strErrors)
                return;

            $this->AnalyzeDatabase();
        }

        protected function AnalyzeDatabase() {
            if (!QApplication::$Database) {
                $this->strErrors = 'FATAL ERROR: No databases are listed in the configuration file.';
                return;
            }
            
            // Set aside the Database object
            if (array_key_exists($this->intDatabaseIndex, QApplication::$Database))
                $this->objDb = QApplication::$Database[$this->intDatabaseIndex];

            // Ensure the DB Exists
            if (!$this->objDb) {
                $this->strErrors = 'FATAL ERROR: No database configured at index ' . $this->intDatabaseIndex . '. Check your configuration file.';
                return;
            }

            // Ensure DB Profiling is DISABLED on this DB
            if ($this->objDb->EnableProfiling) {
                $this->strErrors = 'FATAL ERROR: Code generator cannot analyze the database at index ' . $this->intDatabaseIndex . ' while DB Profiling is enabled.';
                return;
            }

            // Get the list of Tables as a string[]
            $strTableArray = $this->objDb->GetTables();


            // ITERATION 1: Simply create the Table and TypeTable Arrays
            if ($strTableArray) {
                foreach ($strTableArray as $strTableName) {

                    // Do we Exclude this Table Name? (given includeTables and excludeTables)
                    // First check the lists of Excludes and the Exclude Patterns
                    if (in_array($strTableName, $this->strExcludeListArray) ||
                        (strlen($this->strExcludePattern) > 0 && preg_match(":" . $this->strExcludePattern . ":i", $strTableName))
                    ) {

                        // So we THINK we may be excluding this table
                        // But check against the explicit INCLUDE list and patterns
                        if (in_array($strTableName, $this->strIncludeListArray) ||
                            (strlen($this->strIncludePattern) > 0 && preg_match(":" . $this->strIncludePattern . ":i", $strTableName))
                        ) {
                            // If we're here, we explicitly want to include this table
                            // Therefore, do nothing
                        } else {
                            // If we're here, then we want to exclude this table
                            $this->strExcludedTableArray[strtolower($strTableName)] = true;

                            // Exit this iteration of the foreach loop
                            continue;
                        }
                    }

                    // Check to see if this table name exists anywhere else yet, and warn if it is
                    foreach (QCodeGen::$CodeGenArray as $objCodeGen) {
                        if ($objCodeGen instanceof QDatabaseCodeGen) {
                            foreach ($objCodeGen->objTableArray as $objPossibleDuplicate)
                                if (strtolower($objPossibleDuplicate->Name) == strtolower($strTableName)) {
                                    $this->strErrors .= 'Duplicate Table Name Used: ' . $strTableName . "\r\n";
                                }
                        }
                    }

                    // Perform different tasks based on whether it's an Association table,
                    // a Type table, or just a regular table
                    $blnIsTypeTable = false;
                    foreach ($this->intTypeTableSuffixLengthArray as $intIndex => $intTypeTableSuffixLength) {
                        if (($intTypeTableSuffixLength) &&
                            (strlen($strTableName) > $intTypeTableSuffixLength) &&
                            (substr($strTableName, strlen($strTableName) - $intTypeTableSuffixLength) == $this->strTypeTableSuffixArray[$intIndex])
                        ) {
                            // Let's mark, that we have type table
                            $blnIsTypeTable = true;
                            // Create a TYPE Table and add it to the array
                            $objTypeTable = new QTypeTable($strTableName);
                            $this->objTypeTableArray[strtolower($strTableName)] = $objTypeTable;
                            // If we found type table, there is no point of iterating for other type table suffixes
                            break;
//                        _p("TYPE Table: $strTableName<br />", false);
                        }
                    }
                    if (!$blnIsTypeTable) {
                        // If current table wasn't type table, let's look for other table types
                        if (($this->intAssociationTableSuffixLength) &&
                            (strlen($strTableName) > $this->intAssociationTableSuffixLength) &&
                            (substr($strTableName, strlen($strTableName) - $this->intAssociationTableSuffixLength) == $this->strAssociationTableSuffix)
                        ) {
                            // Add this ASSOCIATION Table Name to the array
                            $this->strAssociationTableNameArray[strtolower($strTableName)] = $strTableName;
//                        _p("ASSN Table: $strTableName<br />", false);

                        } else {
                            // Create a Regular Table and add it to the array
                            $objTable = new QSqlTable($strTableName);
                            $this->objTableArray[strtolower($strTableName)] = $objTable;
//                        _p("Table: $strTableName<br />", false);
                        }
                    }
                }
            }


            // Analyze All the Type Tables
            if ($this->objTypeTableArray) foreach ($this->objTypeTableArray as $objTypeTable)
                $this->AnalyzeTypeTable($objTypeTable);

            // Analyze All the Regular Tables
            if ($this->objTableArray) foreach ($this->objTableArray as $objTable)
                $this->AnalyzeTable($objTable);

            // Analyze All the Association Tables
            if ($this->strAssociationTableNameArray) foreach ($this->strAssociationTableNameArray as $strAssociationTableName)
                $this->AnalyzeAssociationTable($strAssociationTableName);

            // Finally, for each Relationship in all Tables, Warn on Non Single Column PK based FK:
            if ($this->objTableArray) foreach ($this->objTableArray as $objTable)
                if ($objTable->ColumnArray) foreach ($objTable->ColumnArray as $objColumn)
                    if ($objColumn->Reference && !$objColumn->Reference->IsType) {
                        $objReference = $objColumn->Reference;
//                        $objReferencedTable = $this->objTableArray[strtolower($objReference->Table)];
                        $objReferencedTable = $this->GetTable($objReference->Table);
                        $objReferencedColumn = $objReferencedTable->ColumnArray[strtolower($objReference->Column)];


                        if (!$objReferencedColumn->PrimaryKey) {
                            $this->strErrors .= sprintf("Warning: Invalid Relationship created in %s class (for foreign key \"%s\") -- column \"%s\" is not the single-column primary key for the referenced \"%s\" table\r\n",
                                $objReferencedTable->ClassName, $objReference->KeyName, $objReferencedColumn->Name, $objReferencedTable->Name);
                        } else if (count($objReferencedTable->PrimaryKeyColumnArray) != 1) {
                            $this->strErrors .= sprintf("Warning: Invalid Relationship created in %s class (for foreign key \"%s\") -- column \"%s\" is not the single-column primary key for the referenced \"%s\" table\r\n",
                                $objReferencedTable->ClassName, $objReference->KeyName, $objReferencedColumn->Name, $objReferencedTable->Name);
                        }
                    }
        }

        protected function ListOfColumnsFromTable(QSqlTable $objTable) {
            $strArray = array();
            $objColumnArray = $objTable->ColumnArray;
            if ($objColumnArray) foreach ($objColumnArray as $objColumn)
                array_push($strArray, $objColumn->Name);
            return implode(', ', $strArray);
        }

        protected function GetColumnArray(QSqlTable $objTable, $strColumnNameArray) {
            $objToReturn = array();

            if ($strColumnNameArray) foreach ($strColumnNameArray as $strColumnName) {
                array_push($objToReturn, $objTable->ColumnArray[strtolower($strColumnName)]);
            }

            return $objToReturn;
        }

        public function GenerateTable(QSqlTable $objTable) {
            // Create Argument Array
            $mixArgumentArray = array('objTable' => $objTable);
            return $this->GenerateFiles('db_orm', $mixArgumentArray);
        }

        public function GenerateTypeTable(QTypeTable $objTypeTable) {
            // Create Argument Array
            $mixArgumentArray = array('objTypeTable' => $objTypeTable);
            return $this->GenerateFiles('db_type', $mixArgumentArray);
        }

        protected function AnalyzeAssociationTable($strTableName) {
            $objFieldArray = $this->objDb->GetFieldsForTable($strTableName);

            // Association tables must have 2 fields
            if (count($objFieldArray) != 2) {
                $this->strErrors .= sprintf("AssociationTable %s does not have exactly 2 columns.\n",
                    $strTableName);
                return;
            }

            if ((!$objFieldArray[0]->NotNull) ||
                (!$objFieldArray[1]->NotNull)) {
                $this->strErrors .= sprintf("AssociationTable %s's two columns must both be not null",
                    $strTableName);
                return;
            }

            if (((!$objFieldArray[0]->PrimaryKey) &&
                 ($objFieldArray[1]->PrimaryKey)) ||
                (($objFieldArray[0]->PrimaryKey) &&
                 (!$objFieldArray[1]->PrimaryKey))) {
                $this->strErrors .= sprintf("AssociationTable %s only support two-column composite Primary Keys.\n",
                    $strTableName);
                return;
            }

            $objForeignKeyArray = $this->objDb->GetForeignKeysForTable($strTableName);

            // Add to it, the list of Foreign Keys from any Relationships Script
            $objForeignKeyArray = $this->GetForeignKeysFromRelationshipsScript($strTableName, $objForeignKeyArray);

            if (count($objForeignKeyArray) != 2) {
                $this->strErrors .= sprintf("AssociationTable %s does not have exactly 2 foreign keys.  Code Gen analysis found %s.\n",
                    $strTableName, count($objForeignKeyArray));
                return;
            }

            // Setup two new ManyToManyReference objects
            $objManyToManyReferenceArray[0] = new QManyToManyReference();
            $objManyToManyReferenceArray[1] = new QManyToManyReference();

            // Ensure that the linked tables are both not excluded
            if (array_key_exists($objForeignKeyArray[0]->ReferenceTableName, $this->strExcludedTableArray) ||
                array_key_exists($objForeignKeyArray[1]->ReferenceTableName, $this->strExcludedTableArray))
                return;

            // Setup GraphPrefixArray (if applicable)
            if ($objForeignKeyArray[0]->ReferenceTableName == $objForeignKeyArray[1]->ReferenceTableName) {
                // We are analyzing a graph association
                $strGraphPrefixArray = $this->CalculateGraphPrefixArray($objForeignKeyArray);
            } else {
                $strGraphPrefixArray = array('', '');
            }

            // Go through each FK and setup each ManyToManyReference object
            for ($intIndex = 0; $intIndex < 2; $intIndex++) {
                $objManyToManyReference = $objManyToManyReferenceArray[$intIndex];

                $objForeignKey = $objForeignKeyArray[$intIndex];
                $objOppositeForeignKey = $objForeignKeyArray[($intIndex == 0) ? 1 : 0];

                // Make sure the FK is a single-column FK
                if (count($objForeignKey->ColumnNameArray) != 1) {
                    $this->strErrors .= sprintf("AssoiationTable %s has multi-column foreign keys.\n",
                        $strTableName);
                    return;
                }

                $objManyToManyReference->KeyName = $objForeignKey->KeyName;
                $objManyToManyReference->Table = $strTableName;
                $objManyToManyReference->Column = $objForeignKey->ColumnNameArray[0];
                $objManyToManyReference->PropertyName = $this->ModelColumnPropertyName($objManyToManyReference->Column);
                $objManyToManyReference->OppositeColumn = $objOppositeForeignKey->ColumnNameArray[0];
                $objManyToManyReference->AssociatedTable = $objOppositeForeignKey->ReferenceTableName;

                // Calculate OppositeColumnVariableName
                // Do this by first making a fake column which is the PK column of the AssociatedTable,
                // but who's column name is ManyToManyReference->Column
//                $objOppositeColumn = clone($this->objTableArray[strtolower($objManyToManyReference->AssociatedTable)]->PrimaryKeyColumnArray[0]);

                $objTable = $this->GetTable($objManyToManyReference->AssociatedTable);
                $objOppositeColumn = clone($objTable->PrimaryKeyColumnArray[0]);
                $objOppositeColumn->Name = $objManyToManyReference->OppositeColumn;
                $objManyToManyReference->OppositeVariableName = $this->ModelColumnVariableName($objOppositeColumn);
                $objManyToManyReference->OppositePropertyName = $this->ModelColumnPropertyName($objOppositeColumn->Name);
                $objManyToManyReference->OppositeVariableType = $objOppositeColumn->VariableType;
                $objManyToManyReference->OppositeDbType = $objOppositeColumn->DbType;

                $objManyToManyReference->VariableName = $this->ModelReverseReferenceVariableName($objOppositeForeignKey->ReferenceTableName);
                $objManyToManyReference->VariableType = $this->ModelReverseReferenceVariableType($objOppositeForeignKey->ReferenceTableName);

                $objManyToManyReference->ObjectDescription = $strGraphPrefixArray[$intIndex] . $this->CalculateObjectDescriptionForAssociation($strTableName, $objForeignKey->ReferenceTableName, $objOppositeForeignKey->ReferenceTableName, false);
                $objManyToManyReference->ObjectDescriptionPlural = $strGraphPrefixArray[$intIndex] . $this->CalculateObjectDescriptionForAssociation($strTableName, $objForeignKey->ReferenceTableName, $objOppositeForeignKey->ReferenceTableName, true);

                $objManyToManyReference->OppositeObjectDescription = $strGraphPrefixArray[($intIndex == 0) ? 1 : 0] . $this->CalculateObjectDescriptionForAssociation($strTableName, $objOppositeForeignKey->ReferenceTableName, $objForeignKey->ReferenceTableName, false);
                $objManyToManyReference->IsTypeAssociation = ($objTable instanceof QTypeTable);
                $objManyToManyReference->Options = $this->objModelConnectorOptions->GetOptions($this->ModelClassName($objForeignKey->ReferenceTableName), $objManyToManyReference->ObjectDescription);

            }


            // Iterate through the list of Columns to create objColumnArray
            $objColumnArray = array();
            foreach ($objFieldArray as $objField) {
                if (($objField->Name != $objManyToManyReferenceArray[0]->Column) &&
                    ($objField->Name != $objManyToManyReferenceArray[1]->Column)) {
                    $objColumn = $this->AnalyzeTableColumn($objField, null);
                    if ($objColumn) {
                        $objColumnArray[strtolower($objColumn->Name)] = $objColumn;
                    }
                }
            }

            // Make sure lone primary key columns are marked as unique
            $objKeyColumn = null;
            foreach ($objColumnArray as $objColumn) {
                if ($objColumn->PrimaryKey) {
                    if ($objKeyColumn === null) {
                        $objKeyColumn = $objColumn;
                    }
                    else {
                        $objKeyColumn = false; // multiple key columns
                    }
                }
            }
            if ($objKeyColumn) {
                $objKeyColumn->Unique = true;
            }

            $objManyToManyReferenceArray[0]->ColumnArray = $objColumnArray;
            $objManyToManyReferenceArray[1]->ColumnArray = $objColumnArray;

            // Push the ManyToManyReference Objects to the tables
            for ($intIndex = 0; $intIndex < 2; $intIndex++) {
                $objManyToManyReference = $objManyToManyReferenceArray[$intIndex];
                $strTableWithReference = $objManyToManyReferenceArray[($intIndex == 0) ? 1 : 0]->AssociatedTable;

                $objTable = $this->GetTable($strTableWithReference);
                $objArray = $objTable->ManyToManyReferenceArray;
                array_push($objArray, $objManyToManyReference);
                $objTable->ManyToManyReferenceArray = $objArray;
            }

        }

        protected function AnalyzeTypeTable(QTypeTable $objTypeTable) {
            // Setup the Array of Reserved Words
            $strReservedWords = explode(',', QCodeGen::PhpReservedWords);
            for ($intIndex = 0; $intIndex < count($strReservedWords); $intIndex++)
                $strReservedWords[$intIndex] = strtolower(trim($strReservedWords[$intIndex]));

            // Setup the Type Table Object
            $strTableName = $objTypeTable->Name;
            $objTypeTable->ClassName = $this->ModelClassName($strTableName);

            // Ensure that there are only 2 fields, an integer PK field (can be named anything) and a unique varchar field
            $objFieldArray = $this->objDb->GetFieldsForTable($strTableName);

            if (($objFieldArray[0]->Type != QDatabaseFieldType::Integer) ||
                (!$objFieldArray[0]->PrimaryKey)) {
                $this->strErrors .= sprintf("TypeTable %s's first column is not a PK integer.\n",
                    $strTableName);
                return;
            }

            if (($objFieldArray[1]->Type != QDatabaseFieldType::VarChar) ||
                (!$objFieldArray[1]->Unique)) {
                $this->strErrors .= sprintf("TypeTable %s's second column is not a unique VARCHAR.\n",
                    $strTableName);
                return;
            }

            // Get the rows
            $objResult = $this->objDb->Query(sprintf('SELECT * FROM %s', $strTableName));
            $strNameArray = array();
            $strTokenArray = array();
            $strExtraPropertyArray = array();
            $strExtraFields = array();
            $intRowWidth = count($objFieldArray);
            while ($objDbRow = $objResult->GetNextRow()) {
                $strRowArray = $objDbRow->GetColumnNameArray();
                $id = $strRowArray[0];
                $name = $strRowArray[1];

                $strNameArray[$id] = str_replace("'", "\\'", str_replace('\\', '\\\\', $name));
                $strTokenArray[$id] = $this->TypeTokenFromTypeName($name);
                if ($intRowWidth > 2) { // there are extra columns to process
                    $strExtraPropertyArray[$id] = array();
                    for ($i = 2; $i < $intRowWidth; $i++) {
                        $strFieldName = QCodeGen::TypeColumnPropertyName($objFieldArray[$i]->Name);
                        $strExtraFields[$i - 2] = $strFieldName;

                        // Get and resolve type based value
                        $value = $objDbRow->GetColumn($objFieldArray[$i]->Name, $objFieldArray[$i]->Type);
                        $strExtraPropertyArray[$id][$strFieldName] = $value;
                    }
                }

                foreach ($strReservedWords as $strReservedWord)
                    if (trim(strtolower($strTokenArray[$id])) == $strReservedWord) {
                        $this->strErrors .= sprintf("Warning: TypeTable %s contains a type name which is a reserved word: %s.  Appended _ to the beginning of it.\r\n",
                            $strTableName, $strReservedWord);
                        $strTokenArray[$id] = '_' . $strTokenArray[$id];
                    }
                if (strlen($strTokenArray[$id]) == 0) {
                    $this->strErrors .= sprintf("Warning: TypeTable %s contains an invalid type name: %s\r\n",
                        $strTableName, stripslashes($strNameArray[$id]));
                    return;
                }
            }

            ksort($strNameArray);
            ksort($strTokenArray);

            $objTypeTable->NameArray = $strNameArray;
            $objTypeTable->TokenArray = $strTokenArray;
            $objTypeTable->ExtraFieldNamesArray = $strExtraFields;
            $objTypeTable->ExtraPropertyArray = $strExtraPropertyArray;
            $objColumn = $this->AnalyzeTableColumn ($objFieldArray[0], $objTypeTable);
            $objColumn->Unique = true;
            $objTypeTable->KeyColumn = $objColumn;
        }

        protected function AnalyzeTable(QSqlTable $objTable) {
            // Setup the Table Object
            $objTable->OwnerDbIndex = $this->intDatabaseIndex;
            $strTableName = $objTable->Name;
            $objTable->ClassName = $this->ModelClassName($strTableName);
            $objTable->ClassNamePlural = $this->Pluralize($objTable->ClassName);

            $objTable->Options = $this->objModelConnectorOptions->GetOptions($objTable->ClassName, QModelConnectorOptions::TableOptionsFieldName);

            // Get the List of Columns
            $objFieldArray = $this->objDb->GetFieldsForTable($strTableName);

            // Iterate through the list of Columns to create objColumnArray
            $objColumnArray = array();
            if ($objFieldArray) foreach ($objFieldArray as $objField) {
                $objColumn = $this->AnalyzeTableColumn($objField, $objTable);
                if ($objColumn) {
                    $objColumnArray[strtolower($objColumn->Name)] = $objColumn;
                }
            }
            $objTable->ColumnArray = $objColumnArray;

            // Make sure lone primary key columns are marked as unique
            $objKeyColumn = null;
            foreach ($objColumnArray as $objColumn) {
                if ($objColumn->PrimaryKey) {
                    if ($objKeyColumn === null) {
                        $objKeyColumn = $objColumn;
                    }
                    else {
                        $objKeyColumn = false; // multiple key columns
                    }
                }
            }
            if ($objKeyColumn) {
                $objKeyColumn->Unique = true;
            }




            // Get the List of Indexes
            $objTable->IndexArray = $this->objDb->GetIndexesForTable($objTable->Name);

            // Create an Index array
            $objIndexArray = array();
            // Create our Index for Primary Key (if applicable)
            $strPrimaryKeyArray = array();
            foreach ($objColumnArray as $objColumn)
                if ($objColumn->PrimaryKey) {
                    $objPkColumn = $objColumn;
                    array_push($strPrimaryKeyArray, $objColumn->Name);
                }
            if (count($strPrimaryKeyArray)) {
                $objIndex = new QIndex();
                $objIndex->KeyName = 'pk_' . $strTableName;
                $objIndex->PrimaryKey = true;
                $objIndex->Unique = true;
                $objIndex->ColumnNameArray = $strPrimaryKeyArray;
                array_push($objIndexArray, $objIndex);

                if (count($strPrimaryKeyArray) == 1) {
                    $objPkColumn->Unique = true;
                    $objPkColumn->Indexed = true;
                }
            }

            // Iterate though each Index that exists in this table, set any Columns's "Index" property
            // to TRUE if they are a single-column index
            if ($objTable->IndexArray) foreach ($objArray = $objTable->IndexArray as $objDatabaseIndex) {
                // Make sure the columns are defined
                if (count ($objDatabaseIndex->ColumnNameArray) == 0)
                    $this->strErrors .= sprintf("Index %s in table %s indexes on no columns.\n",
                        $objDatabaseIndex->KeyName, $strTableName);
                else {
                    // Ensure every column exist in the DbIndex's ColumnNameArray
                    $blnFailed = false;
                    foreach ($objArray = $objDatabaseIndex->ColumnNameArray as $strColumnName) {
                        if (array_key_exists(strtolower($strColumnName), $objTable->ColumnArray) &&
                            ($objTable->ColumnArray[strtolower($strColumnName)])) {
                            // It exists -- do nothing
                        } else {
                            // Otherwise, add a warning
                            $this->strErrors .= sprintf("Index %s in table %s indexes on the column %s, which does not appear to exist.\n",
                                $objDatabaseIndex->KeyName, $strTableName, $strColumnName);
                            $blnFailed = true;
                        }
                    }

                    if (!$blnFailed) {
                        // Let's make sure if this is a single-column index, we haven't already created a single-column index for this column
                        $blnAlreadyCreated = false;
                        foreach ($objIndexArray as $objIndex)
                            if (count($objIndex->ColumnNameArray) == count($objDatabaseIndex->ColumnNameArray))
                                if (implode(',', $objIndex->ColumnNameArray) == implode(',', $objDatabaseIndex->ColumnNameArray))
                                    $blnAlreadyCreated = true;

                        if (!$blnAlreadyCreated) {
                            // Create the Index Object
                            $objIndex = new QIndex();
                            $objIndex->KeyName = $objDatabaseIndex->KeyName;
                            $objIndex->PrimaryKey = $objDatabaseIndex->PrimaryKey;
                            $objIndex->Unique = $objDatabaseIndex->Unique;
                            if ($objDatabaseIndex->PrimaryKey)
                                $objIndex->Unique = true;
                            $objIndex->ColumnNameArray = $objDatabaseIndex->ColumnNameArray;

                            // Add the new index object to the index array
                            array_push($objIndexArray, $objIndex);

                            // Lastly, if it's a single-column index, update the Column in the table to reflect this
                            if (count($objDatabaseIndex->ColumnNameArray) == 1) {
                                $strColumnName = $objDatabaseIndex->ColumnNameArray[0];
                                $objColumn = $objTable->ColumnArray[strtolower($strColumnName)];
                                $objColumn->Indexed = true;

                                if ($objIndex->Unique)
                                    $objColumn->Unique = true;
                            }
                        }
                    }
                }
            }

            // Add the IndexArray to the table
            $objTable->IndexArray = $objIndexArray;




            // Get the List of Foreign Keys from the database
            $objForeignKeys = $this->objDb->GetForeignKeysForTable($objTable->Name);

            // Add to it, the list of Foreign Keys from any Relationships Script
            $objForeignKeys = $this->GetForeignKeysFromRelationshipsScript($strTableName, $objForeignKeys);

            // Iterate through each foreign key that exists in this table
            if ($objForeignKeys) foreach ($objForeignKeys as $objForeignKey) {

                // Make sure it's a single-column FK
                if (count($objForeignKey->ColumnNameArray) != 1)
                    $this->strErrors .= sprintf("Foreign Key %s in table %s keys on multiple columns.  Multiple-columned FKs are not supported by the code generator.\n",
                        $objForeignKey->KeyName, $strTableName);
                else {
                    // Make sure the column in the FK definition actually exists in this table
                    $strColumnName = $objForeignKey->ColumnNameArray[0];

                    if (array_key_exists(strtolower($strColumnName), $objTable->ColumnArray) &&
                        ($objColumn = $objTable->ColumnArray[strtolower($strColumnName)])) {

                        // Now, we make sure there is a single-column index for this FK that exists
                        $blnFound = false;
                        if ($objIndexArray = $objTable->IndexArray) foreach ($objIndexArray as $objIndex) {
                            if ((count($objIndex->ColumnNameArray) == 1) &&
                                (strtolower($objIndex->ColumnNameArray[0]) == strtolower($strColumnName)))
                                $blnFound = true;
                        }

                        if (!$blnFound) {
                            // Single Column Index for this FK does not exist.  Let's create a virtual one and warn
                            $objIndex = new QIndex();
                            $objIndex->KeyName = sprintf('virtualix_%s_%s', $objTable->Name, $objColumn->Name);
                            $objIndex->Unique = $objColumn->Unique;
                            $objIndex->ColumnNameArray = array($objColumn->Name);

                            $objIndexArray = $objTable->IndexArray;
                            $objIndexArray[] = $objIndex;
                            $objTable->IndexArray = $objIndexArray;

                            if ($objIndex->Unique)
                                $this->strWarnings .= sprintf("Notice: It is recommended that you add a single-column UNIQUE index on \"%s.%s\" for the Foreign Key %s\r\n",
                                    $strTableName, $strColumnName, $objForeignKey->KeyName);
                            else
                                $this->strWarnings .= sprintf("Notice: It is recommended that you add a single-column index on \"%s.%s\" for the Foreign Key %s\r\n",
                                    $strTableName, $strColumnName, $objForeignKey->KeyName);
                        }

                        // Make sure the table being referenced actually exists
                        if ((array_key_exists(strtolower($objForeignKey->ReferenceTableName), $this->objTableArray)) ||
                            (array_key_exists(strtolower($objForeignKey->ReferenceTableName), $this->objTypeTableArray))) {

                            // STEP 1: Create the New Reference
                            $objReference = new QReference();

                            // Retrieve the Column object
                            $objColumn = $objTable->ColumnArray[strtolower($strColumnName)];

                            // Setup Key Name
                            $objReference->KeyName = $objForeignKey->KeyName;

                            $strReferencedTableName = $objForeignKey->ReferenceTableName;

                            // Setup IsType flag
                            if (array_key_exists(strtolower($strReferencedTableName), $this->objTypeTableArray)) {
                                $objReference->IsType = true;
                            } else {
                                $objReference->IsType = false;
                            }

                            // Setup Table and Column names
                            $objReference->Table = $strReferencedTableName;
                            $objReference->Column = $objForeignKey->ReferenceColumnNameArray[0];

                            // Setup VariableType
                            $objReference->VariableType = $this->ModelClassName($strReferencedTableName);

                            // Setup PropertyName and VariableName
                            $objReference->PropertyName = $this->ModelReferencePropertyName($objColumn->Name);
                            $objReference->VariableName = $this->ModelReferenceVariableName($objColumn->Name);
                            $objReference->Name = $this->ModelReferenceColumnName($objColumn->Name);

                            // Add this reference to the column
                            $objColumn->Reference = $objReference;

                            // References will not have been correctly read earlier, so try again with the reference name
                            $objColumn->Options = $this->objModelConnectorOptions->GetOptions($objTable->ClassName, $objReference->PropertyName) + $objColumn->Options;



                            // STEP 2: Setup the REVERSE Reference for Non Type-based References
                            if (!$objReference->IsType) {
                                // Retrieve the ReferencedTable object
//                                $objReferencedTable = $this->objTableArray[strtolower($objReference->Table)];
                                $objReferencedTable = $this->GetTable($objReference->Table);
                                $objReverseReference = new QReverseReference();
                                $objReverseReference->Reference = $objReference;
                                $objReverseReference->KeyName = $objReference->KeyName;
                                $objReverseReference->Table = $strTableName;
                                $objReverseReference->Column = $strColumnName;
                                $objReverseReference->NotNull = $objColumn->NotNull;
                                $objReverseReference->Unique = $objColumn->Unique;
                                $objReverseReference->PropertyName = $this->ModelColumnPropertyName($strColumnName);

                                $objReverseReference->ObjectDescription = $this->CalculateObjectDescription($strTableName, $strColumnName, $strReferencedTableName, false);
                                $objReverseReference->ObjectDescriptionPlural = $this->CalculateObjectDescription($strTableName, $strColumnName, $strReferencedTableName, true);
                                $objReverseReference->VariableName = $this->ModelReverseReferenceVariableName($objTable->Name);
                                $objReverseReference->VariableType = $this->ModelReverseReferenceVariableType($objTable->Name);

                                // For Special Case ReverseReferences, calculate Associated MemberVariableName and PropertyName...

                                // See if ReverseReference is due to an ORM-based Class Inheritence Chain
                                if ((count($objTable->PrimaryKeyColumnArray) == 1) && ($objColumn->PrimaryKey)) {
                                    $objReverseReference->ObjectMemberVariable = QConvertNotation::PrefixFromType(QType::Object) . $objReverseReference->VariableType;
                                    $objReverseReference->ObjectPropertyName = $objReverseReference->VariableType;
                                    $objReverseReference->ObjectDescription = $objReverseReference->VariableType;
                                    $objReverseReference->ObjectDescriptionPlural = $this->Pluralize($objReverseReference->VariableType);

                                // Otherwise, see if it's just plain ol' unique
                                } else if ($objColumn->Unique) {
                                    $objReverseReference->ObjectMemberVariable = $this->CalculateObjectMemberVariable($strTableName, $strColumnName, $strReferencedTableName);
                                    $objReverseReference->ObjectPropertyName = $this->CalculateObjectPropertyName($strTableName, $strColumnName, $strReferencedTableName);
                                    // get override options for codegen
                                    $objReverseReference->Options = $this->objModelConnectorOptions->GetOptions($objReference->VariableType, $objReverseReference->ObjectDescription);
                                }

                                $objReference->ReverseReference = $objReverseReference;     // Let forward reference also see things from the other side looking back

                                // Add this ReverseReference to the referenced table's ReverseReferenceArray
                                $objArray = $objReferencedTable->ReverseReferenceArray;
                                array_push($objArray, $objReverseReference);
                                $objReferencedTable->ReverseReferenceArray = $objArray;
                            }
                        } else {
                            $this->strErrors .= sprintf("Foreign Key %s in table %s references a table %s that does not appear to exist.\n",
                                $objForeignKey->KeyName, $strTableName, $objForeignKey->ReferenceTableName);
                        }
                    } else {
                        $this->strErrors .= sprintf("Foreign Key %s in table %s indexes on a column that does not appear to exist.\n",
                            $objForeignKey->KeyName, $strTableName);
                    }
                }
            }

            // Verify: Table Name is valid (alphanumeric + "_" characters only, must not start with a number)
            // and NOT a PHP Reserved Word
            $strMatches = array();
            preg_match('/' . $this->strPatternTableName . '/', $strTableName, $strMatches);
            if (count($strMatches) && ($strMatches[0] == $strTableName) && ($strTableName != '_')) {
                // Setup Reserved Words
                $strReservedWords = explode(',', QCodeGen::PhpReservedWords);
                for ($intIndex = 0; $intIndex < count($strReservedWords); $intIndex++)
                    $strReservedWords[$intIndex] = strtolower(trim($strReservedWords[$intIndex]));

                $strTableNameToTest = trim(strtolower($strTableName));
                foreach ($strReservedWords as $strReservedWord)
                    if ($strTableNameToTest == $strReservedWord) {
                        $this->strErrors .= sprintf("Table '%s' has a table name which is a PHP reserved word.\r\n", $strTableName);
                        unset($this->objTableArray[strtolower($strTableName)]);
                        return;
                    }
            } else {
                $this->strErrors .= sprintf("Table '%s' can only contain characters that are alphanumeric or _, and must not begin with a number.\r\n", $strTableName);
                unset($this->objTableArray[strtolower($strTableName)]);
                return;
            }

            // Verify: Column Names are all valid names
            $objColumnArray = $objTable->ColumnArray;
            foreach ($objColumnArray as $objColumn) {
                $strColumnName = $objColumn->Name;
                $strMatches = array();
                preg_match('/' . $this->strPatternColumnName . '/', $strColumnName, $strMatches);
                if (count($strMatches) && ($strMatches[0] == $strColumnName) && ($strColumnName != '_')) {
                } else {
                    $this->strErrors .= sprintf("Table '%s' has an invalid column name: '%s'\r\n", $strTableName, $strColumnName);
                    unset($this->objTableArray[strtolower($strTableName)]);
                    return;
                }
            }

            // Verify: Table has at least one PK
            $blnFoundPk = false;
            $objColumnArray = $objTable->ColumnArray;
            foreach ($objColumnArray as $objColumn) {
                if ($objColumn->PrimaryKey)
                    $blnFoundPk = true;
            }
            if (!$blnFoundPk) {
                $this->strErrors .= sprintf("Table %s does not have any defined primary keys.\n", $strTableName);
                unset($this->objTableArray[strtolower($strTableName)]);
                return;
            }
        }

        protected function AnalyzeTableColumn(QDatabaseFieldBase $objField, $objTable) {
            $objColumn = new QSqlColumn();
            $objColumn->Name = $objField->Name;
            $objColumn->OwnerTable = $objTable;
            if (substr_count($objField->Name, "-")) {
                $tableName = $objTable ? " in table " . $objTable->Name : "";
                $this->strErrors .= "Invalid column name" . $tableName . ": " . $objField->Name . ". Dashes are not allowed.";
                return null;
            }

            $objColumn->DbType = $objField->Type;

            $objColumn->VariableType = $this->VariableTypeFromDbType($objColumn->DbType);
            $objColumn->VariableTypeAsConstant = QType::Constant($objColumn->VariableType);

            $objColumn->Length = $objField->MaxLength;
            $objColumn->Default = $objField->Default;

            $objColumn->PrimaryKey = $objField->PrimaryKey;
            $objColumn->NotNull = $objField->NotNull;
            $objColumn->Identity = $objField->Identity;
            $objColumn->Unique = $objField->Unique;


            $objColumn->Timestamp = $objField->Timestamp;

            $objColumn->VariableName = $this->ModelColumnVariableName($objColumn);
            $objColumn->PropertyName = $this->ModelColumnPropertyName($objColumn->Name);

            // separate overrides embedded in the comment

            // extract options embedded in the comment field
            if (($strComment = $objField->Comment) &&
                ($pos1 = strpos ($strComment, '{')) !== false &&
                ($pos2 = strrpos ($strComment, '}', $pos1))) {

                $strJson = substr ($strComment, $pos1, $pos2 - $pos1 + 1);
                $a = json_decode($strJson, true);

                if ($a) {
                    $objColumn->Options = $a;
                    $objColumn->Comment = substr ($strComment, 0, $pos1) . substr ($strComment, $pos2 + 1); // return comment without options
                    if (!empty ($a['Timestamp'])) {
                        $objColumn->Timestamp = true;    // alternate way to specify that a column is a self-updating timestamp
                    }
                    if ($objColumn->Timestamp && !empty($a['AutoUpdate'])) {
                        $objColumn->AutoUpdate = true;
                    }
                } else {
                    $objColumn->Comment = $strComment;
                }
            }

            // merge with options found in the design editor, letting editor take precedence
            $objColumn->Options = $this->objModelConnectorOptions->GetOptions($objTable->ClassName, $objColumn->PropertyName) + $objColumn->Options;

            return $objColumn;
        }

        protected function StripPrefixFromTable($strTableName) {
            // If applicable, strip any StripTablePrefix from the table name
            if ($this->intStripTablePrefixLength &&
                (strlen($strTableName) > $this->intStripTablePrefixLength) &&
                (substr($strTableName, 0, $this->intStripTablePrefixLength - strlen($strTableName)) == $this->strStripTablePrefix))
                return substr($strTableName, $this->intStripTablePrefixLength);

            return $strTableName;
        }

        protected function GetForeignKeyForQcubedRelationshipDefinition($strTableName, $strLine) {
            $strTokens = explode('=>', $strLine);
            if (count($strTokens) != 2) {
                $this->strErrors .= sprintf("Could not parse Relationships Script reference: %s (Incorrect Format)\r\n", $strLine);
                $this->strRelationshipLinesQcubed[$strLine] = null;
                return null;
            }

            $strSourceTokens = explode('.', $strTokens[0]);
            $strDestinationTokens = explode('.', $strTokens[1]);

            if ((count($strSourceTokens) != 2) ||
                (count($strDestinationTokens) != 2)) {
                $this->strErrors .= sprintf("Could not parse Relationships Script reference: %s (Incorrect Table.Column Format)\r\n", $strLine);
                $this->strRelationshipLinesQcubed[$strLine] = null;
                return null;
            }

            $strColumnName = trim($strSourceTokens[1]);
            $strReferenceTableName = trim($strDestinationTokens[0]);
            $strReferenceColumnName = trim($strDestinationTokens[1]);
            $strFkName = sprintf('virtualfk_%s_%s', $strTableName, $strColumnName);

            if (strtolower($strTableName) == trim($strSourceTokens[0])) {
                $this->strRelationshipLinesQcubed[$strLine] = null;
                return $this->GetForeignKeyHelper($strLine, $strFkName, $strTableName, $strColumnName, $strReferenceTableName, $strReferenceColumnName);
            }

            return null;
        }

        protected function GetForeignKeyForSqlRelationshipDefinition($strTableName, $strLine) {
            $strMatches = array();

            // Start
            $strPattern = '/alter[\s]+table[\s]+';
            // Table Name
            $strPattern .= '[\[\`\'\"]?(' . $this->strPatternTableName . ')[\]\`\'\"]?[\s]+';

            // Add Constraint
            $strPattern .= '(add[\s]+)?(constraint[\s]+';
            $strPattern .= '[\[\`\'\"]?(' . $this->strPatternKeyName . ')[\]\`\'\"]?[\s]+)?[\s]*';
            // Foreign Key
            $strPattern .= 'foreign[\s]+key[\s]*(' . $this->strPatternKeyName . ')[\s]*\(';
            $strPattern .= '([^)]+)\)[\s]*';
            // References
            $strPattern .= 'references[\s]+';
            $strPattern .= '[\[\`\'\"]?(' . $this->strPatternTableName . ')[\]\`\'\"]?[\s]*\(';
            $strPattern .= '([^)]+)\)[\s]*';
            // End
            $strPattern .= '/';

            // Perform the RegExp
            preg_match($strPattern, $strLine, $strMatches);

            if (count($strMatches) == 9) {
                $strColumnName = trim($strMatches[6]);
                $strReferenceTableName = trim($strMatches[7]);
                $strReferenceColumnName = trim($strMatches[8]);
                $strFkName = $strMatches[5];
                if (!$strFkName)
                    $strFkName = sprintf('virtualfk_%s_%s', $strTableName, $strColumnName);

                if ((strpos($strColumnName, ',') !== false) ||
                    (strpos($strReferenceColumnName, ',') !== false)) {
                    $this->strErrors .= sprintf("Relationships Script has a foreign key definition with multiple columns: %s (Multiple-columned FKs are not supported by the code generator)\r\n", $strLine);
                    $this->strRelationshipLinesSql[$strLine] = null;
                    return null;
                }

                // Cleanup strColumnName nad strreferenceColumnName
                $strColumnName = str_replace("'", '', $strColumnName);
                $strColumnName = str_replace('"', '', $strColumnName);
                $strColumnName = str_replace('[', '', $strColumnName);
                $strColumnName = str_replace(']', '', $strColumnName);
                $strColumnName = str_replace('`', '', $strColumnName);
                $strColumnName = str_replace('    ', '', $strColumnName);
                $strColumnName = str_replace(' ', '', $strColumnName);
                $strColumnName = str_replace("\r", '', $strColumnName);
                $strColumnName = str_replace("\n", '', $strColumnName);
                $strReferenceColumnName = str_replace("'", '', $strReferenceColumnName);
                $strReferenceColumnName = str_replace('"', '', $strReferenceColumnName);
                $strReferenceColumnName = str_replace('[', '', $strReferenceColumnName);
                $strReferenceColumnName = str_replace(']', '', $strReferenceColumnName);
                $strReferenceColumnName = str_replace('`', '', $strReferenceColumnName);
                $strReferenceColumnName = str_replace('    ', '', $strReferenceColumnName);
                $strReferenceColumnName = str_replace(' ', '', $strReferenceColumnName);
                $strReferenceColumnName = str_replace("\r", '', $strReferenceColumnName);
                $strReferenceColumnName = str_replace("\n", '', $strReferenceColumnName);

                if (strtolower($strTableName) == trim($strMatches[1])) {
                    $this->strRelationshipLinesSql[$strLine] = null;
                    return $this->GetForeignKeyHelper($strLine, $strFkName, $strTableName, $strColumnName, $strReferenceTableName, $strReferenceColumnName);
                }

                return null;
            } else {
                $this->strErrors .= sprintf("Could not parse Relationships Script reference: %s (Not in ANSI SQL Format)\r\n", $strLine);
                $this->strRelationshipLinesSql[$strLine] = null;
                return null;
            }
        }

        protected function GetForeignKeyHelper($strLine, $strFkName, $strTableName, $strColumnName, $strReferencedTable, $strReferencedColumn) {
            // Make Sure Tables/Columns Exist, or display error otherwise
            if (!$this->ValidateTableColumn($strTableName, $strColumnName)) {
                $this->strErrors .= sprintf("Could not parse Relationships Script reference: \"%s\" (\"%s.%s\" does not exist)\r\n",
                    $strLine, $strTableName, $strColumnName);
                return null;
            }

            if (!$this->ValidateTableColumn($strReferencedTable, $strReferencedColumn)) {
                $this->strErrors .= sprintf("Could not parse Relationships Script reference: \"%s\" (\"%s.%s\" does not exist)\r\n",
                    $strLine, $strReferencedTable, $strReferencedColumn);
                return null;
            }

            return new QDatabaseForeignKey($strFkName, array($strColumnName), $strReferencedTable, array($strReferencedColumn));
        }

        /**
         * This will go through the various Relationships Script lines (if applicable) as setup during
         * the __constructor() through the <relationships> and <relationshipsScript> tags in the
         * configuration settings.
         *
         * If no Relationships are defined, this method will simply exit making no changes.
         *
         * @param string $strTableName Name of the table to pull foreign keys for
         * @param DatabaseForeignKeyBase[] Array of currently found DB FK objects which will be appended to
         * @return DatabaseForeignKeyBase[] Array of DB FK objects that were parsed out
         */
        protected function GetForeignKeysFromRelationshipsScript($strTableName, $objForeignKeyArray) {
            foreach ($this->strRelationshipLinesQcubed as $strLine) {
                if ($strLine) {
                    $objForeignKey = $this->GetForeignKeyForQcubedRelationshipDefinition($strTableName, $strLine);

                    if ($objForeignKey) {
                        array_push($objForeignKeyArray, $objForeignKey);
                        $this->strRelationshipLinesQcubed[$strLine] = null;
                    }
                }
            }

            foreach ($this->strRelationshipLinesSql as $strLine) {
                if ($strLine) {
                    $objForeignKey = $this->GetForeignKeyForSqlRelationshipDefinition($strTableName, $strLine);

                    if ($objForeignKey) {
                        array_push($objForeignKeyArray, $objForeignKey);
                        $this->strRelationshipLinesSql[$strLine] = null;
                    }
                }
            }

            return $objForeignKeyArray;
        }

        public function GenerateControlId($objTable, $objColumn) {
            $strControlId = null;
            if (isset($objColumn->Options['ControlId'])) {
                $strControlId = $objColumn->Options['ControlId'];
            } elseif ($this->blnGenerateControlId) {
                $strObjectName = $this->ModelVariableName($objTable->Name);
                $strClassName = $objTable->ClassName;
                $strControlVarName = $this->ModelConnectorVariableName($objColumn);
                $strLabelName = QCodeGen::ModelConnectorControlName($objColumn);

                $strControlId = $strControlVarName . $strClassName;

            }
            return $strControlId;
        }




        /**
         * Returns a string that will cast a variable coming from the database into a php type.
         * Doing this in the template saves significant amounts of time over using QType::Cast() or GetColumn.
         * @param QSqlColumn $objColumn
         * @return string
         * @throws Exception
         */
        public function GetCastString (QSqlColumn $objColumn) {
            switch ($objColumn->DbType) {
                case QDatabaseFieldType::Bit:
                    return ('$mixVal = (bool)$mixVal;');

                case QDatabaseFieldType::Blob:
                case QDatabaseFieldType::Char:
                case QDatabaseFieldType::VarChar:
                case QDatabaseFieldType::Json:
                    return ''; // no need to cast, since its already a string or a null

                case QDatabaseFieldType::Date:
                    return ('$mixVal = new QDateTime($mixVal, null, QDateTime::DateOnlyType);');

                case QDatabaseFieldType::DateTime:
                    return ('$mixVal = new QDateTime($mixVal);');

                case QDatabaseFieldType::Time:
                    return ('$mixVal = new QDateTime($mixVal, null, QDateTime::TimeOnlyType);');

                case QDatabaseFieldType::Float:
                case QDatabaseFieldType::Integer:
                    return ('$mixVal = (' . $objColumn->VariableType . ')$mixVal;');

                default:
                    throw new Exception ('Invalid database field type');
                    exit;
            }
        }



        ////////////////////
        // Public Overriders
        ////////////////////

        /**
         * Override method to perform a property "Get"
         * This will get the value of $strName
         *
         * @param string strName Name of the property to get
         * @return mixed
         */
        public function __get($strName) {
            switch ($strName) {
                case 'TableArray':
                    return $this->objTableArray;
                case 'TypeTableArray':
                    return $this->objTypeTableArray;
                case 'DatabaseIndex':
                    return $this->intDatabaseIndex;
                case 'CommentConnectorLabelDelimiter':
                    return $this->strCommentConnectorLabelDelimiter;
                case 'AutoInitialize':
                    return $this->blnAutoInitialize;
                case 'PrivateColumnVars':
                    return $this->blnPrivateColumnVars;
                case 'objSettingsXml':
                    throw new QCallerException('The field objSettingsXml is deprecated');
                default:
                    try {
                        return parent::__get($strName);
                    } catch (QCallerException $objExc) {
                        $objExc->IncrementOffset();
                        throw $objExc;
                    }
            }
        }

        public function __set($strName, $mixValue) {
            try {
                switch($strName) {
                    default:
                        return parent::__set($strName, $mixValue);
                }
            } catch (QCallerException $objExc) {
                $objExc->IncrementOffset();
            }
        }
    }

    function array_trim(&$strValue) {
        $strValue = trim($strValue);
    }