316 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nulib\db\pgsql;
 | |
| 
 | |
| use nulib\cl;
 | |
| use nulib\db\_private\_config;
 | |
| use nulib\db\_private\Tvalues;
 | |
| use nulib\db\IDatabase;
 | |
| use nulib\db\ITransactor;
 | |
| use nulib\php\func;
 | |
| use nulib\ValueException;
 | |
| 
 | |
| class Pgsql implements IDatabase {
 | |
|   use Tvalues;
 | |
| 
 | |
|   const PREFIX = "pgsql";
 | |
| 
 | |
|   function getPrefix(): ?string {
 | |
|     return self::PREFIX;
 | |
|   }
 | |
| 
 | |
|   static function with($pgsql, ?array $params=null): self {
 | |
|     if ($pgsql instanceof static) {
 | |
|       return $pgsql;
 | |
|     } elseif ($pgsql instanceof self) {
 | |
|       # recréer avec les mêmes paramètres
 | |
|       return new static(null, cl::merge([
 | |
|         "dbconn" => $pgsql->dbconn,
 | |
|         "options" => $pgsql->options,
 | |
|         "config" => $pgsql->config,
 | |
|         "migration" => $pgsql->migration,
 | |
|       ], $params));
 | |
|     } else {
 | |
|       return new static($pgsql, $params);
 | |
|     }
 | |
|   }
 | |
| 
 | |
| 
 | |
|   protected const OPTIONS = [
 | |
|     # XXX désactiver les connexions persistantes par défaut
 | |
|     # pour réactiver par défaut, il faudrait vérifier la connexion à chaque fois
 | |
|     # qu'elle est ouverte avec un "select 1". en effet, l'expérience jusqu'ici
 | |
|     # est que la première connexion après un long timeout échoue
 | |
|     "persistent" => false,
 | |
|     "force_new" => false,
 | |
|     "serial_support" => true,
 | |
|   ];
 | |
| 
 | |
|   const CONFIG = null;
 | |
| 
 | |
|   const MIGRATION = null;
 | |
| 
 | |
|   const params_SCHEMA = [
 | |
|     "dbconn" => ["array"],
 | |
|     "options" => ["?array|callable"],
 | |
|     "replace_config" => ["?array|callable"],
 | |
|     "config" => ["?array|callable"],
 | |
|     "migration" => ["?array|string|callable"],
 | |
|     "auto_open" => ["bool", true],
 | |
|   ];
 | |
| 
 | |
|   const dbconn_SCHEMA = [
 | |
|     "" => "?string",
 | |
|     "host" => "string",
 | |
|     "hostaddr" => "?string",
 | |
|     "port" => "?int",
 | |
|     "dbname" => "string",
 | |
|     "user" => "string",
 | |
|     "password" => "string",
 | |
|     "connect_timeout" => "?int",
 | |
|     "options" => "?string",
 | |
|     "sslmode" => "?string",
 | |
|     "service" => "?string",
 | |
|   ];
 | |
| 
 | |
|   protected const dbconn_MAP = [
 | |
|     "name" => "dbname",
 | |
|     "pass" => "password",
 | |
|   ];
 | |
| 
 | |
|   const options_SCHEMA = [
 | |
|     "persistent" => ["bool", self::OPTIONS["persistent"]],
 | |
|     "force_new" => ["bool", self::OPTIONS["force_new"]],
 | |
|   ];
 | |
| 
 | |
|   function __construct($dbconn=null, ?array $params=null) {
 | |
|     if ($dbconn !== null) {
 | |
|       if (!is_array($dbconn)) {
 | |
|         $dbconn = ["" => $dbconn];
 | |
|         #XXX à terme, il faudra interroger config
 | |
|         #$tmp = config::db($dbconn);
 | |
|         #if ($tmp !== null) $dbconn = $tmp;
 | |
|         #else $dbconn = ["" => $dbconn];
 | |
|       }
 | |
|       unset($dbconn["type"]);
 | |
|       $name = $dbconn["name"] ?? null;
 | |
|       if ($name !== null) {
 | |
|         $dbconn[""] = $name;
 | |
|         unset($dbconn["name"]);
 | |
|       }
 | |
|       $params["dbconn"] = $dbconn;
 | |
|     }
 | |
|     # dbconn
 | |
|     $this->dbconn = $params["dbconn"] ?? null;
 | |
|     # options
 | |
|     $this->options = $params["options"] ?? static::OPTIONS;
 | |
|     # configuration
 | |
|     $config = $params["replace_config"] ?? null;
 | |
|     if ($config === null) {
 | |
|       $config = $params["config"] ?? static::CONFIG;
 | |
|       if (is_callable($config)) $config = [$config];
 | |
|     }
 | |
|     $this->config = $config;
 | |
|     # migrations
 | |
|     $this->migration = $params["migration"] ?? static::MIGRATION;
 | |
|     #
 | |
|     $defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
 | |
|     if ($params["auto_open"] ?? $defaultAutoOpen) {
 | |
|       $this->open();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected ?array $dbconn;
 | |
| 
 | |
|   /** @var array|callable|null */
 | |
|   protected $options;
 | |
| 
 | |
|   /** @var array|string|callable */
 | |
|   protected $config;
 | |
| 
 | |
|   /** @var array|string|callable */
 | |
|   protected $migration;
 | |
| 
 | |
|   /** @var resource */
 | |
|   protected $db = null;
 | |
| 
 | |
|   function getSql($query, ?array $params=null): string {
 | |
|     $query = new _pgsqlQuery($query, $params);
 | |
|     return $query->getSql();
 | |
|   }
 | |
| 
 | |
|   function open(): self {
 | |
|     if ($this->db === null) {
 | |
|       $dbconn = $this->dbconn;
 | |
|       $connection_string = [$dbconn[""] ?? null];
 | |
|       unset($dbconn[""]);
 | |
|       foreach ($dbconn as $key => $value) {
 | |
|         if ($value === null) continue;
 | |
|         $value = strval($value);
 | |
|         if ($value === "" || preg_match("/[ '\\\\]/", $value)) {
 | |
|           $value = str_replace("\\", "\\\\", $value);
 | |
|           $value = str_replace("'", "\\'", $value);
 | |
|           $value = "'$value'";
 | |
|         }
 | |
|         $key = cl::get(self::dbconn_MAP, $key, $key);
 | |
|         $connection_string[] = "$key=$value";
 | |
|       }
 | |
|       $connection_string = implode(" ", array_filter($connection_string));
 | |
|       $options = $this->options;
 | |
|       if (is_callable($options)) {
 | |
|         $options = func::with($options)->bind($this)->invoke();
 | |
|       }
 | |
|       $forceNew = $options["force_new"] ?? false;
 | |
|       $flags = $forceNew? PGSQL_CONNECT_FORCE_NEW: 0;
 | |
| 
 | |
|       if ($options["persistent"] ?? true) $db = pg_pconnect($connection_string, $flags);
 | |
|       else $db = pg_connect($connection_string, $flags);
 | |
|       if ($db === false) throw new PgsqlException("unable to connect");
 | |
|       $this->db = $db;
 | |
| 
 | |
|       _config::with($this->config)->configure($this);
 | |
|       //_migration::with($this->migration)->migrate($this);
 | |
|     }
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   function close(): self {
 | |
|     if ($this->db !== null) {
 | |
|       pg_close($this->db);
 | |
|       $this->db = null;
 | |
|     }
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   protected function db() {
 | |
|     $this->open();
 | |
|     return $this->db;
 | |
|   }
 | |
| 
 | |
|   function _exec(string $query): bool {
 | |
|     $result = pg_query($this->db(), $query);
 | |
|     if ($result === false) return false;
 | |
|     pg_free_result($result);
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   function getLastSerial() {
 | |
|     $db = $this->db();
 | |
|     $result = @pg_query($db, "select lastval()");
 | |
|     if ($result === false) return false;
 | |
|     $lastSerial = pg_fetch_row($result)[0];
 | |
|     pg_free_result($result);
 | |
|     return $lastSerial;
 | |
|   }
 | |
| 
 | |
|   function exec($query, ?array $params=null) {
 | |
|     $db = $this->db();
 | |
|     $query = new _pgsqlQuery($query, $params);
 | |
|     $result = $query->_exec($db);
 | |
|     $serialSupport = $this->options["serial_support"] ?? true;
 | |
|     if ($serialSupport && $query->isInsert()) return $this->getLastSerial();
 | |
|     $affected_rows = pg_affected_rows($result);
 | |
|     pg_free_result($result);
 | |
|     return $affected_rows;
 | |
|   }
 | |
| 
 | |
|   /** @var ITransactor[] */
 | |
|   protected ?array $transactors = null;
 | |
| 
 | |
|   function willUpdate(...$transactors): self {
 | |
|     foreach ($transactors as $transactor) {
 | |
|       if ($transactor instanceof ITransactor) {
 | |
|         $this->transactors[] = $transactor;
 | |
|         $transactor->willUpdate();
 | |
|       } else {
 | |
|         throw ValueException::invalid_type($transactor, ITransactor::class);
 | |
|       }
 | |
|     }
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   function inTransaction(?bool &$inerror=null): bool {
 | |
|     $status = pg_transaction_status($this->db());
 | |
|     if ($status === PGSQL_TRANSACTION_ACTIVE || $status === PGSQL_TRANSACTION_INTRANS) {
 | |
|       $inerror = false;
 | |
|       return true;
 | |
|     } elseif ($status === PGSQL_TRANSACTION_INERROR) {
 | |
|       $inerror = true;
 | |
|       return true;
 | |
|     } else {
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function beginTransaction(?callable $func=null, bool $commit=true): void {
 | |
|     $this->_exec("begin");
 | |
|     if ($this->transactors !== null) {
 | |
|       foreach ($this->transactors as $transactor) {
 | |
|         $transactor->beginTransaction();
 | |
|       }
 | |
|     }
 | |
|     if ($func !== null) {
 | |
|       $commited = false;
 | |
|       try {
 | |
|         func::call($func, $this);
 | |
|         if ($commit) {
 | |
|           $this->commit();
 | |
|           $commited = true;
 | |
|         }
 | |
|       } finally {
 | |
|         if ($commit && !$commited) $this->rollback();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function commit(): void {
 | |
|     $this->_exec("commit");
 | |
|     if ($this->transactors !== null) {
 | |
|       foreach ($this->transactors as $transactor) {
 | |
|         $transactor->commit();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function rollback(): void {
 | |
|     $this->_exec("rollback");
 | |
|     if ($this->transactors !== null) {
 | |
|       foreach ($this->transactors as $transactor) {
 | |
|         $transactor->rollback();
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   function get($query, ?array $params=null, bool $entireRow=false) {
 | |
|     $db = $this->db();
 | |
|     $query = new _pgsqlQuery($query, $params);
 | |
|     $result = $query->_exec($db);
 | |
|     $row = pg_fetch_assoc($result);
 | |
|     pg_free_result($result);
 | |
|     if ($row === false) return null;
 | |
|     $this->verifixRow($row);
 | |
|     if ($entireRow) return $row;
 | |
|     else return cl::first($row);
 | |
|   }
 | |
| 
 | |
|   function one($query, ?array $params=null): ?array {
 | |
|     return $this->get($query, $params, true);
 | |
|   }
 | |
| 
 | |
|   function all($query, ?array $params=null, $primaryKeys=null): iterable {
 | |
|     $db = $this->db();
 | |
|     $query = new _pgsqlQuery($query, $params);
 | |
|     $result = $query->_exec($db);
 | |
|     $primaryKeys = cl::withn($primaryKeys);
 | |
|     while (($row = pg_fetch_assoc($result)) !== false) {
 | |
|       $this->verifixRow($row);
 | |
|       if ($primaryKeys !== null) {
 | |
|         $key = implode("-", cl::select($row, $primaryKeys));
 | |
|         yield $key => $row;
 | |
|       } else {
 | |
|         yield $row;
 | |
|       }
 | |
|     }
 | |
|     pg_free_result($result);
 | |
|   }
 | |
| }
 |