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

io.questdb.griffin.SqlCodeGenerator Maven / Gradle / Ivy

/*******************************************************************************
 *     ___                  _   ____  ____
 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
 *   | | | | | | |/ _ \/ __| __| | | |  _ \
 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
 *    \__\_\\__,_|\___||___/\__|____/|____/
 *
 *  Copyright (c) 2014-2019 Appsicle
 *  Copyright (c) 2019-2022 QuestDB
 *
 *  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.questdb.griffin;

import io.questdb.cairo.*;
import io.questdb.cairo.map.RecordValueSink;
import io.questdb.cairo.map.RecordValueSinkFactory;
import io.questdb.cairo.sql.Record;
import io.questdb.cairo.sql.*;
import io.questdb.cairo.sql.async.PageFrameReduceTask;
import io.questdb.cairo.vm.Vm;
import io.questdb.cairo.vm.api.MemoryCARW;
import io.questdb.griffin.engine.*;
import io.questdb.griffin.engine.analytic.AnalyticFunction;
import io.questdb.griffin.engine.analytic.CachedAnalyticRecordCursorFactory;
import io.questdb.griffin.engine.functions.GroupByFunction;
import io.questdb.griffin.engine.functions.SymbolFunction;
import io.questdb.griffin.engine.functions.bind.IndexedParameterLinkFunction;
import io.questdb.griffin.engine.functions.bind.NamedParameterLinkFunction;
import io.questdb.griffin.engine.functions.cast.*;
import io.questdb.griffin.engine.functions.columns.*;
import io.questdb.griffin.engine.functions.constants.*;
import io.questdb.griffin.engine.groupby.*;
import io.questdb.griffin.engine.groupby.vect.GroupByRecordCursorFactory;
import io.questdb.griffin.engine.groupby.vect.*;
import io.questdb.griffin.engine.join.*;
import io.questdb.griffin.engine.orderby.LimitedSizeSortedLightRecordCursorFactory;
import io.questdb.griffin.engine.orderby.RecordComparatorCompiler;
import io.questdb.griffin.engine.orderby.SortedLightRecordCursorFactory;
import io.questdb.griffin.engine.orderby.SortedRecordCursorFactory;
import io.questdb.griffin.engine.table.*;
import io.questdb.griffin.engine.union.*;
import io.questdb.griffin.model.*;
import io.questdb.jit.CompiledFilter;
import io.questdb.jit.CompiledFilterIRSerializer;
import io.questdb.jit.JitUtil;
import io.questdb.log.Log;
import io.questdb.log.LogFactory;
import io.questdb.std.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.Closeable;

import static io.questdb.cairo.sql.DataFrameCursorFactory.ORDER_ANY;
import static io.questdb.griffin.SqlKeywords.*;
import static io.questdb.griffin.model.ExpressionNode.FUNCTION;
import static io.questdb.griffin.model.ExpressionNode.LITERAL;
import static io.questdb.griffin.model.ExpressionNode.CONSTANT;
import static io.questdb.griffin.model.QueryModel.*;

public class SqlCodeGenerator implements Mutable, Closeable {
    public static final int GKK_HOUR_INT = 1;
    public static final int GKK_VANILLA_INT = 0;
    private static final VectorAggregateFunctionConstructor COUNT_CONSTRUCTOR = (keyKind, columnIndex, workerCount) -> new CountVectorAggregateFunction(keyKind);
    private static final FullFatJoinGenerator CREATE_FULL_FAT_AS_OF_JOIN = SqlCodeGenerator::createFullFatAsOfJoin;
    private static final FullFatJoinGenerator CREATE_FULL_FAT_LT_JOIN = SqlCodeGenerator::createFullFatLtJoin;
    private static final Log LOG = LogFactory.getLog(SqlCodeGenerator.class);
    private static final SetRecordCursorFactoryConstructor SET_EXCEPT_CONSTRUCTOR = ExceptRecordCursorFactory::new;
    private static final SetRecordCursorFactoryConstructor SET_INTERSECT_CONSTRUCTOR = IntersectRecordCursorFactory::new;
    private static final SetRecordCursorFactoryConstructor SET_UNION_CONSTRUCTOR = UnionRecordCursorFactory::new;
    private static final IntObjHashMap avgConstructors = new IntObjHashMap<>();
    private static final IntObjHashMap countConstructors = new IntObjHashMap<>();
    private static final boolean[] joinsRequiringTimestamp = new boolean[JOIN_MAX + 1];
    private static final IntObjHashMap ksumConstructors = new IntObjHashMap<>();
    private static final IntHashSet limitTypes = new IntHashSet();
    private static final IntObjHashMap maxConstructors = new IntObjHashMap<>();
    private static final IntObjHashMap minConstructors = new IntObjHashMap<>();
    private static final IntObjHashMap nsumConstructors = new IntObjHashMap<>();
    private static final IntObjHashMap sumConstructors = new IntObjHashMap<>();
    private final ArrayColumnTypes arrayColumnTypes = new ArrayColumnTypes();
    private final BytecodeAssembler asm = new BytecodeAssembler();
    private final CairoConfiguration configuration;
    private final ObjList deferredAnalyticMetadata = new ObjList<>();
    private final boolean enableJitDebug;
    private final CairoEngine engine;
    private final EntityColumnFilter entityColumnFilter = new EntityColumnFilter();
    private final ObjectPool expressionNodePool;
    private final FunctionParser functionParser;
    private final IntList groupByFunctionPositions = new IntList();
    private final ObjObjHashMap> groupedAnalytic = new ObjObjHashMap<>();
    private final IntHashSet intHashSet = new IntHashSet();
    private final ObjectPool intListPool = new ObjectPool<>(IntList::new, 4);
    private final MemoryCARW jitIRMem;
    private final CompiledFilterIRSerializer jitIRSerializer = new CompiledFilterIRSerializer();
    private final ArrayColumnTypes keyTypes = new ArrayColumnTypes();
    // this list is used to generate record sinks
    private final ListColumnFilter listColumnFilterA = new ListColumnFilter();
    private final ListColumnFilter listColumnFilterB = new ListColumnFilter();
    private final LongList prefixes = new LongList();
    private final RecordComparatorCompiler recordComparatorCompiler;
    private final IntList recordFunctionPositions = new IntList();
    private final WeakClosableObjectPool reduceTaskPool;
    private final WhereClauseSymbolEstimator symbolEstimator = new WhereClauseSymbolEstimator();
    private final IntList tempAggIndex = new IntList();
    private final IntList tempKeyIndex = new IntList();
    private final IntList tempKeyIndexesInBase = new IntList();
    private final IntList tempKeyKinds = new IntList();
    private final GenericRecordMetadata tempMetadata = new GenericRecordMetadata();
    private final IntList tempSymbolSkewIndexes = new IntList();
    private final ObjList tempVaf = new ObjList<>();
    private final IntList tempVecConstructorArgIndexes = new IntList();
    private final ObjList tempVecConstructors = new ObjList<>();
    private final ArrayColumnTypes valueTypes = new ArrayColumnTypes();
    private final WhereClauseParser whereClauseParser = new WhereClauseParser();
    private boolean enableJitNullChecks = true;
    private boolean fullFatJoins = false;

    public SqlCodeGenerator(
            CairoEngine engine,
            CairoConfiguration configuration,
            FunctionParser functionParser,
            ObjectPool expressionNodePool
    ) {
        this.engine = engine;
        this.configuration = configuration;
        this.functionParser = functionParser;
        this.recordComparatorCompiler = new RecordComparatorCompiler(asm);
        this.enableJitDebug = configuration.isSqlJitDebugEnabled();
        this.jitIRMem = Vm.getCARWInstance(configuration.getSqlJitIRMemoryPageSize(),
                configuration.getSqlJitIRMemoryMaxPages(), MemoryTag.NATIVE_JIT);
        // Pre-touch JIT IR memory to avoid false positive memory leak detections.
        jitIRMem.putByte((byte) 0);
        jitIRMem.truncate();
        this.expressionNodePool = expressionNodePool;
        this.reduceTaskPool = new WeakClosableObjectPool<>(
                () -> new PageFrameReduceTask(configuration),
                configuration.getPageFrameReduceTaskPoolCapacity()
        );
    }

    @Override
    public void clear() {
        whereClauseParser.clear();
        symbolEstimator.clear();
        intListPool.clear();
    }

    @Override
    public void close() {
        Misc.free(jitIRMem);
        Misc.free(reduceTaskPool);
    }

    @NotNull
    public Function compileBooleanFilter(
            ExpressionNode expr,
            RecordMetadata metadata,
            SqlExecutionContext executionContext
    ) throws SqlException {
        final Function filter = functionParser.parseFunction(expr, metadata, executionContext);
        if (ColumnType.isBoolean(filter.getType())) {
            return filter;
        }
        Misc.free(filter);
        throw SqlException.$(expr.position, "boolean expression expected");
    }

    public RecordCursorFactory generate(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        return generateQuery(model, executionContext, true);
    }

    public RecordCursorFactory generateExplain(QueryModel model, RecordCursorFactory factory, int format) {
        RecordCursorFactory recordCursorFactory = new RecordCursorFactoryStub(model, factory);
        return new ExplainPlanFactory(recordCursorFactory, format);
    }

    public RecordCursorFactory generateExplain(ExplainModel model, SqlExecutionContext executionContext) throws SqlException {
        ExecutionModel innerModel = model.getInnerExecutionModel();
        QueryModel queryModel = innerModel.getQueryModel();
        RecordCursorFactory factory;
        if (queryModel != null) {
            factory = generate(queryModel, executionContext);
            if (innerModel.getModelType() != QUERY) {
                factory = new RecordCursorFactoryStub(innerModel, factory);
            }
        } else {
            factory = new RecordCursorFactoryStub(innerModel, null);
        }

        return new ExplainPlanFactory(factory, model.getFormat());
    }

    private static boolean allGroupsFirstLastWithSingleSymbolFilter(QueryModel model, RecordMetadata metadata) {
        final ObjList columns = model.getColumns();
        for (int i = 0, n = columns.size(); i < n; i++) {
            final QueryColumn column = columns.getQuick(i);
            final ExpressionNode node = column.getAst();

            if (node.type != ExpressionNode.LITERAL) {
                ExpressionNode columnAst = column.getAst();
                CharSequence token = columnAst.token;
                if (!SqlKeywords.isFirstKeyword(token) && !SqlKeywords.isLastKeyword(token)) {
                    return false;
                }

                if (columnAst.rhs.type != ExpressionNode.LITERAL || metadata.getColumnIndex(columnAst.rhs.token) < 0) {
                    return false;
                }
            }
        }

        return true;
    }

    private static RecordCursorFactory createFullFatAsOfJoin(CairoConfiguration configuration,
                                                             RecordMetadata metadata,
                                                             RecordCursorFactory masterFactory,
                                                             RecordCursorFactory slaveFactory,
                                                             @Transient ColumnTypes mapKeyTypes,
                                                             @Transient ColumnTypes mapValueTypes,
                                                             @Transient ColumnTypes slaveColumnTypes,
                                                             RecordSink masterKeySink,
                                                             RecordSink slaveKeySink,
                                                             int columnSplit,
                                                             RecordValueSink slaveValueSink,
                                                             IntList columnIndex,
                                                             JoinContext joinContext) {
        return new AsOfJoinRecordCursorFactory(configuration, metadata, masterFactory, slaveFactory, mapKeyTypes, mapValueTypes, slaveColumnTypes, masterKeySink, slaveKeySink, columnSplit, slaveValueSink, columnIndex, joinContext);
    }

    private static RecordCursorFactory createFullFatLtJoin(CairoConfiguration configuration,
                                                           RecordMetadata metadata,
                                                           RecordCursorFactory masterFactory,
                                                           RecordCursorFactory slaveFactory,
                                                           @Transient ColumnTypes mapKeyTypes,
                                                           @Transient ColumnTypes mapValueTypes,
                                                           @Transient ColumnTypes slaveColumnTypes,
                                                           RecordSink masterKeySink,
                                                           RecordSink slaveKeySink,
                                                           int columnSplit,
                                                           RecordValueSink slaveValueSink,
                                                           IntList columnIndex,
                                                           JoinContext joinContext) {
        return new LtJoinRecordCursorFactory(configuration, metadata, masterFactory, slaveFactory, mapKeyTypes, mapValueTypes, slaveColumnTypes, masterKeySink, slaveKeySink, columnSplit, slaveValueSink, columnIndex, joinContext);
    }

    private static int getOrderByDirectionOrDefault(QueryModel model, int index) {
        IntList direction = model.getOrderByDirectionAdvice();
        if (index >= direction.size()) {
            return ORDER_DIRECTION_ASCENDING;
        }
        return model.getOrderByDirectionAdvice().getQuick(index);
    }

    private VectorAggregateFunctionConstructor assembleFunctionReference(RecordMetadata metadata, ExpressionNode ast) {
        int columnIndex;
        if (ast.type == FUNCTION && ast.paramCount == 1 && SqlKeywords.isSumKeyword(ast.token) && ast.rhs.type == LITERAL) {
            columnIndex = metadata.getColumnIndex(ast.rhs.token);
            tempVecConstructorArgIndexes.add(columnIndex);
            return sumConstructors.get(metadata.getColumnType(columnIndex));
        } else if (ast.type == FUNCTION && SqlKeywords.isCountKeyword(ast.token) &&
                (ast.paramCount == 0 || (ast.paramCount == 1 && ast.rhs.type == CONSTANT && !isNullKeyword(ast.rhs.token)))) {
            // count() is a no-arg function, count(1) is the same as count(*)
            tempVecConstructorArgIndexes.add(-1);
            return COUNT_CONSTRUCTOR;
        } else if (isSingleColumnFunction(ast, "count")) {
            columnIndex = metadata.getColumnIndex(ast.rhs.token);
            tempVecConstructorArgIndexes.add(columnIndex);
            return countConstructors.get(metadata.getColumnType(columnIndex));
        } else if (isSingleColumnFunction(ast, "ksum")) {
            columnIndex = metadata.getColumnIndex(ast.rhs.token);
            tempVecConstructorArgIndexes.add(columnIndex);
            return ksumConstructors.get(metadata.getColumnType(columnIndex));
        } else if (isSingleColumnFunction(ast, "nsum")) {
            columnIndex = metadata.getColumnIndex(ast.rhs.token);
            tempVecConstructorArgIndexes.add(columnIndex);
            return nsumConstructors.get(metadata.getColumnType(columnIndex));
        } else if (isSingleColumnFunction(ast, "avg")) {
            columnIndex = metadata.getColumnIndex(ast.rhs.token);
            tempVecConstructorArgIndexes.add(columnIndex);
            return avgConstructors.get(metadata.getColumnType(columnIndex));
        } else if (isSingleColumnFunction(ast, "min")) {
            columnIndex = metadata.getColumnIndex(ast.rhs.token);
            tempVecConstructorArgIndexes.add(columnIndex);
            return minConstructors.get(metadata.getColumnType(columnIndex));
        } else if (isSingleColumnFunction(ast, "max")) {
            columnIndex = metadata.getColumnIndex(ast.rhs.token);
            tempVecConstructorArgIndexes.add(columnIndex);
            return maxConstructors.get(metadata.getColumnType(columnIndex));
        }
        return null;
    }

    private boolean assembleKeysAndFunctionReferences(
            ObjList columns,
            RecordMetadata metadata,
            boolean checkLiterals
    ) {
        tempVaf.clear();
        tempMetadata.clear();
        tempSymbolSkewIndexes.clear();
        tempVecConstructors.clear();
        tempVecConstructorArgIndexes.clear();
        tempAggIndex.clear();

        for (int i = 0, n = columns.size(); i < n; i++) {
            final QueryColumn qc = columns.getQuick(i);
            final ExpressionNode ast = qc.getAst();
            if (ast.type == LITERAL) {
                if (checkLiterals) {
                    final int columnIndex = metadata.getColumnIndex(ast.token);
                    final int type = metadata.getColumnType(columnIndex);
                    if (ColumnType.isInt(type)) {
                        tempKeyIndexesInBase.add(columnIndex);
                        tempKeyIndex.add(i);
                        arrayColumnTypes.add(ColumnType.INT);
                        tempKeyKinds.add(GKK_VANILLA_INT);
                    } else if (ColumnType.isSymbol(type)) {
                        tempKeyIndexesInBase.add(columnIndex);
                        tempKeyIndex.add(i);
                        tempSymbolSkewIndexes.extendAndSet(i, columnIndex);
                        arrayColumnTypes.add(ColumnType.SYMBOL);
                        tempKeyKinds.add(GKK_VANILLA_INT);
                    } else {
                        return false;
                    }
                }
            } else {
                final VectorAggregateFunctionConstructor constructor = assembleFunctionReference(metadata, ast);
                if (constructor != null) {
                    tempVecConstructors.add(constructor);
                    tempAggIndex.add(i);
                } else {
                    return false;
                }
            }
        }
        return true;
    }

    // Check if lo, hi is set and lo >=0 while hi < 0 (meaning - return whole result set except some rows at start and some at the end)
    // because such case can't really be optimized by topN/bottomN
    private boolean canBeOptimized(QueryModel model, SqlExecutionContext context, Function loFunc, Function hiFunc) {
        if (model.getLimitLo() == null && model.getLimitHi() == null) {
            return false;
        }

        if (loFunc != null && loFunc.isConstant()
                && hiFunc != null && hiFunc.isConstant()) {
            try {
                loFunc.init(null, context);
                hiFunc.init(null, context);

                return !(loFunc.getLong(null) >= 0 && hiFunc.getLong(null) < 0);
            } catch (SqlException ex) {
                LOG.error().$("Failed to initialize lo or hi functions [").$("error=").$(ex.getMessage()).I$();
            }
        }

        return true;
    }

    private boolean checkIfSetCastIsRequired(RecordMetadata metadataA, RecordMetadata metadataB, boolean symbolDisallowed) {
        int columnCount = metadataA.getColumnCount();
        assert columnCount == metadataB.getColumnCount();

        for (int i = 0; i < columnCount; i++) {
            int typeA = metadataA.getColumnType(i);
            int typeB = metadataB.getColumnType(i);
            if (typeA != typeB || (typeA == ColumnType.SYMBOL && symbolDisallowed)) {
                return true;
            }
        }
        return false;
    }

    @Nullable
    private Function compileFilter(
            IntrinsicModel intrinsicModel,
            RecordMetadata readerMeta,
            SqlExecutionContext executionContext
    ) throws SqlException {
        if (intrinsicModel.filter != null) {
            return compileBooleanFilter(intrinsicModel.filter, readerMeta, executionContext);
        }
        return null;
    }

    private @Nullable ObjList compileWorkerFilterConditionally(
            boolean condition,
            int workerCount,
            ExpressionNode filterExpr,
            RecordMetadata metadata,
            SqlExecutionContext executionContext
    ) throws SqlException {
        if (condition) {
            ObjList workerFilters = new ObjList<>();
            for (int i = 0; i < workerCount; i++) {
                workerFilters.extendAndSet(i, compileBooleanFilter(filterExpr, metadata, executionContext));
            }
            return workerFilters;
        }
        return null;
    }

    private RecordCursorFactory createAsOfJoin(
            RecordMetadata metadata,
            RecordCursorFactory master,
            RecordSink masterKeySink,
            RecordCursorFactory slave,
            RecordSink slaveKeySink,
            int columnSplit,
            JoinContext joinContext
    ) {
        valueTypes.clear();
        valueTypes.add(ColumnType.LONG);
        valueTypes.add(ColumnType.LONG);

        return new AsOfJoinLightRecordCursorFactory(
                configuration,
                metadata,
                master,
                slave,
                keyTypes,
                valueTypes,
                masterKeySink,
                slaveKeySink,
                columnSplit,
                joinContext
        );
    }

    @NotNull
    private RecordCursorFactory createFullFatJoin(
            RecordCursorFactory master,
            RecordMetadata masterMetadata,
            CharSequence masterAlias,
            RecordCursorFactory slave,
            RecordMetadata slaveMetadata,
            CharSequence slaveAlias,
            int joinPosition,
            FullFatJoinGenerator generator,
            JoinContext joinContext
    ) throws SqlException {

        // create hash set of key columns to easily find them
        intHashSet.clear();
        for (int i = 0, n = listColumnFilterA.getColumnCount(); i < n; i++) {
            intHashSet.add(listColumnFilterA.getColumnIndexFactored(i));
        }

        // map doesn't support variable length types in map value, which is ok
        // when we join tables on strings - technically string is the key,
        // and we do not need to store it in value, but we will still reject
        //
        // never mind, this is a stop-gap measure until I understand the problem
        // fully

        for (int k = 0, m = slaveMetadata.getColumnCount(); k < m; k++) {
            if (intHashSet.excludes(k)) {
                if (ColumnType.isVariableLength(slaveMetadata.getColumnType(k))) {
                    throw SqlException
                            .position(joinPosition).put("right side column '")
                            .put(slaveMetadata.getColumnName(k)).put("' is of unsupported type");
                }
            }
        }

        RecordSink masterSink = RecordSinkFactory.getInstance(
                asm,
                masterMetadata,
                listColumnFilterB,
                true
        );

        // This metadata allocates native memory, it has to be closed in case join
        // generation is unsuccessful. The exception can be thrown anywhere between
        // try...catch
        JoinRecordMetadata metadata = new JoinRecordMetadata(
                configuration,
                masterMetadata.getColumnCount() + slaveMetadata.getColumnCount()
        );

        try {

            // metadata will have master record verbatim
            metadata.copyColumnMetadataFrom(masterAlias, masterMetadata);

            // slave record is split across key and value of map
            // the rationale is not to store columns twice
            // especially when map value does not support variable
            // length types


            final IntList columnIndex = new IntList(slaveMetadata.getColumnCount());
            // In map record value columns go first, so at this stage
            // we add to metadata all slave columns that are not keys.
            // Add same columns to filter while we are in this loop.
            listColumnFilterB.clear();
            valueTypes.clear();
            ArrayColumnTypes slaveTypes = new ArrayColumnTypes();
            if (slaveMetadata instanceof AbstractRecordMetadata) {
                for (int i = 0, n = slaveMetadata.getColumnCount(); i < n; i++) {
                    if (intHashSet.excludes(i)) {
                        final TableColumnMetadata m = ((AbstractRecordMetadata) slaveMetadata).getColumnMetadata(i);
                        metadata.add(slaveAlias, m);
                        listColumnFilterB.add(i + 1);
                        columnIndex.add(i);
                        valueTypes.add(m.getType());
                        slaveTypes.add(m.getType());
                    }
                }

                // now add key columns to metadata
                for (int i = 0, n = listColumnFilterA.getColumnCount(); i < n; i++) {
                    int index = listColumnFilterA.getColumnIndexFactored(i);
                    final TableColumnMetadata m = ((AbstractRecordMetadata) slaveMetadata).getColumnMetadata(index);
                    if (ColumnType.isSymbol(m.getType())) {
                        metadata.add(
                                slaveAlias,
                                m.getName(),
                                ColumnType.STRING,
                                false,
                                0,
                                false,
                                null
                        );
                        slaveTypes.add(ColumnType.STRING);
                    } else {
                        metadata.add(slaveAlias, m);
                        slaveTypes.add(m.getType());
                    }
                    columnIndex.add(index);
                }
            } else {
                for (int i = 0, n = slaveMetadata.getColumnCount(); i < n; i++) {
                    if (intHashSet.excludes(i)) {
                        int type = slaveMetadata.getColumnType(i);
                        metadata.add(
                                slaveAlias,
                                slaveMetadata.getColumnName(i),
                                type,
                                slaveMetadata.isColumnIndexed(i),
                                slaveMetadata.getIndexValueBlockCapacity(i),
                                slaveMetadata.isSymbolTableStatic(i),
                                slaveMetadata.getMetadata(i)
                        );
                        listColumnFilterB.add(i + 1);
                        columnIndex.add(i);
                        valueTypes.add(type);
                        slaveTypes.add(type);
                    }
                }

                // now add key columns to metadata
                for (int i = 0, n = listColumnFilterA.getColumnCount(); i < n; i++) {
                    int index = listColumnFilterA.getColumnIndexFactored(i);
                    int type = slaveMetadata.getColumnType(index);
                    if (ColumnType.isSymbol(type)) {
                        type = ColumnType.STRING;
                    }
                    metadata.add(
                            slaveAlias,
                            slaveMetadata.getColumnName(index),
                            type,
                            slaveMetadata.isColumnIndexed(i),
                            slaveMetadata.getIndexValueBlockCapacity(i),
                            slaveMetadata.isSymbolTableStatic(i),
                            slaveMetadata.getMetadata(i)
                    );
                    columnIndex.add(index);
                    slaveTypes.add(type);
                }
            }


            if (masterMetadata.getTimestampIndex() != -1) {
                metadata.setTimestampIndex(masterMetadata.getTimestampIndex());
            }

            return generator.create(
                    configuration,
                    metadata,
                    master,
                    slave,
                    keyTypes,
                    valueTypes,
                    slaveTypes,
                    masterSink,
                    RecordSinkFactory.getInstance(
                            asm,
                            slaveMetadata,
                            listColumnFilterA,
                            true
                    ),
                    masterMetadata.getColumnCount(),
                    RecordValueSinkFactory.getInstance(asm, slaveMetadata, listColumnFilterB),
                    columnIndex,
                    joinContext
            );

        } catch (Throwable e) {
            Misc.free(metadata);
            throw e;
        }
    }

    private RecordCursorFactory createHashJoin(
            RecordMetadata metadata,
            RecordCursorFactory master,
            RecordCursorFactory slave,
            int joinType,
            Function filter,
            JoinContext context
    ) {
        /*
         * JoinContext provides the following information:
         * a/bIndexes - index of model where join column is coming from
         * a/bNames - name of columns in respective models, these column names are not prefixed with table aliases
         * a/bNodes - the original column references, that can include table alias. Sometimes it doesn't when column name is unambiguous
         *
         * a/b are "inverted" in that "a" for slave and "b" for master
         *
         * The issue is when we use model indexes and vanilla column names they would only work on single-table
         * record cursor but original names with prefixed columns will only work with JoinRecordMetadata
         */
        final RecordMetadata masterMetadata = master.getMetadata();
        final RecordMetadata slaveMetadata = slave.getMetadata();
        final RecordSink masterKeySink = RecordSinkFactory.getInstance(
                asm,
                masterMetadata,
                listColumnFilterB,
                true
        );

        final RecordSink slaveKeySink = RecordSinkFactory.getInstance(
                asm,
                slaveMetadata,
                listColumnFilterA,
                true
        );

        valueTypes.clear();
        valueTypes.add(ColumnType.LONG);
        valueTypes.add(ColumnType.LONG);

        if (slave.recordCursorSupportsRandomAccess() && !fullFatJoins) {
            if (joinType == JOIN_INNER) {
                return new HashJoinLightRecordCursorFactory(
                        configuration,
                        metadata,
                        master,
                        slave,
                        keyTypes,
                        valueTypes,
                        masterKeySink,
                        slaveKeySink,
                        masterMetadata.getColumnCount(),
                        context
                );
            }

            if (filter != null) {
                return new HashOuterJoinFilteredLightRecordCursorFactory(
                        configuration,
                        metadata,
                        master,
                        slave,
                        keyTypes,
                        valueTypes,
                        masterKeySink,
                        slaveKeySink,
                        masterMetadata.getColumnCount(),
                        filter,
                        context
                );
            }

            return new HashOuterJoinLightRecordCursorFactory(
                    configuration,
                    metadata,
                    master,
                    slave,
                    keyTypes,
                    valueTypes,
                    masterKeySink,
                    slaveKeySink,
                    masterMetadata.getColumnCount(),
                    context
            );
        }

        entityColumnFilter.of(slaveMetadata.getColumnCount());
        RecordSink slaveSink = RecordSinkFactory.getInstance(
                asm,
                slaveMetadata,
                entityColumnFilter,
                false
        );

        if (joinType == JOIN_INNER) {
            return new HashJoinRecordCursorFactory(
                    configuration,
                    metadata,
                    master,
                    slave,
                    keyTypes,
                    valueTypes,
                    masterKeySink,
                    slaveKeySink,
                    slaveSink,
                    masterMetadata.getColumnCount(),
                    context
            );
        }

        if (filter != null) {
            return new HashOuterJoinFilteredRecordCursorFactory(
                    configuration,
                    metadata,
                    master,
                    slave,
                    keyTypes,
                    valueTypes,
                    masterKeySink,
                    slaveKeySink,
                    slaveSink,
                    masterMetadata.getColumnCount(),
                    filter,
                    context
            );
        }

        return new HashOuterJoinRecordCursorFactory(
                configuration,
                metadata,
                master,
                slave,
                keyTypes,
                valueTypes,
                masterKeySink,
                slaveKeySink,
                slaveSink,
                masterMetadata.getColumnCount(),
                context
        );
    }

    @NotNull
    private JoinRecordMetadata createJoinMetadata(
            CharSequence masterAlias,
            RecordMetadata masterMetadata,
            CharSequence slaveAlias,
            RecordMetadata slaveMetadata
    ) {
        return createJoinMetadata(
                masterAlias,
                masterMetadata,
                slaveAlias,
                slaveMetadata,
                masterMetadata.getTimestampIndex()
        );
    }

    @NotNull
    private JoinRecordMetadata createJoinMetadata(
            CharSequence masterAlias,
            RecordMetadata masterMetadata,
            CharSequence slaveAlias,
            RecordMetadata slaveMetadata,
            int timestampIndex
    ) {
        JoinRecordMetadata metadata;
        metadata = new JoinRecordMetadata(
                configuration,
                masterMetadata.getColumnCount() + slaveMetadata.getColumnCount()
        );

        metadata.copyColumnMetadataFrom(masterAlias, masterMetadata);
        metadata.copyColumnMetadataFrom(slaveAlias, slaveMetadata);

        if (timestampIndex != -1) {
            metadata.setTimestampIndex(timestampIndex);
        }
        return metadata;
    }

    private RecordCursorFactory createLtJoin(
            RecordMetadata metadata,
            RecordCursorFactory master,
            RecordSink masterKeySink,
            RecordCursorFactory slave,
            RecordSink slaveKeySink,
            int columnSplit,
            JoinContext joinContext
    ) {
        valueTypes.clear();
        valueTypes.add(ColumnType.LONG);
        valueTypes.add(ColumnType.LONG);

        return new LtJoinLightRecordCursorFactory(
                configuration,
                metadata,
                master,
                slave,
                keyTypes,
                valueTypes,
                masterKeySink,
                slaveKeySink,
                columnSplit,
                joinContext
        );
    }

    private RecordCursorFactory createSpliceJoin(
            RecordMetadata metadata,
            RecordCursorFactory master,
            RecordSink masterKeySink,
            RecordCursorFactory slave,
            RecordSink slaveKeySink,
            int columnSplit,
            JoinContext context
    ) {
        valueTypes.clear();
        valueTypes.add(ColumnType.LONG); // master previous
        valueTypes.add(ColumnType.LONG); // master current
        valueTypes.add(ColumnType.LONG); // slave previous
        valueTypes.add(ColumnType.LONG); // slave current

        return new SpliceJoinLightRecordCursorFactory(
                configuration,
                metadata,
                master,
                slave,
                keyTypes,
                valueTypes,
                masterKeySink,
                slaveKeySink,
                columnSplit,
                context
        );
    }

    private ObjList generateCastFunctions(
            RecordMetadata castToMetadata,
            RecordMetadata castFromMetadata,
            int modelPosition
    ) throws SqlException {
        int columnCount = castToMetadata.getColumnCount();
        ObjList castFunctions = new ObjList<>();
        for (int i = 0; i < columnCount; i++) {
            int toType = castToMetadata.getColumnType(i);
            int fromType = castFromMetadata.getColumnType(i);
            int toTag = ColumnType.tagOf(toType);
            int fromTag = ColumnType.tagOf(fromType);
            if (fromTag == ColumnType.NULL) {
                castFunctions.add(NullConstant.NULL);
            } else {
                switch (toTag) {
                    case ColumnType.BOOLEAN:
                        castFunctions.add(new BooleanColumn(i));
                        break;
                    case ColumnType.BYTE:
                        castFunctions.add(new ByteColumn(i));
                        break;
                    case ColumnType.SHORT:
                        switch (fromTag) {
                            // BOOLEAN will not be cast to CHAR
                            // in cast of BOOLEAN -> CHAR combination both will be cast to STRING
                            case ColumnType.BYTE:
                                castFunctions.add(new ByteColumn(i));
                                break;
                            case ColumnType.CHAR:
                                castFunctions.add(new CharColumn(i));
                                break;
                            case ColumnType.SHORT:
                                castFunctions.add(new ShortColumn(i));
                                break;
                            // wider types are not possible here
                            // SHORT will be cast to wider types, not other way around
                            // Wider types tested are: SHORT, INT, LONG, FLOAT, DOUBLE, DATE, TIMESTAMP, SYMBOL, STRING, LONG256
                            // GEOBYTE, GEOSHORT, GEOINT, GEOLONG
                        }
                        break;
                    case ColumnType.CHAR:
                        switch (fromTag) {
                            // BOOLEAN will not be cast to CHAR
                            // in cast of BOOLEAN -> CHAR combination both will be cast to STRING
                            case ColumnType.BYTE:
                                castFunctions.add(new CastByteToCharFunctionFactory.CastByteToCharFunction(new ByteColumn(i)));
                                break;
                            case ColumnType.CHAR:
                                castFunctions.add(new CharColumn(i));
                                break;
                            // wider types are not possible here
                            // CHAR will be cast to wider types, not other way around
                            // Wider types tested are: SHORT, INT, LONG, FLOAT, DOUBLE, DATE, TIMESTAMP, SYMBOL, STRING, LONG256
                            // GEOBYTE, GEOSHORT, GEOINT, GEOLONG
                            default:

                        }
                        break;
                    case ColumnType.INT:
                        switch (fromTag) {
                            // BOOLEAN will not be cast to INT
                            // in cast of BOOLEAN -> INT combination both will be cast to STRING
                            case ColumnType.BYTE:
                                castFunctions.add(new ByteColumn(i));
                                break;
                            case ColumnType.SHORT:
                                castFunctions.add(new ShortColumn(i));
                                break;
                            case ColumnType.CHAR:
                                castFunctions.add(new CharColumn(i));
                                break;
                            case ColumnType.INT:
                                castFunctions.add(new IntColumn(i));
                                break;
                            // wider types are not possible here
                            // INT will be cast to wider types, not other way around
                            // Wider types tested are: LONG, FLOAT, DOUBLE, DATE, TIMESTAMP, SYMBOL, STRING, LONG256
                            // GEOBYTE, GEOSHORT, GEOINT, GEOLONG
                        }
                        break;
                    case ColumnType.LONG:
                        switch (fromTag) {
                            // BOOLEAN will not be cast to LONG
                            // in cast of BOOLEAN -> LONG combination both will be cast to STRING
                            case ColumnType.BYTE:
                                castFunctions.add(new ByteColumn(i));
                                break;
                            case ColumnType.SHORT:
                                castFunctions.add(new ShortColumn(i));
                                break;
                            case ColumnType.CHAR:
                                castFunctions.add(new CharColumn(i));
                                break;
                            case ColumnType.INT:
                                castFunctions.add(new IntColumn(i));
                                break;
                            case ColumnType.LONG:
                                castFunctions.add(new LongColumn(i));
                                break;
                            default:
                                throw SqlException.unsupportedCast(
                                        modelPosition,
                                        castFromMetadata.getColumnName(i),
                                        fromType,
                                        toType
                                );
                                // wider types are not possible here
                                // LONG will be cast to wider types, not other way around
                                // Wider types tested are: FLOAT, DOUBLE, DATE, TIMESTAMP, SYMBOL, STRING, LONG256
                                // GEOBYTE, GEOSHORT, GEOINT, GEOLONG
                        }
                        break;
                    case ColumnType.DATE:
                        if (fromTag == ColumnType.DATE) {
                            castFunctions.add(new DateColumn(i));
                        } else {
                            throw SqlException.unsupportedCast(
                                    modelPosition,
                                    castFromMetadata.getColumnName(i),
                                    fromType,
                                    toType
                            );
                        }
                        break;
                    case ColumnType.UUID:
                        assert fromTag == ColumnType.UUID;
                        castFunctions.add(new UuidColumn(i));
                        break;
                    case ColumnType.TIMESTAMP:
                        switch (fromTag) {
                            case ColumnType.DATE:
                                castFunctions.add(new CastDateToTimestampFunctionFactory.CastDateToTimestampFunction(new DateColumn(i)));
                                break;
                            case ColumnType.TIMESTAMP:
                                castFunctions.add(new TimestampColumn(i));
                                break;
                            default:
                                throw SqlException.unsupportedCast(
                                        modelPosition,
                                        castFromMetadata.getColumnName(i),
                                        fromType,
                                        toType
                                );
                        }
                        break;
                    case ColumnType.FLOAT:
                        switch (fromTag) {
                            case ColumnType.BYTE:
                                castFunctions.add(new ByteColumn(i));
                                break;
                            case ColumnType.SHORT:
                                castFunctions.add(new ShortColumn(i));
                                break;
                            case ColumnType.INT:
                                castFunctions.add(new IntColumn(i));
                                break;
                            case ColumnType.LONG:
                                castFunctions.add(new LongColumn(i));
                                break;
                            case ColumnType.FLOAT:
                                castFunctions.add(new FloatColumn(i));
                                break;
                            default:
                                throw SqlException.unsupportedCast(
                                        modelPosition,
                                        castFromMetadata.getColumnName(i),
                                        fromType,
                                        toType
                                );
                        }
                        break;
                    case ColumnType.DOUBLE:
                        switch (fromTag) {
                            case ColumnType.BYTE:
                                castFunctions.add(new ByteColumn(i));
                                break;
                            case ColumnType.SHORT:
                                castFunctions.add(new ShortColumn(i));
                                break;
                            case ColumnType.INT:
                                castFunctions.add(new IntColumn(i));
                                break;
                            case ColumnType.LONG:
                                castFunctions.add(new LongColumn(i));
                                break;
                            case ColumnType.FLOAT:
                                castFunctions.add(new FloatColumn(i));
                                break;
                            case ColumnType.DOUBLE:
                                castFunctions.add(new DoubleColumn(i));
                                break;
                            default:
                                throw SqlException.unsupportedCast(
                                        modelPosition,
                                        castFromMetadata.getColumnName(i),
                                        fromType,
                                        toType
                                );
                        }
                        break;
                    case ColumnType.STRING:
                        switch (fromTag) {
                            case ColumnType.BOOLEAN:
                                castFunctions.add(new BooleanColumn(i));
                                break;
                            case ColumnType.BYTE:
                                castFunctions.add(new CastByteToStrFunctionFactory.CastByteToStrFunction(new ByteColumn(i)));
                                break;
                            case ColumnType.SHORT:
                                castFunctions.add(new CastShortToStrFunctionFactory.CastShortToStrFunction(new ShortColumn(i)));
                                break;
                            case ColumnType.CHAR:
                                // CharFunction has built-in cast to String
                                castFunctions.add(new CharColumn(i));
                                break;
                            case ColumnType.INT:
                                castFunctions.add(new CastIntToStrFunctionFactory.CastIntToStrFunction(new IntColumn(i)));
                                break;
                            case ColumnType.LONG:
                                castFunctions.add(new CastLongToStrFunctionFactory.CastLongToStrFunction(new LongColumn(i)));
                                break;
                            case ColumnType.DATE:
                                castFunctions.add(new CastDateToStrFunctionFactory.CastDateToStrFunction(new DateColumn(i)));
                                break;
                            case ColumnType.TIMESTAMP:
                                castFunctions.add(new CastTimestampToStrFunctionFactory.CastTimestampToStrFunction(new TimestampColumn(i)));
                                break;
                            case ColumnType.FLOAT:
                                castFunctions.add(new CastFloatToStrFunctionFactory.CastFloatToStrFunction(
                                        new FloatColumn(i),
                                        configuration.getFloatToStrCastScale()
                                ));
                                break;
                            case ColumnType.DOUBLE:
                                castFunctions.add(new CastDoubleToStrFunctionFactory.CastDoubleToStrFunction(
                                        new DoubleColumn(i),
                                        configuration.getDoubleToStrCastScale()
                                ));
                                break;
                            case ColumnType.STRING:
                                castFunctions.add(new StrColumn(i));
                                break;
                            case ColumnType.UUID:
                                castFunctions.add(new CastUuidToStrFunctionFactory.Func(new UuidColumn(i)));
                                break;
                            case ColumnType.SYMBOL:
                                castFunctions.add(
                                        new CastSymbolToStrFunctionFactory.CastSymbolToStrFunction(
                                                new SymbolColumn(i, castFromMetadata.isSymbolTableStatic(i))
                                        )
                                );
                                break;
                            case ColumnType.LONG256:
                                castFunctions.add(
                                        new CastLong256ToStrFunctionFactory.CastLong256ToStrFunction(
                                                new Long256Column(i)
                                        )
                                );
                                break;
                            case ColumnType.GEOBYTE:
                                castFunctions.add(
                                        CastGeoHashToGeoHashFunctionFactory.getGeoByteToStrCastFunction(
                                                new GeoByteColumn(i, toTag),
                                                ColumnType.getGeoHashBits(fromType)
                                        )
                                );
                                break;
                            case ColumnType.GEOSHORT:
                                castFunctions.add(
                                        CastGeoHashToGeoHashFunctionFactory.getGeoShortToStrCastFunction(
                                                new GeoShortColumn(i, toTag),
                                                ColumnType.getGeoHashBits(castFromMetadata.getColumnType(i))
                                        )
                                );
                                break;
                            case ColumnType.GEOINT:
                                castFunctions.add(
                                        CastGeoHashToGeoHashFunctionFactory.getGeoIntToStrCastFunction(
                                                new GeoIntColumn(i, toTag),
                                                ColumnType.getGeoHashBits(castFromMetadata.getColumnType(i))
                                        )
                                );
                                break;
                            case ColumnType.GEOLONG:
                                castFunctions.add(
                                        CastGeoHashToGeoHashFunctionFactory.getGeoLongToStrCastFunction(
                                                new GeoLongColumn(i, toTag),
                                                ColumnType.getGeoHashBits(castFromMetadata.getColumnType(i))
                                        )
                                );
                                break;
                            case ColumnType.BINARY:
                                throw SqlException.unsupportedCast(
                                        modelPosition,
                                        castFromMetadata.getColumnName(i),
                                        fromType,
                                        toType
                                );
                        }
                        break;
                    case ColumnType.SYMBOL:
                        castFunctions.add(new CastSymbolToStrFunctionFactory.CastSymbolToStrFunction(
                                new SymbolColumn(
                                        i,
                                        castFromMetadata.isSymbolTableStatic(i)
                                )));
                        break;
                    case ColumnType.LONG256:
                        castFunctions.add(new Long256Column(i));
                        break;
                    case ColumnType.GEOBYTE:
                        switch (fromTag) {
                            case ColumnType.STRING:
                                castFunctions.add(
                                        CastStrToGeoHashFunctionFactory.newInstance(
                                                0,
                                                toType,
                                                new StrColumn(i)
                                        )
                                );
                                break;
                            case ColumnType.GEOBYTE:
                                castFunctions.add(new GeoByteColumn(i, fromType));
                                break;
                            case ColumnType.GEOSHORT:
                                castFunctions.add(
                                        CastGeoHashToGeoHashFunctionFactory.newInstance(
                                                0,
                                                new GeoShortColumn(i, fromType),
                                                toType,
                                                fromType
                                        )
                                );
                                break;
                            case ColumnType.GEOINT:
                                castFunctions.add(
                                        CastGeoHashToGeoHashFunctionFactory.newInstance(
                                                0,
                                                new GeoIntColumn(i, fromType),
                                                toType,
                                                fromType
                                        )
                                );
                                break;
                            case ColumnType.GEOLONG:
                                castFunctions.add(
                                        CastGeoHashToGeoHashFunctionFactory.newInstance(
                                                0,
                                                new GeoLongColumn(i, fromType),
                                                toType,
                                                fromType
                                        )
                                );
                                break;
                            default:
                                throw SqlException.unsupportedCast(
                                        modelPosition,
                                        castFromMetadata.getColumnName(i),
                                        fromType,
                                        toType
                                );
                        }
                        break;
                    case ColumnType.GEOSHORT:
                        switch (fromTag) {
                            case ColumnType.STRING:
                                castFunctions.add(
                                        CastStrToGeoHashFunctionFactory.newInstance(
                                                0,
                                                toType,
                                                new StrColumn(i)
                                        )
                                );
                                break;
                            case ColumnType.GEOSHORT:
                                castFunctions.add(new GeoShortColumn(i, toType));
                                break;
                            case ColumnType.GEOINT:
                                castFunctions.add(
                                        CastGeoHashToGeoHashFunctionFactory.newInstance(
                                                0,
                                                new GeoIntColumn(i, fromType),
                                                toType,
                                                fromType
                                        )
                                );
                                break;
                            case ColumnType.GEOLONG:
                                castFunctions.add(
                                        CastGeoHashToGeoHashFunctionFactory.newInstance(
                                                0,
                                                new GeoLongColumn(i, fromType),
                                                toType,
                                                fromType
                                        )
                                );
                                break;
                            default:
                                throw SqlException.unsupportedCast(
                                        modelPosition,
                                        castFromMetadata.getColumnName(i),
                                        fromType,
                                        toType
                                );
                        }
                        break;
                    case ColumnType.GEOINT:
                        switch (fromTag) {
                            case ColumnType.STRING:
                                castFunctions.add(
                                        CastStrToGeoHashFunctionFactory.newInstance(
                                                0,
                                                toType,
                                                new StrColumn(i)
                                        )
                                );
                                break;
                            case ColumnType.GEOINT:
                                castFunctions.add(new GeoIntColumn(i, fromType));
                                break;
                            case ColumnType.GEOLONG:
                                castFunctions.add(
                                        CastGeoHashToGeoHashFunctionFactory.newInstance(
                                                0,
                                                new GeoLongColumn(i, fromType),
                                                toType,
                                                fromType
                                        )
                                );
                                break;
                            default:
                                throw SqlException.unsupportedCast(
                                        modelPosition,
                                        castFromMetadata.getColumnName(i),
                                        fromType,
                                        toType
                                );
                        }
                        break;
                    case ColumnType.GEOLONG:
                        switch (fromTag) {
                            case ColumnType.STRING:
                                castFunctions.add(
                                        CastStrToGeoHashFunctionFactory.newInstance(
                                                0,
                                                toType,
                                                new StrColumn(i)
                                        )
                                );
                                break;
                            case ColumnType.GEOLONG:
                                castFunctions.add(new GeoLongColumn(i, fromType));
                                break;
                            default:
                                throw SqlException.unsupportedCast(
                                        modelPosition,
                                        castFromMetadata.getColumnName(i),
                                        fromType,
                                        toType
                                );
                        }
                        break;
                    case ColumnType.BINARY:
                        castFunctions.add(new BinColumn(i));
                        break;
                }
            }
        }
        return castFunctions;
    }

    private RecordCursorFactory generateFilter(RecordCursorFactory factory, QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        final ExpressionNode filter = model.getWhereClause();
        return filter == null ? factory : generateFilter0(factory, model, executionContext, filter);
    }

    @NotNull
    private RecordCursorFactory generateFilter0(
            RecordCursorFactory factory,
            QueryModel model,
            SqlExecutionContext executionContext,
            ExpressionNode filterExpr
    ) throws SqlException {
        model.setWhereClause(null);

        final Function filter;
        try {
            filter = compileBooleanFilter(filterExpr, factory.getMetadata(), executionContext);
        } catch (Throwable e) {
            Misc.free(factory);
            throw e;
        }

        if (filter.isConstant()) {
            try {
                if (filter.getBool(null)) {
                    return factory;
                }
                RecordMetadata metadata = factory.getMetadata();
                assert (metadata instanceof GenericRecordMetadata);
                Misc.free(factory);
                return new EmptyTableRecordCursorFactory(metadata);
            } finally {
                filter.close();
            }
        }

        final boolean enableParallelFilter = configuration.isSqlParallelFilterEnabled();
        final boolean preTouchColumns = configuration.isSqlParallelFilterPreTouchEnabled();
        if (enableParallelFilter && factory.supportPageFrameCursor()) {

            final boolean useJit = executionContext.getJitMode() != SqlJitMode.JIT_MODE_DISABLED
                    && (!model.isUpdate() || executionContext.isWalApplication());
            final boolean canCompile = factory.supportPageFrameCursor() && JitUtil.isJitSupported();
            if (useJit && canCompile) {
                CompiledFilter jitFilter = null;
                try {
                    int jitOptions;
                    final ObjList bindVarFunctions = new ObjList<>();
                    try (PageFrameCursor cursor = factory.getPageFrameCursor(executionContext, ORDER_ANY)) {
                        final boolean forceScalar = executionContext.getJitMode() == SqlJitMode.JIT_MODE_FORCE_SCALAR;
                        jitIRSerializer.of(jitIRMem, executionContext, factory.getMetadata(), cursor, bindVarFunctions);
                        jitOptions = jitIRSerializer.serialize(filterExpr, forceScalar, enableJitDebug, enableJitNullChecks);
                    }

                    jitFilter = new CompiledFilter();
                    jitFilter.compile(jitIRMem, jitOptions);

                    final Function limitLoFunction = getLimitLoFunctionOnly(model, executionContext);
                    final int limitLoPos = model.getLimitAdviceLo() != null ? model.getLimitAdviceLo().position : 0;

                    LOG.info()
                            .$("JIT enabled for (sub)query [tableName=").utf8(model.getName())
                            .$(", fd=").$(executionContext.getRequestFd()).$(']').$();
                    return new AsyncJitFilteredRecordCursorFactory(
                            configuration,
                            executionContext.getMessageBus(),
                            factory,
                            bindVarFunctions,
                            filter,
                            compileWorkerFilterConditionally(
                                    !filter.isReadThreadSafe(),
                                    executionContext.getSharedWorkerCount(),
                                    filterExpr,
                                    factory.getMetadata(),
                                    executionContext
                            ),
                            jitFilter,
                            reduceTaskPool,
                            limitLoFunction,
                            limitLoPos,
                            preTouchColumns,
                            executionContext.getSharedWorkerCount()
                    );
                } catch (SqlException | LimitOverflowException ex) {
                    Misc.free(jitFilter);
                    LOG.debug()
                            .$("JIT cannot be applied to (sub)query [tableName=").utf8(model.getName())
                            .$(", ex=").$(ex.getFlyweightMessage())
                            .$(", fd=").$(executionContext.getRequestFd()).$(']').$();
                } finally {
                    jitIRSerializer.clear();
                    jitIRMem.truncate();
                }
            }

            // Use Java filter.
            final Function limitLoFunction;
            try {
                limitLoFunction = getLimitLoFunctionOnly(model, executionContext);
            } catch (Throwable e) {
                Misc.free(filter);
                Misc.free(factory);
                throw e;
            }
            final int limitLoPos = model.getLimitAdviceLo() != null ? model.getLimitAdviceLo().position : 0;
            return new AsyncFilteredRecordCursorFactory(
                    configuration,
                    executionContext.getMessageBus(),
                    factory,
                    filter,
                    reduceTaskPool,
                    compileWorkerFilterConditionally(
                            !filter.isReadThreadSafe(),
                            executionContext.getSharedWorkerCount(),
                            filterExpr,
                            factory.getMetadata(),
                            executionContext
                    ),
                    limitLoFunction,
                    limitLoPos,
                    preTouchColumns,
                    executionContext.getSharedWorkerCount()
            );
        }
        return new FilteredRecordCursorFactory(factory, filter);
    }

    private RecordCursorFactory generateFunctionQuery(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        final Function function = model.getTableNameFunction();
        if (function != null) {
            // We're transferring ownership of the function's factory to another factory
            // setting function to NULL will prevent double-ownership.
            // We should not release function itself, they typically just a lightweight factory wrapper.
            // Releasing function will also release the factory, which we don't want to happen.
            model.setTableNameFunction(null);
            return function.getRecordCursorFactory();
        } else {
            // when function is null we have to recompile it from scratch, including creating new factory
            return TableUtils.createCursorFunction(functionParser, model, executionContext).getRecordCursorFactory();
        }
    }

    private RecordCursorFactory generateJoins(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        final ObjList joinModels = model.getJoinModels();
        IntList ordered = model.getOrderedJoinModels();
        RecordCursorFactory master = null;
        CharSequence masterAlias = null;

        try {
            int n = ordered.size();
            assert n > 1;
            for (int i = 0; i < n; i++) {
                int index = ordered.getQuick(i);
                QueryModel slaveModel = joinModels.getQuick(index);

                if (i > 0) {
                    executionContext.pushTimestampRequiredFlag(joinsRequiringTimestamp[slaveModel.getJoinType()]);
                } else { // i == 0
                    // This is first model in the sequence of joins
                    // TS requirement is symmetrical on both right and left sides
                    // check if next join requires a timestamp
                    int nextJointType = joinModels.getQuick(ordered.getQuick(1)).getJoinType();
                    executionContext.pushTimestampRequiredFlag(joinsRequiringTimestamp[nextJointType]);
                }

                RecordCursorFactory slave = null;
                boolean releaseSlave = true;
                try {
                    // compile
                    slave = generateQuery(slaveModel, executionContext, index > 0);

                    // check if this is the root of joins
                    if (master == null) {
                        // This is an opportunistic check of order by clause
                        // to determine if we can get away ordering main record source only
                        // Ordering main record source could benefit from rowid access thus
                        // making it faster compared to ordering of join record source that
                        // doesn't allow rowid access.
                        master = slave;
                        releaseSlave = false;
                        masterAlias = slaveModel.getName();
                    } else {
                        // not the root, join to "master"
                        final int joinType = slaveModel.getJoinType();
                        final RecordMetadata masterMetadata = master.getMetadata();
                        final RecordMetadata slaveMetadata = slave.getMetadata();
                        Function filter = null;
                        JoinRecordMetadata joinMetadata;

                        switch (joinType) {
                            case JOIN_CROSS_LEFT:
                                assert slaveModel.getOuterJoinExpressionClause() != null;
                                joinMetadata = createJoinMetadata(masterAlias, masterMetadata, slaveModel.getName(), slaveMetadata);
                                filter = functionParser.parseFunction(slaveModel.getOuterJoinExpressionClause(), joinMetadata, executionContext);

                                master = new NestedLoopLeftJoinRecordCursorFactory(
                                        joinMetadata,
                                        master,
                                        slave,
                                        masterMetadata.getColumnCount(),
                                        filter,
                                        NullRecordFactory.getInstance(slaveMetadata)
                                );
                                masterAlias = null;
                                break;
                            case JOIN_CROSS:
                                validateOuterJoinExpressions(slaveModel, "CROSS");
                                master = new CrossJoinRecordCursorFactory(
                                        createJoinMetadata(masterAlias, masterMetadata, slaveModel.getName(), slaveMetadata),
                                        master,
                                        slave,
                                        masterMetadata.getColumnCount()
                                );
                                masterAlias = null;
                                break;
                            case JOIN_ASOF:
                                validateBothTimestamps(slaveModel, masterMetadata, slaveMetadata);
                                validateOuterJoinExpressions(slaveModel, "ASOF");
                                processJoinContext(index == 1, slaveModel.getContext(), masterMetadata, slaveMetadata);
                                if (slave.recordCursorSupportsRandomAccess() && !fullFatJoins) {
                                    if (listColumnFilterA.size() > 0 && listColumnFilterB.size() > 0) {
                                        master = createAsOfJoin(
                                                createJoinMetadata(masterAlias, masterMetadata, slaveModel.getName(), slaveMetadata),
                                                master,
                                                RecordSinkFactory.getInstance(
                                                        asm,
                                                        masterMetadata,
                                                        listColumnFilterB,
                                                        true
                                                ),
                                                slave,
                                                RecordSinkFactory.getInstance(
                                                        asm,
                                                        slaveMetadata,
                                                        listColumnFilterA,
                                                        true
                                                ),
                                                masterMetadata.getColumnCount(),
                                                slaveModel.getContext()
                                        );
                                    } else {
                                        master = new AsOfJoinNoKeyRecordCursorFactory(
                                                createJoinMetadata(masterAlias, masterMetadata, slaveModel.getName(), slaveMetadata),
                                                master,
                                                slave,
                                                masterMetadata.getColumnCount()
                                        );
                                    }
                                } else {
                                    master = createFullFatJoin(
                                            master,
                                            masterMetadata,
                                            masterAlias,
                                            slave,
                                            slaveMetadata,
                                            slaveModel.getName(),
                                            slaveModel.getJoinKeywordPosition(),
                                            CREATE_FULL_FAT_AS_OF_JOIN,
                                            slaveModel.getContext()
                                    );
                                }
                                masterAlias = null;
                                // if we fail after this step, master will release slave
                                releaseSlave = false;
                                validateBothTimestampOrders(master, slave, slaveModel.getJoinKeywordPosition());
                                break;
                            case JOIN_LT:
                                validateBothTimestamps(slaveModel, masterMetadata, slaveMetadata);
                                validateOuterJoinExpressions(slaveModel, "LT");
                                processJoinContext(index == 1, slaveModel.getContext(), masterMetadata, slaveMetadata);
                                if (slave.recordCursorSupportsRandomAccess() && !fullFatJoins) {
                                    if (listColumnFilterA.size() > 0 && listColumnFilterB.size() > 0) {
                                        master = createLtJoin(
                                                createJoinMetadata(masterAlias, masterMetadata, slaveModel.getName(), slaveMetadata),
                                                master,
                                                RecordSinkFactory.getInstance(
                                                        asm,
                                                        masterMetadata,
                                                        listColumnFilterB,
                                                        true
                                                ),
                                                slave,
                                                RecordSinkFactory.getInstance(
                                                        asm,
                                                        slaveMetadata,
                                                        listColumnFilterA,
                                                        true
                                                ),
                                                masterMetadata.getColumnCount(),
                                                slaveModel.getContext()
                                        );
                                    } else {
                                        master = new LtJoinNoKeyRecordCursorFactory(
                                                createJoinMetadata(masterAlias, masterMetadata, slaveModel.getName(), slaveMetadata),
                                                master,
                                                slave,
                                                masterMetadata.getColumnCount()
                                        );
                                    }
                                } else {
                                    master = createFullFatJoin(
                                            master,
                                            masterMetadata,
                                            masterAlias,
                                            slave,
                                            slaveMetadata,
                                            slaveModel.getName(),
                                            slaveModel.getJoinKeywordPosition(),
                                            CREATE_FULL_FAT_LT_JOIN,
                                            slaveModel.getContext()
                                    );
                                }
                                masterAlias = null;
                                // if we fail after this step, master will release slave
                                releaseSlave = false;
                                validateBothTimestampOrders(master, slave, slaveModel.getJoinKeywordPosition());
                                break;
                            case JOIN_SPLICE:
                                validateBothTimestamps(slaveModel, masterMetadata, slaveMetadata);
                                validateOuterJoinExpressions(slaveModel, "SPLICE");
                                processJoinContext(index == 1, slaveModel.getContext(), masterMetadata, slaveMetadata);
                                if (slave.recordCursorSupportsRandomAccess() && master.recordCursorSupportsRandomAccess() && !fullFatJoins) {
                                    master = createSpliceJoin(
                                            // splice join result does not have timestamp
                                            createJoinMetadata(masterAlias, masterMetadata, slaveModel.getName(), slaveMetadata, -1),
                                            master,
                                            RecordSinkFactory.getInstance(
                                                    asm,
                                                    masterMetadata,
                                                    listColumnFilterB,
                                                    true
                                            ),
                                            slave,
                                            RecordSinkFactory.getInstance(
                                                    asm,
                                                    slaveMetadata,
                                                    listColumnFilterA,
                                                    true
                                            ),
                                            masterMetadata.getColumnCount(),
                                            slaveModel.getContext()
                                    );
                                    // if we fail after this step, master will release slave
                                    releaseSlave = false;
                                    validateBothTimestampOrders(master, slave, slaveModel.getJoinKeywordPosition());
                                } else {
                                    assert false;
                                }
                                break;
                            default:
                                processJoinContext(index == 1, slaveModel.getContext(), masterMetadata, slaveMetadata);

                                joinMetadata = createJoinMetadata(masterAlias, masterMetadata, slaveModel.getName(), slaveMetadata);
                                if (slaveModel.getOuterJoinExpressionClause() != null) {
                                    filter = functionParser.parseFunction(slaveModel.getOuterJoinExpressionClause(), joinMetadata, executionContext);
                                }

                                if (joinType == JOIN_OUTER &&
                                        filter != null && filter.isConstant() && !filter.getBool(null)) {
                                    Misc.free(slave);
                                    slave = new EmptyTableRecordCursorFactory(slaveMetadata);
                                }

                                if (joinType == JOIN_INNER) {
                                    validateOuterJoinExpressions(slaveModel, "INNER");
                                }

                                master = createHashJoin(
                                        joinMetadata,
                                        master,
                                        slave,
                                        joinType,
                                        filter,
                                        slaveModel.getContext()
                                );
                                masterAlias = null;
                                break;
                        }
                    }
                } catch (Throwable th) {
                    master = Misc.free(master);
                    if (releaseSlave) {
                        Misc.free(slave);
                    }
                    throw th;
                } finally {
                    executionContext.popTimestampRequiredFlag();
                }

                // check if there are post-filters
                ExpressionNode filterExpr = slaveModel.getPostJoinWhereClause();
                if (filterExpr != null) {
                    if (configuration.isSqlParallelFilterEnabled() && master.supportPageFrameCursor()) {
                        final Function filter = compileBooleanFilter(
                                filterExpr,
                                master.getMetadata(),
                                executionContext
                        );

                        master = new AsyncFilteredRecordCursorFactory(
                                configuration,
                                executionContext.getMessageBus(),
                                master,
                                filter,
                                reduceTaskPool,
                                compileWorkerFilterConditionally(
                                        !filter.isReadThreadSafe(),
                                        executionContext.getSharedWorkerCount(),
                                        filterExpr,
                                        master.getMetadata(),
                                        executionContext
                                ),
                                null,
                                0,
                                false,
                                executionContext.getSharedWorkerCount()
                        );
                    } else {
                        master = new FilteredRecordCursorFactory(
                                master,
                                functionParser.parseFunction(filterExpr, master.getMetadata(), executionContext)
                        );
                    }
                }
            }

            // unfortunately we had to go all out to create join metadata
            // now it is time to check if we have constant conditions
            ExpressionNode constFilter = model.getConstWhereClause();
            if (constFilter != null) {
                Function function = functionParser.parseFunction(constFilter, null, executionContext);
                if (!function.getBool(null)) {
                    // do not copy metadata here
                    // this would have been JoinRecordMetadata, which is new instance anyway
                    // we have to make sure that this metadata is safely transitioned
                    // to empty cursor factory
                    JoinRecordMetadata metadata = (JoinRecordMetadata) master.getMetadata();
                    metadata.incrementRefCount();
                    RecordCursorFactory factory = new EmptyTableRecordCursorFactory(metadata);
                    Misc.free(master);
                    return factory;
                }
            }
            return master;
        } catch (Throwable e) {
            Misc.free(master);
            throw e;
        }
    }

    @NotNull
    private RecordCursorFactory generateLatestBy(RecordCursorFactory factory, QueryModel model) throws SqlException {
        final ObjList latestBy = model.getLatestBy();
        if (latestBy.size() == 0) {
            return factory;
        }

        // We require timestamp with any order.
        final int timestampIndex;
        try {
            timestampIndex = getTimestampIndex(model, factory);
            if (timestampIndex == -1) {
                throw SqlException.$(model.getModelPosition(), "latest by query does not provide dedicated TIMESTAMP column");
            }
        } catch (Throwable e) {
            Misc.free(factory);
            throw e;
        }

        final RecordMetadata metadata = factory.getMetadata();
        prepareLatestByColumnIndexes(latestBy, metadata);

        if (!factory.recordCursorSupportsRandomAccess()) {
            return new LatestByRecordCursorFactory(
                    configuration,
                    factory,
                    RecordSinkFactory.getInstance(asm, metadata, listColumnFilterA, false),
                    keyTypes,
                    timestampIndex
            );
        }

        boolean orderedByTimestampAsc = false;
        final QueryModel nested = model.getNestedModel();
        assert nested != null;
        final LowerCaseCharSequenceIntHashMap orderBy = nested.getOrderHash();
        CharSequence timestampColumn = metadata.getColumnName(timestampIndex);
        if (orderBy.get(timestampColumn) == QueryModel.ORDER_DIRECTION_ASCENDING) {
            // ORDER BY the timestamp column case.
            orderedByTimestampAsc = true;
        } else if (timestampIndex == metadata.getTimestampIndex() && orderBy.size() == 0) {
            // Empty ORDER BY, but the timestamp column in the designated timestamp.
            orderedByTimestampAsc = true;
        }

        return new LatestByLightRecordCursorFactory(
                configuration,
                factory,
                RecordSinkFactory.getInstance(asm, metadata, listColumnFilterA, false),
                keyTypes,
                timestampIndex,
                orderedByTimestampAsc
        );
    }

    @NotNull
    private RecordCursorFactory generateLatestByTableQuery(
            QueryModel model,
            @Transient TableReader reader,
            RecordMetadata metadata,
            TableToken tableToken,
            IntrinsicModel intrinsicModel,
            Function filter,
            SqlExecutionContext executionContext,
            int timestampIndex,
            @NotNull IntList columnIndexes,
            @NotNull IntList columnSizes,
            @NotNull LongList prefixes
    ) throws SqlException {
        final DataFrameCursorFactory dataFrameCursorFactory;
        if (intrinsicModel.hasIntervalFilters()) {
            dataFrameCursorFactory = new IntervalBwdDataFrameCursorFactory(
                    tableToken,
                    model.getTableId(),
                    model.getTableVersion(),
                    intrinsicModel.buildIntervalModel(),
                    timestampIndex,
                    GenericRecordMetadata.deepCopyOf(reader.getMetadata())
            );
        } else {
            dataFrameCursorFactory = new FullBwdDataFrameCursorFactory(
                    tableToken,
                    model.getTableId(),
                    model.getTableVersion(),
                    GenericRecordMetadata.deepCopyOf(reader.getMetadata())
            );
        }

        assert model.getLatestBy() != null && model.getLatestBy().size() > 0;
        ObjList latestBy = new ObjList<>(model.getLatestBy().size());
        latestBy.addAll(model.getLatestBy());
        final ExpressionNode latestByNode = latestBy.get(0);
        final int latestByIndex = metadata.getColumnIndexQuiet(latestByNode.token);
        final boolean indexed = metadata.isColumnIndexed(latestByIndex);

        // 'latest by' clause takes over the filter and the latest by nodes,
        // so that the later generateFilter() and generateLatestBy() are no-op
        model.setWhereClause(null);
        model.getLatestBy().clear();

        // if there are > 1 columns in the latest by statement, we cannot use indexes
        if (latestBy.size() > 1 || !ColumnType.isSymbol(metadata.getColumnType(latestByIndex))) {
            boolean symbolKeysOnly = true;
            for (int i = 0, n = keyTypes.getColumnCount(); i < n; i++) {
                symbolKeysOnly &= ColumnType.isSymbol(keyTypes.getColumnType(i));
            }
            if (symbolKeysOnly) {
                final IntList partitionByColumnIndexes = new IntList(listColumnFilterA.size());
                for (int i = 0, n = listColumnFilterA.size(); i < n; i++) {
                    partitionByColumnIndexes.add(listColumnFilterA.getColumnIndexFactored(i));
                }
                final IntList partitionBySymbolCounts = symbolEstimator.estimate(
                        model,
                        intrinsicModel.filter,
                        metadata,
                        partitionByColumnIndexes
                );
                return new LatestByAllSymbolsFilteredRecordCursorFactory(
                        metadata,
                        configuration,
                        dataFrameCursorFactory,
                        RecordSinkFactory.getInstance(asm, metadata, listColumnFilterA, false),
                        keyTypes,
                        partitionByColumnIndexes,
                        partitionBySymbolCounts,
                        filter,
                        columnIndexes
                );
            }
            return new LatestByAllFilteredRecordCursorFactory(
                    metadata,
                    configuration,
                    dataFrameCursorFactory,
                    RecordSinkFactory.getInstance(asm, metadata, listColumnFilterA, false),
                    keyTypes,
                    filter,
                    columnIndexes
            );
        }

        if (intrinsicModel.keyColumn != null) {
            // key column must always be the same as latest by column
            assert latestByIndex == metadata.getColumnIndexQuiet(intrinsicModel.keyColumn);

            if (intrinsicModel.keySubQuery != null) {
                final RecordCursorFactory rcf;
                final Record.CharSequenceFunction func;
                try {
                    rcf = generate(intrinsicModel.keySubQuery, executionContext);
                    func = validateSubQueryColumnAndGetGetter(intrinsicModel, rcf.getMetadata());
                } catch (Throwable e) {
                    Misc.free(dataFrameCursorFactory);
                    throw e;
                }

                return new LatestBySubQueryRecordCursorFactory(
                        configuration,
                        metadata,
                        dataFrameCursorFactory,
                        latestByIndex,
                        rcf,
                        filter,
                        indexed,
                        func,
                        columnIndexes
                );
            }

            final int nKeyValues = intrinsicModel.keyValueFuncs.size();
            final int nExcludedKeyValues = intrinsicModel.keyExcludedValueFuncs.size();
            if (indexed) {

                assert nKeyValues > 0;
                // deal with key values as a list
                // 1. resolve each value of the list to "int"
                // 2. get first row in index for each value (stream)

                final SymbolMapReader symbolMapReader = reader.getSymbolMapReader(columnIndexes.getQuick(latestByIndex));
                final RowCursorFactory rcf;
                if (nKeyValues == 1) {
                    final Function symbolValueFunc = intrinsicModel.keyValueFuncs.get(0);
                    final int symbol = symbolValueFunc.isRuntimeConstant()
                            ? SymbolTable.VALUE_NOT_FOUND
                            : symbolMapReader.keyOf(symbolValueFunc.getStr(null));

                    if (filter == null) {
                        if (symbol == SymbolTable.VALUE_NOT_FOUND) {
                            rcf = new LatestByValueDeferredIndexedRowCursorFactory(
                                    columnIndexes.getQuick(latestByIndex),
                                    symbolValueFunc,
                                    false
                            );
                        } else {
                            rcf = new LatestByValueIndexedRowCursorFactory(
                                    columnIndexes.getQuick(latestByIndex),
                                    symbol,
                                    false
                            );
                        }
                        return new DataFrameRecordCursorFactory(
                                configuration,
                                metadata,
                                dataFrameCursorFactory,
                                rcf,
                                false,
                                null,
                                false,
                                columnIndexes,
                                columnSizes,
                                true
                        );
                    }

                    if (symbol == SymbolTable.VALUE_NOT_FOUND) {
                        return new LatestByValueDeferredIndexedFilteredRecordCursorFactory(
                                metadata,
                                dataFrameCursorFactory,
                                latestByIndex,
                                symbolValueFunc,
                                filter,
                                columnIndexes
                        );
                    }
                    return new LatestByValueIndexedFilteredRecordCursorFactory(
                            metadata,
                            dataFrameCursorFactory,
                            latestByIndex,
                            symbol,
                            filter,
                            columnIndexes
                    );
                }

                return new LatestByValuesIndexedFilteredRecordCursorFactory(
                        configuration,
                        metadata,
                        dataFrameCursorFactory,
                        latestByIndex,
                        intrinsicModel.keyValueFuncs,
                        symbolMapReader,
                        filter,
                        columnIndexes
                );
            }

            assert nKeyValues > 0;

            // we have "latest by" column values, but no index

            if (nKeyValues > 1 || nExcludedKeyValues > 0) {
                return new LatestByDeferredListValuesFilteredRecordCursorFactory(
                        configuration,
                        metadata,
                        dataFrameCursorFactory,
                        latestByIndex,
                        intrinsicModel.keyValueFuncs,
                        intrinsicModel.keyExcludedValueFuncs,
                        filter,
                        columnIndexes
                );
            }

            assert nExcludedKeyValues == 0;

            // we have a single symbol key
            final Function symbolKeyFunc = intrinsicModel.keyValueFuncs.get(0);
            final SymbolMapReader symbolMapReader = reader.getSymbolMapReader(columnIndexes.getQuick(latestByIndex));
            final int symbolKey = symbolKeyFunc.isRuntimeConstant()
                    ? SymbolTable.VALUE_NOT_FOUND
                    : symbolMapReader.keyOf(symbolKeyFunc.getStr(null));
            if (symbolKey == SymbolTable.VALUE_NOT_FOUND) {
                return new LatestByValueDeferredFilteredRecordCursorFactory(
                        metadata,
                        dataFrameCursorFactory,
                        latestByIndex,
                        symbolKeyFunc,
                        filter,
                        columnIndexes
                );
            }

            return new LatestByValueFilteredRecordCursorFactory(
                    metadata,
                    dataFrameCursorFactory,
                    latestByIndex,
                    symbolKey,
                    filter,
                    columnIndexes
            );
        }
        // we select all values of "latest by" column

        assert intrinsicModel.keyValueFuncs.size() == 0;
        // get the latest rows for all values of "latest by" column

        if (indexed && filter == null) {
            return new LatestByAllIndexedRecordCursorFactory(
                    metadata,
                    configuration,
                    dataFrameCursorFactory,
                    latestByIndex,
                    columnIndexes,
                    prefixes
            );
        } else {
            return new LatestByDeferredListValuesFilteredRecordCursorFactory(
                    configuration,
                    metadata,
                    dataFrameCursorFactory,
                    latestByIndex,
                    filter,
                    columnIndexes
            );
        }
    }

    private RecordCursorFactory generateLimit(
            RecordCursorFactory factory,
            QueryModel model,
            SqlExecutionContext executionContext
    ) throws SqlException {

        if (factory.followedLimitAdvice()) {
            return factory;
        }

        ExpressionNode limitLo = model.getLimitLo();
        ExpressionNode limitHi = model.getLimitHi();

        // we've to check model otherwise we could be skipping limit in outer query that's actually different from the one in inner query!
        if ((limitLo == null && limitHi == null) || (factory.implementsLimit() && model.isLimitImplemented())) {
            return factory;
        }

        try {
            final Function loFunc = getLoFunction(model, executionContext);
            final Function hiFunc = getHiFunction(model, executionContext);

            return new LimitRecordCursorFactory(factory, loFunc, hiFunc);
        } catch (Throwable e) {
            Misc.free(factory);
            throw e;
        }
    }

    private RecordCursorFactory generateNoSelect(
            QueryModel model,
            SqlExecutionContext executionContext
    ) throws SqlException {
        ExpressionNode tableNameExpr = model.getTableNameExpr();
        if (tableNameExpr != null) {
            if (tableNameExpr.type == FUNCTION) {
                return generateFunctionQuery(model, executionContext);
            } else {
                return generateTableQuery(model, executionContext);
            }
        }
        return generateSubQuery(model, executionContext);
    }

    private RecordCursorFactory generateOrderBy(
            RecordCursorFactory recordCursorFactory,
            QueryModel model,
            SqlExecutionContext executionContext
    ) throws SqlException {
        if (recordCursorFactory.followedOrderByAdvice()) {
            return recordCursorFactory;
        }
        try {
            final LowerCaseCharSequenceIntHashMap orderBy = model.getOrderHash();
            final ObjList columnNames = orderBy.keys();
            final int orderByColumnCount = columnNames.size();

            if (orderByColumnCount > 0) {

                final RecordMetadata metadata = recordCursorFactory.getMetadata();
                final int timestampIndex = metadata.getTimestampIndex();

                listColumnFilterA.clear();
                intHashSet.clear();

                // column index sign indicates direction
                // therefore 0 index is not allowed
                for (int i = 0; i < orderByColumnCount; i++) {
                    final CharSequence column = columnNames.getQuick(i);
                    int index = metadata.getColumnIndexQuiet(column);

                    // check if column type is supported
                    if (ColumnType.isBinary(metadata.getColumnType(index))) {
                        // find position of offending column

                        ObjList nodes = model.getOrderBy();
                        int position = 0;
                        for (int j = 0, y = nodes.size(); j < y; j++) {
                            if (Chars.equals(column, nodes.getQuick(i).token)) {
                                position = nodes.getQuick(i).position;
                                break;
                            }
                        }
                        throw SqlException.$(position, "unsupported column type: ").put(ColumnType.nameOf(metadata.getColumnType(index)));
                    }

                    // we also maintain unique set of column indexes for better performance
                    if (intHashSet.add(index)) {
                        if (orderBy.get(column) == QueryModel.ORDER_DIRECTION_DESCENDING) {
                            listColumnFilterA.add(-index - 1);
                        } else {
                            listColumnFilterA.add(index + 1);
                        }
                    }
                }

                // if first column index is the same as timestamp of underling record cursor factory
                // we could have two possibilities:
                // 1. if we only have one column to order by - the cursor would already be ordered
                //    by timestamp (either ASC or DESC); we have nothing to do
                // 2. metadata of the new cursor will have the timestamp
                if (timestampIndex != -1) {
                    CharSequence column = columnNames.getQuick(0);
                    int index = metadata.getColumnIndexQuiet(column);
                    if (index == timestampIndex) {
                        if (orderByColumnCount == 1) {
                            if (orderBy.get(column) == QueryModel.ORDER_DIRECTION_ASCENDING) {
                                return recordCursorFactory;
                            } else if (orderBy.get(column) == ORDER_DIRECTION_DESCENDING &&
                                    recordCursorFactory.hasDescendingOrder()) {
                                return recordCursorFactory;
                            }
                        }
                    }
                }

                RecordMetadata orderedMetadata = GenericRecordMetadata.copyOfSansTimestamp(metadata);
                final Function loFunc = getLoFunction(model, executionContext);
                final Function hiFunc = getHiFunction(model, executionContext);

                if (recordCursorFactory.recordCursorSupportsRandomAccess()) {
                    if (canBeOptimized(model, executionContext, loFunc, hiFunc)) {
                        model.setLimitImplemented(true);
                        return new LimitedSizeSortedLightRecordCursorFactory(
                                configuration,
                                orderedMetadata,
                                recordCursorFactory,
                                recordComparatorCompiler.compile(metadata, listColumnFilterA),
                                loFunc,
                                hiFunc,
                                listColumnFilterA.copy()
                        );
                    } else {
                        return new SortedLightRecordCursorFactory(
                                configuration,
                                orderedMetadata,
                                recordCursorFactory,
                                recordComparatorCompiler.compile(metadata, listColumnFilterA),
                                listColumnFilterA.copy()
                        );
                    }
                }

                // when base record cursor does not support random access
                // we have to copy entire record into ordered structure

                entityColumnFilter.of(orderedMetadata.getColumnCount());
                return new SortedRecordCursorFactory(
                        configuration,
                        orderedMetadata,
                        recordCursorFactory,
                        RecordSinkFactory.getInstance(
                                asm,
                                orderedMetadata,
                                entityColumnFilter,
                                false
                        ),
                        recordComparatorCompiler.compile(metadata, listColumnFilterA),
                        listColumnFilterA.copy()
                );
            }

            return recordCursorFactory;
        } catch (SqlException | CairoException e) {
            recordCursorFactory.close();
            throw e;
        }
    }

    private RecordCursorFactory generateQuery(QueryModel model, SqlExecutionContext executionContext, boolean processJoins) throws SqlException {
        RecordCursorFactory factory = generateQuery0(model, executionContext, processJoins);
        if (model.getUnionModel() != null) {
            return generateSetFactory(model, factory, executionContext);
        }

        return factory;
    }

    private RecordCursorFactory generateQuery0(QueryModel model, SqlExecutionContext executionContext, boolean processJoins) throws SqlException {
        return generateLimit(
                generateOrderBy(
                        generateLatestBy(
                                generateFilter(
                                        generateSelect(
                                                model,
                                                executionContext,
                                                processJoins
                                        ),
                                        model,
                                        executionContext
                                ),
                                model
                        ),
                        model,
                        executionContext
                ),
                model,
                executionContext
        );
    }

    @NotNull
    private RecordCursorFactory generateSampleBy(
            QueryModel model,
            SqlExecutionContext executionContext,
            ExpressionNode sampleByNode,
            ExpressionNode sampleByUnits
    ) throws SqlException {
        final ExpressionNode timezoneName = model.getSampleByTimezoneName();
        final Function timezoneNameFunc;
        final int timezoneNameFuncPos;
        final ExpressionNode offset = model.getSampleByOffset();
        final Function offsetFunc;
        final int offsetFuncPos;

        if (timezoneName != null) {
            timezoneNameFunc = functionParser.parseFunction(
                    timezoneName,
                    EmptyRecordMetadata.INSTANCE,
                    executionContext
            );
            timezoneNameFuncPos = timezoneName.position;
        } else {
            timezoneNameFunc = StrConstant.NULL;
            timezoneNameFuncPos = 0;
        }

        if (ColumnType.isUndefined(timezoneNameFunc.getType())) {
            timezoneNameFunc.assignType(ColumnType.STRING, executionContext.getBindVariableService());
        } else if ((!timezoneNameFunc.isConstant() && !timezoneNameFunc.isRuntimeConstant())
                || !ColumnType.isAssignableFrom(timezoneNameFunc.getType(), ColumnType.STRING)) {
            throw SqlException.$(timezoneNameFuncPos, "timezone must be a constant expression of STRING or CHAR type");
        }

        if (offset != null) {
            offsetFunc = functionParser.parseFunction(
                    offset,
                    EmptyRecordMetadata.INSTANCE,
                    executionContext
            );
            offsetFuncPos = offset.position;
        } else {
            offsetFunc = StrConstant.NULL;
            offsetFuncPos = 0;
        }

        if (ColumnType.isUndefined(offsetFunc.getType())) {
            offsetFunc.assignType(ColumnType.STRING, executionContext.getBindVariableService());
        } else if ((!offsetFunc.isConstant() && !offsetFunc.isRuntimeConstant())
                || !ColumnType.isAssignableFrom(offsetFunc.getType(), ColumnType.STRING)) {
            throw SqlException.$(offsetFuncPos, "offset must be a constant expression of STRING or CHAR type");
        }

        RecordCursorFactory factory = null;
        // We require timestamp with asc order.
        final int timestampIndex;
        // Require timestamp in sub-query when it's not additionally specified as timestamp(col).
        executionContext.pushTimestampRequiredFlag(model.getTimestamp() == null);
        try {
            factory = generateSubQuery(model, executionContext);
            timestampIndex = getTimestampIndex(model, factory);
            if (timestampIndex == -1 || factory.hasDescendingOrder()) {
                throw SqlException.$(model.getModelPosition(), "base query does not provide ASC order over dedicated TIMESTAMP column");
            }
        } catch (Throwable e) {
            Misc.free(factory);
            throw e;
        } finally {
            executionContext.popTimestampRequiredFlag();
        }

        final RecordMetadata metadata = factory.getMetadata();
        final ObjList sampleByFill = model.getSampleByFill();
        final TimestampSampler timestampSampler;
        final int fillCount = sampleByFill.size();
        try {
            if (sampleByUnits == null) {
                timestampSampler = TimestampSamplerFactory.getInstance(sampleByNode.token, sampleByNode.position);
            } else {
                Function sampleByPeriod = functionParser.parseFunction(
                        sampleByNode,
                        EmptyRecordMetadata.INSTANCE,
                        executionContext
                );
                if (!sampleByPeriod.isConstant() || (sampleByPeriod.getType() != ColumnType.LONG && sampleByPeriod.getType() != ColumnType.INT)) {
                    Misc.free(sampleByPeriod);
                    throw SqlException.$(sampleByNode.position, "sample by period must be a constant expression of INT or LONG type");
                }
                long period = sampleByPeriod.getLong(null);
                sampleByPeriod.close();
                timestampSampler = TimestampSamplerFactory.getInstance(period, sampleByUnits.token, sampleByUnits.position);
            }

            keyTypes.clear();
            valueTypes.clear();
            listColumnFilterA.clear();

            if (fillCount == 1 && Chars.equalsLowerCaseAscii(sampleByFill.getQuick(0).token, "linear")) {

                final int columnCount = metadata.getColumnCount();
                final ObjList groupByFunctions = new ObjList<>(columnCount);
                final ObjList recordFunctions = new ObjList<>(columnCount);

                valueTypes.add(ColumnType.BYTE); // gap flag

                GroupByUtils.prepareGroupByFunctions(
                        model,
                        metadata,
                        functionParser,
                        executionContext,
                        groupByFunctions,
                        groupByFunctionPositions,
                        valueTypes
                );

                final GenericRecordMetadata groupByMetadata = new GenericRecordMetadata();
                GroupByUtils.prepareGroupByRecordFunctions(
                        model,
                        metadata,
                        listColumnFilterA,
                        groupByFunctions,
                        groupByFunctionPositions,
                        recordFunctions,
                        recordFunctionPositions,
                        groupByMetadata,
                        keyTypes,
                        valueTypes.getColumnCount(),
                        false,
                        timestampIndex
                );

                return new SampleByInterpolateRecordCursorFactory(
                        asm,
                        configuration,
                        factory,
                        groupByMetadata,
                        groupByFunctions,
                        recordFunctions,
                        timestampSampler,
                        model,
                        listColumnFilterA,
                        keyTypes,
                        valueTypes,
                        entityColumnFilter,
                        groupByFunctionPositions,
                        timestampIndex
                );
            }

            final int columnCount = model.getColumns().size();
            final ObjList groupByFunctions = new ObjList<>(columnCount);
            valueTypes.add(ColumnType.TIMESTAMP); // first value is always timestamp

            GroupByUtils.prepareGroupByFunctions(
                    model,
                    metadata,
                    functionParser,
                    executionContext,
                    groupByFunctions,
                    groupByFunctionPositions,
                    valueTypes
            );

            final ObjList recordFunctions = new ObjList<>(columnCount);
            final GenericRecordMetadata groupByMetadata = new GenericRecordMetadata();

            GroupByUtils.prepareGroupByRecordFunctions(
                    model,
                    metadata,
                    listColumnFilterA,
                    groupByFunctions,
                    groupByFunctionPositions,
                    recordFunctions,
                    recordFunctionPositions,
                    groupByMetadata,
                    keyTypes,
                    valueTypes.getColumnCount(),
                    false,
                    timestampIndex
            );


            boolean isFillNone = fillCount == 0 || fillCount == 1 && Chars.equalsLowerCaseAscii(sampleByFill.getQuick(0).token, "none");
            boolean allGroupsFirstLast = isFillNone && allGroupsFirstLastWithSingleSymbolFilter(model, metadata);
            if (allGroupsFirstLast) {
                SingleSymbolFilter symbolFilter = factory.convertToSampleByIndexDataFrameCursorFactory();
                if (symbolFilter != null) {
                    return new SampleByFirstLastRecordCursorFactory(
                            factory,
                            timestampSampler,
                            groupByMetadata,
                            model.getColumns(),
                            metadata,
                            timezoneNameFunc,
                            timezoneNameFuncPos,
                            offsetFunc,
                            offsetFuncPos,
                            timestampIndex,
                            symbolFilter,
                            configuration.getSampleByIndexSearchPageSize()
                    );
                }
            }

            if (fillCount == 1 && Chars.equalsLowerCaseAscii(sampleByFill.getQuick(0).token, "prev")) {
                if (keyTypes.getColumnCount() == 0) {
                    return new SampleByFillPrevNotKeyedRecordCursorFactory(
                            asm,
                            factory,
                            timestampSampler,
                            groupByMetadata,
                            groupByFunctions,
                            recordFunctions,
                            timestampIndex,
                            valueTypes.getColumnCount(),
                            timezoneNameFunc,
                            timezoneNameFuncPos,
                            offsetFunc,
                            offsetFuncPos
                    );
                }

                return new SampleByFillPrevRecordCursorFactory(
                        asm,
                        configuration,
                        factory,
                        timestampSampler,
                        listColumnFilterA,
                        keyTypes,
                        valueTypes,
                        groupByMetadata,
                        groupByFunctions,
                        recordFunctions,
                        timestampIndex,
                        timezoneNameFunc,
                        timezoneNameFuncPos,
                        offsetFunc,
                        offsetFuncPos
                );
            }

            if (isFillNone) {

                if (keyTypes.getColumnCount() == 0) {
                    // this sample by is not keyed
                    return new SampleByFillNoneNotKeyedRecordCursorFactory(
                            asm,
                            factory,
                            timestampSampler,
                            groupByMetadata,
                            groupByFunctions,
                            recordFunctions,
                            valueTypes.getColumnCount(),
                            timestampIndex,
                            timezoneNameFunc,
                            timezoneNameFuncPos,
                            offsetFunc,
                            offsetFuncPos
                    );
                }

                return new SampleByFillNoneRecordCursorFactory(
                        asm,
                        configuration,
                        factory,
                        groupByMetadata,
                        groupByFunctions,
                        recordFunctions,
                        timestampSampler,
                        listColumnFilterA,
                        keyTypes,
                        valueTypes,
                        timestampIndex,
                        timezoneNameFunc,
                        timezoneNameFuncPos,
                        offsetFunc,
                        offsetFuncPos
                );
            }

            if (fillCount == 1 && isNullKeyword(sampleByFill.getQuick(0).token)) {
                if (keyTypes.getColumnCount() == 0) {
                    return new SampleByFillNullNotKeyedRecordCursorFactory(
                            asm,
                            factory,
                            timestampSampler,
                            groupByMetadata,
                            groupByFunctions,
                            recordFunctions,
                            recordFunctionPositions,
                            valueTypes.getColumnCount(),
                            timestampIndex,
                            timezoneNameFunc,
                            timezoneNameFuncPos,
                            offsetFunc,
                            offsetFuncPos
                    );
                }

                return new SampleByFillNullRecordCursorFactory(
                        asm,
                        configuration,
                        factory,
                        timestampSampler,
                        listColumnFilterA,
                        keyTypes,
                        valueTypes,
                        groupByMetadata,
                        groupByFunctions,
                        recordFunctions,
                        recordFunctionPositions,
                        timestampIndex,
                        timezoneNameFunc,
                        timezoneNameFuncPos,
                        offsetFunc,
                        offsetFuncPos
                );
            }

            assert fillCount > 0;

            if (keyTypes.getColumnCount() == 0) {
                return new SampleByFillValueNotKeyedRecordCursorFactory(
                        asm,
                        factory,
                        timestampSampler,
                        sampleByFill,
                        groupByMetadata,
                        groupByFunctions,
                        recordFunctions,
                        recordFunctionPositions,
                        valueTypes.getColumnCount(),
                        timestampIndex,
                        timezoneNameFunc,
                        timezoneNameFuncPos,
                        offsetFunc,
                        offsetFuncPos
                );
            }

            return new SampleByFillValueRecordCursorFactory(
                    asm,
                    configuration,
                    factory,
                    timestampSampler,
                    listColumnFilterA,
                    sampleByFill,
                    keyTypes,
                    valueTypes,
                    groupByMetadata,
                    groupByFunctions,
                    recordFunctions,
                    recordFunctionPositions,
                    timestampIndex,
                    timezoneNameFunc,
                    timezoneNameFuncPos,
                    offsetFunc,
                    offsetFuncPos
            );
        } catch (Throwable e) {
            Misc.free(factory);
            throw e;
        }
    }

    private RecordCursorFactory generateSelect(
            QueryModel model,
            SqlExecutionContext executionContext,
            boolean processJoins
    ) throws SqlException {
        switch (model.getSelectModelType()) {
            case QueryModel.SELECT_MODEL_CHOOSE:
                return generateSelectChoose(model, executionContext);
            case QueryModel.SELECT_MODEL_GROUP_BY:
                return generateSelectGroupBy(model, executionContext);
            case QueryModel.SELECT_MODEL_VIRTUAL:
                return generateSelectVirtual(model, executionContext);
            case QueryModel.SELECT_MODEL_ANALYTIC:
                return generateSelectAnalytic(model, executionContext);
            case QueryModel.SELECT_MODEL_DISTINCT:
                return generateSelectDistinct(model, executionContext);
            case QueryModel.SELECT_MODEL_CURSOR:
                return generateSelectCursor(model, executionContext);
            default:
                if (model.getJoinModels().size() > 1 && processJoins) {
                    return generateJoins(model, executionContext);
                }
                return generateNoSelect(model, executionContext);
        }
    }

    private RecordCursorFactory generateSelectAnalytic(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        final RecordCursorFactory base = generateSubQuery(model, executionContext);
        final RecordMetadata baseMetadata = base.getMetadata();
        final ObjList columns = model.getColumns();
        final int columnCount = columns.size();
        groupedAnalytic.clear();
        ObjList naturalOrderFunctions = null;

        valueTypes.clear();
        ArrayColumnTypes chainTypes = valueTypes;
        GenericRecordMetadata chainMetadata = new GenericRecordMetadata();
        GenericRecordMetadata factoryMetadata = new GenericRecordMetadata();

        listColumnFilterA.clear();
        listColumnFilterB.clear();

        // we need two passes over columns because partitionBy and orderBy clauses of
        // the analytical function must reference the metadata of "this" factory.

        // pass #1 assembles metadata of non-analytic columns

        // set of column indexes in the base metadata that has already been added to the main
        // metadata instance
        intHashSet.clear();
        final IntList columnIndexes = new IntList();
        for (int i = 0; i < columnCount; i++) {
            final QueryColumn qc = columns.getQuick(i);
            if (!(qc instanceof AnalyticColumn)) {
                final int columnIndex = baseMetadata.getColumnIndexQuiet(qc.getAst().token);
                final TableColumnMetadata m = AbstractRecordMetadata.copyOf(baseMetadata, columnIndex);
                chainMetadata.add(i, m);
                factoryMetadata.add(i, m);
                chainTypes.add(i, m.getType());
                listColumnFilterA.extendAndSet(i, i + 1);
                listColumnFilterB.extendAndSet(i, columnIndex);
                intHashSet.add(columnIndex);
                columnIndexes.extendAndSet(i, columnIndex);
            }
        }

        // pass #2 - add remaining base metadata column that are not in intHashSet already
        // we need to pay attention to stepping over analytic column slots
        // Chain metadata is assembled in such way that all columns the factory
        // needs to provide are at the beginning of the metadata so the record the factory cursor
        // returns can be chain record, because the chain record is always longer than record needed out of the
        // cursor and relevant columns are 0..n limited by factory metadata

        int addAt = columnCount;
        for (int i = 0, n = baseMetadata.getColumnCount(); i < n; i++) {
            if (intHashSet.excludes(i)) {
                final TableColumnMetadata m = AbstractRecordMetadata.copyOf(baseMetadata, i);
                chainMetadata.add(addAt, m);
                chainTypes.add(addAt, m.getType());
                listColumnFilterA.extendAndSet(addAt, addAt + 1);
                listColumnFilterB.extendAndSet(addAt, i);
                columnIndexes.extendAndSet(addAt, i);
                addAt++;
            }
        }

        // pass #3 assembles analytic column metadata into a list
        // not main metadata to avoid partitionBy functions accidentally looking up
        // analytic columns recursively

        deferredAnalyticMetadata.clear();
        for (int i = 0; i < columnCount; i++) {
            final QueryColumn qc = columns.getQuick(i);
            if (qc instanceof AnalyticColumn) {
                final AnalyticColumn ac = (AnalyticColumn) qc;
                final ExpressionNode ast = qc.getAst();
                if (ast.paramCount > 1) {
                    Misc.free(base);
                    throw SqlException.$(ast.position, "too many arguments");
                }

                ObjList partitionBy = null;
                int psz = ac.getPartitionBy().size();
                if (psz > 0) {
                    partitionBy = new ObjList<>(psz);
                    for (int j = 0; j < psz; j++) {
                        partitionBy.add(
                                functionParser.parseFunction(ac.getPartitionBy().getQuick(j), chainMetadata, executionContext)
                        );
                    }
                }

                final VirtualRecord partitionByRecord;
                final RecordSink partitionBySink;

                if (partitionBy != null) {
                    partitionByRecord = new VirtualRecord(partitionBy);
                    keyTypes.clear();
                    final int partitionByCount = partitionBy.size();

                    for (int j = 0; j < partitionByCount; j++) {
                        keyTypes.add(partitionBy.getQuick(j).getType());
                    }
                    entityColumnFilter.of(partitionByCount);
                    // create sink
                    partitionBySink = RecordSinkFactory.getInstance(
                            asm,
                            keyTypes,
                            entityColumnFilter,
                            false
                    );
                } else {
                    partitionByRecord = null;
                    partitionBySink = null;
                }


                final int osz = ac.getOrderBy().size();
                executionContext.configureAnalyticContext(
                        partitionByRecord,
                        partitionBySink,
                        keyTypes,
                        osz > 0,
                        base.recordCursorSupportsRandomAccess()
                );
                final Function f;
                try {
                    f = functionParser.parseFunction(ast, baseMetadata, executionContext);
                    if (!(f instanceof AnalyticFunction)) {
                        Misc.free(base);
                        throw SqlException.$(ast.position, "non-analytic function called in analytic context");
                    }
                } finally {
                    executionContext.clearAnalyticContext();
                }

                AnalyticFunction analyticFunction = (AnalyticFunction) f;

                // analyze order by clause on the current model and optimise out
                // order by on analytic function if it matches the one on the model
                final LowerCaseCharSequenceIntHashMap orderHash = model.getOrderHash();
                boolean dismissOrder;
                if (osz > 0 && orderHash.size() > 0) {
                    dismissOrder = true;
                    for (int j = 0; j < osz; j++) {
                        ExpressionNode node = ac.getOrderBy().getQuick(j);
                        int direction = ac.getOrderByDirection().getQuick(j);
                        if (orderHash.get(node.token) != direction) {
                            dismissOrder = false;
                            break;
                        }
                    }
                } else {
                    dismissOrder = false;
                }

                if (osz > 0 && !dismissOrder) {
                    IntList order = toOrderIndices(chainMetadata, ac.getOrderBy(), ac.getOrderByDirection());
                    // init comparator if we need
                    analyticFunction.initRecordComparator(recordComparatorCompiler, chainTypes, order);
                    ObjList funcs = groupedAnalytic.get(order);
                    if (funcs == null) {
                        groupedAnalytic.put(order, funcs = new ObjList<>());
                    }
                    funcs.add(analyticFunction);
                } else {
                    if (naturalOrderFunctions == null) {
                        naturalOrderFunctions = new ObjList<>();
                    }
                    naturalOrderFunctions.add(analyticFunction);
                }

                analyticFunction.setColumnIndex(i);

                deferredAnalyticMetadata.extendAndSet(i, new TableColumnMetadata(
                        Chars.toString(qc.getAlias()),
                        analyticFunction.getType(),
                        false,
                        0,
                        false,
                        null
                ));

                listColumnFilterA.extendAndSet(i, -i - 1);
            }
        }

        // after all columns are processed we can re-insert deferred metadata
        for (int i = 0, n = deferredAnalyticMetadata.size(); i < n; i++) {
            TableColumnMetadata m = deferredAnalyticMetadata.getQuick(i);
            if (m != null) {
                chainTypes.add(i, m.getType());
                factoryMetadata.add(i, m);
            }
        }

        final ObjList analyticComparators = new ObjList<>(groupedAnalytic.size());
        final ObjList> functionGroups = new ObjList<>(groupedAnalytic.size());
        for (ObjObjHashMap.Entry> e : groupedAnalytic) {
            analyticComparators.add(recordComparatorCompiler.compile(chainTypes, e.key));
            functionGroups.add(e.value);
        }

        final RecordSink recordSink = RecordSinkFactory.getInstance(
                asm,
                chainTypes,
                listColumnFilterA,
                false,
                listColumnFilterB
        );

        return new CachedAnalyticRecordCursorFactory(
                configuration,
                base,
                recordSink,
                factoryMetadata,
                chainTypes,
                analyticComparators,
                functionGroups,
                naturalOrderFunctions,
                columnIndexes
        );
    }

    private RecordCursorFactory generateSelectChoose(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        final RecordCursorFactory factory = generateSubQuery(model, executionContext);

        final RecordMetadata metadata = factory.getMetadata();
        final ObjList columns = model.getColumns();
        final int selectColumnCount = columns.size();
        final ExpressionNode timestamp = model.getTimestamp();

        // If this is update query and column types don't match exactly
        // to the column type of table to be updated we have to fall back to
        // select-virtual
        if (model.isUpdate()) {
            boolean columnTypeMismatch = false;
            ObjList updateColumnNames = model.getUpdateTableColumnNames();
            IntList updateColumnTypes = model.getUpdateTableColumnTypes();

            for (int i = 0, n = columns.size(); i < n; i++) {
                QueryColumn queryColumn = columns.getQuick(i);
                CharSequence columnName = queryColumn.getAlias();
                int index = metadata.getColumnIndexQuiet(queryColumn.getAst().token);
                assert index > -1 : "wtf? " + queryColumn.getAst().token;

                int updateColumnIndex = updateColumnNames.indexOf(columnName);
                int updateColumnType = updateColumnTypes.get(updateColumnIndex);

                if (updateColumnType != metadata.getColumnType(index)) {
                    columnTypeMismatch = true;
                    break;
                }
            }

            if (columnTypeMismatch) {
                return generateSelectVirtualWithSubQuery(model, executionContext, factory);
            }
        }

        boolean entity;
        // the model is considered entity when it doesn't add any value to its nested model
        //
        if (timestamp == null && metadata.getColumnCount() == selectColumnCount) {
            entity = true;
            for (int i = 0; i < selectColumnCount; i++) {
                QueryColumn qc = columns.getQuick(i);
                if (
                        !Chars.equals(metadata.getColumnName(i), qc.getAst().token) ||
                                qc.getAlias() != null && !(Chars.equals(qc.getAlias(), qc.getAst().token))
                ) {
                    entity = false;
                    break;
                }
            }
        } else {
            entity = false;
        }

        if (entity) {
            return factory;
        }

        // We require timestamp with asc order.
        final int timestampIndex;
        try {
            timestampIndex = getTimestampIndex(model, factory);
            if (executionContext.isTimestampRequired() && (timestampIndex == -1 || factory.hasDescendingOrder())) {
                throw SqlException.$(model.getModelPosition(), "ASC order over TIMESTAMP column is required but not provided");
            }
        } catch (Throwable e) {
            Misc.free(factory);
            throw e;
        }

        final IntList columnCrossIndex = new IntList(selectColumnCount);
        final GenericRecordMetadata selectMetadata = new GenericRecordMetadata();
        boolean timestampSet = false;
        for (int i = 0; i < selectColumnCount; i++) {
            final QueryColumn queryColumn = columns.getQuick(i);
            int index = metadata.getColumnIndexQuiet(queryColumn.getAst().token);
            assert index > -1 : "wtf? " + queryColumn.getAst().token;
            columnCrossIndex.add(index);

            if (queryColumn.getAlias() == null) {
                selectMetadata.add(AbstractRecordMetadata.copyOf(metadata, index));
            } else {
                selectMetadata.add(
                        new TableColumnMetadata(
                                Chars.toString(queryColumn.getAlias()),
                                metadata.getColumnType(index),
                                metadata.isColumnIndexed(index),
                                metadata.getIndexValueBlockCapacity(index),
                                metadata.isSymbolTableStatic(index),
                                metadata.getMetadata(index)
                        )
                );
            }

            if (index == timestampIndex) {
                selectMetadata.setTimestampIndex(i);
                timestampSet = true;
            }
        }

        if (!timestampSet && executionContext.isTimestampRequired()) {
            selectMetadata.add(AbstractRecordMetadata.copyOf(metadata, timestampIndex));
            selectMetadata.setTimestampIndex(selectMetadata.getColumnCount() - 1);
            columnCrossIndex.add(timestampIndex);
        }

        return new SelectedRecordCursorFactory(selectMetadata, columnCrossIndex, factory);
    }

    private RecordCursorFactory generateSelectCursor(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        // sql parser ensures this type of model always has only one column
        return new RecordAsAFieldRecordCursorFactory(
                generate(model.getNestedModel(), executionContext),
                model.getColumns().getQuick(0).getAlias()
        );
    }

    private RecordCursorFactory generateSelectDistinct(QueryModel model, SqlExecutionContext executionContext) throws SqlException {

        QueryModel twoDeepNested;
        ExpressionNode tableNameEn;

        if (
                model.getColumns().size() == 1
                        && model.getNestedModel() != null
                        && model.getNestedModel().getSelectModelType() == QueryModel.SELECT_MODEL_CHOOSE
                        && (twoDeepNested = model.getNestedModel().getNestedModel()) != null
                        && twoDeepNested.getLatestBy().size() == 0
                        && (tableNameEn = twoDeepNested.getTableNameExpr()) != null
                        && twoDeepNested.getWhereClause() == null
        ) {
            CharSequence tableName = tableNameEn.token;
            TableToken tableToken = executionContext.getTableToken(tableName);
            try (TableReader reader = executionContext.getReader(tableToken)) {
                CharSequence columnName = model.getBottomUpColumnNames().get(0);
                TableReaderMetadata readerMetadata = reader.getMetadata();
                int columnIndex = readerMetadata.getColumnIndex(columnName);
                int columnType = readerMetadata.getColumnType(columnIndex);

                final GenericRecordMetadata distinctColumnMetadata = new GenericRecordMetadata();
                distinctColumnMetadata.add(AbstractRecordMetadata.copyOf(readerMetadata, columnIndex));
                if (ColumnType.isSymbol(columnType) || columnType == ColumnType.INT) {

                    final RecordCursorFactory factory = generateSubQuery(model.getNestedModel(), executionContext);

                    if (factory.supportPageFrameCursor()) {
                        try {
                            return new DistinctKeyRecordCursorFactory(
                                    engine.getConfiguration(),
                                    factory,
                                    distinctColumnMetadata,
                                    arrayColumnTypes,
                                    tempVaf,
                                    executionContext.getSharedWorkerCount(),
                                    tempSymbolSkewIndexes
                            );
                        } catch (Throwable t) {
                            Misc.free(factory);
                            throw t;
                        }
                    } else {
                        // Shouldn't really happen, we cannot recompile below, QueryModel is changed during compilation
                        Misc.free(factory);
                        throw CairoException.critical(0).put("Optimization error, incorrect path chosen, please contact support.");
                    }
                }
            }
        }

        final RecordCursorFactory factory = generateSubQuery(model, executionContext);
        try {
            if (factory.recordCursorSupportsRandomAccess() && factory.getMetadata().getTimestampIndex() != -1) {
                return new DistinctTimeSeriesRecordCursorFactory(
                        configuration,
                        factory,
                        entityColumnFilter,
                        asm
                );
            }
            return new DistinctRecordCursorFactory(
                    configuration,
                    factory,
                    entityColumnFilter,
                    asm
            );
        } catch (Throwable e) {
            factory.close();
            throw e;
        }
    }

    private RecordCursorFactory generateSelectGroupBy(QueryModel model, SqlExecutionContext executionContext) throws SqlException {

        // fail fast if we cannot create timestamp sampler

        final ExpressionNode sampleByNode = model.getSampleBy();
        if (sampleByNode != null) {
            return generateSampleBy(model, executionContext, sampleByNode, model.getSampleByUnit());
        }

        RecordCursorFactory factory = null;
        try {
            ObjList columns;
            ExpressionNode columnExpr;

            // generate special case plan for "select count() from somewhere"
            columns = model.getColumns();
            if (columns.size() == 1) {
                CharSequence columnName = columns.getQuick(0).getName();
                columnExpr = columns.getQuick(0).getAst();
                if (columnExpr.type == FUNCTION && columnExpr.paramCount == 0 && isCountKeyword(columnExpr.token)) {
                    // check if count() was not aliased, if it was, we need to generate new metadata, bummer
                    final RecordMetadata metadata = isCountKeyword(columnName) ? CountRecordCursorFactory.DEFAULT_COUNT_METADATA :
                            new GenericRecordMetadata().add(new TableColumnMetadata(Chars.toString(columnName), ColumnType.LONG));
                    return new CountRecordCursorFactory(metadata, generateSubQuery(model, executionContext));
                }
            }

            tempKeyIndexesInBase.clear();
            tempKeyIndex.clear();
            arrayColumnTypes.clear();
            tempKeyKinds.clear();

            boolean pageFramingSupported = false;
            boolean specialCaseKeys = false;

            // check for special case time function aggregations
            final QueryModel nested = model.getNestedModel();
            assert nested != null;
            // check if underlying model has reference to hour(column) function
            if (nested.getSelectModelType() == QueryModel.SELECT_MODEL_VIRTUAL
                    && (columnExpr = nested.getColumns().getQuick(0).getAst()).type == FUNCTION
                    && isHourKeyword(columnExpr.token)
                    && columnExpr.paramCount == 1
                    && columnExpr.rhs.type == LITERAL
            ) {
                specialCaseKeys = true;
                QueryModel.backupWhereClause(expressionNodePool, model);
                factory = generateSubQuery(nested, executionContext);
                pageFramingSupported = factory.supportPageFrameCursor();
                if (pageFramingSupported) {

                    // find position of the hour() argument in the factory meta
                    tempKeyIndexesInBase.add(factory.getMetadata().getColumnIndex(columnExpr.rhs.token));

                    // find position of hour() alias in selected columns
                    // also make sure there are no other literal column than our function reference
                    final CharSequence functionColumnName = columns.getQuick(0).getName();
                    for (int i = 0, n = columns.size(); i < n; i++) {
                        columnExpr = columns.getQuick(i).getAst();
                        if (columnExpr.type == LITERAL) {
                            if (Chars.equals(columnExpr.token, functionColumnName)) {
                                tempKeyIndex.add(i);
                                // storage dimension for Rosti is INT when we use hour(). This function produces INT.
                                tempKeyKinds.add(GKK_HOUR_INT);
                                arrayColumnTypes.add(ColumnType.INT);
                            } else {
                                // there is something else here, fallback to default implementation
                                pageFramingSupported = false;
                                break;
                            }
                        }
                    }
                } else {
                    factory = Misc.free(factory);
                }
            }

            if (factory == null) {
                if (specialCaseKeys) {
                    QueryModel.restoreWhereClause(expressionNodePool, model);
                }
                factory = generateSubQuery(model, executionContext);
                pageFramingSupported = factory.supportPageFrameCursor();
            }

            RecordMetadata metadata = factory.getMetadata();

            // Inspect model for possibility of vector aggregate intrinsics.
            if (pageFramingSupported && assembleKeysAndFunctionReferences(columns, metadata, !specialCaseKeys)) {
                // Create metadata from everything we've gathered.
                GenericRecordMetadata meta = new GenericRecordMetadata();

                // Start with keys.
                for (int i = 0, n = tempKeyIndex.size(); i < n; i++) {
                    final int indexInThis = tempKeyIndex.getQuick(i);
                    final int indexInBase = tempKeyIndexesInBase.getQuick(i);
                    final int type = arrayColumnTypes.getColumnType(i);

                    if (ColumnType.isSymbol(type)) {
                        meta.add(
                                indexInThis,
                                new TableColumnMetadata(
                                        Chars.toString(columns.getQuick(indexInThis).getName())
                                        , type
                                        , false
                                        , 0
                                        , metadata.isSymbolTableStatic(indexInBase),
                                        null
                                )
                        );
                    } else {
                        meta.add(
                                indexInThis,
                                new TableColumnMetadata(
                                        Chars.toString(columns.getQuick(indexInThis).getName()),
                                        type,
                                        null
                                )
                        );
                    }
                }

                // Add the aggregate functions.
                for (int i = 0, n = tempVecConstructors.size(); i < n; i++) {
                    VectorAggregateFunctionConstructor constructor = tempVecConstructors.getQuick(i);
                    int indexInBase = tempVecConstructorArgIndexes.getQuick(i);
                    int indexInThis = tempAggIndex.getQuick(i);
                    VectorAggregateFunction vaf = constructor.create(tempKeyKinds.size() == 0 ? 0 : tempKeyKinds.getQuick(0), indexInBase, executionContext.getSharedWorkerCount());
                    tempVaf.add(vaf);
                    meta.add(indexInThis,
                            new TableColumnMetadata(
                                    Chars.toString(columns.getQuick(indexInThis).getName()),
                                    vaf.getType(),
                                    null
                            )
                    );
                }

                if (tempKeyIndexesInBase.size() == 0) {
                    return new GroupByNotKeyedVectorRecordCursorFactory(
                            configuration,
                            factory,
                            meta,
                            tempVaf
                    );
                }

                if (tempKeyIndexesInBase.size() == 1) {
                    for (int i = 0, n = tempVaf.size(); i < n; i++) {
                        tempVaf.getQuick(i).pushValueTypes(arrayColumnTypes);
                    }

                    try {
                        GroupByUtils.validateGroupByColumns(model, 1);
                    } catch (Throwable e) {
                        Misc.freeObjList(tempVaf);
                        throw e;
                    }

                    return new GroupByRecordCursorFactory(
                            configuration,
                            factory,
                            meta,
                            arrayColumnTypes,
                            executionContext.getSharedWorkerCount(),
                            tempVaf,
                            tempKeyIndexesInBase.getQuick(0),
                            tempKeyIndex.getQuick(0),
                            tempSymbolSkewIndexes
                    );
                }

                // Free the vector aggregate functions since we didn't use them.
                Misc.freeObjList(tempVaf);
            }

            if (specialCaseKeys) {
                // uh-oh, we had special case keys, but could not find implementation for the functions
                // release factory we created unnecessarily
                factory = Misc.free(factory);
                // create factory on top level model
                QueryModel.restoreWhereClause(expressionNodePool, model);
                factory = generateSubQuery(model, executionContext);
                // and reset metadata
                metadata = factory.getMetadata();
            }

            final int timestampIndex = getTimestampIndex(model, factory);

            keyTypes.clear();
            valueTypes.clear();
            listColumnFilterA.clear();

            final int columnCount = model.getColumns().size();
            ObjList groupByFunctions = new ObjList<>(columnCount);
            try {
                GroupByUtils.prepareGroupByFunctions(
                        model,
                        metadata,
                        functionParser,
                        executionContext,
                        groupByFunctions,
                        groupByFunctionPositions,
                        valueTypes
                );
            } catch (Throwable e) {
                Misc.freeObjList(groupByFunctions);
                throw e;
            }

            final ObjList recordFunctions = new ObjList<>(columnCount);
            final GenericRecordMetadata groupByMetadata = new GenericRecordMetadata();
            try {
                GroupByUtils.prepareGroupByRecordFunctions(
                        model,
                        metadata,
                        listColumnFilterA,
                        groupByFunctions,
                        groupByFunctionPositions,
                        recordFunctions,
                        recordFunctionPositions,
                        groupByMetadata,
                        keyTypes,
                        valueTypes.getColumnCount(),
                        true,
                        timestampIndex
                );
            } catch (Throwable e) {
                Misc.freeObjList(recordFunctions);
                throw e;
            }

            if (keyTypes.getColumnCount() == 0) {
                return new GroupByNotKeyedRecordCursorFactory(
                        asm,
                        factory,
                        groupByMetadata,
                        groupByFunctions,
                        recordFunctions,
                        valueTypes.getColumnCount()
                );
            }

            return new io.questdb.griffin.engine.groupby.GroupByRecordCursorFactory(
                    asm,
                    configuration,
                    factory,
                    listColumnFilterA,
                    keyTypes,
                    valueTypes,
                    groupByMetadata,
                    groupByFunctions,
                    recordFunctions
            );

        } catch (Throwable e) {
            Misc.free(factory);
            throw e;
        }
    }

    private RecordCursorFactory generateSelectVirtual(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        final RecordCursorFactory factory = generateSubQuery(model, executionContext);
        return generateSelectVirtualWithSubQuery(model, executionContext, factory);
    }

    @NotNull
    private VirtualRecordCursorFactory generateSelectVirtualWithSubQuery(QueryModel model, SqlExecutionContext executionContext, RecordCursorFactory factory) throws SqlException {
        try {
            final ObjList columns = model.getColumns();
            final int columnCount = columns.size();
            final RecordMetadata metadata = factory.getMetadata();
            final ObjList functions = new ObjList<>(columnCount);
            final GenericRecordMetadata virtualMetadata = new GenericRecordMetadata();

            // attempt to preserve timestamp on new data set
            CharSequence timestampColumn;
            final int timestampIndex = metadata.getTimestampIndex();
            if (timestampIndex > -1) {
                timestampColumn = metadata.getColumnName(timestampIndex);
            } else {
                timestampColumn = null;
            }

            for (int i = 0; i < columnCount; i++) {
                final QueryColumn column = columns.getQuick(i);
                final ExpressionNode node = column.getAst();
                if (node.type == ExpressionNode.LITERAL && Chars.equalsNc(node.token, timestampColumn)) {
                    virtualMetadata.setTimestampIndex(i);
                }

                Function function = functionParser.parseFunction(
                        column.getAst(),
                        metadata,
                        executionContext
                );
                int targetColumnType = -1;
                if (model.isUpdate()) {
                    // Check the type of the column to be updated
                    int columnIndex = model.getUpdateTableColumnNames().indexOf(column.getAlias());
                    targetColumnType = model.getUpdateTableColumnTypes().get(columnIndex);
                }

                // define "undefined" functions as string unless it's update. Leave Undefined if update
                if (function.isUndefined()) {
                    if (!model.isUpdate()) {
                        function.assignType(ColumnType.STRING, executionContext.getBindVariableService());
                    } else {
                        // Set bind variable the type of the column
                        function.assignType(targetColumnType, executionContext.getBindVariableService());
                    }
                }

                int columnType = function.getType();
                if (targetColumnType != -1 && targetColumnType != columnType) {
                    // This is an update and the target column does not match with column the update is trying to perform
                    if (ColumnType.isBuiltInWideningCast(function.getType(), targetColumnType)) {
                        // All functions will be able to getLong() if they support getInt(), no need to generate cast here
                        columnType = targetColumnType;
                    } else {
                        Function castFunction = functionParser.createImplicitCast(column.getAst().position, function, targetColumnType);
                        if (castFunction != null) {
                            function = castFunction;
                            columnType = targetColumnType;
                        }
                        // else - update code will throw incompatibility exception. It will have better chance close resources then
                    }
                }

                functions.add(function);

                if (columnType == ColumnType.SYMBOL) {
                    if (function instanceof SymbolFunction) {
                        virtualMetadata.add(
                                new TableColumnMetadata(
                                        Chars.toString(column.getAlias()),
                                        function.getType(),
                                        false,
                                        0,
                                        ((SymbolFunction) function).isSymbolTableStatic(),
                                        function.getMetadata()
                                )
                        );
                    } else if (function instanceof NullConstant) {
                        virtualMetadata.add(
                                new TableColumnMetadata(
                                        Chars.toString(column.getAlias()),
                                        ColumnType.SYMBOL,
                                        false,
                                        0,
                                        false,
                                        function.getMetadata()
                                )
                        );
                        // Replace with symbol null constant
                        functions.setQuick(functions.size() - 1, SymbolConstant.NULL);
                    }
                } else {
                    virtualMetadata.add(
                            new TableColumnMetadata(
                                    Chars.toString(column.getAlias()),
                                    columnType,
                                    function.getMetadata()
                            )
                    );
                }
            }

            // if timestamp was required and present in the base model but
            // not selected, we will need to add it
            if (
                    executionContext.isTimestampRequired()
                            && timestampColumn != null
                            && virtualMetadata.getTimestampIndex() == -1
            ) {
                final Function timestampFunction = FunctionParser.createColumn(
                        0,
                        timestampColumn,
                        metadata
                );
                functions.add(timestampFunction);

                // here the base timestamp column name can name-clash with one of the
                // functions, so we have to use bottomUpColumns to lookup alias we should
                // be using. Bottom up column should have our timestamp because optimiser puts it there

                for (int i = 0, n = model.getBottomUpColumns().size(); i < n; i++) {
                    QueryColumn qc = model.getBottomUpColumns().getQuick(i);
                    if (qc.getAst().type == LITERAL && Chars.equals(timestampColumn, qc.getAst().token)) {
                        virtualMetadata.setTimestampIndex(virtualMetadata.getColumnCount());
                        virtualMetadata.add(
                                new TableColumnMetadata(
                                        Chars.toString(qc.getAlias()),
                                        timestampFunction.getType(),
                                        timestampFunction.getMetadata()
                                )
                        );
                        break;
                    }
                }
            }
            return new VirtualRecordCursorFactory(virtualMetadata, functions, factory);
        } catch (SqlException | CairoException e) {
            factory.close();
            throw e;
        }
    }

    /**
     * Generates chain of parent factories each of which takes only two argument factories.
     * Parent factory will perform one of SET operations on its arguments, such as UNION, UNION ALL,
     * INTERSECT or EXCEPT
     *
     * @param model            incoming model is expected to have a chain of models via its QueryModel.getUnionModel() function
     * @param factoryA         is compiled first argument
     * @param executionContext execution context for authorization and parallel execution purposes
     * @return factory that performs a SET operation
     * @throws SqlException when query contains syntax errors
     */
    private RecordCursorFactory generateSetFactory(
            QueryModel model,
            RecordCursorFactory factoryA,
            SqlExecutionContext executionContext
    ) throws SqlException {
        final RecordCursorFactory factoryB = generateQuery0(model.getUnionModel(), executionContext, true);
        ObjList castFunctionsA = null;
        ObjList castFunctionsB = null;
        try {
            final RecordMetadata metadataA = factoryA.getMetadata();
            final RecordMetadata metadataB = factoryB.getMetadata();
            final int positionA = model.getModelPosition();
            final int positionB = model.getUnionModel().getModelPosition();

            switch (model.getSetOperationType()) {
                case SET_OPERATION_UNION: {
                    final boolean castIsRequired = checkIfSetCastIsRequired(metadataA, metadataB, true);
                    final RecordMetadata setMetadata = castIsRequired ? widenSetMetadata(metadataA, metadataB) : GenericRecordMetadata.removeTimestamp(metadataA);
                    if (castIsRequired) {
                        castFunctionsA = generateCastFunctions(setMetadata, metadataA, positionA);
                        castFunctionsB = generateCastFunctions(setMetadata, metadataB, positionB);
                    }

                    return generateUnionFactory(
                            model,
                            executionContext,
                            factoryA,
                            factoryB,
                            castFunctionsA,
                            castFunctionsB,
                            setMetadata,
                            SET_UNION_CONSTRUCTOR
                    );
                }
                case SET_OPERATION_UNION_ALL: {
                    final boolean castIsRequired = checkIfSetCastIsRequired(metadataA, metadataB, true);
                    final RecordMetadata setMetadata = castIsRequired ? widenSetMetadata(metadataA, metadataB) : GenericRecordMetadata.removeTimestamp(metadataA);
                    if (castIsRequired) {
                        castFunctionsA = generateCastFunctions(setMetadata, metadataA, positionA);
                        castFunctionsB = generateCastFunctions(setMetadata, metadataB, positionB);
                    }

                    return generateUnionAllFactory(
                            model,
                            executionContext,
                            factoryA,
                            factoryB,
                            castFunctionsA,
                            castFunctionsB,
                            setMetadata
                    );
                }
                case SET_OPERATION_EXCEPT: {
                    final boolean castIsRequired = checkIfSetCastIsRequired(metadataA, metadataB, false);
                    final RecordMetadata setMetadata = castIsRequired ? widenSetMetadata(metadataA, metadataB) : metadataA;
                    if (castIsRequired) {
                        castFunctionsA = generateCastFunctions(setMetadata, metadataA, positionA);
                        castFunctionsB = generateCastFunctions(setMetadata, metadataB, positionB);
                    }

                    return generateUnionFactory(
                            model,
                            executionContext,
                            factoryA,
                            factoryB,
                            castFunctionsA,
                            castFunctionsB,
                            setMetadata,
                            SET_EXCEPT_CONSTRUCTOR
                    );
                }
                case SET_OPERATION_INTERSECT: {
                    final boolean castIsRequired = checkIfSetCastIsRequired(metadataA, metadataB, false);
                    final RecordMetadata setMetadata = castIsRequired ? widenSetMetadata(metadataA, metadataB) : metadataA;
                    if (castIsRequired) {
                        castFunctionsA = generateCastFunctions(setMetadata, metadataA, positionA);
                        castFunctionsB = generateCastFunctions(setMetadata, metadataB, positionB);
                    }

                    return generateUnionFactory(
                            model,
                            executionContext,
                            factoryA,
                            factoryB,
                            castFunctionsA,
                            castFunctionsB,
                            setMetadata,
                            SET_INTERSECT_CONSTRUCTOR
                    );
                }
                default:
                    assert false;
                    return null;
            }
        } catch (Throwable e) {
            Misc.free(factoryA);
            Misc.free(factoryB);
            Misc.freeObjList(castFunctionsA);
            Misc.freeObjList(castFunctionsB);
            throw e;
        }
    }

    private RecordCursorFactory generateSubQuery(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        assert model.getNestedModel() != null;
        return generateQuery(model.getNestedModel(), executionContext, true);
    }

    private RecordCursorFactory generateTableQuery(
            QueryModel model,
            SqlExecutionContext executionContext
    ) throws SqlException {
        final ObjList latestBy = model.getLatestBy();

        final GenericLexer.FloatingSequence tab = (GenericLexer.FloatingSequence) model.getTableName();
        final boolean supportsRandomAccess;
        if (Chars.startsWith(tab, NO_ROWID_MARKER)) {
            tab.setLo(tab.getLo() + NO_ROWID_MARKER.length());
            supportsRandomAccess = false;
        } else {
            supportsRandomAccess = true;
        }

        final TableToken tableToken = executionContext.getTableToken(tab);
        if (model.isUpdate() && !executionContext.isWalApplication()) {
            try (
                    TableReader reader = executionContext.getReader(tableToken);
                    TableRecordMetadata metadata = executionContext.getMetadata(tableToken, model.getTableVersion())
            ) {
                return generateTableQuery0(model, executionContext, latestBy, supportsRandomAccess, reader, metadata);
            }
        } else {
            try (TableReader reader = executionContext.getReader(
                    tableToken,
                    model.getTableVersion())) {
                return generateTableQuery0(model, executionContext, latestBy, supportsRandomAccess, reader, reader.getMetadata());
            }
        }
    }

    private RecordCursorFactory generateTableQuery0(QueryModel model, SqlExecutionContext executionContext, ObjList latestBy, boolean supportsRandomAccess, TableReader reader, TableRecordMetadata metadata) throws SqlException {
        // create metadata based on top-down columns that are required

        final ObjList topDownColumns = model.getTopDownColumns();
        final int topDownColumnCount = topDownColumns.size();
        final IntList columnIndexes = new IntList();
        final IntList columnSizes = new IntList();

        // topDownColumnCount can be 0 for 'select count()' queries

        int readerTimestampIndex;
        readerTimestampIndex = getTimestampIndex(model, metadata);

        // Latest by on a table requires the provided timestamp column to be the designated timestamp.
        if (latestBy.size() > 0 && readerTimestampIndex != metadata.getTimestampIndex()) {
            throw SqlException.$(model.getTimestamp().position, "latest by over a table requires designated TIMESTAMP");
        }

        boolean requiresTimestamp = joinsRequiringTimestamp[model.getJoinType()];
        final GenericRecordMetadata myMeta = new GenericRecordMetadata();
        boolean framingSupported;
        try {
            if (requiresTimestamp) {
                executionContext.pushTimestampRequiredFlag(true);
            }

            boolean contextTimestampRequired = executionContext.isTimestampRequired();
            // some "sample by" queries don't select any cols but needs timestamp col selected
            // for example "select count() from x sample by 1h" implicitly needs timestamp column selected
            if (topDownColumnCount > 0 || contextTimestampRequired || model.isUpdate()) {
                framingSupported = true;
                for (int i = 0; i < topDownColumnCount; i++) {
                    int columnIndex = metadata.getColumnIndexQuiet(topDownColumns.getQuick(i).getName());
                    int type = metadata.getColumnType(columnIndex);
                    int typeSize = ColumnType.sizeOf(type);

                    columnIndexes.add(columnIndex);
                    columnSizes.add(Numbers.msb(typeSize));

                    myMeta.add(new TableColumnMetadata(
                            Chars.toString(topDownColumns.getQuick(i).getName()),
                            type,
                            metadata.isColumnIndexed(columnIndex),
                            metadata.getIndexValueBlockCapacity(columnIndex),
                            metadata.isSymbolTableStatic(columnIndex),
                            metadata.getMetadata(columnIndex)
                    ));

                    if (columnIndex == readerTimestampIndex) {
                        myMeta.setTimestampIndex(myMeta.getColumnCount() - 1);
                    }
                }

                // select timestamp when it is required but not already selected
                if (readerTimestampIndex != -1 && myMeta.getTimestampIndex() == -1 && contextTimestampRequired) {
                    myMeta.add(new TableColumnMetadata(
                            metadata.getColumnName(readerTimestampIndex),
                            metadata.getColumnType(readerTimestampIndex),
                            metadata.getMetadata(readerTimestampIndex)
                    ));
                    myMeta.setTimestampIndex(myMeta.getColumnCount() - 1);

                    columnIndexes.add(readerTimestampIndex);
                    columnSizes.add((Numbers.msb(ColumnType.TIMESTAMP)));
                }
            } else {
                framingSupported = false;
            }
        } finally {
            if (requiresTimestamp) {
                executionContext.popTimestampRequiredFlag();
            }
        }

        GenericRecordMetadata dfcFactoryMeta = GenericRecordMetadata.deepCopyOf(reader.getMetadata());
        final int latestByColumnCount = prepareLatestByColumnIndexes(latestBy, myMeta);
        // Reader TableToken can have out of date table name in getLoggingName().
        // We need to resolve it from the engine to get correct value.
        final TableToken tableToken = reader.getTableToken();

        final ExpressionNode withinExtracted = whereClauseParser.extractWithin(
                model,
                model.getWhereClause(),
                metadata,
                functionParser,
                executionContext,
                prefixes
        );

        model.setWhereClause(withinExtracted);

        if (withinExtracted != null) {

            CharSequence preferredKeyColumn = null;

            if (latestByColumnCount == 1) {
                final int latestByIndex = listColumnFilterA.getColumnIndexFactored(0);

                if (ColumnType.isSymbol(myMeta.getColumnType(latestByIndex))) {
                    preferredKeyColumn = latestBy.getQuick(0).token;
                }
            }

            final IntrinsicModel intrinsicModel = whereClauseParser.extract(
                    model,
                    withinExtracted,
                    metadata,
                    preferredKeyColumn,
                    readerTimestampIndex,
                    functionParser,
                    myMeta,
                    executionContext,
                    latestByColumnCount > 1,
                    reader
            );

            // intrinsic parser can collapse where clause when removing parts it can replace
            // need to make sure that filter is updated on the model in case it is processed up the call stack
            //
            // At this juncture filter can use used up by one of the implementations below.
            // We will clear it preemptively. If nothing picks filter up we will set model "where"
            // to the downsized filter
            model.setWhereClause(null);

            if (intrinsicModel.intrinsicValue == IntrinsicModel.FALSE) {
                return new EmptyTableRecordCursorFactory(myMeta);
            }

            DataFrameCursorFactory dfcFactory;

            if (latestByColumnCount > 0) {
                Function f = compileFilter(intrinsicModel, myMeta, executionContext);
                if (f != null && f.isConstant() && !f.getBool(null)) {
                    // 'latest by' clause takes over the latest by nodes, so that the later generateLatestBy() is no-op
                    model.getLatestBy().clear();
                    Misc.free(f);
                    return new EmptyTableRecordCursorFactory(myMeta);
                }

                // a sub-query present in the filter may have used the latest by
                // column index lists, so we need to regenerate them
                prepareLatestByColumnIndexes(latestBy, myMeta);

                return generateLatestByTableQuery(
                        model,
                        reader,
                        myMeta,
                        tableToken,
                        intrinsicModel,
                        f,
                        executionContext,
                        readerTimestampIndex,
                        columnIndexes,
                        columnSizes,
                        prefixes
                );
            }

            // below code block generates index-based filter
            final boolean intervalHitsOnlyOnePartition;
            if (intrinsicModel.hasIntervalFilters()) {
                RuntimeIntrinsicIntervalModel intervalModel = intrinsicModel.buildIntervalModel();
                dfcFactory = new IntervalFwdDataFrameCursorFactory(tableToken, model.getTableId(), model.getTableVersion(), intervalModel, readerTimestampIndex, dfcFactoryMeta);
                intervalHitsOnlyOnePartition = intervalModel.allIntervalsHitOnePartition(reader.getPartitionedBy());
            } else {
                dfcFactory = new FullFwdDataFrameCursorFactory(tableToken, model.getTableId(), model.getTableVersion(), dfcFactoryMeta);
                intervalHitsOnlyOnePartition = false;
            }

            if (intrinsicModel.keyColumn != null) {
                // existence of column would have been already validated
                final int keyColumnIndex = metadata.getColumnIndexQuiet(intrinsicModel.keyColumn);
                final int nKeyValues = intrinsicModel.keyValueFuncs.size();
                final int nKeyExcludedValues = intrinsicModel.keyExcludedValueFuncs.size();

                if (intrinsicModel.keySubQuery != null) {
                    final RecordCursorFactory rcf = generate(intrinsicModel.keySubQuery, executionContext);
                    final Record.CharSequenceFunction func = validateSubQueryColumnAndGetGetter(intrinsicModel, rcf.getMetadata());

                    Function f = compileFilter(intrinsicModel, myMeta, executionContext);
                    if (f != null && f.isConstant() && !f.getBool(null)) {
                        Misc.free(dfcFactory);
                        return new EmptyTableRecordCursorFactory(myMeta);
                    }
                    return new FilterOnSubQueryRecordCursorFactory(
                            myMeta,
                            dfcFactory,
                            rcf,
                            keyColumnIndex,
                            f,
                            func,
                            columnIndexes
                    );
                }
                assert nKeyValues > 0 || nKeyExcludedValues > 0;

                boolean orderByKeyColumn = false;
                int indexDirection = BitmapIndexReader.DIR_FORWARD;
                if (intervalHitsOnlyOnePartition) {
                    final ObjList orderByAdvice = model.getOrderByAdvice();
                    final int orderByAdviceSize = orderByAdvice.size();
                    if (orderByAdviceSize > 0 && orderByAdviceSize < 3) {
                        // todo: when order by coincides with keyColumn and there is index we can incorporate
                        //    ordering in the code that returns rows from index rather than having an
                        //    "overhead" order by implementation, which would be trying to oder already ordered symbols
                        if (Chars.equals(orderByAdvice.getQuick(0).token, intrinsicModel.keyColumn)) {
                            myMeta.setTimestampIndex(-1);
                            if (orderByAdviceSize == 1) {
                                orderByKeyColumn = true;
                            } else if (Chars.equals(orderByAdvice.getQuick(1).token, model.getTimestamp().token)) {
                                orderByKeyColumn = true;
                                if (getOrderByDirectionOrDefault(model, 1) == QueryModel.ORDER_DIRECTION_DESCENDING) {
                                    indexDirection = BitmapIndexReader.DIR_BACKWARD;
                                }
                            }
                        }
                    }
                }

                if (intrinsicModel.keyExcludedValueFuncs.size() == 0) {
                    Function f = compileFilter(intrinsicModel, myMeta, executionContext);
                    if (f != null && f.isConstant()) {
                        try {
                            if (!f.getBool(null)) {
                                Misc.free(dfcFactory);
                                return new EmptyTableRecordCursorFactory(myMeta);
                            }
                        } finally {
                            f = Misc.free(f);
                        }
                    }
                    if (nKeyValues == 1) {
                        final RowCursorFactory rcf;
                        final Function symbolFunc = intrinsicModel.keyValueFuncs.get(0);
                        final SymbolMapReader symbolMapReader = reader.getSymbolMapReader(keyColumnIndex);
                        final int symbolKey = symbolFunc.isRuntimeConstant()
                                ? SymbolTable.VALUE_NOT_FOUND
                                : symbolMapReader.keyOf(symbolFunc.getStr(null));

                        if (symbolKey == SymbolTable.VALUE_NOT_FOUND) {
                            if (f == null) {
                                rcf = new DeferredSymbolIndexRowCursorFactory(keyColumnIndex,
                                        symbolFunc,
                                        true,
                                        indexDirection
                                );
                            } else {
                                rcf = new DeferredSymbolIndexFilteredRowCursorFactory(
                                        keyColumnIndex,
                                        symbolFunc,
                                        f,
                                        true,
                                        indexDirection,
                                        columnIndexes
                                );
                            }
                        } else {
                            if (f == null) {
                                rcf = new SymbolIndexRowCursorFactory(keyColumnIndex, symbolKey, true, indexDirection, null);
                            } else {
                                rcf = new SymbolIndexFilteredRowCursorFactory(keyColumnIndex, symbolKey, f, true, indexDirection, columnIndexes, null);
                            }
                        }

                        if (f == null) {
                            // This special case factory can later be disassembled to framing and index
                            // cursors in Sample By processing
                            return new DeferredSingleSymbolFilterDataFrameRecordCursorFactory(
                                    configuration,
                                    keyColumnIndex,
                                    symbolFunc,
                                    rcf,
                                    myMeta,
                                    dfcFactory,
                                    orderByKeyColumn,
                                    columnIndexes,
                                    columnSizes,
                                    supportsRandomAccess
                            );
                        }
                        return new DataFrameRecordCursorFactory(
                                configuration,
                                myMeta,
                                dfcFactory,
                                rcf,
                                orderByKeyColumn,
                                f,
                                false,
                                columnIndexes,
                                columnSizes,
                                supportsRandomAccess
                        );
                    }

                    if (orderByKeyColumn) {
                        myMeta.setTimestampIndex(-1);
                    }

                    return new FilterOnValuesRecordCursorFactory(
                            myMeta,
                            dfcFactory,
                            intrinsicModel.keyValueFuncs,
                            keyColumnIndex,
                            reader,
                            f,
                            model.getOrderByAdviceMnemonic(),
                            orderByKeyColumn,
                            getOrderByDirectionOrDefault(model, 0),
                            indexDirection,
                            columnIndexes
                    );

                } else if (
                        intrinsicModel.keyExcludedValueFuncs.size() > 0
                                && reader.getSymbolMapReader(keyColumnIndex).getSymbolCount() < configuration.getMaxSymbolNotEqualsCount()
                ) {
                    Function f = compileFilter(intrinsicModel, myMeta, executionContext);
                    if (f != null && f.isConstant()) {
                        try {
                            if (!f.getBool(null)) {
                                Misc.free(dfcFactory);
                                return new EmptyTableRecordCursorFactory(myMeta);
                            }
                        } finally {
                            f = Misc.free(f);
                        }
                    }

                    return new FilterOnExcludedValuesRecordCursorFactory(
                            myMeta,
                            dfcFactory,
                            intrinsicModel.keyExcludedValueFuncs,
                            keyColumnIndex,
                            f,
                            model.getOrderByAdviceMnemonic(),
                            orderByKeyColumn,
                            indexDirection,
                            columnIndexes,
                            configuration.getMaxSymbolNotEqualsCount()
                    );
                }
            }

            if (intervalHitsOnlyOnePartition && intrinsicModel.filter == null) {
                final ObjList orderByAdvice = model.getOrderByAdvice();
                final int orderByAdviceSize = orderByAdvice.size();
                if (orderByAdviceSize > 0 && orderByAdviceSize < 3 && intrinsicModel.hasIntervalFilters()) {
                    // we can only deal with 'order by symbol, timestamp' at best
                    // skip this optimisation if order by is more extensive
                    final int columnIndex = myMeta.getColumnIndexQuiet(model.getOrderByAdvice().getQuick(0).token);
                    assert columnIndex > -1;

                    // this is our kind of column
                    if (myMeta.isColumnIndexed(columnIndex)) {
                        boolean orderByKeyColumn = false;
                        int indexDirection = BitmapIndexReader.DIR_FORWARD;
                        if (orderByAdviceSize == 1) {
                            orderByKeyColumn = true;
                        } else if (Chars.equals(orderByAdvice.getQuick(1).token, model.getTimestamp().token)) {
                            orderByKeyColumn = true;
                            if (getOrderByDirectionOrDefault(model, 1) == QueryModel.ORDER_DIRECTION_DESCENDING) {
                                indexDirection = BitmapIndexReader.DIR_BACKWARD;
                            }
                        }

                        if (orderByKeyColumn) {
                            // check that intrinsicModel.intervals hit only one partition
                            myMeta.setTimestampIndex(-1);
                            return new SortedSymbolIndexRecordCursorFactory(
                                    myMeta,
                                    dfcFactory,
                                    columnIndex,
                                    getOrderByDirectionOrDefault(model, 0) == QueryModel.ORDER_DIRECTION_ASCENDING,
                                    indexDirection,
                                    columnIndexes
                            );
                        }
                    }
                }
            }

            boolean isOrderByTimestampDesc = isOrderDescendingByDesignatedTimestampOnly(model);
            RowCursorFactory rowFactory;

            if (isOrderByTimestampDesc && !intrinsicModel.hasIntervalFilters()) {
                Misc.free(dfcFactory);
                dfcFactory = new FullBwdDataFrameCursorFactory(tableToken, model.getTableId(), model.getTableVersion(), dfcFactoryMeta);
                rowFactory = new BwdDataFrameRowCursorFactory();
            } else {
                rowFactory = new DataFrameRowCursorFactory();
            }

            model.setWhereClause(intrinsicModel.filter);
            return new DataFrameRecordCursorFactory(
                    configuration,
                    myMeta,
                    dfcFactory,
                    rowFactory,
                    false,
                    null,
                    framingSupported,
                    columnIndexes,
                    columnSizes,
                    supportsRandomAccess
            );
        }

        // no where clause
        if (latestByColumnCount == 0) {
            // construct new metadata, which is a copy of what we constructed just above, but
            // in the interest of isolating problems we will only affect this factory

            AbstractDataFrameCursorFactory cursorFactory;
            RowCursorFactory rowCursorFactory;

            if (isOrderDescendingByDesignatedTimestampOnly(model)) {
                cursorFactory = new FullBwdDataFrameCursorFactory(tableToken, model.getTableId(), model.getTableVersion(), dfcFactoryMeta);
                rowCursorFactory = new BwdDataFrameRowCursorFactory();
            } else {
                cursorFactory = new FullFwdDataFrameCursorFactory(tableToken, model.getTableId(), model.getTableVersion(), dfcFactoryMeta);
                rowCursorFactory = new DataFrameRowCursorFactory();
            }

            return new DataFrameRecordCursorFactory(
                    configuration,
                    myMeta,
                    cursorFactory,
                    rowCursorFactory,
                    false,
                    null,
                    framingSupported,
                    columnIndexes,
                    columnSizes,
                    supportsRandomAccess
            );
        }

        // 'latest by' clause takes over the latest by nodes, so that the later generateLatestBy() is no-op
        model.getLatestBy().clear();

        // listColumnFilterA = latest by column indexes
        if (latestByColumnCount == 1) {
            int latestByColumnIndex = listColumnFilterA.getColumnIndexFactored(0);
            if (myMeta.isColumnIndexed(latestByColumnIndex)) {
                return new LatestByAllIndexedRecordCursorFactory(
                        myMeta,
                        configuration,
                        new FullBwdDataFrameCursorFactory(tableToken, model.getTableId(), model.getTableVersion(), dfcFactoryMeta),
                        listColumnFilterA.getColumnIndexFactored(0),
                        columnIndexes,
                        prefixes
                );
            }

            if (ColumnType.isSymbol(myMeta.getColumnType(latestByColumnIndex))
                    && myMeta.isSymbolTableStatic(latestByColumnIndex)) {
                // we have "latest by" symbol column values, but no index
                return new LatestByDeferredListValuesFilteredRecordCursorFactory(
                        configuration,
                        myMeta,
                        new FullBwdDataFrameCursorFactory(tableToken, model.getTableId(), model.getTableVersion(), dfcFactoryMeta),
                        latestByColumnIndex,
                        null,
                        columnIndexes
                );
            }
        }

        boolean symbolKeysOnly = true;
        for (int i = 0, n = keyTypes.getColumnCount(); i < n; i++) {
            symbolKeysOnly &= ColumnType.isSymbol(keyTypes.getColumnType(i));
        }
        if (symbolKeysOnly) {
            IntList partitionByColumnIndexes = new IntList(listColumnFilterA.size());
            for (int i = 0, n = listColumnFilterA.size(); i < n; i++) {
                partitionByColumnIndexes.add(listColumnFilterA.getColumnIndexFactored(i));
            }
            return new LatestByAllSymbolsFilteredRecordCursorFactory(
                    myMeta,
                    configuration,
                    new FullBwdDataFrameCursorFactory(tableToken, model.getTableId(), model.getTableVersion(), dfcFactoryMeta),
                    RecordSinkFactory.getInstance(asm, myMeta, listColumnFilterA, false),
                    keyTypes,
                    partitionByColumnIndexes,
                    null,
                    null,
                    columnIndexes
            );
        }

        return new LatestByAllFilteredRecordCursorFactory(
                myMeta,
                configuration,
                new FullBwdDataFrameCursorFactory(tableToken, model.getTableId(), model.getTableVersion(), dfcFactoryMeta),
                RecordSinkFactory.getInstance(asm, myMeta, listColumnFilterA, false),
                keyTypes,
                null,
                columnIndexes
        );
    }

    private RecordCursorFactory generateUnionAllFactory(
            QueryModel model,
            SqlExecutionContext executionContext,
            RecordCursorFactory factoryA,
            RecordCursorFactory factoryB,
            ObjList castFunctionsA,
            ObjList castFunctionsB,
            RecordMetadata setMetadata
    ) throws SqlException {
        final RecordCursorFactory setFactory = new UnionAllRecordCursorFactory(
                setMetadata,
                factoryA,
                factoryB,
                castFunctionsA,
                castFunctionsB
        );

        if (model.getUnionModel().getUnionModel() != null) {
            return generateSetFactory(model.getUnionModel(), setFactory, executionContext);
        }
        return setFactory;
    }

    private RecordCursorFactory generateUnionFactory(
            QueryModel model,
            SqlExecutionContext executionContext,
            RecordCursorFactory factoryA,
            RecordCursorFactory factoryB,
            ObjList castFunctionsA,
            ObjList castFunctionsB,
            RecordMetadata setMetadata,
            SetRecordCursorFactoryConstructor constructor
    ) throws SqlException {
        entityColumnFilter.of(factoryA.getMetadata().getColumnCount());
        final RecordSink recordSink = RecordSinkFactory.getInstance(
                asm,
                setMetadata,
                entityColumnFilter,
                true
        );
        valueTypes.clear();
        // Remap symbol columns to string type since that's how recordSink copies them.
        keyTypes.clear();
        for (int i = 0, n = setMetadata.getColumnCount(); i < n; i++) {
            final int columnType = setMetadata.getColumnType(i);
            if (ColumnType.isSymbol(columnType)) {
                keyTypes.add(ColumnType.STRING);
            } else {
                keyTypes.add(columnType);
            }
        }
        RecordCursorFactory unionFactory = constructor.create(
                configuration,
                setMetadata,
                factoryA,
                factoryB,
                castFunctionsA,
                castFunctionsB,
                recordSink,
                keyTypes,
                valueTypes
        );

        if (model.getUnionModel().getUnionModel() != null) {
            return generateSetFactory(model.getUnionModel(), unionFactory, executionContext);
        }
        return unionFactory;
    }

    @Nullable
    private Function getHiFunction(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        return toLimitFunction(executionContext, model.getLimitHi(), null);
    }

    @Nullable
    private Function getLimitLoFunctionOnly(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        if (model.getLimitAdviceLo() != null && model.getLimitAdviceHi() == null) {
            return toLimitFunction(executionContext, model.getLimitAdviceLo(), LongConstant.ZERO);
        }
        return null;
    }

    @NotNull
    private Function getLoFunction(QueryModel model, SqlExecutionContext executionContext) throws SqlException {
        return toLimitFunction(executionContext, model.getLimitLo(), LongConstant.ZERO);
    }

    private int getTimestampIndex(QueryModel model, RecordCursorFactory factory) throws SqlException {
        return getTimestampIndex(model, factory.getMetadata());
    }

    private int getTimestampIndex(QueryModel model, RecordMetadata metadata) throws SqlException {
        final ExpressionNode timestamp = model.getTimestamp();
        if (timestamp != null) {
            int timestampIndex = metadata.getColumnIndexQuiet(timestamp.token);
            if (timestampIndex == -1) {
                throw SqlException.invalidColumn(timestamp.position, timestamp.token);
            }
            if (!ColumnType.isTimestamp(metadata.getColumnType(timestampIndex))) {
                throw SqlException.$(timestamp.position, "not a TIMESTAMP");
            }
            return timestampIndex;
        }
        return metadata.getTimestampIndex();
    }

    private boolean isOrderDescendingByDesignatedTimestampOnly(QueryModel model) {
        return model.getOrderByAdvice().size() == 1 && model.getTimestamp() != null &&
                Chars.equalsIgnoreCase(model.getOrderByAdvice().getQuick(0).token, model.getTimestamp().token) &&
                getOrderByDirectionOrDefault(model, 0) == ORDER_DIRECTION_DESCENDING;
    }

    private boolean isSingleColumnFunction(ExpressionNode ast, CharSequence name) {
        return ast.type == FUNCTION && ast.paramCount == 1 && Chars.equalsIgnoreCase(ast.token, name) && ast.rhs.type == LITERAL;
    }

    private void lookupColumnIndexes(
            ListColumnFilter filter,
            ObjList columnNames,
            RecordMetadata metadata
    ) throws SqlException {
        filter.clear();
        for (int i = 0, n = columnNames.size(); i < n; i++) {
            final CharSequence columnName = columnNames.getQuick(i).token;
            int columnIndex = metadata.getColumnIndexQuiet(columnName);
            if (columnIndex > -1) {
                filter.add(columnIndex + 1);
            } else {
                int dot = Chars.indexOf(columnName, '.');
                if (dot > -1) {
                    columnIndex = metadata.getColumnIndexQuiet(columnName, dot + 1, columnName.length());
                    if (columnIndex > -1) {
                        filter.add(columnIndex + 1);
                        return;
                    }
                }
                throw SqlException.invalidColumn(columnNames.getQuick(i).position, columnName);
            }
        }
    }

    private void lookupColumnIndexesUsingVanillaNames(
            ListColumnFilter filter,
            ObjList columnNames,
            RecordMetadata metadata
    ) {
        filter.clear();
        for (int i = 0, n = columnNames.size(); i < n; i++) {
            filter.add(metadata.getColumnIndex(columnNames.getQuick(i)) + 1);
        }
    }

    private int prepareLatestByColumnIndexes(ObjList latestBy, RecordMetadata myMeta) throws SqlException {
        keyTypes.clear();
        listColumnFilterA.clear();

        final int latestByColumnCount = latestBy.size();
        if (latestByColumnCount > 0) {
            // validate the latest by against the current reader
            // first check if column is valid
            for (int i = 0; i < latestByColumnCount; i++) {
                final ExpressionNode latestByNode = latestBy.getQuick(i);
                final int index = myMeta.getColumnIndexQuiet(latestByNode.token);
                if (index == -1) {
                    throw SqlException.invalidColumn(latestByNode.position, latestByNode.token);
                }

                // check the type of the column, not all are supported
                int columnType = myMeta.getColumnType(index);
                switch (ColumnType.tagOf(columnType)) {
                    case ColumnType.BOOLEAN:
                    case ColumnType.CHAR:
                    case ColumnType.SHORT:
                    case ColumnType.INT:
                    case ColumnType.LONG:
                    case ColumnType.LONG256:
                    case ColumnType.STRING:
                    case ColumnType.SYMBOL:
                    case ColumnType.UUID:
                    case ColumnType.LONG128:
                        // we are reusing collections which leads to confusing naming for this method
                        // keyTypes are types of columns we collect 'latest by' for
                        keyTypes.add(columnType);
                        // listColumnFilterA are indexes of columns we collect 'latest by' for
                        listColumnFilterA.add(index + 1);
                        break;

                    default:
                        throw SqlException
                                .position(latestByNode.position)
                                .put(latestByNode.token)
                                .put(" (")
                                .put(ColumnType.nameOf(columnType))
                                .put("): invalid type, only [BOOLEAN, SHORT, INT, LONG, LONG128, LONG256, CHAR, STRING, SYMBOL, UUID] are supported in LATEST BY");
                }
            }
        }
        return latestByColumnCount;
    }

    private void processJoinContext(
            boolean vanillaMaster,
            JoinContext jc,
            RecordMetadata masterMetadata,
            RecordMetadata slaveMetadata
    ) throws SqlException {
        lookupColumnIndexesUsingVanillaNames(listColumnFilterA, jc.aNames, slaveMetadata);
        if (vanillaMaster) {
            lookupColumnIndexesUsingVanillaNames(listColumnFilterB, jc.bNames, masterMetadata);
        } else {
            lookupColumnIndexes(listColumnFilterB, jc.bNodes, masterMetadata);
        }

        // compare types and populate keyTypes
        keyTypes.clear();
        for (int k = 0, m = listColumnFilterA.getColumnCount(); k < m; k++) {
            // Don't use tagOf(columnType) to compare the types.
            // Key types have too much exactly except SYMBOL and STRING special case
            int columnTypeA = slaveMetadata.getColumnType(listColumnFilterA.getColumnIndexFactored(k));
            int columnTypeB = masterMetadata.getColumnType(listColumnFilterB.getColumnIndexFactored(k));
            if (columnTypeB != columnTypeA && !(ColumnType.isSymbolOrString(columnTypeB) && ColumnType.isSymbolOrString(columnTypeA))) {
                // index in column filter and join context is the same
                throw SqlException.$(jc.aNodes.getQuick(k).position, "join column type mismatch");
            }
            keyTypes.add(columnTypeB == ColumnType.SYMBOL ? ColumnType.STRING : columnTypeB);
        }
    }

    private Function toLimitFunction(
            SqlExecutionContext executionContext,
            ExpressionNode limit,
            ConstantFunction defaultValue
    ) throws SqlException {
        if (limit == null) {
            return defaultValue;
        }

        final Function func = functionParser.parseFunction(limit, EmptyRecordMetadata.INSTANCE, executionContext);
        final int type = func.getType();
        if (limitTypes.excludes(type)) {
            if (type == ColumnType.UNDEFINED) {
                if (func instanceof IndexedParameterLinkFunction) {
                    executionContext.getBindVariableService().setLong(((IndexedParameterLinkFunction) func).getVariableIndex(), defaultValue.getLong(null));
                    return func;
                }

                if (func instanceof NamedParameterLinkFunction) {
                    executionContext.getBindVariableService().setLong(((NamedParameterLinkFunction) func).getVariableName(), defaultValue.getLong(null));
                    return func;
                }
            }
            throw SqlException.$(limit.position, "invalid type: ").put(ColumnType.nameOf(type));
        }
        return func;
    }

    private IntList toOrderIndices(RecordMetadata m, ObjList orderBy, IntList orderByDirection) throws SqlException {
        final IntList indices = intListPool.next();
        for (int i = 0, n = orderBy.size(); i < n; i++) {
            ExpressionNode tok = orderBy.getQuick(i);
            int index = m.getColumnIndexQuiet(tok.token);
            if (index == -1) {
                throw SqlException.invalidColumn(tok.position, tok.token);
            }

            // shift index by 1 to use sign as sort direction
            index++;

            // negative column index means descending order of sort
            if (orderByDirection.getQuick(i) == QueryModel.ORDER_DIRECTION_DESCENDING) {
                index = -index;
            }

            indices.add(index);
        }
        return indices;
    }

    private void validateBothTimestampOrders(RecordCursorFactory masterFactory, RecordCursorFactory slaveFactory, int position) throws SqlException {
        if (masterFactory.hasDescendingOrder()) {
            throw SqlException.$(position, "left side of time series join has DESC timestamp order");
        }

        if (slaveFactory.hasDescendingOrder()) {
            throw SqlException.$(position, "right side of time series join has DESC timestamp order");
        }
    }

    private void validateBothTimestamps(QueryModel slaveModel, RecordMetadata masterMetadata, RecordMetadata slaveMetadata) throws SqlException {
        if (masterMetadata.getTimestampIndex() == -1) {
            throw SqlException.$(slaveModel.getJoinKeywordPosition(), "left side of time series join has no timestamp");
        }

        if (slaveMetadata.getTimestampIndex() == -1) {
            throw SqlException.$(slaveModel.getJoinKeywordPosition(), "right side of time series join has no timestamp");
        }
    }

    private void validateOuterJoinExpressions(QueryModel model, CharSequence joinType) throws SqlException {
        if (model.getOuterJoinExpressionClause() != null) {
            throw SqlException.$(model.getOuterJoinExpressionClause().position, "unsupported ").put(joinType).put(" join expression ")
                    .put("[expr='").put(model.getOuterJoinExpressionClause()).put("']");
        }
    }

    private Record.CharSequenceFunction validateSubQueryColumnAndGetGetter(IntrinsicModel intrinsicModel, RecordMetadata metadata) throws SqlException {
        int columnType = metadata.getColumnType(0);
        if (!ColumnType.isSymbolOrString(columnType)) {
            assert intrinsicModel.keySubQuery.getColumns() != null;
            assert intrinsicModel.keySubQuery.getColumns().size() > 0;

            throw SqlException
                    .position(intrinsicModel.keySubQuery.getColumns().getQuick(0).getAst().position)
                    .put("unsupported column type: ")
                    .put(metadata.getColumnName(0))
                    .put(": ")
                    .put(ColumnType.nameOf(columnType));
        }

        return ColumnType.isString(columnType) ? Record.GET_STR : Record.GET_SYM;
    }

    private RecordMetadata widenSetMetadata(RecordMetadata typesA, RecordMetadata typesB) {
        int columnCount = typesA.getColumnCount();
        assert columnCount == typesB.getColumnCount();

        GenericRecordMetadata metadata = new GenericRecordMetadata();
        for (int i = 0; i < columnCount; i++) {
            int typeA = typesA.getColumnType(i);
            int typeB = typesB.getColumnType(i);

            if (typeA == typeB && typeA != ColumnType.SYMBOL) {
                metadata.add(AbstractRecordMetadata.copyOf(typesA, i));
            } else if (ColumnType.isToSameOrWider(typeB, typeA) && typeA != ColumnType.SYMBOL && typeA != ColumnType.CHAR) {
                // CHAR is "specially" assignable from SHORT, but we don't want that
                metadata.add(AbstractRecordMetadata.copyOf(typesA, i));
            } else if (ColumnType.isToSameOrWider(typeA, typeB) && typeB != ColumnType.SYMBOL) {
                // even though A is assignable to B (e.g. A union B)
                // set metadata will use A column names
                metadata.add(new TableColumnMetadata(
                        typesA.getColumnName(i),
                        typeB
                ));
            } else {
                // we can cast anything to string
                metadata.add(new TableColumnMetadata(
                        typesA.getColumnName(i),
                        ColumnType.STRING
                ));
            }
        }

        return metadata;
    }

    // used in tests
    void setEnableJitNullChecks(boolean value) {
        enableJitNullChecks = value;
    }

    void setFullFatJoins(boolean fullFatJoins) {
        this.fullFatJoins = fullFatJoins;
    }

    @FunctionalInterface
    public interface FullFatJoinGenerator {
        RecordCursorFactory create(
                CairoConfiguration configuration,
                RecordMetadata metadata,
                RecordCursorFactory masterFactory,
                RecordCursorFactory slaveFactory,
                @Transient ColumnTypes mapKeyTypes,
                @Transient ColumnTypes mapValueTypes,
                @Transient ColumnTypes slaveColumnTypes,
                RecordSink masterKeySink,
                RecordSink slaveKeySink,
                int columnSplit,
                RecordValueSink slaveValueSink,
                IntList columnIndex,
                JoinContext joinContext
        );
    }

    private static class RecordCursorFactoryStub implements RecordCursorFactory {
        final ExecutionModel model;
        RecordCursorFactory factory;

        protected RecordCursorFactoryStub(ExecutionModel model, RecordCursorFactory factory) {
            this.model = model;
            this.factory = factory;
        }

        @Override
        public void close() {
            factory = Misc.free(factory);
        }

        @Override
        public RecordCursor getCursor(SqlExecutionContext executionContext) throws SqlException {
            if (factory != null) {
                return factory.getCursor(executionContext);
            } else {
                return null;
            }
        }

        @Override
        public RecordMetadata getMetadata() {
            return null;
        }

        @Override
        public boolean recordCursorSupportsRandomAccess() {
            return false;
        }

        @Override
        public void toPlan(PlanSink sink) {
            sink.type(model.getTypeName());

            CharSequence tableName = model.getTableName();
            if (tableName != null) {
                sink.meta("table").val(tableName);
            }
            if (factory != null) {
                sink.child(factory);
            }
        }
    }

    static {
        joinsRequiringTimestamp[JOIN_INNER] = false;
        joinsRequiringTimestamp[JOIN_OUTER] = false;
        joinsRequiringTimestamp[JOIN_CROSS] = false;
        joinsRequiringTimestamp[JOIN_ASOF] = true;
        joinsRequiringTimestamp[JOIN_SPLICE] = true;
        joinsRequiringTimestamp[JOIN_LT] = true;
        joinsRequiringTimestamp[JOIN_ONE] = false;
    }

    static {
        limitTypes.add(ColumnType.LONG);
        limitTypes.add(ColumnType.BYTE);
        limitTypes.add(ColumnType.SHORT);
        limitTypes.add(ColumnType.INT);
    }

    static {
        limitTypes.add(ColumnType.LONG);
        limitTypes.add(ColumnType.BYTE);
        limitTypes.add(ColumnType.SHORT);
        limitTypes.add(ColumnType.INT);
    }

    static {
        countConstructors.put(ColumnType.DOUBLE, CountDoubleVectorAggregateFunction::new);
        countConstructors.put(ColumnType.INT, CountIntVectorAggregateFunction::new);
        countConstructors.put(ColumnType.LONG, CountLongVectorAggregateFunction::new);
        countConstructors.put(ColumnType.DATE, CountLongVectorAggregateFunction::new);
        countConstructors.put(ColumnType.TIMESTAMP, CountLongVectorAggregateFunction::new);

        sumConstructors.put(ColumnType.DOUBLE, SumDoubleVectorAggregateFunction::new);
        sumConstructors.put(ColumnType.INT, SumIntVectorAggregateFunction::new);
        sumConstructors.put(ColumnType.LONG, SumLongVectorAggregateFunction::new);
        sumConstructors.put(ColumnType.LONG256, SumLong256VectorAggregateFunction::new);
        sumConstructors.put(ColumnType.DATE, SumDateVectorAggregateFunction::new);
        sumConstructors.put(ColumnType.TIMESTAMP, SumTimestampVectorAggregateFunction::new);

        ksumConstructors.put(ColumnType.DOUBLE, KSumDoubleVectorAggregateFunction::new);
        nsumConstructors.put(ColumnType.DOUBLE, NSumDoubleVectorAggregateFunction::new);

        avgConstructors.put(ColumnType.DOUBLE, AvgDoubleVectorAggregateFunction::new);
        avgConstructors.put(ColumnType.LONG, AvgLongVectorAggregateFunction::new);
        avgConstructors.put(ColumnType.TIMESTAMP, AvgLongVectorAggregateFunction::new);
        avgConstructors.put(ColumnType.DATE, AvgLongVectorAggregateFunction::new);
        avgConstructors.put(ColumnType.INT, AvgIntVectorAggregateFunction::new);

        minConstructors.put(ColumnType.DOUBLE, MinDoubleVectorAggregateFunction::new);
        minConstructors.put(ColumnType.LONG, MinLongVectorAggregateFunction::new);
        minConstructors.put(ColumnType.DATE, MinDateVectorAggregateFunction::new);
        minConstructors.put(ColumnType.TIMESTAMP, MinTimestampVectorAggregateFunction::new);
        minConstructors.put(ColumnType.INT, MinIntVectorAggregateFunction::new);

        maxConstructors.put(ColumnType.DOUBLE, MaxDoubleVectorAggregateFunction::new);
        maxConstructors.put(ColumnType.LONG, MaxLongVectorAggregateFunction::new);
        maxConstructors.put(ColumnType.DATE, MaxDateVectorAggregateFunction::new);
        maxConstructors.put(ColumnType.TIMESTAMP, MaxTimestampVectorAggregateFunction::new);
        maxConstructors.put(ColumnType.INT, MaxIntVectorAggregateFunction::new);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy