All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.inversion.jdbc.SqlQuery Maven / Gradle / Ivy

There is a newer version: 0.10.2
Show newest version
/*
 * Copyright (c) 2015-2019 Rocket Partners, LLC
 * https://github.com/inversion-api
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.inversion.jdbc;

import io.inversion.*;
import io.inversion.rql.*;
import io.inversion.rql.Order.Sort;
import io.inversion.utils.Rows;
import io.inversion.utils.Utils;

import java.sql.Connection;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;

/**
 * Composes and executes a SQL SELECT based on supplied RQL Terms.
 */
public class SqlQuery extends Query, SqlQuery>, From, SqlQuery>, Where, SqlQuery>, Group, SqlQuery>, Order, SqlQuery>, Page, SqlQuery>> {

    protected char stringQuote = '\'';
    protected char columnQuote = '"';

    String type = null;

    LinkedHashMap joins;

//    public SqlQuery() {
//
//    }

    public SqlQuery(D db, Collection table, List terms) {
        super(db, table, terms, "_query");

        //-- this is done in the super call but this second call 
        //-- is required to set the columnQuote after member variable
        //-- initialization happens AFTER the super call.
        withDb(db);
    }

    protected boolean addTerm(String token, Term term) {

        if (term.hasToken("eq")) {
            String name = term.getToken(0);

            //TODO document this
            if (name.endsWith(".select"))
                return true;
        }

        if (term.hasToken("join")) {
            if (joins == null)
                joins = new LinkedHashMap<>();

            //a map is used to avoid adding duplicate joins
            String key = term.toString();
            if (!joins.containsKey(key)) {
                joins.put(key, term);
            }
            return true;
        }

        return super.addTerm(token, term);
    }

    @Override
    public Results doSelect() throws ApiException {
        JdbcDb db  = (JdbcDb) getDb();
        String sql = getPreparedStmt();

        Results results = new Results(this);
        List    values  = getColValues();

        //-- for test cases and query explain
        String debug = getClass().getSimpleName() + " " + getType() + ": " + sql + " args=" + values;
        debug = debug.replaceAll("\r", "");
        debug = debug.replaceAll("\n", " ");
        debug = debug.replaceAll(" +", " ");
        Chain.debug(debug);
        results.withTestQuery(debug);

        if (!isDryRun()) {
            Connection conn = db.getConnection();
            //-- prepared statement variables are computing during the
            //-- generation of the prepared statement above

            try {
                Rows rows      = JdbcUtils.selectRows(conn, sql, values);
                int  foundRows = rows.size();

                int limit = getPage().getLimit();
                int page  = getPage().getOffset();

                //-- don't query for total row count if the DB returned fewer that
                //-- the max number of rows on the first page...foundRows must be
                //-- number of rows we just found
                boolean needsPaging = page > 1 || foundRows == limit;
                if (needsPaging) {
                    if (Chain.peek().get("foundRows") == null && Chain.first().getRequest().isMethod("GET")) {
                        if (rows.size() == 0) {
                            foundRows = 0;
                        } else {

                            foundRows = queryFoundRows(conn, sql, values);
                        }

                        Chain.peek().put("foundRows", foundRows);
                    }
                }

                results.withFoundRows(foundRows);
                results.withRows(rows);
            } catch (Exception ex) {
                //System.out.println(sql);
                ex.printStackTrace();
                throw ApiException.new500InternalServerError(ex);
            }
        }

        return results;
    }

    public String getPreparedStmt() {
        return toSql(true);
    }

    public String getDynamicStmt() {
        return toSql(false);
    }

    protected String toSql(boolean preparedStmt) {
        clearValues();
        Parts parts = new Parts();

        printInitialSelect(parts);
        printTermsSelect(parts, preparedStmt);
        //printJoins(parts, joins);
        printWhereClause(parts, getWhere().getFilters(), preparedStmt);
        printGroupClause(parts, getGroup().getGroupBy());
        printOrderClause(parts, getOrder().getSorts());
        printLimitClause(parts, getPage().getOffset(), getPage().getLimit());

        String sql = printSql(parts);
        return sql;
    }

    protected String printSql(Parts parts) {
        //--compose the final statement
        String buff = parts.select;

        buff += " \r\n" + parts.from;

        if (parts.where != null)
            buff += " \r\n" + parts.where;

        if (parts.select.toLowerCase().startsWith("select ")) {
            if (parts.group != null)
                buff += " \r\n" + parts.group;

            if (parts.order != null)
                buff += " \r\n" + parts.order;

            if (parts.limit != null)
                buff += " \r\n" + parts.limit;
        }

        String sql = buff;
        return sql;
    }

    protected String printInitialSelect(Parts parts) {
        String initialSelect = (String) find("_query", 0);

        if (initialSelect == null) {
            String  expression = getFrom().getSubquery();
            String  alias      = quoteCol(getFrom().getAlias());
            boolean distinct   = getSelect().isDistinct();

            if (expression != null) {
                initialSelect = " SELECT " + (distinct ? "DISTINCT " : "") + alias + ".* FROM (" + expression + ")";
            } else {
                expression = quoteCol(getFrom().getTable());
                initialSelect = " SELECT " + (distinct ? "DISTINCT " : "") + alias + ".* FROM " + expression;
            }

            if (!expression.equals(alias)) {
                initialSelect += " AS " + alias;
            }
        }

        parts.withSql(initialSelect);
        return initialSelect;
    }

    protected String printTermsSelect(Parts parts, boolean preparedStmt) {
        StringBuilder cols = new StringBuilder();

        List terms = getSelect().columns();
        for (int i = 0; i < terms.size(); i++) {
            Term term = terms.get(i);
            if (term.hasToken("as")) {
                Term function = term.getTerm(0);
                cols.append(" ").append(printTerm(function, null, preparedStmt));

                String colName = term.getToken(1);
                if (!(Utils.empty(colName) || colName.contains("$$$ANON"))) {
                    //TODO: validate if this mysql special case is required...will require changing tests if removed
                    if (db == null || db.isType("mysql"))
                        cols.append(" AS ").append(asString(colName));
                    else
                        cols.append(" AS ").append(quoteCol(colName));
                }
            } else if (!term.getToken().contains(".")) {
                cols.append(" ").append(printCol(term.getToken()));
            }

            if (i < terms.size() - 1)
                cols.append(", ");
        }

        if (cols.length() > 0) {
            boolean aggregate = find("sum", "min", "max", "count", "function", "aggregate", "distinct") != null;
            //boolean restrictCols = find("includes", "sum", "min", "max", "count", "function", "aggregate") != null;
            boolean restrictCols = find("includes") != null || aggregate;

            if (db == null || db.isType("mysql")) {
                //-- this a special case for legacy mysql that does not require grouping each column when aggregating
                restrictCols = find("includes") != null;
            }

            int star = parts.select.lastIndexOf("* ");
            if (restrictCols && star > 0) {
                //force the inclusion of pk cols even if there
                //were not requested by the caller.  Actions such
                //as RestGetHandler need the pk values to do
                //anything interesting with the results and they
                //are responsible for filtering out the values
                //
                //TODO: maybe this should be put into Select.columns()
                //so all query subclasses will inherit the behavior???
                if (!aggregate) {
                    if (getCollection() != null) {
                        Index primaryIndex = getCollection().getPrimaryIndex();
                        if (primaryIndex != null) {
                            for (String colName : primaryIndex.getColumnNames()) {
                                if (cols.indexOf(printCol(colName)) < 0)
                                    cols.append(", ").append(printCol(colName));
                            }
                        }
                    }
                }

                //inserts the select list before the *
                int    idx       = parts.select.substring(0, star).indexOf(" ");
                String newSelect = parts.select.substring(0, idx) + cols + parts.select.substring(star + 1);
                parts.select = newSelect;
            } else {
                String select = parts.select.trim();
                if (!select.toLowerCase().endsWith("select"))
                    select += ", ";
                select += cols;

                parts.select = select;
            }
        }

        if (getSelect().isDistinct() && !parts.select.toLowerCase().contains("distinct")) {
            int idx = parts.select.toLowerCase().indexOf("select") + 6;
            parts.select = parts.select.substring(0, idx) + " DISTINCT " + parts.select.substring(idx);
        }

        if (Chain.peek() != null && Chain.peek().get("foundRows") == null && "mysql".equalsIgnoreCase(getType()) && parts.select.toLowerCase().trim().startsWith("select")) {
            int idx = parts.select.toLowerCase().indexOf("select") + 6;
            parts.select = parts.select.substring(0, idx) + " SQL_CALC_FOUND_ROWS " + parts.select.substring(idx);
        }

        return parts.select;
    }

    protected String printJoins(Parts parts, LinkedHashMap joins) {
        if (joins != null) {
            for (Entry joinTerm : joins.entrySet()) {
                Term join = joinTerm.getValue();

                StringBuilder where = new StringBuilder();

                for (int j = 2; j < join.size(); j += 4)//the first token is the related name, the second token is the table alias
                {
                    if (j > 2)
                        where.append(" AND ");
                    where.append(quoteCol(join.getToken(j))).append(".").append(quoteCol(join.getToken(j + 1))).append(" = ").append(quoteCol(join.getToken(j + 2))).append(".").append(quoteCol(join.getToken(j + 3)));
                }

                if (where != null) {
                    where = new StringBuilder("(" + where + ")");

                    if (Utils.empty(parts.where))
                        parts.where = " WHERE " + where;
                    else
                        parts.where += " AND " + where;
                }
            }
        }
        return parts.where;
    }

    //   protected Term findJoinTerm(Term term)
    //   {
    //      if (!this.joins.isEmpty())
    //      {
    //         String s = term.getToken(0);
    //         if (s.contains("."))
    //         {
    //            String[] arr = s.split("\\.");
    //            for (Entry joinTerm : joins.entrySet())
    //            {
    //               Term join = joinTerm.getValue();
    //               if (join.getToken(1).equals(arr[0]))
    //               {
    //                  return join;
    //               }
    //            }
    //         }
    //      }
    //      return null;
    //   }

    //   protected String printJoinSubselect(Term term, String col, boolean preparedStmt, Term joinTerm)
    //   {
    //      // (`Location`.`id` not in(select locationId from LocationTag where `content` LIKE '%froster%'))
    //
    //      String t1 = joinTerm.getToken(2);
    //      String c1 = joinTerm.getToken(3);
    //
    //      String t2 = joinTerm.getToken(0);
    //      String c2 = joinTerm.getToken(5);
    //
    //      String alias = joinTerm.getToken(1);
    //
    //      boolean negation = term.hasToken("ne", "nw", "wo");
    //
    //      String in = " in (select ";
    //      if (negation)
    //      {
    //         in = " not in (select ";
    //
    //         // need to swap to the positive form since it will be a "not in"
    //         Map ops = new HashMap<>();
    //         ops.put("ne", "eq");
    //         ops.put("nw", "w");
    //         ops.put("wo", "w");
    //         term.withToken(ops.get(term.getToken()));
    //      }
    //
    //      String where = printTerm(term, col, preparedStmt);
    //      String subselect = quoteCol(t1) + "." + quoteCol(c1) + in + quoteCol(c2) + " from " + quoteCol(t2) + " " + quoteCol(alias) + " where " + where + ") ";
    //
    //      return subselect;
    //   }

    protected String printWhereClause(Parts parts, List terms, boolean preparedStmt) {
        for (Term term : terms) {
            String where = printTerm(term, null, preparedStmt);
            if (where != null) {
                if (Utils.empty(parts.where))
                    parts.where = " WHERE " + where;
                else
                    parts.where += " AND " + where;
            }
        }
        return parts.where;
    }

    protected String printGroupClause(Parts parts, List groupBy) {
        if (groupBy.size() > 0) {

            StringBuilder group = new StringBuilder(parts.group != null ? parts.group : "GROUP BY ");

            for (String column : groupBy) {
                if (!Utils.endsWith(group, "GROUP BY "))
                    group.append(", ");
                group.append(printCol(column));
            }
            parts.group = group.toString();
        }
        return parts.group;
    }

    protected String printOrderClause(Parts parts, List sorts) {
        //-- before printing the "order by" statement, if the users did not supply
        //-- any sort terms but the primary index key is being selected, sort on the
        //-- primary index.
        //-- TODO: can this be moved into the Order builder?

        if (sorts.isEmpty()) {
            sorts = getDefaultSorts(parts);
        }

        for (Sort sort : sorts) {
            //-- now setup the "ORDER BY" clause based on the
            //-- "sort" parameter.  Multiple sorts can be
            //-- comma separated and a leading '-' indicates
            //-- descending sort for that field
            //--
            //-- ex: "sort=firstName,-age"

            if (parts.order == null)
                parts.order = "ORDER BY ";

            if (!parts.order.endsWith("ORDER BY "))
                parts.order += ", ";

            parts.order += printCol(sort.getProperty()) + (sort.isAsc() ? " ASC" : " DESC");
        }
        return parts.order;
    }

    protected List getDefaultSorts(Parts parts) {
        List sorts = new ArrayList<>();

        boolean wildcard = parts.select.contains("* ") //
                || parts.select.contains("*,");//this trailing space or comma is important because otherwise this would incorrectly match "COUNT(*)"

        if (collection != null && collection.getPrimaryIndex() != null) {
            for (String pkCol : collection.getPrimaryIndex().getColumnNames()) {
                if (wildcard || parts.select.contains(quoteCol(pkCol))) {
                    Sort sort = new Sort(pkCol, true);
                    sorts.add(sort);
                }
            }
        }

        //-- in this case the primaryIndex cols are not included in the select
        //-- meaning this must be some time of aggregate...so we will do a
        //-- full sort on all selected columns

        //-- TODO: make test case for this
        if (sorts.size() == 0) {
            boolean hasCol = false;
            for (Term term : select.columns()) {
                if (term.isLeaf() || term.hasToken("as") && term.getTerm(0).isLeaf()) {
                    hasCol = true;
                    break;
                }
            }

            if (!hasCol)
                return sorts;

            Collection coll = getCollection();
            if (coll != null) {
                for (Property prop : coll.getProperties()) {
                    if (parts.select.contains(quoteCol(prop.getColumnName())))
                        sorts.add(new Sort(prop.getColumnName(), true));
                }
            } else {
                for (String col : getSelect().getColumnNames()) {
                    sorts.add(new Sort(col, true));
                }
            }
        }

        return sorts;

    }

    protected String printLimitClause(Parts parts, int offset, int limit) {
        if (Utils.empty(parts.order))
            return "";

        String s = null;
        if (limit >= 0 || offset >= 0) {
            if (db == null || getDb().isType("mysql")) {
                s = "LIMIT ";
                if (offset > 0)
                    s += offset;

                if (limit >= 0) {
                    if (!s.endsWith("LIMIT "))
                        s += ", ";

                    s += limit;
                }
            } else if (getDb().isType("sqlserver")) {
                //https://docs.microsoft.com/en-us/sql/t-sql/queries/select-order-by-clause-transact-sql?view=sql-server-2017#Offset
                //OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY

                s = "";
                if (offset >= 0)
                    s += " OFFSET " + offset + " ROWS ";

                if (limit >= 0)
                    s += " FETCH NEXT " + limit + " ROWS ONLY ";
            } else {
                s = "";

                if (limit >= 0)
                    s += " LIMIT " + limit;

                if (offset >= 0)
                    s += " OFFSET " + offset;
            }
        }

        parts.limit = s;
        return s;
    }

    protected int queryFoundRows(Connection conn, String sql, List values) throws Exception {
        int foundRows;
        if (db.isType("mysql")) {
            sql = "SELECT FOUND_ROWS()";
            foundRows = JdbcUtils.selectInt(conn, sql);
        } else {
            if (sql.indexOf("LIMIT ") > 0)
                sql = sql.substring(0, sql.lastIndexOf("LIMIT "));

            if (sql.indexOf("OFFSET ") > 0)
                sql = sql.substring(0, sql.lastIndexOf("OFFSET "));

            if (sql.indexOf("ORDER BY ") > 0)
                sql = sql.substring(0, sql.lastIndexOf("ORDER BY "));

            sql = "SELECT count(1) FROM ( " + sql + " ) as q";

            foundRows = JdbcUtils.selectInt(conn, sql, values);
        }
        return foundRows;
    }

    protected String printTerm(Term term, String col, boolean preparedStmt) {
        if (term.isLeaf()) {
            String token = term.getToken();

            String value;
            if (isCol(term)) {
                value = printCol(token);
            } else if (isBool(term)) {
                value = asBool(token);
            } else if (isNum(term)) {
                value = asNum(token);
            } else {
                value = asString(term);
            }
            return value;
        }

        for (Term child : term.getTerms()) {
            if (isCol(child)) {
                col = child.token;
                break;
            }
        }

        List dynamicSqlChildText = new ArrayList<>();
        for (Term t : term.getTerms()) {
            dynamicSqlChildText.add(printTerm(t, col, preparedStmt));
        }

        List preparedStmtChildText = new ArrayList(dynamicSqlChildText);

        //allows for callers to substitute callable statement "?"s
        //and/or to account for data type conversion
        if (preparedStmt) {
            for (int i = 0; i < term.size(); i++) {
                Term t = term.getTerm(i);
                if (t.isLeaf()) {
                    String val = preparedStmtChildText.get(i);
                    if (val.charAt(0) != columnQuote) {
                        val = Utils.dequote(val);//go back to the unprinted/quoted version
                        preparedStmtChildText.set(i, replace(term, t, i, col, val));
                    }
                }
            }
        }

        return printExpression(term, dynamicSqlChildText, preparedStmtChildText);

    }

    /**
     * Override to handle printing additional functions or to change the
     * way a specific function is printed.
     *
     * @param term                  the term to print
     * @param dynamicSqlChildText   child tokens printed as dynamic sql
     * @param preparedStmtChildText child tokens printed as prepared statements
     * @return the sql statement for term
     */
    protected String printExpression(Term term, List dynamicSqlChildText, List preparedStmtChildText) {
        String token = term.getToken();

        StringBuilder sql = new StringBuilder();

        if (term.hasToken("eq", "ne", "like", "w", "sw", "ew", "wo")) {
            boolean negation = term.hasToken("ne", "nw", "wo");

            if (term.size() > 2 || negation)
                sql.append("(");

            if (negation)
                sql.append("NOT (");

            String string0 = preparedStmtChildText.get(0);

            for (int i = 1; i < term.size(); i++) {
                String stringI = preparedStmtChildText.get(i);
                if ("null".equalsIgnoreCase(stringI)) {
                    sql.append(string0).append(" IS NULL ");
                } else {
                    boolean wildcard = dynamicSqlChildText.get(i).indexOf('%') >= 0;

                    if (wildcard) {
                        //this postgres engine uses "ilike" instead of "like" for case insensitive matching
                        if (getDb().isType("h2", "postgres", "redshift"))
                            sql.append(string0).append(" ILIKE ").append(stringI);
                        else {
                            sql.append(string0).append(" LIKE ").append(stringI);

                            //-- the escape character in sqlserver must be intentionally
                            //-- identified in the sql
                            //-- @see https://docs.microsoft.com/en-us/sql/connect/jdbc/using-sql-escape-sequences?view=sql-server-ver15
                            if (getDb().isType("sqlserver") //
                                    && dynamicSqlChildText.get(i).indexOf('\\') >= 0) {
                                sql.append(" {escape '\\'}");
                            }
                        }
                    } else {
                        sql.append(string0).append(" = ").append(stringI);
                    }
                }

                if (i < term.size() - 1)
                    sql.append(" OR ");
            }

            if (term.size() > 2 || negation)
                sql.append(")");

            if (negation)
                sql.append(")");
        } else if ("nn".equalsIgnoreCase(token)) {
            sql.append(concatAll(" AND ", " IS NOT NULL", preparedStmtChildText));
        } else if ("emp".equalsIgnoreCase(token)) {
            sql.append("(").append(preparedStmtChildText.get(0)).append(" IS NULL OR ").append(preparedStmtChildText.get(0)).append(" = ").append(asString("")).append(")");
        } else if ("nemp".equalsIgnoreCase(token)) {
            sql.append("(").append(preparedStmtChildText.get(0)).append(" IS NOT NULL AND ").append(preparedStmtChildText.get(0)).append(" != ").append(asString("")).append(")");
        } else if ("n".equalsIgnoreCase(token)) {
            sql.append(concatAll(" AND ", " IS NULL", preparedStmtChildText));
        } else if ("lt".equalsIgnoreCase(token)) {
            sql.append(preparedStmtChildText.get(0)).append(" < ").append(preparedStmtChildText.get(1));
        } else if ("le".equalsIgnoreCase(token)) {
            sql.append(preparedStmtChildText.get(0)).append(" <= ").append(preparedStmtChildText.get(1));
        } else if ("gt".equalsIgnoreCase(token)) {
            sql.append(preparedStmtChildText.get(0)).append(" > ").append(preparedStmtChildText.get(1));
        } else if ("ge".equalsIgnoreCase(token)) {
            sql.append(preparedStmtChildText.get(0)).append(" >= ").append(preparedStmtChildText.get(1));
        } else if ("in".equalsIgnoreCase(token) || "out".equalsIgnoreCase(token)) {
            sql.append(preparedStmtChildText.get(0));

            if ("out".equalsIgnoreCase(token))
                sql.append(" NOT");

            sql.append(" IN(");
            for (int i = 1; i < preparedStmtChildText.size(); i++) {
                sql.append(preparedStmtChildText.get(i));
                if (i < preparedStmtChildText.size() - 1)
                    sql.append(", ");
            }
            sql.append(")");
        } else if ("if".equalsIgnoreCase(token)) {
            if (db == null || db.isType("mysql")) {
                sql.append("IF(").append(preparedStmtChildText.get(0)).append(", ").append(preparedStmtChildText.get(1)).append(", ").append(preparedStmtChildText.get(2)).append(")");
            } else {
                sql.append("CASE WHEN ").append(preparedStmtChildText.get(0)).append(" THEN ").append(preparedStmtChildText.get(1)).append(" ELSE ").append(preparedStmtChildText.get(2)).append(" END");
            }
        } else if ("and".equalsIgnoreCase(token) || "or".equalsIgnoreCase(token)) {
            sql.append("(");
            for (int i = 0; i < preparedStmtChildText.size(); i++) {
                sql.append(preparedStmtChildText.get(i).trim());
                if (i < preparedStmtChildText.size() - 1)
                    sql.append(" ").append(token.toUpperCase()).append(" ");
            }
            sql.append(")");
        } else if ("not".equalsIgnoreCase(token)) {
            sql.append("NOT (");
            for (int i = 0; i < preparedStmtChildText.size(); i++) {
                sql.append(preparedStmtChildText.get(i).trim());
                if (i < preparedStmtChildText.size() - 1)
                    sql.append(" ");
            }
            sql.append(")");
        } else if ("count".equalsIgnoreCase(token)) {
            //String s = "COUNT (1)";
            //sql.append(s);

            String pt   = printTableAlias() + ".*";
            String acol = preparedStmtChildText.get(0);
            if (pt.equals(acol))//reset count(table.*) to count(*)
                acol = "*";

            String s = token.toUpperCase() + "(" + acol + ")";
            sql.append(s);

        } else if ("distinct".equalsIgnoreCase(token)) {
            //TODO: what do you do with this as a function????
        } else if ("sum".equalsIgnoreCase(token) || "min".equalsIgnoreCase(token) || "max".equalsIgnoreCase(token)) {
            String acol = preparedStmtChildText.get(0);
            String s    = token.toUpperCase() + "(" + acol + ")";
            sql.append(s);
        } else if (Utils.in(token.toLowerCase(), "_exists", "_notexists")) {
            if ("_notexists".equalsIgnoreCase(token))
                sql.append("NOT ");
            sql.append("EXISTS (SELECT 1 FROM ");

            ///-----

            //the RQL comes in looking like like eq(relationship.column,value), and Where.java
            //transforms it to eq(~~relTbl_relationship.column,value)

            String relName = term.getTerm(0).getTerm(0).getToken();
            relName = relName.substring(0, relName.indexOf("."));
            relName = relName.substring(relName.indexOf("_") + 1);

            Relationship rel = collection.getRelationship(relName);

            if (rel == null)
                throw ApiException.new400BadRequest("Unable to find relationship for term '{}'", term);

            String relTbl   = rel.getRelated().getTableName();
            String relAlias = "~~relTbl_" + relTbl;

            String lnkTbl   = rel.getFk1Col1().getCollection().getTableName();
            String lnkAlias = "~~lnkTbl_" + lnkTbl;

            sql.append(quoteCol(relTbl)).append(" ").append(quoteCol(relAlias));

            if (rel.isManyToMany()) {
                sql.append(", ").append(quoteCol(lnkTbl)).append(" ").append(quoteCol(lnkAlias));
            }

            ///-----

            sql.append(" WHERE ");

            Index fk1 = rel.getFkIndex1();

            if (rel.isManyToOne()) {
                for (int i = 0; i < fk1.size(); i++) {
                    Property prop   = fk1.getProperty(i);
                    String   pkName = prop.getPk().getColumnName();
                    String   fkName = prop.getColumnName();
                    sql.append(printTableAlias()).append(".").append(quoteCol(fkName)).append(" = ").append(quoteCol(relAlias)).append(".").append(quoteCol(pkName));

                    if (i < fk1.size() - 1)
                        sql.append(" AND ");
                }
            } else if (rel.isOneToMany()) {
                for (int i = 0; i < fk1.size(); i++) {
                    Property prop   = fk1.getProperty(i);
                    String   pkName = prop.getPk().getColumnName();
                    String   fkName = prop.getColumnName();
                    sql.append(printTableAlias()).append(".").append(quoteCol(pkName)).append(" = ").append(quoteCol(relAlias)).append(".").append(quoteCol(fkName));
                    if (i < fk1.size() - 1)
                        sql.append(" AND ");
                }
            } else if (rel.isManyToMany()) {
                for (int i = 0; i < fk1.size(); i++) {
                    Property prop   = fk1.getProperty(i);
                    String   pkName = prop.getPk().getColumnName();
                    String   fkName = prop.getColumnName();
                    sql.append(printTableAlias()).append(".").append(quoteCol(pkName)).append(" = ").append(quoteCol(lnkAlias)).append(".").append(quoteCol(fkName));
                    sql.append(" AND ");
                }

                Index fk2 = rel.getFkIndex2();
                for (int i = 0; i < fk2.size(); i++) {
                    Property prop   = fk2.getProperty(i);
                    String   pkName = prop.getPk().getColumnName();
                    String   fkName = prop.getColumnName();
                    sql.append(quoteCol(lnkAlias)).append(".").append(quoteCol(fkName)).append(" = ").append(quoteCol(relAlias)).append(".").append(quoteCol(pkName));

                    if (i < fk2.size() - 1)
                        sql.append(" AND ");
                }
            }

            sql.append(" AND ");
            for (int i = 0; i < preparedStmtChildText.size(); i++) {
                sql.append(preparedStmtChildText.get(i));
                if (i < preparedStmtChildText.size() - 1)
                    sql.append(" AND ");
            }
            sql.append(")");
        } else if ("miles".equalsIgnoreCase(token)) {
            sql.append("point(").append(preparedStmtChildText.get(0)).append(",").append(preparedStmtChildText.get(1)).append(") <@> point(").append(preparedStmtChildText.get(2)).append(",").append(preparedStmtChildText.get(3)).append(")");
        } else {
            throw new RuntimeException("Unable to parse: " + term);
        }

        return sql.toString();
    }

    protected String replace(Term parent, Term leaf, int index, String col, String val) {
        if (val == null || val.trim().equalsIgnoreCase("null"))
            return "NULL";

        //      if("*".equals(val))
        //         return val;

        if (parent.hasToken("if") && index > 0) {
            if (isBool(leaf))
                return asBool(val);

            if (isNum(leaf))
                return val;
        }

        withColValue(col, val);
        return asVariableName(values.size() - 1);
    }

    protected String concatAll(String connector, String function, List strings) {
        StringBuilder buff = new StringBuilder((strings.size() > 1 ? "(" : ""));
        for (int i = 0; i < strings.size(); i++) {
            buff.append(strings.get(i)).append(function);
            if (i < strings.size() - 1)
                buff.append(connector);
        }
        if (strings.size() > 1)
            buff.append(")");

        return buff.toString();
    }

    protected boolean isCol(Term term) {
        if (!term.isLeaf())
            return false; //this is a function

        if (term.getQuote() == '"')//" is always the term identifier quote
            return true;

        if (term.getQuote() == '\'')
            return false; //this a string as specified by the user in the parsed rql

        if (isBool(term))
            return false;

        if (isNum(term))
            return false;

        if (collection != null && collection.getProperty(term.getToken()) != null)
            return true;

        return term.getParent() != null && term.getParent().indexOf(term) == 0;
    }

    public String quoteCol(String str) {
        StringBuilder buff  = new StringBuilder();
        String[]      parts = str.split("\\.");
        for (int i = 0; i < parts.length; i++) {
            if ("*".equals(parts[i])) {
                buff.append(parts[i]);
            } else {
                if (parts[i].startsWith("~~relTbl_")) {
                    String relName = parts[i];
                    relName = relName.substring(relName.indexOf("_") + 1);

                    Relationship rel = collection.getRelationship(relName);
                    if (rel != null) {
                        parts[i] = "~~relTbl_" + rel.getRelated().getTableName();
                    }
                }

                buff.append(columnQuote).append(parts[i]).append(columnQuote);
            }

            if (i < parts.length - 1)
                buff.append(".");
        }

        return buff.toString();//columnQuote + str + columnQuote;
    }

    public String printTableAlias() {
        String tableName = getFrom().getAlias();

        if (tableName != null) {
            return quoteCol(tableName);
        }

        return "";
    }

    public String printCol(String columnName) {
        if (!columnName.contains(".")) {
            return printTableAlias() + "." + quoteCol(columnName);
        }

        return quoteCol(columnName);
    }

    protected String asVariableName(int valuesPairIdx) {
        return "?";
    }

    protected String asString(String string) {
        return stringQuote + string + stringQuote;
    }

    protected String asString(Term term) {
        String token  = term.token;
        Term   parent = term.getParent();
        if (parent != null && parent.hasToken("eq", "ne", "w", "sw", "ew", "like", "wo")) {
            if (parent.hasToken("w") || parent.hasToken("wo")) {
                if (!token.startsWith("*"))
                    token = "*" + token;

                if (!token.endsWith("*"))
                    token += "*";
            } else if (parent.hasToken("sw") && !token.endsWith("*")) {
                token = token + "*";
            } else if (parent.hasToken("ew") && !token.startsWith("*")) {
                token = "*" + token;
            }

            boolean wildcard = token.indexOf('*') >= 0;
            if (wildcard) {
                token = token.replace("%", "\\%");
                token = token.replace("_", "\\_");
                token = token.replace('*', '%');
            }
        }

        return stringQuote + token + stringQuote;
    }

    protected String asNum(String token) {
        if ("true".equalsIgnoreCase(token))
            return "1";

        if ("false".equalsIgnoreCase(token))
            return "0";

        return token;
    }

    protected boolean isNum(Term term) {
        if (!term.isLeaf() || term.isQuoted())
            return false;

        String token = term.getToken();
        try {
            Double.parseDouble(token);
            return true;
        } catch (Exception ex) {
            //not a number, ignore
        }

        if ("true".equalsIgnoreCase(token))
            return true;

        if ("false".equalsIgnoreCase(token))
            return true;

        return "null".equalsIgnoreCase(token);
    }

    protected boolean isBool(Term term) {
        if (!term.isLeaf() || term.isQuoted())
            return false;

        String token = term.getToken();

        if ("true".equalsIgnoreCase(token))
            return true;

        return "false".equalsIgnoreCase(token);
    }

    public String asBool(String token) {
        if ("true".equalsIgnoreCase(token) || "1".equals(token))
            return "true";

        return "false";
    }

    @Override
    public SqlQuery withDb(D db) {
        super.withDb(db);
        if (db.isType("mysql"))
            withColumnQuote('`');

        return this;
    }

    public SqlQuery withType(String type) {
        this.type = type;
        return this;
    }

    public String getType() {
        if (db != null)
            return db.getType();

        return type;
    }

    public void withStringQuote(char stringQuote) {
        this.stringQuote = stringQuote;
    }

    public void withColumnQuote(char columnQuote) {
        this.columnQuote = columnQuote;
    }

    public static class Parts {

        public String select = null;
        public String from   = null;
        public String where  = null;
        public String group  = null;
        public String order  = null;
        public String limit  = null;

        void withSql(String sql) {
            if (sql == null)
                return;

            sql = sql.trim();

            SqlTokenizer tok = new SqlTokenizer(sql);

            String clause;
            while ((clause = tok.nextClause()) != null) {
                String token = (clause.indexOf(" ") > 0 ? clause.substring(0, clause.indexOf(" ")) : clause).toLowerCase();

                switch (token) {
                    //case "insert":
                    //case "update":
                    case "delete":
                    case "select":
                        select = clause;
                        continue;
                    case "from":
                        from = clause;
                        continue;
                    case "where":
                        where = clause;
                        continue;
                    case "group":
                        group = clause;
                        continue;
                    case "order":
                        order = clause;
                        continue;
                    case "limit":
                        limit = clause;
                        continue;
                    default:
                        throw new RuntimeException("Unable to parse: \"" + clause + "\" - \"" + sql + "\"");
                }
            }
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy