query("SELECT tablename FROM pg_tables WHERE tablename NOT ILIKE 'pg_%' AND tablename NOT ILIKE 'sql_%'") as $record) {
//$table = strtolower(reset($record));
$table = reset($record);
$tables[$table] = $table;
}
//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;
}
/**
* Return the number of rows affected by the previous operation.
* @return int
*/
public function affectedRows() {
return pg_affected_rows(DB::$lastQuery);
}
/**
* A function to return the field names and datatypes for the particular table
*/
public function tableDetails($tableName){
$query="SELECT a.attname as \"Column\", pg_catalog.format_type(a.atttypid, a.atttypmod) as \"Datatype\" FROM pg_catalog.pg_attribute a WHERE a.attnum > 0 AND NOT a.attisdropped AND a.attrelid = ( SELECT c.oid FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relname ~ '^($tableName)$' AND pg_catalog.pg_table_is_visible(c.oid));";
$result=DB::query($query);
$table=Array();
while($row=pg_fetch_assoc($result)){
$table[]=Array('Column'=>$row['Column'], 'DataType'=>$row['DataType']);
}
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
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function boolean($values, $asDbValue=false){
//Annoyingly, we need to do a good ol' fashioned switch here:
($values['default']) ? $default='1' : $default='0';
if($asDbValue)
return Array('data_type'=>'smallint');
else {
if($values['arrayValue']!='')
$default='';
else
$default=' default ' . (int)$values['default'];
return "smallint{$values['arrayValue']}" . $default;
}
}
/**
* Return a date type-formatted string
* For MySQL, we simply return the word 'date', no other parameters are necessary
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function date($values){
//For reference, this is what typically gets passed to this function:
//$parts=Array('datatype'=>'date');
//DB::requireField($this->tableName, $this->name, "date");
return "date{$values['arrayValue']}";
}
/**
* Return a decimal type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function decimal($values, $asDbValue=false){
//For reference, this is what typically gets passed to this function:
//$parts=Array('datatype'=>'decimal', 'precision'=>"$this->wholeSize,$this->decimalSize");
//DB::requireField($this->tableName, $this->name, "decimal($this->wholeSize,$this->decimalSize)");
// Avoid empty strings being put in the db
if($values['precision'] == '') {
$precision = 1;
} else {
$precision = $values['precision'];
}
if($asDbValue)
return Array('data_type'=>'numeric', 'precision'=>'9');
else return "decimal($precision){$values['arrayValue']}";
}
/**
* Return a enum type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function enum($values){
//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
if(!isset($values['arrayValue']))
$values['arrayValue']='';
if($values['arrayValue']!='')
$default='';
else
$default=" default '{$values['default']}'";
return "varchar(255){$values['arrayValue']}" . $default . " check (\"" . $values['name'] . "\" in ('" . implode('\', \'', $values['enums']) . "'))";
}
/**
* Return a float type-formatted string
* For MySQL, we simply return the word 'date', no other parameters are necessary
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function float($values, $asDbValue=false){
//For reference, this is what typically gets passed to this function:
//$parts=Array('datatype'=>'float');
//DB::requireField($this->tableName, $this->name, "float");
if($asDbValue)
return Array('data_type'=>'double precision');
else return "float{$values['arrayValue']}";
}
/**
* Return a int type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function int($values, $asDbValue=false){
if($asDbValue)
return Array('data_type'=>'numeric', 'precision'=>$values['precision']);
else {
if($values['arrayValue']!='')
$default='';
else
$default=' default ' . (int)$values['default'];
return "numeric(11){$values['arrayValue']}" . $default;
}
}
/**
* Return a datetime type-formatted string
* For PostgreSQL, we simply return the word 'timestamp', no other parameters are necessary
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function ssdatetime($values, $asDbValue=false){
//For reference, this is what typically gets passed to this function:
//$parts=Array('datatype'=>'datetime');
//DB::requireField($this->tableName, $this->name, $values);
if($asDbValue)
return Array('data_type'=>'timestamp without time zone');
else
return "timestamp{$values['arrayValue']}";
}
/**
* Return a text type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function text($values, $asDbValue=false){
//For reference, this is what typically gets passed to this function:
//$parts=Array('datatype'=>'mediumtext', 'character set'=>'utf8', 'collate'=>'utf8_general_ci');
//DB::requireField($this->tableName, $this->name, "mediumtext character set utf8 collate utf8_general_ci");
if($asDbValue)
return Array('data_type'=>'text');
else
return "text{$values['arrayValue']}";
}
/**
* Return a time type-formatted string
* For MySQL, we simply return the word 'time', no other parameters are necessary
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function time($values){
//For reference, this is what typically gets passed to this function:
//$parts=Array('datatype'=>'time');
//DB::requireField($this->tableName, $this->name, "time");
return "time{$values['arrayValue']}";
}
/**
* Return a varchar type-formatted string
*
* @params array $values Contains a tokenised list of info about this data type
* @return string
*/
public function varchar($values, $asDbValue=false){
//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'=>'varchar', 'precision'=>$values['precision']);
else
return "varchar({$values['precision']}){$values['arrayValue']}";
}
/*
* Return a 4 digit numeric type. MySQL has a proprietary 'Year' type.
*/
public function year($values, $asDbValue=false){
if($asDbValue)
return Array('data_type'=>'numeric', 'precision'=>'4');
else return "numeric(4){$values['arrayValue']}";
}
function escape_character($escape=false){
if($escape)
return "\\\"";
else
return "\"";
}
/**
* Create a fulltext search datatype for PostgreSQL
*
* @param array $spec
*/
/*function fulltext($table, $spec){
//$spec['name'] is the column we've created that holds all the words we want to index.
//This is a coalesced collection of multiple columns if necessary
$spec='create index ix_' . $table . '_' . $spec['name'] . ' on ' . $table . ' using ' . $this->default_fts_cluster_method . '(' . $spec['name'] . ');';
return $spec;
}*/
/**
* This returns the column which is the primary key for each table
* In Postgres, it is a SERIAL8, which is the equivalent of an auto_increment
*
* @return string
*/
function IdColumn($asDbValue=false){
if($asDbValue)
return 'bigint';
else return 'serial8 not null';
}
/**
* Returns true if this table exists
* @todo Make a proper implementation
*/
function hasTable($tableName) {
return true;
}
/**
* Returns the SQL command to get all the tables in this database
*/
function allTablesSQL(){
return "select table_name from information_schema.tables where table_schema='public' and table_type='BASE TABLE';";
}
/**
* Return enum values for the given field
* @todo Make a proper implementation
*/
function enumValuesForField($tableName, $fieldName) {
return array('SiteTree','Page');
}
/**
* Because NOW() doesn't always work...
* MSSQL, I'm looking at you
*
*/
function now(){
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
* helper function in Database?
*/
public function sqlQueryToString(SQLQuery $sqlQuery) {
if (!$sqlQuery->from) return '';
$distinct = $sqlQuery->distinct ? "DISTINCT " : "";
if($sqlQuery->delete) {
$text = "DELETE ";
} else if($sqlQuery->select) {
$text = "SELECT $distinct" . implode(", ", $sqlQuery->select);
}
$text .= " FROM " . implode(" ", $sqlQuery->from);
if($sqlQuery->where) $text .= " WHERE (" . $sqlQuery->getFilter(). ")";
if($sqlQuery->groupby) $text .= " GROUP BY " . implode(", ", $sqlQuery->groupby);
if($sqlQuery->having) $text .= " HAVING ( " . implode(" ) AND ( ", $sqlQuery->having) . " )";
if($sqlQuery->orderby) $text .= " ORDER BY " . $sqlQuery->orderby;
echo 'limit: ';
print_r($sqlQuery->limit);
echo '
';
echo 'order by:';
print_r($sqlQuery->orderby);
echo '
';
if($sqlQuery->limit) {
$limit = $sqlQuery->limit;
// Pass limit as array or SQL string value
if(is_array($limit)) {
if(isset($limit['start']))
$text.=' OFFSET ' . $limit['start'];
if(isset($limit['limit']))
$text.=' LIMIT ' . $limit['limit'];
} else {
if(strpos($sqlQuery->limit, ',')){
$limit=str_replace(',', ' LIMIT ', $sqlQuery->limit);
$text .= ' OFFSET ' . $limit;
} else {
$text.=' LIMIT ' . $sqlQuery->limit;
}
}
}
return $text;
}
/*
* This will return text which has been escaped in a database-friendly manner
* Using PHP's addslashes method won't work in MSSQL
*/
function addslashes($value){
return pg_escape_string($value);
}
/*
* This changes the index name depending on database requirements.
* MySQL doesn't need any changes.
*/
function modifyIndex($index, $spec){
if(is_array($spec) && $spec['type']=='fulltext')
return 'ts_' . str_replace(',', '_', $index);
else
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 = "ts_rank DESC", $extraFilter = "", $booleanSearch = false, $alternativeFileFilter = "", $invertedMatch = false) {
/*
$fileFilter = '';
$keywords = Convert::raw2sql($keywords);
$htmlEntityKeywords = htmlentities($keywords);
$extraFilters = array('SiteTree' => '', 'File' => '');
//if($booleanSearch) $boolean = "IN BOOLEAN MODE";
$boolean='';
if($extraFilter) {
$extraFilters['SiteTree'] = " AND $extraFilter";
if($alternativeFileFilter) $extraFilters['File'] = " AND $alternativeFileFilter";
else $extraFilters['File'] = $extraFilters['SiteTree'];
}
// Always ensure that only pages with ShowInSearch = 1 can be searched
$extraFilters['SiteTree'] .= " AND ShowInSearch <> 0";
$limit = $start . ", " . (int) $pageLength;
$notMatch = $invertedMatch ? "NOT " : "";
if($keywords) {
$match['SiteTree'] = "
MATCH (Title, MenuTitle, Content, MetaTitle, MetaDescription, MetaKeywords) AGAINST ('$keywords' $boolean)
+ MATCH (Title, MenuTitle, Content, MetaTitle, MetaDescription, MetaKeywords) AGAINST ('$htmlEntityKeywords' $boolean)
";
$match['File'] = "MATCH (Filename, Title, Content) AGAINST ('$keywords' $boolean) AND ClassName = 'File'";
// We make the relevance search by converting a boolean mode search into a normal one
$relevanceKeywords = str_replace(array('*','+','-'),'',$keywords);
$htmlEntityRelevanceKeywords = str_replace(array('*','+','-'),'',$htmlEntityKeywords);
$relevance['SiteTree'] = "MATCH (Title, MenuTitle, Content, MetaTitle, MetaDescription, MetaKeywords) AGAINST ('$relevanceKeywords') + MATCH (Title, MenuTitle, Content, MetaTitle, MetaDescription, MetaKeywords) AGAINST ('$htmlEntityRelevanceKeywords')";
$relevance['File'] = "MATCH (Filename, Title, Content) AGAINST ('$relevanceKeywords')";
} else {
$relevance['SiteTree'] = $relevance['File'] = 1;
$match['SiteTree'] = $match['File'] = "1 = 1";
}
// Generate initial queries and base table names
$baseClasses = array('SiteTree' => '', 'File' => '');
foreach($classesToSearch as $class) {
$queries[$class] = singleton($class)->extendedSQL($notMatch . $match[$class] . $extraFilters[$class], "");
$baseClasses[$class] = reset($queries[$class]->from);
}
// Make column selection lists
$select = array(
'SiteTree' => array("ClassName","$baseClasses[SiteTree].ID","ParentID","Title","URLSegment","Content","LastEdited","Created","_utf8'' AS Filename", "_utf8'' AS Name", "$relevance[SiteTree] AS Relevance", "CanViewType"),
'File' => array("ClassName","$baseClasses[File].ID","_utf8'' AS ParentID","Title","_utf8'' AS URLSegment","Content","LastEdited","Created","Filename","Name","$relevance[File] AS Relevance","NULL AS CanViewType"),
);
// Process queries
foreach($classesToSearch as $class) {
// There's no need to do all that joining
$queries[$class]->from = array(str_replace('`','',$baseClasses[$class]) => $baseClasses[$class]);
$queries[$class]->select = $select[$class];
$queries[$class]->orderby = null;
}
// Combine queries
$querySQLs = array();
$totalCount = 0;
foreach($queries as $query) {
$querySQLs[] = $query->sql();
$totalCount += $query->unlimitedRowCount();
}
$fullQuery = implode(" UNION ", $querySQLs) . " ORDER BY $sortBy LIMIT $limit";
// Get records
$records = DB::query($fullQuery);
foreach($records as $record)
$objects[] = new $record['ClassName']($record);
if(isset($objects)) $doSet = new DataObjectSet($objects);
else $doSet = new DataObjectSet();
$doSet->setPageLimits($start, $pageLength, $totalCount);
return $doSet;
*/
$keywords = Convert::raw2sql(trim($keywords));
$htmlEntityKeywords = htmlentities($keywords);
//We can get a list of all the tsvector columns though this query:
//We know what tables to search in based on the $classesToSearch variable:
$result=DB::query("SELECT table_name, column_name, data_type FROM information_schema.columns WHERE data_type='tsvector' AND table_name in ('" . implode("', '", $classesToSearch) . "');");
if (!$result->numRecords()) throw Exception('there are no full text columns to search');
$tables=Array();
foreach($result as $row){
if($row['table_name']=='SiteTree')
$showInSearch="AND \"ShowInSearch\"=1 ";
else
$showInSearch='';
if($keywords){
$thisSql = "SELECT \"ID\", '{$row['table_name']}' AS ClassName, ts_rank(\"{$row['column_name']}\", q) AS Relevance FROM \"{$row['table_name']}\", to_tsquery('english', '$keywords') AS q WHERE \"{$row['column_name']}\" " . $this->default_fts_search_method . " q $showInSearch";
} else {
$thisSql = "SELECT \"ID\", '{$row['table_name']}' AS ClassName FROM \"{$row['table_name']}\" WHERE 1=1 $showInSearch";
}
//Add this query to the collection
$tables[] = $thisSql;
}
$doSet=new DataObjectSet();
$limit=$pageLength;
$offset=$start;
if($keywords)
$orderBy=" ORDER BY $sortBy";
else $orderBy='';
$fullQuery = "SELECT * FROM (" . implode(" UNION ", $tables) . ") AS q1 $orderBy LIMIT $limit OFFSET $offset";
// Get records
$records = DB::query($fullQuery);
$totalCount=0;
foreach($records as $record){
$item=DB::query("SELECT * FROM \"{$record['classname']}\" WHERE \"ID\"={$record['ID']};")->first();
$objects[] = new $record['classname']($item);
$totalCount++;
}
if(isset($objects)) $doSet = new DataObjectSet($objects);
else $doSet = new DataObjectSet();
$doSet->setPageLimits($start, $pageLength, $totalCount);
return $doSet;
}
}
/**
* A result-set from a MySQL database.
* @package sapphire
* @subpackage model
*/
class PostgreSQLQuery extends Query {
/**
* The MySQLDatabase object that created this result set.
* @var MySQLDatabase
*/
private $database;
/**
* The internal MySQL handle that points to the result set.
* @var resource
*/
private $handle;
/**
* Hook the result-set given into a Query class, suitable for use by sapphire.
* @param database The database object that created this query.
* @param handle the internal mysql handle that is points to the resultset.
*/
public function __construct(PostgreSQLDatabase $database, $handle) {
$this->database = $database;
$this->handle = $handle;
}
public function __destroy() {
pg_free_result($this->handle);
}
public function seek($row) {
return pg_result_seek($this-handle, $row);
}
public function numRecords() {
return pg_num_rows($this->handle);
}
public function nextRecord() {
// Coalesce rather than replace common fields.
if($data = pg_fetch_row($this->handle)) {
foreach($data as $columnIdx => $value) {
$columnName = pg_field_name($this->handle, $columnIdx);
// $value || !$ouput[$columnName] means that the *last* occurring value is shown
// !$ouput[$columnName] means that the *first* occurring value is shown
if(isset($value) || !isset($output[$columnName])) {
$output[$columnName] = $value;
}
}
return $output;
} else {
return false;
}
}
}
?>