/*
* JBoss, Home of Professional Open Source.
* See the COPYRIGHT.txt file distributed with this work for information
* regarding copyright ownership. Some portions may be licensed
* to Red Hat, Inc. under one or more contributor license agreements.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*/
package org.teiid.translator.couchbase;
import static org.teiid.language.SQLConstants.Reserved.*;
import static org.teiid.language.SQLConstants.Tokens.*;
import static org.teiid.translator.couchbase.CouchbaseProperties.*;
import static org.teiid.translator.couchbase.CouchbaseMetadataProcessor.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.teiid.core.util.StringUtil;
import org.teiid.language.*;
import org.teiid.language.SQLConstants.NonReserved;
import org.teiid.language.SQLConstants.Reserved;
import org.teiid.language.SQLConstants.Tokens;
import org.teiid.language.visitor.SQLStringVisitor;
public class N1QLVisitor extends SQLStringVisitor{
protected CouchbaseExecutionFactory ef;
private boolean recordColumnName = true;
private List<String> selectColumns = new ArrayList<>();
private List<String> selectColumnReferences = new ArrayList<>();
private Map<String, CBColumn> columnMap = new HashMap<>();
private AliasGenerator columnAliasGenerator;
private AliasGenerator tableAliasGenerator;
protected boolean isArrayTable = false;
private List<CBColumn> letStack = new ArrayList<>();
private List<CBColumn> unrelatedStack = new ArrayList<>();
private List<CBColumn> tmpStack = new ArrayList<>();
protected String typedName = null;
protected String typedValue = null;
private boolean isUnrelatedColumns = false;
private String topTableAlias;
public N1QLVisitor(CouchbaseExecutionFactory ef) {
this.ef = ef;
}
@Override
public void visit(Select obj) {
buffer.append(SELECT).append(Tokens.SPACE);
if (obj.isDistinct()) {
buffer.append(DISTINCT).append(Tokens.SPACE);
}
append(obj.getDerivedColumns());
if (obj.getFrom() != null && !obj.getFrom().isEmpty()) {
buffer.append(Tokens.SPACE).append(FROM).append(Tokens.SPACE);
append(obj.getFrom());
}
appendLet(obj);
appendWhere(obj);
if (obj.getGroupBy() != null) {
buffer.append(Tokens.SPACE);
append(obj.getGroupBy());
}
if (obj.getHaving() != null) {
buffer.append(Tokens.SPACE).append(HAVING).append(Tokens.SPACE);
append(obj.getHaving());
}
if (obj.getOrderBy() != null) {
buffer.append(Tokens.SPACE);
append(obj.getOrderBy());
}
if (obj.getLimit() != null) {
buffer.append(Tokens.SPACE);
append(obj.getLimit());
}
}
private void appendLet(Select obj) {
if(this.letStack.size() > 0) {
buffer.append(SPACE).append(LET).append(SPACE);
boolean comma = false;
for(int i = 0 ; i < this.letStack.size() ; i++) {
if (comma) {
buffer.append(COMMA).append(SPACE);
}
comma = true;
buffer.append(this.letStack.get(i).getValueReference());
}
}
initUnrelatedColumns(obj);
for(int i = 0 ; i < this.unrelatedStack.size() ; i ++) {
CBColumn column = this.unrelatedStack.get(i);
String nameReference = column.getNameReference();
StringBuilder letValueReference = new StringBuilder();
letValueReference.append(buildEQ(nameReference));
if(column.isPK()) {
letValueReference.append(buildMeta(column.getTableAlias()));
column.setValueReference(letValueReference.toString());
} else if (column.isIdx()) {
//todo - handle unreleated column in idx conlumn
} else {
letValueReference.append(this.nameInSource(column.getTableAlias()));
String nameInSource = column.getNameInSource();
if(nameInSource != null) {
nameInSource = nameInSource.substring(nameInSource.indexOf(SOURCE_SEPARATOR) + 1, nameInSource.length());
letValueReference.append(SOURCE_SEPARATOR).append(nameInSource);
}
column.setValueReference(letValueReference.toString());
}
}
boolean comma = this.letStack.size() > 0;
for(int i = 0 ; i < this.unrelatedStack.size() ; i ++) {
if (comma) {
buffer.append(COMMA).append(SPACE);
} else {
buffer.append(SPACE).append(LET).append(SPACE);
comma = true;
}
buffer.append(this.unrelatedStack.get(i).getValueReference());
}
}
private void initUnrelatedColumns(Select obj) {
isUnrelatedColumns = true;
if(obj.getWhere() != null) {
append(obj.getWhere());
}
if (obj.getOrderBy() != null) {
append(obj.getOrderBy());
}
isUnrelatedColumns = false;
}
@Override
public void visit(AndOr obj) {
appendNestedCondition(obj, obj.getLeftCondition());
if(!isUnrelatedColumns) {
buffer.append(Tokens.SPACE).append(obj.getOperator().toString()).append(Tokens.SPACE);
}
appendNestedCondition(obj, obj.getRightCondition());
}
private void appendWhere(Select obj) {
this.tmpStack.clear();
if(this.typedName != null && this.typedValue != null) {
boolean isTypedNameInLetStack = false;
List<CBColumn> typedColumn = new ArrayList<>();
for(CBColumn column : this.letStack) {
if(column.hasTypedWhere() && column.getLeafName().equals(trimWave(this.typedName))) {
typedColumn.add(column);
isTypedNameInLetStack = true;
}
}
if(isTypedNameInLetStack) {
if(obj.getWhere() != null) {
buffer.append(SPACE).append(WHERE).append(SPACE);
append(obj.getWhere());
if(!isDuplicatedTypeColumn(typedColumn)) {
appendTypedWhere(false, typedColumn);
}
} else {
if(!isDuplicatedTypeColumn(typedColumn)) {
buffer.append(SPACE).append(WHERE).append(SPACE);
appendTypedWhere(true, typedColumn);
}
}
} else {
String keyspace = nameInSource(this.topTableAlias);
if(this.letStack.size() > 0) {
keyspace = nameInSource(this.letStack.get(this.letStack.size() - 1).getTableAlias());
}
String unrelatedType = keyspace + SOURCE_SEPARATOR + buildTypedWhere(this.typedName, this.typedValue);
if(obj.getWhere() != null) {
buffer.append(SPACE).append(WHERE).append(SPACE);
append(obj.getWhere());
if(!isDuplicatedTypeColumn(this.typedName)){
buffer.append(SPACE).append(Reserved.AND).append(SPACE).append(unrelatedType);
}
} else {
if(!isDuplicatedTypeColumn(this.typedName)) {
buffer.append(SPACE).append(WHERE).append(SPACE);
buffer.append(unrelatedType);
}
}
}
} else {
if(obj.getWhere() != null) {
buffer.append(SPACE).append(WHERE).append(SPACE);
append(obj.getWhere());
}
}
}
private void appendTypedWhere(boolean and, List<CBColumn> typedColumn) {
for(CBColumn column : typedColumn) {
if(column.hasTypedWhere()) {
if(and) {
buffer.append(column.getTypedWhere());
and = false;
} else {
buffer.append(SPACE).append(Reserved.AND).append(SPACE).append(column.getTypedWhere());
}
}
}
}
private boolean isDuplicatedTypeColumn(List<CBColumn> typedColumn) {
boolean result = false;
for(CBColumn column : typedColumn) {
if(isDuplicatedTypeColumn(column)) {
result = true;
break;
}
}
return result;
}
private boolean isDuplicatedTypeColumn(CBColumn typed) {
if(typed.isPK() || typed.isIdx()) {
return false;
}
boolean result = false;
for(CBColumn column : this.tmpStack) {
if(column.isPK() || column.isIdx()) {
continue;
}
if(column.getNameInSource().equals(typed.getNameInSource())) {
result = true;
break;
}
}
return result;
}
private boolean isDuplicatedTypeColumn(String typedName) {
boolean result = false;
for(CBColumn column : this.tmpStack) {
if(column.getLeafName().equals(this.trimWave(typedName))) {
result = true;
break;
}
}
return result;
}
@Override
public void visit(NamedTable obj) {
retrieveTableProperty(obj);
String tableNameInSource = obj.getMetadataObject().getNameInSource();
String alias = getTableAliasGenerator().generate();
this.topTableAlias = alias;
if(this.isArrayTable) {
String baseName = tableNameInSource;
String newAlias;
for(int i = this.letStack.size() ; i > 0 ; i --) {
CBColumn column = this.letStack.get(i -1);
String nameReference = column.getNameReference();
StringBuilder letValueReference = new StringBuilder();
letValueReference.append(buildEQ(nameReference));
if(column.isPK()) {
letValueReference.append(buildMeta(alias));
column.setValueReference(letValueReference.toString());
column.setTableAlias(alias);
continue;
} else if (column.isIdx()) {
letValueReference.append(UNNEST_POSITION);
letValueReference.append(LPAREN).append(nameInSource(alias)).append(RPAREN);
column.setValueReference(letValueReference.toString());
newAlias = tableAliasGenerator.generate();
baseName = baseName.substring(0, baseName.length() - SQUARE_BRACKETS.length());
StringBuilder unnestBuilder = new StringBuilder();
unnestBuilder.append(UNNEST).append(SPACE);
unnestBuilder.append(this.nameInSource(newAlias));
if(!baseName.endsWith(SQUARE_BRACKETS)) { // the dim 1 array has a attribute name under keyspace
String dimArrayAttrName = baseName.substring(baseName.lastIndexOf(SOURCE_SEPARATOR) + 1, baseName.length());
unnestBuilder.append(SOURCE_SEPARATOR).append(dimArrayAttrName);
}
unnestBuilder.append(SPACE).append(this.nameInSource(alias));
column.setUnnest(unnestBuilder.toString());
alias = newAlias ;
continue;
}
letValueReference.append(this.nameInSource(alias));
if(column.hasLeaf()) {
letValueReference.append(SOURCE_SEPARATOR).append(this.nameInSource(column.getLeafName()));
}
column.setValueReference(letValueReference.toString());
column.setTableAlias(alias);
}
String keyspace = baseName.substring(0, baseName.indexOf(SOURCE_SEPARATOR));
buffer.append(keyspace);
buffer.append(SPACE);
buffer.append(nameInSource(alias));
for(int i = 0 ; i < this.letStack.size() ; i++) {
CBColumn column = this.letStack.get(i);
if(column.hasUnnest()) {
buffer.append(SPACE);
buffer.append(column.getUnnest());
}
}
} else {
for(int i = this.letStack.size() ; i > 0 ; i --) {
CBColumn column = this.letStack.get(i -1);
String nameReference = column.getNameReference();
StringBuilder letValueReference = new StringBuilder();
letValueReference.append(buildEQ(nameReference));
if(column.isPK()){
buildMeta(alias);
letValueReference.append(buildMeta(alias));
column.setValueReference(letValueReference.toString());
column.setTableAlias(alias);
continue;
}
letValueReference.append(this.nameInSource(alias));
String nameInSource = column.getNameInSource();
if(nameInSource != null) {
nameInSource = nameInSource.substring(nameInSource.indexOf(SOURCE_SEPARATOR) + 1, nameInSource.length());
letValueReference.append(SOURCE_SEPARATOR).append(nameInSource);
}
column.setValueReference(letValueReference.toString());
column.setTableAlias(alias);
}
buffer.append(tableNameInSource); // if a table not array table, the table name in source is keyspace name
buffer.append(SPACE);
buffer.append(nameInSource(alias));
}
}
private String buildEQ(String nameReference) {
StringBuilder sb = new StringBuilder();
sb.append(this.nameInSource(nameReference));
sb.append(SPACE).append(EQ).append(SPACE);
return sb.toString();
}
private String buildTypedWhere(String typedName, String typedValue) {
StringBuilder sb = new StringBuilder();
sb.append(typedName).append(SPACE).append(EQ).append(SPACE);
sb.append(typedValue);
return sb.toString();
}
protected String buildMeta(String alias) {
StringBuilder sb = new StringBuilder();
sb.append("META").append(LPAREN).append(nameInSource(alias)).append(RPAREN).append(".id"); //$NON-NLS-1$ //$NON-NLS-2$
return sb.toString();
}
@Override
public void visit(GroupBy obj) {
recordColumnName = false;
super.visit(obj);
recordColumnName = true;
}
@Override
public void visit(OrderBy obj) {
recordColumnName = false;
if(!isUnrelatedColumns) {
buffer.append(ORDER).append(Tokens.SPACE).append(BY).append(Tokens.SPACE);
}
append(obj.getSortSpecifications());
recordColumnName = true;
}
@Override
public void visit(Comparison obj) {
recordColumnName = false;
append(obj.getLeftExpression());
if(!isUnrelatedColumns) {
buffer.append(Tokens.SPACE);
buffer.append(obj.getOperator());
buffer.append(Tokens.SPACE);
appendRightComparison(obj);
}
recordColumnName = true;
}
@Override
public void visit(DerivedColumn obj) {
if(recordColumnName) {
selectColumnReferences.add(obj.getAlias());
}
append(obj.getExpression());
}
@Override
public void visit(ColumnReference obj) {
if(obj.getTable() != null) {
if(isUnrelatedColumns && this.columnMap.get(obj.getName()) != null) {
return;
}
if(!isUnrelatedColumns && !recordColumnName && this.columnMap.get(obj.getName()) != null) {
String aliasName = this.columnMap.get(obj.getName()).getNameReference();
buffer.append(this.nameInSource(aliasName));
this.tmpStack.add(this.columnMap.get(obj.getName()));
return;
}
retrieveTableProperty(obj.getTable());
CBColumn column = formCBColumn(obj);
if(this.typedName != null && this.typedValue != null && column.getLeafName().equals(trimWave(this.typedName))) {
String typedWhere = buildTypedWhere(nameInSource(column.getNameReference()), this.typedValue);
column.setTypedWhere(typedWhere);
}
if(recordColumnName) {
this.letStack.add(column);
this.columnMap.put(obj.getName(), column);
this.selectColumns.add(column.getNameReference());
buffer.append(this.nameInSource(column.getNameReference()));
} else if(isUnrelatedColumns && !recordColumnName && letStack.size() > 0){
String tableAlias = letStack.get(letStack.size() -1).getTableAlias();
column.setTableAlias(tableAlias);
this.unrelatedStack.add(column);
this.columnMap.put(obj.getName(), column);
} else if(isUnrelatedColumns && !recordColumnName && letStack.size() == 0) {
column.setTableAlias(topTableAlias);
this.unrelatedStack.add(column);
this.columnMap.put(obj.getName(), column);
}
} else {
super.visit(obj);
}
}
protected CBColumn formCBColumn (ColumnReference obj) {
boolean isPK = false;
boolean isIdx = false;
String leafName = ""; //$NON-NLS-1$
if(isPKColumn(obj)) {
isPK = true;
} else if(isIDXColumn(obj)) {
isIdx = true;
} else if(obj.getMetadataObject().getNameInSource() != null && !obj.getMetadataObject().getNameInSource().endsWith(SQUARE_BRACKETS)){
String nameInSource = obj.getMetadataObject().getNameInSource();
leafName = nameInSource.substring(nameInSource.lastIndexOf(SOURCE_SEPARATOR) + 1, nameInSource.length());
leafName = this.trimWave(leafName);
}
String colExpr = this.getColumnAliasGenerator().generate() + UNDERSCORE + obj.getName();
return new CBColumn(isPK, isIdx, colExpr, leafName, obj.getMetadataObject().getSourceName());
}
protected void retrieveTableProperty(NamedTable table) {
if(table == null) {
return;
}
if(!isArrayTable && table.getMetadataObject().getProperty(IS_ARRAY_TABLE, false).equals(TRUE_VALUE)) {
this.isArrayTable = true;
}
if(this.typedName == null && this.typedValue == null) {
String typedNamePair = table.getMetadataObject().getProperty(NAMED_TYPE_PAIR, false);
if(typedNamePair != null && typedNamePair.length() > 0) {
String[] pair = typedNamePair.split(COLON);
this.typedName = pair[0];
this.typedValue = pair[1];
}
}
}
private boolean isIDXColumn(ColumnReference obj) {
return obj.getName().endsWith(IDX_SUFFIX) && obj.getMetadataObject().getNameInSource() == null;
}
private boolean isPKColumn(ColumnReference obj) {
return obj.getName().equals(DOCUMENTID) && obj.getMetadataObject().getNameInSource() == null;
}
@Override
public void visit(Function obj) {
String functionName = obj.getName();
if(functionName.equalsIgnoreCase(CONVERT) || functionName.equalsIgnoreCase(CAST)) {
List<?> parts = this.ef.getFunctionModifiers().get(functionName).translate(obj);
buffer.append(parts.get(0));
super.append(obj.getParameters().get(0));
buffer.append(parts.get(2));
return;
} else if (functionName.equalsIgnoreCase(NonReserved.TRIM)){
buffer.append(obj.getName()).append(LPAREN);
append(obj.getParameters());
buffer.append(RPAREN);
return;
} else if (this.ef.getFunctionModifiers().containsKey(functionName)) {
List<?> parts = this.ef.getFunctionModifiers().get(functionName).translate(obj);
if (parts != null) {
obj = (Function)parts.get(0);
}
}
super.visit(obj);
}
@Override
public void visit(Limit limit) {
if(limit.getRowOffset() > 0) {
buffer.append(LIMIT).append(SPACE);
buffer.append(limit.getRowLimit()).append(SPACE);
buffer.append(OFFSET).append(SPACE);
buffer.append(limit.getRowOffset());
} else {
super.visit(limit);
}
}
public List<String> getSelectColumns() {
return selectColumns;
}
public List<String> getSelectColumnReferences() {
return selectColumnReferences;
}
public AliasGenerator getColumnAliasGenerator() {
if(this.columnAliasGenerator == null) {
this.columnAliasGenerator = new AliasGenerator(N1QL_COLUMN_ALIAS_PREFIX);
}
return columnAliasGenerator;
}
public AliasGenerator getTableAliasGenerator() {
if(this.tableAliasGenerator == null) {
this.tableAliasGenerator = new AliasGenerator(N1QL_TABLE_ALIAS_PREFIX);
}
return tableAliasGenerator;
}
@Override
public void visit(Call call) {
String procName = call.getProcedureName();
String keyspace = null;
if(procName.equalsIgnoreCase(GETDOCUMENTS) || procName.equalsIgnoreCase(GETDOCUMENT)) {
keyspace = (String) call.getArguments().get(1).getArgumentValue().getValue();
}
if(call.getProcedureName().equalsIgnoreCase(GETDOCUMENTS)) {
appendKeyspace(keyspace);
appendN1QLWhere(call);
return;
} else if(call.getProcedureName().equalsIgnoreCase(GETDOCUMENT)) {
appendKeyspace(keyspace);
appendN1QLPK(call);
return;
}
}
@Override
protected String escapeString(String str, String quote) {
return StringUtil.replaceAll(str, quote, "\\u0027"); //$NON-NLS-1$
}
private void appendKeyspace(String keyspace) {
buffer.append(SELECT).append(SPACE);
buffer.append(RESULT).append(SPACE);
buffer.append(Reserved.FROM).append(SPACE);
buffer.append(nameInSource(keyspace)).append(SPACE);
buffer.append(Reserved.AS).append(SPACE).append(RESULT).append(SPACE);
}
private void appendN1QLWhere(Call call) {
buffer.append(Reserved.WHERE).append(SPACE);
buffer.append("META").append(LPAREN).append(RPAREN).append(".id").append(SPACE); //$NON-NLS-1$ //$NON-NLS-2$
buffer.append(Reserved.LIKE).append(SPACE);
append(call.getArguments().get(0));
}
private void appendN1QLPK(Call call) {
buffer.append("USE PRIMARY KEYS").append(SPACE); //$NON-NLS-1$
append(call.getArguments().get(0));
}
protected String nameInSource(String path) {
if(path.startsWith(WAVE) && path.endsWith(WAVE)) {
return path;
}
return WAVE + path + WAVE;
}
protected String trimWave(String value) {
String results = value;
if(results.startsWith(WAVE)) {
results = results.substring(1);
}
if(results.endsWith(WAVE)) {
results = results.substring(0, results.length() - 1);
}
return results;
}
private class AliasGenerator {
private final String prefix;
private Integer aliasCounter;
AliasGenerator(String prefix) {
this.prefix = prefix;
this.aliasCounter = Integer.valueOf(1);
}
public String generate() {
int index = this.aliasCounter.intValue();
String alias = this.prefix + index;
this.aliasCounter = Integer.valueOf(this.aliasCounter.intValue() + 1);
return alias;
}
}
protected class CBColumn {
private boolean isPK;
private boolean isIdx;
private String nameReference;
private String leafName;
private String nameInSource;
private String valueReference;
private String unnest;
private String tableAlias;
private String typedWhere;
public CBColumn(boolean isPK, boolean isIdx, String nameReference, String leafName, String nameInSource) {
this.isPK = isPK;
this.isIdx = isIdx;
this.nameReference = nameReference;
this.leafName = leafName;
this.nameInSource = nameInSource;
}
public boolean isPK() {
return isPK;
}
public boolean isIdx() {
return isIdx;
}
public String getNameReference() {
return nameReference;
}
public boolean hasLeaf() {
return this.leafName != null && this.leafName.length() > 0;
}
public String getLeafName() {
return leafName;
}
public String getNameInSource() {
return nameInSource;
}
public String getValueReference() {
return valueReference;
}
public void setValueReference(String valueReference) {
this.valueReference = valueReference;
}
boolean hasUnnest() {
return this.unnest != null && this.unnest.length() > 0 ;
}
public String getUnnest() {
return unnest;
}
public void setUnnest(String unnest) {
this.unnest = unnest;
}
public String getTableAlias() {
return tableAlias;
}
public void setTableAlias(String tableAlias) {
this.tableAlias = tableAlias;
}
public boolean hasTypedWhere() {
return this.typedWhere != null && this.typedWhere.length() > 0;
}
public String getTypedWhere() {
return typedWhere;
}
public void setTypedWhere(String typedWhere) {
this.typedWhere = typedWhere;
}
}
}