PageRenderTime 28ms CodeModel.GetById 1ms RepoModel.GetById 0ms app.codeStats 0ms

/generator/lib/reverse/pgsql/PgsqlSchemaParser.php

https://github.com/1989gaurav/Propel
PHP | 566 lines | 401 code | 69 blank | 96 comment | 60 complexity | 62e36f51bae7a645c2c21243a73efd64 MD5 | raw file
  1. <?php
  2. /**
  3. * This file is part of the Propel package.
  4. * For the full copyright and license information, please view the LICENSE
  5. * file that was distributed with this source code.
  6. *
  7. * @license MIT License
  8. */
  9. require_once dirname(__FILE__) . '/../BaseSchemaParser.php';
  10. /**
  11. * Postgresql database schema parser.
  12. *
  13. * @author Hans Lellelid <hans@xmpl.org>
  14. * @version $Revision$
  15. * @package propel.generator.reverse.pgsql
  16. */
  17. class PgsqlSchemaParser extends BaseSchemaParser
  18. {
  19. /**
  20. * Map PostgreSQL native types to Propel types.
  21. * @var array
  22. */
  23. /** Map MySQL native types to Propel (JDBC) types. */
  24. private static $pgsqlTypeMap = array(
  25. 'bool' => PropelTypes::BOOLEAN,
  26. 'boolean' => PropelTypes::BOOLEAN,
  27. 'tinyint' => PropelTypes::TINYINT,
  28. 'smallint' => PropelTypes::SMALLINT,
  29. 'mediumint' => PropelTypes::SMALLINT,
  30. 'int2' => PropelTypes::SMALLINT,
  31. 'int' => PropelTypes::INTEGER,
  32. 'int4' => PropelTypes::INTEGER,
  33. 'serial4' => PropelTypes::INTEGER,
  34. 'integer' => PropelTypes::INTEGER,
  35. 'int8' => PropelTypes::BIGINT,
  36. 'bigint' => PropelTypes::BIGINT,
  37. 'bigserial' => PropelTypes::BIGINT,
  38. 'serial8' => PropelTypes::BIGINT,
  39. 'int24' => PropelTypes::BIGINT,
  40. 'real' => PropelTypes::REAL,
  41. 'float' => PropelTypes::FLOAT,
  42. 'float4' => PropelTypes::FLOAT,
  43. 'decimal' => PropelTypes::DECIMAL,
  44. 'numeric' => PropelTypes::DECIMAL,
  45. 'double' => PropelTypes::DOUBLE,
  46. 'float8' => PropelTypes::DOUBLE,
  47. 'char' => PropelTypes::CHAR,
  48. 'character' => PropelTypes::CHAR,
  49. 'varchar' => PropelTypes::VARCHAR,
  50. 'date' => PropelTypes::DATE,
  51. 'time' => PropelTypes::TIME,
  52. 'timetz' => PropelTypes::TIME,
  53. //'year' => PropelTypes::YEAR, PropelTypes::YEAR does not exist... does this need to be mapped to a different propel type?
  54. 'datetime' => PropelTypes::TIMESTAMP,
  55. 'timestamp' => PropelTypes::TIMESTAMP,
  56. 'timestamptz' => PropelTypes::TIMESTAMP,
  57. 'bytea' => PropelTypes::BLOB,
  58. 'text' => PropelTypes::LONGVARCHAR,
  59. );
  60. /**
  61. * Gets a type mapping from native types to Propel types
  62. *
  63. * @return array
  64. */
  65. protected function getTypeMapping()
  66. {
  67. return self::$pgsqlTypeMap;
  68. }
  69. /**
  70. *
  71. */
  72. public function parse(Database $database, Task $task = null)
  73. {
  74. $stmt = $this->dbh->query("SELECT version() as ver");
  75. $nativeVersion = $stmt->fetchColumn();
  76. if (!$nativeVersion) {
  77. throw new EngineException("Failed to get database version");
  78. }
  79. $arrVersion = sscanf ($nativeVersion, '%*s %d.%d');
  80. $version = sprintf ("%d.%d", $arrVersion[0], $arrVersion[1]);
  81. // Clean up
  82. $stmt = null;
  83. $stmt = $this->dbh->query("SELECT c.oid,
  84. c.relname, n.nspname
  85. FROM pg_class c join pg_namespace n on (c.relnamespace=n.oid)
  86. WHERE c.relkind = 'r'
  87. AND n.nspname NOT IN ('information_schema','pg_catalog')
  88. AND n.nspname NOT LIKE 'pg_temp%'
  89. AND n.nspname NOT LIKE 'pg_toast%'
  90. ORDER BY relname");
  91. $tableWraps = array();
  92. // First load the tables (important that this happen before filling out details of tables)
  93. if ($task) $task->log("Reverse Engineering Tables", Project::MSG_VERBOSE);
  94. while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
  95. $name = $row['relname'];
  96. $namespacename = $row['nspname'];
  97. if ($name == $this->getMigrationTable()) {
  98. continue;
  99. }
  100. if ($task) $task->log(" Adding table '" . $name . "' in schema '" . $namespacename . "'", Project::MSG_VERBOSE);
  101. $oid = $row['oid'];
  102. $table = new Table($name);
  103. if ($namespacename != 'public') {
  104. $table->setSchema($namespacename);
  105. }
  106. $table->setIdMethod($database->getDefaultIdMethod());
  107. $database->addTable($table);
  108. // Create a wrapper to hold these tables and their associated OID
  109. $wrap = new stdClass;
  110. $wrap->table = $table;
  111. $wrap->oid = $oid;
  112. $tableWraps[] = $wrap;
  113. }
  114. // Now populate only columns.
  115. if ($task) $task->log("Reverse Engineering Columns", Project::MSG_VERBOSE);
  116. foreach ($tableWraps as $wrap) {
  117. if ($task) $task->log(" Adding columns for table '" . $wrap->table->getName() . "'", Project::MSG_VERBOSE);
  118. $this->addColumns($wrap->table, $wrap->oid, $version);
  119. }
  120. // Now add indexes and constraints.
  121. if ($task) $task->log("Reverse Engineering Indices And Constraints", Project::MSG_VERBOSE);
  122. foreach ($tableWraps as $wrap) {
  123. if ($task) $task->log(" Adding indices and constraints for table '" . $wrap->table->getName() . "'", Project::MSG_VERBOSE);
  124. $this->addForeignKeys($wrap->table, $wrap->oid, $version);
  125. $this->addIndexes($wrap->table, $wrap->oid, $version);
  126. $this->addPrimaryKey($wrap->table, $wrap->oid, $version);
  127. }
  128. // TODO - Handle Sequences ...
  129. return count($tableWraps);
  130. }
  131. /**
  132. * Adds Columns to the specified table.
  133. *
  134. * @param Table $table The Table model class to add columns to.
  135. * @param int $oid The table OID
  136. * @param string $version The database version.
  137. */
  138. protected function addColumns(Table $table, $oid, $version)
  139. {
  140. // Get the columns, types, etc.
  141. // Based on code from pgAdmin3 (http://www.pgadmin.org/)
  142. $stmt = $this->dbh->prepare("SELECT
  143. att.attname,
  144. att.atttypmod,
  145. att.atthasdef,
  146. att.attnotnull,
  147. def.adsrc,
  148. CASE WHEN att.attndims > 0 THEN 1 ELSE 0 END AS isarray,
  149. CASE
  150. WHEN ty.typname = 'bpchar'
  151. THEN 'char'
  152. WHEN ty.typname = '_bpchar'
  153. THEN '_char'
  154. ELSE
  155. ty.typname
  156. END AS typname,
  157. ty.typtype
  158. FROM pg_attribute att
  159. JOIN pg_type ty ON ty.oid=att.atttypid
  160. LEFT OUTER JOIN pg_attrdef def ON adrelid=att.attrelid AND adnum=att.attnum
  161. WHERE att.attrelid = ? AND att.attnum > 0
  162. AND att.attisdropped IS FALSE
  163. ORDER BY att.attnum");
  164. $stmt->bindValue(1, $oid, PDO::PARAM_INT);
  165. $stmt->execute();
  166. while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
  167. $size = null;
  168. $precision = null;
  169. $scale = null;
  170. // Check to ensure that this column isn't an array data type
  171. if (((int) $row['isarray']) === 1) {
  172. throw new EngineException (sprintf ("Array datatypes are not currently supported [%s.%s]", $this->name, $row['attname']));
  173. } // if (((int) $row['isarray']) === 1)
  174. $name = $row['attname'];
  175. // If they type is a domain, Process it
  176. if (strtolower ($row['typtype']) == 'd') {
  177. $arrDomain = $this->processDomain ($row['typname']);
  178. $type = $arrDomain['type'];
  179. $size = $arrDomain['length'];
  180. $precision = $size;
  181. $scale = $arrDomain['scale'];
  182. $boolHasDefault = (strlen (trim ($row['atthasdef'])) > 0) ? $row['atthasdef'] : $arrDomain['hasdefault'];
  183. $default = (strlen (trim ($row['adsrc'])) > 0) ? $row['adsrc'] : $arrDomain['default'];
  184. $is_nullable = (strlen (trim ($row['attnotnull'])) > 0) ? $row['attnotnull'] : $arrDomain['notnull'];
  185. $is_nullable = (($is_nullable == 't') ? false : true);
  186. } else {
  187. $type = $row['typname'];
  188. $arrLengthPrecision = $this->processLengthScale ($row['atttypmod'], $type);
  189. $size = $arrLengthPrecision['length'];
  190. $precision = $size;
  191. $scale = $arrLengthPrecision['scale'];
  192. $boolHasDefault = $row['atthasdef'];
  193. $default = $row['adsrc'];
  194. $is_nullable = (($row['attnotnull'] == 't') ? false : true);
  195. } // else (strtolower ($row['typtype']) == 'd')
  196. $autoincrement = null;
  197. // if column has a default
  198. if (($boolHasDefault == 't') && (strlen (trim ($default)) > 0)) {
  199. if (!preg_match('/^nextval\(/', $default)) {
  200. $strDefault= preg_replace('/::[\W\D]*/', '', $default);
  201. $default = preg_replace('/(\'?)\'/', '${1}', $strDefault);
  202. } else {
  203. $autoincrement = true;
  204. $default = null;
  205. }
  206. } else {
  207. $default = null;
  208. }
  209. $propelType = $this->getMappedPropelType($type);
  210. if (!$propelType) {
  211. $propelType = Column::DEFAULT_TYPE;
  212. $this->warn("Column [" . $table->getName() . "." . $name. "] has a column type (".$type.") that Propel does not support.");
  213. }
  214. $column = new Column($name);
  215. $column->setTable($table);
  216. $column->setDomainForType($propelType);
  217. // We may want to provide an option to include this:
  218. // $column->getDomain()->replaceSqlType($type);
  219. $column->getDomain()->replaceSize($size);
  220. $column->getDomain()->replaceScale($scale);
  221. if ($default !== null) {
  222. if (in_array($default, array('now()'))) {
  223. $type = ColumnDefaultValue::TYPE_EXPR;
  224. } else {
  225. $type = ColumnDefaultValue::TYPE_VALUE;
  226. }
  227. $column->getDomain()->setDefaultValue(new ColumnDefaultValue($default, $type));
  228. }
  229. $column->setAutoIncrement($autoincrement);
  230. $column->setNotNull(!$is_nullable);
  231. $table->addColumn($column);
  232. }
  233. } // addColumn()
  234. private function processLengthScale($intTypmod, $strName)
  235. {
  236. // Define the return array
  237. $arrRetVal = array ('length'=>null, 'scale'=>null);
  238. // Some datatypes don't have a Typmod
  239. if ($intTypmod == -1)
  240. {
  241. return $arrRetVal;
  242. } // if ($intTypmod == -1)
  243. // Numeric Datatype?
  244. if ($strName == $this->getMappedNativeType(PropelTypes::NUMERIC)) {
  245. $intLen = ($intTypmod - 4) >> 16;
  246. $intPrec = ($intTypmod - 4) & 0xffff;
  247. $intLen = sprintf ("%ld", $intLen);
  248. if ($intPrec)
  249. {
  250. $intPrec = sprintf ("%ld", $intPrec);
  251. } // if ($intPrec)
  252. $arrRetVal['length'] = $intLen;
  253. $arrRetVal['scale'] = $intPrec;
  254. } // if ($strName == $this->getMappedNativeType(PropelTypes::NUMERIC))
  255. elseif ($strName == $this->getMappedNativeType(PropelTypes::TIME) || $strName == 'timetz'
  256. || $strName == $this->getMappedNativeType(PropelTypes::TIMESTAMP) || $strName == 'timestamptz'
  257. || $strName == 'interval' || $strName == 'bit')
  258. {
  259. $arrRetVal['length'] = sprintf ("%ld", $intTypmod);
  260. } // elseif (TIME, TIMESTAMP, INTERVAL, BIT)
  261. else
  262. {
  263. $arrRetVal['length'] = sprintf ("%ld", ($intTypmod - 4));
  264. } // else
  265. return $arrRetVal;
  266. } // private function processLengthScale ($intTypmod, $strName)
  267. private function processDomain($strDomain)
  268. {
  269. if (strlen(trim ($strDomain)) < 1) {
  270. throw new EngineException ("Invalid domain name [" . $strDomain . "]");
  271. }
  272. $stmt = $this->dbh->prepare("SELECT
  273. d.typname as domname,
  274. b.typname as basetype,
  275. d.typlen,
  276. d.typtypmod,
  277. d.typnotnull,
  278. d.typdefault
  279. FROM pg_type d
  280. INNER JOIN pg_type b ON b.oid = CASE WHEN d.typndims > 0 then d.typelem ELSE d.typbasetype END
  281. WHERE
  282. d.typtype = 'd'
  283. AND d.typname = ?
  284. ORDER BY d.typname");
  285. $stmt->bindValue(1, $strDomain);
  286. $stmt->execute();
  287. $row = $stmt->fetch(PDO::FETCH_ASSOC);
  288. if (!$row) {
  289. throw new EngineException ("Domain [" . $strDomain . "] not found.");
  290. }
  291. $arrDomain = array ();
  292. $arrDomain['type'] = $row['basetype'];
  293. $arrLengthPrecision = $this->processLengthScale($row['typtypmod'], $row['basetype']);
  294. $arrDomain['length'] = $arrLengthPrecision['length'];
  295. $arrDomain['scale'] = $arrLengthPrecision['scale'];
  296. $arrDomain['notnull'] = $row['typnotnull'];
  297. $arrDomain['default'] = $row['typdefault'];
  298. $arrDomain['hasdefault'] = (strlen (trim ($row['typdefault'])) > 0) ? 't' : 'f';
  299. $stmt = null; // cleanup
  300. return $arrDomain;
  301. } // private function processDomain($strDomain)
  302. /**
  303. * Load foreign keys for this table.
  304. */
  305. protected function addForeignKeys(Table $table, $oid, $version)
  306. {
  307. $database = $table->getDatabase();
  308. $stmt = $this->dbh->prepare("SELECT
  309. conname,
  310. confupdtype,
  311. confdeltype,
  312. CASE nl.nspname WHEN 'public' THEN cl.relname ELSE nl.nspname||'.'||cl.relname END as fktab,
  313. array_agg(DISTINCT a2.attname) AS fkcols,
  314. CASE nr.nspname WHEN 'public' THEN cr.relname ELSE nr.nspname||'.'||cr.relname END as reftab,
  315. array_agg(DISTINCT a1.attname) AS refcols
  316. FROM pg_constraint ct
  317. JOIN pg_class cl ON cl.oid=conrelid
  318. JOIN pg_class cr ON cr.oid=confrelid
  319. JOIN pg_namespace nl ON nl.oid = cl.relnamespace
  320. JOIN pg_namespace nr ON nr.oid = cr.relnamespace
  321. LEFT JOIN pg_catalog.pg_attribute a1 ON a1.attrelid = ct.confrelid
  322. LEFT JOIN pg_catalog.pg_attribute a2 ON a2.attrelid = ct.conrelid
  323. WHERE
  324. contype='f'
  325. AND conrelid = ?
  326. AND a2.attnum = ANY (ct.conkey)
  327. AND a1.attnum = ANY (ct.confkey)
  328. GROUP BY conname, confupdtype, confdeltype, fktab, reftab
  329. ORDER BY conname");
  330. $stmt->bindValue(1, $oid);
  331. $stmt->execute();
  332. $foreignKeys = array(); // local store to avoid duplicates
  333. while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
  334. $name = $row['conname'];
  335. $local_table = $row['fktab'];
  336. $local_columns = explode(',', trim($row['fkcols'], '{}'));
  337. $foreign_table = $row['reftab'];
  338. $foreign_columns = explode(',', trim($row['refcols'], '{}'));
  339. // On Update
  340. switch ($row['confupdtype']) {
  341. case 'c':
  342. $onupdate = ForeignKey::CASCADE; break;
  343. case 'd':
  344. $onupdate = ForeignKey::SETDEFAULT; break;
  345. case 'n':
  346. $onupdate = ForeignKey::SETNULL; break;
  347. case 'r':
  348. $onupdate = ForeignKey::RESTRICT; break;
  349. default:
  350. case 'a':
  351. //NOACTION is the postgresql default
  352. $onupdate = ForeignKey::NONE; break;
  353. }
  354. // On Delete
  355. switch ($row['confdeltype']) {
  356. case 'c':
  357. $ondelete = ForeignKey::CASCADE; break;
  358. case 'd':
  359. $ondelete = ForeignKey::SETDEFAULT; break;
  360. case 'n':
  361. $ondelete = ForeignKey::SETNULL; break;
  362. case 'r':
  363. $ondelete = ForeignKey::RESTRICT; break;
  364. default:
  365. case 'a':
  366. //NOACTION is the postgresql default
  367. $ondelete = ForeignKey::NONE; break;
  368. }
  369. $foreignTable = $database->getTable($foreign_table);
  370. $localTable = $database->getTable($local_table);
  371. if (!isset($foreignKeys[$name])) {
  372. $fk = new ForeignKey($name);
  373. $fk->setForeignTableCommonName($foreignTable->getCommonName());
  374. $fk->setForeignSchemaName($foreignTable->getSchema());
  375. $fk->setOnDelete($ondelete);
  376. $fk->setOnUpdate($onupdate);
  377. $table->addForeignKey($fk);
  378. $foreignKeys[$name] = $fk;
  379. }
  380. for ($i = 0; $i < count($local_columns); $i++) {
  381. $foreignKeys[$name]->addReference(
  382. $localTable->getColumn($local_columns[$i]),
  383. $foreignTable->getColumn($foreign_columns[$i])
  384. );
  385. }
  386. }
  387. }
  388. /**
  389. * Load indexes for this table
  390. */
  391. protected function addIndexes(Table $table, $oid, $version)
  392. {
  393. $stmt = $this->dbh->prepare("SELECT
  394. DISTINCT ON(cls.relname)
  395. cls.relname as idxname,
  396. indkey,
  397. indisunique
  398. FROM pg_index idx
  399. JOIN pg_class cls ON cls.oid=indexrelid
  400. WHERE indrelid = ? AND NOT indisprimary
  401. ORDER BY cls.relname");
  402. $stmt->bindValue(1, $oid);
  403. $stmt->execute();
  404. $stmt2 = $this->dbh->prepare("SELECT a.attname
  405. FROM pg_catalog.pg_class c JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
  406. WHERE c.oid = ? AND a.attnum = ? AND NOT a.attisdropped
  407. ORDER BY a.attnum");
  408. $indexes = array();
  409. while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
  410. $name = $row["idxname"];
  411. $unique = ($row["indisunique"] == 't') ? true : false;
  412. if (!isset($indexes[$name])) {
  413. if ($unique) {
  414. $indexes[$name] = new Unique($name);
  415. } else {
  416. $indexes[$name] = new Index($name);
  417. }
  418. $table->addIndex($indexes[$name]);
  419. }
  420. $arrColumns = explode (' ', $row['indkey']);
  421. foreach ($arrColumns as $intColNum)
  422. {
  423. $stmt2->bindValue(1, $oid);
  424. $stmt2->bindValue(2, $intColNum);
  425. $stmt2->execute();
  426. $row2 = $stmt2->fetch(PDO::FETCH_ASSOC);
  427. $indexes[$name]->addColumn($table->getColumn($row2['attname']));
  428. } // foreach ($arrColumns as $intColNum)
  429. }
  430. }
  431. /**
  432. * Loads the primary key for this table.
  433. */
  434. protected function addPrimaryKey(Table $table, $oid, $version)
  435. {
  436. $stmt = $this->dbh->prepare("SELECT
  437. DISTINCT ON(cls.relname)
  438. cls.relname as idxname,
  439. indkey,
  440. indisunique
  441. FROM pg_index idx
  442. JOIN pg_class cls ON cls.oid=indexrelid
  443. WHERE indrelid = ? AND indisprimary
  444. ORDER BY cls.relname");
  445. $stmt->bindValue(1, $oid);
  446. $stmt->execute();
  447. // Loop through the returned results, grouping the same key_name together
  448. // adding each column for that key.
  449. while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
  450. $arrColumns = explode (' ', $row['indkey']);
  451. foreach ($arrColumns as $intColNum) {
  452. $stmt2 = $this->dbh->prepare("SELECT a.attname
  453. FROM pg_catalog.pg_class c JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
  454. WHERE c.oid = ? AND a.attnum = ? AND NOT a.attisdropped
  455. ORDER BY a.attnum");
  456. $stmt2->bindValue(1, $oid);
  457. $stmt2->bindValue(2, $intColNum);
  458. $stmt2->execute();
  459. $row2 = $stmt2->fetch(PDO::FETCH_ASSOC);
  460. $table->getColumn($row2['attname'])->setPrimaryKey(true);
  461. } // foreach ($arrColumns as $intColNum)
  462. }
  463. }
  464. /**
  465. * Adds the sequences for this database.
  466. *
  467. * @return void
  468. * @throws SQLException
  469. */
  470. protected function addSequences(Database $database)
  471. {
  472. /*
  473. -- WE DON'T HAVE ANY USE FOR THESE YET IN REVERSE ENGINEERING ...
  474. $this->sequences = array();
  475. $result = pg_query($this->conn->getResource(), "SELECT c.oid,
  476. case when n.nspname='public' then c.relname else n.nspname||'.'||c.relname end as relname
  477. FROM pg_class c join pg_namespace n on (c.relnamespace=n.oid)
  478. WHERE c.relkind = 'S'
  479. AND n.nspname NOT IN ('information_schema','pg_catalog')
  480. AND n.nspname NOT LIKE 'pg_temp%'
  481. AND n.nspname NOT LIKE 'pg_toast%'
  482. ORDER BY relname");
  483. if (!$result) {
  484. throw new SQLException("Could not list sequences", pg_last_error($this->dblink));
  485. }
  486. while ($row = pg_fetch_assoc($result)) {
  487. // FIXME -- decide what info we need for sequences & then create a SequenceInfo object (if needed)
  488. $obj = new stdClass;
  489. $obj->name = $row['relname'];
  490. $obj->oid = $row['oid'];
  491. $this->sequences[strtoupper($row['relname'])] = $obj;
  492. }
  493. */
  494. }
  495. }