diff --git a/code/PostgreSQLDatabase.php b/code/PostgreSQLDatabase.php
index a31411c..c235632 100644
--- a/code/PostgreSQLDatabase.php
+++ b/code/PostgreSQLDatabase.php
@@ -29,6 +29,15 @@ class PostgreSQLDatabase extends Database {
*/
private $database;
+ /*
+ * This holds the name of the original database
+ * So if you switch to another for unit tests, you
+ * can then switch back in order to drop the temp database
+ */
+ private $database_original;
+
+ private $parameters;
+
/**
* Connect to a PostgreSQL database.
* @param array $parameters An map of parameters, which should include:
@@ -39,22 +48,44 @@ class PostgreSQLDatabase extends Database {
*/
public function __construct($parameters) {
+ //We will store these connection parameters for use elsewhere (ie, unit tests)
+ $this->parameters=$parameters;
+ $this->connectDatabase();
+
+ $this->database_original=$this->database;
+ }
+
+ /*
+ * Uses whatever connection details are in the $parameters array to connect to a database of a given name
+ */
+ function connectDatabase(){
+
+ $parameters=$this->parameters;
+
+ if(!$parameters)
+ return false;
+
($parameters['username']!='') ? $username=' user=' . $parameters['username'] : $username='';
($parameters['password']!='') ? $password=' password=' . $parameters['password'] : $password='';
+
+ if(!isset($this->database))
+ $dbName=$parameters['database'];
+ else $dbName=$this->database;
+
//assumes that the server and dbname will always be provided:
- $this->dbConn = pg_connect('host=' . $parameters['server'] . ' port=5432 dbname=' . $parameters['database'] . $username . $password);
+ $this->dbConn = pg_connect('host=' . $parameters['server'] . ' port=5432 dbname=' . $dbName . $username . $password);
//By virtue of getting here, the connection is active:
$this->active=true;
- $this->database = $parameters['database'];
-
+ $this->database = $dbName;
+
if(!$this->dbConn) {
$this->databaseError("Couldn't connect to PostgreSQL database");
+ return false;
}
-
- parent::__construct();
+
+ return true;
}
-
/**
* Not implemented, needed for PDO
*/
@@ -64,14 +95,15 @@ class PostgreSQLDatabase extends Database {
/**
* Returns true if this database supports collations
+ * TODO: get rid of this?
* @return boolean
*/
public function supportsCollations() {
- return $this->getVersion() >= 4.1;
+ return true;
}
/**
- * The version of MySQL.
+ * The version of PostgreSQL.
* @var float
*/
private $pgsqlVersion;
@@ -92,7 +124,7 @@ class PostgreSQLDatabase extends Database {
}
/**
- * Get the database server, namely mysql.
+ * Get the database server, namely PostgreSQL.
* @return string
*/
public function getDatabaseServer() {
@@ -100,6 +132,7 @@ class PostgreSQLDatabase extends Database {
}
public function query($sql, $errorLevel = E_USER_ERROR) {
+
if(isset($_REQUEST['previewwrite']) && in_array(strtolower(substr($sql,0,strpos($sql,' '))), array('insert','update','delete','replace'))) {
Debug::message("Will execute: $sql");
return;
@@ -109,7 +142,6 @@ class PostgreSQLDatabase extends Database {
$starttime = microtime(true);
}
- //echo $sql . '
';
$handle = pg_query($this->dbConn, $sql);
if(isset($_REQUEST['showqueries'])) {
@@ -119,7 +151,7 @@ class PostgreSQLDatabase extends Database {
DB::$lastQuery=$handle;
- if(!$handle && $errorLevel) $this->databaseError("Couldn't run query: $sql | " . pgsql_error($this->dbConn), $errorLevel);
+ if(!$handle && $errorLevel) $this->databaseError("Couldn't run query: $sql | " . pg_last_error($this->dbConn), $errorLevel);
return new PostgreSQLQuery($this, $handle);
}
@@ -146,8 +178,16 @@ class PostgreSQLDatabase extends Database {
return $this->active ? true : false;
}
+ /*
+ * This will create a database based on whatever is in the $this->database value
+ * So you need to have called $this->selectDatabase() first, or used the __construct method
+ */
public function createDatabase() {
+
$this->query("CREATE DATABASE $this->database");
+
+ $this->connectDatabase();
+
}
/**
@@ -155,7 +195,13 @@ class PostgreSQLDatabase extends Database {
* Use with caution.
*/
public function dropDatabase() {
- $this->query("DROP DATABASE $this->database");
+
+ //First, we need to switch back to the original database so we can drop the current one
+ $db_to_drop=$this->database;
+ $this->selectDatabase($this->database_original);
+ $this->connectDatabase();
+
+ $this->query("DROP DATABASE $db_to_drop");
}
/**
@@ -170,35 +216,59 @@ class PostgreSQLDatabase extends Database {
* If the database doesn't exist, you should call createDatabase() after calling selectDatabase()
*/
public function selectDatabase($dbname) {
- $this->database = $dbname;
- if($this->databaseExists($this->database)) mysql_select_db($this->database, $this->dbConn);
+ //$this->database = $dbname;
+ //if($this->databaseExists($this->database)) mysql_select_db($this->database, $this->dbConn);
+ //if($this->databaseExists($this->database)) pg_d
+ $this->database=$dbname;
+
$this->tableList = $this->fieldList = $this->indexList = null;
+
+ return true;
}
+
/**
* Returns true if the named database exists.
*/
public function databaseExists($name) {
- //TODO: fix me to test for the existance of the database
- //For the moment, this always returns true
- //$SQL_name = Convert::raw2sql($name);
- //return $this->query("SHOW DATABASES LIKE '$SQL_name'")->value() ? true : false;
- return true;
+ $SQL_name=Convert::raw2sql($name);
+ $result=$this->query("SELECT datname FROM pg_database WHERE datname='$SQL_name';")->first();
+ return $this->query("SELECT datname FROM pg_database WHERE datname='$SQL_name';")->first() ? true : false;
}
public function createTable($tableName, $fields = null, $indexes = null, $options = null) {
$fieldSchemas = $indexSchemas = "";
if($fields) foreach($fields as $k => $v) $fieldSchemas .= "\"$k\" $v,\n";
- $addOptions = (isset($options[$this->class])) ? $options[$this->class] : null;
+ if(isset($this->class)){
+ $addOptions = (isset($options[$this->class])) ? $options[$this->class] : null;
+ } else $addOptions=null;
+ //First of all, does this table already exist
+ $doesExist=$this->TableExists($tableName);
+ if($doesExist)
+ return false;
+
//If we have a fulltext search request, then we need to create a special column
//for GiST searches
$fulltexts='';
+ $triggers='';
foreach($indexes as $name=>$this_index){
if($this_index['type']=='fulltext'){
- //For full text search, we need to create a column for the index
- $fulltexts .= "\"ts_$name\" tsvector, ";
+ //For full text search, we need to create a column for the index
+ $columns=explode(',', $this_index['value']);
+ for($i=0; $idropTrigger($triggerName, $tableName);
+ $triggers.="CREATE TRIGGER $triggerName BEFORE INSERT OR UPDATE
+ ON \"$tableName\" FOR EACH ROW EXECUTE PROCEDURE
+ tsvector_update_trigger(\"ts_$name\", 'pg_catalog.english', $columns);";
}
}
@@ -209,6 +279,10 @@ class PostgreSQLDatabase extends Database {
$fulltexts
primary key (\"ID\")
); $indexSchemas $addOptions");
+
+ if($triggers!=''){
+ $this->query($triggers);
+ }
}
/**
@@ -238,17 +312,32 @@ class PostgreSQLDatabase extends Database {
//see http://www.postgresql.org/docs/8.1/static/sql-altertable.html
$alterIndexList=Array();
if($alteredIndexes) foreach($alteredIndexes as $v) {
- if(is_array($v))
- $alterIndexList[] = 'DROP INDEX ix_' . strtolower($tableName) . '_' . strtolower($v['value']) . ';';
- else
- $alterIndexList[] = 'DROP INDEX ix_' . strtolower($tableName) . '_' . strtolower(trim($v, '()')) . ';';
-
- $k=$v['value'];
- $alterIndexList[] .= $this->getIndexSqlDefinition($tableName, $k, $v);
+ //We are only going to delete indexes which exist
+ $indexes=$this->indexList($tableName);
+
+ if(isset($indexes[$v['value']])){
+ if(is_array($v))
+ $alterIndexList[] = 'DROP INDEX ix_' . strtolower($tableName) . '_' . strtolower($v['value']) . ';';
+ else
+ $alterIndexList[] = 'DROP INDEX ix_' . strtolower($tableName) . '_' . strtolower(trim($v, '()')) . ';';
+
+ $k=$v['value'];
+ $alterIndexList[] .= $this->getIndexSqlDefinition($tableName, $k, $v);
+ }
}
//Add the new indexes:
- if($newIndexes) foreach($newIndexes as $k=>$v){
+ if($newIndexes) foreach($newIndexes as $k=>$v){
+ //Check that this index doesn't already exist:
+ $indexes=$this->indexList($tableName);
+ if(isset($indexes[trim($v, '()')])){
+ if(is_array($v)){
+ $alterIndexList[] = 'DROP INDEX ix_' . strtolower($tableName) . '_' . strtolower($v['value']) . ';';
+ } else {
+ $alterIndexList[] = 'DROP INDEX ' . $indexes[trim($v, '()')]['indexname'] . ';';
+ }
+ }
+
$alterIndexList[] = $this->getIndexSqlDefinition($tableName, $k, $v);
}
@@ -290,7 +379,9 @@ class PostgreSQLDatabase extends Database {
print_r($matches);
echo '';
}*/
-
+ if(sizeof($matches)==0)
+ return '';
+
if($matches[1]=='serial8')
return '';
@@ -384,6 +475,7 @@ class PostgreSQLDatabase extends Database {
case 'character varying':
//Check to see if there's a constraint attached to this column:
$constraint=$this->query("SELECT conname,pg_catalog.pg_get_constraintdef(r.oid, true) FROM pg_catalog.pg_constraint r WHERE r.contype = 'c' AND conname='" . $table . '_' . $field['column_name'] . "_check' ORDER BY 1;")->first();
+ $enum='';
if($constraint){
//Now we need to break this constraint text into bits so we can see what we have:
//Examples:
@@ -418,12 +510,15 @@ class PostgreSQLDatabase extends Database {
//Get the default:
//TODO: perhaps pass this to the enum function so we can
$default=trim(substr($field['column_default'], 0, strpos($field['column_default'], '::')), "'");
- //$field['data_type']="varchar(255) not null default '" . $default . "' check (\"" . $field['column_name'] . "\" in ('" . implode("', '", $constraints) . "'))";
- $field['data_type']=$this->enum(Array('default'=>$default, 'name'=>$field['column_name'], 'enums'=>$constraints));
+ $output[$field['column_name']]=$this->enum(Array('default'=>$default, 'name'=>$field['column_name'], 'enums'=>$constraints));
}
+ } else{
+ $output[$field['column_name']]='varchar(' . $field['character_maximum_length'] . ')';
}
+ break;
- $output[$field['column_name']]=$field;
+ case 'numeric':
+ $output[$field['column_name']]='numeric(' . $field['numeric_precision'] . ')';
break;
default:
$output[$field['column_name']] = $field;
@@ -457,8 +552,7 @@ class PostgreSQLDatabase extends Database {
//Here we create a db-specific version of whatever index we need to create.
switch($indexSpec['type']){
case 'fulltext':
- //$indexSpec='fulltext (' . str_replace(' ', '', $indexSpec['value']) . ')';
- $indexSpec='(ts_' . $indexSpec['indexName'] . ')';
+ $indexSpec='(ts_' . $indexSpec['name'] . ')';
break;
case 'unique':
$indexSpec='unique (' . $indexSpec['value'] . ')';
@@ -479,46 +573,65 @@ class PostgreSQLDatabase extends Database {
protected function getIndexSqlDefinition($tableName, $indexName, $indexSpec, $asDbValue=false) {
+ //TODO: create fill factor support and table partition support
+ //TODO: create clustering options
+
if(!$asDbValue){
+
+ $tableCol= 'ix_' . $tableName . '_' . $indexName;
+ if(strlen($tableCol)>64){
+ $tableCol=substr($indexName, 0, 59) . rand(1000, 9999);
+ //echo 'it is now ' . $tableCol . "\n\n";
+ }
+
if(!is_array($indexSpec)){
$indexSpec=trim($indexSpec, '()');
$bits=explode(',', $indexSpec);
$indexes="\"" . implode("\",\"", $bits) . "\"";
+
+ $indexSpec=$this->indexList($tableName);
- return 'create index ix_' . $tableName . '_' . $indexName . " ON \"" . $tableName . "\" (" . $indexes . ");";
+ return "create index $tableCol ON \"" . $tableName . "\" (" . $indexes . ");";
} else {
+
+ //Misc options first:
+ $fillfactor=$where='';;
+ if(isset($indexSpec['fillfactor']))
+ $fillfactor='WITH (FILLFACTOR = ' . $indexSpec['fillfactor'] . ')';
+ if(isset($indexSpec['where']))
+ $where='WHERE ' . $indexSpec['where'];
+
+ //TODO: create tablespace and where clause support
+
//create a type-specific index
+ //NOTE: hash should be removed. This is only here to demonstrate how other indexes can be made
switch($indexSpec['type']){
case 'fulltext':
- $spec='create index ix_' . $tableName . '_' . $indexName . " ON \"" . $tableName . "\" USING gist(\"ts_" . $indexName . "\");";
+ $spec="create index $tableCol ON \"" . $tableName . "\" USING gist(\"ts_" . $indexName . "\") $fillfactor $where";
break;
case 'unique':
- $spec='create unique index ix_' . $tableName . '_' . $indexName . " ON \"" . $tableName . "\" (\"" . $indexSpec['value'] . "\");";
+ $spec="create unique index $tableCol ON \"" . $tableName . "\" (\"" . $indexSpec['value'] . "\") $fillfactor $where";
break;
case 'btree':
- $spec='create index ix_' . $tableName . '_' . $indexName . " ON \"" . $tableName . "\" USING btree (\"" . $indexSpec['value'] . "\");";
+ $spec="create index $tableCol ON \"" . $tableName . "\" USING btree (\"" . $indexSpec['value'] . "\") $fillfactor $where";
break;
case 'hash':
- $spec='create index ix_' . $tableName . '_' . $indexName . " ON \"" . $tableName . "\" USING hash (\"" . $indexSpec['value'] . "\");";
- break;
-
- case 'rtree':
- $spec='create index ix_' . $tableName . '_' . $indexName . " ON \"" . $tableName . "\" USING rtree (\"" . $indexSpec['value'] . "\");";
+ //NOTE: this is not a recommended index type
+ $spec="create index $tableCol ON \"" . $tableName . "\" USING hash (\"" . $indexSpec['value'] . "\") $fillfactor $where";
break;
default:
- $spec='create index ix_' . $tableName . '_' . $indexName . " ON \"" . $tableName . "\" (\"" . $indexSpec['value'] . "\");";
+ $spec="create index $tableCol ON \"" . $tableName . "\" (\"" . $indexSpec['value'] . "\") $fillfactor $where";
}
- return $spec;
+ return trim($spec) . ';';
}
} else {
$indexName=trim($indexName, '()');
-
return $indexName;
}
}
@@ -559,6 +672,7 @@ class PostgreSQLDatabase extends Database {
//Retrieve a list of indexes for the specified table
$indexes=DB::query("SELECT tablename, indexname, indexdef FROM pg_indexes WHERE tablename='$table';");
+ $indexList=Array();
foreach($indexes as $index) {
//We don't actually need the entire created command, just a few bits:
$prefix='';
@@ -591,7 +705,7 @@ class PostgreSQLDatabase extends Database {
}
- return isset($indexList) ? $indexList : null;
+ return isset($indexList) ? $indexList : null;
}
@@ -602,10 +716,23 @@ class PostgreSQLDatabase extends Database {
*/
public function tableList() {
foreach($this->query("SELECT tablename FROM pg_tables WHERE tablename NOT ILIKE 'pg_%' AND tablename NOT ILIKE 'sql_%'") as $record) {
- $table = strtolower(reset($record));
+ //$table = strtolower(reset($record));
+ $table = reset($record);
$tables[$table] = $table;
}
- return isset($tables) ? $tables : null;
+
+ //Return an empty array if there's nothing in this database
+ return isset($tables) ? $tables : Array();
+ }
+
+ function TableExists($tableName){
+ $result=$this->query("SELECT tablename FROM pg_tables WHERE tablename='$tableName';")->first();
+
+ if($result)
+ return true;
+ else
+ return false;
+
}
/**
@@ -631,6 +758,18 @@ class PostgreSQLDatabase extends Database {
return $table;
}
+ /**
+ * Pass a legit trigger name and it will be dropped
+ * This assumes that the trigger has been named in a unique fashion
+ */
+ function dropTrigger($triggerName, $tableName){
+
+ $exists=DB::query("SELECT tgname FROM pg_trigger WHERE tgname='$triggerName';")->first();
+ if($exists){
+ DB::query("DROP trigger $triggerName ON \"$tableName\";");
+ }
+ }
+
/**
* Return a boolean type-formatted string
*
@@ -681,8 +820,8 @@ class PostgreSQLDatabase extends Database {
}
if($asDbValue)
- return Array('data_type'=>'numeric', 'numeric_precision'=>'9');
- else return 'decimal(' . $precision . ') not null';
+ return Array('data_type'=>'numeric', 'precision'=>'9');
+ else return 'decimal(' . $precision . ')';
}
/**
@@ -695,7 +834,7 @@ class PostgreSQLDatabase extends Database {
//Enums are a bit different. We'll be creating a varchar(255) with a constraint of all the usual enum options.
//NOTE: In this one instance, we are including the table name in the values array
- return "varchar(255) not null default '" . $values['default'] . "' check (\"" . $values['name'] . "\" in ('" . implode('\', \'', $values['enums']) . "'))";
+ return "varchar(255) default '" . $values['default'] . "' check (\"" . $values['name'] . "\" in ('" . implode('\', \'', $values['enums']) . "'))";
}
@@ -724,11 +863,10 @@ class PostgreSQLDatabase extends Database {
*/
public function int($values, $asDbValue=false){
//We'll be using an 8 digit precision to keep it in line with the serial8 datatype for ID columns
-
if($asDbValue)
- return Array('data_type'=>'numeric', 'numeric_precision'=>'8');
+ return Array('data_type'=>'numeric', 'precision'=>$values['precision']);
else
- return 'numeric(8) not null default ' . (int)$values['default'];
+ return 'numeric(11) not null default ' . (int)$values['default'];
}
/**
@@ -791,8 +929,11 @@ class PostgreSQLDatabase extends Database {
//For reference, this is what typically gets passed to this function:
//$parts=Array('datatype'=>'varchar', 'precision'=>$this->size, 'character set'=>'utf8', 'collate'=>'utf8_general_ci');
//DB::requireField($this->tableName, $this->name, "varchar($this->size) character set utf8 collate utf8_general_ci");
+ if(!isset($values['precision']))
+ $values['precision']=255;
+
if($asDbValue)
- return Array('data_type'=>'character varying', 'character_maximum_length'=>'255');
+ return Array('data_type'=>'varchar', 'precision'=>$values['precision']);
else
return 'varchar(' . $values['precision'] . ')';
}
@@ -802,7 +943,7 @@ class PostgreSQLDatabase extends Database {
*/
public function year($values, $asDbValue=false){
if($asDbValue)
- return Array('data_type'=>'numeric', 'numeric_precision'=>'4');
+ return Array('data_type'=>'numeric', 'precision'=>'4');
else return 'numeric(4)';
}
@@ -870,6 +1011,13 @@ class PostgreSQLDatabase extends Database {
return 'NOW()';
}
+ /*
+ * Returns the database-specific version of the random() function
+ */
+ function random(){
+ return 'RANDOM()';
+ }
+
/**
* Convert a SQLQuery object into a SQL statement
* @todo There is a lot of duplication between this and MySQLDatabase::sqlQueryToString(). Perhaps they could both call a common
@@ -906,7 +1054,8 @@ class PostgreSQLDatabase extends Database {
if(!empty($combinedLimit)) $this->limit = $combinedLimit;
} else {
- $text .= " LIMIT " . $sqlQuery->limit;
+ $limit=str_replace(',', ' OFFSET ', $sqlQuery->limit);
+ $text .= " LIMIT " . $limit;
}
}
@@ -933,6 +1082,95 @@ class PostgreSQLDatabase extends Database {
return str_replace('_', ',', $index);
}
+
+ /**
+ * The core search engine configuration.
+ * @todo There is no result relevancy or ordering as it currently stands.
+ *
+ * @param string $keywords Keywords as a space separated string
+ * @return object DataObjectSet of result pages
+ */
+ public function searchEngine($classesToSearch, $keywords, $start, $pageLength, $sortBy = "Relevance DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) {
+
+ $keywords = Convert::raw2sql(trim($keywords));
+ $htmlEntityKeywords = htmlentities($keywords);
+
+ /*$keywordList = explode(' ', $keywords);
+ if($keywordList) {
+ foreach($keywordList as $index => $keyword) {
+ $keywordList[$index] = "'{$keyword}'";
+ }
+ $keywords = implode(' AND ', $keywordList);
+ }*/
+
+ /*$htmlEntityKeywordList = explode(' ', $htmlEntityKeywords);
+ if($htmlEntityKeywordList) {
+ foreach($htmlEntityKeywordList as $index => $keyword) {
+ $htmlEntityKeywordList[$index] = "\"{$keyword}\"";
+ }
+ $htmlEntityKeywords = implode(' AND ', $htmlEntityKeywordList);
+ }*/
+
+ //We can get a list of all the tsvector columns though this query:
+ $result=DB::query("SELECT table_name, column_name, data_type FROM information_schema.columns WHERE data_type='tsvector';");
+
+ if (!$result->numRecords()) throw Exception('there are no full text columns to search');
+
+ $tables=Array();
+
+ foreach($result as $row){
+
+ if(substr($row['table_name'], -5)!='_Live' && substr($row['table_name'], -9)!='_versions') {
+ $thisSql = "SELECT \"ID\", '{$row['table_name']}' AS ClassName, ts_rank(\"{$row['column_name']}\", q) FROM \"{$row['table_name']}\", to_tsquery('english', '$keywords') AS q WHERE \"{$row['column_name']}\" @@@ q ";
+ $tables[] = $thisSql;
+ }
+ }
+
+ $doSet=new DataObjectSet();
+
+ $sortBy='ts_rank';
+ $limit=10;
+ $fullQuery = "SELECT * FROM (" . implode(" UNION ", $tables) . ") AS q1 ORDER BY $sortBy LIMIT $limit";
+ $totalCount=1;
+ // Get records
+ $records = DB::query($fullQuery);
+
+ foreach($records as $record){
+ $item=DB::query("SELECT * FROM \"{$record['classname']}\" WHERE \"ID\"={$record['ID']};")->first();
+ $objects[] = new $record['classname']($item);
+ }
+ if(isset($objects)) $doSet = new DataObjectSet($objects);
+ else $doSet = new DataObjectSet();
+
+ $doSet->setPageLimits($start, $pageLength, $totalCount);
+
+
+ //$resultRows=Array();
+ //Right, now we go and run each of these queries and take their rank and put them into an array accordinaly
+ //$totalCount=0;
+ //foreach($tables as $sql){
+ // $result=DB::query($sql);
+ // foreach($result as $row){
+ //
+ // $resultRows[$row['ts_rank']][]=Array('ID'=>$row['ID'], 'Table'=>$row['source']);
+ // $totalCount++;
+ // }
+ //}
+
+ //Now we populate the dataobject with the results in order of relevance:
+ /*foreach($resultRows as $row_array){
+ foreach($row_array as $row){
+ $item=DataObject::get_by_id($row['Table'], $row['ID']);
+
+ $searchResults->push($item);
+ }
+ }
+
+ /*$searchResults->setPageLimits($start, $pageLength, $totalCount);*/
+
+ return $doSet;
+
+ }
}
/**
@@ -961,17 +1199,14 @@ class PostgreSQLQuery extends Query {
public function __construct(PostgreSQLDatabase $database, $handle) {
$this->database = $database;
$this->handle = $handle;
- parent::__construct();
}
public function __destroy() {
- //mysql_free_result($this->handle);
+ pg_free_result($this->handle);
}
public function seek($row) {
- //return mysql_data_seek($this->handle, $row);
- //This is unnecessary in postgres. You can just provide a row number with the fetch
- //command.
+ return pg_result_seek($this-handle, $row);
}
public function numRecords() {