001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.beanquery.sql;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.util.*;
022
023import org.apache.juneau.commons.beanquery.*;
024
025/**
026 * Compiles a resolved {@link Filter} tree into a SQL {@code WHERE} predicate for a {@link SqlDialect}.
027 *
028 * <p>
029 * This is the dialect-agnostic core of the shared JDBC engine.  The terms are already parsed, allow-listed and
030 * type-checked.  The compiler walks each tree and:
031 * <ul>
032 *    <li>combines the terms (one per column) with {@code AND};
033 *    <li>combines a column's {@code $and}/{@code $or}/{@code $not} sub-expressions itself;
034 *    <li>delegates every leaf to the {@link SqlDialect} (a built-in via {@link SqlDialect#builtinRenderer(String)}, a
035 *       bare pattern via {@link SqlDialect#renderBare}) or, for a custom operator, to that operator's
036 *       {@link SearchOperator#extension(Class) SearchSqlRenderer extension}.
037 *    <li>a {@link #compile(Filter)} caller gets the same per-leaf rendering across a resolved, cross-column tree.
038 * </ul>
039 *
040 * <p>
041 * <p>
042 * The SQL result must match the in-memory result, so a built-in the dialect cannot faithfully express is rejected with a
043 * {@link BeanQuerySyntaxException} whose code is {@link org.apache.juneau.commons.beanquery.BeanQuerySyntaxException.Code#BAD_VALUE BAD_VALUE} rather than
044 * rendered differently: an operator for which {@link SqlDialect#builtinRenderer(String)} returns <jk>null</jk> (for
045 * example {@code $regex} on a database without regex support), and a range/ordering operator ({@code $gt}, {@code $gte},
046 * {@code $lt}, {@code $lte}, {@code $between}) on a {@code VERSION} column when
047 * {@link SqlDialect#supportsVersionOrdering()} is <jk>false</jk>.  Equality on versions is unaffected.  A custom
048 * operator with no usable renderer still raises {@link IllegalStateException}.
049 *
050 * <h5 class='section'>Example:</h5>
051 * <p class='bjava'>
052 *    ResolvedQuery <jv>q</jv> = <jv>context</jv>.resolve(BeanQuery.<jsm>create</jsm>().gt(<js>"age"</js>, 21).eq(<js>"name"</js>, <js>"Bob"</js>).build());
053 *    SqlFragment <jv>where</jv> = SqlSearchCompiler.<jsm>create</jsm>(<jv>dialect</jv>, <jv>context</jv>).compile(<jv>q</jv>.filter());
054 *    <jc>// where.sql():   "age" &gt; ? AND "name" = ?</jc>
055 *    <jc>// where.binds(): [21, Bob]</jc>
056 * </p>
057 *
058 * @since 10.0.0
059 */
060public final class SqlSearchCompiler {
061
062   /** Range/ordering built-ins; on a VERSION column these need the dialect to order dotted tuples semantically. */
063   private static final Set<String> VERSION_ORDERING = Set.of("$gt", "$gte", "$lt", "$lte", "$between");
064
065   private final SqlDialect dialect;
066   private final ColumnResolver columns;
067
068   private SqlSearchCompiler(SqlDialect dialect, ColumnResolver columns) {
069      reqnn("dialect", dialect, "columns", columns);
070      this.dialect = dialect;
071      this.columns = columns;
072   }
073
074   /**
075    * Creates a compiler for a dialect and column resolver.
076    *
077    * @param dialect The target dialect.  Must not be <jk>null</jk>.
078    * @param columns Maps validated column names to quoted SQL references.
079    * @return A new compiler.
080    */
081   public static SqlSearchCompiler create(SqlDialect dialect, ColumnResolver columns) {
082      return new SqlSearchCompiler(dialect, columns);
083   }
084
085   /**
086    * Compiles an already-resolved {@link Filter} tree into a boolean {@code WHERE} predicate.
087    *
088    * <p>
089    * Renders the full cross-column {@code Filter.And}/{@code Filter.Or}/{@code Filter.Not} tree, so a top-level
090    * {@code $or} across <i>different</i> columns (for example DataTables' global search) compiles correctly.
091    * {@code Filter.And}/{@code Filter.Or} with no items render as always-true/always-false ({@code 1=1}/{@code 1=0})
092    * per their own contract &mdash; see {@code SqlBeanQueryContext.appendWhere}, which special-cases an empty
093    * top-level filter so an unfiltered query's {@code WHERE} clause does not pick up a spurious {@code (1=1)}.
094    *
095    * <h5 class='section'>Example:</h5>
096    * <p class='bjava'>
097    *    SqlFragment <jv>where</jv> = SqlSearchCompiler.<jsm>create</jsm>(<jv>dialect</jv>, <jv>columns</jv>)
098    *       .compile(<jv>filter</jv>);
099    * </p>
100    *
101    * @param filter The resolved filter, or <jk>null</jk> for no predicate.
102    * @return The predicate fragment, or <jk>null</jk> if {@code filter} is <jk>null</jk>.
103    * @throws BeanQuerySyntaxException (BAD_VALUE) If a built-in leaf is not supported by the dialect.
104    * @throws IllegalStateException If a custom-operator leaf cannot be rendered for the dialect.
105    */
106   public SqlFragment compile(Filter filter) {
107      return filter == null ? null : renderFilter(filter);
108   }
109
110   private SqlFragment renderFilter(Filter node) {
111      if (node instanceof Filter.Leaf leaf)
112         return render(leaf.expression(), columns.columnSql(leaf.column()), leaf.type());
113      if (node instanceof Filter.Not not) {
114         var child = renderFilter(not.item());
115         return not(child);
116      }
117      if (node instanceof Filter.And and) {
118         if (and.items().isEmpty())
119            return SqlFragment.of("1=1");
120         return join(and.items().stream().map(this::renderFilter).toList(), " AND ");
121      }
122      if (node instanceof Filter.Or or) {
123         if (or.items().isEmpty())
124            return SqlFragment.of("1=0");
125         return join(or.items().stream().map(this::renderFilter).toList(), " OR ");
126      }
127      throw new IllegalStateException("Unknown Filter type: " + node);
128   }
129
130   private SqlFragment render(SearchExpression node, String columnSql, SearchType type) {
131      if (node.isLiteral())
132         return required(dialect.renderBare(columnSql, type, node.typedValue(), node.isQuoted()), "bare pattern", node);
133      var op = node.operator();
134      if (op.isCombinator())
135         return renderCombinator(node, columnSql, type);
136      if (op.isCustom()) {
137         var r = op.extension(SearchSqlRenderer.class).orElse(null);
138         if (r == null)
139            throw new IllegalStateException("Custom operator '" + op.name() + "' has no SQL renderer; it cannot run against dialect '" + dialect.id() + "'.");
140         return required(r.render(dialect, columnSql, type, op.typedArgs() ? node.typedArgs() : List.copyOf(node.literalArgs())), "custom operator '" + op.name() + "'", node);
141      }
142      if (type == SearchType.VERSION && VERSION_ORDERING.contains(op.name()) && ! dialect.supportsVersionOrdering())
143         throw new BeanQuerySyntaxException(BeanQuerySyntaxException.Code.BAD_VALUE,
144            "Operator '%s' is not supported on version columns by the '%s' SQL dialect (version ordering is semantic in memory, so it is rejected rather than compared as text).", op.name(), dialect.id());
145      var r = dialect.builtinRenderer(op.name());
146      if (r == null)
147         throw new BeanQuerySyntaxException(BeanQuerySyntaxException.Code.BAD_VALUE,
148            "Operator '%s' is not supported by the '%s' SQL dialect.", op.name(), dialect.id());
149      return required(r.render(dialect, columnSql, type, node.typedArgs()), "operator '" + op.name() + "'", node);
150   }
151
152   private SqlFragment renderCombinator(SearchExpression node, String columnSql, SearchType type) {
153      if ("$not".equals(node.name())) {
154         return not(render(node.args().get(0), columnSql, type));
155      }
156      var children = node.args().stream().map(a -> render(a, columnSql, type)).toList();
157      return join(children, "$or".equals(node.name()) ? " OR " : " AND ");
158   }
159
160   /**
161    * Negates a fragment with two-valued (strictly true/false) semantics.
162    *
163    * <p>
164    * The in-memory engine evaluates every leaf to true or false, so a negated leaf matches a row whose cell is
165    * {@code null}.  In SQL a predicate over a {@code NULL} cell is {@code NULL}, and {@code NOT NULL} is still
166    * {@code NULL}, which would drop the row.  Coalescing the inner predicate to {@code FALSE} first restores the
167    * in-memory result.
168    */
169   private static SqlFragment not(SqlFragment child) {
170      return SqlFragment.of("(NOT COALESCE(" + child.sql() + ", FALSE))", child.binds());
171   }
172
173   /** Joins fragments with a boolean operator, concatenating binds; the whole group is always parenthesized. */
174   private static SqlFragment join(List<SqlFragment> frags, String sep) {
175      var sb = new StringBuilder("(");
176      var binds = new ArrayList<>();
177      for (var i = 0; i < frags.size(); i++) {
178         if (i > 0)
179            sb.append(sep);
180         sb.append(frags.get(i).sql());
181         binds.addAll(frags.get(i).binds());
182      }
183      sb.append(')');
184      return SqlFragment.of(sb.toString(), binds);
185   }
186
187   private SqlFragment required(SqlFragment f, String what, SearchExpression node) {
188      if (f == null)
189         throw new IllegalStateException("Dialect '" + dialect.id() + "' returned no SQL for " + what + " in '" + node + "'.");
190      return f;
191   }
192}