Use of jOOQ: Improve the code to COUNT a sub-select.
[online-glom:gwt-glom.git] / src / main / java / org / glom / web / server / SqlUtils.java
1 /*
2  * Copyright (C) 2012 Openismus GmbH
3  *
4  * This file is part of GWT-Glom.
5  *
6  * GWT-Glom is free software: you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as published by the
8  * Free Software Foundation, either version 3 of the License, or (at your
9  * option) any later version.
10  *
11  * GWT-Glom is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
14  * for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with GWT-Glom.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package org.glom.web.server;
21
22 import java.sql.Connection;
23 import java.util.ArrayList;
24 import java.util.List;
25
26 import org.apache.commons.lang3.StringUtils;
27 import org.glom.libglom.Document;
28 import org.glom.libglom.Field;
29 import org.glom.libglom.FieldVector;
30 import org.glom.libglom.LayoutFieldVector;
31 import org.glom.libglom.LayoutItem_Field;
32 import org.glom.libglom.Relationship;
33 import org.glom.libglom.SortClause;
34 import org.glom.libglom.SortFieldPair;
35 import org.glom.libglom.Value;
36 import org.jooq.AggregateFunction;
37 import org.jooq.Condition;
38 import org.jooq.SQLDialect;
39 import org.jooq.SelectFinalStep;
40 import org.jooq.SelectJoinStep;
41 import org.jooq.SelectSelectStep;
42 import org.jooq.conf.RenderKeywordStyle;
43 import org.jooq.conf.RenderNameStyle;
44 import org.jooq.conf.Settings;
45 import org.jooq.impl.Factory;
46
47 /**
48  * @author Ben Konrath <ben@bagu.org>
49  * 
50  */
51 public class SqlUtils {
52
53         public static class UsesRelationship {
54
55                 private Relationship relationship;
56                 private Relationship relatedRelationship;
57
58                 public void setRelationship(final Relationship relationship) {
59                         this.relationship = relationship;
60                 }
61
62                 /**
63                  * @param get_related_relationship
64                  */
65                 public void setRelatedRelationship(final Relationship relationship) {
66                         this.relatedRelationship = relationship;
67                 }
68
69                 public Relationship getRelationship() {
70                         return relationship;
71                 }
72
73                 public Relationship getRelatedRelationship() {
74                         return relatedRelationship;
75                 }
76
77                 private boolean getHasRelationshipName() {
78                         if (relationship == null) {
79                                 return false;
80                         }
81
82                         if (StringUtils.isEmpty(relationship.get_name())) {
83                                 return false;
84                         }
85
86                         return true;
87                 }
88
89                 private boolean getHasRelatedRelationshipName() {
90                         if (relatedRelationship == null) {
91                                 return false;
92                         }
93
94                         if (StringUtils.isEmpty(relatedRelationship.get_name())) {
95                                 return false;
96                         }
97
98                         return true;
99                 }
100
101                 public String get_sql_join_alias_name() {
102                         String result = "";
103
104                         if (getHasRelationshipName() && relationship.get_has_fields()) // relationships that link to tables together
105                                                                                                                                                         // via a field
106                         {
107                                 // We use relationship_name.field_name instead of related_tableName.field_name,
108                                 // because, in the JOIN below, will specify the relationship_name as an alias for the related table name
109                                 result += ("relationship_" + relationship.get_name());
110
111                                 if (getHasRelatedRelationshipName() && relatedRelationship.get_has_fields()) {
112                                         result += ('_' + relatedRelationship.get_name());
113                                 }
114                         }
115
116                         return result;
117                 }
118
119                 /*
120                  * (non-Javadoc)
121                  * 
122                  * @see java.lang.Object#hashCode()
123                  */
124                 /*
125                  * @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result +
126                  * ((relatedRelationship == null) ? 0 : relatedRelationship.hashCode()); result = prime * result +
127                  * ((relationship == null) ? 0 : relationship.hashCode()); return result; }
128                  */
129
130                 /*
131                  * (non-Javadoc)
132                  * 
133                  * @see java.lang.Object#equals(java.lang.Object)
134                  * 
135                  * 
136                  * TODO: This causes NullPointerExceptions when used from contains().
137                  */
138                 @Override
139                 public boolean equals(final Object obj) {
140                         if (this == obj) {
141                                 return true;
142                         }
143
144                         if (obj == null) {
145                                 return false;
146                         }
147
148                         if (!(obj instanceof UsesRelationship)) {
149                                 return false;
150                         }
151
152                         final UsesRelationship other = (UsesRelationship) obj;
153                         if (relationship == null) {
154                                 if (other.relationship != null) {
155                                         return false;
156                                 }
157                         } else if (!relationship_equals(relationship, other.relationship)) {
158                                 return false;
159                         }
160
161                         if (relatedRelationship == null) {
162                                 if (other.relatedRelationship != null) {
163                                         return false;
164                                 }
165                         } else if (!relationship_equals(relatedRelationship, other.relatedRelationship)) {
166                                 return false;
167                         }
168
169                         return true;
170                 }
171
172                 /**
173                  * We use this utility function because Relationship.equals() fails in the the generated SWIG C++ code with a
174                  * NullPointerException.
175                  */
176                 public static boolean relationship_equals(final Relationship a, final Relationship b) {
177                         if (a == null) {
178                                 if (b == null) {
179                                         return true;
180                                 } else {
181                                         return false;
182                                 }
183                         }
184
185                         if (b == null) {
186                                 return false;
187                         }
188
189                         final String a_name = a.get_name();
190                         final String b_name = b.get_name();
191
192                         if (!StringUtils.equals(a_name, b_name)) { // TODO: And the rest.
193                                 return false;
194                         }
195
196                         return true;
197                 }
198         }
199
200         // TODO: Change to final ArrayList<LayoutItem_Field> fieldsToGet
201         public static String build_sql_select_with_key(final Connection connection, final String tableName,
202                         final LayoutFieldVector fieldsToGet, final Field primaryKey, final Value gdaPrimaryKeyValue) {
203
204                 Condition whereClause = null; // Note that we ignore quickFind.
205                 if (gdaPrimaryKeyValue != null) {
206                         whereClause = build_simple_where_expression(tableName, primaryKey, gdaPrimaryKeyValue);
207                 }
208
209                 final SortClause sortClause = null; // Ignored.
210                 return build_sql_select_with_where_clause(connection, tableName, fieldsToGet, whereClause, sortClause);
211         }
212
213         public static Condition build_simple_where_expression(final String tableName, final Field primaryKey,
214                         final Value gdaPrimaryKeyValue) {
215
216                 Condition result = null;
217
218                 if (primaryKey == null) {
219                         return result;
220                 }
221
222                 final String fieldName = primaryKey.get_name();
223                 if (StringUtils.isEmpty(fieldName)) {
224                         return result;
225                 }
226
227                 // TODO: field() takes general SQL, not specifically a field name, so this is unsafe.
228                 final String sqlFieldName = get_sql_field_name(tableName, fieldName);
229                 final org.jooq.Field<Object> field = Factory.field(sqlFieldName);
230                 result = field.equal(gdaPrimaryKeyValue.get_double()); // TODO: Handle other types too.
231                 return result;
232         }
233
234         /*
235          * private static String build_sql_select_with_where_clause(final Connection connection, final String tableName,
236          * final LayoutFieldVector fieldsToGet) { final Condition whereClause = null; return
237          * build_sql_select_with_where_clause(connection, tableName, fieldsToGet, whereClause); }
238          */
239
240         /*
241          * private static String build_sql_select_with_where_clause(final Connection connection, final String tableName,
242          * final LayoutFieldVector fieldsToGet, final Condition whereClause) { final SortClause sortClause = null; return
243          * build_sql_select_with_where_clause(connection, tableName, fieldsToGet, whereClause, sortClause); }
244          */
245
246         public static String build_sql_select_with_where_clause(final Connection connection, final String tableName,
247                         final LayoutFieldVector fieldsToGet, final Condition whereClause, final SortClause sortClause) {
248                 final SelectFinalStep step = build_sql_select_step_with_where_clause(connection, tableName, fieldsToGet,
249                                 whereClause, sortClause);
250                 if (step == null) {
251                         return "";
252                 }
253
254                 final String query = step.getQuery().getSQL(true);
255                 // Log.info("Query: " + query);
256                 return query;
257         }
258
259         private static SelectSelectStep createSelect(final Connection connection) {
260                 final Factory factory = new Factory(connection, SQLDialect.POSTGRES);
261                 final Settings settings = factory.getSettings();
262                 settings.setRenderNameStyle(RenderNameStyle.QUOTED); // TODO: This doesn't seem to have any effect.
263                 settings.setRenderKeywordStyle(RenderKeywordStyle.UPPER); // TODO: Just to make debugging nicer.
264
265                 final SelectSelectStep selectStep = factory.select();
266                 return selectStep;
267         }
268
269         private static SelectFinalStep build_sql_select_step_with_where_clause(final Connection connection,
270                         final String tableName, final LayoutFieldVector fieldsToGet, final Condition whereClause,
271                         final SortClause sortClause) {
272
273                 final SelectSelectStep selectStep = createSelect(connection);
274
275                 // Add the fields, and any necessary joins:
276                 final List<UsesRelationship> listRelationships = build_sql_select_add_fields_to_get(selectStep, tableName,
277                                 fieldsToGet, sortClause, false /* extraJoin */);
278
279                 final SelectJoinStep joinStep = selectStep.from(tableName);
280
281                 // LEFT OUTER JOIN will get the field values from the other tables,
282                 // and give us our fields for this table even if there is no corresponding value in the other table.
283                 for (final UsesRelationship usesRelationship : listRelationships) {
284                         builder_add_join(joinStep, usesRelationship);
285                 }
286
287                 SelectFinalStep finalStep = joinStep;
288                 if (whereClause != null) {
289                         finalStep = joinStep.where(whereClause);
290                 }
291
292                 return finalStep;
293         }
294
295         public static String build_sql_count_select_with_where_clause(final Connection connection, final String tableName,
296                         final LayoutFieldVector fieldsToGet) {
297                 final SelectFinalStep selectInner = build_sql_select_step_with_where_clause(connection, tableName, fieldsToGet,
298                                 null, null);
299                 return build_sql_select_count_rows(connection, selectInner);
300         }
301
302         public static String build_sql_count_select_with_where_clause(final Connection connection, final String tableName,
303                         final LayoutFieldVector fieldsToGet, final Condition whereClause) {
304                 final SelectFinalStep selectInner = build_sql_select_step_with_where_clause(connection, tableName, fieldsToGet,
305                                 whereClause, null);
306                 return build_sql_select_count_rows(connection, selectInner);
307         }
308
309         private static String build_sql_select_count_rows(final Connection connection, final SelectFinalStep selectInner) {
310                 // TODO: Find a way to do this with the jOOQ API:
311                 final SelectSelectStep select = createSelect(connection);
312
313                 final org.jooq.Field<?> field = Factory.field("*");
314                 final AggregateFunction<?> count = Factory.count(field);
315                 select.select(count).from(selectInner);
316                 return select.getQuery().getSQL(true);
317                 // return "SELECT COUNT(*) FROM (" + query + ") AS glomarbitraryalias";
318         }
319
320         private static List<UsesRelationship> build_sql_select_add_fields_to_get(SelectSelectStep step,
321                         final String tableName, final LayoutFieldVector fieldsToGet, final SortClause sortClause,
322                         final boolean extraJoin) {
323
324                 // Get all relationships used in the query:
325                 final List<UsesRelationship> listRelationships = new ArrayList<UsesRelationship>();
326
327                 final int layoutFieldsSize = Utils.safeLongToInt(fieldsToGet.size());
328                 for (int i = 0; i < layoutFieldsSize; i++) {
329                         final LayoutItem_Field layout_item = fieldsToGet.get(i);
330                         add_to_relationships_list(listRelationships, layout_item);
331                 }
332
333                 if (sortClause != null) {
334                         final int sortFieldsSize = Utils.safeLongToInt(sortClause.size());
335                         for (int i = 0; i < sortFieldsSize; i++) {
336                                 final SortFieldPair pair = sortClause.get(i);
337                                 final LayoutItem_Field layout_item = pair.getFirst();
338                                 add_to_relationships_list(listRelationships, layout_item);
339                         }
340                 }
341
342                 boolean one_added = false;
343                 for (int i = 0; i < layoutFieldsSize; i++) {
344                         final LayoutItem_Field layout_item = fieldsToGet.get(i);
345
346                         if (layout_item == null) {
347                                 // g_warn_if_reached();
348                                 continue;
349                         }
350
351                         // Get the parent, such as the table name, or the alias name for the join:
352                         // final String parent = layout_item.get_sql_table_or_join_alias_name(tableName);
353
354                         /*
355                          * TODO: const LayoutItem_FieldSummary* fieldsummary = dynamic_cast<const
356                          * LayoutItem_FieldSummary*>(layout_item.obj()); if(fieldsummary) { const Gnome::Gda::SqlBuilder::Id
357                          * id_function = builder->add_function( fieldsummary->get_summary_type_sql(),
358                          * builder->add_field_id(layout_item->get_name(), tableName)); builder->add_field_value_id(id_function); }
359                          * else {
360                          */
361                         final String sql_field_name = get_sql_field_name(tableName, layout_item);
362                         if (!StringUtils.isEmpty(sql_field_name)) {
363                                 // TODO Factory.field() takes SQL, which can be a field name,
364                                 // but this does not interpret it as a field name, so this is unsafe.
365                                 final org.jooq.Field<?> field = Factory.field(sql_field_name);
366                                 step = step.select(field);
367
368                                 // Avoid duplicate records with doubly-related fields:
369                                 // TODO: if(extra_join)
370                                 // builder->select_group_by(id);
371                         }
372                         // }
373
374                         one_added = true;
375                 }
376
377                 if (!one_added) {
378                         // TODO: std::cerr << G_STRFUNC << ": No fields added: fieldsToGet.size()=" << fieldsToGet.size() <<
379                         // std::endl;
380                         return listRelationships;
381                 }
382
383                 return listRelationships;
384         }
385
386         private static String get_sql_field_name(final String tableName, final String fieldName) {
387
388                 if (StringUtils.isEmpty(tableName)) {
389                         return "";
390                 }
391
392                 if (StringUtils.isEmpty(fieldName)) {
393                         return "";
394                 }
395
396                 // TODO: Quoting, escaping, etc:
397                 return tableName + "." + fieldName;
398         }
399
400         private static String get_sql_field_name(final String tableName, final LayoutItem_Field layoutItemField) {
401
402                 if (layoutItemField == null) {
403                         return "";
404                 }
405
406                 if (StringUtils.isEmpty(tableName)) {
407                         return "";
408                 }
409
410                 // TODO: Quoting, escaping, etc:
411                 return get_sql_field_name(layoutItemField.get_sql_table_or_join_alias_name(tableName),
412                                 layoutItemField.get_name());
413         }
414
415         private static void add_to_relationships_list(final List<UsesRelationship> listRelationships,
416                         final LayoutItem_Field layout_item) {
417
418                 if (layout_item == null) {
419                         return;
420                 }
421
422                 if (!layout_item.get_has_relationship_name()) {
423                         return;
424                 }
425
426                 // If this is a related relationship, add the first-level relationship too, so that the related relationship can
427                 // be defined in terms of it:
428                 // TODO: //If the table is not yet in the list:
429                 if (layout_item.get_has_related_relationship_name()) {
430                         final UsesRelationship usesRel = new UsesRelationship();
431                         usesRel.setRelationship(layout_item.get_relationship());
432
433                         // Remove any UsesRelationship that has only the same relationship (not related relationship),
434                         // to avoid adding that part of the relationship to the SQL twice (two identical JOINS).
435                         // listRemoveIfUsesRelationship(listRelationships, usesRel.getRelationship());
436
437                         if (!listRelationships.contains(usesRel)) {
438                                 // These need to be at the front, so that related relationships can use
439                                 // them later in the SQL statement.
440                                 listRelationships.add(usesRel);
441                         }
442
443                 }
444
445                 // Add the relationship to the list:
446                 final UsesRelationship usesRel = new UsesRelationship();
447                 usesRel.setRelationship(layout_item.get_relationship());
448                 usesRel.setRelatedRelationship(layout_item.get_related_relationship());
449                 if (!listRelationships.contains(usesRel)) {
450                         listRelationships.add(usesRel);
451                 }
452
453         }
454
455         /**
456          * @param listRelationships
457          * @param relationship
458          */
459         /*
460          * private static void listRemoveIfUsesRelationship(final List<UsesRelationship> listRelationships, final
461          * Relationship relationship) { if (relationship == null) { return; }
462          * 
463          * final Iterator<UsesRelationship> i = listRelationships.iterator(); while (i.hasNext()) { final UsesRelationship
464          * eachUsesRel = i.next(); if (eachUsesRel == null) continue;
465          * 
466          * // Ignore these: if (eachUsesRel.getHasRelatedRelationshipName()) { continue; }
467          * 
468          * final Relationship eachRel = eachUsesRel.getRelationship(); if (eachRel == null) { continue; }
469          * 
470          * Log.info("Checking: rel name=" + relationship.get_name() + ", eachRel name=" + eachRel.get_name());
471          * 
472          * if (UsesRelationship.relationship_equals(relationship, eachRel)) { i.remove(); Log.info("  Removed"); } else {
473          * Log.info(" not equal"); }
474          * 
475          * } }
476          */
477
478         private static void builder_add_join(SelectJoinStep step, final UsesRelationship uses_relationship) {
479                 final Relationship relationship = uses_relationship.getRelationship();
480                 if (!relationship.get_has_fields()) { // TODO: Handle related_record has_fields.
481                         if (relationship.get_has_to_table()) {
482                                 // It is a relationship that only specifies the table, without specifying linking fields:
483
484                                 // TODO: from() takes SQL, not specifically a table name, so this is unsafe.
485                                 // TODO: stepResult = step.from(relationship.get_to_table());
486                         }
487
488                         return;
489                 }
490
491                 // Define the alias name as returned by get_sql_join_alias_name():
492
493                 // Specify an alias, to avoid ambiguity when using 2 relationships to the same table.
494                 final String alias_name = uses_relationship.get_sql_join_alias_name();
495
496                 // Add the JOIN:
497                 if (!uses_relationship.getHasRelatedRelationshipName()) {
498
499                         final String sql_field_name_from = get_sql_field_name(relationship.get_from_table(),
500                                         relationship.get_from_field());
501                         final org.jooq.Field<Object> fieldFrom = Factory.field(sql_field_name_from);
502                         final String sql_field_name_to = get_sql_field_name(alias_name, relationship.get_to_field());
503                         final org.jooq.Field<Object> fieldTo = Factory.field(sql_field_name_to);
504                         final Condition condition = fieldFrom.equal(fieldTo);
505
506                         // TODO: join() takes SQL, not specifically an alias name, so this is unsafe.
507                         // Note that LEFT JOIN (used in libglom/GdaSqlBuilder) is apparently the same as LEFT OUTER JOIN.
508                         step = step.leftOuterJoin(relationship.get_to_table() + " AS " + alias_name).on(condition);
509                 } else {
510                         final UsesRelationship parent_relationship = new UsesRelationship();
511                         parent_relationship.setRelationship(relationship);
512                         final Relationship relatedRelationship = uses_relationship.getRelatedRelationship();
513
514                         final String sql_field_name_from = get_sql_field_name(parent_relationship.get_sql_join_alias_name(),
515                                         relatedRelationship.get_from_field());
516                         final org.jooq.Field<Object> fieldFrom = Factory.field(sql_field_name_from);
517                         final String sql_field_name_to = get_sql_field_name(alias_name, relatedRelationship.get_to_field());
518                         final org.jooq.Field<Object> fieldTo = Factory.field(sql_field_name_to);
519                         final Condition condition = fieldFrom.equal(fieldTo);
520
521                         // TODO: join() takes SQL, not specifically an alias name, so this is unsafe.
522                         // Note that LEFT JOIN (used in libglom/GdaSqlBuilder) is apparently the same as LEFT OUTER JOIN.
523                         step = step.leftOuterJoin(relatedRelationship.get_to_table() + " AS " + alias_name).on(condition);
524                 }
525         }
526
527         public static Condition get_find_where_clause_quick(final Document document, final String tableName,
528                         final Value quickFindValue) {
529                 if (StringUtils.isEmpty(tableName)) {
530                         return null;
531                 }
532
533                 // TODO: if(Conversions::value_is_empty(quick_search))
534                 // return Gnome::Gda::SqlExpr();
535
536                 Condition condition = null;
537
538                 // TODO: Cache the list of all fields, as well as caching (m_Fields) the list of all visible fields:
539                 final FieldVector fields = document.get_table_fields(tableName);
540
541                 final int fieldsSize = Utils.safeLongToInt(fields.size());
542                 for (int i = 0; i < fieldsSize; i++) {
543                         final Field field = fields.get(i);
544                         if (field == null) {
545                                 continue;
546                         }
547
548                         if (field.get_glom_type() != Field.glom_field_type.TYPE_TEXT) {
549                                 continue;
550                         }
551
552                         final String sql_field_name = get_sql_field_name(tableName, field.get_name());
553                         final org.jooq.Field<Object> jooqField = Factory.field(sql_field_name);
554                         final Condition thisCondition = jooqField.equal(quickFindValue.get_string());
555
556                         if (condition == null) {
557                                 condition = thisCondition;
558                         } else {
559                                 condition = condition.or(thisCondition);
560                         }
561                 }
562
563                 return condition;
564         }
565 }