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" > ? 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 — 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}