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}