Răsfoiți Sursa

PostgreSQL draft

https://github.com/FreshRSS/FreshRSS/issues/416
Based on @Damstre work https://github.com/FreshRSS/FreshRSS/pull/1071
Not tested
Alexandre Alapetite 9 ani în urmă
părinte
comite
7c1b5e322c

+ 4 - 3
app/Models/CategoryDAO.php

@@ -10,7 +10,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		);
 
 		if ($stm && $stm->execute($values)) {
-			return $this->bd->lastInsertId();
+			return $this->bd->lastInsertId('"' . parent::prefix . 'category_id_seq"');
 		} else {
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			Minz_Log::error('SQL error addCategory: ' . $info[2]);
@@ -207,12 +207,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 
 		$previousLine = null;
 		$feedsDao = array();
+		$feedDao = FreshRSS_Factory::createFeedDAO();
 		foreach ($listDAO as $line) {
 			if ($previousLine['c_id'] != null && $line['c_id'] !== $previousLine['c_id']) {
 				// End of the current category, we add it to the $list
 				$cat = new FreshRSS_Category(
 					$previousLine['c_name'],
-					FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id'])
+					$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
 				);
 				$cat->_id($previousLine['c_id']);
 				$list[$previousLine['c_id']] = $cat;
@@ -228,7 +229,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		if ($previousLine != null) {
 			$cat = new FreshRSS_Category(
 				$previousLine['c_name'],
-				FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id'])
+				$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
 			);
 			$cat->_id($previousLine['c_id']);
 			$list[$previousLine['c_id']] = $cat;

+ 1 - 0
app/Models/ConfigurationSetter.php

@@ -287,6 +287,7 @@ class FreshRSS_ConfigurationSetter {
 
 		switch ($value['type']) {
 		case 'mysql':
+		case 'pgsql':
 			if (empty($value['host']) ||
 					empty($value['user']) ||
 					empty($value['base']) ||

+ 1 - 1
app/Models/EntryDAO.php

@@ -3,7 +3,7 @@
 class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 	public function isCompressed() {
-		return parent::$sharedDbType !== 'sqlite';
+		return parent::$sharedDbType === 'mysql';
 	}
 
 	public function hasNativeHex() {

+ 92 - 0
app/Models/EntryDAOPGSQL.php

@@ -0,0 +1,92 @@
+<?php
+
+class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAO {
+
+	protected function addColumn($name) {
+		return false;
+	}
+
+	protected function updateCacheUnreads($catId = false, $feedId = false) {
+		return true;	//done via triggers
+	}
+
+	/**
+	 * Mark all the articles in a feed as read.
+	 * There is a fail safe to prevent to mark as read articles that are
+	 * loaded during the mark as read action. Then the cache is updated.
+	 *
+	 * If $idMax equals 0, a deprecated debug message is logged
+	 *
+	 * @param integer $id_feed feed ID
+	 * @param integer $idMax fail safe article ID
+	 * @return integer affected rows
+	 */
+	public function markReadFeed($id_feed, $idMax = 0) {
+		if ($idMax == 0) {
+			$idMax = time() . '000000';
+			Minz_Log::debug('Calling markReadFeed(0) is deprecated!');
+		}
+		$this->bd->beginTransaction();
+
+		$sql = 'UPDATE "' . $this->prefix . 'entry" '
+			 . 'SET is_read=:is_read '
+			 . 'WHERE id_feed=:id_feed AND NOT is_read AND id <= :idmax';
+		$values = array($id_feed, $idMax);
+		$stm = $this->bd->prepare($sql);
+		$stm->bindValue(':is_read', true, PDO::PARAM_BOOL);
+		$stm->bindValue(':id_feed', $id_feed);
+		$stm->bindValue(':idmax', $idMax);
+
+		if (!($stm && $stm->execute())) {
+			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
+			Minz_Log::error('SQL error markReadFeed: ' . $info[2]);
+			$this->bd->rollBack();
+			return false;
+		}
+		$affected = $stm->rowCount();
+
+		$this->bd->commit();
+		return $affected;
+	}
+
+	public function listHashForFeedGuids($id_feed, $guids) {
+		if (count($guids) < 1) {
+			return array();
+		}
+		$sql = 'SELECT guid, hash AS hexHash FROM "' . $this->prefix . 'entry" WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
+		$stm = $this->bd->prepare($sql);
+		$values = array($id_feed);
+		$values = array_merge($values, $guids);
+		if ($stm && $stm->execute($values)) {
+			$result = array();
+			$rows = $stm->fetchAll(PDO::FETCH_ASSOC);
+			foreach ($rows as $row) {
+				$result[$row['guid']] = $row['hexHash'];
+			}
+			return $result;
+		} else {
+			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			if ($this->autoAddColumn($info)) {
+				return $this->listHashForFeedGuids($id_feed, $guids);
+			}
+			Minz_Log::error('SQL error listHashForFeedGuids: ' . $info[0] . ': ' . $info[1] . ' ' . $info[2]
+				. ' while querying feed ' . $id_feed);
+			return false;
+		}
+	}
+
+	public function optimizeTable() {
+		return null;
+	}
+
+	public function size($all = true) {
+		$db = FreshRSS_Context::$system_conf->db;
+		$sql = 'SELECT pg_size_pretty(pg_database_size(?))';
+		$values = array($db['base']);
+		$stm = $this->bd->prepare($sql);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+		return $res[0];
+	}
+
+}

+ 26 - 16
app/Models/Factory.php

@@ -4,37 +4,47 @@ class FreshRSS_Factory {
 
 	public static function createFeedDao($username = null) {
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_FeedDAOSQLite($username);
-		} else {
-			return new FreshRSS_FeedDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_FeedDAOSQLite($username);
+			default:
+				return new FreshRSS_FeedDAO($username);
 		}
 	}
 
 	public static function createEntryDao($username = null) {
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_EntryDAOSQLite($username);
-		} else {
-			return new FreshRSS_EntryDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_EntryDAOSQLite($username);
+			case 'pgsql':
+				return new FreshRSS_EntryDAOPGSQL($username);
+			default:
+				return new FreshRSS_EntryDAO($username);
 		}
 	}
 
 	public static function createStatsDAO($username = null) {
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_StatsDAOSQLite($username);
-		} else {
-			return new FreshRSS_StatsDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_StatsDAOSQLite($username);
+			case 'pgsql':
+				return new FreshRSS_StatsDAOPGSQL($username);
+			default:
+				return new FreshRSS_StatsDAO($username);
 		}
 	}
 
 	public static function createDatabaseDAO($username = null) {
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_DatabaseDAOSQLite($username);
-		} else {
-			return new FreshRSS_DatabaseDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_DatabaseDAOSQLite($username);
+			case 'pgsql':
+				return new FreshRSS_DatabaseDAOPGSQL($username);
+			default:
+				return new FreshRSS_DatabaseDAO($username);
 		}
 	}
 

+ 1 - 1
app/Models/FeedDAO.php

@@ -16,7 +16,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		);
 
 		if ($stm && $stm->execute($values)) {
-			return $this->bd->lastInsertId();
+			return $this->bd->lastInsertId('"' . parent::prefix . 'feed_id_seq"');
 		} else {
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			Minz_Log::error('SQL error addFeed: ' . $info[2]);

+ 4 - 4
app/Models/StatsDAO.php

@@ -37,10 +37,10 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
 			$filter .= "AND e.id_feed = {$feed}";
 		}
 		$sql = <<<SQL
-SELECT COUNT(1) AS `total`,
-COUNT(1) - SUM(e.is_read) AS `unread`,
-SUM(e.is_read) AS `read`,
-SUM(e.is_favorite) AS `favorite`
+SELECT COUNT(1) AS total,
+COUNT(1) - SUM(e.is_read) AS unread,
+SUM(e.is_read) AS read,
+SUM(e.is_favorite) AS favorite
 FROM {$this->prefix}entry AS e
 , {$this->prefix}feed AS f
 WHERE e.id_feed = f.id

+ 192 - 0
app/Models/StatsDAOPGSQL.php

@@ -0,0 +1,192 @@
+<?php
+
+class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
+
+	/**
+	 * Calculates entry repartition for the selection.
+	 * The repartition includes:
+	 *   - total entries
+	 *   - read entries
+	 *   - unread entries
+	 *   - favorite entries
+	 *
+	 * @param null|integer $feed feed id
+	 * @param boolean $only_main
+	 * @return array
+	 */
+	public function calculateEntryRepartitionPerFeed($feed = null, $only_main = false) {
+		$filter = '';
+		if ($only_main) {
+			$filter .= 'AND f.priority = 10';
+		}
+		if (!is_null($feed)) {
+			$filter .= "AND e.id_feed = {$feed}";
+		}
+		$sql = <<<SQL
+SELECT COUNT(1) AS total,
+COUNT(1) - SUM(case when e.is_read then 1 else 0 end) AS unread,
+SUM(case when e.is_read then 1 else 0 end) AS read,
+SUM(case when e.is_favorite then 1 else 0 end) AS favorite
+FROM "{$this->prefix}entry" AS e
+, "{$this->prefix}feed" AS f
+WHERE e.id_feed = f.id
+{$filter}
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+		return $res[0];
+	}
+
+	/**
+	 * Calculates entry count per day on a 30 days period.
+	 * Returns the result as a JSON string.
+	 *
+	 * @return string
+	 */
+	public function calculateEntryCount() {
+		$count = $this->initEntryCountArray();
+		$period = self::ENTRY_COUNT_PERIOD;
+
+		// Get stats per day for the last 30 days
+		$sql = <<<SQL
+SELECT to_timestamp(e.date) - NOW() AS day,
+COUNT(1) AS count
+FROM "{$this->prefix}entry" AS e
+WHERE to_timestamp(e.date) BETWEEN NOW() - INTERVAL '{$period} DAYS' AND NOW() - INTERVAL '1 DAY'
+GROUP BY day
+ORDER BY day ASC
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+
+		foreach ($res as $value) {
+			$count[$value['day']] = (int) $value['count'];
+		}
+
+		return $this->convertToSerie($count);
+	}
+
+	/**
+	 * Calculates entry average per day on a 30 days period.
+	 *
+	 * @return integer
+	 */
+	public function calculateEntryAverage() {
+		$period = self::ENTRY_COUNT_PERIOD;
+
+		// Get stats per day for the last 30 days
+		$sql = <<<SQL
+SELECT COUNT(1) / {$period} AS average
+FROM "{$this->prefix}entry" AS e
+WHERE to_timestamp(e.date) BETWEEN NOW() - INTERVAL '{$period} DAYS' AND NOW() - INTERVAL '1 DAY'
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetch(PDO::FETCH_NAMED);
+
+		return round($res['average'], 2);
+	}
+
+	/**
+	 * Calculates the number of article per hour of the day per feed
+	 *
+	 * @param integer $feed id
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerHour($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('hour', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per day of week per feed
+	 *
+	 * @param integer $feed id
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('day', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per month per feed
+	 *
+	 * @param integer $feed
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerMonth($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('month', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per period per feed
+	 *
+	 * @param string $period format string to use for grouping
+	 * @param integer $feed id
+	 * @return string
+	 */
+	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
+		$restrict = '';
+		if ($feed) {
+			$restrict = "WHERE e.id_feed = {$feed}";
+		}
+		$sql = <<<SQL
+SELECT extract( {$period} from to_timestamp(e.date)) AS period
+, COUNT(1) AS count
+FROM "{$this->prefix}entry" AS e
+{$restrict}
+GROUP BY period
+ORDER BY period ASC
+SQL;
+
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_NAMED);
+
+		foreach ($res as $value) {
+			$repartition[(int) $value['period']] = (int) $value['count'];
+		}
+
+		return $this->convertToSerie($repartition);
+	}
+
+	/**
+	 * Calculates the average number of article per feed
+	 *
+	 * @param float $period number used to divide the number of day in the period
+	 * @param integer $feed id
+	 * @return integer
+	 */
+	protected function calculateEntryAveragePerFeedPerPeriod($period, $feed = null) {
+		$restrict = '';
+		if ($feed) {
+			$restrict = "WHERE e.id_feed = {$feed}";
+		}
+		$sql = <<<SQL
+SELECT COUNT(1) AS count
+, MIN(date) AS date_min
+, MAX(date) AS date_max
+FROM "{$this->prefix}entry" AS e
+{$restrict}
+SQL;
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetch(PDO::FETCH_NAMED);
+		$date_min = new \DateTime();
+		$date_min->setTimestamp($res['date_min']);
+		$date_max = new \DateTime();
+		$date_max->setTimestamp($res['date_max']);
+		$interval = $date_max->diff($date_min, true);
+		$interval_in_days = $interval->format('%a');
+		if ($interval_in_days <= 0) {
+			// Surely only one article.
+			// We will return count / (period/period) == count.
+			$interval_in_days = $period;
+		}
+
+		return $res['count'] / ($interval_in_days / $period);
+	}
+
+}

+ 2 - 0
app/SQL/install.sql.mysql.php

@@ -1,4 +1,6 @@
 <?php
+define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+
 define('SQL_CREATE_TABLES', '
 CREATE TABLE IF NOT EXISTS `%1$scategory` (
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7

+ 91 - 0
app/SQL/install.sql.pgsql.php

@@ -0,0 +1,91 @@
+<?php
+define('SQL_CREATE_TABLES', '
+CREATE TABLE IF NOT EXISTS "%1$scategory" (
+	id SERIAL PRIMARY KEY,
+	name varchar(255) UNIQUE NOT NULL
+)
+
+CREATE TABLE IF NOT EXISTS "%1$sfeed" (
+	id SERIAL PRIMARY KEY,
+	url varchar(511) UNIQUE NOT NULL,
+	category SMALLINT DEFAULT 0,
+	name varchar(255) NOT NULL,
+	website varchar(255),
+	description text,
+	lastUpdate int DEFAULT 0,
+	priority smallint NOT NULL DEFAULT 10,
+	pathEntries varchar(511) DEFAULT NULL,
+	httpAuth varchar(511) DEFAULT NULL,
+	error boolean DEFAULT FALSE,
+	keep_history INT NOT NULL DEFAULT -2,
+	ttl INT NOT NULL DEFAULT -2,
+	cache_nbEntries int DEFAULT 0,
+	cache_nbUnreads int DEFAULT 0,
+	FOREIGN KEY (category) REFERENCES "%1$scategory" (id) ON DELETE SET NULL ON UPDATE CASCADE
+)
+CREATE INDEX name_index ON "%1$sfeed" (name)
+CREATE INDEX priority_index ON "%1$sfeed" (priority)
+CREATE INDEX keep_history_index ON "%1$sfeed" (keep_history)
+
+CREATE TABLE IF NOT EXISTS "%1$sentry" (
+	id bigint NOT NULL PRIMARY KEY,
+	guid varchar(760) UNIQUE NOT NULL,
+	title varchar(255) NOT NULL,
+	author varchar(255),
+	content_bin text,
+	link varchar(1023) NOT NULL,
+	date INT,
+	lastSeen INT DEFAULT 0,
+	hash BYTEA,
+	is_read boolean NOT NULL DEFAULT false,
+	is_favorite boolean NOT NULL DEFAULT false,
+	id_feed SMALLINT,
+	tags varchar(1023),
+	FOREIGN KEY (id_feed) REFERENCES "%1$sfeed" (id) ON DELETE CASCADE ON UPDATE CASCADE
+)
+CREATE INDEX is_favorite_index ON "%1$sentry" (is_favorite)
+CREATE INDEX is_read_index ON "%1$sentry" (is_read)
+CREATE INDEX entry_lastSeen_index ON "%1$sentry" (lastSeen)
+
+CREATE OR REPLACE FUNCTION update_unread_feed() RETURNS TRIGGER AS $$
+	BEGIN
+		UPDATE "%1$sfeed" 
+			SET cache_nbUnreads=(SELECT COUNT(*) FROM "%1$sentry" WHERE id_feed=OLD.id_feed AND NOT is_read)
+			WHERE id=OLD.id_feed;
+		return NULL;
+	end;
+	$$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER update_unread_feed
+	AFTER UPDATE OF is_read OR DELETE OR INSERT ON "%1$sentry"
+	FOR EACH ROW
+	EXECUTE PROCEDURE update_unread_feed();
+
+CREATE OR REPLACE FUNCTION reset_feed_seq() RETURNS TRIGGER AS $$
+	BEGIN
+		PERFORM 1 FROM "%1$sfeed";
+		IF NOT FOUND THEN
+			ALTER SEQUENCE IF EXISTS "%1$sfeed_id_seq" RESTART;
+		END IF;
+		return NULL;
+	end;
+	$$ LANGUAGE PLPGSQL;
+
+CREATE TRIGGER reset_feed_seq
+	AFTER DELETE ON "%1$sfeed"
+	FOR EACH STATEMENT
+	EXECUTE PROCEDURE reset_feed_seq();
+
+
+CREATE OR REPLACE RULE check_constraints_on_entry AS ON INSERT TO "%1$sentry" WHERE EXISTS(SELECT 1 FROM "%1$sentry" WHERE guid=NEW.guid) DO INSTEAD NOTHING;
+CREATE OR REPLACE RULE check_constraints_on_feed AS ON INSERT TO "%1$sfeed" WHERE EXISTS(SELECT 1 FROM "%1$sfeed" WHERE url=NEW.url) DO INSTEAD NOTHING;
+CREATE OR REPLACE RULE check_constraints_on_category AS ON INSERT TO "%1$scategory" WHERE EXISTS(SELECT 1 FROM "%1$scategory" WHERE name=NEW.name) DO INSTEAD NOTHING;
+CREATE OR REPLACE RULE check_constraints_on_category as on update to "%1$scategory" WHERE EXISTS(SELECT 1 FROM "%1$scategory" WHERE name=NEW.name) DO INSTEAD NOTHING;
+
+
+INSERT IGNORE INTO "%1$scategory" (id, name) VALUES(1, "%2$s");
+INSERT IGNORE INTO "%1$sfeed" (url, category, name, website, description, ttl) VALUES("http://freshrss.org/feeds/all.atom.xml", 1, "FreshRSS.org", "http://freshrss.org/", "FreshRSS, a free, self-hostable aggregator…", 86400);
+INSERT IGNORE INTO "%1$sfeed" (url, category, name, website, description, ttl) VALUES("https://github.com/FreshRSS/FreshRSS/releases.atom", 1, "FreshRSS @ GitHub", "https://github.com/FreshRSS/FreshRSS/", "FreshRSS releases @ GitHub", 86400);
+');
+
+define('SQL_DROP_TABLES', 'DROP TABLES "%1$sentry", "%1$sfeed", "%1$scategory"');

+ 28 - 3
app/install.php

@@ -19,8 +19,6 @@ if (isset($_GET['step'])) {
 	define('STEP', 0);
 }
 
-define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
-
 if (STEP === 3 && isset($_POST['type'])) {
 	$_SESSION['bd_type'] = $_POST['type'];
 }
@@ -33,6 +31,9 @@ if (isset($_SESSION['bd_type'])) {
 	case 'sqlite':
 		include(APP_PATH . '/SQL/install.sql.sqlite.php');
 		break;
+	case 'pgsql':
+		include(APP_PATH . '/SQL/install.sql.pgsql.php');
+		break;
 	}
 }
 
@@ -301,7 +302,8 @@ function checkStep1() {
 	$curl = extension_loaded('curl');
 	$pdo_mysql = extension_loaded('pdo_mysql');
 	$pdo_sqlite = extension_loaded('pdo_sqlite');
-	$pdo = $pdo_mysql || $pdo_sqlite;
+	$pdo_pgsql = extension_loaded('pdo_pgsql');
+	$pdo = $pdo_mysql || $pdo_sqlite || $pdo_pgsql;
 	$pcre = extension_loaded('pcre');
 	$ctype = extension_loaded('ctype');
 	$dom = class_exists('DOMDocument');
@@ -319,6 +321,7 @@ function checkStep1() {
 		'curl' => $curl ? 'ok' : 'ko',
 		'pdo-mysql' => $pdo_mysql ? 'ok' : 'ko',
 		'pdo-sqlite' => $pdo_sqlite ? 'ok' : 'ko',
+		'pdo-pgsql' => $pdo_pgsql ? 'ok' : 'ko',
 		'pdo' => $pdo ? 'ok' : 'ko',
 		'pcre' => $pcre ? 'ok' : 'ko',
 		'ctype' => $ctype ? 'ok' : 'ko',
@@ -435,6 +438,22 @@ function checkBD() {
 				PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 			);
 			break;
+		case 'pgsql':
+			$driver_options = array(
+				PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+			);
+
+			try {	// on ouvre une connexion juste pour créer la base si elle n'existe pas
+				$str = 'pgsql:host=' . $_SESSION['bd_host'] . ';';
+				$c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
+				$sql = sprintf(SQL_CREATE_DB, $_SESSION['bd_base']);
+				$res = $c->query($sql);
+			} catch (PDOException $e) {
+			}
+
+			// on écrase la précédente connexion en sélectionnant la nouvelle BDD
+			$str = 'pgsql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base'];
+			break;
 		default:
 			return false;
 		}
@@ -708,6 +727,12 @@ function printStep3() {
 					SQLite
 				</option>
 				<?php }?>
+				<?php if (extension_loaded('pdo_pgsql')) {?>
+				<option value="sqlite"
+					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql') ? 'selected="selected"' : ''; ?>>
+					PostgreSQL
+				</option>
+				<?php }?>
 				</select>
 			</div>
 		</div>

+ 49 - 28
lib/Minz/ModelPdo.php

@@ -55,36 +55,36 @@ class Minz_ModelPdo {
 		$driver_options = isset($conf->db['pdo_options']) && is_array($conf->db['pdo_options']) ? $conf->db['pdo_options'] : array();
 
 		try {
-			$type = $db['type'];
-			if ($type === 'mysql') {
-				$string = 'mysql:host=' . $db['host']
-				        . ';dbname=' . $db['base']
-				        . ';charset=utf8mb4';
-				$driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8mb4';
-				$this->prefix = $db['prefix'] . $currentUser . '_';
-			} elseif ($type === 'sqlite') {
-				$string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite');
-				//$driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
-				$this->prefix = '';
-			} else {
-				throw new Minz_PDOConnectionException(
-					'Invalid database type!',
-					$db['user'], Minz_Exception::ERROR
-				);
-			}
-			self::$sharedDbType = $type;
-			self::$sharedPrefix = $this->prefix;
-
-			$this->bd = new MinzPDO(
-				$string,
-				$db['user'],
-				$db['password'],
-				$driver_options
-			);
-			if ($type === 'sqlite') {
-				$this->bd->exec('PRAGMA foreign_keys = ON;');
+			switch ($db['type']) {
+				case 'mysql':
+					$string = 'mysql:host=' . $db['host'] . ';dbname=' . $db['base'] . ';charset=utf8mb4';
+					$driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8mb4';
+					$this->prefix = $db['prefix'] . $currentUser . '_';
+					$this->bd = new MinzPDO($string, $db['user'], $db['password'], $driver_options);
+					//TODO Consider: $this->bd->exec("SET SESSION sql_mode = 'ANSI_QUOTES';");
+					break;
+				case 'sqlite':
+					$string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite');
+					$this->prefix = '';
+					$this->bd = new MinzPDO($string, $db['user'], $db['password'], $driver_options);
+					$this->bd->exec('PRAGMA foreign_keys = ON;');
+					break;
+				case 'pgsql':
+					$string = 'pgsql:host=' . $db['host'] . ';dbname=' . $db['base'] . ';charset=utf8';
+					$this->prefix = $db['prefix'] . $currentUser . '_';
+					$this->bd = new MinzPDOPGSQL($string, $db['user'], $db['password'], $driver_options);
+					$this->bd->exec("SET NAMES 'UTF8';");
+					break;
+				default:
+					throw new Minz_PDOConnectionException(
+						'Invalid database type!',
+						$db['user'], Minz_Exception::ERROR
+					);
+					break;
 			}
 			self::$sharedBd = $this->bd;
+			self::$sharedDbType = $db['type'];
+			self::$sharedPrefix = $this->prefix;
 		} catch (Exception $e) {
 			throw new Minz_PDOConnectionException(
 				$string,
@@ -119,18 +119,39 @@ class MinzPDO extends PDO {
 		}
 	}
 
+	protected function compatibility($statement) {
+		return $statement;
+	}
+
 	public function prepare($statement, $driver_options = array()) {
 		MinzPDO::check($statement);
+		$statement = MinzPDO::compatibility($statement);
 		return parent::prepare($statement, $driver_options);
 	}
 
 	public function exec($statement) {
 		MinzPDO::check($statement);
+		$statement = MinzPDO::compatibility($statement);
 		return parent::exec($statement);
 	}
 
 	public function query($statement) {
 		MinzPDO::check($statement);
+		$statement = MinzPDO::compatibility($statement);
 		return parent::query($statement);
 	}
+
+	public function lastInsertId($name = null) {
+		return parent::lastInsertId();	//We discard the name, only used by PostgreSQL
+	}
+}
+
+class MinzPDOPGSQL extends MinzPDO {
+	protected function compatibility($statement) {
+		return str_replace(array('`', " X'"), array('"', " E'\\x"), $statement);
+	}
+
+	public function lastInsertId($name = null) {
+		return parent::lastInsertId($name);
+	}
 }

+ 4 - 2
p/scripts/install.js

@@ -42,13 +42,15 @@ if (auth_type) {
 function mySqlShowHide() {
 	var mysql = document.getElementById('mysql');
 	if (mysql) {
-		mysql.style.display = document.getElementById('type').value === 'mysql' ? 'block' : 'none';
-		if (document.getElementById('type').value !== 'mysql') {
+		if (document.getElementById('type').value === 'sqlite') {
 			document.getElementById('host').value = '';
 			document.getElementById('user').value = '';
 			document.getElementById('pass').value = '';
 			document.getElementById('base').value = '';
 			document.getElementById('prefix').value = '';
+			mysql.style.display = 'none';
+		} else {
+			mysql.style.display = 'block';
 		}
 	}
 }