/*
* Copyright 1999-2010 Luca Garulli (l.garulli--at--orientechnologies.com)
*
* 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 com.orientechnologies.orient.core.sql;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.orientechnologies.common.parser.OStringParser;
import com.orientechnologies.common.profiler.OProfiler;
import com.orientechnologies.common.util.OPair;
import com.orientechnologies.orient.core.command.OCommandRequestText;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import com.orientechnologies.orient.core.exception.OQueryParsingException;
import com.orientechnologies.orient.core.id.ORID;
import com.orientechnologies.orient.core.id.ORecordId;
import com.orientechnologies.orient.core.index.OIndex;
import com.orientechnologies.orient.core.index.OIndexFullText;
import com.orientechnologies.orient.core.index.OIndexInternal;
import com.orientechnologies.orient.core.index.OIndexNotUnique;
import com.orientechnologies.orient.core.index.OIndexUnique;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OProperty;
import com.orientechnologies.orient.core.metadata.security.ODatabaseSecurityResources;
import com.orientechnologies.orient.core.metadata.security.ORole;
import com.orientechnologies.orient.core.record.ORecord;
import com.orientechnologies.orient.core.record.ORecordInternal;
import com.orientechnologies.orient.core.record.ORecordSchemaAware;
import com.orientechnologies.orient.core.record.impl.ODocument;
import com.orientechnologies.orient.core.serialization.serializer.OStringSerializerHelper;
import com.orientechnologies.orient.core.sort.ODocumentSorter;
import com.orientechnologies.orient.core.sql.filter.OSQLFilter;
import com.orientechnologies.orient.core.sql.filter.OSQLFilterCondition;
import com.orientechnologies.orient.core.sql.filter.OSQLFilterItem;
import com.orientechnologies.orient.core.sql.filter.OSQLFilterItemField;
import com.orientechnologies.orient.core.sql.filter.OSQLFilterItemParameter;
import com.orientechnologies.orient.core.sql.functions.OSQLFunctionRuntime;
import com.orientechnologies.orient.core.sql.operator.OQueryOperatorBetween;
import com.orientechnologies.orient.core.sql.operator.OQueryOperatorContainsText;
import com.orientechnologies.orient.core.sql.operator.OQueryOperatorEquals;
import com.orientechnologies.orient.core.sql.query.OSQLAsynchQuery;
import com.orientechnologies.orient.core.sql.query.OSQLSynchQuery;
import com.orientechnologies.orient.core.storage.ORecordBrowsingListener;
import com.orientechnologies.orient.core.storage.OStorageEmbedded;
/**
* Executes the SQL SELECT statement. the parse() method compiles the query and builds the meta information needed by the execute().
* If the query contains the ORDER BY clause, the results are temporary collected internally, then ordered and finally returned all
* together to the listener.
*
* @author Luca Garulli
*/
public class OCommandExecutorSQLSelect extends OCommandExecutorSQLAbstract implements ORecordBrowsingListener {
private static final String KEYWORD_AS = " AS ";
public static final String KEYWORD_SELECT = "SELECT";
public static final String KEYWORD_ASC = "ASC";
public static final String KEYWORD_DESC = "DESC";
public static final String KEYWORD_ORDER = "ORDER";
public static final String KEYWORD_BY = "BY";
public static final String KEYWORD_ORDER_BY = "ORDER BY";
public static final String KEYWORD_LIMIT = "LIMIT";
public static final String KEYWORD_RANGE = "RANGE";
private OSQLAsynchQuery<ORecordSchemaAware<?>> request;
private OSQLFilter compiledFilter;
private Map<String, Object> projections = null;
private List<OPair<String, String>> orderedFields;
private List<ODocument> tempResult;
private int limit = -1;
private int resultCount;
private ORecordId rangeFrom;
private ORecordId rangeTo;
private Object flattenTarget;
private boolean anyFunctionAggregates = false;
/**
* Compile the filter conditions only the first time.
*/
@SuppressWarnings("unchecked")
public OCommandExecutorSQLSelect parse(final OCommandRequestText iRequest) {
iRequest.getDatabase().checkSecurity(ODatabaseSecurityResources.COMMAND, ORole.PERMISSION_READ);
init(iRequest.getDatabase(), iRequest.getText());
if (iRequest instanceof OSQLSynchQuery) {
request = (OSQLSynchQuery<ORecordSchemaAware<?>>) iRequest;
rangeFrom = request.getBeginRange().isValid() ? request.getBeginRange() : null;
rangeTo = request.getEndRange().isValid() ? request.getEndRange() : null;
} else if (iRequest instanceof OSQLAsynchQuery)
request = (OSQLAsynchQuery<ORecordSchemaAware<?>>) iRequest;
else {
// BUILD A QUERY OBJECT FROM THE COMMAND REQUEST
request = new OSQLSynchQuery<ORecordSchemaAware<?>>(iRequest.getText());
request.setDatabase(iRequest.getDatabase());
if (iRequest.getResultListener() != null)
request.setResultListener(iRequest.getResultListener());
}
final int pos = extractProjections();
if (pos == -1)
return this;
int endPosition = textUpperCase.indexOf(" " + OCommandExecutorSQLSelect.KEYWORD_ORDER_BY, currentPos);
if (endPosition == -1) {
endPosition = textUpperCase.indexOf(" " + OCommandExecutorSQLSelect.KEYWORD_RANGE, currentPos);
if (endPosition == -1) {
endPosition = textUpperCase.indexOf(" " + OCommandExecutorSQLSelect.KEYWORD_LIMIT, currentPos);
if (endPosition == -1) {
// NO OTHER STUFF: GET UNTIL THE END AND ASSURE TO RETURN FALSE IN ORDER TO AVOID PARSING OF CONDITIONS
endPosition = text.length();
}
}
}
compiledFilter = OSQLEngine.getInstance().parseFromWhereCondition(iRequest.getDatabase(), text.substring(pos, endPosition));
currentPos = compiledFilter.currentPos + pos;
if (currentPos > -1 && currentPos < text.length()) {
currentPos = OStringParser.jump(text, currentPos, " \r\n");
final StringBuilder word = new StringBuilder();
String w;
while (currentPos > -1) {
currentPos = OSQLHelper.nextWord(text, textUpperCase, currentPos, word, true);
if (currentPos > -1) {
w = word.toString();
if (w.equals(KEYWORD_ORDER))
extractOrderBy(word);
else if (w.equals(KEYWORD_RANGE))
extractRange(word);
else if (w.equals(KEYWORD_LIMIT))
extractLimit(word);
}
}
}
if (limit == 0 || limit < -1) {
throw new IllegalArgumentException("Limit must be > 0 or = -1 (no limit)");
}
return this;
}
public Object execute(final Map<Object, Object> iArgs) {
// TODO: SUPPORT MULTIPLE CLASSES LIKE A SQL JOIN
compiledFilter.bindParameters(iArgs);
if (compiledFilter.getTargetClasses() != null)
searchInClasses();
else if (compiledFilter.getTargetClusters() != null)
searchInClusters();
else if (compiledFilter.getTargetIndex() != null)
searchInIndex();
else if (compiledFilter.getTargetRecords() != null)
searchInRecords();
else
throw new OQueryParsingException("No source found in query: specify class, clusters or single records");
applyOrderBy();
applyFlatten();
return processResult();
}
public boolean foreach(final ORecordInternal<?> iRecord) {
if (filter(iRecord))
return addResult(iRecord);
return true;
}
protected boolean addResult(final ORecordInternal<?> iRecord) {
resultCount++;
addResult(iRecord.copy());
if (limit > -1 && resultCount >= limit || request.getLimit() > -1 && resultCount >= request.getLimit())
// BREAK THE EXECUTION
return false;
return true;
}
public Map<String, Object> getProjections() {
return projections;
}
public List<OPair<String, String>> getOrderedFields() {
return orderedFields;
}
protected void extractOrderBy(final StringBuilder word) {
int newPos = OSQLHelper.nextWord(text, textUpperCase, currentPos, word, true);
if (!KEYWORD_BY.equals(word.toString()))
throw new OQueryParsingException("Expected keyword " + KEYWORD_BY);
currentPos = newPos;
String fieldName;
String fieldOrdering;
orderedFields = new ArrayList<OPair<String, String>>();
while (currentPos != -1 && (orderedFields.size() == 0 || word.toString().equals(","))) {
currentPos = OSQLHelper.nextWord(text, textUpperCase, currentPos, word, false);
if (currentPos == -1)
throw new OCommandSQLParsingException("Field name expected", text, currentPos);
fieldName = word.toString();
currentPos = OSQLHelper.nextWord(text, textUpperCase, currentPos, word, true);
if (currentPos == -1 || word.toString().equals(KEYWORD_LIMIT))
// END/NEXT CLAUSE: SET AS ASC BY DEFAULT
fieldOrdering = KEYWORD_ASC;
else {
if (word.toString().endsWith(",")) {
currentPos--;
word.deleteCharAt(word.length() - 1);
}
if (word.toString().equals(KEYWORD_ASC))
fieldOrdering = KEYWORD_ASC;
else if (word.toString().equals(KEYWORD_DESC))
fieldOrdering = KEYWORD_DESC;
else
throw new OCommandSQLParsingException("Ordering mode '" + word
+ "' not supported. Valid is 'ASC', 'DESC' or nothing ('ASC' by default)", text, currentPos);
currentPos = OSQLHelper.nextWord(text, textUpperCase, currentPos, word, true);
}
orderedFields.add(new OPair<String, String>(fieldName, fieldOrdering));
if (currentPos == -1)
break;
}
if (orderedFields.size() == 0)
throw new OCommandSQLParsingException("Order by field set was missed. Example: ORDER BY name ASC, salary DESC", text,
currentPos);
if (word.toString().equals(KEYWORD_LIMIT))
// GO BACK
currentPos -= KEYWORD_LIMIT.length();
}
protected void extractRange(final StringBuilder word) {
int newPos = OSQLHelper.nextWord(text, textUpperCase, currentPos, word, true);
if (!word.toString().contains(":"))
throw new OCommandSQLParsingException(
"Range must contains record id in the form of <cluster-id>:<cluster-pos>. Example: RANGE 10:50, 10:100", text, currentPos);
try {
rangeFrom = new ORecordId(word.toString());
} catch (Exception e) {
throw new OCommandSQLParsingException("Invalid record id setted as RANGE from. Value setted is '" + word
+ "' but it should be a valid record id in the form of <cluster-id>:<cluster-pos>. Example: RANGE 10:50", text,
currentPos);
}
if (newPos == -1)
return;
currentPos = newPos;
newPos = OSQLHelper.nextWord(text, textUpperCase, currentPos, word, true);
if (newPos == -1)
return;
if (!word.toString().equalsIgnoreCase("LIMIT")) {
if (!word.toString().contains(":"))
throw new OCommandSQLParsingException(
"Range must contains record id in the form of <cluster-id>:<cluster-pos>. Example: RANGE 10:50, 10:100", text,
currentPos);
try {
rangeTo = new ORecordId(word.toString());
} catch (Exception e) {
throw new OCommandSQLParsingException("Invalid record id setted as RANGE to. Value setted is '" + word
+ "' but it should be a valid record id in the form of <cluster-id>:<cluster-pos>. Example: RANGE 10:50, 10:100", text,
currentPos);
}
currentPos = newPos;
}
}
protected void extractLimit(final StringBuilder word) {
if (!word.toString().equals(KEYWORD_LIMIT))
return;
currentPos = OSQLHelper.nextWord(text, textUpperCase, currentPos, word, true);
try {
limit = Integer.parseInt(word.toString());
} catch (Exception e) {
throw new OCommandSQLParsingException("Invalid LIMIT value setted to '" + word
+ "' but it should be a valid integer. Example: LIMIT 10", text, currentPos);
}
}
private void addResult(final ORecord<?> iRecord) {
if (orderedFields != null || flattenTarget != null) {
// ORDER BY CLAUSE: COLLECT ALL THE RECORDS AND ORDER THEM AT THE END
if (tempResult == null)
tempResult = new ArrayList<ODocument>();
tempResult.add((ODocument) iRecord);
} else
// CALL THE LISTENER NOW
processRecordAsResult(iRecord);
}
private boolean searchForIndexes(final List<ORecord<?>> iResultSet, final OClass iSchemaClass) {
return analyzeQueryBranch(iResultSet, iSchemaClass, compiledFilter.getRootCondition());
}
private boolean analyzeQueryBranch(final List<ORecord<?>> iResultSet, final OClass iSchemaClass,
final OSQLFilterCondition iCondition) {
if (iCondition == null)
return false;
if (iCondition.getLeft() != null)
if (iCondition.getLeft() instanceof OSQLFilterCondition)
analyzeQueryBranch(iResultSet, iSchemaClass, (OSQLFilterCondition) iCondition.getLeft());
if (iCondition.getRight() != null)
if (iCondition.getRight() instanceof OSQLFilterCondition)
analyzeQueryBranch(iResultSet, iSchemaClass, (OSQLFilterCondition) iCondition.getRight());
if (!searchIndexedProperty(iResultSet, iSchemaClass, iCondition, iCondition.getLeft()))
if (!searchIndexedProperty(iResultSet, iSchemaClass, iCondition, iCondition.getRight()))
return false;
// INDEX FOUND
return true;
}
/**
* Searches a value in index if the property defines it.
*
* @param iResultSet
* result set to fill
* @param iSchemaClass
* Schema class
* @param iCondition
* Condition item
* @param iItem
* Value to search
* @return true if the property was indexed, otherwise false
*/
private boolean searchIndexedProperty(final List<ORecord<?>> iResultSet, final OClass iSchemaClass,
final OSQLFilterCondition iCondition, final Object iItem) {
if (iItem == null || !(iItem instanceof OSQLFilterItemField))
return false;
OSQLFilterItemField item = (OSQLFilterItemField) iItem;
final OProperty prop = iSchemaClass.getProperty(item.getName());
if (prop != null && prop.isIndexed()) {
// TODO: IMPROVE THIS MANAGEMENT
// ONLY EQUALS IS SUPPORTED NOW!
OIndex idx = prop.getIndex().getUnderlying();
idx = idx.getInternal();
if (((idx instanceof OIndexUnique || idx instanceof OIndexNotUnique) && iCondition.getOperator() instanceof OQueryOperatorEquals)
|| idx instanceof OIndexFullText && iCondition.getOperator() instanceof OQueryOperatorContainsText) {
Object value = iCondition.getLeft() == iItem ? iCondition.getRight() : iCondition.getLeft();
if (value != null) {
if (value instanceof OSQLFilterItemParameter)
value = ((OSQLFilterItemParameter) value).getValue(null);
final Collection<?> resultSet = prop.getIndex().getUnderlying().get(value);
if (resultSet != null && resultSet.size() > 0)
for (Object o : resultSet) {
if (o instanceof ORID)
iResultSet.add(database.load((ORID) o));
else
iResultSet.add((ORecord<?>) o);
}
return true;
}
}
}
return false;
}
protected boolean filter(final ORecordInternal<?> iRecord) {
return compiledFilter.evaluate(database, (ORecordSchemaAware<?>) iRecord);
}
protected int extractProjections() {
int currentPos = 0;
final StringBuilder word = new StringBuilder();
currentPos = OSQLHelper.nextWord(text, textUpperCase, currentPos, word, true);
if (!word.toString().equals(KEYWORD_SELECT))
return -1;
int fromPosition = textUpperCase.indexOf(KEYWORD_FROM, currentPos);
if (fromPosition == -1)
throw new OQueryParsingException("Missed " + KEYWORD_FROM, text, currentPos);
Object projectionValue;
final String projectionString = text.substring(currentPos, fromPosition).trim();
if (projectionString.length() > 0 && !projectionString.equals("*")) {
// EXTRACT PROJECTIONS
projections = new HashMap<String, Object>();
final List<String> items = OStringSerializerHelper.smartSplit(projectionString, ',');
String fieldName;
int pos;
for (String projection : items) {
projection = projection.trim();
fieldName = null;
pos = projection.toUpperCase().indexOf(KEYWORD_AS);
if (pos > -1) {
// EXTRACT ALIAS
fieldName = projection.substring(pos + KEYWORD_AS.length()).trim();
projection = projection.substring(0, pos).trim();
if (projections.containsKey(fieldName))
throw new OCommandSQLParsingException("Field '" + fieldName
+ "' is duplicated in current SELECT, choose a different name");
} else {
// EXTRACT THE FIELD NAME WITHOUT FUNCTIONS AND/OR LINKS
pos = projection.indexOf('.');
fieldName = pos > -1 ? projection.substring(0, pos) : projection;
fieldName = OSQLHelper.stringContent(fieldName);
// FIND A UNIQUE NAME BY ADDING A COUNTER
for (int fieldIndex = 2; projections.containsKey(fieldName); ++fieldIndex) {
fieldName += fieldIndex;
}
}
if (projection.toUpperCase().startsWith("FLATTEN(")) {
List<String> pars = OStringSerializerHelper.getParameters(projection);
if (pars.size() != 1)
throw new OCommandSQLParsingException(
"FLATTEN operator expects the field name as parameter. Example FLATTEN( outEdges )");
flattenTarget = OSQLHelper.parseValue(database, this, pars.get(0).trim());
// BY PASS THIS AS PROJECTION BUT TREAT IT AS SPECIAL
projections = null;
continue;
}
projectionValue = OSQLHelper.parseValue(database, this, projection);
projections.put(fieldName, projectionValue);
if (!anyFunctionAggregates && projectionValue instanceof OSQLFunctionRuntime
&& ((OSQLFunctionRuntime) projectionValue).aggregateResults())
anyFunctionAggregates = true;
}
}
currentPos = fromPosition + KEYWORD_FROM.length() + 1;
return currentPos;
}
private void scanEntireClusters(final int[] clusterIds) {
((OStorageEmbedded) database.getStorage()).browse(clusterIds, rangeFrom, rangeTo, this,
(ORecordInternal<?>) database.newInstance(), false);
}
private void applyOrderBy() {
if (orderedFields == null || tempResult == null)
return;
ODocumentSorter.sort(tempResult, orderedFields);
orderedFields.clear();
}
/**
* Extract the content of collections and/or links and put it as result
*/
private void applyFlatten() {
if (flattenTarget == null)
return;
final List<ODocument> finalResult = new ArrayList<ODocument>();
Object fieldValue;
if (tempResult != null)
for (ODocument record : tempResult) {
if (flattenTarget instanceof OSQLFilterItem)
fieldValue = ((OSQLFilterItem) flattenTarget).getValue(record);
else
fieldValue = flattenTarget.toString();
if (fieldValue != null)
if (fieldValue instanceof Collection<?>) {
for (Object o : ((Collection<?>) fieldValue)) {
if (o instanceof ODocument)
finalResult.add((ODocument) o);
}
} else
finalResult.add((ODocument) fieldValue);
}
tempResult = finalResult;
}
private void processRecordAsResult(final ORecord<?> iRecord) {
if (projections != null) {
// APPLY PROJECTIONS
final ODocument doc = (ODocument) iRecord;
final ODocument result = new ODocument(database);
boolean canExcludeResult = false;
Object value;
for (Entry<String, Object> projection : projections.entrySet()) {
if (projection.getValue() instanceof OSQLFilterItemField)
value = ((OSQLFilterItemField) projection.getValue()).getValue(doc);
else if (projection.getValue() instanceof OSQLFunctionRuntime) {
final OSQLFunctionRuntime f = (OSQLFunctionRuntime) projection.getValue();
canExcludeResult = f.filterResult();
value = f.execute(doc);
} else
value = projection.getValue();
if (value != null)
result.field(projection.getKey(), value);
}
if (canExcludeResult && result.fieldValues().length == 0)
// RESULT EXCLUDED FOR EMPTY RECORD
return;
if (!anyFunctionAggregates)
// INVOKE THE LISTENER
request.getResultListener().result(result);
} else
// INVOKE THE LISTENER
request.getResultListener().result(iRecord);
}
private void searchInClasses() {
final int[] clusterIds;
final OClass cls = compiledFilter.getTargetClasses().keySet().iterator().next();
database.checkSecurity(ODatabaseSecurityResources.CLASS, ORole.PERMISSION_READ, cls.getName());
clusterIds = cls.getPolymorphicClusterIds();
// CHECK PERMISSION TO ACCESS TO ALL THE CONFIGURED CLUSTERS
for (int clusterId : clusterIds)
database.checkSecurity(ODatabaseSecurityResources.CLUSTER, ORole.PERMISSION_READ, database.getClusterNameById(clusterId),
clusterId);
final List<ORecord<?>> resultSet = new ArrayList<ORecord<?>>();
if (searchForIndexes(resultSet, cls)) {
OProfiler.getInstance().updateCounter("Query.indexUsage", 1);
// FOUND USING INDEXES
for (ORecord<?> record : resultSet)
addResult(record);
} else
// NO INDEXES: SCAN THE ENTIRE CLUSTER
scanEntireClusters(clusterIds);
}
private void searchInClusters() {
final int[] clusterIds;
String firstCluster = compiledFilter.getTargetClusters().keySet().iterator().next();
if (firstCluster == null || firstCluster.length() == 0)
throw new OCommandExecutionException("No cluster or schema class selected in query");
if (Character.isDigit(firstCluster.charAt(0)))
// GET THE CLUSTER NUMBER
clusterIds = OStringSerializerHelper.splitIntArray(firstCluster);
else
// GET THE CLUSTER NUMBER BY THE CLASS NAME
clusterIds = new int[] { database.getClusterIdByName(firstCluster.toLowerCase()) };
database.checkSecurity(ODatabaseSecurityResources.CLUSTER, ORole.PERMISSION_READ, firstCluster.toLowerCase(), clusterIds[0]);
scanEntireClusters(clusterIds);
}
private void searchInRecords() {
ORecordId rid = new ORecordId();
ORecordInternal<?> record;
for (String rec : compiledFilter.getTargetRecords()) {
rid.fromString(rec);
record = database.load(rid);
foreach(record);
}
}
private void searchInIndex() {
final OIndex index = database.getMetadata().getIndexManager().getIndex(compiledFilter.getTargetIndex());
if (index == null)
throw new OCommandExecutionException("Target index '" + compiledFilter.getTargetIndex() + "' not found");
if (compiledFilter.getRootCondition() != null) {
if (!"KEY".equalsIgnoreCase(compiledFilter.getRootCondition().getLeft().toString()))
throw new OCommandExecutionException("'Key' field is required for queries against indexes");
final Object right = compiledFilter.getRootCondition().getRight();
Collection<OIdentifiable> result = null;
if (compiledFilter.getRootCondition().getOperator() instanceof OQueryOperatorBetween) {
final Object[] values = (Object[]) compiledFilter.getRootCondition().getRight();
result = index.getBetween(OSQLHelper.getValue(values[0]), OSQLHelper.getValue(values[2]));
} else
result = index.get(OSQLHelper.getValue(right));
if (result != null)
for (OIdentifiable r : result) {
addResult((ORecord<?>) r);
}
} else {
if (anyFunctionAggregates) {
for (Entry<String, Object> projection : projections.entrySet()) {
if (projection.getValue() instanceof OSQLFunctionRuntime) {
final OSQLFunctionRuntime f = (OSQLFunctionRuntime) projection.getValue();
f.setResult(index.getSize());
}
}
}
}
}
private Object processResult() {
if (anyFunctionAggregates) {
// EXECUTE AGGREGATIONS
Object value;
final ODocument result = new ODocument(database);
for (Entry<String, Object> projection : projections.entrySet()) {
if (projection.getValue() instanceof OSQLFilterItemField)
value = ((OSQLFilterItemField) projection.getValue()).getValue(result);
else if (projection.getValue() instanceof OSQLFunctionRuntime) {
final OSQLFunctionRuntime f = (OSQLFunctionRuntime) projection.getValue();
value = f.getResult();
} else
value = projection.getValue();
result.field(projection.getKey(), value);
}
request.getResultListener().result(result);
} else if (tempResult != null) {
// TEMP RESULT: RETURN ALL THE RECORDS AT THE END
for (ODocument doc : tempResult)
// CALL THE LISTENER
processRecordAsResult(doc);
tempResult.clear();
tempResult = null;
}
if (request instanceof OSQLSynchQuery)
return ((OSQLSynchQuery<ORecordSchemaAware<?>>) request).getResult();
return null;
}
}