001/* 
002 * JKNIV, whinstone one contract to access your database.
003 * 
004 * Copyright (C) 2017, the original author or authors.
005 *
006 * This library is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 2.1 of the License.
010 * 
011 * This library is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 * 
016 * You should have received a copy of the GNU Lesser General Public
017 * License along with this library; if not, write to the Free Software Foundation, Inc., 
018 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
019 */
020package net.sf.jkniv.whinstone.jdbc.dialect;
021
022import java.sql.PreparedStatement;
023import java.util.Locale;
024import java.util.regex.Matcher;
025
026import net.sf.jkniv.sqlegance.dialect.AnsiDialect;
027import net.sf.jkniv.sqlegance.dialect.SqlFeatureFactory;
028import net.sf.jkniv.sqlegance.dialect.SqlFeatureSupport;
029
030/**
031 * Dialect to SQLServer
032 * 
033 * <p>
034 * Limit clause:
035 *  <code>select TOP 2 name from author</code>
036 * </p>
037 * 
038 * <ul>
039 *  <li>Supports limits? true</li>
040 *  <li>Supports limit off set? false</li>
041 *  <li>Supports rownum? false</li>
042 * </ul>
043 *
044 * @author Alisson Gomes 
045 * @since 0.6.0
046 */
047public class SqlServerDialect extends AnsiDialect
048{
049    public SqlServerDialect()
050    {
051        super();
052        addFeature(SqlFeatureFactory.newInstance(SqlFeatureSupport.LIMIT, true));
053        addFeature(SqlFeatureFactory.newInstance(SqlFeatureSupport.LIMIT_OFF_SET, true));
054        addFeature(SqlFeatureFactory.newInstance(SqlFeatureSupport.STMT_HOLDABILITY, false));
055    }
056    
057    /**
058     *  LIMIT clause for SqlServer, where TOP is a parameter from
059     *  String.format
060     *  
061     *  @return Return query pattern: 
062     *  <pre>
063     *   {@code
064     *   WITH query AS (
065     *           SELECT inner_query.*
066     *                , ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as _jkniv_rownum_
067     *             FROM ( select ID, SERVICO from ine.configuracao ) inner_query
068     *         )
069     *        SELECT * FROM query WHERE _jkniv_rownum_ >= 3 AND _jkniv_rownum_ < 5 + 3
070     *   }
071     * </pre>
072     */
073    @Override
074    public String getSqlPatternPaging()
075    {
076        //return "select TOP %2$s %1$s";
077        //return "%1$s OFFSET %2$s ROWS FETCH NEXT %3$s ROWS ONLY";
078        final StringBuilder pagingSelect = new StringBuilder(100);
079        pagingSelect.append(" WITH query AS (");
080        pagingSelect.append("  SELECT inner_query.*");
081        pagingSelect.append("      , ROW_NUMBER() OVER (ORDER BY CURRENT_TIMESTAMP) as _jkniv_rownum_");
082        pagingSelect.append("  FROM ( %1$s ) inner_query");
083        pagingSelect.append(" )");
084        pagingSelect.append(" SELECT * FROM query WHERE _jkniv_rownum_ > %2$s AND _jkniv_rownum_ <= %3$s + %2$s");
085        return pagingSelect.toString();
086
087    }
088    
089    //@Override protected void buildSqlLimits()
090    @Override
091    public String buildQueryPaging(final String sqlText, int offset, int max)
092    {
093        String sqlTextPaginated = null;
094//        if (isSelect() && queryable.isPaging())
095//        {
096            //sqlText = queryable.getSql().getSql(queryable.getParams());
097            String pagingSelect = getSqlPatternPaging();
098            //String sqlPreparedToTop = this.sql.replaceFirst("(?i)select", "");
099            if (supportsFeature(SqlFeatureSupport.LIMIT_OFF_SET))
100            {
101                StringBuilder sb = new StringBuilder(sqlText);
102                //final int orderByIndex = shallowIndexOfWord( sb, "order by", 0 );
103                Matcher matcher = sqlEndsWithOrderBy(sqlText);
104                if ( matcher.find() ) {
105                    // ORDER BY requires using TOP.
106                    addTopExpression( sb, offset, max );
107                }
108                sqlTextPaginated = String.format(pagingSelect, sb.toString(), offset, max);
109            }
110////            else
111////                this.sqlWithLimit = String.format(pagingSelect, sqlPreparedToTop, queryable.getMax());
112//        }
113//        else
114//            this.sql = queryable.getSql().getSql(queryable.getParams());
115//        
116//        replaceForQuestionMark();
117        return sqlTextPaginated;
118    }
119    
120    /**
121     * Adds {@code TOP} expression. Parameter value is bind in
122     * {@link #bindLimitParametersAtStartOfQuery(PreparedStatement, int)} method.
123     *
124     * @param sql SQL query.
125     */
126    private void addTopExpression(StringBuilder sql, int offset, int max) {
127        Matcher matcher = super.sqlStartWithSelectOrDistinct(sql.toString());
128        if(matcher.find())
129            sql.insert(matcher.end(), "TOP(" + offset + "+" + max +") " );
130        /*
131        final int distinctStartPos = shallowIndexOfWord( sql, "distinct", 0 );
132        if ( distinctStartPos > 0 ) {
133            // Place TOP after DISTINCT.
134            sql.insert( distinctStartPos + "distinct".length(), " TOP(" + offset + "+" + max +")" );
135        }
136        else {
137            final int selectStartPos = shallowIndexOf( sql, "select ", 0 );
138            // Place TOP after SELECT.
139            sql.insert( selectStartPos + "select".length(), " TOP(" + offset + "+" + max +")" );
140        }
141        */
142    }
143
144    /**
145     * Returns index of the first case-insensitive match of search term surrounded by spaces
146     * that is not enclosed in parentheses.
147     *
148     * @param sb String to search.
149     * @param search Search term.
150     * @param fromIndex The index from which to start the search.
151     *
152     * @return Position of the first match, or {@literal -1} if not found.
153     */
154    private static int shallowIndexOfWord(final StringBuilder sb, final String search, int fromIndex) {
155        final int index = shallowIndexOf( sb, ' ' + search + ' ', fromIndex );
156        // In case of match adding one because of space placed in front of search term.
157        return index != -1 ? ( index + 1 ) : -1;
158    }
159
160    /**
161     * Returns index of the first case-insensitive match of search term that is not enclosed in parentheses.
162     *
163     * @param sb String to search.
164     * @param search Search term.
165     * @param fromIndex The index from which to start the search.
166     *
167     * @return Position of the first match, or {@literal -1} if not found.
168     */
169    private static int shallowIndexOf(StringBuilder sb, String search, int fromIndex) {
170        // case-insensitive match
171        final String lowercase = sb.toString().toLowerCase(Locale.ROOT);
172        final int len = lowercase.length();
173        final int searchlen = search.length();
174        int pos = -1;
175        int depth = 0;
176        int cur = fromIndex;
177        do {
178            pos = lowercase.indexOf( search, cur );
179            if ( pos != -1 ) {
180                for ( int iter = cur; iter < pos; iter++ ) {
181                    final char c = sb.charAt( iter );
182                    if ( c == '(' ) {
183                        depth = depth + 1;
184                    }
185                    else if ( c == ')' ) {
186                        depth = depth - 1;
187                    }
188                }
189                cur = pos + searchlen;
190            }
191        } while ( cur < len && depth != 0 && pos != -1 );
192        return depth == 0 ? pos : -1;
193    }
194}