Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
/**
3
 * TPgsqlMetaData class file.
4
 *
5
 * @author Wei Zhuo <weizhuo[at]gmail[dot]com>
6
 * @link http://www.pradosoft.com/
7
 * @copyright Copyright &copy; 2005-2008 PradoSoft
8
 * @license http://www.pradosoft.com/license/
9
 * @version $Id: TPgsqlMetaData.php 2541 2008-10-21 15:05:13Z qiang.xue $
10
 * @package System.Data.Common.Pgsql
11
 */
12
 
13
/**
14
 * Load the base TDbMetaData class.
15
 */
16
Prado::using('System.Data.Common.TDbMetaData');
17
Prado::using('System.Data.Common.Pgsql.TPgsqlTableInfo');
18
 
19
/**
20
 * TPgsqlMetaData loads PostgreSQL database table and column information.
21
 *
22
 * @author Wei Zhuo <weizho[at]gmail[dot]com>
23
 * @version $Id: TPgsqlMetaData.php 2541 2008-10-21 15:05:13Z qiang.xue $
24
 * @package System.Data.Common.Pgsql
25
 * @since 3.1
26
 */
27
class TPgsqlMetaData extends TDbMetaData
28
{
29
	private $_defaultSchema = 'public';
30
 
31
	/**
32
	 * @return string TDbTableInfo class name.
33
	 */
34
	protected function getTableInfoClass()
35
	{
36
		return 'TPgsqlTableInfo';
37
	}
38
 
39
	/**
40
	 * @param string default schema.
41
	 */
42
	public function setDefaultSchema($schema)
43
	{
44
		$this->_defaultSchema=$schema;
45
	}
46
 
47
	/**
48
	 * @return string default schema.
49
	 */
50
	public function getDefaultSchema()
51
	{
52
		return $this->_defaultSchema;
53
	}
54
 
55
	/**
56
	 * @param string table name with optional schema name prefix, uses default schema name prefix is not provided.
57
	 * @return array tuple as ($schemaName,$tableName)
58
	 */
59
	protected function getSchemaTableName($table)
60
	{
61
		if(count($parts= explode('.', str_replace('"','',$table))) > 1)
62
			return array($parts[0], $parts[1]);
63
		else
64
			return array($this->getDefaultSchema(),$parts[0]);
65
	}
66
 
67
	/**
68
	 * Get the column definitions for given table.
69
	 * @param string table name.
70
	 * @return TPgsqlTableInfo table information.
71
	 */
72
	protected function createTableInfo($table)
73
	{
74
		list($schemaName,$tableName) = $this->getSchemaTableName($table);
75
 
76
		// This query is made much more complex by the addition of the 'attisserial' field.
77
		// The subquery to get that field checks to see if there is an internally dependent
78
		// sequence on the field.
79
		$sql =
80
<<<EOD
81
		SELECT
82
			a.attname,
83
			pg_catalog.format_type(a.atttypid, a.atttypmod) as type,
84
			a.atttypmod,
85
			a.attnotnull, a.atthasdef, adef.adsrc,
86
			(
87
				SELECT 1 FROM pg_catalog.pg_depend pd, pg_catalog.pg_class pc
88
				WHERE pd.objid=pc.oid
89
				AND pd.classid=pc.tableoid
90
				AND pd.refclassid=pc.tableoid
91
				AND pd.refobjid=a.attrelid
92
				AND pd.refobjsubid=a.attnum
93
				AND pd.deptype='i'
94
				AND pc.relkind='S'
95
			) IS NOT NULL AS attisserial
96
 
97
		FROM
98
			pg_catalog.pg_attribute a LEFT JOIN pg_catalog.pg_attrdef adef
99
			ON a.attrelid=adef.adrelid
100
			AND a.attnum=adef.adnum
101
			LEFT JOIN pg_catalog.pg_type t ON a.atttypid=t.oid
102
		WHERE
103
			a.attrelid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table
104
				AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace WHERE
105
				nspname = :schema))
106
			AND a.attnum > 0 AND NOT a.attisdropped
107
		ORDER BY a.attnum
108
EOD;
109
		$this->getDbConnection()->setActive(true);
110
		$command = $this->getDbConnection()->createCommand($sql);
111
		$command->bindValue(':table', $tableName);
112
		$command->bindValue(':schema', $schemaName);
113
		$tableInfo = $this->createNewTableInfo($schemaName, $tableName);
114
		$index=0;
115
		foreach($command->query() as $col)
116
		{
117
			$col['index'] = $index++;
118
			$this->processColumn($tableInfo, $col);
119
		}
120
		if($index===0)
121
			throw new TDbException('dbmetadata_invalid_table_view', $table);
122
		return $tableInfo;
123
	}
124
 
125
	/**
126
	 * @param string table schema name
127
	 * @param string table name.
128
	 * @return TPgsqlTableInfo
129
	 */
130
	protected function createNewTableInfo($schemaName,$tableName)
131
	{
132
		$info['SchemaName'] = $this->assertIdentifier($schemaName);
133
		$info['TableName'] = $this->assertIdentifier($tableName);
134
		if($this->getIsView($schemaName,$tableName))
135
			$info['IsView'] = true;
136
		list($primary, $foreign) = $this->getConstraintKeys($schemaName, $tableName);
137
		$class = $this->getTableInfoClass();
138
		return new $class($info,$primary,$foreign);
139
	}
140
 
141
	/**
142
	 * @param string table name, schema name or column name.
143
	 * @return string a valid identifier.
144
	 * @throws TDbException when table name contains a double quote (").
145
	 */
146
	protected function assertIdentifier($name)
147
	{
148
		if(strpos($name, '"')!==false)
149
		{
150
			$ref = 'http://www.postgresql.org/docs/7.4/static/sql-syntax.html#SQL-SYNTAX-IDENTIFIERS';
151
			throw new TDbException('dbcommon_invalid_identifier_name', $name, $ref);
152
		}
153
		return $name;
154
	}
155
 
156
	/**
157
	 * @param string table schema name
158
	 * @param string table name.
159
	 * @return boolean true if the table is a view.
160
	 */
161
	protected function getIsView($schemaName,$tableName)
162
	{
163
		$sql =
164
<<<EOD
165
		SELECT count(c.relname) FROM pg_catalog.pg_class c
166
		LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
167
		WHERE (n.nspname=:schema) AND (c.relkind = 'v'::"char") AND c.relname = :table
168
EOD;
169
		$this->getDbConnection()->setActive(true);
170
		$command = $this->getDbConnection()->createCommand($sql);
171
		$command->bindValue(':schema',$schemaName);
172
		$command->bindValue(':table', $tableName);
173
		return intval($command->queryScalar()) === 1;
174
	}
175
 
176
	/**
177
	 * @param TPgsqlTableInfo table information.
178
	 * @param array column information.
179
	 */
180
	protected function processColumn($tableInfo, $col)
181
	{
182
		$columnId = $col['attname']; //use column name as column Id
183
 
184
		$info['ColumnName'] = '"'.$columnId.'"'; //quote the column names!
185
		$info['ColumnId'] = $columnId;
186
		$info['ColumnIndex'] = $col['index'];
187
		if(!$col['attnotnull'])
188
			$info['AllowNull'] = true;
189
		if(in_array($columnId, $tableInfo->getPrimaryKeys()))
190
			$info['IsPrimaryKey'] = true;
191
		if($this->isForeignKeyColumn($columnId, $tableInfo))
192
			$info['IsForeignKey'] = true;
193
 
194
		if($col['atttypmod'] > 0)
195
			$info['ColumnSize'] =  $col['atttypmod'] - 4;
196
		if($col['atthasdef'])
197
			$info['DefaultValue'] = $col['adsrc'];
198
		if($col['attisserial'] || substr($col['adsrc'],0,8) === 'nextval(')
199
		{
200
			if(($sequence = $this->getSequenceName($tableInfo, $col['adsrc']))!==null)
201
			{
202
				$info['SequenceName'] = $sequence;
203
				unset($info['DefaultValue']);
204
			}
205
		}
206
		$matches = array();
207
		if(preg_match('/\((\d+)(?:,(\d+))?+\)/', $col['type'], $matches))
208
		{
209
			$info['DbType'] = preg_replace('/\(\d+(?:,\d+)?\)/','',$col['type']);
210
			if($this->isPrecisionType($info['DbType']))
211
			{
212
				$info['NumericPrecision'] = intval($matches[1]);
213
				if(count($matches) > 2)
214
					$info['NumericScale'] = intval($matches[2]);
215
			}
216
			else
217
				$info['ColumnSize'] = intval($matches[1]);
218
		}
219
		else
220
			$info['DbType'] = $col['type'];
221
 
222
		$tableInfo->Columns[$columnId] = new TPgsqlTableColumn($info);
223
	}
224
 
225
	/**
226
	 * @return string serial name if found, null otherwise.
227
	 */
228
	protected function getSequenceName($tableInfo,$src)
229
	{
230
		$matches = array();
231
		if(preg_match('/nextval\([^\']*\'([^\']+)\'[^\)]*\)/i',$src,$matches))
232
		{
233
			if(is_int(strpos($matches[1], '.')))
234
				return $matches[1];
235
			else
236
				return $tableInfo->getSchemaName().'.'.$matches[1];
237
		}
238
	}
239
 
240
	/**
241
	 * @return boolean true if column type if "numeric", "interval" or begins with "time".
242
	 */
243
	protected function isPrecisionType($type)
244
	{
245
		$type = strtolower(trim($type));
246
		return $type==='numeric' || $type==='interval' || strpos($type, 'time')===0;
247
	}
248
 
249
	/**
250
	 * Gets the primary and foreign key column details for the given table.
251
	 * @param string schema name
252
	 * @param string table name.
253
	 * @return array tuple ($primary, $foreign)
254
	 */
255
	protected function getConstraintKeys($schemaName, $tableName)
256
	{
257
		$sql =
258
<<<EOD
259
	SELECT conname, consrc, contype, indkey, indisclustered FROM (
260
			SELECT
261
					conname,
262
					CASE WHEN contype='f' THEN
263
							pg_catalog.pg_get_constraintdef(oid)
264
					ELSE
265
							'CHECK (' || consrc || ')'
266
					END AS consrc,
267
					contype,
268
					conrelid AS relid,
269
					NULL AS indkey,
270
					FALSE AS indisclustered
271
			FROM
272
					pg_catalog.pg_constraint
273
			WHERE
274
					contype IN ('f', 'c')
275
			UNION ALL
276
			SELECT
277
					pc.relname,
278
					NULL,
279
					CASE WHEN indisprimary THEN
280
							'p'
281
					ELSE
282
							'u'
283
					END,
284
					pi.indrelid,
285
					indkey,
286
					pi.indisclustered
287
			FROM
288
					pg_catalog.pg_class pc,
289
					pg_catalog.pg_index pi
290
			WHERE
291
					pc.oid=pi.indexrelid
292
					AND EXISTS (
293
							SELECT 1 FROM pg_catalog.pg_depend d JOIN pg_catalog.pg_constraint c
294
							ON (d.refclassid = c.tableoid AND d.refobjid = c.oid)
295
							WHERE d.classid = pc.tableoid AND d.objid = pc.oid AND d.deptype = 'i' AND c.contype IN ('u', 'p')
296
			)
297
	) AS sub
298
	WHERE relid = (SELECT oid FROM pg_catalog.pg_class WHERE relname=:table
299
					AND relnamespace = (SELECT oid FROM pg_catalog.pg_namespace
300
					WHERE nspname=:schema))
301
	ORDER BY
302
			1
303
EOD;
304
		$this->getDbConnection()->setActive(true);
305
		$command = $this->getDbConnection()->createCommand($sql);
306
		$command->bindValue(':table', $tableName);
307
		$command->bindValue(':schema', $schemaName);
308
		$primary = array();
309
		$foreign = array();
310
		foreach($command->query() as $row)
311
		{
312
			switch($row['contype'])
313
			{
314
				case 'p':
315
					$primary = $this->getPrimaryKeys($tableName, $schemaName, $row['indkey']);
316
					break;
317
				case 'f':
318
					if(($fkey = $this->getForeignKeys($row['consrc']))!==null)
319
						$foreign[] = $fkey;
320
					break;
321
			}
322
		}
323
		return array($primary,$foreign);
324
	}
325
 
326
	/**
327
	 * Gets the primary key field names
328
	 * @param string pgsql primary key definition
329
	 * @return array primary key field names.
330
	 */
331
	protected function getPrimaryKeys($tableName, $schemaName, $columnIndex)
332
	{
333
		$index = join(', ', split(' ', $columnIndex));
334
		$sql =
335
<<<EOD
336
    SELECT attnum, attname FROM pg_catalog.pg_attribute WHERE
337
		attrelid=(
338
			SELECT oid FROM pg_catalog.pg_class WHERE relname=:table AND relnamespace=(
339
				SELECT oid FROM pg_catalog.pg_namespace WHERE nspname=:schema
340
			)
341
		)
342
        AND attnum IN ({$index})
343
EOD;
344
		$command = $this->getDbConnection()->createCommand($sql);
345
		$command->bindValue(':table', $tableName);
346
		$command->bindValue(':schema', $schemaName);
347
//		$command->bindValue(':columnIndex', join(', ', split(' ', $columnIndex)));
348
		$primary = array();
349
		foreach($command->query() as $row)
350
		{
351
            $primary[] = $row['attname'];
352
		}
353
 
354
		return $primary;
355
	}
356
 
357
	/**
358
	 * Gets foreign relationship constraint keys and table name
359
	 * @param string pgsql foreign key definition
360
	 * @return array foreign relationship table name and keys, null otherwise
361
	 */
362
	protected function getForeignKeys($src)
363
	{
364
		$matches = array();
365
		$brackets = '\(([^\)]+)\)';
366
		$find = "/FOREIGN\s+KEY\s+{$brackets}\s+REFERENCES\s+([^\(]+){$brackets}/i";
367
		if(preg_match($find, $src, $matches))
368
		{
369
			$keys = preg_split('/,\s+/', $matches[1]);
370
			$fkeys = array();
371
			foreach(preg_split('/,\s+/', $matches[3]) as $i => $fkey)
372
				$fkeys[$keys[$i]] = $fkey;
373
			return array('table' => str_replace('"','',$matches[2]), 'keys' => $fkeys);
374
		}
375
	}
376
 
377
	/**
378
	 * @param string column name.
379
	 * @param TPgsqlTableInfo table information.
380
	 * @return boolean true if column is a foreign key.
381
	 */
382
	protected function isForeignKeyColumn($columnId, $tableInfo)
383
	{
384
		foreach($tableInfo->getForeignKeys() as $fk)
385
		{
386
			if(in_array($columnId, array_keys($fk['keys'])))
387
				return true;
388
		}
389
		return false;
390
	}
391
}
392