views_handler_join_chado_aggregator.inc 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <?php
  2. module_load_include('inc', 'views', 'includes/base');
  3. module_load_include('inc', 'views', 'includes/handlers');
  4. /**
  5. * @file
  6. * Handler to allow joins between records via a linking table
  7. *
  8. * Example Usage:
  9. * To join the analysisprop table to the analysis table,
  10. * Use the following code in the analysisprop hook_views_data:
  11. * @code
  12. $data['analysisprop']['table']['join']['analysis'] = array(
  13. 'left_field' => 'analysis_id',
  14. 'field' => 'analysis_id',
  15. 'handler' => 'views_handler_join_chado_aggregator',
  16. 'pre-aggregated' => TRUE | FALSE //whether the table is already aggregated (contains arrays)
  17. 'table_aggregated' => CURRENT | LEFT //the table which has many records for each record in the other
  18. );
  19. * @endcode
  20. */
  21. class views_handler_join_chado_aggregator extends views_join {
  22. // PHP 4 doesn't call constructors of the base class automatically from a
  23. // constructor of a derived class. It is your responsibility to propagate
  24. // the call to constructors upstream where appropriate.
  25. function construct($table = NULL, $left_table = NULL, $left_field = NULL, $field = NULL, $extra = array(), $type = 'LEFT', $added = NULL) {
  26. parent::construct($table, $left_table, $left_field, $field, $extra, $type);
  27. // Determine the postgresql version
  28. $postgresql_version = pg_version();
  29. $this->postgresql_version = $postgresql_version['client'];
  30. // If version is 9.0+ then indicate
  31. // Needed to apply sorting for aggregated fields
  32. if (intval($postgresql_version['client']) >= 9) {
  33. $this->postgresql_9up = TRUE;
  34. }
  35. }
  36. /**
  37. * Creates SQL including aggregation query used in join
  38. */
  39. function join($table, &$query) {
  40. $opt = array(
  41. 'table' => $this->definition['table'],
  42. 'field' => $this->definition['field'],
  43. 'left_table' => $this->definition['left_table'],
  44. 'left_field' => $this->definition['left_field'],
  45. 'table_aggregated' => $this->definition['table_aggregated'],
  46. 'sort' => $this->sort,
  47. 'filter' => $this->filter,
  48. 'postgresql_9up' => $this->postgresql_9up,
  49. );
  50. $output = $this->aggregate_join($query, $opt);
  51. return implode("\n", $output);
  52. }
  53. function aggregate_join(&$query, $opt) {
  54. // Create the table SQL (used in join) -------
  55. // query creating one-to-one table using array_agg
  56. // Only aggregate each field if it the join table hadn't been pre-aggregated
  57. // Example where it might be pre-aggregated: Materialized view
  58. if (!$this->definition['pre-aggregated']) {
  59. $sql = $this->get_aggregate_sql_for_table_field($opt);
  60. // Create the join (full SQL) ----------------
  61. $output[] = $this->create_single_join(
  62. $query,
  63. array(
  64. 'table' => $opt['table'],
  65. 'field' => $opt['field'],
  66. 'table_sql' => $sql,
  67. 'is_drupal' => FALSE,
  68. ),
  69. array(
  70. 'table' => $opt['left_table'],
  71. 'field' => $opt['left_field'],
  72. ),
  73. 'LEFT'
  74. );
  75. // Otherwise the table has been pre-aggregated
  76. // Then only need to do a regular join with any in where
  77. }
  78. else {
  79. // Create the join
  80. $current_table_spec = array(
  81. 'table' => $opt['table'],
  82. 'field' => $opt['field'],
  83. 'is_drupal' => FALSE,
  84. );
  85. $left_table_spec = array(
  86. 'table' => $opt['left_table'],
  87. 'field' => $opt['left_field'],
  88. );
  89. switch ($opt['table_aggregated']) {
  90. default:
  91. case 'CURRENT':
  92. $current_table_spec['pre-aggregated'] = TRUE;
  93. break;
  94. case 'LEFT':
  95. $left_table_spec['pre-aggregated'] = TRUE;
  96. break;
  97. }
  98. $output[] = $this->create_single_join(
  99. $query,
  100. $current_table_spec,
  101. $left_table_spec,
  102. 'LEFT'
  103. );
  104. }
  105. return $output;
  106. }
  107. /**
  108. * Create the SQL needed to aggregate a table
  109. */
  110. function get_aggregate_sql_for_table_field($opt) {
  111. // Determine Order BY's for aggregates
  112. $order_by = array();
  113. if (!is_array($opt['sort'])) {
  114. $opt['sort'] = array();
  115. }
  116. foreach ($opt['sort'] as $s) {
  117. $order_by[] = $s['table'] . '.' . $s['field'] . ' ' . $s['order'];
  118. }
  119. // get table description (if defined via schema api)
  120. $table_desc = tripal_core_get_chado_table_schema($opt['table']);
  121. $select_fields[ $opt['table'] ] = $table_desc['fields'];
  122. if (!empty($table_desc)) {
  123. // Add joins to tables with a foreign key in this table
  124. // (ie: add join to cvterm if this table has a type_id
  125. $joins = array();
  126. foreach ($table_desc['foreign keys'] as $defn) {
  127. if ($defn['table'] != $opt['left_table']) {
  128. foreach ( $defn['columns'] as $left => $right) {
  129. $left = $opt['table'] . '.' . $left;
  130. $right = $defn['table'] . '.' . $right;
  131. $joins[] = "LEFT JOIN $defn[table] $defn[table] ON $left=$right";
  132. }
  133. // Fields to be selected from joined table
  134. $join_table = tripal_core_get_chado_table_schema($defn['table']);
  135. $select_fields[ $defn['table'] ] = $join_table['fields'];
  136. }
  137. }
  138. // Fields to be selected
  139. foreach ($select_fields as $table => $table_fields) {
  140. foreach ($table_fields as $fname => $f) {
  141. $alias = '';
  142. if ($table != $opt['table']) {
  143. $alias = $table . '_';
  144. }
  145. if ($fname != $opt['field']) {
  146. // Add sort to aggregate field if postgreSQL 9.0+
  147. if ($opt['postgresql_9up'] && !empty($order_by)) {
  148. $fields[] = 'array_agg(' . $table . '.' . $fname . ' ORDER BY ' . implode(',', $order_by) . ') as ' . $alias . $fname;
  149. }
  150. else {
  151. $fields[] = 'array_agg(' . $table . '.' . $fname . ') as '. $alias . $fname;
  152. }
  153. $composite_field_parts[] = "'" . $alias . $fname . "::' ||" . $table . '.' . $fname;
  154. }
  155. else {
  156. $fields[] = $fname;
  157. $composite_field_parts[] = "'" . $alias . $fname . "::' ||" . $table . '.' . $fname;
  158. }
  159. }
  160. }
  161. // There is no definition in schema api
  162. // then use postgresql select
  163. }
  164. else {
  165. // No known foreign key reelationships
  166. $joins = array();
  167. // Fields to be selected
  168. $sql = "SELECT
  169. attname as column,
  170. format_type(atttypid, atttypmod) as datatype
  171. FROM pg_attribute, pg_type
  172. WHERE typname='nd_genotype_experiment'
  173. AND attrelid=typrelid
  174. AND attname NOT IN ('cmin','cmax','ctid','oid','tableoid','xmin','xmax')";
  175. $previous_db = tripal_db_set_active('chado');
  176. $resource = db_query($sql);
  177. tripal_db_set_active($previous_db);
  178. while ($r = db_fetch_object($resource)) {
  179. $table = $opt['table'];
  180. $alias = ''; //no alias needed if table is current table (only option if no schema api definition)
  181. $fname = $r->column;
  182. if ($fname != $opt['field']) {
  183. // Add sort to aggregate field if postgreSQL 9.0+
  184. if ($opt['postgresql_9up'] && !empty($order_by)) {
  185. $fields[] = 'array_agg(' . $table . '.' . $fname . ' ORDER BY ' . implode(',', $order_by) . ') as ' . $alias . $fname;
  186. }
  187. else {
  188. $fields[] = 'array_agg(' . $table . '.' . $fname . ') as ' . $alias . $fname;
  189. }
  190. $composite_field_parts[] = "'" . $alias . $fname . "::' ||" . $table . '.' . $fname;
  191. }
  192. else {
  193. $fields[] = $fname;
  194. $composite_field_parts[] = "'" . $alias . $fname . "::' ||" . $table . '.' . $fname;
  195. }
  196. }
  197. }
  198. // composite field
  199. // (combines all other fields before aggregating)
  200. // Add sort to aggregate field if postgreSQL 9.0+
  201. if ($opt['postgresql_9up'] && !empty($order_by)) {
  202. $composite_field = "array_agg('{'||" . implode(" || ',' || ", $composite_field_parts) . "||'}' ORDER BY " . implode(',', $order_by) . ") as all";
  203. }
  204. else {
  205. $composite_field = "array_agg('{'||" . implode(" || ',' || ", $composite_field_parts) . "||'}') as all";
  206. }
  207. $fields[] = $composite_field;
  208. // SQL to use in the join
  209. $sql = 'SELECT ' . implode(', ', $fields)
  210. .' FROM ' . $opt['table']
  211. .' ' . implode(' ', $joins);
  212. if (!empty($opt['filter'])) {
  213. $sql .= ' WHERE ' . implode(', ', $opt['filter']);
  214. }
  215. $sql .= ' GROUP BY ' . $opt['field'];
  216. return $sql;
  217. }
  218. /**
  219. * Creates SQL for a single join based on parameters
  220. * Join will be: <type> JOIN (<query creating one-to-one table using array_agg>) <table alias>
  221. * ON <qualified left field>=<qualified right field>
  222. */
  223. function create_single_join(&$query, $right_spec, $left_spec, $join_type) {
  224. if ($right_spec['table']) {
  225. $right = $query->get_table_info($right_spec['table']);
  226. if (!$right['alias']) {
  227. $right['alias'] = $right_spec['table'];
  228. }
  229. $right_field = "$right[alias].$right_spec[field]";
  230. // Add any() around field if already aggregated
  231. if ($right_spec['pre-aggregated']) {
  232. $right_field = "any(" . $right_field . ")";
  233. }
  234. // Add drupal { } around table
  235. if ($right_spec['is_drupal']) {
  236. $right_table = '{' . $right_spec['table'] . '}';
  237. }
  238. else {
  239. $right_table = $right_spec['table'];
  240. }
  241. }
  242. if ($left_spec['table']) {
  243. $left = $query->get_table_info($left_spec['table']);
  244. if (!$left['alias']) {
  245. $left['alias'] = $left_spec['table'];
  246. }
  247. $left_field = "$left[alias].$left_spec[field]";
  248. }
  249. else {
  250. // This can be used if left_field is a formula or something. It should be used only *very* rarely.
  251. $left_field = $this->left_spec['field'];
  252. }
  253. // Add any() around field if already aggregated
  254. if ($left_spec['pre-aggregated']) {
  255. $left_field = "any(" . $left_field . ")";
  256. }
  257. // Concatenate parts together to form join sql
  258. if (!empty($right_spec['table_sql'])) {
  259. $output = " $join_type JOIN ($right_spec[table_sql]) $right[alias] ON $left_field = $right_field";
  260. }
  261. else {
  262. $output = " $join_type JOIN $right_spec[table] $right[alias] ON $left_field = $right_field";
  263. }
  264. return $output;
  265. }
  266. }