Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
/**
3
 * ADOdb PostgreSQL 6.4 driver
4
 *
5
 * This file is part of ADOdb, a Database Abstraction Layer library for PHP.
6
 *
7
 * @package ADOdb
8
 * @link https://adodb.org Project's web site and documentation
9
 * @link https://github.com/ADOdb/ADOdb Source code and issue tracker
10
 *
11
 * The ADOdb Library is dual-licensed, released under both the BSD 3-Clause
12
 * and the GNU Lesser General Public Licence (LGPL) v2.1 or, at your option,
13
 * any later version. This means you can use it in proprietary products.
14
 * See the LICENSE.md file distributed with this source code for details.
15
 * @license BSD-3-Clause
16
 * @license LGPL-2.1-or-later
17
 *
18
 * @copyright 2000-2013 John Lim
19
 * @copyright 2014 Damien Regad, Mark Newnham and the ADOdb community
20
 */
21
 
22
// security - hide paths
23
if (!defined('ADODB_DIR')) die();
24
 
25
class ADODB_postgres64 extends ADOConnection{
26
	var $databaseType = 'postgres64';
27
	var $dataProvider = 'postgres';
28
	var $hasInsertID = true;
29
	/** @var PgSql\Connection|resource|false */
30
	var $_resultid = false;
31
	var $concat_operator='||';
32
	var $metaDatabasesSQL = "select datname from pg_database where datname not in ('template0','template1') order by 1";
33
	var $metaTablesSQL = "select tablename,'T' from pg_tables where tablename not like 'pg\_%'
34
		and tablename not in ('sql_features', 'sql_implementation_info', 'sql_languages',
35
			'sql_packages', 'sql_sizing', 'sql_sizing_profiles')
36
	union
37
		select viewname,'V' from pg_views where viewname not like 'pg\_%'";
38
	//"select tablename from pg_tables where tablename not like 'pg_%' order by 1";
39
	var $isoDates = true; // accepts dates in ISO format
40
	var $sysDate = "CURRENT_DATE";
41
	var $sysTimeStamp = "CURRENT_TIMESTAMP";
42
	var $blobEncodeType = 'C';
43
	var $metaColumnsSQL = "SELECT a.attname,t.typname,a.attlen,a.atttypmod,a.attnotnull,a.atthasdef,a.attnum
44
		FROM pg_class c, pg_attribute a,pg_type t
45
		WHERE relkind in ('r','v') AND (c.relname='%s' or c.relname = lower('%s')) and a.attname not like '....%%'
46
		AND a.attnum > 0 AND a.atttypid = t.oid AND a.attrelid = c.oid ORDER BY a.attnum";
47
 
48
	// used when schema defined
49
	var $metaColumnsSQL1 = "SELECT a.attname, t.typname, a.attlen, a.atttypmod, a.attnotnull, a.atthasdef, a.attnum
50
		FROM pg_class c, pg_attribute a, pg_type t, pg_namespace n
51
		WHERE relkind in ('r','v') AND (c.relname='%s' or c.relname = lower('%s'))
52
		and c.relnamespace=n.oid and n.nspname='%s'
53
		and a.attname not like '....%%' AND a.attnum > 0
54
		AND a.atttypid = t.oid AND a.attrelid = c.oid ORDER BY a.attnum";
55
 
56
	// get primary key etc -- from Freek Dijkstra
57
	var $metaKeySQL = "SELECT ic.relname AS index_name, a.attname AS column_name,i.indisunique AS unique_key, i.indisprimary AS primary_key
58
		FROM pg_class bc, pg_class ic, pg_index i, pg_attribute a
59
		WHERE bc.oid = i.indrelid AND ic.oid = i.indexrelid
60
		AND (i.indkey[0] = a.attnum OR i.indkey[1] = a.attnum OR i.indkey[2] = a.attnum OR i.indkey[3] = a.attnum OR i.indkey[4] = a.attnum OR i.indkey[5] = a.attnum OR i.indkey[6] = a.attnum OR i.indkey[7] = a.attnum)
61
		AND a.attrelid = bc.oid AND bc.relname = '%s'";
62
 
63
	var $hasAffectedRows = true;
64
	var $hasLimit = false;	// set to true for pgsql 7 only. support pgsql/mysql SELECT * FROM TABLE LIMIT 10
65
	// below suggested by Freek Dijkstra
66
	var $true = 'TRUE';		// string that represents TRUE for a database
67
	var $false = 'FALSE';		// string that represents FALSE for a database
68
	var $fmtDate = "'Y-m-d'";	// used by DBDate() as the default date format used by the database
69
	var $fmtTimeStamp = "'Y-m-d H:i:s'"; // used by DBTimeStamp as the default timestamp fmt.
70
	var $hasMoveFirst = true;
71
	var $hasGenID = true;
72
	var $_genIDSQL = "SELECT NEXTVAL('%s')";
73
	var $_genSeqSQL = "CREATE SEQUENCE %s START %s";
74
	var $_dropSeqSQL = "DROP SEQUENCE %s";
75
	var $metaDefaultsSQL = "SELECT d.adnum as num, d.adsrc as def from pg_attrdef d, pg_class c where d.adrelid=c.oid and c.relname='%s' order by d.adnum";
76
	var $random = 'random()';		/// random function
77
	var $autoRollback = true; // apparently pgsql does not autorollback properly before php 4.3.4
78
							// http://bugs.php.net/bug.php?id=25404
79
 
80
	var $uniqueIisR = true;
81
	var $_bindInputArray = false; // requires postgresql 7.3+ and ability to modify database
82
	var $disableBlobs = false; // set to true to disable blob checking, resulting in 2-5% improvement in performance.
83
 
84
	/** @var int $_pnum Number of the last assigned query parameter {@see param()} */
85
	var $_pnum = 0;
86
 
87
	var $version;
88
	var $_nestedSQL = false;
89
 
90
	// The last (fmtTimeStamp is not entirely correct:
91
	// PostgreSQL also has support for time zones,
92
	// and writes these time in this format: "2001-03-01 18:59:26+02".
93
	// There is no code for the "+02" time zone information, so I just left that out.
94
	// I'm not familiar enough with both ADODB as well as Postgres
95
	// to know what the concequences are. The other values are correct (wheren't in 0.94)
96
	// -- Freek Dijkstra
97
 
98
	/**
99
	 * Retrieve Server information.
100
	 * In addition to server version and description, the function also returns
101
	 * the client version.
102
	 * @param bool $detailed If true, retrieve detailed version string (executes
103
	 *                       a SQL query) in addition to the version number
104
	 * @return array|bool Server info or false if version could not be retrieved
105
	 *                    e.g. if there is no active connection
106
	 */
107
	function ServerInfo($detailed = true)
108
	{
109
		if (empty($this->version['version'])) {
110
			// We don't have a connection, so we can't retrieve server info
111
			if (!$this->_connectionID) {
112
				return false;
113
			}
114
 
115
			$version = pg_version($this->_connectionID);
116
			$this->version = array(
117
				// If PHP has been compiled with PostgreSQL 7.3 or lower, then
118
				// server version is not set so we use pg_parameter_status()
119
				// which includes logic to obtain values server_version
120
				'version' => isset($version['server'])
121
					? $version['server']
122
					: pg_parameter_status($this->_connectionID, 'server_version'),
123
				'client' => $version['client'],
124
				'description' => null,
125
			);
126
		}
127
		if ($detailed && $this->version['description'] === null) {
128
			$this->version['description'] = $this->GetOne('select version()');
129
		}
130
 
131
		return $this->version;
132
	}
133
 
134
	function IfNull( $field, $ifNull )
135
	{
136
		return " coalesce($field, $ifNull) ";
137
	}
138
 
139
	// get the last id - never tested
140
	function pg_insert_id($tablename,$fieldname)
141
	{
142
		$result=pg_query($this->_connectionID, 'SELECT last_value FROM '. $tablename .'_'. $fieldname .'_seq');
143
		if ($result) {
144
			$arr = @pg_fetch_row($result,0);
145
			pg_free_result($result);
146
			if (isset($arr[0])) return $arr[0];
147
		}
148
		return false;
149
	}
150
 
151
	/**
152
	 * Warning from http://www.php.net/manual/function.pg-getlastoid.php:
153
	 * Using a OID as a unique identifier is not generally wise.
154
	 * Unless you are very careful, you might end up with a tuple having
155
	 * a different OID if a database must be reloaded.
156
	 *
157
	 * @inheritDoc
158
	 */
159
	protected function _insertID($table = '', $column = '')
160
	{
161
		if ($this->_resultid === false) return false;
162
		$oid = pg_last_oid($this->_resultid);
163
		// to really return the id, we need the table and column-name, else we can only return the oid != id
164
		return empty($table) || empty($column) ? $oid : $this->GetOne("SELECT $column FROM $table WHERE oid=".(int)$oid);
165
	}
166
 
167
	function _affectedrows()
168
	{
169
		if ($this->_resultid === false) return false;
170
		return pg_affected_rows($this->_resultid);
171
	}
172
 
173
 
174
	/**
175
	 * @return bool
176
	 */
177
	function BeginTrans()
178
	{
179
		if ($this->transOff) return true;
180
		$this->transCnt += 1;
181
		return pg_query($this->_connectionID, 'begin '.$this->_transmode);
182
	}
183
 
184
	function RowLock($tables,$where,$col='1 as adodbignore')
185
	{
186
		if (!$this->transCnt) $this->BeginTrans();
187
		return $this->GetOne("select $col from $tables where $where for update");
188
	}
189
 
190
	// returns true/false.
191
	function CommitTrans($ok=true)
192
	{
193
		if ($this->transOff) return true;
194
		if (!$ok) return $this->RollbackTrans();
195
 
196
		$this->transCnt -= 1;
197
		return pg_query($this->_connectionID, 'commit');
198
	}
199
 
200
	// returns true/false
201
	function RollbackTrans()
202
	{
203
		if ($this->transOff) return true;
204
		$this->transCnt -= 1;
205
		return pg_query($this->_connectionID, 'rollback');
206
	}
207
 
208
	function MetaTables($ttype=false,$showSchema=false,$mask=false)
209
	{
210
		$info = $this->ServerInfo();
211
		if ($info['version'] >= 7.3) {
212
		$this->metaTablesSQL = "
213
			select table_name,'T' from information_schema.tables where table_schema not in ( 'pg_catalog','information_schema')
214
			union
215
			select table_name,'V' from information_schema.views where table_schema not in ( 'pg_catalog','information_schema') ";
216
		}
217
		if ($mask) {
218
			$save = $this->metaTablesSQL;
219
			$mask = $this->qstr(strtolower($mask));
220
			if ($info['version']>=7.3)
221
				$this->metaTablesSQL = "
222
					select table_name,'T' from information_schema.tables where table_name like $mask and table_schema not in ( 'pg_catalog','information_schema')
223
					union
224
					select table_name,'V' from information_schema.views where table_name like $mask and table_schema not in ( 'pg_catalog','information_schema') ";
225
			else
226
				$this->metaTablesSQL = "
227
					select tablename,'T' from pg_tables where tablename like $mask
228
					union
229
					select viewname,'V' from pg_views where viewname like $mask";
230
		}
231
		$ret = ADOConnection::MetaTables($ttype,$showSchema);
232
 
233
		if ($mask) {
234
			$this->metaTablesSQL = $save;
235
		}
236
		return $ret;
237
	}
238
 
239
 
240
	/**
241
	 * Quotes a string to be sent to the database.
242
	 *
243
	 * Relies on pg_escape_string()
244
	 * @link https://adodb.org/dokuwiki/doku.php?id=v5:reference:connection:qstr
245
	 *
246
	 * @param string $s            The string to quote
247
	 * @param bool   $magic_quotes This param is not used since 5.21.0.
248
	 *                             It remains for backwards compatibility.
249
	 *
250
	 * @return string Quoted string
251
	 */
252
	function qStr($s, $magic_quotes=false)
253
	{
254
		if (is_bool($s)) {
255
			return $s ? 'true' : 'false';
256
		}
257
 
258
		if ($this->_connectionID) {
259
			return "'" . pg_escape_string($this->_connectionID, $s) . "'";
260
		} else {
261
			// Fall back to emulated escaping when there is no database connection.
262
			// Avoids errors when using setSessionVariables() in the load balancer.
263
			return parent::qStr( $s );
264
		}
265
	}
266
 
267
 
268
	// Format date column in sql string given an input format that understands Y M D
269
	function SQLDate($fmt, $col=false)
270
	{
271
		if (!$col) $col = $this->sysTimeStamp;
272
		$s = 'TO_CHAR('.$col.",'";
273
 
274
		$len = strlen($fmt);
275
		for ($i=0; $i < $len; $i++) {
276
			$ch = $fmt[$i];
277
			switch($ch) {
278
			case 'Y':
279
			case 'y':
280
				$s .= 'YYYY';
281
				break;
282
			case 'Q':
283
			case 'q':
284
				$s .= 'Q';
285
				break;
286
 
287
			case 'M':
288
				$s .= 'Mon';
289
				break;
290
 
291
			case 'm':
292
				$s .= 'MM';
293
				break;
294
			case 'D':
295
			case 'd':
296
				$s .= 'DD';
297
				break;
298
 
299
			case 'H':
300
				$s.= 'HH24';
301
				break;
302
 
303
			case 'h':
304
				$s .= 'HH';
305
				break;
306
 
307
			case 'i':
308
				$s .= 'MI';
309
				break;
310
 
311
			case 's':
312
				$s .= 'SS';
313
				break;
314
 
315
			case 'a':
316
			case 'A':
317
				$s .= 'AM';
318
				break;
319
 
320
			case 'w':
321
				$s .= 'D';
322
				break;
323
 
324
			case 'l':
325
				$s .= 'DAY';
326
				break;
327
 
328
			case 'W':
329
				$s .= 'WW';
330
				break;
331
 
332
			default:
333
			// handle escape characters...
334
				if ($ch == '\\') {
335
					$i++;
336
					$ch = substr($fmt,$i,1);
337
				}
338
				if (strpos('-/.:;, ',$ch) !== false) $s .= $ch;
339
				else $s .= '"'.$ch.'"';
340
 
341
			}
342
		}
343
		return $s. "')";
344
	}
345
 
346
 
347
 
348
	/*
349
	* Load a Large Object from a file
350
	* - the procedure stores the object id in the table and imports the object using
351
	* postgres proprietary blob handling routines
352
	*
353
	* contributed by Mattia Rossi mattia@technologist.com
354
	* modified for safe mode by juraj chlebec
355
	*/
356
	function UpdateBlobFile($table,$column,$path,$where,$blobtype='BLOB')
357
	{
358
		pg_query($this->_connectionID, 'begin');
359
 
360
		$fd = fopen($path,'r');
361
		$contents = fread($fd,filesize($path));
362
		fclose($fd);
363
 
364
		$oid = pg_lo_create($this->_connectionID);
365
		$handle = pg_lo_open($this->_connectionID, $oid, 'w');
366
		pg_lo_write($handle, $contents);
367
		pg_lo_close($handle);
368
 
369
		// $oid = pg_lo_import ($path);
370
		pg_query($this->_connectionID, 'commit');
371
		$rs = ADOConnection::UpdateBlob($table,$column,$oid,$where,$blobtype);
372
		$rez = !empty($rs);
373
		return $rez;
374
	}
375
 
376
	/*
377
	* Deletes/Unlinks a Blob from the database, otherwise it
378
	* will be left behind
379
	*
380
	* Returns TRUE on success or FALSE on failure.
381
	*
382
	* contributed by Todd Rogers todd#windfox.net
383
	*/
384
	function BlobDelete( $blob )
385
	{
386
		pg_query($this->_connectionID, 'begin');
387
		$result = @pg_lo_unlink($this->_connectionID, $blob);
388
		pg_query($this->_connectionID, 'commit');
389
		return( $result );
390
	}
391
 
392
	/*
393
		Heuristic - not guaranteed to work.
394
	*/
395
	function GuessOID($oid)
396
	{
397
		if (strlen($oid)>16) return false;
398
		return is_numeric($oid);
399
	}
400
 
401
	/*
402
	* If an OID is detected, then we use pg_lo_* to open the oid file and read the
403
	* real blob from the db using the oid supplied as a parameter. If you are storing
404
	* blobs using bytea, we autodetect and process it so this function is not needed.
405
	*
406
	* contributed by Mattia Rossi mattia@technologist.com
407
	*
408
	* see http://www.postgresql.org/idocs/index.php?largeobjects.html
409
	*
410
	* Since adodb 4.54, this returns the blob, instead of sending it to stdout. Also
411
	* added maxsize parameter, which defaults to $db->maxblobsize if not defined.
412
	*/
413
	function BlobDecode($blob,$maxsize=false,$hastrans=true)
414
	{
415
		if (!$this->GuessOID($blob)) return $blob;
416
 
417
		if ($hastrans) pg_query($this->_connectionID,'begin');
418
		$fd = @pg_lo_open($this->_connectionID,$blob,'r');
419
		if ($fd === false) {
420
			if ($hastrans) pg_query($this->_connectionID,'commit');
421
			return $blob;
422
		}
423
		if (!$maxsize) $maxsize = $this->maxblobsize;
424
		$realblob = @pg_lo_read($fd,$maxsize);
425
		@pg_lo_close($fd);
426
		if ($hastrans) pg_query($this->_connectionID,'commit');
427
		return $realblob;
428
	}
429
 
430
	/**
431
	 * Encode binary value prior to DB storage.
432
	 *
433
	 * See https://www.postgresql.org/docs/current/static/datatype-binary.html
434
	 *
435
	 * NOTE: SQL string literals (input strings) must be preceded with two
436
	 * backslashes due to the fact that they must pass through two parsers in
437
	 * the PostgreSQL backend.
438
	 *
439
	 * @param string $blob
440
	 */
441
	function BlobEncode($blob)
442
	{
443
		return pg_escape_bytea($this->_connectionID, $blob);
444
	}
445
 
446
	// assumes bytea for blob, and varchar for clob
447
	function UpdateBlob($table,$column,$val,$where,$blobtype='BLOB')
448
	{
449
		if ($blobtype == 'CLOB') {
450
			return $this->Execute("UPDATE $table SET $column=" . $this->qstr($val) . " WHERE $where");
451
		}
452
		// do not use bind params which uses qstr(), as blobencode() already quotes data
453
		return $this->Execute("UPDATE $table SET $column='".$this->BlobEncode($val)."'::bytea WHERE $where");
454
	}
455
 
456
	function OffsetDate($dayFraction,$date=false)
457
	{
458
		if (!$date) $date = $this->sysDate;
459
		else if (strncmp($date,"'",1) == 0) {
460
			$len = strlen($date);
461
			if (10 <= $len && $len <= 12) $date = 'date '.$date;
462
			else $date = 'timestamp '.$date;
463
		}
464
 
465
 
466
		return "($date+interval'".($dayFraction * 1440)." minutes')";
467
		#return "($date+interval'$dayFraction days')";
468
	}
469
 
470
	/**
471
	 * Generate the SQL to retrieve MetaColumns data
472
	 * @param string $table Table name
473
	 * @param string $schema Schema name (can be blank)
474
	 * @return string SQL statement to execute
475
	 */
476
	protected function _generateMetaColumnsSQL($table, $schema)
477
	{
478
		if ($schema) {
479
			return sprintf($this->metaColumnsSQL1, $table, $table, $schema);
480
		}
481
		else {
482
			return sprintf($this->metaColumnsSQL, $table, $table, $schema);
483
		}
484
	}
485
 
486
	// for schema support, pass in the $table param "$schema.$tabname".
487
	// converts field names to lowercase, $upper is ignored
488
	// see PHPLens Issue No: 14018 for more info
489
	function MetaColumns($table,$normalize=true)
490
	{
491
		global $ADODB_FETCH_MODE;
492
 
493
		$schema = false;
494
		$false = false;
495
		$this->_findschema($table,$schema);
496
 
497
		if ($normalize) $table = strtolower($table);
498
 
499
		$save = $ADODB_FETCH_MODE;
500
		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
501
		if ($this->fetchMode !== false) $savem = $this->SetFetchMode(false);
502
 
503
		$rs = $this->Execute($this->_generateMetaColumnsSQL($table, $schema));
504
		if (isset($savem)) $this->SetFetchMode($savem);
505
		$ADODB_FETCH_MODE = $save;
506
 
507
		if ($rs === false) {
508
			return $false;
509
		}
510
		if (!empty($this->metaKeySQL)) {
511
			// If we want the primary keys, we have to issue a separate query
512
			// Of course, a modified version of the metaColumnsSQL query using a
513
			// LEFT JOIN would have been much more elegant, but postgres does
514
			// not support OUTER JOINS. So here is the clumsy way.
515
 
516
			$ADODB_FETCH_MODE = ADODB_FETCH_ASSOC;
517
 
518
			$rskey = $this->Execute(sprintf($this->metaKeySQL,($table)));
519
			// fetch all result in once for performance.
520
			$keys = $rskey->GetArray();
521
			if (isset($savem)) $this->SetFetchMode($savem);
522
			$ADODB_FETCH_MODE = $save;
523
 
524
			$rskey->Close();
525
			unset($rskey);
526
		}
527
 
528
		$rsdefa = array();
529
		if (!empty($this->metaDefaultsSQL)) {
530
			$ADODB_FETCH_MODE = ADODB_FETCH_ASSOC;
531
			$sql = sprintf($this->metaDefaultsSQL, ($table));
532
			$rsdef = $this->Execute($sql);
533
			if (isset($savem)) $this->SetFetchMode($savem);
534
			$ADODB_FETCH_MODE = $save;
535
 
536
			if ($rsdef) {
537
				while (!$rsdef->EOF) {
538
					$num = $rsdef->fields['num'];
539
					$s = $rsdef->fields['def'];
540
					if (strpos($s,'::')===false && substr($s, 0, 1) == "'") { /* quoted strings hack... for now... fixme */
541
						$s = substr($s, 1);
542
						$s = substr($s, 0, strlen($s) - 1);
543
					}
544
 
545
					$rsdefa[$num] = $s;
546
					$rsdef->MoveNext();
547
				}
548
			} else {
549
				ADOConnection::outp( "==> SQL => " . $sql);
550
			}
551
			unset($rsdef);
552
		}
553
 
554
		$retarr = array();
555
		while (!$rs->EOF) {
556
			$fld = new ADOFieldObject();
557
			$fld->name = $rs->fields[0];
558
			$fld->type = $rs->fields[1];
559
			$fld->max_length = $rs->fields[2];
560
			$fld->attnum = $rs->fields[6];
561
 
562
			if ($fld->max_length <= 0) $fld->max_length = $rs->fields[3]-4;
563
			if ($fld->max_length <= 0) $fld->max_length = -1;
564
			if ($fld->type == 'numeric') {
565
				$fld->scale = $fld->max_length & 0xFFFF;
566
				$fld->max_length >>= 16;
567
			}
568
			// dannym
569
			// 5 hasdefault; 6 num-of-column
570
			$fld->has_default = ($rs->fields[5] == 't');
571
			if ($fld->has_default) {
572
				$fld->default_value = $rsdefa[$rs->fields[6]];
573
			}
574
 
575
			//Freek
576
			$fld->not_null = $rs->fields[4] == 't';
577
 
578
 
579
			// Freek
580
			if (is_array($keys)) {
581
				foreach($keys as $key) {
582
					if ($fld->name == $key['column_name'] AND $key['primary_key'] == 't')
583
						$fld->primary_key = true;
584
					if ($fld->name == $key['column_name'] AND $key['unique_key'] == 't')
585
						$fld->unique = true; // What name is more compatible?
586
				}
587
			}
588
 
589
			if ($ADODB_FETCH_MODE == ADODB_FETCH_NUM) $retarr[] = $fld;
590
			else $retarr[($normalize) ? strtoupper($fld->name) : $fld->name] = $fld;
591
 
592
			$rs->MoveNext();
593
		}
594
		$rs->Close();
595
		if (empty($retarr))
596
			return  $false;
597
		else
598
			return $retarr;
599
 
600
	}
601
 
602
	function param($name, $type='C')
603
	{
604
		if (!$name) {
605
			// Reset parameter number if $name is falsy
606
			$this->_pnum = 0;
607
			if ($name === false) {
608
				// and don't return placeholder if false (see #380)
609
				return '';
610
			}
611
		}
612
 
613
		return '$' . ++$this->_pnum;
614
	}
615
 
616
	function MetaIndexes ($table, $primary = FALSE, $owner = false)
617
	{
618
		global $ADODB_FETCH_MODE;
619
 
620
		$schema = false;
621
		$this->_findschema($table,$schema);
622
 
623
		if ($schema) { // requires pgsql 7.3+ - pg_namespace used.
624
			$sql = '
625
				SELECT c.relname as "Name", i.indisunique as "Unique", i.indkey as "Columns"
626
				FROM pg_catalog.pg_class c
627
				JOIN pg_catalog.pg_index i ON i.indexrelid=c.oid
628
				JOIN pg_catalog.pg_class c2 ON c2.oid=i.indrelid
629
					,pg_namespace n
630
				WHERE (c2.relname=\'%s\' or c2.relname=lower(\'%s\'))
631
				and c.relnamespace=c2.relnamespace
632
				and c.relnamespace=n.oid
633
				and n.nspname=\'%s\'';
634
		} else {
635
			$sql = '
636
				SELECT c.relname as "Name", i.indisunique as "Unique", i.indkey as "Columns"
637
				FROM pg_catalog.pg_class c
638
				JOIN pg_catalog.pg_index i ON i.indexrelid=c.oid
639
				JOIN pg_catalog.pg_class c2 ON c2.oid=i.indrelid
640
				WHERE (c2.relname=\'%s\' or c2.relname=lower(\'%s\'))';
641
		}
642
 
643
		if ($primary == FALSE) {
644
			$sql .= ' AND i.indisprimary=false;';
645
		}
646
 
647
		$save = $ADODB_FETCH_MODE;
648
		$ADODB_FETCH_MODE = ADODB_FETCH_NUM;
649
		if ($this->fetchMode !== FALSE) {
650
			$savem = $this->SetFetchMode(FALSE);
651
		}
652
 
653
		$rs = $this->Execute(sprintf($sql,$table,$table,$schema));
654
		if (isset($savem)) {
655
			$this->SetFetchMode($savem);
656
		}
657
		$ADODB_FETCH_MODE = $save;
658
 
659
		if (!is_object($rs)) {
660
			$false = false;
661
			return $false;
662
		}
663
 
664
		// Get column names indexed by attnum so we can lookup the index key
665
		$col_names = $this->MetaColumnNames($table,true,true);
666
		$indexes = array();
667
		while ($row = $rs->FetchRow()) {
668
			$columns = array();
669
			foreach (explode(' ', $row[2]) as $col) {
670
				// When index attribute (pg_index.indkey) is an expression, $col == 0
671
				// @see https://www.postgresql.org/docs/current/catalog-pg-index.html
672
				// so there is no matching column name - set it to null (see #940).
673
				$columns[] = $col_names[$col] ?? null;
674
			}
675
 
676
			$indexes[$row[0]] = array(
677
				'unique' => ($row[1] == 't'),
678
				'columns' => $columns
679
			);
680
		}
681
		return $indexes;
682
	}
683
 
684
	/**
685
	 * Connect to a database.
686
	 *
687
	 * Examples:
688
	 *   $db->Connect("host=host1 user=user1 password=secret port=4341");
689
	 *   $db->Connect('host1:4341', 'user1', 'secret');
690
	 *
691
	 * @param string $str  pg_connect() Connection string or Hostname[:port]
692
	 * @param string $user (Optional) The username to connect as.
693
	 * @param string $pwd  (Optional) The password to connect with.
694
	 * @param string $db   (Optional) The name of the database to start in when connected.
695
	 * @param int $ctype   Connection type
696
	 * @return bool|null   True if connected successfully, false if connection failed, or
697
	 *                     null if the PostgreSQL extension is not loaded.
698
	 */
699
	function _connect($str, $user='', $pwd='', $db='', $ctype=0)
700
	{
701
		if (!function_exists('pg_connect')) {
702
			return null;
703
		}
704
 
705
		$this->_errorMsg = false;
706
 
707
		// If $user, $pwd and $db are all null, then $str is a pg_connect()
708
		// connection string. Otherwise we expect it to be a hostname,
709
		// with optional port separated by ':'
710
		if ($user || $pwd || $db) {
711
			// Hostname & port
712
			if ($str) {
713
				$host = explode(':', $str);
714
				if ($host[0]) {
715
					$conn['host'] = $host[0];
716
				}
717
				if (isset($host[1])) {
718
					$conn['port'] = (int)$host[1];
719
				} elseif (!empty($this->port)) {
720
					$conn['port'] = $this->port;
721
				}
722
			}
723
			$conn['user'] = $user;
724
			$conn['password'] = $pwd;
725
			// @TODO not sure why we default to 'template1', pg_connect() uses the username when dbname is empty
726
			$conn['dbname'] = $db ?: 'template1';
727
 
728
			// Generate connection string
729
			$str = '';
730
			foreach ($conn as $param => $value) {
731
				// Escaping single quotes and backslashes per pg_connect() documentation
732
				$str .= $param . "='" . addcslashes($value, "'\\") . "' ";
733
			}
734
		}
735
 
736
		if ($ctype === 1) { // persistent
737
			$this->_connectionID = pg_pconnect($str);
738
		} else {
739
			if ($ctype === -1) { // nconnect, we trick pgsql ext by changing the connection str
740
				static $ncnt;
741
 
742
				if (empty($ncnt)) $ncnt = 1;
743
				else $ncnt += 1;
744
 
745
				$str .= str_repeat(' ',$ncnt);
746
			}
747
			$this->_connectionID = pg_connect($str);
748
		}
749
		if ($this->_connectionID === false) return false;
750
		$this->Execute("set datestyle='ISO'");
751
 
752
		$info = $this->ServerInfo(false);
753
 
754
		if (version_compare($info['version'], '7.1', '>=')) {
755
			$this->_nestedSQL = true;
756
		}
757
 
758
		# PostgreSQL 9.0 changed the default output for bytea from 'escape' to 'hex'
759
		# PHP does not handle 'hex' properly ('x74657374' is returned as 't657374')
760
		# https://bugs.php.net/bug.php?id=59831 states this is in fact not a bug,
761
		# so we manually set bytea_output
762
		if (version_compare($info['version'], '9.0', '>=')
763
			&& version_compare($info['client'], '9.2', '<')
764
		) {
765
			$this->Execute('set bytea_output=escape');
766
		}
767
 
768
		return true;
769
	}
770
 
771
	function _nconnect($argHostname, $argUsername, $argPassword, $argDatabaseName)
772
	{
773
		return $this->_connect($argHostname, $argUsername, $argPassword, $argDatabaseName,-1);
774
	}
775
 
776
	// returns true or false
777
	//
778
	// examples:
779
	// 	$db->PConnect("host=host1 user=user1 password=secret port=4341");
780
	// 	$db->PConnect('host1','user1','secret');
781
	function _pconnect($str,$user='',$pwd='',$db='')
782
	{
783
		return $this->_connect($str,$user,$pwd,$db,1);
784
	}
785
 
786
 
787
	function _query($sql,$inputarr=false)
788
	{
789
		$this->_pnum = 0;
790
		$this->_errorMsg = false;
791
		if ($inputarr) {
792
		/*
793
			It appears that PREPARE/EXECUTE is slower for many queries.
794
 
795
			For query executed 1000 times:
796
			"select id,firstname,lastname from adoxyz
797
				where firstname not like ? and lastname not like ? and id = ?"
798
 
799
			with plan = 1.51861286163 secs
800
			no plan =   1.26903700829 secs
801
		*/
802
			$plan = 'P'.md5($sql);
803
 
804
			$execp = '';
805
			foreach($inputarr as $v) {
806
				if ($execp) $execp .= ',';
807
				if (is_string($v)) {
808
					if (strncmp($v,"'",1) !== 0) $execp .= $this->qstr($v);
809
				} else {
810
					$execp .= $v;
811
				}
812
			}
813
 
814
			if ($execp) $exsql = "EXECUTE $plan ($execp)";
815
			else $exsql = "EXECUTE $plan";
816
 
817
			$rez = @pg_query($this->_connectionID, $exsql);
818
			if (!$rez) {
819
			# Perhaps plan does not exist? Prepare/compile plan.
820
				$params = '';
821
				foreach($inputarr as $v) {
822
					if ($params) $params .= ',';
823
					if (is_string($v)) {
824
						$params .= 'VARCHAR';
825
					} else if (is_integer($v)) {
826
						$params .= 'INTEGER';
827
					} else {
828
						$params .= "REAL";
829
					}
830
				}
831
				$sqlarr = explode('?',$sql);
832
				//print_r($sqlarr);
833
				$sql = '';
834
				$i = 1;
835
				foreach($sqlarr as $v) {
836
					$sql .= $v.' $'.$i;
837
					$i++;
838
				}
839
				$s = "PREPARE $plan ($params) AS ".substr($sql,0,strlen($sql)-2);
840
				//adodb_pr($s);
841
				$rez = pg_query($this->_connectionID, $s);
842
				//echo $this->ErrorMsg();
843
			}
844
			if ($rez)
845
				$rez = pg_query($this->_connectionID, $exsql);
846
		} else {
847
			//adodb_backtrace();
848
			$rez = pg_query($this->_connectionID, $sql);
849
		}
850
		// check if no data returned, then no need to create real recordset
851
		if ($rez && pg_num_fields($rez) <= 0) {
852
			if ($this->_resultid !== false) {
853
				pg_free_result($this->_resultid);
854
			}
855
			$this->_resultid = $rez;
856
			return true;
857
		}
858
 
859
		return $rez;
860
	}
861
 
862
	function _errconnect()
863
	{
864
		if (defined('DB_ERROR_CONNECT_FAILED')) return DB_ERROR_CONNECT_FAILED;
865
		else return 'Database connection failed';
866
	}
867
 
868
	/*	Returns: the last error message from previous database operation	*/
869
	function ErrorMsg()
870
	{
871
		if ($this->_errorMsg !== false) {
872
			return $this->_errorMsg;
873
		}
874
 
875
		if (!empty($this->_resultid)) {
876
			$this->_errorMsg = @pg_result_error($this->_resultid);
877
			if ($this->_errorMsg) {
878
				return $this->_errorMsg;
879
			}
880
		}
881
 
882
		if (!empty($this->_connectionID)) {
883
			$this->_errorMsg = @pg_last_error($this->_connectionID);
884
		} else {
885
			$this->_errorMsg = $this->_errconnect();
886
		}
887
 
888
		return $this->_errorMsg;
889
	}
890
 
891
	function ErrorNo()
892
	{
893
		$e = $this->ErrorMsg();
894
		if (strlen($e)) {
895
			return ADOConnection::MetaError($e);
896
		}
897
		return 0;
898
	}
899
 
900
	// returns true or false
901
	function _close()
902
	{
903
		if ($this->transCnt) $this->RollbackTrans();
904
		if ($this->_resultid) {
905
			@pg_free_result($this->_resultid);
906
			$this->_resultid = false;
907
		}
908
		@pg_close($this->_connectionID);
909
		$this->_connectionID = false;
910
		return true;
911
	}
912
 
913
 
914
	/*
915
	* Maximum size of C field
916
	*/
917
	function CharMax()
918
	{
919
		return 1000000000;  // should be 1 Gb?
920
	}
921
 
922
	/*
923
	* Maximum size of X field
924
	*/
925
	function TextMax()
926
	{
927
		return 1000000000; // should be 1 Gb?
928
	}
929
 
930
 
931
}
932
 
933
/*--------------------------------------------------------------------------------------
934
	Class Name: Recordset
935
--------------------------------------------------------------------------------------*/
936
 
937
class ADORecordSet_postgres64 extends ADORecordSet{
938
	var $_blobArr;
939
	var $databaseType = "postgres64";
940
	var $canSeek = true;
941
 
942
	function __construct($queryID, $mode=false)
943
	{
944
		if ($mode === false) {
945
			global $ADODB_FETCH_MODE;
946
			$mode = $ADODB_FETCH_MODE;
947
		}
948
		switch ($mode)
949
		{
950
		case ADODB_FETCH_NUM: $this->fetchMode = PGSQL_NUM; break;
951
		case ADODB_FETCH_ASSOC:$this->fetchMode = PGSQL_ASSOC; break;
952
 
953
		case ADODB_FETCH_DEFAULT:
954
		case ADODB_FETCH_BOTH:
955
		default: $this->fetchMode = PGSQL_BOTH; break;
956
		}
957
		$this->adodbFetchMode = $mode;
958
 
959
		// Parent's constructor
960
		parent::__construct($queryID);
961
	}
962
 
963
	function GetRowAssoc($upper = ADODB_ASSOC_CASE)
964
	{
965
		if ($this->fetchMode == PGSQL_ASSOC && $upper == ADODB_ASSOC_CASE_LOWER) {
966
			return $this->fields;
967
		}
968
		$row = ADORecordSet::GetRowAssoc($upper);
969
		return $row;
970
	}
971
 
972
	function _initRS()
973
	{
974
		global $ADODB_COUNTRECS;
975
		$qid = $this->_queryID;
976
		$this->_numOfRows = ($ADODB_COUNTRECS)? @pg_num_rows($qid):-1;
977
		$this->_numOfFields = @pg_num_fields($qid);
978
 
979
		// cache types for blob decode check
980
		// apparently pg_field_type actually performs an sql query on the database to get the type.
981
		if (empty($this->connection->noBlobs))
982
		for ($i=0, $max = $this->_numOfFields; $i < $max; $i++) {
983
			if (pg_field_type($qid,$i) == 'bytea') {
984
				$this->_blobArr[$i] = pg_field_name($qid,$i);
985
			}
986
		}
987
	}
988
 
989
	function fields($colname)
990
	{
991
		if ($this->fetchMode != PGSQL_NUM) {
992
			return @$this->fields[$colname];
993
		}
994
 
995
		if (!$this->bind) {
996
			$this->bind = array();
997
			for ($i=0; $i < $this->_numOfFields; $i++) {
998
				$o = $this->FetchField($i);
999
				$this->bind[strtoupper($o->name)] = $i;
1000
			}
1001
		}
1002
		return $this->fields[$this->bind[strtoupper($colname)]];
1003
	}
1004
 
1005
	function fetchField($fieldOffset = 0)
1006
	{
1007
		// offsets begin at 0
1008
 
1009
		$o = new ADOFieldObject();
1010
		$o->name = @pg_field_name($this->_queryID, $fieldOffset);
1011
		$o->type = @pg_field_type($this->_queryID, $fieldOffset);
1012
		$o->max_length = @pg_field_size($this->_queryID, $fieldOffset);
1013
		return $o;
1014
	}
1015
 
1016
	function _seek($row)
1017
	{
1018
		return @pg_fetch_row($this->_queryID,$row);
1019
	}
1020
 
1021
	function _decode($blob)
1022
	{
1023
		if ($blob === NULL) return NULL;
1024
//		eval('$realblob="'.str_replace(array('"','$'),array('\"','\$'),$blob).'";');
1025
		return pg_unescape_bytea($blob);
1026
	}
1027
 
1028
	/**
1029
	 * Fetches and prepares the RecordSet's fields.
1030
	 *
1031
	 * Fixes the blobs if there are any.
1032
	 */
1033
	protected function _prepFields()
1034
	{
1035
		$this->fields = @pg_fetch_array($this->_queryID,$this->_currentRow,$this->fetchMode);
1036
 
1037
		// Check prerequisites and bail early if we do not have what we need.
1038
		if (!isset($this->_blobArr) || $this->fields === false) {
1039
			return;
1040
		}
1041
 
1042
		if ($this->fetchMode == PGSQL_NUM || $this->fetchMode == PGSQL_BOTH) {
1043
			foreach($this->_blobArr as $k => $v) {
1044
				$this->fields[$k] = ADORecordSet_postgres64::_decode($this->fields[$k]);
1045
			}
1046
		}
1047
		if ($this->fetchMode == PGSQL_ASSOC || $this->fetchMode == PGSQL_BOTH) {
1048
			foreach($this->_blobArr as $k => $v) {
1049
				$this->fields[$v] = ADORecordSet_postgres64::_decode($this->fields[$v]);
1050
			}
1051
		}
1052
	}
1053
 
1054
	// 10% speedup to move MoveNext to child class
1055
	function MoveNext()
1056
	{
1057
		if (!$this->EOF) {
1058
			$this->_currentRow++;
1059
			if ($this->_numOfRows < 0 || $this->_numOfRows > $this->_currentRow) {
1060
				$this->_prepfields();
1061
				if ($this->fields !== false) {
1062
					return true;
1063
				}
1064
			}
1065
			$this->fields = false;
1066
			$this->EOF = true;
1067
		}
1068
		return false;
1069
	}
1070
 
1071
	function _fetch()
1072
	{
1073
		if ($this->_currentRow >= $this->_numOfRows && $this->_numOfRows >= 0) {
1074
			return false;
1075
		}
1076
 
1077
		$this->_prepfields();
1078
		return $this->fields !== false;
1079
	}
1080
 
1081
	function _close()
1082
	{
1083
		if ($this->_queryID === false || $this->_queryID == self::DUMMY_QUERY_ID) {
1084
			return true;
1085
		}
1086
		return pg_free_result($this->_queryID);
1087
	}
1088
 
1089
	function MetaType($t,$len=-1,$fieldobj=false)
1090
	{
1091
		if (is_object($t)) {
1092
			$fieldobj = $t;
1093
			$t = $fieldobj->type;
1094
			$len = $fieldobj->max_length;
1095
		}
1096
 
1097
		$t = strtoupper($t);
1098
 
1099
		if (array_key_exists($t,$this->connection->customActualTypes))
1100
			return  $this->connection->customActualTypes[$t];
1101
 
1102
		switch ($t) {
1103
				case 'MONEY': // stupid, postgres expects money to be a string
1104
				case 'INTERVAL':
1105
				case 'CHAR':
1106
				case 'CHARACTER':
1107
				case 'VARCHAR':
1108
				case 'NAME':
1109
				case 'BPCHAR':
1110
				case '_VARCHAR':
1111
				case 'CIDR':
1112
				case 'INET':
1113
				case 'MACADDR':
1114
				case 'UUID':
1115
					if ($len <= $this->blobSize) return 'C';
1116
 
1117
				case 'TEXT':
1118
					return 'X';
1119
 
1120
				case 'IMAGE': // user defined type
1121
				case 'BLOB': // user defined type
1122
				case 'BIT':	// This is a bit string, not a single bit, so don't return 'L'
1123
				case 'VARBIT':
1124
				case 'BYTEA':
1125
					return 'B';
1126
 
1127
				case 'BOOL':
1128
				case 'BOOLEAN':
1129
					return 'L';
1130
 
1131
				case 'DATE':
1132
					return 'D';
1133
 
1134
 
1135
				case 'TIMESTAMP WITHOUT TIME ZONE':
1136
				case 'TIME':
1137
				case 'DATETIME':
1138
				case 'TIMESTAMP':
1139
				case 'TIMESTAMPTZ':
1140
					return 'T';
1141
 
1142
				case 'SMALLINT':
1143
				case 'BIGINT':
1144
				case 'INTEGER':
1145
				case 'INT8':
1146
				case 'INT4':
1147
				case 'INT2':
1148
					if (isset($fieldobj) &&
1149
				empty($fieldobj->primary_key) && (!$this->connection->uniqueIisR || empty($fieldobj->unique))) return 'I';
1150
 
1151
				case 'OID':
1152
				case 'SERIAL':
1153
					return 'R';
1154
 
1155
				case 'NUMERIC':
1156
				case 'DECIMAL':
1157
				case 'FLOAT4':
1158
				case 'FLOAT8':
1159
					return 'N';
1160
 
1161
				default:
1162
					return ADODB_DEFAULT_METATYPE;
1163
			}
1164
	}
1165
 
1166
}