
io.questdb.griffin.engine.groupby.GroupByFunctionsUpdaterFactory Maven / Gradle / Ivy
Show all versions of questdb Show documentation
/*******************************************************************************
* ___ _ ____ ____
* / _ \ _ _ ___ ___| |_| _ \| __ )
* | | | | | | |/ _ \/ __| __| | | | _ \
* | |_| | |_| | __/\__ \ |_| |_| | |_) |
* \__\_\\__,_|\___||___/\__|____/|____/
*
* Copyright (c) 2014-2019 Appsicle
* Copyright (c) 2019-2024 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.engine.groupby;
import io.questdb.griffin.engine.functions.GroupByFunction;
import io.questdb.log.Log;
import io.questdb.log.LogFactory;
import io.questdb.std.BytecodeAssembler;
import io.questdb.std.ObjList;
import io.questdb.std.ex.BytecodeException;
import org.jetbrains.annotations.NotNull;
public class GroupByFunctionsUpdaterFactory {
private static final int FIELD_POOL_OFFSET = 3;
private static final Log LOG = LogFactory.getLog(GroupByFunctionsUpdaterFactory.class);
private GroupByFunctionsUpdaterFactory() {
}
/**
* Creates a GroupByFunctionUpdater instance capturing the provided group by functions.
* The generated class will have fields GroupByFunction f0, GroupByFunction f1, GroupByFunction f2 ... GroupByFunction fn
* for each group by function from the provided list.
*
* The generated class will have the following methods:
*
* - updateNew(MapValue value, Record record, long rowId) - calls f0, f1, f2 ... fn.computeFirst(value, record, rowId) for each group by function
* - updateExisting(MapValue value, Record record, long rowId) - calls f0, f1, f2 ... fn.computeNext(value, record, rowId) for each group by function
* - updateEmpty(MapValue value) - calls f0, f1, f2 ... fn.setEmpty(value) for each group by function
* - merge(MapValue destValue, MapValue srcValue) - calls fn.merge(destValue, srcValue) for each group by function
* - setFunctions(ObjList<GroupByFunction> groupByFunctions) - sets the group by functions to the fields. This method is called by the factory and should not be called by the caller.
*
*
* @param asm BytecodeAssembler instance
* @param groupByFunctions list of group by functions
* @return GroupByFunctionUpdater instance
*/
public static GroupByFunctionsUpdater getInstance(
BytecodeAssembler asm,
@NotNull ObjList groupByFunctions
) {
final Class clazz = getInstanceClass(asm, groupByFunctions.size());
return getInstance(clazz, groupByFunctions);
}
/**
* Creates an instance of a record sink class previously generated via the
* {@link #getInstanceClass(BytecodeAssembler, int)} method.
*/
public static GroupByFunctionsUpdater getInstance(
Class clazz,
@NotNull ObjList groupByFunctions
) {
try {
final GroupByFunctionsUpdater updater = clazz.getDeclaredConstructor().newInstance();
updater.setFunctions(groupByFunctions);
return updater;
} catch (Exception e) {
LOG.critical().$("could not create an instance of GroupByFunctionsUpdater, cause: ").$(e).$();
throw BytecodeException.INSTANCE;
}
}
/**
* Same as the {@link #getInstance(BytecodeAssembler, ObjList)} method, but returns the generated class instead
* of its instance. An instance can be later created via the {@link #getInstance(Class, ObjList)} method.
*
* Used when creating per-worker updaters for parallel GROUP BY.
*/
public static Class getInstanceClass(BytecodeAssembler asm, int functionCount) {
asm.init(GroupByFunctionsUpdater.class);
asm.setupPool();
final int thisClassIndex = asm.poolClass(asm.poolUtf8("io/questdb/griffin/engine/groupby/GroupByFunctionsUpdaterAsm"));
final int superclassIndex = asm.poolClass(Object.class);
int interfaceClassIndex = asm.poolClass(GroupByFunctionsUpdater.class);
final int superIndex = asm.poolMethod(superclassIndex, "", "()V");
final int typeIndex = asm.poolUtf8("Lio/questdb/griffin/engine/functions/GroupByFunction;");
int firstFieldNameIndex = 0;
int firstFieldIndex = 0;
for (int i = 0; i < functionCount; i++) {
// if you change pool calls then you will likely need to change the FIELD_POOL_OFFSET constant
int fieldNameIndex = asm.poolUtf8().putAscii("f").put(i).$();
int nameAndType = asm.poolNameAndType(fieldNameIndex, typeIndex);
int fieldIndex = asm.poolField(thisClassIndex, nameAndType);
if (i == 0) {
firstFieldNameIndex = fieldNameIndex;
firstFieldIndex = fieldIndex;
}
}
final int computeFirstIndex = asm.poolInterfaceMethod(GroupByFunction.class, "computeFirst", "(Lio/questdb/cairo/map/MapValue;Lio/questdb/cairo/sql/Record;J)V");
final int computeNextIndex = asm.poolInterfaceMethod(GroupByFunction.class, "computeNext", "(Lio/questdb/cairo/map/MapValue;Lio/questdb/cairo/sql/Record;J)V");
final int setEmptyIndex = asm.poolInterfaceMethod(GroupByFunction.class, "setEmpty", "(Lio/questdb/cairo/map/MapValue;)V");
final int mergeFunctionIndex = asm.poolInterfaceMethod(GroupByFunction.class, "merge", "(Lio/questdb/cairo/map/MapValue;Lio/questdb/cairo/map/MapValue;)V");
final int updateNewIndex = asm.poolUtf8("updateNew");
final int updateNewSigIndex = asm.poolUtf8("(Lio/questdb/cairo/map/MapValue;Lio/questdb/cairo/sql/Record;J)V");
final int updateExistingIndex = asm.poolUtf8("updateExisting");
final int updateExistingSigIndex = asm.poolUtf8("(Lio/questdb/cairo/map/MapValue;Lio/questdb/cairo/sql/Record;J)V");
final int updateEmptyIndex = asm.poolUtf8("updateEmpty");
final int updateEmptySigIndex = asm.poolUtf8("(Lio/questdb/cairo/map/MapValue;)V");
final int setFunctionsIndex = asm.poolUtf8("setFunctions");
final int setFunctionsSigIndex = asm.poolUtf8("(Lio/questdb/std/ObjList;)V");
final int mergeIndex = asm.poolUtf8("merge");
final int mergeSigIndex = asm.poolUtf8("(Lio/questdb/cairo/map/MapValue;Lio/questdb/cairo/map/MapValue;)V");
final int getIndex = asm.poolMethod(ObjList.class, "get", "(I)Ljava/lang/Object;");
asm.finishPool();
asm.defineClass(thisClassIndex, superclassIndex);
asm.interfaceCount(1);
asm.putShort(interfaceClassIndex);
asm.fieldCount(functionCount);
for (int i = 0; i < functionCount; i++) {
asm.defineField(firstFieldNameIndex + (i * FIELD_POOL_OFFSET), typeIndex);
}
asm.methodCount(6);
asm.defineDefaultConstructor(superIndex);
generateUpdateNew(asm, functionCount, firstFieldIndex, computeFirstIndex, updateNewIndex, updateNewSigIndex);
generateUpdateExisting(asm, functionCount, firstFieldIndex, computeNextIndex, updateExistingIndex, updateExistingSigIndex);
generateUpdateEmpty(asm, functionCount, firstFieldIndex, setEmptyIndex, updateEmptyIndex, updateEmptySigIndex);
generateSetFunctions(asm, functionCount, firstFieldIndex, setFunctionsIndex, setFunctionsSigIndex, getIndex);
generateMerge(asm, functionCount, firstFieldIndex, mergeFunctionIndex, mergeIndex, mergeSigIndex);
// class attribute count
asm.putShort(0);
return asm.loadClass();
}
private static void generateMerge(
BytecodeAssembler asm,
int fieldCount,
int firstFieldIndex,
int mergeFunctionIndex,
int mergeIndex,
int mergeSigIndex
) {
asm.startMethod(mergeIndex, mergeSigIndex, 3, 3);
for (int i = 0; i < fieldCount; i++) {
asm.aload(0);
asm.getfield(firstFieldIndex + (i * FIELD_POOL_OFFSET));
asm.aload(1); // destValue
asm.aload(2); // srcValue
asm.invokeInterface(mergeFunctionIndex, 2);
}
asm.return_();
asm.endMethodCode();
// exceptions
asm.putShort(0);
// attributes
asm.putShort(0);
asm.endMethod();
}
private static void generateSetFunctions(BytecodeAssembler asm, int functionSize, int firstFieldIndex, int setFunctionsIndex, int setFunctionsSigIndex, int getIndex) {
asm.startMethod(setFunctionsIndex, setFunctionsSigIndex, 3, 3);
for (int i = 0; i < functionSize; i++) {
asm.aload(0);
asm.aload(1);
asm.iconst(i);
asm.invokeVirtual(getIndex);
asm.putfield(firstFieldIndex + (i * FIELD_POOL_OFFSET));
}
asm.return_();
asm.endMethodCode();
// exceptions
asm.putShort(0);
// attributes
asm.putShort(0);
asm.endMethod();
}
private static void generateUpdateEmpty(
BytecodeAssembler asm,
int fieldCount,
int firstFieldIndex,
int setEmptyIndex,
int updateNameIndex,
int updateSigIndex
) {
asm.startMethod(updateNameIndex, updateSigIndex, 3, 3);
for (int i = 0; i < fieldCount; i++) {
asm.aload(0);
asm.getfield(firstFieldIndex + (i * FIELD_POOL_OFFSET));
asm.aload(1);
asm.invokeInterface(setEmptyIndex, 1);
}
asm.return_();
asm.endMethodCode();
// exceptions
asm.putShort(0);
// attributes
asm.putShort(0);
asm.endMethod();
}
private static void generateUpdateExisting(
BytecodeAssembler asm,
int fieldCount,
int firstFieldIndex,
int computeNextIndex,
int updateNameIndex,
int updateSigIndex
) {
asm.startMethod(updateNameIndex, updateSigIndex, 5, 5);
for (int i = 0; i < fieldCount; i++) {
asm.aload(0);
asm.getfield(firstFieldIndex + (i * FIELD_POOL_OFFSET));
asm.aload(1); // map value
asm.aload(2); // record
asm.lload(3); // row id
asm.invokeInterface(computeNextIndex, 4);
}
asm.return_();
asm.endMethodCode();
// exceptions
asm.putShort(0);
// attributes
asm.putShort(0);
asm.endMethod();
}
/**
* Here we simply unroll the function list and call computeNext on each of them.
* The end bytecode equals to the following:
*
* f0.computeFist(value, record);
* f1.computeFirst(value, record);
* // ...
* fn.computeFirst(value, record);
*
*
* Other generated methods look similar.
*/
private static void generateUpdateNew(
BytecodeAssembler asm,
int fieldCount,
int firstFieldIndex,
int computeFirstIndex,
int updateNameIndex,
int updateSigIndex
) {
asm.startMethod(updateNameIndex, updateSigIndex, 5, 5);
for (int i = 0; i < fieldCount; i++) {
asm.aload(0);
asm.getfield(firstFieldIndex + (i * FIELD_POOL_OFFSET));
asm.aload(1); // map value
asm.aload(2); // record
asm.lload(3); // row id
asm.invokeInterface(computeFirstIndex, 4);
}
asm.return_();
asm.endMethodCode();
// exceptions
asm.putShort(0);
// attributes
asm.putShort(0);
asm.endMethod();
}
}